# FreeRTOS Study Note ![](https://i.imgur.com/3FTMrCH.jpg) 內容都是自己K書看來的,有錯請告知,謝謝。 :smile::laughing::smiley: 而我安排自己學習的方式是先從FreeRTOS每種機制應用著手,包含: 1. Task Management 2. Message Queue 3. Semaphore 4. Mutex 5. Event Group 6. Software Timer 7. Task Notification 8. Heap Management 另外還有其他學習筆記: 1. LeetCode解題心得:https://app.gitbook.com/@stanley7342/s/programming/ 2. Bluetooth LE Study Note:https://hackmd.io/@stanley7342/ble_note 3. G3-PLC Study Note:https://hackmd.io/@stanley7342/g3plc_note ## Table of Content [TOC] ## Reference * The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors * Mastering the FreeRTOS™ Real Time Kernel * The FreeRTOS™ Reference Manual ## System Architecture ### Super Loop System (Polling) ![](https://i.imgur.com/KtT5ms1.png) Pseudo Code ```c= int main(void) { /* 硬體相關初始化 */ prvSetupHardware(); /* 系統死循環 */ while(1) { /* 處理事件A */ ProcessA(); /* 處理事件B */ ProcessB(); /* 處理事件C */ ProcessC(); } } ``` Super Loop系統一般來說就是不斷地輪詢,順序地處理每個事件,通常只適用依順序執行且不需要外部事件驅動就能完成的系統。 ### Foreground Background System (Polling + ISR) ![](https://i.imgur.com/MizFnF7.png) Pseudo Code ```c= int flagA = 0; int flagB = 0; int flagC = 0; int main(void) { /* 硬體相關初始化 */ prvSetupHardware(); /* 系統死循環 */ while(1) { if(flagA) { /* 處理事件A */ ProcessA(); } if(flagB) { /* 處理事件B */ ProcessB(); } if(flagC) { /* 處理事件C */ ProcessC(); } } } void ISRA(void) { flagA = 1; } void ISRB(void) { flagB = 1; } void ISRC(void) { flagC = 1; } ``` 前後台系統是輪詢系統加入了中斷,中斷處理稱為前台,main裡處理稱為後台,雖然事件響應和處理分開,但是事件的處理還是在後台中依序執行,但相較輪詢系統,前後台系統確保了事件不會遺失。 ### Multi-Task System (RTOS) ![](https://i.imgur.com/oymkMIe.png) Pseudo Code ```c= int main(void) { /* 硬體相關初始化 */ prvSetupHardware(); /* RTOS相關初始化 */ RTOS_Init(); /* RTOS創建任務A */ RTOS_TaskCreate(TaskA); /* RTOS創建任務B */ RTOS_TaskCreate(TaskB); /* RTOS創建任務C */ RTOS_TaskCreate(TaskC); /* RTOS任務啟動 */ RTOS_Start(); } void TaskA(void) { while(1) { } } void TaskB(void) { while(1) { } } void TaskC(void) { while(1) { } } ``` 多任務系統的事件響應也是在中斷完成,但是事件的處理主要還是在任務中處理,多任務系統中,任務是有優先權等級的,優先權高的任務會優先被執行。當如果有一個緊急事件發生時,在中斷裡拉起flag,如果相對應的任務優先權等級夠高,將立即處理,相較於前後台系統,多任務系統又更有即時性。 ## Task State Machine ![](https://i.imgur.com/MNCylTP.png) ### Running State 一個正在使用CPU資源的Task,就是處在Running State。 ### Blocked State 可能正在等待某些事件就會進入Blocked State,譬如某個任務呼叫了vTaskDelay()就會進入Blocked State,直到延遲的週期完成,任務在等待Queue、Semaphore、Notification和Mutex也會進入Blocked State,任務進入Blocked State可以設定一個Timeout時間,一旦時間到了,也能夠退出Blocked State。 ### Suspended State 進入Suspended State是需要呼叫vTaskSuspend(),退出Suspended State是需要呼叫vTaskResume(),進入Suspended State之後就不能被Scheduler排程進入Running State了。 ### Ready State 這些任務沒有被Blocked或者是Suspended,而是等著被排程準備進入Running State,但現在處於Ready State是因為可能有更高優先權的任務正在執行。 ### How to create tasks {%youtube QobCtcOI8VE %} ### Task Suspend & Task Resume {%youtube gAcP96P-YTY %} ## Context Switch ### Basic Concept ![](https://i.imgur.com/4FIG48n.png) 在Task要被switch out之前,需要作以下事情 1. 當SysTick進入IRQ中,PUSH Processor registers r0、r1、r2、r3、r12、LR、PC和xPSR到task的私有Stack Memory區域。 2. 如果需要Context Switch,SysTick將觸發PendSV_Handler。 3. 手動儲存r4-r11和r14至Task私有的Stack Memory區域。 4. 儲存最上層的Stack值至TCB的第一個成員pxTopOfStack。 5. 執行vTaskSwitchContext()選出下一個可能使用CPU資源的Task。 ### PendSV exception PendSV對OS操作非常重要,優先權可以透過程式設定,且透過SCB->ICSR中的第28位元來觸發,也就是將其設置為1就能夠觸發PendSV_Handler。 在FreeRTOS中是利用SysTick進入中斷後,由Scheduler來決定是否應該執行Context Switch,來切換到不同的任務,如果其他的中斷請求(IRQ)在SysTick之前產生,則SysTick可能會去搶佔IRQ的處理,這種情況下,FreeRTOS不應該去執行Context Switch,否則IRQ處理就會被延遲,對於IRQ設計應該是越短越好,對於延遲往往會有不可預期的事情發生,以ARM Cortex M3和ARM Cortex M4處理器來說,當Processor進入Handler Mode中,默認是不允許返回Thread Mode,如果試圖想由Handler Mode返回Thread Mode則會產生**Usage Fault**。 ![](https://i.imgur.com/7WdD19X.png) 在FreeRTOS設計中,為了要解決這個問題,PendSV的特性就是將Context Switch請求延遲到所有的IRQ處理完成後,此時,需要將PendSV的優先權設置為最低優先權,如果FreeRTOS需要執行Context Switch,他會設置SCB->ICSR |= (1 << 28);,並且在PendSV_Handler中執行Context Switch。 ![](https://i.imgur.com/54w5jRG.png) ### Code Analysis 以FreeRTOSv10.2.1\FreeRTOS\Source\portable\GCC\ARM_CM3\potr.c版本來分析Context Switch如何實現。 * FreeRTOS將PendSV_Handler另外用Macro定義為xPortPendSVHandler,定義在FreeRTOSConfig.h中。 ```c=123 #define xPortPendSVHandler PendSV_Handler ``` * 以下為FreeRTOS Context Switch實作。 ```c= void xPortPendSVHandler( void ) { /* This is a naked function. */ __asm volatile ( " mrs r0, psp \n" " isb \n" " \n" " ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. */ " ldr r2, [r3] \n" " \n" " stmdb r0!, {r4-r11} \n" /* Save the remaining registers. */ " str r0, [r2] \n" /* Save the new top of stack into the first member of the TCB. */ " \n" " stmdb sp!, {r3, r14} \n" " mov r0, %0 \n" " msr basepri, r0 \n" " bl vTaskSwitchContext \n" " mov r0, #0 \n" " msr basepri, r0 \n" " ldmia sp!, {r3, r14} \n" " \n" /* Restore the context, including the critical nesting count. */ " ldr r1, [r3] \n" " ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */ " ldmia r0!, {r4-r11} \n" /* Pop the registers. */ " msr psp, r0 \n" " isb \n" " bx r14 \n" " \n" " .align 4 \n" " pxCurrentTCBConst: .word pxCurrentTCB \n" ::"i"(configMAX_SYSCALL_INTERRUPT_PRIORITY) ); } ``` #### SysTick_Handler Processor會自動將r0、r1、r2、r3、r12、r14、PC、xPSR PUSH到Stack Memory中,然後剩下的r4~r11需要手動保存,接下來可以看一下要如何保存上下文的上文。此時現行的Task的Stack Memory會是以下情形。 ![](https://i.imgur.com/VfrtiSJ.png) #### mrs r0, psp ```c=7 " mrs r0, psp \n" ``` 將psp值儲存到r0,此時r0指向psp的位址。 ![](https://i.imgur.com/RnO9832.png) #### ldr r3, pxCurrentTCBConst ```c=10 " ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. */ ``` pxCurrentTCBConst等於pxCurrentTCB,將pxCurrentTCB的位址存到r3,因此r3儲存了pxCurrentTCB的位址,假設pxCurrentTCB的位址為0x20001100,所以r3=0x20001100。 ![](https://i.imgur.com/TnwqaV2.png) #### ldr r2, [r3] ```c=11 " ldr r2, [r3] \n" ``` 將r3指向的內容存到r2,因此r2為目前正在執行的Task's TCB,假設pxCurrentTCB所指向的是0x20001200,所以r2=0x20001200。 ![](https://i.imgur.com/j2sCPrc.png) #### stmdb r0!, {r4-r11} ```c=13 " stmdb r0!, {r4-r11} \n" /* Save the remaining registers. */ ``` 以r0作為基底,將r4~r11的值存到Stack Memory,我們知道Stack是往下生長,所以使用stmdb中的db為先遞減(decrease before),所以r0會先遞減位指在存放r4-r11。 ![](https://i.imgur.com/ishhCub.png) #### str r0, [r2] ```c=14 " str r0, [r2] \n" /* Save the new top of stack into the first member of the TCB. */ ``` 將r0目前所儲存的位址存到Task's TCB的第一個成員,其型態為tskTCB,而tskTCB定義在tasks.c中,也就是說,其中第一個成員為指向Stack頂端的指標,因此把r0目前指向的位址存到pxTopOfStack中。 ```c= typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; /*< Points to the location of the last item placed on the tasks stack. THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */ . . . . } tskTCB; ``` #### stmdb sp!, {r3, r14} ```c=16 " stmdb sp!, {r3, r14} \n" ``` 因為之後還需要用到r3=pxCurrentTCB的位址,以及會呼叫vTaskSwitchContext函數,r14(LR)會自動存入返回地址,為了防止呼叫vTaskSwitchContext函數時14(LR)會被複寫,因此將r3、r14暫時保存在主堆疊中。 ![](https://i.imgur.com/VwvqQNE.png) #### mov r0, %0 and msr basepri, r0 ```c=17 " mov r0, %0 \\n" " msr basepri, r0 \\n" ``` 進入Critical Section,主要是保護在執行vTaskSwithContext函數時,不被打斷。 #### bl vTaskSwitchContext ```c=19 " bl vTaskSwitchContext \n" ``` vTaskSwitchContext函數會去執行taskSELECT_HIGHEST_PRIORITY_TASK()選出一個新的Task,假設選出新的Task位址在0x20001300,taskSELECT_HIGHEST_PRIORITY_TASK()是定義在tasks.c的MACRO。 ```c= /* Select a new task to run using either the generic C or port optimised asm code. */ taskSELECT_HIGHEST_PRIORITY_TASK(); ``` ![](https://i.imgur.com/A1mLjB4.png) #### mov r0, #0 and msr basepri, r0 ```c=20 " mov r0, #0 \n" " msr basepri, r0 \n" ``` 退出Critical Section。 #### ldmia sp!, {r3, r14} ```c=22 " ldmia sp!, {r3, r14} \n" ``` 從MSP把先前所存的r3、r14值,恢復現在的r3、r14,r3=0x20001100,其中ia為increase after。 #### ldr r1, [r3] ```c=24 " ldr r1, [r3] \n" ``` 因為r3=0x20001100,而現在的pxCurrentTCB已經指向0x20001300,因此將0x20001300存到r1,所以r1為新的Task's TCB的位址。 #### ldr r0, [r1] ```c=25 " ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */ ``` 將pxCurrentTCB指向的New Task's TCB的第一個成員pxTopOfStack值,載入到r0。 ![](https://i.imgur.com/lAdYN9J.png) #### ldmia r0!, {r4-r11} ```c=26 " ldmia r0!, {r4-r11} \n" /* Pop the registers. */ ``` 相較之前stmdb的操作,反向做法,以r0作為base address,從新的Task的Stack Memory頂端POP回r4-r11。 ![](https://i.imgur.com/Bc49C5a.png) #### msr psp, r0 ```c=27 " msr psp, r0 \n" ``` 更新psp值,等退出PendSV_Handler時,會以psp作為base address,將Stack Memory剩下的內容載回Processor registers。 ![](https://i.imgur.com/y7MNvF8.png) #### bx r14 ```c=29 " bx r14 \n" ``` r14(LR)中保存了返回地址,包含返回後應進入Thread Mode還是Handler Mode,使用psp還是msp,此時,r14(LR)應為0xfffffffd,表示返回後應進入Thread Mode,sp應該使用psp,當呼叫bx r14後,就會跳出PendSV_Handler,processor POP r0、r1、r2、r3、r12、LR、PC和xPSR,完成了Context Switch。 ### Operation Modes and States ![](https://i.imgur.com/cF5b8E4.png) ARM Cortex-M3和ARM Cortex-M4有兩種Operation Modes和兩種Operation States。 * Thumb State * Debug State * Thread Mode * 執行使用者應用程式,處理器可以執行Privileged Access Level或者是Non-privileged Access Mode,實際的Access Level可以由CONTROL暫存器控制。 * Handler Mode * 執行中斷副程式,處理器總是執行Priviledged Access Level。 * Example * 影片中有三個中斷點,程式剛進入main()(Line 59)是處於Thread Mode,然後開啟WatchDog中斷,當程序進入WWDG_IRQHandler()(Line 79)處理就會處於Handler Mode,一旦中斷副程式執行完畢回到主程式中(Line 69)處理器又回到Thread Mode。 {%youtube 8dpqF180lAE %} ## Critical Section 臨界區間是指那些必須連續執行,不能被打斷的程式區段,比如說有些外部設備初始化設定時有嚴格的時間限制,像我以前在寫通訊SoC PHY層OFDM驅動的時候,發生過當我收完PHY的Header後,得到一些解調變的資訊,需要很快速地計算Reed Solomon、Convolutional coding,來推導之後會有多少長度的資料,告訴硬體如何解調變,其中這些動作延遲一些時間往往會發生解碼的錯誤,FreeRTOS的作法是在進入臨界區間的程式段前先關閉中斷,等程式處理完之後,再開啟中斷。 什麼情況下會發生臨界區間被搶斷?一個是系統調度,另一個是外部中斷,在FreeRTOS中,系統調度最終也是透過PendSV_Handler來做Context Switch,換言之,臨界區間的保護也可以說是中斷開關的控制。 FreeRTOS有四個關於臨界區間保護的API: * 任務級的臨界區間保護 * taskENTER_CRITICAL():進入臨界區間 * taskEXIT_CRITICAL():退出臨界區間 * 中斷級的臨界區間保護 * taskENTER_CRITICAL_FROM_ISR():進入臨界區間 * taskEXIT_CRITICAL_FROM_ISR():退出臨界區間 ### CS example ```c= void cstest_task(void *pvParameters) { while(1) { /* 進入臨界區間 */ taskENTER_CRITICAL(); /* 想被保護的程式區段 */ total_value++; /* 退出臨界區間 */ taskEXIT_CRITICAL(); /* 讓出CPU使用權 */ taskYIELD(); } } ``` ## Queue 通常是用在中斷副程式與任務之間、任務與任務之間的通訊,也就是相互傳遞資料。 * 定義資料的結構有一個字串紀錄Name,一個Value。 ```c= typedef struct { char Name[8]; uint8_t Value; }Data_t; ``` * SenderTask持續產生資料,一直往Queue塞。 ```c=+ void SenderTask(void *pvParameters) { Data_t TxBuf = { .Name = "Apple", .Value = 10}; while(1) { sprintf(msg, "%s, Name: %s, Value: %d\r\n", __FUNCTION__, TxBuf.Name, TxBuf.Value); printmsg(msg); xQueueSend(Queue_Handle, &TxBuf, 10); TxBuf.Value++; vTaskDelay(500); } } ``` * ReceiverTask會去接收Queue的資料,有資料的話就印出來,沒有資料就進入Blocked State。 ```c=+ void ReceiverTask(void *pvParameters) { Data_t RxBuf; while(1) { xQueueReceive(Queue_Handle, &RxBuf, portMAX_DELAY); sprintf(msg, "%s, Name: %s, Value: %d\r\n", __FUNCTION__, RxBuf.Name, RxBuf.Value); printmsg(msg); } } ``` 影片DEMO {%youtube bk7C_30zQ6k %} ## Semaphore 常常用在控制共享資源的使用和任務的同步。舉個例子,一般公共電話亭,我們知道一次只能一個人使用電話,那麼公共電話亭的狀態就只有使用或未使用,如果這時候公共電話亭被使用,其他的人就必須站在外面排隊等待,等到講電話的人使用完畢,其他人才能去使用這座公共電話亭,如果以公共電話亭的狀態作為Semaphore,那這就是Binary Semaphore。 另一個常見的例子,假如某個停車場有100個停車位,對於每個開車族來說這100個停車位就是共享資源,當駕駛開到停車場入口的時候,一般來說都會看一下現在還有多少個停車位,然而當前的停車位數量就是一個Semaphore,當有車子從停車場開走的時候,停車位數量就會加一,也就是Semaphore count加一,當有車子從入口停入車子的時候,停車位數就會減一,也就是Semaphore count減一,這就是Counting Semaphore的一個例子。 Semaphore用於控制共享資源使用的情形比較像是一個上鎖的機制,只有取得這個鎖的鑰匙才能夠執行。以公共電話亭和停車位為例,就是Semaphore在共享資源使用的應用,另一種應用就是任務之間的同步或是中斷與任務之間的同步。 ### Binary Semaphore 常用於互斥訪問和同步,和Mutex非常相似,但還是有些許差異,Mutex有**Priority Inheritance**的機制,而Binary Sempahore沒有,所以使用Binary Semaphore來實現Mutex機制的時候,會遇到**Priority Inversion**的問題,因此,Binary Semaphore適合用在任務跟任務之間的同步或者是中斷與任務之間的同步,而Mutex更適合用在互斥訪問。 ### Priority Inversion ![](https://i.imgur.com/CysTo67.png) 1. Task 1搶到CPU資源所以開始運行。 2. Task 1想要使用外部設備的資源,且比更高優先權的Task 2還先拿了semaphore。 3. Task 2可能也想要使用外部設備的資源,所以也想去拿semaphore,只不過semaphore還握在Task 1手中,因此Task 2進入了Blocked State,讓出了CPU使用權。 4. Task 1使用完外部設備的資源且釋放semaphore,使得Task 2可以退出Blocked State,然後去搶斷Task 1 CPU的使用權。 5. Task 2也使用完外部設備的資源且釋放semaphore。 因為低優先權任務先搶佔了Semaphore,而讓高優先權任務必須等待低優先權任務執行完畢後釋放Semaphore,才有機會輪到高優先權任務執行,這種情況稱為**Priority Inversion**。 然而,最壞的情況是 ![](https://i.imgur.com/OnyN01J.png) 1. 低優先權的任務先搶佔Semaphore。 2. Semaphore依然握在低優先權任務的手上,高優先權任務進入Blocked State。 3. 低優先權任務剛好又被中優先權任務搶佔了CPU使用權,且Semaphore還握在低優先權任務手上。 4. 此時,中優先權任務正在執行,高優先權任務依然在等待低優先權任務釋放Semaphore,而低優先權任務還在排程中無法執行。 ### Priority Inheritance ![](https://i.imgur.com/0qel80m.png) **Priority Inheritance**是一個為了減少對於**Priority Inversion**造成負面影響的機制,它無法修復**Priority Inverion**的問題,但可以確保Inversion問題可以減到最短時間,也就是說高優先權任務不會因為低優先權任務把持Semphore,然後低優先權任務本身又一直被中優先權任務搶佔CPU資源而等太久,導致時間不可預期。 **Priority Inheritance**的作法是暫時提高把持住Semaphore低優先權任務的優先權,提高到跟一樣想要搶佔同一個Semaphore的高優先權任務的優先權,等到執行完畢,提升的優先權就會回復到原始的優先權,理由是希望低優先權任務趕緊執行完畢釋放Semaphore,使得高優先權任務可以更快拿到Semaphore。 也因為**Priority Inheritance**機制會改變優先權,因此Mutex不能夠使用在IRQ裡。 ### Application 實際應用案例中,比如一個Ethernet的MAC封包,一般最簡單的方法就是建一個任務去讓CPU不斷地輪詢MAC外部設備是否有網路數據,如此一來,CPU的資源全部只能使用在輪詢MAC外部設備上,其他的任務都要不到CPU使用權,最好的方法是當MAC外部設備還沒有收到網路數據時就讓任務進入Blocked State,把CPU資源先讓給其他任務使用,一旦收到網路數據再通知任務來處理數據即可,這時候可以使用Binary Semaphore來設計,一般外部設備收到數據都能夠發出中斷,如果使用DMA去收也會有DMA中斷,這時候網路中斷副程式可以透過Binary Semaphore釋放Semaphore來通知任務做數據的處理,在中斷副程式**必須**使用xSemaphoreGiveFromISR()帶有FromISR結尾的API,如此一來,就完成了中斷副程式與任務之間的同步功能,不但達到中斷副程式處理時間越短越好,也能夠延續網路數據的處理。 Binary Semaphore使用過程 1. 如果Semaphore還沒有被釋放,處理網路數據的任務(例如處裡TCP/IP任務)執行xSemaphoreTake()後進入Blocked State。 ![](https://i.imgur.com/Okmhm7v.png) 2. 一旦網路外部設備收到數據後發出中斷請求,在中斷副程式中使用xSemaphoreGiveFromISR()給出一個Semaphore。 ![](https://i.imgur.com/pcXQ2Kw.png) 3. 此時處理網路數據的任務Unblocked。 ![](https://i.imgur.com/4CDEQB3.png) 4. 網路任務成功拿到Semaphore。 ![](https://i.imgur.com/3zqezpu.png) 5. 網路任務可以開始處理數據,處理完成後,如果想再一次嘗試拿取Semaphore,則會重新進入Blocked State。 ![](https://i.imgur.com/hdNxtQh.png) ###### tags: `FreeRTOS` `RTOS`