文章目錄
CubeMX使用FreeRTOS編程指南
一、開發前言
1.1 軟件准備
-
STM32CubeMX 代碼生成軟件
-
MDK 集成代碼開發環境
1.2 開啟FreeRTOS
新建一個 CubeMX 工程,在配置好時鍾后,點擊 Middleware -> 選擇 FreeRTOS -> 下拉框選擇 V2 版本 CMSIS
到此在 CubeMX 中就已經開啟 FreeRTOS 系統了,下面分享 FreeRTOS 的配置:
二、配置界面
開啟 FreeRTOS 之后,可以看到配置項主要分為以下幾個部分
這幾個部分的主要功能如下表:
配置項 | 功能 |
---|---|
Tasks and Queues | 任務與隊列,用於配置任務體以及消息隊列; |
Timers and Semaphores | 軟件定時器與信號量,用於配置內核對象 (軟件定時器和信號量); |
Mutexes | 互斥量,用於配置內核對象(互斥量) |
Events | 事件,配置內核對象(事件) |
FreeRTOS Heap Usage | 查看用戶任務和系統任務的堆占用 |
Config Parameters | 系統的參數配置 |
Include Parameters | 系統的功能裁剪 |
Advanced Settings | CubeMX 生成代碼預配置項 |
User Constants | 用戶常量定義 |
以上各個功能分的很清晰,我們需要配置什么功能就去對應的選項下進行配置,下面根據各個配置項進行詳細配置介紹
三、系統設置
首先我們先了解一下 Config Parameters,他的配置參數如下
參數功能表:
參數 | 功能 |
---|---|
API | 顯示 FreeRTOS API 接口版本 |
Version | 顯示 FreeRTOS 內核版本 顯示 CMSIS 版本 |
Kernel Setting | FreeRTOS 調度內核設置 |
Memory management setting | 內存管理設置 |
Hook function related definitions | 鈎子函數有關定義 |
Run time and task stats gathering related definitions | 系統運行時的參數收集配置 |
Co-routine related definitions | 協程配置 |
Software timer definitons | 軟件定時器任務配置 |
Interrupt nesting behaviour configuration | 中斷優先級配置 |
API 和 Version 不過多解釋,顯示版本信息
2.1 調度內核設置
Kernel Setting 是 FreeRTOS 的調度內核配置,展開后有下面的配置項,使用時一般保持默認,也可以根據需要修改
- USE_PREEMPTION
USE_PREEMPTION 是 RTOS 的調度方式選擇,為 1 時使用搶占式調度器,為 0 時使用協程,如果使用搶占式調度器的話內核會在每個時鍾節拍中斷中進行任務切換,當使用協程的話會在如下地方進行任務切換
- 一個任務調用了函數 taskYIELD()。
- 一個任務調用了可以使任務進入阻塞態的 API 函數。
- 應用程序明確定義了在中斷中執行上下文切換。
- CPU_CLOCK_HZ
CPU_CLOCK_HZ 是 CPU 系統時鍾頻率,默認使用的是晶振通過時鍾樹后獲得的時鍾頻率
- TICK_RATE_HZ
TICK_RATE_HZ 是 RTOS 的心跳時鍾頻率,默認為最大值 1000 ,即心跳時鍾 1ms 跳動一次
- MAX_PRIORITIES
MAX_PRIORITIES 是 RTOS 任務的最高優先級設置,默認56級,一般來說一個優先級表是32位,這里用了兩個,對應64位,其中8位用於系統任務的優先級處理
- MINIMAL_STACK_SIZE
MINIMAL_STACK_SIZE 設置分配給空閑任務的堆棧大小,該值是用字(32位)指定的,而不是字節,默認為128個字,如果修改過空閑任務,則根據實際情況修改
- MAX_TASK_NAME_LEN
MAX_TASK_NAME_LEN 設置任務名稱的最大字符數,默認16位足夠
- USE_16_BIT_TICKS
USE_16_BIT_TICKS 存放 Tick 周期的計數器的數字位寬,默認為 Disable 即 16 位
- IDLE_SHOULD_YIELD
如果IDLE_SHOULD_YIELD 設置為0,則空閑任務永遠不會讓位於另一個任務,只在被搶占時才會離開運行狀態。如果 IDLE_SHOULD_YIELD 設置為1,那么當有另一個空閑優先級任務處於Ready狀態時,空閑任務將不會執行它定義的功能的不止一次迭代,而不會讓位於另一個任務,這確保當應用程序任務處於空閑狀態時,在空閑任務中花費的時間最少,即同在空閑優先級下,空閑任務優先級更高,不會被搶占,不會以時間片運行
- USE_MUTEXES、USE_RECURSIVE_MUTEXES、USE_COUNTING_SEMAPHORES
為 1 則開啟系統構建過程中的互斥量、遞歸互斥量和信號量,該值強制為1(ENABLE)
- QUEUE_REGISTRY_SIZE
隊列注冊表的大小,可以用於管理隊列名稱和隊列實體,方便運行中進行查看與管理,默認為8
- USE_APPLICATION_TASK_TAG
使能時會給任務一個 TAG 標簽,便於用戶進行使用
- ENABLE_BACKWARD_COMPATIBILITY
一個兼容性使能,使能后, FreeRTOS 8.0.0 之后的版本可以通過宏定義使用 8.0.0 版本之前的函數接口,默認使能
- USE_PORT_OPTIMISED_TASK_SELECTION
查找下一個任務方式的選擇,查找下一個就緒任務就是查找優先級表,對優先級表進行導0算法,分為通用切換或者針對性切換,一般默認不使能,使用通用切換,通用切換使用C編寫,執行效率低,兼容性高;針對性切換使用處理器自帶的導0指令,使用匯編編寫,切換效率高,但兼容性差
- USE_TICKLESS_IDLE
使能后會生成的兩個空函數PreSleepProcessing和PostSleepProcessing,用戶可以編寫代碼進入低功耗模式,生成函數如下圖
- USE_TASK_NOTIFICATIONS
任務通知使能,每個RTOS任務都有一個32位的通知值,RTOS任務通知是一個直接發送給任務的事件,它可以解除接收任務的阻塞,並可選地更新接收任務的通知值,為1開啟,為0關閉,關閉可以為每個任務節省8個字節的內存空間
- RECORD_STACK_HIGH_ADDRESS
記錄任務的堆棧入口地址到TCB,為1使能,為0關閉
2.2 內存管理設置
內存管理可以看到3個配置參數
- Memory Allocation
內存分配方式,此處默認動態和靜態都可以
- TOTAL_HEAP_SIZE
內存堆的分配大小,堆本質上就是一個數組,此處是設置堆數組的大小,設置時要考慮最小要滿足所有任務的使用要求,最大不要超過系統的分配上限
- Memory Management scheme
內存分配方式,有heap_1.c, heap_2.c, heap_3.c, heap_4.c and heap5.c 5種,其中1、2、4、5都是先建立一個堆數組,從數組中申請,用完再釋放,與C語言中molloc和free使用鏈表的方式不同,該方式在 MCU 中更安全穩定,此處默認使用的方式4,具體申請釋放方式可以在heap4.c中閱讀到
關於堆和棧的區別,可以閱讀我的另外一篇文章進行了解:C語言:內存四區
2.3 鈎子函數配置
鈎子函數是一種回調函數,用於在任務執行一次之后或者某些事件發生后執行的函數,該配置項里面有五個選項,控制5種不同功能的鈎子函數開啟,當然用戶也可以在代碼中自己定義
- USE_IDLE_HOOK
使能后,系統生成一個空回調函數,由用戶編寫函數主體
void vApplicationIdleHook(void)
每當空閑任務執行一次,鈎子函數都會被執行一次
- USE_TICK_HOOK
使能后,系統生成一個空回調函數,由用戶編寫函數主體
void vApplicationTickHook(void)
每個TICK周期,鈎子函數都會執行一次
- USE_MALLOC_FAILED_HOOK
使能后,系統生成一個空回調函數,由用戶編寫函數主體
void vApplicationMallocFailedHook(void)
當申請動態內存失敗時,鈎子函數會執行一次
- USE_DAEMON_TASK_STARTUP_HOOK
使能后,系統生成一個空回調函數,由用戶編寫函數主體
void vApplicationDaemonTaskStartupHook(void).
任務剛啟動時,鈎子函數會執行一次
- CHECK_FOR_STACK_OVERFLOW
使能后,系統生成一個空回調函數,由用戶編寫函數主體
void vApplicationStackOverflowHook( xTaskHandle xTask, signed char *pcTaskName );
任務棧溢出時,鈎子函數會執行一次,傳入任務 TCB 和任務名稱
當我們在 CubeMX 里面開啟對應鈎子函數,生成代碼之后,在FreeRTOS就可以看到自動生成的鈎子函數,我們在里面編寫相應的功能就行
2.5 任務運行追蹤配置
功能配置項如下:
- GENERATE_RUN_TIME_STATS
開啟時間統計功能,在調用 vTaskGetRunTimeStats() 函數時,將任務運行時間信息保存到可讀列表中
- USE_TRACE_FACILITY
使能后會包含額外的結構成員和函數以幫助執行可視化和跟蹤,默認開啟,方便 MDK 軟件工具調試使用
- USE_STATS_FORMATTING_FUNCTIONS
使能后會生成 vTaskList() 和 vTaskGetRunTimeStats() 函數用於獲取任務運行狀態
2.6 協程配置
Co-routine related definitions 是協程的配置項,兩個選項用來配置協程是否開啟,以及協程的優先級,開啟后,需要用戶手動創建協程,在協程幾乎很少用到了,是 FreeRTOS目前還沒有把協程移除的計划,但 FreeRTOS是不會再更新和維護協程了,因此大家解一下就行
協程特點:
- 堆棧使用
所有的協程使用同一個堆棧(如果是任務的話每個任務都有自己的堆棧),這樣就比使用任務消耗更少的 RAM - 調度器和優先級
協程使用合作式的調度器,但是可以在使用搶占式的調度器中使用協程 - 宏實現
協程是通過宏定義來實現的 - 使用限制
為了降低對 RAM 的消耗做了很多的限制
具體 API 接口和調度原理可以參考這篇文章 : FreeRTOS協程
2.7 軟件定時器配置
軟件定時器配置的一些相關項如下:
這四個配置項主要與軟件定時器處理任務有關,軟件定時器任務屬於系統任務(守護線程),開啟軟件定時器后用於維護軟件定時器
- USE_TIMERS
默認開啟軟件定時器任務
- TIMER_TASK_PRIORITY
軟件定時器任務優先級
- TIMER_QUEUE_LENGTH
定時器任務隊列長度,FreeRTOS 是通過隊列來發送控制命令給定時器任務,叫做定時器命令隊列,此處設置隊列長度
- TIMER_TASK_STACK_DEPTH
軟件定時器任務堆棧大小
2.8 中斷優先級配置
- LIBRARY_LOWEST_INTERRUPT_PRIORITY
此宏是用來設置最低優先級,FreeRTOS 使用的4位優先級,對應16位優先級,對應的最低優先級為15
- LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
設置FreeRTOS 系統可管理的最大優先級,也就是設置閾值優先級,這個大家可以自由設置,這里設置為5,也就是高於5 的優先級(優先級數小於5)不歸 FreeRTOS 管理
三、內核裁剪
Include Parameters 下的選項應用於內核裁剪,裁剪不必要的功能,精簡系統功能,減少資源占用,主要有以下幾個選項:
配置項可裁剪的函數功能如下:
選項 | 功能 |
---|---|
vTaskPrioritySet | 改變某個任務的任務優先級。 |
uxTaskPriorityGet | 查詢某個任務的優先級。 |
vTaskDelete | 刪除任務 |
vTaskCleanUpResources | 回收任務刪除后的資源如RAM等等 |
vTaskSuspend | 掛起任務 |
vTaskDelayUntil | 阻塞延時一段絕對時間(絕對延時去去除程序執行時間,執行更精准) |
vTaskDelay | 阻塞延時一段相對時間 |
xTaskGetSchedulerState | 獲取任務調度器的狀態,開啟或未開啟 |
xTaskResumeFromISR | 在中斷服務函數中恢復一個任務的運行 |
xQueueGetMutexHolder | 獲取信號量的隊列擁有者,返回擁有此信號量的隊列 |
xSemaphoreGetMutexHolder | 查詢擁有互斥鎖的任務,返回任務控制塊 |
pcTaskGetTaskName | 獲取任務名稱 |
uxTaskGetStackHighWaterMark | 獲取任務的堆棧的歷史剩余最小值,FreeRTOS 中叫做“高水位線” |
xTaskGetCurrentTaskHandle | 此函數用於獲取當前任務的任務句柄,就是獲取當前任務控制塊 |
eTaskGetState | 此函數用於查詢某個任務的運行壯態,比如:運行態、阻塞態、掛起態、就緒態等 |
xEventGroupSetBitFromISR | 在中斷服務函數中將指定的事件位清零 |
xTimerPendFunctionCall | 定時器守護任務的回調函數(定時器守護任務使用到一個命令隊列,只要向隊列發送信號就可以執行相應代碼,可以實現“中斷推遲處理”功能) |
xTaskAbortDelay | 中止延時函數,該函數能立即解除任務的阻塞狀態,將任務插入就緒列表中 |
xTaskGetHandle | 此函數根據任務名字獲取的任務句柄(控制塊) |
四、創建任務與隊列
4.1 CubeMX 下任務創建與配置
任務(線程)是操作系統運行的基本單元,也是資源分配的基本單元, CubeMX 任務的創建基本以圖形化進行,配置方式如下
進入Tashs and Queues 配置,點擊 Add 添加新任務
任務配置參數介紹
參數 | 功能 |
---|---|
Task Name | 任務名稱,保存在 TCB 結構體中,設置時自己起名字 |
Priority | 任務優先級,任務的調度等級,根據自己創建任務的緊急程度設定 比如通信任務不能被打斷,可以設計較高優先級 |
Stack Size(Words) | 設定給任務分配的內存大小,單位是字,對於32位單片機來說占4個字節 |
Entry Function | 任務實體,即任務的運行函數名 |
Code Generation | 代碼生成模式 As weak: 產生一個用 __weak 修飾的弱定義任務函數,用戶可自己在進行定義; As external: 產生一個外部引用的任務函數,用戶需要自己定義該函數; Default: 產生一個默認格式的任務函數,用戶需要在該函數內實現自己的功能 |
Parameter: | 傳入的參數,保持默認就行 |
Allocation: | 內存分配方式 Static: 靜態方式是直接在RAM占據一個靜態空間 Dynamic:動態方則是在初始配置的內存池大小數組中動態申請、釋放空間 |
設置完成后點擊OK,配置就完成了,之后生成代碼,使用 MDK 進一步配置任務的具體信息
在生成的代碼中,我們打開 freertos.c 文件可以在代碼中看到任務的配置信息
在 freertos.c 文件的末尾部分,我們可以看到生成的任務實體
任務實體本身就是一個死循環函數,循環執行程序代碼,但循環體代碼里面必須要有延時函數,釋放當前任務對 MCU 的控制權,使其他低優先級可以執行,此外,關於任務,CubeMX 提供了一系列的用戶調用接口函數,具體如下
函數 | 功能 |
---|---|
osThreadNew | 創建新任務 |
*osThreadGetName | 獲取任務名稱 |
osThreadGetId | 獲取當前任務的控制塊(TCB) |
osThreadGetState | 獲取當前任務的運行狀態 |
osThreadGetStackSize | 獲取任務的堆棧大小 |
osThreadGetStackSpace | 獲取任務剩余的堆棧大小 |
osThreadSetPriority | 設定任務優先級 |
osThreadGetPriority | 獲取任務優先級 |
osThreadYield | 切換控制權給下一個任務 |
osThreadSuspend | 掛起任務 |
osThreadResume | 恢復任務(掛起多少次恢復多少次) |
osThreadDetach | 分離任務,方便任務結束進行回收 |
osThreadJoin | 等待指定的任務停止 |
osThreadExit | 停止當前任務 |
osThreadTerminate | 停止指定任務 |
osThreadGetCount | 獲取激活的任務數量 |
osThreadEnumerate | 列舉激活的任務 |
4.2 CubeMX 下隊列的創建與配置
隊列,又稱為消息隊列,用於任務間的數據通信,傳輸數據,在操作系統里面,直接使用全局變量傳輸數據十分危險,看似正常運行,但不知道啥時候就會因為寄存器或者內存等等原因引起崩潰,所以引入消息,隊列的概念,任務發送數據到隊列,需要接受消息的任務掛起在隊列的掛起列表,等待消息的到來,CubeMX 創建隊列的步驟如下:
先點擊 Add 添加隊列
隊列配置參數介紹
參數 | 功能 |
---|---|
Queue Name | 隊列名稱(自己設定) |
Queue Size | 消息隊列大小 |
Item Size | 隊列傳輸類型,保持默認16 位就行 |
Allocation | 隊列內存的分配方式 Static: 靜態方式是直接在RAM占據一個靜態空間 Dynamic:動態方則是在初始配置的內存池大小數組中動態申請、釋放空間 |
配置需要的參數后,點擊OK,然后生成代碼
生成代碼后,我們可以在 freertos.c 中系統初始話函數中看到隊列的初始化
初始化函數會在一開始被調用,對 FreeRTOS 系統和內核對象進行初始化,初始化后系統就可以進行調度和使用內核對象,CubeMX 生成的代碼自動將創建的內核對象放到初始化函數內,所以我們在任務和中斷中直接使用就可以,隊列的 FreeRTOS API 接口在CubeMX 內再次進行了封裝,使用更加簡單,使用方式如下:
我們使用的 CMSIS 2.0 版本,所以在任務文件中包含調用聲明頭文件
#include "cmsis_os2.h"
在隊列頭文件內我們可以在 600 多行的位置找到有關隊列的 API 函數聲明:
下面介紹一下隊列有關接口的函數接口:
函數 | 功能 |
---|---|
osMessageQueueNew | 創建並初始化一個新的隊列 |
osMessageQueueGetName | 獲取隊列的名字 |
osMessageQueuePut | 發送一條消息到隊列 |
osMessageQueueGet | 從隊列等待一條消息 |
osMessageQueueGetCapacity | 獲取隊列傳輸消息的峰值 |
osMessageQueueGetMsgSize | 獲取隊列使用內存池的最大峰值 |
osMessageQueueGetCount | 獲取隊列的消息數量 |
osMessageQueueGetSpace | 獲取隊列剩余的可用空槽 |
osMessageQueueReset | 清空隊列 |
osMessageQueueDelete | 刪除隊列 |
以上的API接口有其對應的傳入參數,具體使用方式需要在翻源碼的注釋,這里我選常用的來介紹一下:
消息隊列常用的是插入與獲取消息,初始化系統已經幫助我們完成,在初始化的時候會獲取一個隊列的句柄,之后對隊列的操作都是圍繞這個句柄展開,比如上面的代碼中,句柄就是 myQueue01Handle
,我們發送一個消息到這個隊列,就是調用發送函數,對句柄進行操作,先看一下發送消息的函數原型
osStatus_t osMessageQueuePut (osMessageQueueId_t mq_id, const void *msg_ptr, uint8_t msg_prio, uint32_t timeout);
參數的功能
參數 | 功能 |
---|---|
mq_id | 傳入隊列的句柄 |
*msg_ptr | 指向需要發送的消息內容的指針 |
msg_prio | 本次發送消息的優先級(目前API未加入功能) |
timeout | 發送消息的超時時間(設置為0代表一直等待發送成功) |
osStatus_t(返回值) | 返回執行結果 |
返回值的可能
錯誤 | 含義 |
---|---|
osOK | 執行正常 |
osError | 系統錯誤 |
osErrorTimeout | 執行超時 |
osErrorResource | 資源不可用 |
osErrorParameter | 參數無效 |
osErrorNoMemory | 內存不足 |
osErrorISR | 不允許在中斷調用 |
osStatusReserved | 防止編譯器優化項,不需要管他 |
所以我們發送一個消息到隊列,函數用法如下:
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
osStatus_t result;
uint8_t dat[]="666\r\n";
/* Infinite loop */
for(;;)
{
result= osMessageQueuePut(&myQueue01Handle,dat,1,0);
if(result == osOK)
{
//發送成功
}else
{
//發送失敗
}
osDelay(1);
}
/* USER CODE END StartTask02 */
}
發送消息的優先級暫時無用,CubeMX 對 FreeRTOS 的支持還不完善,發送消息里面的優先級未使用到,並且入隊方式使用的是發送到隊列尾部,沒有從頭部插入的方式,有需求可以 通過包含 queue.h 文件,調用 FreeRTOS 的官方代碼,或者自己修改 生成代碼的 API 接口結合優先級使用隊列的向前插入和向后插入,豐富系統功能!
除了發送消息到隊列,接受隊列的消息 API 接口也經常用到,函數原型如下
osStatus_t osMessageQueueGet (osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout);
參數的功能
參數 | 功能 |
---|---|
mq_id | 接受隊列的句柄 |
*msg_ptr | 用於接受消息內容的指針 |
msg_prio | 存放接受消息的優先級(目前API未加入功能) |
timeout | 接受消息的超時時間(設置為10代表,當前任務掛起在掛起列表,直到接收成功時恢復,或者10個TICK等待周期到達然后任務強行恢復,不再等待,為0則是不等待,等待期間任務掛起在內核對象的掛起隊列) |
osStatus_t(返回值) | 返回執行結果 |
函數用法
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
osStatus_t result;
uint8_t dat[10]={};
uint8_t *pro;
/* Infinite loop */
for(;;)
{
result= osMessageQueueGet(&myQueue01Handle,dat,pro,10);
if(result == osOK)
{
//接受成功
}else
{
//接受失敗
}
osDelay(1);
}
/* USER CODE END StartTask02 */
}
注意:FreeRTOS 中獲取和發送消息的 API 接口函數分為任務中調用和中斷中調用,CubeMX 代碼接口將兩者整合了,調用時自動判斷調用環境是在 ISR 還是正常運行環境中
五、創建定時器和信號量
5.1 CubeMX下定時器的創建和配置
軟件定時器本質上就是設置一段時間,當設置的時間到達之后就執行指定的功能函數,調用的這個函數叫做回調函數。回調函數的兩次執行間隔叫做定時器的定時周期,簡而言之,當定時器的定時周期到了以后就會執行回調函數,下面介紹一下 CubeMX 中開啟定時器的方法:
在 CubeMX 里面按下面步驟添加定時器
然后配置具體參數,參數的功能如下:
參數 | 功能 |
---|---|
Timer Name | 設置定時器的名稱 |
Callback | 設定定時器的回調函數體 |
Type | 設定定時器的執行類型 osTimerPeriodic 定時器周期執行回調函數 osTimerOnce 定時器只執行一次回調函數 |
Code Generation Option | 代碼生成模式 As weak: 產生一個用 __weak 修飾的弱定義任務函數,用戶可自己在進行定義; As external: 產生一個外部引用的任務函數,用戶需要自己定義該函數; Default: 產生一個默認格式的任務函數,用戶需要在該函數內實現自己的功能 |
Parameter | 傳入參數,保持默認NULL就行 |
Allocation | 軟件定時器內存的分配方式,一般使用動態 Static: 靜態方式是直接在RAM占據一個靜態空間 Dynamic:動態方則是在初始配置的內存池大小數組中動態申請、釋放空間 |
參數配置完成后,生成代碼,我們可以在 freertos.c 文件里面看到定時器創建后獲得的句柄,以及生成的回調函數:
有了句柄,我們就可以調用 cmsis_os2.c 里面的定時器接口函數對定時器進行操作,先看一下 CubeMX 提供的定時器接口函數及其功能
函數 | 功能 |
---|---|
osTimerNew | 新建定時器,返回定時器控制句柄 |
osTimerGetName | 獲取定時器名稱 |
osTimerStart | 設置定時器周期,啟動定時器 |
osTimerStop | 停止定時器 |
osTimerIsRunning | 檢測定時器是否在運行 |
osTimerDelete | 刪除定時器 |
其中常用的接口是定時器的啟動和停止
定時器啟動: osTimerStart
,函數原型
osStatus_t osTimerStart (osTimerId_t timer_id, uint32_t ticks);
參數介紹:
參數 | 功能 |
---|---|
timer_id | 需要啟動的定時器句柄 |
ticks | 設置定時器的運行周期 |
此處的 ticks 設定的數字是定時器兩次調用回調函數的周期數目,每個 tick 是一個心跳時鍾的長度
使用例程:
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
osStatus_t result;
uint8_t dat[10]={0};
uint8_t *pro;
result= osTimerStart(&myTimer01Handle,10);
if(result == osOK)
{
//啟動成功
}else
{
//啟動失敗
}
/* Infinite loop */
for(;;)
{
osDelay(10);
}
/* USER CODE END StartTask02 */
}
按照例程啟動定時器,定時器會以 10個tick 的周期,調用回調函數
回調函數不要放阻塞函數,程序盡可能短
定時器啟動: osTimerStop
,函數原型
osStatus_t osTimerStop (osTimerId_t timer_id);
參數只有一個,就是定時器的控制句柄,傳入即可停止定時器,例程如下
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
osStatus_t result;
uint8_t dat[10]={0};
uint8_t *pro;
result= osTimerStop(&myTimer01Handle);
if(result == osOK)
{
//停止成功
}else
{
//停止失敗
}
/* Infinite loop */
for(;;)
{
osDelay(10);
}
/* USER CODE END StartTask02 */
}
軟件定時器是由軟件定時器維護任務進行維護,檢測各個定時器的狀態,進行處理,回調回調函數,軟件定時器維護任務的參數配置在前面的 Config 就已經提到過
5.2 CubeMX下信號量的創建和配置
信號量是 RTOS 的一個內核對象,該對象有一個隊列表示該信號量擁有的信號數目,任何任務都可以對這個信號數目進行獲取和釋放,獲取時信號-1,釋放時信號+1,為0時不能繼續獲取,此時有任務想要繼續獲取信號量的話,任務會掛起在該內核對象的掛起列表,等到信號可以獲取時進行恢復,根據這個特性,信號量常用於控制對共享資源的訪問和任務同步,下面介紹一下 CubeMX 下信號量的配置:
點開配置頁面,可以看到有兩個信號量添加頁面,其中 Binary Semaphores 是二值信號量,Counting Semaphores 是計數信號量,二進制信號量,僅有一個隊列或者說 token,用於同步一個操作;計數信號量則擁有多個 tokens,可用於同步多個操作,或者管理有限資源
二值信號量創建:
點擊 Add,配置參數
參數介紹
參數 | 功能 |
---|---|
Semaphore Name | 信號量名稱 |
Allocation | 內存分配方式,一般使用動態 Static: 靜態方式是直接在RAM占據一個靜態空間 Dynamic:動態方則是在初始配置的內存池大小數組中動態申請、釋放空間 |
計數信號量:
點擊 Add,配置參數
參數介紹
參數 | 功能 |
---|---|
Semaphore Name | 信號量名稱 |
Count | 計數信號量的最大數目 |
Allocation | 內存分配方式,一般使用動態 Static: 靜態方式是直接在RAM占據一個靜態空間 Dynamic:動態方則是在初始配置的內存池大小數組中動態申請、釋放空間 |
配置完成后我們生成代碼,在 freertos.c 的初始化代碼中可以看到信號量被創建,並且返回了信號量的控制句柄
下面介紹一下 CubeMX 提供的信號量操作函數接口:
函數 | 功能 |
---|---|
osSemaphoreNew | 創建新的信號量 |
*osSemaphoreGetName | 獲取信號量的名稱 |
osSemaphoreAcquire | 獲取信號量 |
osSemaphoreRelease | 釋放信號量 |
osSemaphoreGetCount | 獲取當前可用信號量的數目 |
osSemaphoreDelete | 刪除信號量 |
其中常用的函數有獲取和釋放信號量,下面介紹一下這兩個函數的參數和使用方式
獲取信號量 osSemaphoreAcquire
函數原型
osStatus_t osSemaphoreAcquire (osSemaphoreId_t semaphore_id, uint32_t timeout);
參數介紹
參數 | 功能 |
---|---|
semaphore_id | 傳入要獲取信號量的控制句柄 |
timeout | 獲取等待時間(等待期間任務掛起在內核對象的掛起隊列) |
使用例程
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osSemaphoreAcquire(&myBinarySem01Handle,10);
if(result == osOK)
{
//獲取成功
}else
{
//獲取失敗
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
釋放信號量 osSemaphoreRelease
函數原型
osStatus_t osSemaphoreRelease (osSemaphoreId_t semaphore_id);
參數 | 功能 |
---|---|
semaphore_id | 傳入要釋放的信號量控制句柄 |
使用例程
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osSemaphoreRelease(&myBinarySem01Handle);
if(result == osOK)
{
//釋放成功
}else
{
//釋放失敗
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
二值信號量和計數信號量的操作基本一致,沒用區別,只是用有的信號隊列最大數目不同而已
同時注意信號量在使用過程中會出現優先級反轉的Bug,使用時需要注意
六、創建互斥量
6.1 CubeMX下互斥量的創建和配置
互斥量其實就是一個擁有優先級繼承的二值信號量,互斥信號量適合用於那些需要互斥訪問的應用中,在互斥訪問中互斥信號量相當於一個鑰匙,當任務想要使用資源的時候就必須先獲得這個鑰匙,當使用完資源以后就必須歸還這個鑰匙,這樣其他的任務就可以拿着這個鑰匙去使用資源,與信號量不同的是,互斥量的釋放必須由獲取他的任務進行釋放,如果不釋放,可能會造成死鎖
死鎖就是兩個任務獲取對方擁有的鎖,各自進入掛起列表,無法釋放互斥鎖
下面介紹一下 CubeMX 下互斥量的配置,在配置界面我們可用看到兩個互斥量配置界面,上面的是普通互斥量,其獲取只能獲取一次,重復獲取是無效的,而第二個則是遞歸互斥量,遞歸互斥信號量可以獲取多次,但對應的也要釋放多次才能讓出使用權,比如我獲取3次,任務要釋放3次才能釋放該互斥量的使用權
使用互斥量,需要點擊 Add 然后配置參數
參數介紹:
參數 | 功能 |
---|---|
Mutex Name | 互斥量名稱 |
Allocation | 內存分配方式,一般使用動態 Static: 靜態方式是直接在RAM占據一個靜態空間 Dynamic:動態方則是在初始配置的內存池大小數組中動態申請、釋放空間 |
遞歸互斥信號量的配置方式與其相同,包括配置參數也相同,兩者只是在用法上有些許區別,添加方式如下:
添加配置完成后,點擊生成代碼,在 freertos.c 文件中我們可以看到互斥量初始化完成,並且生成了對應的控制句柄
CubeMX 提供的 API 接口函數如下
函數 | 功能 |
---|---|
osMutexNew | 創建互斥量 |
*osMutexGetName | 獲取互斥量名稱 |
osMutexAcquire | 任務獲取互斥量 |
osMutexRelease | 任務釋放互斥量 |
osMutexGetOwner | 獲取互斥量的擁有任務的任務 TCB |
osMutexDelete | 刪除互斥量 |
主要使用到的還是互斥量的獲取與釋放,下面分析一下這兩個函數:
獲取互斥量 osMutexAcquire
函數原型
osStatus_t osMutexAcquire (osMutexId_t mutex_id, uint32_t timeout);
參數介紹:
參數 | 功能 |
---|---|
mutex_id | 互斥量控制句柄 |
timeout | 獲取互斥量時的等待時間(等待期間任務掛起在內核對象的掛起隊列) |
使用方式
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
osStatus_t result;
result= osMutexAcquire(&myMutex01Handle,10);
if(result == osOK)
{
//獲取成功
}else
{
//獲取失敗
}
/* Infinite loop */
for(;;)
{
osDelay(10);
}
/* USER CODE END StartTask02 */
}
釋放互斥量 osMutexRelease
函數原型
osStatus_t osMutexRelease (osMutexId_t mutex_id);
參數介紹:
參數 | 功能 |
---|---|
mutex_id | 互斥量控制句柄 |
使用方式
void StartTask02(void *argument)
{
/* USER CODE BEGIN StartTask02 */
osStatus_t result;
result= osMutexRelease(&myMutex01Handle);
if(result == osOK)
{
//釋放成功
}else
{
//釋放失敗
}
/* Infinite loop */
for(;;)
{
osDelay(10);
}
/* USER CODE END StartTask02 */
}
使用方式和信號量基本相同,因為互斥量本質上就是信號量的一種
七、創建事件標志組
7.1 CubeMX下事件的創建和配置
任務間的同步除了信號量還有時間標志組,信號的同步通常是一對一的同步,有的時候系統需要多對一的同步,比如同時滿足5個按鍵按下時,任務啟動,如果使用信號會很占據資源,所以 RTOS 引入了事件標志組來滿足這一需求,下面我們看一下 CubeMX 內事件標志組的配置方法:
點擊 Add 創建事件標志組
配置介紹
參數 | 功能 |
---|---|
Event flags Name | 事件標志組名稱 |
Allocation | 內存分配方式,一般使用動態 Static: 靜態方式是直接在RAM占據一個靜態空間 Dynamic:動態方則是在初始配置的內存池大小數組中動態申請、釋放空間 |
配置完成后,生成代碼,在系統初始化內,看有沒有生成事件標志組控制句柄,可以看到句柄創建完成
CubeMX 提供的配置事件標志組的接口 API 如下:
函數 | 功能 |
---|---|
osEventFlagsNew | 創建事件標志組 |
*osEventFlagsGetName | 獲取事件標志組名稱 |
osEventFlagsSet | 設置事件標志組 |
osEventFlagsClear | 清除事件標志組 |
osEventFlagsGet | 獲取當前事件組標志信息 |
osEventFlagsWait | 等待事件標志組觸發 |
osEventFlagsDelete | 刪除事件標志組 |
常用的 API 接口是設置事件標志組以及等待事件標志組的觸發,下面我們分析一下這兩個 API
在了解 API 前我們需要簡單了解一下事件的觸發原理:首先事件標志組的數據類型為 EventGroupHandle_t,事件標志組中的所有事件位都存儲在一個無符號的 EventBits_t 類型的變量中,當 configUSE_16_BIT_TICKS 為 1 的時候事件標志組可以存儲 8 個事件位,當 configUSE_16_BIT_TICKS 為 0 的時候事件標志組存儲 24個事件位,每個事件位其實就是一個0或者1數字,就像下面的24位組成一個事件標志組
我們在使用事件API接口函數前需要先定義我們需要的觸發事件位,比如添加如下的代碼
#define event1 1<<1 //事件1
#define event2 1<<2 //事件2
編寫好觸發事件后,我們在看如何使用 API 接口
設置事件標志 osEventFlagsSet
函數原型
uint32_t osEventFlagsSet (osEventFlagsId_t ef_id, uint32_t flags);
參數介紹:
參數 | 功能 |
---|---|
ef_id | 事件標志組控制句柄 |
flags | 事件位 |
使用方式:設置事件1和事件2
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osEventFlagsSet(&myEvent01Handle,event1);
if(result == osOK)
{
//事件1設置成功
}else
{
//事件1設置失敗
}
result = osEventFlagsSet(&myEvent01Handle,event2);
if(result == osOK)
{
//事件2設置成功
}else
{
//事件2設置失敗
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
等待事件標志 osEventFlagsWait
函數原型
uint32_t osEventFlagsWait (osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout);
參數介紹:
參數 | 功能 |
---|---|
ef_id | 事件標志組控制句柄 |
flags | 等待的事件位 |
options | 等待事件位的操作 osFlagsWaitAny :等待的事件位有任意一個等到就恢復任務 osFlagsWaitAll:等待的事件位全部等到才恢復任務 osFlagsNoClear:等待成功后不清楚所等待的標志位(默認清除) |
timeout | 等待事件組的等待時間(等待期間任務掛起在內核對象的掛起隊列) |
使用例子:同時等待事件1和事件2,且等待到不清除
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osEventFlagsWait(&myEvent01Handle,event1|event2,osFlagsWaitAll|osFlagsNoClear,10);
if(result == osOK)
{
//等待成功
}else
{
//等待失敗
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
八、用戶常量
User Constants 用於添加用戶常量,將不變的量轉化為常量保存,可以節省 RAM 資源空間,因為常量和變量的保存位置不同,詳細了解可以參考這篇文章:C語言:內存四區
九、任務通知
FreeRTOS 的每個任務都有一個 32 位的通知值,任務控制塊中的成員變量 ulNotifiedValue 就是這個通知值。任務通知是一個事件,假如某個任務通知的接收任務因為等待任務通知而阻塞的話,向這個接收任務發送任務通知以后就會解除這個任務的阻塞狀態,CubeMX內沒有提供相關的配置項,但在其生成的 FreeRTOS 接口里面有相關函數進行配置,函數位置如下:
接口函數功能:
函數 | 功能 |
---|---|
osThreadFlagsSet | 設置任務的通知標志 |
osThreadFlagsClear | 清除任務通知 |
osThreadFlagsGet | 獲取任務標志 |
osThreadFlagsWait | 等待特定的任務標志 |
常用的兩個 API 就是設置任務通知和等待任務通知函數
設置通知 osThreadFlagsSet
函數原型
uint32_t osThreadFlagsSet (osThreadId_t thread_id, uint32_t flags);
參數介紹:
參數 | 功能 |
---|---|
thread_id | 任務控制塊 |
flags | 設置的標志 |
使用方式
先定義一個事件標志
#define event1 1<<1 //事件1
然后調用 API 通知對應任務事件發生
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osThreadFlagsSet(&myTask02Handle,event1);
if(result == osOK)
{
//設置成功
}else
{
//設置失敗
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
等待通知 osThreadFlagsWait
函數原型
uint32_t osThreadFlagsWait (uint32_t flags, uint32_t options, uint32_t timeout);
參數介紹:
參數 | 功能 |
---|---|
flags | 設置的標志 |
options | 設置功能 |
timeout | 超時時間 |
options參數
參數 | 功能 |
---|---|
osFlagsWaitAny | 等待32位通知值任意一位觸發后恢復任務(默認) |
osFlagsWaitAll | 等待指定的任務通知值全部觸發后再恢復任務 |
osFlagsNoClear | 恢復任務后不清除任務標志(默認清除) |
使用方式
調用 API 等待對應的任務通知就緒,當其他任務設置到對應的通知后,任務恢復運行
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
osStatus_t result;
/* Infinite loop */
for(;;)
{
result = osThreadFlagsWait(&myTask02Handle,osFlagsWaitAll,event1);
if(result == osOK)
{
//等待成功
}else
{
//等待失敗
}
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
任務通知其實個任務事件標志組使用上沒有多大的區別,但他們兩個的實現原理不同,同時任務通知對資源的占用更少
根據 FreeRTOS 官方的統計,使用任務通知替代二值信號量的時候任務解除阻 塞的時間要快 45%,並且需要的 RAM 也更少
十、系統內核配置
CubeMX 生成的代碼中封裝了一系列內核配置函數,有些函數也經常使用到,比如獲取時間戳和調度器管理的函數,這里不做過多解釋,簡單的介紹一下函數的功能
函數 | 功能 |
---|---|
osKernelInitialize | 初始化RTOS的內核 |
osKernelGetInfo | 獲取RTOS的信息 |
osKernelGetState | 獲取當前內核的運行狀態 |
osKernelStart | 啟動內核調度 |
osKernelLock | 鎖內核調度器 |
osKernelUnlock | 解鎖內核調度器 |
osKernelRestoreLock | 恢復RTOS內核調度器鎖狀態 |
osKernelSuspend | 掛起任務 |
osKernelResume | 恢復任務 |
osKernelGetTickCount | 用於獲取系統當前運行的時鍾節拍數 |
osKernelGetTickFreq | 用於獲取系統當前運行的時鍾節拍的分頻頻率 |
osKernelGetSysTimerCount | 獲取系統時鍾(SysTick)的計數值 |
osKernelGetSysTimerFreq | 獲取系統時鍾(SysTick)的頻率 |