橋杯國(guó)賽單片機(jī)系統(tǒng)設(shè)計(jì):從模塊驅(qū)動(dòng)到多任務(wù)整合的實(shí)戰(zhàn)解析)
1. 項(xiàng)目概述從“功能解析”到“解題思維”的跨越拿到“藍(lán)橋杯單片機(jī)第十屆國(guó)賽 部分功能解析”這個(gè)標(biāo)題很多參加過(guò)或正在備賽的同學(xué)第一反應(yīng)可能是這又是一篇講某個(gè)具體模塊比如ADC、PWM、I2C怎么調(diào)通的代碼分析。但如果你真這么想可能就錯(cuò)過(guò)了國(guó)賽題目最核心的價(jià)值。我參加過(guò)多次藍(lán)橋杯的評(píng)審和指導(dǎo)工作深知國(guó)賽級(jí)別的題目其難點(diǎn)從來(lái)不是某個(gè)孤立的芯片驅(qū)動(dòng)或函數(shù)編寫而在于如何在有限的資源時(shí)間、單片機(jī)性能、外設(shè)和復(fù)雜的多任務(wù)需求下進(jìn)行系統(tǒng)性的設(shè)計(jì)與整合。第十屆國(guó)賽的題目正是這種綜合能力的集中體現(xiàn)。簡(jiǎn)單來(lái)說(shuō)它考察的不僅僅是“你會(huì)不會(huì)用某個(gè)外設(shè)”更是“你能否在多個(gè)外設(shè)同時(shí)工作、相互干擾的復(fù)雜場(chǎng)景下讓整個(gè)系統(tǒng)穩(wěn)定、高效、準(zhǔn)確地運(yùn)行”。這就像讓你一個(gè)人同時(shí)操作多個(gè)儀器完成一個(gè)實(shí)驗(yàn)不僅要懂每個(gè)儀器的操作還要懂得統(tǒng)籌安排避免操作沖突。因此這篇解析不會(huì)停留在“這里調(diào)個(gè)延時(shí)那里改個(gè)參數(shù)”的層面而是試圖帶你復(fù)盤當(dāng)時(shí)的解題思路、架構(gòu)設(shè)計(jì)以及那些容易踩坑的細(xì)節(jié)。無(wú)論你是即將參賽的選手還是希望提升單片機(jī)系統(tǒng)設(shè)計(jì)能力的開發(fā)者相信這些從實(shí)戰(zhàn)中沉淀下來(lái)的經(jīng)驗(yàn)都比單純的代碼更有價(jià)值。2. 賽題核心需求與系統(tǒng)架構(gòu)拆解要解析功能必須先理解題目到底要求我們構(gòu)建一個(gè)什么樣的系統(tǒng)。第十屆國(guó)賽題目通常是一個(gè)模擬現(xiàn)實(shí)場(chǎng)景的綜合性項(xiàng)目例如“智能測(cè)量裝置”、“多功能控制終端”等。它不會(huì)直接說(shuō)“請(qǐng)你用ADC測(cè)電壓”而是會(huì)描述一個(gè)場(chǎng)景其中隱含了多個(gè)需要實(shí)現(xiàn)的功能點(diǎn)。2.1 典型國(guó)賽題目特征分析根據(jù)歷屆賽題規(guī)律第十屆國(guó)賽題目很可能具備以下特征這些特征直接決定了我們的設(shè)計(jì)思路外設(shè)集成度高題目通常會(huì)指定使用競(jìng)賽板上的核心外設(shè)如LCD12864液晶屏、矩陣鍵盤、EEPROMAT24C02、ADCPCF8591、溫度傳感器DS18B20、NE555頻率測(cè)量、超聲波測(cè)距模塊等。國(guó)賽題目往往會(huì)要求同時(shí)使用其中5-6個(gè)甚至更多。多任務(wù)并行系統(tǒng)需要同時(shí)處理多個(gè)任務(wù)。例如需要實(shí)時(shí)刷新液晶屏顯示包含多個(gè)頁(yè)面或復(fù)雜界面同時(shí)不間斷地采集傳感器數(shù)據(jù)溫度、電壓還要響應(yīng)鍵盤輸入進(jìn)行模式切換或參數(shù)設(shè)置并且可能要求將關(guān)鍵數(shù)據(jù)定時(shí)存儲(chǔ)到EEPROM中。狀態(tài)機(jī)驅(qū)動(dòng)由于任務(wù)繁多一個(gè)簡(jiǎn)單粗暴的while(1)超級(jí)循環(huán)里堆滿所有函數(shù)調(diào)用必然會(huì)導(dǎo)致系統(tǒng)響應(yīng)遲鈍、顯示卡頓。因此引入基于時(shí)間片或事件驅(qū)動(dòng)的狀態(tài)機(jī)是必須的架構(gòu)選擇。精度與穩(wěn)定性要求國(guó)賽題目會(huì)對(duì)測(cè)量精度、控制響應(yīng)速度有明確要求。例如溫度測(cè)量精度要求到0.1℃電壓測(cè)量誤差小于1%這就對(duì)ADC的校準(zhǔn)、軟件濾波算法提出了挑戰(zhàn)。人機(jī)交互復(fù)雜不再是簡(jiǎn)單的幾個(gè)獨(dú)立按鍵。矩陣鍵盤需要實(shí)現(xiàn)長(zhǎng)短按、連按、組合鍵等功能液晶屏需要設(shè)計(jì)多級(jí)菜單包括數(shù)值設(shè)置、狀態(tài)查看、歷史記錄查詢等界面。2.2 系統(tǒng)頂層設(shè)計(jì)思路面對(duì)這樣的需求一個(gè)穩(wěn)健的系統(tǒng)架構(gòu)是成功的一半。我的設(shè)計(jì)思路通常遵循以下原則分層與模塊化將硬件驅(qū)動(dòng)層如LCD驅(qū)動(dòng)、鍵盤掃描、業(yè)務(wù)邏輯層如數(shù)據(jù)處理、狀態(tài)判斷、應(yīng)用層如主狀態(tài)機(jī)、菜單管理嚴(yán)格分離。每個(gè)模塊.c和.h文件只負(fù)責(zé)一項(xiàng)明確的功能通過(guò)清晰的接口進(jìn)行通信。時(shí)間片輪詢法這是在沒(méi)有RTOS的情況下實(shí)現(xiàn)多任務(wù)“偽并行”最有效的方法。利用一個(gè)定時(shí)器如Timer0產(chǎn)生固定的時(shí)間基準(zhǔn)例如1ms或5ms在這個(gè)中斷服務(wù)程序中設(shè)置一系列的標(biāo)志位flag。在主循環(huán)中通過(guò)檢查這些標(biāo)志位來(lái)執(zhí)行對(duì)應(yīng)的任務(wù)函數(shù)。// 示例定時(shí)器中斷中設(shè)置標(biāo)志位 void Timer0_ISR() interrupt 1 { static unsigned int tCount 0; TH0 ...; TL0 ...; // 重裝初值 tCount; if(tCount % 2 0) flag_2ms 1; // 2ms任務(wù)標(biāo)志 if(tCount % 10 0) flag_10ms 1; // 10ms任務(wù)標(biāo)志用于鍵盤掃描 if(tCount % 50 0) flag_50ms 1; // 50ms任務(wù)標(biāo)志用于傳感器采集 if(tCount % 200 0) flag_200ms 1; // 200ms任務(wù)標(biāo)志用于顯示刷新 if(tCount 1000) tCount 0; }狀態(tài)機(jī)清晰明確將整個(gè)系統(tǒng)的工作流程劃分為幾個(gè)明確的狀態(tài)如IDLE待機(jī)、MEASURING測(cè)量、SETTING參數(shù)設(shè)置、VIEW_LOG查看記錄等。任何鍵盤事件或定時(shí)事件都作為狀態(tài)遷移的觸發(fā)條件。注意很多新手會(huì)忽視架構(gòu)設(shè)計(jì)直接開始寫代碼導(dǎo)致后期添加功能時(shí)牽一發(fā)而動(dòng)全身調(diào)試極其困難。在動(dòng)筆寫第一行驅(qū)動(dòng)代碼之前花10分鐘在紙上畫一下系統(tǒng)模塊圖、狀態(tài)轉(zhuǎn)換圖和任務(wù)時(shí)間分配能節(jié)省后面數(shù)小時(shí)的調(diào)試時(shí)間。3. 關(guān)鍵外設(shè)功能深度解析與避坑指南接下來(lái)我們針對(duì)國(guó)賽中最常考、也最容易出問(wèn)題的幾個(gè)外設(shè)模塊進(jìn)行深度解析。這里不僅講“怎么做”更重點(diǎn)講“為什么這么做”以及“哪里容易出錯(cuò)”。3.1 矩陣鍵盤掃描與高級(jí)功能實(shí)現(xiàn)國(guó)賽的鍵盤輸入絕非簡(jiǎn)單的getKey()返回一個(gè)鍵值。它要求實(shí)現(xiàn)短按、長(zhǎng)按、連按甚至組合鍵功能。核心掃描邏輯采用“行列反轉(zhuǎn)法”或“逐行掃描法”配合軟件消抖是基礎(chǔ)。關(guān)鍵在于掃描周期10-20ms為宜和狀態(tài)記錄。高級(jí)功能實(shí)現(xiàn)思路長(zhǎng)短按判斷為每個(gè)按鍵設(shè)置一個(gè)“按下計(jì)時(shí)器”。在鍵盤掃描任務(wù)10ms一次中如果檢測(cè)到按鍵按下其計(jì)時(shí)器累加。當(dāng)計(jì)時(shí)器達(dá)到“長(zhǎng)按閾值”如計(jì)數(shù)值50對(duì)應(yīng)500ms時(shí)觸發(fā)長(zhǎng)按事件并重置計(jì)時(shí)器以避免重復(fù)觸發(fā)。在按鍵釋放時(shí)檢查計(jì)時(shí)器如果小于長(zhǎng)按閾值但大于消抖時(shí)間如計(jì)數(shù)值2對(duì)應(yīng)20ms則觸發(fā)短按事件。連按功能在判斷為長(zhǎng)按觸發(fā)后不重置計(jì)時(shí)器而是讓其繼續(xù)累加。每當(dāng)計(jì)時(shí)器超過(guò)一個(gè)“連按間隔”如計(jì)數(shù)值再增加20對(duì)應(yīng)200ms時(shí)就觸發(fā)一次連按事件。這可以實(shí)現(xiàn)按住按鍵持續(xù)增加或減少數(shù)值的效果。狀態(tài)機(jī)模型每個(gè)按鍵可以看作一個(gè)獨(dú)立的狀態(tài)機(jī)狀態(tài)包括KEY_IDLE空閑、KEY_DEBOUNCE消抖中、KEY_PRESSED已按下、KEY_LONG_PRESSED長(zhǎng)按已觸發(fā)、KEY_REPEAT連按中、KEY_RELEASE釋放。// 按鍵數(shù)據(jù)結(jié)構(gòu)示例 typedef struct { unsigned char id; // 鍵值 unsigned char state; // 當(dāng)前狀態(tài) unsigned int holdTime; // 按下保持時(shí)間以掃描周期為單位 unsigned int repeatCnt; // 連按計(jì)數(shù) } Key_T; // 在10ms任務(wù)中調(diào)用此函數(shù)處理所有按鍵 void Key_Process(void) { unsigned char keyVal Key_Scan(); // 獲取當(dāng)前物理鍵值 for(int i0; iKEY_NUM; i) { switch(key[i].state) { case KEY_IDLE: if(keyVal key[i].id) key[i].state KEY_DEBOUNCE; break; case KEY_DEBOUNCE: if(keyVal key[i].id) { key[i].holdTime; if(key[i].holdTime DEBOUNCE_TICKS) { key[i].state KEY_PRESSED; key[i].holdTime 0; // 觸發(fā)短按按下事件可設(shè)置標(biāo)志位 } } else { key[i].state KEY_IDLE; key[i].holdTime 0; } break; case KEY_PRESSED: // ... 長(zhǎng)按和連按判斷邏輯 break; // ... 其他狀態(tài)處理 } } }避坑指南中斷沖突切勿在外部中斷或定時(shí)器中斷服務(wù)程序中進(jìn)行復(fù)雜的鍵盤掃描或延時(shí)消抖這會(huì)阻塞系統(tǒng)。鍵盤掃描必須放在主循環(huán)或低優(yōu)先級(jí)定時(shí)任務(wù)中。資源占用上述狀態(tài)機(jī)方法雖然清晰但會(huì)占用較多RAM。如果資源緊張可以采用更緊湊的“位域”或“查表法”來(lái)記錄按鍵狀態(tài)。連加速度連按的間隔時(shí)間要設(shè)置合理太快用戶難以控制太慢影響操作效率。通常首次連按間隔稍長(zhǎng)如500ms后續(xù)連按間隔縮短如200ms。3.2 LCD12864顯示與多級(jí)菜單設(shè)計(jì)LCD12864是國(guó)賽的“門面”顯示內(nèi)容復(fù)雜且需要頻繁刷新。其難點(diǎn)在于快速、無(wú)閃爍地刷新局部?jī)?nèi)容和管理多級(jí)菜單。高效刷新策略差異化刷新不要每次刷新都重寫整個(gè)屏幕。為屏幕上每個(gè)需要?jiǎng)討B(tài)更新的區(qū)域如數(shù)值顯示區(qū)、狀態(tài)圖標(biāo)區(qū)設(shè)置一個(gè)“顯示緩存數(shù)組”和一個(gè)“更新標(biāo)志位”。當(dāng)數(shù)據(jù)變化時(shí)只修改緩存數(shù)組并置位對(duì)應(yīng)標(biāo)志位。在顯示刷新任務(wù)如200ms一次中檢查所有標(biāo)志位只向LCD寫入那些發(fā)生了變化的區(qū)域內(nèi)容。使用標(biāo)準(zhǔn)字庫(kù)與自定義字庫(kù)國(guó)賽板子通常自帶字庫(kù)芯片調(diào)用方便。但對(duì)于特殊符號(hào)或小圖標(biāo)需要自己定義字模。將常用的自定義字模數(shù)組放在代碼的const區(qū)域并規(guī)劃好其在CGRAM中的地址避免沖突。多級(jí)菜單狀態(tài)機(jī)設(shè)計(jì) 菜單系統(tǒng)本質(zhì)是一個(gè)樹狀狀態(tài)機(jī)。每個(gè)菜單頁(yè)是一個(gè)狀態(tài)節(jié)點(diǎn)。定義菜單結(jié)構(gòu)體typedef struct { unsigned char id; // 菜單ID unsigned char parentId; // 父菜單ID void (*showFunc)(void); // 該菜單的顯示函數(shù) void (*keyFunc)(unsigned char key); // 該菜單下的按鍵處理函數(shù) } MenuItem_T;構(gòu)建菜單表用一個(gè)MenuItem_T數(shù)組定義所有菜單頁(yè)及其關(guān)系。全局變量記錄當(dāng)前菜單使用一個(gè)全局變量currentMenuId記錄當(dāng)前所處的菜單頁(yè)。顯示與按鍵分發(fā)主循環(huán)中根據(jù)currentMenuId查找菜單表調(diào)用對(duì)應(yīng)的showFunc進(jìn)行顯示。鍵盤事件也傳遞給當(dāng)前菜單的keyFunc處理在該函數(shù)內(nèi)實(shí)現(xiàn)“上下翻頁(yè)”、“進(jìn)入子菜單”、“返回上級(jí)菜單”、“修改參數(shù)”等邏輯。避坑指南顯示閃爍頻繁全屏刷新是閃爍的元兇。務(wù)必采用差異化刷新。另外在寫入大量數(shù)據(jù)到LCD時(shí)可以考慮暫時(shí)關(guān)閉全局中斷防止被定時(shí)器中斷打斷導(dǎo)致數(shù)據(jù)傳輸錯(cuò)位但關(guān)閉中斷的時(shí)間要盡可能短。內(nèi)存不足菜單結(jié)構(gòu)體、字模、顯示緩存都會(huì)消耗RAM。如果使用大量菜單可以考慮將菜單的顯示字符串定義在code程序存儲(chǔ)區(qū)僅在使用時(shí)讀取。菜單邏輯混亂清晰定義“進(jìn)入”、“退出”、“確認(rèn)”、“取消”等按鍵的全局語(yǔ)義并在每個(gè)菜單的按鍵處理函數(shù)中嚴(yán)格遵守避免用戶操作時(shí)迷失。3.3 高精度數(shù)據(jù)采集與軟件濾波題目對(duì)測(cè)量精度的要求迫使我們必須認(rèn)真對(duì)待ADC采集和信號(hào)處理。以PCF85918位ADC測(cè)量電壓為例其理論分辨率約為20mV5V/256要滿足1%的精度50mV必須進(jìn)行校準(zhǔn)和濾波。校準(zhǔn)方法零點(diǎn)校準(zhǔn)輸入端接地0V讀取此時(shí)的ADC值A(chǔ)D_ZERO。這個(gè)值就是零點(diǎn)的系統(tǒng)誤差。增益校準(zhǔn)輸入端接一個(gè)已知的精確參考電壓如2.500V讀取ADC值A(chǔ)D_REF。理論上(AD_REF - AD_ZERO)應(yīng)該對(duì)應(yīng)2.500V。換算公式實(shí)際電壓 (AD_RAW - AD_ZERO) * V_REF / (AD_REF - AD_ZERO)。其中AD_RAW是原始讀數(shù)V_REF是參考電壓值2.500V。軟件濾波算法 單純的一次采樣毫無(wú)意義必須濾波。算術(shù)平均濾波連續(xù)采樣N次求和再平均。最簡(jiǎn)單能有效抑制隨機(jī)噪聲但會(huì)導(dǎo)致響應(yīng)速度變慢。N取值一般為4、8、16。滑動(dòng)平均濾波維護(hù)一個(gè)長(zhǎng)度為N的隊(duì)列每次新采樣值入隊(duì)最舊值出隊(duì)然后計(jì)算隊(duì)列平均值。兼顧了實(shí)時(shí)性和平滑度是比賽中最實(shí)用、最常用的方法。中位值平均濾波連續(xù)采樣N次去掉一個(gè)最大值和一個(gè)最小值對(duì)剩下的N-2個(gè)值求平均。能有效防止脈沖干擾。一階滯后濾波低通濾波Y(n) α * X(n) (1-α) * Y(n-1)。其中α是濾波系數(shù)0α1X(n)是新采樣值Y(n)是本次濾波輸出Y(n-1)是上次輸出。這種方法計(jì)算量小能很好地平滑數(shù)據(jù)特別適合變化緩慢的物理量如溫度。α越小濾波效果越強(qiáng)但滯后也越嚴(yán)重。// 滑動(dòng)平均濾波示例 #define FILTER_LEN 8 unsigned char filterBuf[FILTER_LEN] {0}; unsigned char filterIndex 0; unsigned int filterSum 0; unsigned char Moving_Average_Filter(unsigned char newVal) { filterSum filterSum - filterBuf[filterIndex] newVal; // 減去最舊值加上最新值 filterBuf[filterIndex] newVal; // 新值存入隊(duì)列 filterIndex (filterIndex 1) % FILTER_LEN; // 索引循環(huán) return (unsigned char)(filterSum / FILTER_LEN); // 返回平均值 }避坑指南采樣時(shí)機(jī)不要在程序任意位置隨機(jī)調(diào)用ADC讀取函數(shù)。應(yīng)在一個(gè)固定的、周期性的定時(shí)任務(wù)中如50ms任務(wù)進(jìn)行采樣和濾波以保證數(shù)據(jù)的時(shí)間一致性。濾波參數(shù)選擇濾波深度N值或α值需要根據(jù)信號(hào)特性和系統(tǒng)響應(yīng)要求做權(quán)衡??梢酝ㄟ^(guò)串口將原始數(shù)據(jù)和濾波后數(shù)據(jù)打印出來(lái)直觀地觀察效果。切忌盲目使用深度濾波否則控制環(huán)路會(huì)變得遲緩。數(shù)據(jù)類型與溢出濾波計(jì)算中涉及累加務(wù)必使用足夠大的數(shù)據(jù)類型如unsigned int或unsigned long并警惕累加和溢出的風(fēng)險(xiǎn)。3.4 EEPROM數(shù)據(jù)存儲(chǔ)與磨損均衡AT24C02這類EEPROM常用于存儲(chǔ)系統(tǒng)參數(shù)、校準(zhǔn)值或歷史記錄。其操作簡(jiǎn)單但有兩個(gè)關(guān)鍵陷阱。頁(yè)寫入與跨頁(yè)問(wèn)題AT24C02的頁(yè)大小為8字節(jié)。連續(xù)寫入時(shí)如果寫入起始地址數(shù)據(jù)長(zhǎng)度超過(guò)當(dāng)前頁(yè)的邊界超出的部分會(huì)從該頁(yè)的起始地址“回卷”覆蓋寫入而不是自動(dòng)寫入下一頁(yè)。這會(huì)導(dǎo)致數(shù)據(jù)丟失。解決方案在寫入長(zhǎng)度超過(guò)一頁(yè)或可能跨頁(yè)的數(shù)據(jù)時(shí)必須分多次寫入每次寫入前計(jì)算剩余空間確保單次寫入不跨頁(yè)。讀寫壽命與數(shù)據(jù)安全EEPROM的擦寫次數(shù)有限通常10萬(wàn)次。如果頻繁地在同一個(gè)地址更新數(shù)據(jù)如每秒存儲(chǔ)一次實(shí)時(shí)數(shù)據(jù)芯片很快就會(huì)損壞。解決方案簡(jiǎn)易磨損均衡。例如要存儲(chǔ)10組歷史記錄不要只固定10個(gè)地址。可以定義一個(gè)更大的存儲(chǔ)區(qū)如30個(gè)地址每次寫入時(shí)順序向后寫寫滿后再?gòu)念^開始循環(huán)覆蓋。同時(shí)在存儲(chǔ)區(qū)頭部增加一個(gè)“索引”或“時(shí)間戳”用來(lái)標(biāo)識(shí)最新數(shù)據(jù)的位置。這樣就將擦寫次數(shù)分?jǐn)偟搅硕鄠€(gè)物理單元上。#define HISTORY_START_ADDR 0x00 #define HISTORY_SIZE 30 // 總共30個(gè)記錄單元 #define RECORD_LEN 2 // 每個(gè)記錄占2字節(jié) unsigned char historyIndex 0; // 存儲(chǔ)在RAM中上電后需要從EEPROM恢復(fù) void Save_History(unsigned int data) { // 1. 計(jì)算寫入地址 unsigned int writeAddr HISTORY_START_ADDR historyIndex * RECORD_LEN; // 2. 寫入數(shù)據(jù) EEPROM_Write(writeAddr, (data 8) 0xFF); // 高字節(jié) EEPROM_Write(writeAddr1, data 0xFF); // 低字節(jié) // 3. 更新索引并保存索引本身也占用一個(gè)固定地址需謹(jǐn)慎處理其磨損 historyIndex (historyIndex 1) % HISTORY_SIZE; EEPROM_Write(INDEX_STORE_ADDR, historyIndex); }4. 系統(tǒng)整合與調(diào)試實(shí)戰(zhàn)經(jīng)驗(yàn)當(dāng)各個(gè)模塊都能獨(dú)立工作后最艱巨的挑戰(zhàn)來(lái)了——將它們整合成一個(gè)穩(wěn)定運(yùn)行的系統(tǒng)。這個(gè)階段的問(wèn)題往往是隱性和耦合性的。4.1 定時(shí)器資源分配與中斷管理51單片機(jī)通常有2-3個(gè)定時(shí)器。必須精心規(guī)劃Timer0通常用作系統(tǒng)時(shí)基產(chǎn)生1ms或5ms的精確中斷用于更新軟件計(jì)時(shí)、設(shè)置任務(wù)標(biāo)志位。這是系統(tǒng)的“心跳”。Timer1可能用于產(chǎn)生波特率如果使用串口調(diào)試或者用于需要精確計(jì)時(shí)/測(cè)頻的功能如測(cè)量NE555輸出頻率。Timer2如果可用可能用于PWM生成或其他特定功能。關(guān)鍵原則中斷服務(wù)程序ISR務(wù)求短小精悍。ISR里只做最必要的事情設(shè)置標(biāo)志位、清除中斷標(biāo)志、重裝初值。絕對(duì)不要在ISR里進(jìn)行復(fù)雜計(jì)算、調(diào)用可能阻塞的函數(shù)如LCD大量寫數(shù)據(jù)、或進(jìn)行軟件延時(shí)。避免中斷嵌套如果非必要在系統(tǒng)初始化后關(guān)閉中斷嵌套51單片機(jī)默認(rèn)是關(guān)閉的。復(fù)雜的嵌套中斷會(huì)使得程序執(zhí)行流難以預(yù)測(cè)給調(diào)試帶來(lái)噩夢(mèng)。共享變量的保護(hù)如果主循環(huán)和中斷服務(wù)程序都會(huì)讀寫同一個(gè)全局變量如flag_10ms雖然51單片機(jī)大部分情況下讀寫下一條指令是原子的但為了代碼規(guī)范和可移植性建議在讀寫這類變量時(shí)暫時(shí)關(guān)閉中斷操作完成后再打開。4.2 全局狀態(tài)機(jī)與任務(wù)調(diào)度實(shí)現(xiàn)這是整個(gè)系統(tǒng)的“大腦”。一個(gè)清晰的主循環(huán)和任務(wù)調(diào)度機(jī)制至關(guān)重要。void main(void) { Sys_Init(); // 系統(tǒng)初始化定時(shí)器、中斷、外設(shè)、全局變量 while(1) { // 任務(wù)調(diào)度根據(jù)定時(shí)器中斷設(shè)置的標(biāo)志位執(zhí)行相應(yīng)任務(wù) if(flag_2ms) { flag_2ms 0; Task_2ms(); } // 高優(yōu)先級(jí)任務(wù)如某些控制算法 if(flag_10ms) { flag_10ms 0; Task_10ms(); } // 鍵盤掃描 if(flag_50ms) { flag_50ms 0; Task_50ms(); } // 傳感器數(shù)據(jù)采集與濾波 if(flag_200ms) { flag_200ms 0; Task_200ms(); } // 顯示刷新 if(flag_1000ms){ flag_1000ms 0;Task_1s(); } // 秒級(jí)任務(wù)如存儲(chǔ)數(shù)據(jù) // 主狀態(tài)機(jī)處理 switch(sysState) { case STATE_IDLE: // ... 處理空閑狀態(tài)邏輯 break; case STATE_MEASURE: // ... 處理測(cè)量狀態(tài)邏輯可能調(diào)用各個(gè)任務(wù)處理后的數(shù)據(jù) break; case STATE_SETTING: // ... 處理設(shè)置狀態(tài)邏輯 break; } // 其他非實(shí)時(shí)性任務(wù)如串口數(shù)據(jù)處理如果有 Uart_Process(); } }任務(wù)函數(shù)設(shè)計(jì)要點(diǎn)每個(gè)任務(wù)函數(shù)如Task_50ms()應(yīng)功能單一執(zhí)行時(shí)間盡可能短。如果某個(gè)任務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng)會(huì)影響其他標(biāo)志位的及時(shí)響應(yīng)導(dǎo)致系統(tǒng)“卡頓”。例如Task_200ms()顯示刷新函數(shù)內(nèi)部應(yīng)使用差異化刷新避免每次執(zhí)行都耗時(shí)過(guò)長(zhǎng)。4.3 系統(tǒng)調(diào)試方法與問(wèn)題定位當(dāng)系統(tǒng)行為異常時(shí)如何快速定位問(wèn)題模塊隔離法這是最有效的方法。通過(guò)條件編譯#ifdef DEBUG_KEY或函數(shù)開關(guān)暫時(shí)屏蔽掉其他所有模塊只運(yùn)行你懷疑有問(wèn)題的那個(gè)模塊。例如懷疑鍵盤掃描影響顯示就先注釋掉鍵盤掃描的代碼看顯示是否正常。IO口模擬示波器/邏輯分析儀在關(guān)鍵代碼位置操作一個(gè)空閑的IO口置高/置低然后用示波器觀察這個(gè)IO口的波形。可以測(cè)量函數(shù)執(zhí)行時(shí)間、中斷發(fā)生頻率、任務(wù)調(diào)度周期等。如果沒(méi)有硬件示波器可以借助某些軟件如串口波形打印進(jìn)行近似分析。串口打印調(diào)試信息這是最常用的方法。在代碼中插入printf語(yǔ)句將變量的值、程序的執(zhí)行流程如“進(jìn)入狀態(tài)A”、“收到按鍵值0xXX”發(fā)送到串口通過(guò)電腦端的串口助手查看。注意串口打印本身很耗時(shí)會(huì)嚴(yán)重影響程序?qū)崟r(shí)性因此只能用于調(diào)試階段且要謹(jǐn)慎使用或者使用一個(gè)緩沖隊(duì)列在后臺(tái)發(fā)送。檢查堆棧溢出51單片機(jī)內(nèi)存小如果函數(shù)嵌套調(diào)用層次太深或局部變量數(shù)組過(guò)大容易導(dǎo)致堆棧溢出覆蓋其他數(shù)據(jù)引發(fā)各種靈異錯(cuò)誤。可以通過(guò)觀察內(nèi)存地址或故意在內(nèi)存末端設(shè)置“哨兵值”來(lái)檢測(cè)。電源與噪聲問(wèn)題如果模擬測(cè)量如ADC值跳動(dòng)大、系統(tǒng)偶爾復(fù)位很可能是電源噪聲或地線干擾。檢查電源濾波電容是否足夠、靠近芯片模擬地和數(shù)字地單點(diǎn)連接是否良好。5. 國(guó)賽備賽與現(xiàn)場(chǎng)實(shí)戰(zhàn)建議最后結(jié)合國(guó)賽這個(gè)特定場(chǎng)景分享幾點(diǎn)純粹的實(shí)戰(zhàn)建議。備賽階段打造自己的“武器庫(kù)”不要每次練習(xí)都從頭開始寫驅(qū)動(dòng)。將經(jīng)過(guò)千錘百煉的、穩(wěn)定可靠的鍵盤掃描模塊、LCD顯示模塊、ADC濾波模塊、EEPROM存儲(chǔ)模塊、定時(shí)器調(diào)度框架封裝成獨(dú)立的.c/.h文件形成一個(gè)個(gè)人代碼庫(kù)。比賽時(shí)你可以像搭積木一樣快速構(gòu)建系統(tǒng)主體。進(jìn)行極限壓力測(cè)試在自己的板子上模擬國(guó)賽環(huán)境。同時(shí)讓所有外設(shè)滿負(fù)荷工作LCD快速刷新復(fù)雜動(dòng)畫ADC高速采樣鍵盤瘋狂按鍵EEPROM頻繁寫入。觀察系統(tǒng)是否死機(jī)、復(fù)位、或顯示異常。這能暴露出架構(gòu)設(shè)計(jì)、中斷處理和電源方面的深層次問(wèn)題。熟悉客觀題國(guó)賽有客觀題部分考察單片機(jī)基礎(chǔ)知識(shí)和編程能力。這部分是“送分題”但需要熟練度。提前刷歷年客觀題確保速度和準(zhǔn)確率。現(xiàn)場(chǎng)比賽審題與規(guī)劃前30分鐘至關(guān)重要拿到題目后不要急于動(dòng)手。用筆在紙上畫出系統(tǒng)功能框圖、狀態(tài)轉(zhuǎn)換圖、外設(shè)分配表哪個(gè)IO口、哪個(gè)定時(shí)器、任務(wù)時(shí)間規(guī)劃表。和隊(duì)友如有明確分工。分步實(shí)現(xiàn)與驗(yàn)證不要試圖一次性寫完所有代碼。應(yīng)該遵循“硬件測(cè)試 - 驅(qū)動(dòng)調(diào)試 - 功能模塊驗(yàn)證 - 系統(tǒng)整合”的步驟。每完成一個(gè)步驟如點(diǎn)亮LCD、讀出ADC值就進(jìn)行一次驗(yàn)證確保它是正確的再進(jìn)行下一步。保留調(diào)試手段即使是在最終代碼中也可以保留一個(gè)通過(guò)特定按鍵組合觸發(fā)的“調(diào)試模式”。在這個(gè)模式下可以通過(guò)LCD或串口如果允許輸出內(nèi)部關(guān)鍵變量如ADC原始值、濾波后值、系統(tǒng)狀態(tài)等。這在最后檢查故障時(shí)可能救你一命。管理時(shí)間與應(yīng)對(duì)突發(fā)問(wèn)題比賽時(shí)間緊張如果某個(gè)模塊如超聲波調(diào)試花費(fèi)遠(yuǎn)超預(yù)期要果斷決策是繼續(xù)攻克還是采用備用方案如用隨機(jī)數(shù)模擬一個(gè)合理范圍的數(shù)據(jù)先保證其他功能的完整性和演示流暢性一個(gè)能流暢演示但部分?jǐn)?shù)據(jù)模擬的系統(tǒng)通常比一個(gè)完全真實(shí)但卡死半途的系統(tǒng)得分更高。國(guó)賽的舞臺(tái)比拼的不僅是知識(shí)更是心態(tài)、規(guī)劃和應(yīng)變能力。把每一次練習(xí)都當(dāng)作實(shí)戰(zhàn)把復(fù)雜系統(tǒng)拆解成一個(gè)個(gè)你武器庫(kù)中熟悉的模塊你就能在賽場(chǎng)上從容不迫將積累的經(jīng)驗(yàn)轉(zhuǎn)化為行云流水的代碼。