關注公眾號,回復“入門資料”獲取單片機入門到高級開掛教程
?開發板帶你入門,我們帶你飛
文 | 無際(微信:2777492857)
全文約4501字,閱讀大約需要?15?分鐘
我還記得當年剛踏入嵌入式開發領域的時候,對軟件架構完全沒有概念。寫代碼想到哪寫到哪,最后拼湊成一個能跑的程序。
? ? ? ? ??
但隨著項目越來越復雜,代碼也越來越臃腫,維護起來簡直就是一場噩夢。改動一個小功能,都要提心吊膽,生怕把其他地方搞崩了。
? ? ? ? ??
工作大概4年左右,有幸接接手了大牛做的項目維護,感覺他的功底深不可測,能把復雜的系統拆解成一個個清晰的模塊,而且封裝得也挺復雜,當時看還挺復雜,挺不解,寫這么復雜干吊?看起來頭疼。
? ? ? ? ??
后面自己獨立做復雜點的項目,才發現他的架構是真香,因此吸收了很多他的思路和架構。
? ? ? ? ??
? ? ? ? ??
如果你也想提升自己的代碼質量,那么這篇文章就是為你準備的!我會用最通俗易懂的語言,帶你了解嵌入式軟件開發中常見的幾種架構模式,讓你擺脫“面向過程編程”的原始狀態,掌握“面向架構編程”的高級技巧。
? ? ? ? ??
無論你是初入茅廬的新手,還是想進階提升的老鳥,這篇文章都能幫你打造更清晰、更穩定、更易維護的嵌入式系統。
? ? ? ? ??
好了,廢話不多說,我們直接進入主題。
? ? ? ? ??? ??
1.?輪詢(Polling)架構:簡單直接,但容易阻塞
輪詢架構是最簡單、最基礎的架構模式,就像一個不知疲倦的“巡邏員”,不停地循環檢查各個任務的狀態,哪個任務需要執行就執行哪個。
? ? ? ? ??
?基本原理:
輪詢架構的核心是一個無限循環(while(1)),在循環中按照一定的順序依次執行各個任務模塊。每個任務模塊通常是一個函數,執行完成后立即返回,繼續執行下一個任務。
? ? ? ? ??
?代碼示例:
void?task1()?{??// 任務1的代碼}void?task2()?{??// 任務2的代碼}int?main()?{??while?(1)?? {? ? task1();? ? task2();? ??// ... 其他任務? }??return?0;}
?適用場景:
任務比較簡單,實時性要求不高。
系統資源非常有限,無法支持更復雜的架構。
初學者學習和理解嵌入式編程的入門架構。
? ? ? ? ??
?優缺點:
優點:?簡單易懂,易于實現,代碼量少。
缺點:?響應速度慢,所有任務共享CPU時間,容易出現“任務饑餓”現象(某個任務一直得不到執行);如果某個任務執行時間過長,會阻塞其他任務的執行。
? ? ? ? ??
?實戰避坑
盡量讓每個任務的執行時間短,避免長時間占用CPU。
可以調整任務的執行順序,將優先級較高的任務放在前面執行,以提高響應速度。
輪詢架構適合簡單的控制邏輯和數據采集,但不適合復雜的實時系統。
? ? ? ? ??
2.?中斷(Interrupt)架構:快速響應,但需要謹慎管理
中斷架構是一種基于事件驅動的簡易架構模式,當外部事件發生時,會觸發中斷,CPU暫停當前任務,立即執行中斷服務程序(ISR)。
? ? ? ? ??
?基本原理:? ??
中斷架構將任務分為“前臺任務”和“后臺任務”。后臺任務在主循環中執行,前臺任務(即中斷服務程序)在中斷發生時執行。
? ? ? ? ??
?代碼示例:
// 中斷服務程序void?EXTI0_IRQHandler()?{??if?(EXTI_GetITStatus(EXTI_Line0) != RESET)?? {? ??// 處理中斷事件? ? EXTI_ClearITPendingBit(EXTI_Line0);? }}int?main()?{??// 初始化中斷??// ...??while?(1)?? {? ??// 后臺任務? }??return?0;}
? ? ? ? ??
?適用場景:
需要快速響應外部事件,如按鍵按下、傳感器數據變化等。
系統有嚴格的實時性要求。
外部設備需要實時監控,例如串口接收數據。? ??
? ? ? ? ??
?優缺點:
優點:?響應速度快,能及時處理緊急事件;CPU利用率高,可以在后臺執行其他任務。
缺點:?中斷服務程序應該盡可能短,避免長時間占用CPU;中斷嵌套管理復雜,容易導致中斷優先級反轉等問題。
? ? ? ? ??
?實戰避坑:
中斷服務程序要“短小精悍”,只做最必要的工作,其他耗時操作交給后臺任務處理。
合理設置中斷優先級,避免高優先級的中斷被低優先級的中斷阻塞。
避免在中斷服務程序中使用阻塞函數,如delay(),printf()等。
? ? ? ? ??
3.?狀態機(State Machine)架構:邏輯清晰,但容易“狀態爆炸”
? ? ? ? ??
狀態機架構是一種將系統劃分為多個狀態,并根據輸入事件在不同狀態之間進行轉換的架構模式。
? ? ? ? ??
?基本原理:
狀態機將系統抽象成有限個狀態,每個狀態下有特定的行為和轉移條件。通過狀態轉移圖或表來管理狀態間的轉換。
? ? ? ? ??
?代碼示例:
typedef?enum?{? STATE_IDLE,? STATE_RUNNING,? STATE_ERROR} State_t;State_t currentState = STATE_IDLE;void?stateMachine()?{??switch?(currentState)?? {? ??case?STATE_IDLE:? ? ??if?(startCondition)?? ? ? {? ? ? ? currentState = STATE_RUNNING;? ? ? }? ? ??break;? ??case?STATE_RUNNING:? ? ??if?(errorCondition)?? ? ? {? ? ? ? currentState = STATE_ERROR;? ? ? }? ? ??break;? ??case?STATE_ERROR:? ? ??// 處理錯誤? ? ??break;? }}int?main()?{??while?(1)?? {? ? stateMachine();? ??// ... 其他任務? }??return?0;}
? ? ? ? ??
?適用場景:
系統有明顯的“狀態”概念,如通信協議、控制系統、用戶界面、不同的模式等。
邏輯比較復雜,需要清晰的狀態管理。
需要明確的流程控制,例如按鍵檢測,電梯運行等。
? ? ? ? ??
?優缺點:
優點:?邏輯清晰,易于維護和擴展;能有效管理復雜的狀態轉移。
缺點:?狀態過多時,代碼量會急劇增加,維護成本高;狀態轉移邏輯錯誤容易導致“死循環”等問題。
? ? ? ? ??
?實戰避坑:
在編寫代碼之前,先畫好狀態轉移圖,明確每個狀態的行為和轉移條件。
盡量減少狀態的數量,避免“狀態爆炸”。
可以使用狀態機框架或工具來簡化開發,例如UML狀態機圖。
? ? ? ? ??
4.?實時操作系統(RTOS)架構:多任務并行,但需要更多資源
實時操作系統(RTOS)是一種專門用于嵌入式系統的操作系統,它提供了任務調度、任務間通信、資源管理等功能,讓開發者可以像編寫PC程序一樣編寫嵌入式軟件。
? ? ? ? ??
?基本原理:
RTOS提供任務調度器,可以將系統劃分為多個獨立的任務(線程),每個任務都有自己的優先級和棧空間。RTOS負責調度這些任務,讓它們并行執行。
? ? ? ? ??
?代碼示例(以FreeRTOS為例):
void?task1(void?*pvParameters)?{??while?(1)?? {? ??// 任務1的代碼? ??vTaskDelay(100?/ portTICK_PERIOD_MS);?// 延時100ms? }}void?task2(void?*pvParameters)?{??while?(1)?? {? ??// 任務2的代碼? ??vTaskDelay(200?/ portTICK_PERIOD_MS);?// 延時200ms? }}int?main()?{??xTaskCreate(task1,?"Task1",?128,?NULL,?1,?NULL);??xTaskCreate(task2,?"Task2",?128,?NULL,?2,?NULL);??vTaskStartScheduler();??return?0;}
? ? ? ? ??
?適用場景:
系統功能復雜,需要多個任務并行執行。
對實時性和響應速度有較高要求。
需要復雜的任務管理和資源管理。
? ? ? ? ??
?優缺點:
優點:?任務管理靈活,易于實現復雜功能;提高系統響應速度;可以有效利用CPU資源。
缺點:?資源消耗較大,不適合資源受限的單片機;學習成本高;需要考慮任務同步和互斥等問題。
? ? ? ? ??
?實戰避坑:
根據項目和應用場景選擇合適的RTOS,如FreeRTOS、uC/OS等。
合理分配任務優先級和棧空間,避免任務沖突和棧溢出。
使用信號量、互斥鎖等機制進行任務間通信和資源保護。
避免在中斷服務程序中調用RTOS API。
? ? ? ? ??
5.?混合架構:靈活應對,但需要精心設計
在實際開發中,單一的架構模式往往難以滿足所有需求,因此經常會采用混合架構,將多種架構模式結合起來使用。例如,“輪詢 + 中斷”、“狀態機 + RTOS”等。
? ? ? ? ??
這次我們以一個簡單的溫度監控系統為例,該系統需要:
?實時監控溫度傳感器:?需要中斷來快速響應溫度變化。
?根據溫度進行狀態切換:?需要狀態機來管理不同的工作模式(例如:正常、高溫報警、低溫報警)。
?定期進行數據記錄:?需要輪詢來定期保存溫度數據到存儲器。
// 1. 定義狀態typedef?enum?{? ? STATE_NORMAL,? ? STATE_HIGH_TEMP_ALARM,? ? STATE_LOW_TEMP_ALARM} TemperatureState_t;TemperatureState_t currentTemperatureState = STATE_NORMAL;// 2. 定義全局變量volatile?float?temperature =?25.0;?// 當前溫度,volatile 確保中斷和主循環都能訪問bool?newDataAvailable =?false;?// 新數據標志// 3. 溫度傳感器中斷服務程序 (假設通過ADC讀取)void?ADC_IRQHandler(){? ??if?(ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET)? ? {? ? ? ??// 讀取ADC值? ? ? ??uint16_t?adcValue =?ADC_GetConversionValue(ADC1);? ? ? ??// 轉換成溫度值 (簡化計算)? ? ? ? temperature = (float)adcValue *?0.1;?// 假設每0.1代表1攝氏度? ? ? ? newDataAvailable =?true;?// 設置新數據標志? ? ? ??// 清除中斷標志位? ? ? ??ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);? ? }}// 4. 狀態機函數void?temperatureStateMachine(){? ??switch?(currentTemperatureState)? ? {? ? ? ??case?STATE_NORMAL:? ? ? ? ? ??if?(temperature >?35.0)? ? ? ? ? ? {? ? ? ? ? ? ? ? currentTemperatureState = STATE_HIGH_TEMP_ALARM;? ? ? ? ? ? ? ??// 啟動風扇? ? ? ? ? ? ? ??GPIO_SetBits(FAN_PORT, FAN_PIN);? ? ? ? ? ? ? ??printf("High Temperature Alarm!\n");? ? ? ? ? ? }? ? ? ? ? ??else?if?(temperature 10.0)? ? ? ? ? ? {? ? ? ? ? ? ? ? currentTemperatureState = STATE_LOW_TEMP_ALARM;? ? ? ? ? ? ? ??// 啟動加熱器? ? ? ? ? ? ? ??GPIO_SetBits(HEATER_PORT, HEATER_PIN);? ? ? ? ? ? ? ??printf("Low Temperature Alarm!\n");? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?STATE_HIGH_TEMP_ALARM:? ? ? ? ? ??if?(temperature <=?30.0)? ? ? ? ? ? {? ? ? ? ? ? ? ? currentTemperatureState = STATE_NORMAL;? ? ? ? ? ? ? ??// 關閉風扇? ? ? ? ? ? ? ??GPIO_ResetBits(FAN_PORT, FAN_PIN);? ? ? ? ? ? ? ??printf("Normal Temperature.\n");? ? ? ? ? ? }? ? ? ? ? ??break;? ? ? ??case?STATE_LOW_TEMP_ALARM:? ? ? ? ? ??if?(temperature >=?15.0)? ? ? ? ? ? {? ? ? ? ? ? ? ? currentTemperatureState = STATE_NORMAL;? ? ? ? ? ? ? ??// 關閉加熱器? ? ? ? ? ? ? ??GPIO_ResetBits(HEATER_PORT, HEATER_PIN);? ? ? ? ? ? ? ??printf("Normal Temperature.\n");? ? ? ? ? ? }? ? ? ? ? ??break;? ? }}// 5. 數據記錄任務 (輪詢方式)void?dataLoggingTask(){? ??static?uint32_t?lastLogTime =?0;? ??uint32_t?currentTime =?HAL_GetTick();?// 獲取當前時間 (需要HAL庫支持)? ??if?(currentTime - lastLogTime >=?5000)?// 每5秒記錄一次? ? {? ? ? ? lastLogTime = currentTime;? ? ? ??// 將溫度數據記錄到存儲器 (這里簡化成打印)? ? ? ??printf("Logging: Temperature = %.2f, State = %d\n", temperature, currentTemperatureState);? ? ? ??// 實際應用中,需要寫入Flash或者SD卡? ? }}// 6. 主函數int?main(){? ??// 初始化 ADC,GPIO,中斷? ??// ... (初始化代碼略)? ??HAL_Init();? ??SystemClock_Config();? ??MX_GPIO_Init();? ??MX_ADC1_Init();? ??// 啟動ADC轉換并使能中斷? ??HAL_ADC_Start_IT(&hadc1);? ??// 主循環? ??while?(1)? ? {? ? ? ??// 1. 狀態機處理? ? ? ??temperatureStateMachine();? ? ? ??// 2. 數據記錄 (輪詢)? ? ? ??dataLoggingTask();? ? ? ??// 3. 其他任務 (可以添加更多任務)? ? ? ??// ...? ? ? ??// 4. 降低功耗 (可選)? ? ? ??// HAL_PWR_EnterSleepMode(PWR_LOWPOWERREGULATOR_ON, ADC_IRQn);? ? }}
? ? ? ? ??
代碼說明:
溫度傳感器中斷:?使用ADC讀取溫度,并在每次讀取后設置newDataAvailable標志。volatile關鍵字確保主循環能及時讀取到最新的溫度數據。
? ? ? ? ??
狀態機:temperatureStateMachine()?函數根據溫度值,切換系統狀態,并控制風扇或加熱器的開啟/關閉。
? ? ? ? ??
數據記錄任務:dataLoggingTask()?函數每隔5秒記錄一次溫度數據和系統狀態。
? ? ? ? ??
混合架構的優勢:
?實時響應:?中斷確保系統能及時響應溫度變化。
?狀態管理:?狀態機負責管理系統的工作模式,并根據溫度進行切換。
?數據記錄:?輪詢確保數據能夠被定期記錄。
? ? ? ? ??
這個例子雖然簡單,但已經展示了混合架構的基本思想:將不同的架構模式結合起來,充分發揮各自的優勢,以滿足復雜的系統需求。
? ? ? ? ??
?適用場景:
系統既有實時性要求,又有復雜的邏輯。
資源有限,需要在性能和復雜度之間進行平衡。
各個模塊的功能特性不同,需要采用不同的架構模式。
? ? ? ? ??
?優缺點:
優點:?靈活性高,可以針對具體問題選擇最佳解決方案。
缺點:?架構設計難度較大,需要經驗豐富的工程師;架構間的接口和通信需要精心設計,避免出現“縫合怪”現象。
? ? ? ? ??
我們從事了10年,就喜歡用自己設計的架構,采用了多種混合架構,縫合了不同框架的優勢,相對RTOS更精簡更節省資源,很多51單片機也能用。
? ? ? ? ??
比如我們無際單片機的項目3和項目6,就是把輪詢架構加了一層封裝,讓它們在管理任務時更加靈活方便,以輪詢作為主框架,其余有狀態機、表驅動之類的架構配合,具體可以看我前面幾篇文章。

? ? ? ? ??
關于這個輪詢架構我也在2018年錄了個全面的教程,目前開源,無際粉絲可找我安排。

? ? ? ? ??
?實戰避坑:
在設計混合架構之前,先分析系統需求,明確各個模塊的功能和性能要求。
確定核心架構,例如使用RTOS作為主框架,然后根據需要添加其他架構模塊。
定義清晰的接口規范,確保各個模塊之間的通信和數據交換順暢。
? ? ? ? ??
6.?選擇架構:沒有最好的,只有最合適的
? ? ? ? ??
選擇哪種架構模式,并沒有絕對的答案,關鍵是要根據具體的項目需求和資源情況進行選擇。
? ? ? ? ??
?如果任務簡單,實時性要求不高,資源有限,那么輪詢架構是一個不錯的選擇。
?如果需要快速響應外部事件,或者有嚴格的實時性要求,那么中斷架構是必不可少的。
?如果系統邏輯復雜,有明顯的狀態概念,那么狀態機架構可以幫助你更好地管理代碼。
?如果系統功能復雜,需要多個任務并行執行,那么RTOS架構可以提高系統效率和響應速度。
?如果以上幾種架構都不能滿足你的需求,那么可以考慮采用混合架構,將多種架構模式結合起來使用。
? ? ? ? ??
7.?總結
嵌入式軟件架構是單片機開發的“靈魂”,它決定了你的代碼是“豆腐渣工程”還是“藝術品”。
? ? ? ? ??
掌握這些常見的架構模式,你就能更好地組織你的代碼,構建更清晰、更穩定、更易維護的嵌入式系統。
? ? ? ? ??
希望這篇文章能幫助你擺脫“代碼搬運工”的身份,起到拋磚引玉的作用,助你成為一名真正的嵌入式軟件架構師!好的架構不僅能提高開發效率,還能讓你在開發復雜項目時游刃有余。

下面是更多無際原創的個人成長經歷、行業經驗、技術干貨。
1.電子工程師是怎樣的成長之路?10年5000字總結
2.如何快速看懂別人的代碼和思維
3.單片機開發項目全局變量太多怎么管理?
4.C語言開發單片機為什么大多數都采用全局變量的形式?
5.單片機怎么實現模塊化編程?實用程度讓人發指!
6.c語言回調函數的使用及實際作用詳解
7.手把手教你c語言隊列實現代碼,通俗易懂超詳細!
8.c語言指針用法詳解,通俗易懂超詳細!