在Windows模擬器上運(yùn)行FreeRTOS命令行工程:從任務(wù)調(diào)度到調(diào)試技巧)
簡(jiǎn)介一份面向STM32嵌入式開發(fā)者的FreeRTOS命令行演示工程基于Windows下的集成開發(fā)環(huán)境Keil下載解壓后即可打開、編譯并燒寫免去手工移植和配置的繁瑣步驟。該工程通過(guò)串口中斷接收數(shù)據(jù)進(jìn)而調(diào)用FreeRTOS命令行讓使用者在真實(shí)芯片上體驗(yàn)任務(wù)創(chuàng)建、隊(duì)列通信、定時(shí)器管理等核心功能。資源總體積約七百七十七KB包含一百一十六個(gè)文件其中六十七個(gè)頭文件與三十八個(gè)源文件覆蓋標(biāo)準(zhǔn)外設(shè)驅(qū)動(dòng)、FreeRTOS內(nèi)核源碼以及命令行解析邏輯另有工程配置文件、初始化文件、說(shuō)明文檔和許可證等輔助材料目錄層次分明便于按模塊查閱。目前已有近兩百人學(xué)習(xí)或下載。配合作者撰寫的《FreeRTOS 體驗(yàn)教程2.用命令行操作FreeRTOS》閱讀既能對(duì)照理解串口中斷接收與命令解析的結(jié)合方式也可直接研讀任務(wù)調(diào)度、隊(duì)列和定時(shí)器的源碼實(shí)現(xiàn)適合希望快速上手FreeRTOS并嘗試命令行交互的嵌入式初學(xué)者。1. Windows下開箱即編譯不接開發(fā)板把freeRTOS命令行工程跑起來(lái)在Windows上跑FreeRTOS和RTOS命令行很多人第一反應(yīng)是得先有一塊開發(fā)板——這套Keil uVision4的MDK demo工程證明不一定。我第一次拿到它時(shí)正在等板子到貨手頭的需求卻等不了要在一周內(nèi)把任務(wù)劃分、消息隊(duì)列、命令交互的框架定下來(lái)。這套demo解決得干脆——解壓、打開工程、編譯、進(jìn)調(diào)試器FreeRTOS內(nèi)核就在模擬器里跑起來(lái)自帶的freeRTOS命令行能直接交互敲task-stats看所有任務(wù)的棧余量和運(yùn)行狀態(tài)敲free-heap看堆剩余。不依賴真實(shí)硬件內(nèi)核行為與開發(fā)板一致。適合沒(méi)板子但想學(xué)RTOS調(diào)度機(jī)制的初學(xué)者適合硬件未到需要先驗(yàn)證業(yè)務(wù)邏輯的工程師也適合研究命令行組件如何與RTOS任務(wù)共存的開發(fā)者。2. 工程解剖從目錄布局到配置項(xiàng)和啟動(dòng)鏈路的完整骨架2.1 目錄結(jié)構(gòu)內(nèi)核、移植層、用戶代碼與CLI四塊分區(qū)拿到工程后第一件事不是點(diǎn)編譯而是先翻目錄。這套工程的目錄組織得很規(guī)矩大致劃成四塊每塊職責(zé)單一改起來(lái)不會(huì)串線。目錄主要內(nèi)容職責(zé)說(shuō)明FreeRTOS/Sourcetasks.c、queue.c、list.c、timers.c、event_groups.c內(nèi)核核心代碼純C實(shí)現(xiàn)不依賴具體硬件FreeRTOS/Source/portableRVDS/ARM_CM3含port.c、portmacro.h移植層負(fù)責(zé)上下文切換、PendSV與SysTick處理FreeRTOS/Source/includeFreeRTOS.h、task.h、queue.h、semphr.h公共API頭文件用戶代碼只需包含這些Usermain.c、FreeRTOSConfig.h、啟動(dòng)文件用戶業(yè)務(wù)代碼與內(nèi)核剪裁配置CLIFreeRTOS_CLI.c、FreeRTOS_CLI.hfreeRTOS命令行實(shí)現(xiàn)命令解析與分發(fā)核心先看內(nèi)核這幾份文件。tasks.c是調(diào)度器本體只干三件事任務(wù)狀態(tài)機(jī)切換、時(shí)間片輪轉(zhuǎn)、優(yōu)先級(jí)搶占。queue.c表面上只是隊(duì)列實(shí)際上信號(hào)量和互斥鎖的底層也是它——FreeRTOS的信號(hào)量是基于隊(duì)列機(jī)制實(shí)現(xiàn)的這點(diǎn)很多初學(xué)者不知道。list.c是內(nèi)核內(nèi)部的鏈表實(shí)現(xiàn)所有任務(wù)就緒表、阻塞表、延時(shí)表都靠它串起來(lái)。這些是純C代碼編出來(lái)后就是一組不依賴芯片外設(shè)的庫(kù)這也是FreeRTOS能橫跨幾十種芯片的原因。移植層是最容易被忽略又最不能錯(cuò)的地方。ARM_CM3目錄下port.c用嵌入?yún)R編實(shí)現(xiàn)了PendSV_Handler和SysTick_Handler任務(wù)切換的實(shí)質(zhì)就是這兩段代碼在保存和恢復(fù)CPU寄存器上下文。你不需要改它但得知道任務(wù)怎么切走的答案全在這里——很多新手問(wèn)調(diào)度器為什么能自動(dòng)跳來(lái)跳去其實(shí)每次都是SysTick觸發(fā)中斷然后在PendSV里把當(dāng)前任務(wù)的寄存器壓棧、把下一個(gè)任務(wù)的寄存器出棧僅此而已。CLI那部分就是這套demo的亮點(diǎn)了。FreeRTOSCLI是官方命令解釋器組件不綁定任何外設(shè)只要求你給它一個(gè)字符輸入渠道和一塊輸出緩沖區(qū)。demo里把它接到UART上模擬器里的終端窗口敲命令直接回顯結(jié)果。第3章會(huì)專門拆它的內(nèi)部實(shí)現(xiàn)。2.2 FreeRTOSConfig.h這幾個(gè)配置項(xiàng)直接決定任務(wù)怎么調(diào)度FreeRTOSConfig.h是整個(gè)工程的大腦配置文件它不參與業(yè)務(wù)邏輯卻決定了內(nèi)核怎么剪裁、怎么跑。打開這份文件先盯這幾項(xiàng)#define configTICK_RATE_HZ ( 1000 ) /* 系統(tǒng)時(shí)鐘節(jié)拍單位Hz1000就是一秒1000次tick */ #define configMINIMAL_STACK_SIZE ( 128 ) /* 空閑任務(wù)棧深度單位是字而不是字節(jié) */ #define configTOTAL_HEAP_SIZE ( 16 * 1024 ) /* 內(nèi)核堆總大小任務(wù)棧和TCB都從這里分配 */ #define configUSE_PREEMPTION ( 1 ) /* 1搶占式調(diào)度0協(xié)作式調(diào)度 */ #define configUSE_MUTEXES ( 1 ) /* 使能互斥鎖帶優(yōu)先級(jí)繼承機(jī)制 */ #define configCHECK_FOR_STACK_OVERFLOW ( 2 ) /* 棧溢出檢測(cè)級(jí)別2任務(wù)切換時(shí)檢測(cè)并掛鉤子函數(shù) */configTICK_RATE_HZ設(shè)為1000意味著每毫秒觸發(fā)一次SysTick這是大多數(shù)實(shí)時(shí)性要求不苛刻的業(yè)務(wù)的通用選擇。如果任務(wù)是毫秒級(jí)精確延時(shí)需求這個(gè)值可以提到2000甚至更高但代價(jià)是內(nèi)核調(diào)度開銷增大——每次tick都要跑一遍調(diào)度器判斷是否需要切換。configMINIMAL_STACK_SIZE的128是「字」不是「字節(jié)」在32位Cortex-M上一個(gè)字是4字節(jié)所以128字等于512字節(jié)。這個(gè)值只用于空閑任務(wù)和demo任務(wù)業(yè)務(wù)任務(wù)棧要單獨(dú)算常見做法是給任務(wù)開256到512字。configTOTAL_HEAP_SIZE管著所有任務(wù)控制塊TCB和任務(wù)棧的內(nèi)存來(lái)源。這個(gè)值開小了會(huì)直接導(dǎo)致xTaskCreate失敗開大了又浪費(fèi)RAM調(diào)試期建議用xPortGetFreeHeapSize()打印剩余堆來(lái)反向校準(zhǔn)。configUSE_MUTEXES值得單獨(dú)說(shuō)——互斥鎖和二進(jìn)制信號(hào)量的區(qū)別在于互斥鎖帶優(yōu)先級(jí)繼承能避免低優(yōu)先級(jí)任務(wù)拿著鎖不讓高優(yōu)先級(jí)任務(wù)執(zhí)行這種優(yōu)先級(jí)反轉(zhuǎn)問(wèn)題demo里訪問(wèn)共享資源時(shí)優(yōu)先用互斥鎖而不是信號(hào)量。2.3 啟動(dòng)鏈路從Reset_Handler到vTaskStartScheduler這套demo的啟動(dòng)流程和帶板子的工程完全一致只是執(zhí)行載體換成了Keil的模擬器。芯片上電后先走啟動(dòng)文件里的Reset_Handler初始化棧指針、調(diào)用SystemInit做時(shí)鐘和外設(shè)的初步配置然后跳進(jìn)main()。main()里干的事可以歸納為三段初始化UART命令行要用、創(chuàng)建demo任務(wù)和CLI任務(wù)、最后調(diào)用vTaskStartScheduler()把控制權(quán)交給內(nèi)核。vTaskStartScheduler()一旦成功啟動(dòng)就不會(huì)返回——它會(huì)創(chuàng)建空閑任務(wù)和可選的定時(shí)器任務(wù)然后觸發(fā)第一次上下文切換進(jìn)入調(diào)度循環(huán)。從這一刻起main()的代碼就不再被執(zhí)行了整個(gè)CPU時(shí)間片都由內(nèi)核管理。很多新手在main()后面繼續(xù)寫業(yè)務(wù)代碼發(fā)現(xiàn)永遠(yuǎn)執(zhí)行不到就是因?yàn)闆](méi)理解這一層。所以在demo里你會(huì)發(fā)現(xiàn)所有業(yè)務(wù)邏輯都被封裝進(jìn)任務(wù)函數(shù)main()只做初始化和點(diǎn)火兩件事。3. 核心模塊拆解任務(wù)調(diào)度與命令行從注冊(cè)到回顯的完整鏈路3.1 任務(wù)創(chuàng)建與調(diào)度器啟動(dòng)代碼級(jí)拆解打開main.c最先看到的就是任務(wù)創(chuàng)建。這幾乎是所有FreeRTOS工程的模板寫法/* demo任務(wù)1周期性打印運(yùn)行信息 */ static void vDemoTask1(void *pvParameters) { for (;;) { vPrintStringLine(DemoTask1 running...); vTaskDelay(pdMS_TO_TICKS(1000)); /* 主動(dòng)讓出CPU阻塞1000個(gè)tick */ } } int main(void) { /* 初始化UART命令行輸入輸出都走這個(gè)外設(shè) */ vUartInit(115200); /* 構(gòu)建設(shè)備對(duì)象創(chuàng)建demo任務(wù)和命令行任務(wù) */ xTaskCreate(vDemoTask1, Demo1, 256, NULL, 1, NULL); xTaskCreate(vDemoTask2, Demo2, 256, NULL, 1, NULL); xTaskCreate(vCLITask, CLI, 512, NULL, 2, xCLITaskHandle); /* 啟動(dòng)調(diào)度器成功則永不返回 */ vTaskStartScheduler(); /* 走到這里說(shuō)明調(diào)度器啟動(dòng)失敗通常是堆空間不足 */ for (;;) { } }xTaskCreate的六個(gè)參數(shù)依次是任務(wù)函數(shù)指針、任務(wù)名純調(diào)試用途、棧深度單位是字、傳給任務(wù)的參數(shù)指針、優(yōu)先級(jí)數(shù)字越大優(yōu)先級(jí)越高、任務(wù)句柄輸出指針。這里CLI任務(wù)優(yōu)先級(jí)設(shè)為2兩個(gè)demo任務(wù)為1空閑任務(wù)優(yōu)先級(jí)是0所以CLI任務(wù)能隨時(shí)搶占demo任務(wù)——命令行交互的實(shí)時(shí)性就是這么保證的。vTaskDelay(pdMS_TO_TICKS(1000))里的pdMS_TO_TICKS是個(gè)宏把毫秒換算成tick數(shù)在configTICK_RATE_HZ等于1000時(shí)1000毫秒就是1000個(gè)tick。任務(wù)調(diào)用它之后會(huì)進(jìn)入阻塞態(tài)把CPU讓給其他就緒任務(wù)這是理解RTOS多任務(wù)同時(shí)跑錯(cuò)覺的關(guān)鍵點(diǎn)。3.2 命令行實(shí)現(xiàn)命令表、解析器與逐字符處理freeRTOS命令行的核心是FreeRTOS_CLI.c它把命令字串→處理函數(shù)的映射關(guān)系做成一張表。demo里的命令注冊(cè)和內(nèi)核的任務(wù)創(chuàng)建是解耦的注冊(cè)過(guò)程在主程序啟動(dòng)時(shí)完成/* 命令處理函數(shù)統(tǒng)一簽名輸出緩沖、緩沖長(zhǎng)度、原始命令字串 */ static BaseType_t prvTaskStatsCommand(char *pcWriteBuffer, size_t xWriteBufferLen, const char *pcCommandString); static BaseType_t prvFreeHeapCommand(char *pcWriteBuffer, size_t xWriteBufferLen, const char *pcCommandString); /* 命令注冊(cè)表每一行是一條命令的名字、幫助文本、處理函數(shù) */ static const CLI_Command_Definition_t xCommandTable[] { { task-stats, task-stats: 顯示所有任務(wù)的棧與調(diào)度統(tǒng)計(jì)\r\n, prvTaskStatsCommand }, { free-heap, free-heap: 顯示剩余堆空間\r\n, prvFreeHeapCommand }, { help, help: 列出全部可用命令\r\n, FreeRTOS_CLIHelpCommand }, }; /* 在主程序初始化階段逐條注冊(cè) */ void vRegisterDemoCommands(void) { for (int i 0; i sizeof(xCommandTable) / sizeof(xCommandTable[0]); i) { FreeRTOS_CLIRegisterCommand(xCommandTable[i]); } }這種表驅(qū)動(dòng)的命令設(shè)計(jì)妙處在于新增命令不用改解析器的代碼只在數(shù)組里加一行。FreeRTOS_CLIRegisterCommand內(nèi)部會(huì)把命令節(jié)點(diǎn)掛到一個(gè)全局鏈表中運(yùn)行時(shí)每條命令按名字逐一匹配。參數(shù)說(shuō)明一下命令字串區(qū)分大小寫如果用戶敲了大寫會(huì)匹配不到CLI會(huì)返回未知命令的提示幫助文本在敲help命令時(shí)統(tǒng)一列出所以新命令一定要寫好幫助文本否則別人看不出這命令是干嘛的。真正的解析過(guò)程發(fā)生在CLI任務(wù)里。CLI任務(wù)從UART逐字符讀取輸入攢夠一條完整命令后交給FreeRTOS_CLIProcessCommand處理/* CLI任務(wù)從UART讀字符、組裝命令、調(diào)用解析器、回顯結(jié)果 */ static void vCLITask(void *pvParameters) { char cRxChar; char cCmdBuffer[configCLI_INPUT_BUFFER_LENGTH]; char cOutputBuffer[configCLI_OUTPUT_BUFFER_LENGTH]; uint32_t ulInIndex 0; for (;;) { /* 從UART接收一個(gè)字符模擬器里對(duì)應(yīng)終端窗口的按鍵 */ if (xUartReceiveByte(cRxChar) pdPASS) { /* 回車或換行表示一條命令輸入完畢 */ if ((cRxChar \r) || (cRxChar \n)) { cCmdBuffer[ulInIndex] \0; /* 字符串終結(jié)符 */ ulInIndex 0; /* 索引歸零等下一條 */ /* 調(diào)用命令行解析器結(jié)果寫入cOutputBuffer */ FreeRTOS_CLIProcessCommand(cCmdBuffer, cOutputBuffer, configCLI_OUTPUT_BUFFER_LENGTH); /* 把解析器生成的輸出文本發(fā)回UART */ vUartSendString(cOutputBuffer); } else if (ulInIndex (configCLI_INPUT_BUFFER_LENGTH - 1)) { /* 非結(jié)束符存入輸入緩沖留一個(gè)位置給\0 */ cCmdBuffer[ulInIndex] cRxChar; } } } }這個(gè)循環(huán)的妙處是它本身也是一個(gè)FreeRTOS任務(wù)優(yōu)先級(jí)高于demo任務(wù)所以終端交互永遠(yuǎn)不被業(yè)務(wù)任務(wù)餓死。FreeRTOS_CLIProcessCommand會(huì)在內(nèi)部遍歷命令鏈表找到匹配項(xiàng)后調(diào)用對(duì)應(yīng)的處理函數(shù)把結(jié)果寫入輸出緩沖。configCLI_INPUT_BUFFER_LENGTH和configCLI_OUTPUT_BUFFER_LENGTH在FreeRTOS_CLI.h里定義demo里默認(rèn)給到120和1024字節(jié)——輸出緩沖要慎調(diào)task-stats的表格內(nèi)容在一長(zhǎng)串任務(wù)名和數(shù)字的情況下很容易超出1024字節(jié)超了就會(huì)被截?cái)唷?.3 隊(duì)列與信號(hào)量demo里任務(wù)間通信的三種姿勢(shì)demo里除了CLI交互還演示了任務(wù)間通信的典型寫法。先是隊(duì)列兩個(gè)demo任務(wù)通過(guò)一個(gè)隊(duì)列傳數(shù)據(jù)/* 全局隊(duì)列句柄 */ QueueHandle_t xDemoQueue; /* 發(fā)送任務(wù)每500ms向隊(duì)列寫入一個(gè)遞增的計(jì)數(shù)值 */ static void vDemoTask1(void *pvParameters) { int32_t lCount 0; for (;;) { xQueueSend(xDemoQueue, lCount, 0); lCount; vTaskDelay(pdMS_TO_TICKS(500)); } } /* 接收任務(wù)阻塞等待隊(duì)列消息收到后打印出來(lái) */ static void vDemoTask2(void *pvParameters) { int32_t lReceived; for (;;) { if (xQueueReceive(xDemoQueue, lReceived, portMAX_DELAY) pdPASS) { vPrintStringLine(DemoTask2 received: %ld, lReceived); } } }xQueueSend的第三個(gè)參數(shù)是等待時(shí)間0表示隊(duì)列滿時(shí)直接返回不等待portMAX_DELAY表示無(wú)限阻塞隊(duì)列沒(méi)有數(shù)據(jù)時(shí)接收任務(wù)就掛起不消耗CPU。這里有個(gè)新手容易踩的坑xQueueSend拷貝的是指針還是數(shù)據(jù)默認(rèn)行為是拷貝數(shù)據(jù)本身按隊(duì)列創(chuàng)建時(shí)設(shè)定的元素大小所以傳lCount沒(méi)問(wèn)題——但如果隊(duì)列元素是結(jié)構(gòu)體要確認(rèn)結(jié)構(gòu)體大小和隊(duì)列初始化時(shí)保持一致否則會(huì)越界寫壞堆。信號(hào)量和互斥鎖的用法在demo里通常體現(xiàn)在對(duì)共享資源的保護(hù)上。比如CLI的task-stats命令內(nèi)部要遍歷任務(wù)鏈表如果同時(shí)有別的任務(wù)在創(chuàng)建或刪除任務(wù)鏈表結(jié)構(gòu)正在變化遍歷就會(huì)讀到臟數(shù)據(jù)。常見做法是把命令處理邏輯放進(jìn)去臨界區(qū)或互斥鎖保護(hù)但這層細(xì)節(jié)demo通常簡(jiǎn)化處理真實(shí)產(chǎn)品里建議用xSemaphoreTake/xSemaphoreGive包住所有會(huì)并發(fā)訪問(wèn)內(nèi)核數(shù)據(jù)結(jié)構(gòu)的路徑。4. 編譯與運(yùn)行uVision4里從Build到敲命令的實(shí)操步驟4.1 打開工程與編譯選項(xiàng)核對(duì)工程文件是.uvproj后綴uVision4直接File→Open Project選它就打開了。打開后先做兩個(gè)檢查項(xiàng)別急著編譯。第一項(xiàng)在Options for TargetAltF7的Debug標(biāo)簽頁(yè)里左側(cè)單選按鈕必須選Use Simulator而不是某個(gè)調(diào)試器型號(hào)——這是不接開發(fā)板也能運(yùn)行的前提。選了模擬器后右邊還能設(shè)時(shí)鐘頻率對(duì)FreeRTOS來(lái)說(shuō)這會(huì)影響SysTick的計(jì)時(shí)基準(zhǔn)demo默認(rèn)給的頻率不用改除非你發(fā)現(xiàn)延時(shí)行為異常。第二項(xiàng)在Target標(biāo)簽頁(yè)確認(rèn)Device或CPU型號(hào)已指定為Cortex-M3內(nèi)核這樣Keil才知道用哪套指令集編譯。編譯按F7輸出窗口如果出現(xiàn)0 Error(s)、0 Warning(s)是最理想的。如果報(bào)了warning別無(wú)視——最常見的是declared but never used這類多數(shù)不影響運(yùn)行但如果是implicit declaration of function就要當(dāng)回事說(shuō)明某個(gè)頭文件沒(méi)包含或移植層文件漏加了。4.2 啟動(dòng)模擬器與連接終端窗口編譯通過(guò)后按CtrlF5進(jìn)入調(diào)試會(huì)話模擬器把ELF加載進(jìn)虛擬的Cortex-M3內(nèi)核PC停在Reset_Handler。此刻按F5運(yùn)行main()會(huì)執(zhí)行UART初始化和任務(wù)創(chuàng)建然后vTaskStartScheduler()接管控制權(quán)三個(gè)任務(wù)開始輪轉(zhuǎn)。接下來(lái)關(guān)鍵一步打開View→Serial Windows→UART#1。這就是模擬UART的終端窗口CLI任務(wù)的xUartReceiveByte讀到的字符就是在這個(gè)窗口敲進(jìn)去的vUartSendString的輸出也會(huì)顯示在這里。注意UART#1窗口默認(rèn)可能不彈出需要在調(diào)試會(huì)話進(jìn)行中主動(dòng)打開。打開后鼠標(biāo)點(diǎn)進(jìn)窗口輸入help回車如果能看到命令列表說(shuō)明CLI鏈路完全打通了。4.3 用內(nèi)置命令驗(yàn)證調(diào)度狀態(tài)運(yùn)行穩(wěn)定后終端里依次敲幾條命令驗(yàn)證內(nèi)核行為help列出所有注冊(cè)的命令每條命令的幫助文本也會(huì)顯示task-stats輸出一張表格列是任務(wù)名、狀態(tài)、優(yōu)先級(jí)、棧余量、任務(wù)編號(hào)。狀態(tài)列的X表示當(dāng)前正在運(yùn)行B表示阻塞中R表示就緒。連續(xù)敲兩次你會(huì)發(fā)現(xiàn)X標(biāo)記在CLI和兩個(gè)demo任務(wù)之間跳來(lái)跳去——這是搶占式調(diào)度最直觀的證據(jù)free-heap顯示堆剩余字節(jié)數(shù)。剛啟動(dòng)時(shí)記一個(gè)值跑一段時(shí)間再敲如果數(shù)值持續(xù)下降說(shuō)明有內(nèi)存泄漏task-stats輸出的Stack列單位是字不是字節(jié)計(jì)算棧使用量時(shí)要乘以4。另外vTaskList的實(shí)現(xiàn)依賴configUSE_TRACE_FACILITY為1如果改配置時(shí)把這開關(guān)關(guān)了task-stats命令會(huì)輸出空表或異常字符排查時(shí)優(yōu)先檢查這個(gè)宏。5. 避坑指南MDK模擬器里跑FreeRTOS的五個(gè)典型翻車現(xiàn)場(chǎng)5.1 一進(jìn)調(diào)試就HardFault棧溢出的經(jīng)典偽裝現(xiàn)象編譯、鏈接都通過(guò)進(jìn)調(diào)試器運(yùn)行不到一秒程序卡死在HardFault_Handler里斷點(diǎn)位置毫無(wú)規(guī)律。原因某個(gè)任務(wù)的棧開小了。demo任務(wù)里的vPrintStringLine這類打印函數(shù)內(nèi)部會(huì)走格式化字符串和UART發(fā)送棧消耗能達(dá)到100字以上再加上函數(shù)調(diào)用鏈的現(xiàn)場(chǎng)保存256字很容易見底。解決先把configCHECK_FOR_STACK_OVERFLOW設(shè)為2這樣每次任務(wù)切換時(shí)內(nèi)核會(huì)檢查棧頂?shù)乃痪€標(biāo)記是否被踩掉同時(shí)把棧深度從256加碼到512再試。如果還不穩(wěn)用task-stats命令看Stack列的真實(shí)余量反向調(diào)到夠用再加20%余量。5.2 xTaskCreate返回pdFAIL堆空間不夠的癥狀與定位現(xiàn)象程序跑起來(lái)了但少一個(gè)任務(wù)點(diǎn)開調(diào)試器的Watch窗口看xTaskCreate的返回值是0pdFAIL。原因configTOTAL_HEAP_SIZE設(shè)置太小任務(wù)棧加TCB分配不出來(lái)。這個(gè)坑在新手把堆從默認(rèn)值往小調(diào)想省RAM時(shí)特別常見。解決臨時(shí)把堆調(diào)到32KB在main()里任務(wù)創(chuàng)建前調(diào)xPortGetFreeHeapSize()打印初始堆任務(wù)創(chuàng)建后再打印一次差值就是所有任務(wù)吃掉的總內(nèi)存。按這個(gè)數(shù)反向把configTOTAL_HEAP_SIZE精調(diào)到合適值別拍腦袋。5.3 模擬器里UART輸入不響應(yīng)先分清輪詢還是中斷現(xiàn)象UART#1窗口里敲help窗口沒(méi)反應(yīng)但demo任務(wù)的周期性打印還在刷屏。原因CLI任務(wù)可能用的是中斷方式接收UART數(shù)據(jù)而模擬器對(duì)UART中斷的行為和真實(shí)芯片有細(xì)微差別——中斷優(yōu)先級(jí)配置不對(duì)或外設(shè)時(shí)鐘模型沒(méi)配好導(dǎo)致中斷永遠(yuǎn)不觸發(fā)。解決把接收改成輪詢方式即在CLI任務(wù)里循環(huán)讀UART狀態(tài)寄存器的RXNE位讀到1才取數(shù)據(jù)。模擬器里輪詢比中斷更可靠。如果你就是想驗(yàn)證中斷驅(qū)動(dòng)模式先確認(rèn)NVIC的優(yōu)先級(jí)分組和FreeRTOS的configMAX_SYSCALL_INTERRUPT_PRIORITY配置一致否則內(nèi)核會(huì)把中斷關(guān)掉數(shù)據(jù)自然進(jìn)不來(lái)。5.4 開-O2優(yōu)化后任務(wù)和CLI亂跳volatile與編譯器優(yōu)化之爭(zhēng)現(xiàn)象優(yōu)化級(jí)別從-O0改成-O2后原本正常的任務(wù)調(diào)度變得神經(jīng)質(zhì)或者某個(gè)while循環(huán)永遠(yuǎn)跳不出來(lái)。原因跨任務(wù)共享的變量沒(méi)加volatile編譯器認(rèn)為循環(huán)體內(nèi)沒(méi)人改動(dòng)它直接把讀變量的操作提到循環(huán)外。這種問(wèn)題在自旋等待標(biāo)志位、共享計(jì)數(shù)的場(chǎng)景里幾乎必現(xiàn)。解決對(duì)所有在任務(wù)間共享、會(huì)被任務(wù)或中斷修改的全局變量一律加volatile修飾。這不是玄學(xué)C標(biāo)準(zhǔn)里明確寫了volatile就是告訴編譯器這變量可能被外部改別優(yōu)化掉。5.5 SysTick延時(shí)不準(zhǔn)模擬器時(shí)鐘與configTICK_RATE_HZ的對(duì)齊現(xiàn)象vTaskDelay(pdMS_TO_TICKS(1000))量出來(lái)實(shí)際間隔明顯不是1秒有時(shí)快有時(shí)慢。原因Keil模擬器的指令執(zhí)行速度和真實(shí)芯片時(shí)鐘不是嚴(yán)格對(duì)應(yīng)的而且調(diào)試器單步執(zhí)行時(shí)SysTick計(jì)數(shù)會(huì)被暫停。這在模擬器里是正?,F(xiàn)象不是工程寫錯(cuò)了。解決確認(rèn)Target設(shè)置里的時(shí)鐘頻率與configTICK_RATE_HZ的換算關(guān)系一致做時(shí)間測(cè)量時(shí)用在線運(yùn)行F5而不是單步要驗(yàn)證精確延時(shí)時(shí)在任務(wù)里記錄一個(gè)tick計(jì)數(shù)器的前后差值來(lái)判斷別用墻上的秒表。6. 二次開發(fā)給命令行新增指令并驗(yàn)證調(diào)度行為的改造實(shí)戰(zhàn)6.1 注冊(cè)一條自定義命令三步操作改造第一步模仿內(nèi)置命令寫一個(gè)處理函數(shù)。命令的統(tǒng)一簽名是三個(gè)參數(shù)輸出緩沖、緩沖長(zhǎng)度、原始命令字串。假如我要加一條toggle命令讓一個(gè)共享計(jì)數(shù)加一/* 共享計(jì)數(shù)器跨任務(wù)訪問(wèn)一定要加volatile */ static volatile uint32_t ulSharedCounter 0; /* toggle命令處理函數(shù) */ static BaseType_t prvToggleCounterCommand(char *pcWriteBuffer, size_t xWriteBufferLen, const char *pcCommandString) { ulSharedCounter; sprintf(pcWriteBuffer, counter %lu\r\n, ulSharedCounter); return pdFALSE; /* pdFALSE表示一次性輸出完畢 */ }第二步把命令加進(jìn)xCommandTable數(shù)組注意數(shù)組元素的順序不影響匹配因?yàn)榻馕銎魇前疵种饌€(gè)查的{ counter, counter: 打印并遞增共享計(jì)數(shù)器\r\n, prvToggleCounterCommand },第三步重新編譯、進(jìn)模擬器敲counter驗(yàn)證輸出。這里有個(gè)小習(xí)慣新命令的調(diào)試先用help確認(rèn)它被成功注冊(cè)了再跑實(shí)際命令能省掉不少命令明明是好的但就是不生效的排查時(shí)間。6.2 用互斥鎖保護(hù)命令中的共享資源上面那個(gè)counter命令在demo的簡(jiǎn)單場(chǎng)景下夠用但一旦有另一個(gè)任務(wù)同時(shí)修改ulSharedCounter遞增操作就不是原子性的可能會(huì)丟更新。FreeRTOS里修正這個(gè)問(wèn)題用互斥鎖/* 互斥鎖句柄在main()里創(chuàng)建 */ SemaphoreHandle_t xCounterMutex; /* 受保護(hù)的命令處理 */ static BaseType_t prvCounterCommand(char *pcWriteBuffer, size_t xWriteBufferLen, const char *pcCommandString) { xSemaphoreTake(xCounterMutex, portMAX_DELAY); /* 拿鎖拿不到就阻塞 */ ulSharedCounter; sprintf(pcWriteBuffer, counter %lu\r\n, ulSharedCounter); xSemaphoreGive(xCounterMutex); /* 釋放鎖 */ return pdFALSE; }xSemaphoreTake的portMAX_DELAY參數(shù)表示無(wú)限等鎖實(shí)際產(chǎn)品里建議給一個(gè)有限超時(shí)避免低優(yōu)先級(jí)任務(wù)拿著鎖長(zhǎng)時(shí)間不釋放導(dǎo)致高優(yōu)先級(jí)任務(wù)永久阻塞。注意互斥鎖和二進(jìn)制信號(hào)量的區(qū)別互斥鎖帶優(yōu)先級(jí)繼承機(jī)制當(dāng)高優(yōu)先級(jí)任務(wù)等鎖時(shí)系統(tǒng)會(huì)把拿鎖的低優(yōu)先級(jí)任務(wù)臨時(shí)提到相同優(yōu)先級(jí)讓它盡快跑完釋放鎖這是解決優(yōu)先級(jí)反轉(zhuǎn)的標(biāo)準(zhǔn)做法。6.3 驗(yàn)證方法調(diào)度行為與臨界區(qū)正確性的雙視角確認(rèn)驗(yàn)證改造是否成功我習(xí)慣從兩個(gè)角度看。一是CLI視角連敲幾次counter看返回值是否嚴(yán)格遞增再開一個(gè)demo任務(wù)周期性地也去遞增這個(gè)計(jì)數(shù)器如果CLI輸出的數(shù)值出現(xiàn)跳變大概率是競(jìng)爭(zhēng)條件沒(méi)覆蓋干凈。二是調(diào)試器視角給prvCounterCommand的xSemaphoreTake前后各下一個(gè)斷點(diǎn)運(yùn)行模式下一口氣觸發(fā)命令觀察另一個(gè)任務(wù)是否在同一個(gè)時(shí)刻也在修改ulSharedCounter——如果兩個(gè)任務(wù)同時(shí)停留在臨界區(qū)內(nèi)的斷點(diǎn)說(shuō)明互斥鎖沒(méi)真正攔住這時(shí)候要檢查是不是用了信號(hào)量而不是互斥鎖或者鎖句柄被別的代碼誤刪了。從那以后我每次拿到新的FreeRTOS工程都會(huì)先過(guò)一遍FreeRTOSConfig.h里的堆棧和堆參數(shù)再跑一遍內(nèi)置命令建立基線數(shù)據(jù)最后才動(dòng)手改業(yè)務(wù)代碼。這個(gè)小習(xí)慣幫我避開了至少五次和今天講的一模一樣的坑也希望這篇筆記幫到你。本文還有配套的精品資源點(diǎn)擊獲取