一、基本配置
1.1 數據類型
FreeRTOS 使用的數據類型主要分為 stdint.h 文件中定義的和自己定義的兩種
FreeRTOS 主要自定義了以下四種數據類型:
TickType_t----32 位無符號數( 32位MCU,配置configUSE_16_BIT_TICKS = 0)
BaseType_t----32 位有符號數(32位MCU)
UBaseType_t---32 位無符號數(BaseType_t類型無符號版本)
StackType_t----32 位變量(棧變量數據類型定義,32位MCU)
1.2 代碼風格
變量
- uint32_t 定義的變量都加上前綴 ul。u 代表 unsigned 無符號,l 代表 long 長整型。
- uint16_t 定義的變量都加上前綴 us。u 代表 unsigned 無符號,s 代表 short 短整型。
- uint8_t 定義的變量都加上前綴 uc。u 代表 unsigned 無符號,c 代表 char 字符型。
- stdint.h 文件中未定義的變量類型,在定義變量時需要加上前綴 x,比如 BaseType_t 和
TickType_t 定義的變量。 - stdint.h 文件中未定義的無符號變量類型,在定義變量時要加上前綴 u,比如 UBaseType_t 定義
的變量要加上前綴 ux。 - size_t 定義的變量也要加上前綴 ux。
- 枚舉變量會加上前綴 e。
- 指針變量會加上前綴 p,比如 uint16_t 定義的指針變量會加上前綴 pus。
- 根據 MISRA 代碼規則,char 定義的變量只能用於 ASCII 字符,前綴使用 c。
- 根據 MISRA 代碼規則,char *定義的指針變量只能用於 ASCII 字符串,前綴使用 pc。
函數
- 加上了 static 聲明的函數,定義時要加上前綴 prv,這個是單詞 private 的縮寫。
- 帶有返回值的函數,根據返回值的數據類型,加上相應的前綴,如果沒有返回值,即 void 類型
,函數的前綴加上字母 v。 - 根據文件名,文件中相應的函數定義時也將文件名加到函數命名中,比如 tasks.c 文件中函數
vTaskDelete,函數中的 task 就是文件名中的 task。
宏定義
- 根據宏定義所在的文件,文件中的宏定義聲明時也將文件名加到宏定義中,比如宏定義
configUSE_PREEMPTION 是定義在文件 FreeRTOSConfig.h 里面。宏定義中的 config 就是文
件名中的 config。另外注意,前綴要小寫。 - 除了前綴,其余部分全部大寫,同時用下划線分開。
排版和注釋
- 縮進
Tab 制表符用於縮進,Tab 一次縮進 4 個字符空間。 - 注釋
FreeRTOS 中注釋不會超過 80 個字符寬度,除非對函數的參數進行注釋時。源碼中主要是采用/* */
的形式進行注釋,不采用 C++中的雙斜杠風格來注釋。
1.3 關鍵配置
configCPU_CLOCK_HZ---配置MCU主頻,單位Hz
configTICK_RATE_HZ---配置系統時鍾節拍數,單位 Hz
configMAX_PRIORITIES---配置用戶可用最大優先級數,0~configMAX_PRIORITIES-1
configTOTAL_HEAP_SIZE---配置堆大小
configUSE_TASK_NOTIFICATIONS---配置任務通知
configUSE_TIME_SLICING---配置時間片輪轉調度
configLIBRARY_LOWEST_INTERRUPT_PRIORITY---受RTOS管理的最小中斷優先級
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY---受RTOS管理的最大中斷優先級
二、任務管理
2.1 任務棧
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) )
1,局部變量,函數調用時的現場保護和返回地址,函數的形參,進入中斷函數前和中斷嵌套等都需要棧空間,棧空間定義小了會造成系統崩潰
2,實際分配的棧大小可以在最小棧需求的基礎上乘以一個安全系數,一般取 1.5-2
3,棧生長方向從高地址向低地址生長(M4 和 M3 是這種方式)
2.2 系統棧
任務棧不使用系統棧控件,中斷函數和中斷嵌套使用
Cortex-M3 和 M4 內核具有雙堆棧指針,MSP 主堆棧指針和 PSP 進程堆棧指針
在 FreeRTOS 操作系統中,主堆棧指針 MSP 是給系統棧空間使用的,進
程堆棧指針 PSP 是給任務棧使用的
對於 Cortex-M3 內核和未使用 FPU(浮點運算單元)功能的 Cortex-M4 內核,需要64字節
對於具有 FPU(浮點運算單元)功能的 Cortex-M4 內核,需要200字節
2.3 任務優先級
- FreeRTOS 中任務的最高優先級是通過 FreeRTOSConfig.h 文件中的 configMAX_PRIORITIES 進行配置的,用戶實際可以使用的優先級范圍是 0 到 configMAX_PRIORITIES – 1
- 用戶配置任務的優先級數值越小,那么此任務的優先級越低,空閑任務的優先級是 0
- 建議用戶配置宏定義 configMAX_PRIORITIES 的最大值不要超過 32
- FreeRTOS 中處於運行狀態的任務永遠是當前能夠運行的最高優先級任務
2.3.1 任務優先級分配
- IRQ 任務:IRQ 任務是指通過中斷服務程序進行觸發的任務,此類任務應該設置為所有任務里面優先級最高的
- 高優先級后台任務:比如按鍵檢測,觸摸檢測,USB 消息處理,串口消息處理等,都可以歸為這一類任務 低優先級的時間片調度任務:比如 emWin 的界面顯示,LED 數碼管的顯示等不需要實時執行的都可以歸為這一類任務
實際應用中用戶不必拘泥於將這些任務都設置為優先級 1 的同優先級任務,可以設置多個優先級,只需注意這類任務不需要高實時性 - 空閑任務:空閑任務是系統任務
- 特別注意:IRQ 任務和高優先級任務必須設置為阻塞式(調用消息等待或者延遲等函數即可), 只有這樣,高優先級任務才會釋放 CPU 的使用權,,從而低優先級任務才有機會得到執行
2.3.2 中斷優先級與任務優先級
- 中斷的優先級永遠高於任何任務的優先級,即任務在執行的過程中,中斷來了就開始執行中斷服務程序
- 對於 STM32F103,F407 和 F429 來說,中斷優先級的數值越小,優先級越高。而 FreeRTOS的任務優先級是,任務優先級數值越小,任務優先級越低
2.3.2 任務調度
2.2.3 相關API
vTaskStartScheduler-任務啟動
- 空閑任務和可選的定時器任務是在調用這個函數后自動創建
- 正常情況下這個函數是不會返回的,運行到這里極有可能是用於定時器任務或者空閑任務的 heap 空 間不足造成創建失敗,此時需要加大 FreeRTOSConfig.h 文件中定義的 heap 大小
- 如果在此類任務函數里面填的任務 ID 是 NULL,即數值 0 的話,那么執行生效的就是當前正在執行的任務
系統框架寫法
int main(void)
{
/* 初始化外設 */
xTaskCreate(); /* 創建開始任務 */
vTaskStartScheduler(); /* 開啟任務調度 */
}
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); /* */進入臨界區
xTaskCreate(); /* 創建任務 1 */
xTaskCreate(); /* 創建任務 2 */
xTaskCreate(); /* 創建任務 3 */
vTaskDelete(StartTask_Handler); /* 刪除開始任務 */
taskEXIT_CRITICAL(); /* 退出臨界區 */
}
任務創建、掛起、刪除
/* 相關配置 */
#define configSUPPORT_DYNAMIC_ALLOCATION 1 /* 支持動態內存申請 */
//#define configSUPPORT_STATIC_ALLOCATION 1 /* 支持靜態內存申請 */
#define configTOTAL_HEAP_SIZE ((size_t)(20*1024)) /* 系統所有總的堆大小,heap_x.h需要,動態申請 */
/* 任務創建 */
/* 任務創建宏定義,便於修改 */
#define START_TASK_PRIO 1 /* 任務優先級 */
#define START_STK_SIZE 256 /* 任務堆棧大小 */
TaskHandle_t StartTask_Handler; /* 任務句柄 */
void start_task(void *pvParameters); /* 任務函數 */
/* 任務創建函數 */
xTaskCreate((TaskFunction_t )start_task, /* 任務函數 */
(const char* )"start_task", /* 任務名稱 */
(uint16_t )START_STK_SIZE, /* 任務堆棧大小 */
(void* )NULL, /* 傳遞給任務函數的參數 */
(UBaseType_t )START_TASK_PRIO, /* 任務優先級 */
(TaskHandle_t* )&StartTask_Handler); /* 任務句柄 */
/* 任務掛起 */
vTaskSuspend(Task1Task_Handler);
/* 任務內解掛 */
vTaskResume(Task1Task_Handler);
/* 中斷內解掛 */
BaseType_t YieldRequired;
YieldRequired=xTaskResumeFromISR(Task1Task_Handler);
portYIELD_FROM_ISR(YieldRequired); /* 判斷是否需要調度到恢復的任務 */
/* 任務刪除 */
vTaskDelete(Task1Task_Handler);
任務調度
- 嵌入式實時操作系統的核心就是調度器和任務切換,調度器的核心就是調度算法
搶占式務調度
- 每個任務都有不同的優先級,任務會一直運行直到被高優先級任務搶占或者遇到阻塞式的 API 函數,搶占式調度器會獲得就緒列表中優先級最高的任務,並運行這個任
- 根據搶占式調度器,當前的任務要么被高優先級任務搶占,
要么通過調用阻塞式 API 來釋放 CPU 使用權讓低優先級任務執行,沒有用戶任務執行時就執行空閑任務
時間片調度
- 實現 Round-robin 調度算法需要給同優先級的任務分配一個專門的列表,用於記錄當前就緒的任務,
並為每個任務分配一個時間片 - 每個任務都有相同的優先級,任務會運行固定的時間片個數或者遇到阻塞式的 API 函數
- 在 FreeRTOS 操作系統中只有同優先級任務才會使用時間片調度
- 默認情況下,此宏定義已經在 FreeRTOS.h 文件里面使能
- FreeRTOS的時間片僅支持一個時鍾周期,比如你的系統時鍾節拍是1000Hz,那么時間片的大小就是1ms,時間片調度僅存在於同優先級任務
- 在 RTOS 中,最小的時間單位為一個 tick,即 SysTick 的中斷周期,RT-Thread 和 μC/OS可以指定時間片的大小為多個 tick,但是 FreeRTOS不一樣,時間片只能是一個 tick。與其說 FreeRTOS 支持時間片,倒不如說它的時間片就是正常的任務調度
/* 相關配置,時間片長度即1/Tick中斷頻率 */
#define configUSE_PREEMPTION 1 /* 1使用搶占式內核,0使用協程 */
#define configUSE_TIME_SLICING 1 /* 1使能時間片調度(默認式使能的) */
#define configTICK_RATE_HZ (20) /* 時鍾節拍頻率,20HZ = 50ms */
/* 兩個任務優先級相等 */
#define TASK1_TASK_PRIO 2
#define TASK1_STK_SIZE 128
TaskHandle_t Task1Task_Handler;
void task1_task(void *pvParameters);
#define TASK2_TASK_PRIO 2
#define TASK2_STK_SIZE 128
TaskHandle_t Task2Task_Handler;
void task2_task(void *pvParameters);
三、系統延時
const TickType_t xDelayTime = pdMS_TO_TICKS(300);/* 將延時ms轉換為系統節拍 */
const TickType_t xDelayTime = 300 / portTICK_RATE_MS;/* 將延時ms轉換為系統節拍 */
/* TickType_t 等價於 portTickType */
/* 系統延時函數,單位:系統節拍,阻塞延時 portMAX_DELAY-最大延時等待 */
/* 相對延時 */
const TickType_t xDelayTime = pdMS_TO_TICKS(300);
vTaskDelay(xDelayTime);
/* 絕對延時 */
static portTickType xLastWakeTime;
const portTickType xFrequency = pdMS_TO_TICKS(500);
vTaskDelayUntil(&xLastWakeTime,xFrequency);
如果想精確周期性執行某個任務,可以調用系統節拍鈎子函數vApplicationTickHook(),它在系統節拍中斷服務函數中被調用,因此這個函數中的代碼必須簡潔
四、軟件定時器
/* 定時器結構體、函數聲明 */
TimerHandle_t MyTimer_Handle; /* 定時器句柄 */
void ReloadCallback(TimerHandle_t xTimer); /* 定時器回調函數 */
/* 創建軟件定時器 */
MyTimer_Handle=xTimerCreate((const char* )"ReloadTimer", /* 定時器名稱 */
(TickType_t )1000, /* 周期1s(1000個時鍾節拍) */
(UBaseType_t )pdTRUE, /* 周期模式 */
(void* )1, /* 定時器ID */
(TimerCallbackFunction_t)ReloadCallback); /* 定時器回調函數 */
/* 定時器回調函數 */
void ReloadCallback(TimerHandle_t xTimer)
{
//do something
}
xTimerStart(MyTimer_Handle,0); /* 開啟定時器 */
xTimerStop(MyTimer_Handle,0); /* 定時器停止 */
xTimerReset(MyTimer_Handle, 0); /* 定時器復位 */
五、消息隊列
/* 消息隊列宏定義 */
#define MESSAGE_Q_NUM 4 /* 發送數據的消息隊列的數量 */
#define MESSAGE_Q_ITEM_NUM 200 /* 每個消息的空間大小 */
QueueHandle_t Message_Queue; /* 信息隊列句柄 */
/* 創建消息隊列 */
Message_Queue=xQueueCreate(MESSAGE_Q_NUM,MESSAGE_Q_ITEM_NUM);
/* 任務內消息發送 */
u8 sendData[MESSAGE_Q_ITEM_NUM];
BaseType_t err;
err=xQueueSend(Message_Queue,&senddata,10); /* 10為發送等待時間,有可能隊列已滿,err = errQUEUE_FULL 或 err = pdPASS */
/* 中斷內消息發送 */
8 sendData[MESSAGE_Q_ITEM_NUM];
BaseType_t xHigherPriorityTaskWoken;
xQueueSendFromISR(Message_Queue,sendData,&xHigherPriorityTaskWoken); /* 向隊列中發送數據,返回值,依然是 滿了或Pass,第三個參數是判斷高優先級 */接受到隊列后,退出中斷,是否需要調度
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);/*如果需要的話進行一次任務切換 */
/* 任務內消息接收 */
u8 *receiveData;
xQueueReceive(Message_Queue,receiveData,portMAX_DELAY) /* 返回值為pdPASS 或 errQUEUE_EMPTY,這里等待時間用portMAX_DELAY阻塞,所以不用再判斷 */
/* 中斷內消息接收 */
u8 *receiveData;
err=xQueueReceiveFromISR(Message_Queue,receiveData,&xTaskWokenByReceive); /* 向隊列中接受數據,返回值, FAIL或Pass,第三個參數是判斷高優先級接受到隊列后,退出中斷,是否需要調度 */
portYIELD_FROM_ISR(xTaskWokenByReceive);/* 如果需要的話進行一次任務切換 */
u8 remain_size; /* 消息隊列剩余大小 */
remain_size=uxQueueSpacesAvailable(Message_Queue); /* 得到隊列剩余大小 */
u8 used_size; /* 消息隊列使用大小 */
used_size=uxQueueMessagesWaiting(Message_Queue); /* 得到隊列使用大小 */
六、信號量
- FreeRTOS的信號量包括二進制信號量、計數信號量、互斥信號量(以后簡稱互斥量)和遞歸互斥信號量(以后簡稱遞歸互斥量
信號量API函數實際上都是宏,它使用現有的隊列機制。這些宏定義在semphr.h文件中。如果使用信號量或者互斥量,需要包含semphr.h頭文件。
- 二進制信號量、計數信號量和互斥量信號量的創建API函數是獨立的,但是獲取和釋放API函數都是相同的;遞歸互斥信號量的創建、獲取和釋放API函數都是獨立的
6.1 二值信號量
/* 二值信號量創建 */
SemaphoreHandle_t BinarySemaphore; /* 二值信號量句柄 */
BinarySemaphore=xSemaphoreCreateBinary(); /* 創建二值信號量 */
/* 二值信號量等待 */
BaseType_t err;
err = xSemaphoreTake(BinarySemaphore,portMAX_DELAY); /* 獲取信號量 */
/* 二值信號量發送 */
BaseType_t err;
err = xSemaphoreGive(BinarySemaphore); /* 釋放二值信號量 */
6.2 計數信號量
/* 計數信號量創建 */
SemaphoreHandle_t CountSemaphore;/* 計數型信號量句柄 */
CountSemaphore=xSemaphoreCreateCounting(255,0); /* 創建計數型信號量,最大計數和初始化計數,參數沒改動的話,為long,所以最大值可以設計為不止255 */
/* 計數信號量等待 */
UBaseType_t semavalue;
xSemaphoreTake(CountSemaphore,portMAX_DELAY); /* 等待數值信號量,阻塞 */
semavalue=uxSemaphoreGetCount(CountSemaphore); /* 獲取數值信號量值 */
/* 計數信號量發送 */
BaseType_t err;
err=xSemaphoreGive(CountSemaphore);/* 釋放計數型信號量 */
6.3 互斥信號量
/* 創建互斥信號量 */
SemaphoreHandle_t MutexSemaphore; /* 互斥信號量句柄 */
MutexSemaphore=xSemaphoreCreateMutex(); /* 創建互斥信號量 */
/* 互斥信號量等待 */
xSemaphoreTake(MutexSemaphore,portMAX_DELAY); /* 獲取互斥信號量,因為是阻塞,也就不需要查看什么返回值 */
/* 互斥信號量發送 */
xSemaphoreGive(MutexSemaphore); /* 釋放互斥信號量 */
6.4 遞歸信號量
SemaphoreHandle_t RecursiveMutex;/* 遞歸互斥信號量句柄 */
RecursiveMutex = xSemaphoreCreateRecursiveMutex(); /* 創建遞歸互斥信號量 */
/* 遞歸互斥信號量等待 */
xSemaphoreTakeRecursive(RecursiveMutex,10); /* 10為等待節拍 */
/* 遞歸互斥信號量發送 */
xSemaphoreGiveRecursive(RecursiveMutex); /* 發送遞歸互斥信號量 */
七、事件標志組
/* 例子,3個事件 */
#define EVENTBIT_0 (1<<0)
#define EVENTBIT_1 (1<<1)
#define EVENTBIT_2 (1<<2)
#define EVENTBIT_ALL (EVENTBIT_0|EVENTBIT_1|EVENTBIT_2)
EventGroupHandle_t EventGroupHandler; /* 事件標志組句柄 */
EventGroupHandler=xEventGroupCreate(); /* 創建事件標志組 */
/* 事件標志組置位 */
xEventGroupSetBits(EventGroupHandler,EVENTBIT_1); /* 事件1置位 */
/* 事件標志組清除 */
xEventGroupClearBits(EventGroupHandler,EVENTBIT_1); /* 事件1清除 */
/* 事件標志組獲取 */
EventBits_t NewValue;
NewValue = xEventGroupGetBits(EventGroupHandler);
/* 事件標志組等待 */
EventValue=xEventGroupWaitBits((EventGroupHandle_t )EventGroupHandler, /* 句柄 */
(EventBits_t )EVENTBIT_ALL, /* 標志位 */
(BaseType_t )pdTRUE, /* 獲取成功后 清除 */
(BaseType_t )pdTRUE, /* 等待所有標志位 置位 */
(TickType_t )portMAX_DELAY); /* 阻塞 */
八、任務通知
任務通知可完成消息隊列、信號量、事件標志組的功能,不過任務通知是只能實現一對一的,也就是一個任務對一個任務
/* 通知值發送,設置通知值,可發送一個數據 */
u8 data;
BaseType_t err;
err=xTaskNotify((TaskHandle_t )Task_Handler, /* 接收任務通知的任務句柄 */
(uint32_t )data, /* 任務通知值 */
(eNotifyAction )eSetValueWithOverwrite); /* 覆寫的方式發送任務通知 */
/* 通知值發送,設置通知值,可做標記位組 */
#define EVENTBIT_1 (1<<1)
xTaskNotify((TaskHandle_t )Task_Handler, /* 接收任務通知的任務句柄 */
(uint32_t )EVENTBIT_1, /* 要更新的bit */
(eNotifyAction )eSetBits); /* 更新指定的bit */
/* 通知值獲取,獲取通知值,並判斷是否需要清零 */
BaseType_t err;
uint32_t NotifyValue;
err=xTaskNotifyWait((uint32_t )0x00, /* 進入函數,沒有接受到通知,不清除任何bit */
(uint32_t )ULONG_MAX, /* 退出函數,接受到通知,清除所有(0xffffffffUL)位的bit, */
(uint32_t* )&NotifyValue, /* 保存任務通知值 */
(TickType_t )portMAX_DELAY); /* 阻塞時間 */
九、臨界段
/* 任務內臨界段處理 */
taskEXIT_CRITICAL();
/* 任務處理 */
taskEXIT_CRITICAL();
/* 中斷內臨界段處理 */
taskENTER_CRITICAL_FROM_ISR();
/* 中斷內處理 */
taskEXIT_CRITICAL_FROM_ISR();
十、內存管理
/* 內存申請 */
u8 *buffer;
buffer=pvPortMalloc(30); /* 申請內存,30個字節 */
/* 釋放內存 */
vPortFree(buffer);
/* 獲取內存剩余空間 */
u32 freeSize;
freeSize = xPortGetFreeHeapSize();