FreeRTOS Study Note

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

內容都是自己K書看來的,有錯請告知,謝謝。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

而我安排自己學習的方式是先從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

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)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Pseudo Code

int main(void) { /* 硬體相關初始化 */ prvSetupHardware(); /* 系統死循環 */ while(1) { /* 處理事件A */ ProcessA(); /* 處理事件B */ ProcessB(); /* 處理事件C */ ProcessC(); } }

Super Loop系統一般來說就是不斷地輪詢,順序地處理每個事件,通常只適用依順序執行且不需要外部事件驅動就能完成的系統。

Foreground Background System (Polling + ISR)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Pseudo Code

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)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Pseudo Code

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

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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Task Suspend & Task Resume

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Context Switch

Basic Concept

在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

在FreeRTOS設計中,為了要解決這個問題,PendSV的特性就是將Context Switch請求延遲到所有的IRQ處理完成後,此時,需要將PendSV的優先權設置為最低優先權,如果FreeRTOS需要執行Context Switch,他會設置SCB->ICSR |= (1 << 28);,並且在PendSV_Handler中執行Context Switch。

Code Analysis

以FreeRTOSv10.2.1\FreeRTOS\Source\portable\GCC\ARM_CM3\potr.c版本來分析Context Switch如何實現。

  • FreeRTOS將PendSV_Handler另外用Macro定義為xPortPendSVHandler,定義在FreeRTOSConfig.h中。
#define xPortPendSVHandler PendSV_Handler
  • 以下為FreeRTOS Context Switch實作。
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會是以下情形。

mrs r0, psp

" mrs r0, psp \n"

將psp值儲存到r0,此時r0指向psp的位址。

ldr r3, pxCurrentTCBConst

" ldr r3, pxCurrentTCBConst \n" /* Get the location of the current TCB. */

pxCurrentTCBConst等於pxCurrentTCB,將pxCurrentTCB的位址存到r3,因此r3儲存了pxCurrentTCB的位址,假設pxCurrentTCB的位址為0x20001100,所以r3=0x20001100。

ldr r2, [r3]

" ldr r2, [r3] \n"

將r3指向的內容存到r2,因此r2為目前正在執行的Task's TCB,假設pxCurrentTCB所指向的是0x20001200,所以r2=0x20001200。

stmdb r0!, {r4-r11}

" stmdb r0!, {r4-r11} \n" /* Save the remaining registers. */

以r0作為基底,將r4~r11的值存到Stack Memory,我們知道Stack是往下生長,所以使用stmdb中的db為先遞減(decrease before),所以r0會先遞減位指在存放r4-r11。

str r0, [r2]

" 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中。

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}

" stmdb sp!, {r3, r14} \n"

因為之後還需要用到r3=pxCurrentTCB的位址,以及會呼叫vTaskSwitchContext函數,r14(LR)會自動存入返回地址,為了防止呼叫vTaskSwitchContext函數時14(LR)會被複寫,因此將r3、r14暫時保存在主堆疊中。

mov r0, %0 and msr basepri, r0

" mov r0, %0 \\n" " msr basepri, r0 \\n"

進入Critical Section,主要是保護在執行vTaskSwithContext函數時,不被打斷。

bl vTaskSwitchContext

" bl vTaskSwitchContext \n"

vTaskSwitchContext函數會去執行taskSELECT_HIGHEST_PRIORITY_TASK()選出一個新的Task,假設選出新的Task位址在0x20001300,taskSELECT_HIGHEST_PRIORITY_TASK()是定義在tasks.c的MACRO。

/* Select a new task to run using either the generic C or port optimised asm code. */ taskSELECT_HIGHEST_PRIORITY_TASK();

mov r0, #0 and msr basepri, r0

" mov r0, #0 \n" " msr basepri, r0 \n"

退出Critical Section。

ldmia sp!, {r3, r14}

" ldmia sp!, {r3, r14} \n"

從MSP把先前所存的r3、r14值,恢復現在的r3、r14,r3=0x20001100,其中ia為increase after。

ldr r1, [r3]

" ldr r1, [r3] \n"

因為r3=0x20001100,而現在的pxCurrentTCB已經指向0x20001300,因此將0x20001300存到r1,所以r1為新的Task's TCB的位址。

ldr r0, [r1]

" ldr r0, [r1] \n" /* The first item in pxCurrentTCB is the task top of stack. */

將pxCurrentTCB指向的New Task's TCB的第一個成員pxTopOfStack值,載入到r0。

ldmia r0!, {r4-r11}

" ldmia r0!, {r4-r11} \n" /* Pop the registers. */

相較之前stmdb的操作,反向做法,以r0作為base address,從新的Task的Stack Memory頂端POP回r4-r11。

msr psp, r0

" msr psp, r0 \n"

更新psp值,等退出PendSV_Handler時,會以psp作為base address,將Stack Memory剩下的內容載回Processor registers。

bx r14

" 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


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。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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

void cstest_task(void *pvParameters) { while(1) { /* 進入臨界區間 */ taskENTER_CRITICAL(); /* 想被保護的程式區段 */ total_value++; /* 退出臨界區間 */ taskEXIT_CRITICAL(); /* 讓出CPU使用權 */ taskYIELD(); } }

Queue

通常是用在中斷副程式與任務之間、任務與任務之間的通訊,也就是相互傳遞資料。

  • 定義資料的結構有一個字串紀錄Name,一個Value。
typedef struct { char Name[8]; uint8_t Value; }Data_t;
  • SenderTask持續產生資料,一直往Queue塞。
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。
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

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

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

  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

然而,最壞的情況是

  1. 低優先權的任務先搶佔Semaphore。
  2. Semaphore依然握在低優先權任務的手上,高優先權任務進入Blocked State。
  3. 低優先權任務剛好又被中優先權任務搶佔了CPU使用權,且Semaphore還握在低優先權任務手上。
  4. 此時,中優先權任務正在執行,高優先權任務依然在等待低優先權任務釋放Semaphore,而低優先權任務還在排程中無法執行。

Priority Inheritance


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。

  2. 一旦網路外部設備收到數據後發出中斷請求,在中斷副程式中使用xSemaphoreGiveFromISR()給出一個Semaphore。

  3. 此時處理網路數據的任務Unblocked。

  4. 網路任務成功拿到Semaphore。

  5. 網路任務可以開始處理數據,處理完成後,如果想再一次嘗試拿取Semaphore,則會重新進入Blocked State。

tags: FreeRTOS RTOS