:告別裸機While(1)的多任務實現)
裸機 while(1) 寫多了你會慢慢感覺到一種“越寫越擰巴”的別扭LED 要閃、按鍵要掃描、屏幕要刷新、串口還要收發(fā)全懟在一個大循環(huán)里加一個功能就要改一片狀態(tài)邏輯改著改著就把自己繞進去了。想直接上 FreeRTOS 吧又覺得項目就巴掌大配置任務棧、調度器、內存堆光鋪路就得折騰半天。如果你正好卡在這個尷尬點上那我建議你認真看看 QuarkTS 這個輕量級調度器。QuarkTS 的定位很有意思官方把它叫做“面向嵌入式系統的開源調度器”而不是完整操作系統。它沒有復雜的內存管理不需要你在工程里塞一堆驅動框架核心就是一個任務調度內核加上事件標志、消息隊列、軟件定時器這些常用的任務間同步原語。移植到一個新的單片機上核心工作量往往只有三件事配置好系統節(jié)拍定時器、實現好臨界區(qū)、再把定時器中斷服務函數接到內核的 tick 入口。對資源緊張的小 MCU 來說這東西比 FreeRTOS 輕太多了。這篇文章就圍繞這個標題展開從我實際移植的流程出發(fā)把 QuarkTS 到底解決什么問題、底層接口要動哪里、具體每步怎么配、以及那些網上文檔里不會寫的坑一次性拆開講清楚。適合正在從裸機往多任務過渡的開發(fā)者也適合手上芯片資源有限、塞不下完整 RTOS 的嵌入式從業(yè)者參考。1. 移植前先想明白QuarkTS 到底是解決哪個問題的1.1 裸機大循環(huán)真正做不好的三件事裸機程序的核心結構就是“初始化 超級循環(huán)”這個結構本身沒有問題很多簡單產品跑得也很穩(wěn)。但等你手里的功能模塊變多之后三個問題會越來越明顯。第一所有模塊共享同一個執(zhí)行流。一個任務里寫了阻塞延時其他所有任務全部跟著等待。比如串口發(fā)送函數如果用了輪詢方式等待發(fā)送完成標志在一個 9600 波特率的鏈路上發(fā)一串日志對面整個 LED 閃爍周期都會被拖亂。這是裸機程序里最典型的“一損俱損”。第二時間精度完全依賴主循環(huán)的循環(huán)體長度。你用delay_ms(20)做軟件定時看起來是 20ms但實際執(zhí)行時間等于“20ms 延時 當前循環(huán)里所有任務執(zhí)行時間之和”。今天加一個函數沒事明天再加一個傳感器讀取按鍵消抖的時間窗口就漂了很多玄學故障就是這么來的。第三中斷和主循環(huán)之間的數據交接沒有規(guī)范。中斷里置標志位、主循環(huán)里輪詢清標志這套做法本身沒問題但一旦涉及的共享變量多了變量有沒有加volatile、清標志和讀數據之間的先后順序合不合理全憑個人經驗。同一個團隊里兩個人寫出來的裸機代碼風格可能天差地別。QuarkTS 解決的正是這三點。它把每一個獨立的功能封裝成“任務”每個任務有自己獨立的上下文由調度器決定“誰在什么時候運行”。時間觸發(fā)任務嚴格按照配置的周期去執(zhí)行不受其他任務的影響事件觸發(fā)任務則由中斷或其他任務通過事件標志喚醒數據交接走標準的隊列原語。寫起來干凈后期維護也好做。1.2 它和 FreeRTOS 的本質差異決定了它的適用邊界剛開始接觸 QuarkTS 的人最容易問的一個問題是這玩意兒和 FreeRTOS 有什么區(qū)別區(qū)別還挺大我列個表說明。維度裸機超級循環(huán)QuarkTSFreeRTOS內核體積無很小核心代碼約幾千行較大完整功能更多動態(tài)內存不用默認可不使用任務控制塊靜態(tài)分配常用堆分配也可靜態(tài)任務模型無任務概念時間觸發(fā)/事件觸發(fā)/周期任務搶占式/協作式任務調度方式順序輪詢固定優(yōu)先級 可選時間觸發(fā)的綜合調度優(yōu)先級搶占 時間片輪轉隊列/信號量/事件自己寫內置用法簡單內置功能完整學習曲線無較低中等偏上適用場景邏輯極簡的小產品中小型多任務、資源受限 MCU復雜系統、強實時、多外設QuarkTS 沒有內存保護、沒有設備驅動框架、沒有用戶態(tài)和內核態(tài)的概念。它更準確的叫法其實是一個“調度核心”目標是在盡量小的資源開銷下把多任務調度這件事做扎實。你在 QuarkTS 里不會看到線程、信號量、互斥鎖滿天飛的那套完整 RTOS 語義取而代之的是事件觸發(fā)、時間觸發(fā)、隊列、信號量這些足夠日常用的原語。所以適用邊界也就清晰了如果你的單片機只有幾 KB 內存外設模塊不超過十個希望快速實現多任務又不愿意被完整 RTOS 的配置細節(jié)淹沒QuarkTS 是很好的中間選擇。如果項目涉及復雜的文件系統、網絡協議棧、多級優(yōu)先級嵌套那還是老老實實上 FreeRTOS 或更完整的 RTOS別在調度器這一層省事。2. 移植前要看清的本質QuarkTS 到底要求你提供哪些底層能力2.1 硬件依賴也就三樣東西移植 QuarkTS 不需要特定的單片機型號它本身就設計成可以跑到任意有 C 編譯器的 MCU 上。從實際角度講硬件上有三樣東西是必需的。第一一個穩(wěn)定的周期定時器中斷。這是 QuarkTS 的“心跳”所有時間觸發(fā)任務、超時判斷、隊列阻塞都依賴這個 tick。我習慣把系統 tick 配成 1ms這個粒度對大多數傳感器讀取、按鍵掃描、LED 控制都夠用。如果你有對時間精度要求特別高的模塊比如飛控的角速度積分或者音頻采樣那 1ms 就不夠了需要把 tick 周期調短到 100us 級別但代價是調度器本身的切換開銷占比會變大任務切換頻繁時 CPU 占用率會明顯升高。第二開關中斷的能力。QuarkTS 內部很多操作需要原子性比如就緒隊列的插入和移除必須先關中斷再操作操作完再恢復。在 Cortex-M 內核上一般通過改寫 PRIMASK 或 BASEPRI 寄存器實現。第三一個足夠大的定時器計數器用來給任務提供周期計時。通常 MCU 自帶的 SysTick 或者通用定時器就夠了QuarkTS 本身不需要外部硬件輔助。選平臺的時候我的建議是第一步先別管性能挑一塊你手上最熟悉的開發(fā)板把串口打印和定時器中斷都驗證過的那塊板子拿來移植。移植內核最怕的是“底層有問題但不知道問題在哪”你在熟悉的環(huán)境里做可以把變量壓縮到最小。2.2 QuarkTS 的“移植層”到底只有哪幾個函數打開 QuarkTS 源碼目錄你會看到它把平臺相關的部分單獨隔離出來了。常見結構是一個quarkts.c和一個quarkts.h作為內核主體另外留了幾個由用戶實現的接口或者叫移植回調。不同小版本的接口名稱會有些出入但本質就四類一是 tick 中斷服務入口。定時器中斷觸發(fā)后你需要在這個中斷服務函數里調用內核的 tick 處理函數告訴內核“一個時間片過去了”。不同版本的 QuarkTS 這個函數叫法可能是qOS_Tick()也可能是qTick()具體以你下載到的頭文件為準。這個函數內部要做就緒隊列更新、任務切換檢查所以你在中斷服務函數里除了調用它盡量不要干別的耗時的活。二是臨界區(qū)進入和退出。QuarkTS 在修改就緒隊列、任務狀態(tài)時需要短暫地關掉中斷保證操作過程中不會被 tick 打斷。你通常只需要寫兩組宏分別是進入臨界區(qū)和退出臨界區(qū)然后在quarkts.h的移植配置區(qū)里把它映射好。三是可選的任務切換鉤子。在 Cortex-M 這類需要顯式保存寄存器上下文的內核上任務切換通常借助 PendSV 異常來完成。QuarkTS 本身自帶了一些通用切換代碼但不同編譯器、不同優(yōu)化等級下你可能需要微調一下內聯匯編或者棧對齊方式。四是掛起和解掛調度器的能力。當任務進入一些長時間阻塞的等待狀態(tài)時調度器可能要臨時把中斷里的切換動作掛起來避免反復做無意義的上下文切換。這個功能有的版本會開放給用戶有的版本內部自己處理移植時留意一下宏開關即可。2.3 以某 Cortex-M 內核芯片為例底層映射怎么做我這里用一塊 Cortex-M0 內核的國產開發(fā)板作為例子說明。Cortex-M0 沒有 BASEPRI 寄存器臨界區(qū)只能靠 PRIMASK 全關中斷來實現代碼很簡單/* port.c 示例 */ #include quarkts.h #include core_cm0.h void port_SysTick_Init(void) { /* 配置 SysTick 為 1ms 中斷時鐘源和處理頻率請按實際計算 */ SysTick_Config(SystemCoreClock / 1000); } void SysTick_Handler(void) { /* 進入內核 tick 處理 */ qOS_Tick(); }臨界區(qū)的宏可以這樣映射/* port.h 示例 */ #define QK_CRIT_ENTER() __disable_irq() #define QK_CRIT_EXIT() __enable_irq()內核配置區(qū)里把這個宏關聯進去即可。流程上就三步初始化定時器、定義 tick 中斷、實現臨界區(qū)。后面再把內核初始化、配置結構體填好QuarkTS 就能在你的單片機上跑起來了。這里多說一句Cortex-M3 和 M4 內核的同學臨界區(qū)建議用 BASEPRI 方式而不是 PRIMASK 全關中斷因為 BASEPRI 可以只屏蔽低于某個優(yōu)先級的中斷讓真正緊急的中斷在臨界區(qū)期間也能響應。這是后面調實時性時非常關鍵的一點。3. 實操過程從空工程到雙任務跑起來3.1 把源碼組織進工程的小建議QuarkTS 源碼可以從代碼托管平臺下載找到發(fā)布版本的壓縮包。拿到手以后不要直接把整個目錄全部塞進工程而是只復制真正用到的文件。我的習慣工程結構是這樣my_quarkts_demo/ ├── core/ │ ├── quarkts.c │ ├── quarkts.h │ └── qassert.h ├── port/ │ ├── port.c │ └── port.h ├── app/ │ ├── main.c │ └── app_task.c └── mcu/ └── (你的芯片啟動文件、外設庫等)這樣區(qū)分的好處是core目錄是內核源碼永遠不去改它port目錄放平臺相關代碼app目錄是業(yè)務邏輯。以后內核版本升級時直接替換core目錄里的文件就行自己的業(yè)務代碼不受影響。我見過有人把 QuarkTS 的源碼和自己的業(yè)務邏輯混在一個文件里改最后升級源碼時痛苦不堪不建議這樣做。3.2 系統配置結構體這里填錯最容易出怪問題QuarkTS 啟動之前一般要求填充一個全局配置結構體告訴內核你打算創(chuàng)建多少個任務、多少個隊列、多少個優(yōu)先級等級。不同版本的配置項名稱會有差異但核心參數逃不出下面這些。配置項推薦值說明最大任務數視業(yè)務而定建議 8~16一次性把內核可能用到的任務池開夠優(yōu)先級等級數8 到 16不是越高越好等級越多調度鏈表越復雜最大事件組數業(yè)務需要每個事件組可以包含多個事件位最大消息隊列數業(yè)務需要通常給 4~8 個就夠tick 周期1ms具體以定時器配置為準是否啟用搶占是需要實時響應就開邏輯極簡可以關配置項設置偏小了運行時會直接斷言失敗或者任務創(chuàng)建失敗設置偏大了RAM 會白占。你說不清楚到底要多少的時候先用一個大點的值跑通功能再逐步調小觀察內存占用。這個習慣能省掉很多調試時間。配置結構體的填充代碼大致長這樣qOS_Cfg_t app_qos_cfg; void app_qos_config_init(void) { app_qos_cfg.task_pool_size 16; app_qos_cfg.priority_levels 8; app_qos_cfg.use_preemption true; /* 其他成員按你的頭和實際需求補全 */ }配置完成以后調用內核初始化然后啟動調度器。順序一定要是初始化、配置、然后qOS_Run()進入調度循環(huán)qOS_Run()之后你的入口函數就不需要返回了。3.3 創(chuàng)建第一個任務LED 閃爍驗證 tick 鏈路我用一個最經典的 LED 閃爍任務來驗證整個移植鏈路是否通暢。第一版功能不求多只求能證明“內核收到 tick、任務被調度、任務能周期運行”。#include quarkts.h #include port.h qTCB_t tcb_led; qTCB_t tcb_printf; void task_led_run(qEvent_t evt, qTCB_t *tcb) { static uint8_t level 0; level !level; GPIO_WritePin(LED_PIN, level); } void task_printf_run(qEvent_t evt, qTCB_t *tcb) { /* 這里假設每 500ms 打印一次 */ printf(tick alive, led level toggled\n); } int main(void) { /* 1. 先初始化基礎硬件包括 tick 定時器 */ board_init(); port_SysTick_Init(); /* 2. 初始化內核 */ qOS_Init(); app_qos_config_init(); qOS_Config(app_qos_cfg); /* 3. 創(chuàng)建任務 參數里的周期值在不同版本里含義略有區(qū)別 原則上是“間隔多少個 tick 執(zhí)行一次”。 優(yōu)先級數字越小優(yōu)先級越高。 */ qTask_Create(task_led_run, 5, 20, tcb_led); qTask_Create(task_printf_run, 6, 500, tcb_printf); /* 4. 啟動調度器不再返回 */ qOS_Run(); return 0; }上面代碼里我用的是語義化的 QuarkTS 風格 API具體函數名和參數順序要對照你下載到的quarkts.h去看。第一次調試千萬不要管那么多先把 LED 任務建出來把周期設為 20 個 tick也就是用 1ms tick 算大概 20ms 閃爍一次。燒錄以后觀察 LED 是不是以可辨認的周期翻轉。這一步跑通說明 tick 中斷、內核調度、任務上下文切換這一整條鏈路已經打通了。這里我想強調一個容易誤解的點QuarkTS 的時間觸發(fā)任務它的“周期”通常是指任務被置為就緒狀態(tài)的時間間隔而不是從任務開始執(zhí)行到下次執(zhí)行之間的間隔。也就是說如果你在一個周期任務里執(zhí)行了很長時間比如超過了一個周期那任務的執(zhí)行起點會繼續(xù)按原始節(jié)拍移動表現為“本次還沒跑完下一次就緒時間又快到了”。這與裸機delay的語義有本質區(qū)別移植幾周后你調業(yè)務邏輯時這個概念必須刻在腦子里。3.4 用串口任務驗證多任務下互不拖累第二個任務我經常會做成周期打印任務每 500ms 打印一行字符。它的作用是驗證兩個任務能并行跑并且一個任務執(zhí)行耗時不會污染另一個任務的周期。調試時有個實用技巧在 LED 任務和打印任務里各放一個計數器每執(zhí)行一次加一然后用調試器查看兩個計數器的數值比例。如果你配置的周期比是 1:25那么最終計數器比例應該非常接近 1:25。如果比例明顯不對說明 tick 配置或者任務周期參數有偏差回頭檢查配置結構體。串口打印任務在內核剛移植成功時非常有價值但后面調正式業(yè)務時要注意不要在任務函數里頻繁調用printf因為重定向到串口的打印往往是阻塞式的一個 115200 波特率下打印幾十個字符就要耗費好幾毫秒嚴重時會把時間觸發(fā)任務的節(jié)奏拖亂。我的習慣是任務里只置一個標記主循環(huán)或專門的打印任務再統一輸出。4. 從雙任務到真實應用事件、隊列、軟定時器是怎么用的4.1 事件標志中斷里喚醒任務的標準姿勢QuarkTS 的典型事件觸發(fā)場景是這樣的外部按鍵按下觸發(fā) GPIO 中斷中斷服務函數里不適合做復雜處理于是置一個事件標志通知某個任務去執(zhí)行按鍵消抖和邏輯處理。事件標志的使用思路和裸機里的“標志位”很像區(qū)別在于它是內核管理的對象可以對任務進行精準喚醒而不是靠主循環(huán)輪詢。示意如下qEvent_t key_event; void EXTI_IRQHandler(void) { /* 清除中斷標志等常規(guī)操作 */ qEvt_Signal(key_event); } void task_key_run(qEvent_t evt, qTCB_t *tcb) { while (1) { /* 等待事件阻塞在這里 */ qEvt_Wait(key_event, QWAIT_FOREVER); do_key_scan_and_handle(); } }從裸機過來的朋友請注意事件等待任務里的while (1)結構和 QuarkTS 時間觸發(fā)任務不同。事件觸發(fā)任務如果從函數返回通常會被內核認為任務結束或者重新掛起具體行為取決于版本。你要養(yǎng)成習慣事件觸發(fā)任務寫成“等待事件→處理→再等待”的循環(huán)結構這個結構在裸機里很別扭但在事件驅動模型里非常自然。4.2 消息隊列任務間數據傳遞的正確做法事件標志適合傳遞“發(fā)生了沒有”這種信息但如果要傳遞“發(fā)生了什么數據”就不夠了。比如串口接收中斷把一幀數據收進來交給處理任務解析就需要消息隊列。qQueue_t uart_rx_queue; void UART_RX_IRQHandler(void) { uint8_t byte UART_ReadByte(); /* 隊列滿時返回失敗這里做簡單丟棄處理 */ qQueue_Send(uart_rx_queue, byte, sizeof(byte)); } void task_uart_process(qEvent_t evt, qTCB_t *tcb) { uint8_t data; while (1) { if (qQueue_Receive(uart_rx_queue, data, sizeof(data), QWAIT_FOREVER)) { handle_rx_frame(data); } } }中斷里調用qQueue_Send時要注意一點如果隊列滿這個函數的行為取決于版本和配置有的直接返回失敗有的可能會在中斷里做等待。等待這個動作在中斷上下文里是大忌會直接把系統卡住。所以無論內核怎么處理你的業(yè)務代碼里都應該在發(fā)送前檢查隊列是否有空間或者做好“滿則丟棄”的策略。4.3 軟件定時器比任務周期更靈活的控制手段有些場景不適合用周期任務比如“延時 300ms 后再做某個動作”“超時 200ms 沒收到數據就算通信失敗”。這類一次性或單次觸發(fā)的定時需求QuarkTS 提供了軟件定時器原語。軟件定時器的底層仍然是 tick 計數但使用上比創(chuàng)建一個完整任務輕量得多。典型的創(chuàng)建方式是在初始化階段注冊一個定時器回調函數啟動時設置超時 tick 數。到了時間后內核回調你的函數你可以在里面置事件標志、發(fā)隊列、或者直接執(zhí)行短邏輯。注意回調函數是在內核上下文里執(zhí)行的不能阻塞不能做耗時操作比較重的操作應該在回調里“通知任務去做”。5. 移植過程中最常翻車的點問題排查與調優(yōu)經驗5.1 任務完全不跑十有八九是 tick 鏈路斷了移植 QuarkTS 之后最打擊人的現象就是程序跑起來LED 不閃串口不打印一切看起來都沒反應。查這一步問題要按鏈路順序來。先確認定時器中斷有沒有真正觸發(fā)。在中斷服務函數里加一個 IO 翻轉用示波器看波形如果 IO 不翻轉說明定時器配置有問題內核喊破嗓子也沒用。確認中斷觸發(fā)之后再確認中斷服務函數里有沒有調用 QuarkTS 的 tick 處理函數。有些用戶移植時把中斷服務函數名字寫錯了比如平臺庫要求的名字和編譯器啟動文件里的名字不一致中斷根本沒掛上。另外常見的一個坑是定時器中斷里忘了清除中斷標志。SysTick 一般不需要手動清標志但通用定時器基本都要寫清除寄存器。標志沒清中斷會反復進入QuarkTS 內部狀態(tài)被連續(xù)推進任務表現就會異常比如周期明顯變短或者優(yōu)先級錯亂。5.2 搶占調度不生效先查優(yōu)先級配置再查臨界區(qū)長度如果你配置的任務已經有高低優(yōu)先級之分但高優(yōu)先級任務并沒有搶占低優(yōu)先級任務先從兩個方向排查。第一確認內核的搶占調度開關已經打開。有些版本默認是協作式調度高優(yōu)先級任務必須等低優(yōu)先級任務主動讓出 CPU 才有機會運行此時任務的執(zhí)行順序還是“排隊”的感知不到搶占。第二確認 tick 中斷可以打斷當前任務。搶占的前提是 tick 中斷在高優(yōu)先級任務就緒后能打斷正在運行的低優(yōu)先級任務。如果你的低優(yōu)先級任務里有一段比較長的臨界區(qū)而且臨界區(qū)里把中斷關了那這段臨界區(qū)里發(fā)生的任何高優(yōu)先級任務就緒事件都要等臨界區(qū)退出后才能被響應。這本質是優(yōu)先級被臨界區(qū)延后了不是內核的 bug但容易誤判。排查時可以設計一個測試低優(yōu)先級任務里做個 10ms 的忙等高優(yōu)先級任務每 2ms 翻轉一次 IO。如果 IO 波形在這 10ms 內出現中斷缺失說明有臨界區(qū)過長或者中斷被屏蔽的問題。逐段注釋業(yè)務代碼很快就能定位到是哪一段關過中斷。5.3 時間觸發(fā)任務抖動先懷疑打印和模塊執(zhí)行時間時間觸發(fā)任務最理想的運行情況是“每 20ms 在任何情況下都準點啟動”但很多因素會造成抖動。最常見的是任務內部執(zhí)行時間太長導致本次還沒跑完下一次 tick 就緒標志已經來了。這時候任務會表現為“連續(xù)執(zhí)行”“周期錯亂”看起來就像系統控制不住自己。解決思路有兩個方向。一是給任務內耗時的模塊做拆分把重活拆給多個任務級聯處理二是適當調大周期任務的周期值給任務留足執(zhí)行余量保證在一個周期內完成任務且有空閑余量。還有一個容易忽視的因素任務里的調試打印。調試階段用串口打印特別爽但一個printf可能占用幾十萬個時鐘周期把本來美如畫的時間觸發(fā)表完全打亂。我的習慣是調實時性的時候把打印任務單獨做成低優(yōu)先級任務或者干脆用調試器在線看變量別讓打印干擾時間敏感任務。5.4 跑一段時間隨機死機查棧溢出和內存越界內核本身資源開銷很小但如果業(yè)務任務函數里使用了較大的局部數組或者調用了遞歸函數任務棧就很容易爆掉。QuarkTS 的任務棧內存可以來源于靜態(tài)數組也可以來源于配置好的內存池棧溢出不一定馬上崩潰往往是某次臨界區(qū)嵌套或者長中斷之后突然復位排查起來很頭疼。我的排查順序是這樣的先看編譯器的鏈接 MAP 文件確認 QuarkTS 的任務棧區(qū)域分配在哪個段以及總 RAM 占用率是多少。再在任務函數入口放一個棧高水位標記周期性檢查任務棧剩余空間。如果發(fā)現某個任務棧剩余空間一直在下降說明這個任務的局部變量太大或者調用鏈太深需要加大棧配置或者優(yōu)化代碼結構。另外要特別注意不要在任務函數里動態(tài)分配大塊內存不要在中斷里調用可能分配內存的函數。QuarkTS 默認不依賴 malloc 的輕量特征一旦你自己開了動態(tài)內存就可能引入堆碎片問題表現為“跑幾天才崩一次”這種問題比棧溢出更隱蔽。5.5 一張自檢清單移植前逐項打鉤檢查項做法tick 周期是否滿足業(yè)務精度用定時器實測別依賴理論計算定時器中斷標志是否清除檢查芯片手冊確認清標志寄存器中斷服務函數名是否匹配對比啟動文件別寫錯名字臨界區(qū)是否嵌套安全確認退出時能正確恢復原狀態(tài)優(yōu)先級數量是否覆蓋所有任務只多不少避免運行期斷言失敗時間觸發(fā)任務是否有足夠執(zhí)行余量周期要大于任務最壞執(zhí)行時間任務內是否用了阻塞式 printf實時性敏感場景下嚴禁棧高水位是否監(jiān)控上線前至少做 72 小時壓力測試如果你遇到的問題不在上面列表里還有一個笨但有效的辦法把業(yè)務任務一個個注釋掉只剩 LED 心跳任務看問題是否復現。如果復現問題在內核移植配置層面如果不復現了問題在某一個業(yè)務任務里。這種二分排除法在嵌入式排障中永遠不過時。移植 QuarkTS 這件事本質上是在一個已經足夠好用的裸機循環(huán)之上增加了一層“調度維度”。一旦這條鏈路打通你會發(fā)現寫多任務 Demo 的速度明顯變快很多以前靠狀態(tài)機硬啃的邏輯現在拆成幾個事件觸發(fā)任務就清清楚楚了。我個人實際體會是不要一上來就追求把所有業(yè)務都塞進 QuarkTS先花半天把雙任務和串口打印鏈路跑通再逐步把按鍵、顯示、通信模塊一個一個遷進來。每一步都有可觀測的現象每遷完一個模塊就打包驗證一次這個節(jié)奏比一次性大改造穩(wěn)妥得多。后續(xù)如果想把 CPU 占用率、任務調度延遲這些指標做可視化也可以考慮接一個輕量級調試工具但那是移植成功之后的故事了。