戰(zhàn):從架構(gòu)到任務(wù)切換)
簡介本資源是面向嵌入式初學(xué)者與進(jìn)階開發(fā)者的 TI C2000 系列 MCU FreeRTOS 移植實(shí)踐項(xiàng)目聚焦于 C28x 架構(gòu)如 F28377S、F28034、F28069的實(shí)時操作系統(tǒng)落地難題解決傳統(tǒng)裸機(jī)開發(fā)向多任務(wù)調(diào)度演進(jìn)中的移植適配、中斷管理、時鐘配置等核心痛點(diǎn)。壓縮包共 163 個文件含 27 個 C 源碼覆蓋 tasks、queue、timers、event_groups 等 FreeRTOS 內(nèi)核模塊、32 個頭文件提供芯片外設(shè)與內(nèi)核接口定義、16 個 CMD 鏈接腳本適配不同 Flash/RAM 分布、10 個 CCXML 調(diào)試配置及多個 CCS 項(xiàng)目工程文件.project/.cproject/.ccsproject整體僅 383KB輕量易導(dǎo)入。已有 134 人學(xué)習(xí)下載項(xiàng)目附帶可運(yùn)行的基礎(chǔ)測試用例與完整移植層代碼含 porASM.asm 啟動匯編與 SD_SPI_Initialization 等外設(shè)協(xié)同示例并明確標(biāo)注 controlSUITE 工具鏈依賴與 CCS v6.1.2 編譯環(huán)境要求便于讀者快速復(fù)現(xiàn)、對比調(diào)試、遷移至其他 TMS320 器件。 我最早在 C2000 上跑 FreeRTOS是被實(shí)際項(xiàng)目逼的。那是一個基于 TMS320F28379D 的伺服驅(qū)動器項(xiàng)目電流環(huán) 10kHz 中斷、FOC 計算、CANopen 通信、參數(shù)標(biāo)定、故障記錄、LED 狀態(tài)燈、按鍵調(diào)試全擠在一塊板子上。裸機(jī)的 while 大循環(huán)里塞滿了一個又一個狀態(tài)機(jī)每次加功能都要小心翼翼生怕動到別的模塊。當(dāng)時擺在我面前的選項(xiàng)就是兩個繼續(xù)把裸機(jī)狀態(tài)機(jī)重構(gòu)一遍或者把 FreeRTOS 移植進(jìn)來。我選了后者原因是裸機(jī)狀態(tài)機(jī)的復(fù)雜度已經(jīng)接近失控。等我真的把 FreeRTOS 移上去以后才發(fā)現(xiàn)這條路沒有想象中難但也沒想象中簡單。C2000 和 STM32 這類常規(guī) MCU 有很多底層差異移植過程中有不少細(xì)節(jié)要靠自己趟。這篇就把我實(shí)際移植和后續(xù)開發(fā)過程中的經(jīng)驗(yàn)完整整理一下包括移植前要想清楚的架構(gòu)問題、工程文件怎么搭、任務(wù)切換的底層原理以及一個完整的數(shù)據(jù)采集示例項(xiàng)目。如果你正在往 C2000 系列上搞 FreeRTOS希望這篇能幫你少走幾周彎路。1. C2000 上跑 FreeRTOS 之前先想清楚這四件事1.1 C2000 不是 ARM Cortex-M它的調(diào)度硬件完全不一樣C2000 是 TI 的實(shí)時控制 MCU 系列內(nèi)核是 C28x不是 Cortex-M。盡管它也叫 MCU但它的設(shè)計目標(biāo)是從 ADC 采樣到 PWM 輸出之間的延遲可預(yù)測所以它的架構(gòu)和 ARM 差別很大。具體差異體現(xiàn)在三個地方。第一上下文切換機(jī)制不同。ARM Cortex-M 依賴 PendSV 和 SysTick 這兩個特殊系統(tǒng)異常來做任務(wù)切換C28x 沒有這兩個東西它的任務(wù)切換要靠軟件陷阱指令TRAP和 CPUTimer 中斷來觸發(fā)。第二內(nèi)存模型不同。Cortex-M 的寄存器是 32 位棧按字節(jié)尋址C28x 是 16 位地址尋址的 DSP 類內(nèi)核棧單元寬度、內(nèi)存段的劃分方式和 ARM 完全不一樣。C2000 的內(nèi)存被拆成 M0、M1、L0-L7、GS0-GS15 等若干段不是簡單的一個大 RAM。第三中斷系統(tǒng)不同。C2000 的外設(shè)中斷通過 PIE外設(shè)中斷擴(kuò)展模塊管理中斷向量表是固定的槽位不像 ARM 那樣靈活地掛在 NVIC 上。這也解釋了為什么你沒法直接把 STM32 的 port.c 拿過來用。C2000 必須用獨(dú)立的移植版本而且編譯器必須是 TI 的 CGT 工具鏈或者新版 clang 編譯器GCC 移植版本沒法直接用。1.2 你想用 FreeRTOS 解決什么問題決定了移植方案我見過不少人剛接觸 C2000 就條件反射地要上 FreeRTOS理由是別人都在用。這個理由不成立。在移植之前我通常會先問自己三個問題。項(xiàng)目里有沒有多個實(shí)時性要求不同、并且需要并行執(zhí)行的邏輯比如通信、UI、狀態(tài)機(jī)、故障處理。如果項(xiàng)目就一個簡單的單循環(huán)控制程序那 FreeRTOS 帶來的復(fù)雜度可能比收益還大。有沒有硬實(shí)時任務(wù)比如電流環(huán)、電壓環(huán)。如果有這些任務(wù)大概率還是要留在中斷里FreeRTOS 管不了也不該管。我在實(shí)際產(chǎn)品中見過有人把 20kHz 的 FOC 電流環(huán)放進(jìn)一個最高優(yōu)先級任務(wù)里結(jié)果切換抖動導(dǎo)致電流波形毛刺這就屬于對 RTOS 調(diào)度延遲沒有敬畏心的典型翻車現(xiàn)場。代碼復(fù)雜度是不是已經(jīng)超過裸機(jī)狀態(tài)機(jī)能承受的規(guī)模這個問題最直接如果裸機(jī) while 循環(huán)里的狀態(tài)機(jī)已經(jīng)膨脹到改一個功能要動好幾個地方那就是 RTOS 該上場的時候了。如果第一個和第三個問題占主導(dǎo)FreeRTOS 是合適的。如果只是想上 RTOS 圖個心里安穩(wěn)那它只會給你帶來額外的調(diào)度延遲和調(diào)試負(fù)擔(dān)。1.3 C2000 上跑 RTOS 的最大收益任務(wù)間隔離和通信原語裸機(jī)開發(fā)最常見的問題是一個模塊的數(shù)據(jù)要傳遞給另一個模塊要么用全局變量加標(biāo)志位要么用回調(diào)函數(shù)。模塊一多數(shù)據(jù)流就變得非?;靵y尤其是電機(jī)控制這類應(yīng)用信號鏈路很長ADC 采樣結(jié)果、電流環(huán)輸出、速度環(huán)輸出、上位機(jī)指令每一個環(huán)節(jié)都涉及數(shù)據(jù)交換。FreeRTOS 提供隊(duì)列、信號量、互斥量、事件組這些現(xiàn)成的任務(wù)間通信原語而且有完整的阻塞語義。比如采集任務(wù)把數(shù)據(jù)扔進(jìn)隊(duì)列發(fā)送任務(wù)在隊(duì)列上阻塞等待有數(shù)據(jù)就自動喚醒發(fā)送。這種數(shù)據(jù)流的組織方式比裸機(jī)里到處飛的全局變量清晰得多。這個收益在 C2000 上表現(xiàn)尤其明顯因?yàn)?C2000 的控制類應(yīng)用數(shù)據(jù)流往往很明確ADC 采樣控制算法計算PWM 更新外圍掛通信和診斷。用 FreeRTOS 的隊(duì)列和信號量恰好能把這條數(shù)據(jù)流切成一個個獨(dú)立模塊。1.4 明確邊界FreeRTOS 能做什么不能做什么FreeRTOS 是軟實(shí)時調(diào)度器它保證的是高優(yōu)先級任務(wù)優(yōu)先獲得 CPU但并不保證某個中斷后多少納秒內(nèi)一定執(zhí)行某個代碼。任務(wù)調(diào)度存在延遲延遲大小取決于當(dāng)前中斷狀態(tài)、臨界區(qū)長度、上下文切換開銷。C2000 的強(qiáng)項(xiàng)是硬實(shí)時外設(shè)和可預(yù)測的中斷響應(yīng)。所以 FreeRTOS 在 C2000 上的正確用法是外設(shè)中斷負(fù)責(zé)采樣與控制FreeRTOS 負(fù)責(zé)上層調(diào)度而不是把所有控制算法都搬進(jìn)任務(wù)里。我在第一個版本的架構(gòu)里也犯過這個錯后來明確改成強(qiáng)實(shí)時代碼留中斷軟實(shí)時和通信代碼放任務(wù)。這個邊界一開始就劃清楚省得后面推倒重來。2. 環(huán)境與工程骨架把 FreeRTOS 干凈地裝進(jìn) CCS 工程2.1 工具鏈選型CCS 與編譯器版本我使用的環(huán)境是 CCSCode Composer Studio12.x 代以上版本配合 TI C2000 CGT 編譯器外設(shè)驅(qū)動庫用 C2000Ware較新版本叫 C2000Ware SDK5.x。FreeRTOS 官方 Kernel 是純 C 源碼只要編譯器支持 C99 基本都能編譯。但 C2000 的移植 port 是和編譯器、匯編器綁定的這個非常關(guān)鍵。FreeRTOS 官方 Kernel 倉庫里portable 目錄下保留了 C2000 的移植文件具體位置通常在 CCS 或 ThirdParty 子目錄里面向 TI CGT 編譯器。如果你用 CCS 自帶編譯器直接拿這套就行。C2000Ware 里也提供了不少基于 FreeRTOS 的外設(shè)例程路徑隨版本變化一般是搜索 freertos 目錄就能找到。C2000Ware 的例程依賴 driverlib 封裝適合對 C2000 外設(shè)還不太熟的時候直接抄作業(yè)。我的建議是如果你不熟 C2000 外設(shè)先打開 C2000Ware 里的 FreeRTOS 例程如果外設(shè)已經(jīng)比較熟了用官方 Kernel 移植加 driverlib 的組合更清爽依賴更少可移植性更好。2.2 文件清單與 FreeRTOS 內(nèi)核裁剪一個最小的 C2000 FreeRTOS 移植工程需要以下文件FreeRTOS 內(nèi)核源碼tasks.c、list.c、queue.c這三個是必須的。timers.c和event_groups.c可按需裁剪不用的話就別往工程里加。內(nèi)核頭文件FreeRTOS.h 和一大堆 include 下的頭文件直接把整個 include 目錄加進(jìn)編譯器的 include path 最省事。移植層文件port.c、portasm.asm、portmacro.h這三個文件是 C2000 移植的核心。工程根目錄下的FreeRTOSConfig.h這是配置核心所有內(nèi)核裁剪宏都集中在這里。FreeRTOSConfig.h里常見的重點(diǎn)配置項(xiàng)我用順手之后基本固定成下面這組值配置宏建議值含義configUSE_PREEMPTION1搶占式調(diào)度configCPU_CLOCK_HZ200000000ULCPU 主頻按你的實(shí)際系統(tǒng)時鐘配置configTICK_RATE_HZ1000tick 頻率默認(rèn) 1ms 一個 tickconfigMAX_PRIORITIES5優(yōu)先級數(shù)量夠用就行不要貪多configMINIMAL_STACK_SIZE128最小任務(wù)棧單位是字不是字節(jié)configTOTAL_HEAP_SIZE8192FreeRTOS 堆大小按實(shí)際需求調(diào)整configCHECK_FOR_STACK_OVERFLOW2使能棧溢出檢測調(diào)試期必須開configUSE_IDLE_HOOK1使能空閑任務(wù) hooktick 頻率這個參數(shù)要特別注意。控制類項(xiàng)目不要一上來就選 1000Hz如果你的任務(wù)周期是 500us 甚至 100us那 tick 中斷至少得是 2kHz 以上否則調(diào)度器的定時精度不夠。但 tick 頻率越高CPU 在上下文切換上浪費(fèi)的時間越多。我一般先按 1ms 設(shè)計跑通以后按實(shí)際時間需求重新調(diào)。2.3 內(nèi)存布局linker cmd 文件里給 FreeRTOS 留出空間C2000 的內(nèi)存模型和 ARM 不一樣RAM 被拆成多個段不同型號差異很大。所以 linker cmd 文件必須針對具體型號來改不能隨便拿別人的抄。FreeRTOS 的任務(wù)棧、TCB、隊(duì)列控制塊都需要從 RAM 里分配。在 cmd 文件里通常要給這些段分配空間。我習(xí)慣單獨(dú)劃一塊內(nèi)存給 FreeRTOS 堆比如在 cmd 文件里加入.rtos_heap : RAML4, PAGE 1然后在 FreeRTOSConfig.h 里把configTOTAL_HEAP_SIZE設(shè)成不超過這塊段的容量。這里特別提醒C2000 不同型號的 RAM 容量差別很大。比如 F280049C 的 RAM 總共幾十 KBF28379D 能到 200KB 以上。如果你的任務(wù)棧申請失敗第一步不是調(diào)棧大小而是先看 cmd 文件里給堆分配的段夠不夠。我以前就遇到過 xTaskCreate 返回 pdFAIL排查半天發(fā)現(xiàn)是 .rtos_heap 段只有 1KB而 configTOTAL_HEAP_SIZE 寫的 8KB早就超了。2.4 編譯選項(xiàng)與優(yōu)化級別TI CGT 編譯器優(yōu)化級別我長期用 -O2不建議盲目開 -O3。C2000 編譯器在某些 -O3 場景下對volatile訪問和中斷標(biāo)志位操作的優(yōu)化很激進(jìn)容易觸發(fā)移植層匯編和 C 代碼配合的邊界問題這類問題調(diào)試起來極其痛苦。另一個容易忽略的坑是浮點(diǎn)編譯選項(xiàng)。C2000 有 FPU32浮點(diǎn)運(yùn)算會涉及浮點(diǎn)狀態(tài)寄存器。如果 FreeRTOS 內(nèi)核代碼和應(yīng)用代碼的浮點(diǎn)編譯選項(xiàng)不一致上下文切換時可能丟失浮點(diǎn)上下文導(dǎo)致計算錯誤。我現(xiàn)在的做法是所有工程文件統(tǒng)一用同一套浮點(diǎn)編譯選項(xiàng)不改局部文件的設(shè)置。3. 移植核心三連時鐘、棧幀與上下文切換3.1 tick 時鐘用 CPUTimer0 做系統(tǒng)節(jié)拍C2000 上的 tick 定時器最自然的選擇是 CPU Timer0。這個定時器直接掛在 CPU 上中斷固定映射到 PIE 的 INT1.7路徑最短延遲最小。還有一個好處是它不占 ePWM 或 eCAP 這些控制外設(shè)資源控制類應(yīng)用里這些外設(shè)往往都被占滿了。初始化代碼大致如下以 C2000Ware driverlib 為例不同版本函數(shù)名可能有細(xì)微差別以你實(shí)際安裝的版本為準(zhǔn)#define TICK_PERIOD_US 1000 // 1ms tick void vPortSetupTimerInterrupt(void) { // 1. 初始化 CPUTimer0時基分頻 1基準(zhǔn) 200MHz CPUTimer_init(CPUTIMER0_BASE, CPUTIMER_CLOCK_DIVIDER_1, 200000000); // 2. 設(shè)置周期200MHz, 1ms - 200000 個時鐘周期 CPUTimer_setPeriod(CPUTIMER0_BASE, 200000 - 1); // 3. 使能自動重載先停止定時器 CPUTimer_enableAutoReload(CPUTIMER0_BASE); CPUTimer_stopTimer(CPUTIMER0_BASE); // 4. 使能 PIE 中斷 Interrupt_enable(INT_TIMER0); // 5. 啟動定時器 CPUTimer_startTimer(CPUTIMER0_BASE); }周期計算公式很簡單系統(tǒng)時鐘頻率 F分頻系數(shù) D目標(biāo)周期 T那么計數(shù)周期數(shù)等于 F / D * T。比如 200MHz、1ms tick就是 200MHz * 0.001s 200000 個時鐘周期所以 PRD 寄存器寫 200000-1。中斷服務(wù)函數(shù)里必須調(diào)用移植層提供的 tick 處理入口函數(shù)不同移植版本命名不同有的是xPortSysTickHandler()有的是vPortYieldHandler()。我習(xí)慣在中斷里只調(diào)用這一個函數(shù)由它內(nèi)部完成 tick 計數(shù)遞增和任務(wù)切換判斷。不要在 ISR 里額外加業(yè)務(wù)代碼不然中斷占用時間會直接影響調(diào)度精度。3.2 任務(wù)棧與棧幀初始化FreeRTOS 創(chuàng)建任務(wù)時會調(diào)用移植層提供的pxPortInitialiseStack()來初始化一個假的寄存器上下文。這樣新任務(wù)第一次被切換進(jìn)來的時候能像從老任務(wù)切換過來一樣從返回地址開始執(zhí)行。C28x 的上下文寄存器包括 PC、SP、ST0、ST1、XAR0-XAR7、IER、IFR 等如果啟用浮點(diǎn)單元還有額外的浮點(diǎn)狀態(tài)寄存器。不同移植版本壓棧的順序不完全相同你不需要背下來但必須知道棧幀的順序決定了上下文切換匯編代碼怎么恢復(fù)。如果你改了pxPortInitialiseStack就得同步改上下文切換的匯編代碼順序一錯任務(wù)一啟動就跑飛。我見過一個真實(shí)案例有人在移植版本里加了一個寄存器想保存自己的調(diào)試信息結(jié)果恢復(fù)順序沒對上任務(wù)運(yùn)行幾分鐘就莫名崩潰一次查了整整兩天。后來我的原則是不是非常明確的需求不要本文還有配套的精品資源點(diǎn)擊獲取