# 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`