系列文章目錄
留空
文章目錄
- 系列文章目錄
- 前言
- 一、從頭開始創建一個FreeRTOS工程
- 1.1 在 "Timebase Source" 中,選擇其他TIM
- 1.2 配置FreeRTOS的參數
- 1. 3 添加任務
- 二、動態任務的創建/刪除
- 2.1 函數介紹
- 2.1.1 創建動態任務`xTaskCreate()`
- 2.1.2 創建靜態任務`xTaskCreateStatic()`
- 2.1.3 刪除任務 `vTaskDelete()`
- 2.2 編寫例題代碼
- 2.2.1 添加任務
- 2.2.2 編寫任務
- 2.2.3 完整代碼
- 三、任務掛起與恢復
- 3.1 函數介紹
- 3.1.1 任務掛起`vTaskSuspend()`
- 3.1.2 任務恢復 `vTaskResume()`
- 3.1.3 從中斷任務恢復 `xTaskResumeFromISR()`
- 3.1.4 獲取任務狀態 `eTaskGetState()`
- 3.2 編寫例題代碼
- 3.2.1 任務掛起/恢復
- 3.2.2 從中斷恢復任務
- 四、FreeRTOS中斷管理
- 4.1 概念理解
- 4.1.1 中斷管理
- 4.1.2 中斷優先級推薦設置
- 4.1.3 FreeRTOS相關宏
- 4.2 函數介紹
- 4.2.1 禁用中斷 `portDISABLE_INTERRUPTS()`
- 4.2.2 啟用中斷 `portENABLE_INTERRUPTS()`
- 4.3 編寫例題代碼
- 五、臨界段代碼保護及任務調度器的掛起和恢復
- 5.1 概念理解
- 5.1.1 臨界段代碼保護
- 5.1.2 任務調度器
- 5.2 函數介紹
- 5.2.1 臨界段保護函數(任務級)
- 5.2.2 臨界段保護函數(中斷級)
- 5.2.3 任務調度器的掛起和恢復函數
- 六、列表和列表項
- 6.1 概念理解
- 6.2 函數介紹
- 6.2.1 列表/列表項結構體
- 6.2.2 初始化列表
- 6.2.3 初始化列表項
- 6.2.4 列表項插入列表
- 6.2.5 列表項末尾插入列表
- 6.2.6 列表移出列表項
- 6.3 編寫例題代碼
- 七、啟動任務調度器【內容太多,先略】
- 7.1 概念理解
- 八、時間片調度
- 8.1 概念理解
- 8.2 函數介紹
- 8.3 編寫例題代碼
- 總結
前言
自用
豬豬豬:還在更新中
因為參加完藍橋杯后,想學RTOS,所以直接無縫銜接,此筆記是基于藍橋杯板子G431RBT6學習的!
一、從頭開始創建一個FreeRTOS工程
基本的配置跳過,只記錄有關FreeRTOS的創建!
1.1 在 “Timebase Source” 中,選擇其他TIM
在 STM32 + FreeRTOS 項目中,FreeRTOS 默認使用 SysTick
作為時基,而 STM32CubeMX 默認的 HAL 庫也是使用 SysTick
,這兩個會沖突,導致系統運行不正常,尤其是出現任務調度異常、延時失效等問題。
所以把Timebase Source
改成了TIM17
!
1.2 配置FreeRTOS的參數
關鍵參數(全部默認即可)
參數名稱 | 設置值 | 描述 |
---|---|---|
USE_PREEMPTION | Enabled | 啟用搶占式調度,允許高優先級任務搶占低優先級任務的CPU時間。 |
CPU_CLOCK_HZ | SystemCoreClock | CPU的時鐘頻率,通常由系統定義,表示處理器的時鐘速度。 |
TICK_RATE_HZ | 1000 | 系統的時基(tick)頻率為1000Hz,即每1毫秒產生一個tick。 |
MAX_PRIORITIES | 56 | 系統中任務的最大優先級數,FreeRTOS使用優先級來調度任務。 |
MINIMAL_STACK_SIZE | 128 Words | 任務的最小堆棧大小為128個詞(word)。 |
MAX_TASK_NAME_LEN | 16 | 任務名稱的最大長度為16個字符。 |
TOTAL_HEAP_SIZE | 3072 Bytes | 為FreeRTOS堆分配的總內存大小為3072字節。 |
Memory Management scheme | heap_4 | 使用的內存管理方案,不同的方案可能有不同的內存分配和釋放策略。 |
以下是 FreeRTOS Mode and Configuration 界面中全部參數,按功能模塊分類(可跳過)
(1)Kernel Settings(內核設置)
參數名稱 | 當前配置值 | 含義說明 |
---|---|---|
USE_PREEMPTION | Enabled | 啟用搶占式調度(高優先級任務可立即搶占低優先級任務) |
CPU_CLOCK_HZ | SystemCoreClock | CPU時鐘頻率(通常由MCU定義,如SystemCoreClock=16MHz ) |
TICK_RATE_HZ | 1000 | 系統Tick頻率(1kHz=1ms一個Tick) |
MAX_PRIORITIES | 56 | 最大任務優先級數(0為最低,55為最高) |
MINIMAL_STACK_SIZE | 128 Words | 空閑任務(Idle Task)的堆棧大小(單位:字,具體字節數需乘以字長) |
MAX_TASK_NAME_LEN | 16 | 任務名稱的最大字符長度 |
USE_16_BIT_TICKS | Disabled | 禁用16位Tick計數器(使用32位計數器,支持更長運行時間) |
IDLE_SHOULD_YIELD | Enabled | 空閑任務主動讓出CPU給同等優先級的用戶任務(節能場景可能需要禁用) |
USE_PORT_OPTIMISED _TASK_SELECTION | Disabled | 禁用硬件優化任務選擇(通用軟件實現,兼容性更好) |
USE_TICKLESS_IDLE | Disabled | 禁用Tickless低功耗模式(始終維持Tick中斷) |
(2)Mutexes & Semaphores(互斥量與信號量)
參數名稱 | 當前配置值 | 含義說明 |
---|---|---|
USE_MUTEXES | Enabled | 啟用互斥量(Mutex)支持。 |
USE_RECURSIVE_MUTEXES | Enabled | 啟用遞歸互斥量(同一任務可重復加鎖)。 |
USE_COUNTING_SEMAPHORES | Enabled | 啟用計數信號量。 |
QUEUE_REGISTRY_SIZE | 8 | 隊列注冊表大小(用于調試工具跟蹤隊列/信號量)。 |
(3)Memory Management(內存管理)
參數名稱 | 當前配置值 | 含義說明 |
---|---|---|
TOTAL_HEAP_SIZE | 3072 Bytes | 動態內存堆總大小(根據任務和隊列數量調整)。 |
Memory Management scheme | heap_4 | 使用動態內存分配方案4(合并空閑塊,避免碎片化)。 |
Memory Allocation | Dynamic / Static | 支持動態和靜態內存分配(需用戶提供靜態內存時需配置configSUPPORT_STATIC_ALLOCATION )。 |
1. 3 添加任務
下圖是STM32CubeMX 的默認任務,可以修改它的名稱和函數類型,但不能刪除它。這是 CubeMX 提供的一個固定設置,用于初始化FreeRTOS和提供一個最基本的任務框架。
參數說明
配置項 | 當前值 | 解釋說明 |
---|---|---|
Task Name | defaultTask | 任務的名稱,這里是 defaultTask 。任務名稱用于標識該任務 |
Priority | osPriorityNormal | 任務的優先級,osPriorityNormal 表示任務的優先級為正常(即中等優先級) |
Stack Size (Words) | 128 | 任務堆棧的大小,單位是字(Words),這里的 128 表示任務棧有128個字的空間。每個字的大小通常是4字節(32位系統) |
Entry Function | StartDefaultTask | 任務的入口函數,任務開始執行時會調用該函數。這里的 StartDefaultTask 是該任務的函數名稱 |
Code Generation Option | Default | 代碼生成選項,設置為 Default 表示使用默認的代碼生成設置 |
Parameter | NULL | 傳遞給任務的參數,這里設置為 NULL ,表示任務不需要傳入任何參數 |
Allocation | Dynamic | 任務棧內存分配方式,設置為 Dynamic 表示任務棧的內存是在運行時動態分配的 |
Buffer Name | NULL | 緩沖區名稱,設置為 NULL 表示沒有指定緩沖區。通常用于處理一些任務的輸入輸出緩沖區 |
Control Block Name | NULL | 任務控制塊名稱,設置為 NULL 表示沒有指定任務的控制塊(在FreeRTOS中用于存儲任務的元數據) |
關于 STM32CubeMX 中的默認任務:
- 默認任務:這是 CubeMX 在生成的代碼中自動創建的第一個任務。它通常用于進行系統初始化、測試和調試。
- 修改默認任務:雖然不能刪除默認任務,但可以:
- 修改任務的名稱
- 修改任務執行的函數(即默認任務執行的代碼)
- 修改任務的優先級
后面,我們手寫代碼時,我們可以通過 FreeRTOS
提供的 API 創建自己的任務、隊列、信號量等對象。
最后!創建工程!
然后在,工程文件夾內,創建一個文件夾BSP
,拿來放寫好的底層驅動文件。
OK!完成!!(基本配置完成的文件放在最后了:LED KEY Usart Delay)
二、動態任務的創建/刪除
2.1 函數介紹
2.1.1 創建動態任務xTaskCreate()
(1)函數原型
BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, // 任務函數const char * const pcName, // 任務名稱configSTACK_DEPTH_TYPE usStackDepth, // 棧大小void *pvParameters, // 傳入任務的參數UBaseType_t uxPriority, // 任務優先級TaskHandle_t *pxCreatedTask // 返回任務句柄(可以是 NULL)
);
(2)參數解釋
參數 | 含義 | 舉例 |
---|---|---|
pxTaskCode | 任務函數名(任務函數就是編寫任務具體做什么) | 比如:任務函數為void Task_LED(void *pvParameters) ,任務函數名就是Task_LED |
pcName | 給任務起個名字(調試查看用) | "LED_Task" |
usStackDepth | 分配給任務的棧大小(注意單位是“字”,不是字節) | 一般 128~512 比較常見 |
pvParameters | 傳遞給任務函數的參數 | 可以傳結構體、變量、NULL |
uxPriority | 任務優先級,值越大越重要 | 通常范圍 0~configMAX_PRIORITIES-1 |
pxCreatedTask | 返回這個任務的“身份證”(句柄),我們可以以后用它去操作這個任務。如想刪掉、掛起這個任務,就需要通過句柄去操作 | &xxx_Handle ,或者傳 NULL 表示我不關心這個任務的句柄 |
(3)返回值說明
返回值 | 含義 |
---|---|
pdPASS | 創建成功 |
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY | 內存不足,創建失敗(系統堆不夠) |
(4)示例代碼
/***** (1)任務函數(任務是要做什么) ******/
void LED_Task(void *pvParameters)
{while (1){HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 翻轉LEDvTaskDelay(500); // 延時500ms}
}/***** (2)創建任務函數 ******/
// 創建的任務,系統會把它加入調度器,由 FreeRTOS 自動進行任務切換調度
xTaskCreate(LED_Task,"LED",128,NULL,2,NULL);
這個任務的功能是:每隔 500ms 翻轉一次 GPIOB 的 PIN_0 引腳,從而實現 LED 的閃爍效果
位 | 參數名 | 類型 | 示例值 | 含義 |
---|---|---|---|---|
1 | pxTaskCode | TaskFunction_t | LED_Task | 任務函數指針,告訴 FreeRTOS 這個任務要做什么。這里是一個控制 LED 閃爍的函數。 |
2 | pcName | const char * | "LED" | 任務名稱,用于調試和查看任務狀態時顯示的名字。 |
3 | usStackDepth | uint16_t | 128 | 棧大小,單位是“字”(word),不是字節。STM32 中 1 字 = 4 字節,因此此任務分配了 512 字節棧空間。 |
4 | pvParameters | void * | NULL | 傳遞給任務的參數。如果不需要傳遞參數,寫 NULL 。 |
5 | uxPriority | UBaseType_t | 2 | 任務優先級。值越大,優先級越高。 |
6 | pxCreatedTask | TaskHandle_t * | NULL | 接收創建的任務句柄的指針。如果后續要操作該任務(如刪除、掛起等),需傳入句柄變量地址;后續不需要這些操作就傳 NULL 。 |
如果我們要看任務是否創建成功:
/***** (1)任務函數:LED 閃爍 ******/
void LED_Task(void *pvParameters)
{while (1){HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 翻轉 LED 引腳vTaskDelay(500); // 延時 500ms}
}/***** (2)任務創建函數:包含成功判斷 ******/
void CreateTasks(void)
{BaseType_t xReturn; // 用于接收任務創建結果xReturn = xTaskCreate(LED_Task,"LED",128,NULL,2,NULL);if (xReturn == pdPASS) // 創建成功{printf("LED_Task 創建成功!\r\n");}else // 創建失敗{printf("LED_Task 創建失敗!\r\n");}
}
假設我們傳入了任務句柄變量,例如 &LEDTaskHandle
// 定義一個任務句柄
TaskHandle_t LEDTaskHandle; ///創建任務 `LED_Task`,并把這個任務的“控制權”交給變量 `LEDTaskHandle`
xTaskCreate(LED_Task, "LED", 128, NULL, 2, &LEDTaskHandle);
可以后續使用句柄對任務進行操作
- 刪除LED任務:
vTaskDelete(LEDTaskHandle);
- 掛起LED任務:
vTaskSuspend(LEDTaskHandle);
2.1.2 創建靜態任務xTaskCreateStatic()
這個函數是為 不使用動態內存分配(malloc) 的場景準備的。我們要自己準備好棧空間和任務控制塊。
TaskHandle_t xTaskCreateStatic(TaskFunction_t pxTaskCode,const char * const pcName,const uint32_t ulStackDepth,void * const pvParameters,UBaseType_t uxPriority,StackType_t * const puxStackBuffer, // 提前分配好的棧StaticTask_t * const pxTaskBuffer // 提前準備好的任務控制塊
);
【后續沒用到,我就是一個直接跳過!!】
2.1.3 刪除任務 vTaskDelete()
(1)函數原型
void vTaskDelete(TaskHandle_t xTaskToDelete);
(2)參數解釋
參數 | 含義 |
---|---|
xTaskToDelete | 要刪除的任務的句柄。如果想刪除當前任務,可以傳入 NULL |
vTaskDelete(NULL);
→ 刪除當前正在運行的任務vTaskDelete(xxx_Handle);
→ 刪除指定句柄的任務
(3)示例代碼
/*****(1)任務函數,運行后自刪*****/
void LED_Task(void *pvParameters)
{HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); printf("LED_Task 自我刪除!\r\n");vTaskDelete(NULL); // 刪除自己
}/*****(2)創建任務*****/
xTaskCreate(LED_Task, "LED", 128, NULL, 2, NULL);
或由其他任務/定時器刪除:
/***** 任務函數1,運行后刪任務2 *****/
void LED_Task(void *pvParameters)
{HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); vTaskDelete(Task2_Handle); // 刪除任務2
}
/***** 任務函數2 *****/
void LED2_Task(void *pvParameters)
{HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
}/*****(2)創建任務*****/
xTaskCreate(LED_Task, "LED", 128, NULL, 2, NULL); // 不保存句柄
xTaskCreate(LED2_Task, "LED2", 128, NULL, 2, &Task2_Handle); // 有句柄,用于后續刪掉操作!
2.2 編寫例題代碼
這里參考正點原子例題!
2.2.1 添加任務
打開工程,這里一共四個任務,我們先創建好任務函數和添加任務
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */// 定義三個任務句柄,用于后續管理和控制任務(如掛起、恢復等)
TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;/* USER CODE END Variables *//* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */// 四個任務函數的聲明
void Task_Start(void *argument);
void Task1_LED(void *argument);
void Task2_LED(void *argument);
void Task3_KEY(void *argument); /* USER CODE END FunctionPrototypes *//****** (1) 創建四個任務函數 *******/void Task_Start(void *argument)
{printf("Hello! Task Start!\r\n");// 創建 LED1 任務,優先級 26,堆棧大小 128xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);// 創建 LED2 任務,優先級 27,堆棧大小 128xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle); // 創建 KEY 按鍵任務,優先級 28,堆棧大小 128xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);// 刪除當前任務vTaskDelete(NULL);
}void Task1_LED(void *argument)
{while (1){static int N1 = 0;N1++;printf("Task_1 -- %d\r\n", N1);vTaskDelay(500);}
}void Task2_LED(void *argument)
{while (1){static int N2 = 0;N2++;printf("Task_2 -- %d\r\n", N2);vTaskDelay(1000); }
}void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3++;printf("Task_3 -- %d\r\n", N3);vTaskDelay(100); }
}
... ...
void MX_FREERTOS_Init(void) {... .../* USER CODE BEGIN RTOS_THREADS *//* 添加 FreeRTOS 啟動任務 *//****** (2) 添加任務 *******/// 創建啟動任務,優先級 25,堆棧大小 128,啟動時由調度器自動運行xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL);/* USER CODE END RTOS_THREADS */
}
創建好了四個任務,每個任務對應有任務函數
和添加任務
,打印自增看看任務咋運行的
我們給任務分配了優先級
任務名稱 | 函數名 | 優先級(數字越大優先級越高) | 說明 |
---|---|---|---|
啟動任務 | Task_Start | 25 | 啟動時創建其他任務后自刪除 |
LED1任務 | Task1_LED | 26 | 控制LED1,每500ms打印一次 |
LED2任務 | Task2_LED | 27 | 控制LED2,每1000ms打印一次 |
按鍵處理任務 | Task3_KEY | 28 | 處理按鍵輸入,優先級最高 |
我們在第一章可以看到,優先級設置56個,為什么這是25到28
呢???
我們看看默認任務的優先級是多少
/* Definitions for defaultTask */osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {.name = "defaultTask",.priority = (osPriority_t) osPriorityNormal,.stack_size = 128 * 4
};
是.priority = (osPriority_t) osPriorityNormal,
點擊進去看看這個普通優先級到底多少級
osPriorityBelowNormal6 = 16+6, ///< Priority: below normal + 6osPriorityBelowNormal7 = 16+7, ///< Priority: below normal + 7osPriorityNormal = 24, ///< Priority: normalosPriorityNormal1 = 24+1, ///< Priority: normal + 1osPriorityNormal2 = 24+2, ///< Priority: normal + 2
哦哦哦,原來是24
,那為了避免默認任務打擾我們,直接從25開始!
OKOK,說這么多,先把程序下載到板子看看啥情況,記得打開串口哦
怎么個事,我的Task3
呢!!!
函數介紹里,xTaskCreate()會返回值,可以根據返回值判斷任務是否成功
創建任務不成功的原因有很多,有一個可能就是給FreeRTOS分配的地方太小,裝不下那么多任務
在Task_Start
添加幾行代碼,我們打印出來看看
void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle); xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);BaseType_t xReturn; // 用于接收任務3的創建結果xReturn = xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);;if (xReturn == pdPASS) // 創建成功{printf("Task3 創建成功!\r\n");}else // 創建失敗{printf("Task3 創建失敗!\r\n");}printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());// 刪除自己vTaskDelete(NULL);
}
串口輸出
Task3
創建失敗,但是Free Heap: 568
不是還有空地方嗎??
xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);
豬豬豬:我們創建任務時,任務堆棧實際占用的內存大小 = 128 words × 4 字節
= 512 字節
再看你當時打印的 Free Heap:568
字節,確實還剩下一點,但:
原因 | 說明 |
---|---|
剩余堆空間不夠 | 你還有 568 字節,但新任務創建至少要分配 堆棧空間 + TCB 控制塊內存(約 100~200 字節),總共就超過 568 字節了。 |
堆碎片化 | 即使堆總量看起來夠用,但因為分散,可能沒有一整塊連續的大內存區域給任務使用,導致創建失敗。 |
那么解決辦法
- 減小任務堆棧大小,一般簡單任務(比如只打印或輪詢按鍵)用不了這么大棧。
// 試試減小堆棧到 100 或 96(word 單位)
xTaskCreate(Task3_KEY, "Task3", 100, NULL, 28, &TaskKEY_Handle);
- 增加堆大小,在
FreeRTOSConfig.h
中修改:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 5 * 1024 ) ) // 改為 5KB 或更大
選擇了第二種,改為 5KB!
重新下載,看看是不是這個原因
OK!成功了
前四行是開始任務創建的三個任務,后面也可以看出來是優先級最高的Task3
執行,然后就是Task2
,最后是Task1
。但是為什么創建任務的第一次打印不是Task3
最開始呢???
我們看看開始任務里,我們最先創建的是Task1
,而且它高于開始任務。
xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);
xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle);
xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);
所以,Task1
被創建完成后,直接就開始執行了,Task2
是FreeRTOS執行完Task1
后再回到Task_Start
里創建的,Task3
同理!那開始的時候怎么才能按優先級執行呢?臨界區
!后面我們會詳細說明,這里只需要知道這個是停止執行任務的OK了。
void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");taskENTER_CRITICAL(); // 進入臨界區xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle); xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);vTaskDelete(NULL); // 刪除自己taskEXIT_CRITICAL(); // 退出臨界區
}
加上這兩行代碼即可,我們再下載,打開串口看看(截圖太麻煩啦,直接復制粘貼了)
Hello!Task Start!
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_2--2
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
Task_2--3
Task_1--3
OKOK,這回就對了。
2.2.2 編寫任務
根據題目要求我們把任務函數補充完整
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */uint8_t LEDx = 0x00;
uint8_t LED1_Flag = 1;
uint8_t LED2_Flag = 1;TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */void Task_Start(void *argument);
void Task1_LED(void *argument);
void Task2_LED(void *argument);
void Task3_KEY(void *argument);/****** (1) 創建四個任務函數 *******/void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");taskENTER_CRITICAL(); // 進入臨界區xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle); xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);// 刪除自己vTaskDelete(NULL); taskEXIT_CRITICAL(); // 退出臨界區
}void Task1_LED(void *argument)
{while (1){static int N1 = 0;N1 ++;printf("Task_1--%d\r\n",N1);switch(LED1_Flag) // LED1閃爍{case 1: LEDx |= 0x01; LED1_Flag = 2; break;case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;default: break;}LED_Disp(LEDx);vTaskDelay(500);}
}void Task2_LED(void *argument)
{while (1){static int N2 = 0;N2 ++;printf("Task_2--%d\r\n",N2);switch(LED2_Flag) // LED2閃爍{case 1: LEDx |= 0x02; LED2_Flag = 2; break;case 2: LEDx &= ~(1 << 1); LED2_Flag = 1; break;default: break;}LED_Disp(LEDx); vTaskDelay(500); }
}void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3 ++;printf("Task_3--%d\r\n",N3);KEY_Proc(); // 掃描檢測按鍵if(KEY_Down == 1) // 按鍵1--刪掉任務1{vTaskDelete(TaskLED1_Handle);printf("刪掉了Task_1!!\r\n");}vTaskDelay(100); }
}
然后下載,查看燈,按下按鍵1,打開串口看看
Hello!Task Start!
Free Heap: 1992
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_2--2
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
刪掉了Task_1!!
Task_2--3
Task_3--11
Task_3--12
Task_3--13
Task_3--14
Task_2--4
Task_3--15
Task_3--16
可以看到,下載完成后,兩個燈幾乎同亮同滅,按下按鍵1后,LED1
停止閃爍,串口輸出已刪掉
提示
然后我再次按下按鍵1 ,遇到的問題:
第一次按鍵正常刪除 Task1_LED,但按第二次后串口卡頓,Task3_KEY
不再打印,卡死。
第二次進入 vTaskDelete(TaskLED1_Handle)
,但是任務1已經被刪掉了,所以這時候的 TaskLED1_Handle == NULL
,問題根本在于:
vTaskDelete(NULL); // 當句柄為 NULL 時,刪除的是自己!
連續按兩次后,**Task3_KEY 中自己把自己刪了!**所以就“無了”,串口沒輸出、任務也不在了。
為了避免這個問題!
void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3 ++;printf("Task_3--%d\r\n",N3);KEY_Proc();if(KEY_Val == 1 && TaskLED1_Handle != NULL) // 關鍵!防止再次誤刪if(KEY_Down == 1){vTaskDelete(TaskLED1_Handle);printf("刪掉了Task_1!!\r\n");TaskLED1_Handle = NULL; // 關鍵!防止再次誤刪}vTaskDelay(100); }
}
OK,解決!
2.2.3 完整代碼
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "LED.h"
#include "lcd.h"
#include "KEY.h"
#include "usart.h"
#include "stdio.h"
/* USER CODE END Includes *//* USER CODE BEGIN Variables */
uint8_t LEDx = 0x00;
uint8_t LED1_Flag = 1;
uint8_t LED2_Flag = 1;TaskHandle_t TaskLED1_Handle;
TaskHandle_t TaskLED2_Handle;
TaskHandle_t TaskKEY_Handle;
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
void Task_Start(void *argument);
void Task1_LED(void *argument);
void Task2_LED(void *argument);
void Task3_KEY(void *argument);/****** (1) 創建四個任務函數 *******/void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");taskENTER_CRITICAL(); // 進入臨界區xTaskCreate(Task1_LED, "Task1", 128, NULL, 26, &TaskLED1_Handle);xTaskCreate(Task2_LED, "Task2", 128, NULL, 27, &TaskLED2_Handle); xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);// BaseType_t xReturn; // 用于接收任務創建結果
//
// xReturn = xTaskCreate(Task3_KEY, "Task3", 128, NULL, 28, &TaskKEY_Handle);;// if (xReturn == pdPASS) // 創建成功
// {
// printf("Task3 創建成功!\r\n");
// }
// else // 創建失敗
// {
// printf("Task3 創建失敗!\r\n");
// }printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());vTaskDelete(NULL); // 刪除自己taskEXIT_CRITICAL(); // 退出臨界區
}void Task1_LED(void *argument)
{while (1){static int N1 = 0;N1 ++;printf("Task_1--%d\r\n",N1);switch(LED1_Flag){case 1: LEDx |= 0x01; LED1_Flag = 2; break;case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;default: break;}LED_Disp(LEDx);vTaskDelay(500);}
}void Task2_LED(void *argument)
{while (1){static int N2 = 0;N2 ++;printf("Task_2--%d\r\n",N2);switch(LED2_Flag){case 1: LEDx |= 0x02; LED2_Flag = 2; break;case 2: LEDx &= ~(1 << 1); LED2_Flag = 1; break;default: break;}LED_Disp(LEDx); vTaskDelay(500); }
}void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3 ++;printf("Task_3--%d\r\n",N3);KEY_Proc();if(KEY_Val == 1 && TaskLED1_Handle != NULL)if(KEY_Down == 1){vTaskDelete(TaskLED1_Handle);printf("刪掉了Task_1!!\r\n");TaskLED1_Handle = NULL; // 關鍵!防止再次誤刪}vTaskDelay(100); }
}void MX_FREERTOS_Init(void) {/* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//****** (2) 添加任務 *******/xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL);
}
番外:
除了開始任務,其他三個任務都是死循環,如果放在main.c
的while
函數中
int main(void)
{HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_I2C1_Init();osKernelInitialize();MX_FREERTOS_Init();// osKernelStart();while (1){Led1_Test();LED2_Test();KEY_Test();}
}
會發現,只有LED1燈閃,LED2和按鍵沒反應,因為程序被卡死在LED1_Test();
,進不到下一個程序了。
豬豬豬:FreeRTOS 啟動后,main()
主循環就被“棄用了”
osKernelInitialize(); // 初始化 RTOS 內核MX_FREERTOS_Init(); // 創建任務osKernelStart(); // 啟動 RTOS,開始多任務調度!
一旦執行到 osKernelStart()
,控制權就交給 FreeRTOS 的調度器了,程序不會再執行之后的代碼,包括 while(1)
,所以要先注釋掉osKernelStart();
!
三、任務掛起與恢復
3.1 函數介紹
通過本實驗,掌握 FreeRTOS 中與 任務掛起與恢復 相關的 API 函數,包括:
vTaskSuspend()
?掛起任務vTaskResume()
?恢復被掛起的任務xTaskResumeFromISR()
從中斷服務函數中恢復任務
3.1.1 任務掛起vTaskSuspend()
(1)函數原型
void vTaskSuspend(TaskHandle_t xTaskToSuspend);
(2)參數解釋
參數 | 說明 |
---|---|
xTaskToSuspend | 要掛起的任務句柄。 如果傳 NULL ,表示掛起當前任務 |
- 掛起任務后,該任務會停止運行,直到被恢復。
- 被掛起的任務不會被調度器調度,CPU 不會再執行它。
(3)示例
// 掛起 LEDTask 任務
vTaskSuspend(LEDTaskHandle); // 自己掛起自己
vTaskSuspend(NULL);
3.1.2 任務恢復 vTaskResume()
(1)函數原型
void vTaskResume(TaskHandle_t xTaskToResume);
(2)參數解釋
參數 | 說明 |
---|---|
xTaskToResume | 要恢復的任務句柄 |
- 將之前掛起的任務重新加入就緒隊列,使其可以繼續執行。
- 只能用于恢復由
vTaskSuspend()
掛起的任務。
(3)示例
vTaskResume(LEDTaskHandle); // 讓 LEDTask 任務恢復運行
3.1.3 從中斷任務恢復 xTaskResumeFromISR()
(1)函數原型
BaseType_t xTaskResumeFromISR(TaskHandle_t xTaskToResume);
(2)參數解釋
參數名 | 含義 |
---|---|
xTaskToResume | 要恢復的任務的句柄,僅能用于被 vTaskSuspend() 掛起的任務 |
(3)返回值說明
返回值 | 含義 |
---|---|
pdTRUE | 任務恢復后就緒,建議在中斷中進行一次任務切換 |
pdFALSE | 無需切換上下文(恢復任務未使更高優先級任務就緒) |
(4)示例
void EXTI0_IRQHandler(void)
{BaseType_t xHigherPriorityTaskWoken = pdFALSE;xTaskResumeFromISR(LEDTaskHandle); // 恢復任務portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 判斷是否需要任務切換
}
3.1.4 獲取任務狀態 eTaskGetState()
(1)函數原型
eTaskState eTaskGetState(TaskHandle_t xTask);
(2)參數解釋
參數名 | 含義 |
---|---|
xTask | 要查詢狀態的任務的句柄 |
(3)返回值說明
返回值 | 含義 |
---|---|
eReady | 任務已準備好執行,但當前沒有在運行。任務在就緒隊列中等待調度。 |
eRunning | 任務當前正在運行。 |
eBlocked | 任務因等待某些資源(例如信號量、隊列等)而被阻塞。 |
eSuspended | 任務已被掛起,不能被調度執行。 |
eDeleted | 任務已經被刪除。 |
(4)示例
void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3++;printf("Task_3--%d\r\n", N3);// 查詢任務 1 (TaskLED1) 的狀態eTaskState taskState = eTaskGetState(TaskLED1_Handle);if (taskState == eSuspended){printf("TaskLED1 is suspended.\r\n");}else if (taskState == eRunning){printf("TaskLED1 is running.\r\n");}else if (taskState == eBlocked){printf("TaskLED1 is blocked.\r\n");}else if (taskState == eReady){printf("TaskLED1 is ready.\r\n");}else{printf("TaskLED1 is deleted.\r\n");}// 延時vTaskDelay(100);}
}
說明:eTaskGetState()
用來查詢 xxx_Handle
的狀態。根據返回的狀態值 (eSuspended
, eRunning
, eBlocked
, eReady
, eDeleted
),以便根據任務的當前狀態做出適當的邏輯判斷。
3.2 編寫例題代碼
正點原子例題
3.2.1 任務掛起/恢復
在任務3里進行任務1的掛起和恢復
在前一章的完整代碼下,其他的函數不變,更改一下void Task3_KEY(void *argument)
void Task3_KEY(void *argument)
{while (1){static int N3 = 0;N3 ++;printf("Task_3--%d\r\n",N3);KEY_Proc();// 按下按鍵1 掛起任務1if(KEY_Down == 1){vTaskSuspend(TaskLED1_Handle);printf("-----掛起任務1-----\r\n");}// 按下按鍵2 恢復任務1else if(KEY_Down == 2){vTaskResume(TaskLED1_Handle);printf("-----恢復任務1-----\r\n");}vTaskDelay(100); }
}
下載到板子,打開串口助手,串口輸出如下:
Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
... ...
Task_2--3
Task_1--5
Task_3--20
Task_3--21
Task_3--22
Task_3--23
-----掛起任務1-----
Task_3--24
Task_3--25
Task_3--26
Task_3--27
Task_3--28
Task_2--4
Task_3--29
Task_3--30
Task_3--31
Task_3--32
Task_3--33
Task_3--34
Task_3--35
Task_3--36
Task_3--37
Task_2--5
Task_3--38
Task_3--39
Task_3--40
Task_3--41
Task_3--42
Task_3--43
Task_3--44
-----恢復任務1-----
Task_1--6
Task_3--45
Task_3--46
Task_2--6
Task_3--47
Task_3--48
Task_1--7
Task_3--49
Task_3--50
Task_3--51
Task_3--52
Task_3--53
Task_1--8
Task_3--54
Task_3--55
Task_2--7
Task_3--56
-----掛起任務1-----
Task_3--57
Task_3--58
Task_3--59
Task_3--60
Task_3--61
Task_3--62
Task_3--63
Task_2--8
Task_3--65
Task_3--66
Task_3--67
Task_3--68
Task_3--69
Task_3--70
Task_3--71
Task_3--72
Task_3--73
Task_2--9
Task_3--74
Task_3--75
Task_3--76
Task_3--77
Task_3--78
Task_3--79
Task_3--80
Task_3--81
Task_3--82
Task_2--10
Task_3--83
Task_3--84
Task_3--85
Task_3--86
Task_3--87
-----恢復任務1-----
Task_1--9
Task_3--88
Task_3--89
Task_3--90
Task_2--11
Task_1--10
Task_3--92
可以看見按下按鍵1,掛起后任務1后就沒有再執行過任務1,恢復后繼續執行
第一次掛起前最后是Task_1--5
,掛起后沒有輸出;恢復后繼續之前的輸出Task_1--6
。第二次同理!
但是有一個問題,如果我們重復按下,就會一直顯示“恢復任務“,但其實并沒有,第一次按下時已經恢復了任務1,后面按下都是無效的!!所以我們改一下代碼,避免無效恢復。
這個時候我們就會用到函數 eTaskGetState
獲取任務狀態,如果被掛起,才執行恢復
void Task3_KEY(void *argument)
{while (1){... ... else if(KEY_Down == 2) {switch(Task1_State) // (2)根據判斷執行{case 2: // 未被掛起printf("-----KEY2--已經恢復過啦-----\r\n");//printf("-----KEY2--任務1沒被掛起-----\r\n");break;case 1: // 掛起vTaskResume(TaskLED1_Handle);printf("-----恢復任務1-----\r\n");break;default: break;} }// (1)判斷任務1 是否被掛起if(eTaskGetState(TaskLED1_Handle) == eSuspended){Task1_State = 1; // 掛起}else{Task1_State = 2; // 未被掛起} vTaskDelay(100); }
}
下載到板子,打開串口助手,串口輸出如下:
Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
-----掛起任務1-----
Task_3--5
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
Task_2--2
Task_3--11
-----恢復任務1-----
Task_1--2
Task_3--12
Task_3--13
... ...
Task_1--5
Task_3--26
-----KEY2--已經恢復過啦-----
Task_3--27
Task_2--4
Task_3--29
Task_1--6
Task_3--30
OK!
3.2.2 從中斷恢復任務
跟上面類似,只不過用的函數不同
首先在CubeMX打開按鍵中斷,我是把PB2設置為上升沿中斷觸發
然后編寫中斷代碼
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{if(GPIO_Pin == GPIO_PIN_2) // 判斷是否是 PB2 引腳觸發的中斷{BaseType_t xResume = pdFALSE; // 用來接收 xTaskResumeFromISR() 的返回值switch(Task1_State) // 判斷任務1是否掛起{case 2: // 未被掛起printf("-----EXTI--已經恢復過啦-----\r\n");//printf("-----EXTI--任務1沒被掛起-----\r\n");break;case 1: // 掛起//如果該任務優先級高于當前運行任務,將返回 pdTRUE,否則返回 pdFALSExResume = xTaskResumeFromISR(TaskLED1_Handle); if(xResume == pdTRUE){//pdTRUE,說明 ISR 中恢復的任務的優先級高,需要立即切換到該任務運行。portYIELD_FROM_ISR(xResume);} printf("-----從中斷中恢復任務1-----\r\n");break;default: break;} }
}
這里跟之前的有點不一樣,多出了一個立即切換到該任務運行的判斷,啥意思呢??
假設你在一個公司,正在做自己手頭的工作。突然,老板交給你一個任務。
- 如果老板說這個任務非常緊急,你就必須立刻去做老板的任務,等到老板的任務做完再去做你手頭的工作。
- 如果老板說這個任務不太緊急,可以等下再做,那么你就不需要立刻停止當前的工作,等你做完手頭的工作,再去處理老板的任務。
代碼中的 xTaskResumeFromISR()
和 portYIELD_FROM_ISR()
xTaskResumeFromISR()
:在中斷中恢復任務,就像是老板突然發出“任務”,并且會返回值告訴你是否緊急pdTRUE
:表示中斷被恢復的任務優先級更高,非常緊急pdFALSE
:表示當前任務優先級更高或相同,不急
portYIELD_FROM_ISR()
:這個是立刻切換去執行的函數portYIELD_FROM_ISR(pdTRUE)
:急急如律令,立刻去執行中斷被恢復的任務portYIELD_FROM_ISR(pdFALSE)
:不急,繼續當前的任務,等會兒再說。
OK,下載到板子,打開串口助手,串口輸出如下:
Hello!Task Start!
Free Heap: 1984
Task_3--1
Task_2--1
Task_1--1
Task_3--2
Task_3--3
Task_3--4
Task_3--5
Task_1--2
Task_3--6
Task_3--7
Task_3--8
Task_3--9
Task_3--10
-----掛起任務1-----
Task_2--2
Task_3--11
Task_3--12
Task_3--13
Task_3--14
Task_3--15
Task_3--16
Task_3--17
Task_3--18
-----從中斷中恢復任務1-----
Task_1--3
Task_2--3
Task_3--20
Task_3--21
Task_3--22
Task_3--23
Task_1--4
Task_3--24
Task_3--25
Task_3--26
-----EXTI--已經恢復過啦-----
Task_3--27
Task_3--28
Task_1--5
Task_2--4
Task_3--29
Task_3--30
Task_3--31
Task_3--32
OK,完美!
四、FreeRTOS中斷管理
4.1 概念理解
4.1.1 中斷管理
STM32的中斷優先級的兩個組成部分:
- 搶占優先級(Preemption Priority):決定一個中斷是否可以“打斷”另一個正在執行的中斷。
- 子優先級(Sub Priority):在搶占優先級相同的情況下,決定兩個中斷“誰先響應”。
我們可以打開Cubemx
,點擊NVIC
查看,已經自動的幫我們把一些中斷設置改了。
當前設置:4 bits for pre-emption priority, 0 bits for subpriority
,即 NVIC_PRIORITYGROUP_4
- 這意味著所有中斷的優先級完全由搶占優先級決定,子優先級不起作用。
- 在這種設置下,優先級范圍為 0(最高優先級)到 15(最低優先級)。
這也是FreeRTOS官方建議的中斷設置!
4.1.2 中斷優先級推薦設置
為什么要用 NVIC_PRIORITYGROUP_4
呢??
- FreeRTOS 內核只關注搶占優先級(Preemption Priority)
- 子優先級對 FreeRTOS 是“透明”的,它不會參與調度判斷
- 如果設置了子優先級,FreeRTOS 不會管,結果就容易出“錯”
- 簡化優先級配置邏輯,降低出錯率
舉個例子:假設你用了 2 位搶占、2 位子優先級(NVIC_PRIORITYGROUP_2)
- 兩個中斷 A 和 B:
- 搶占優先級相同
- 子優先級不同
- FreeRTOS 會認為它們優先級一樣(只看搶占),但實際中:
- Cortex-M 內核允許按子優先級執行順序
- 這可能讓“低優先級中斷”先執行 → 打亂預期調度!
4.1.3 FreeRTOS相關宏
打開FreeRTOSConfig.h
,有關中斷定義的相關宏
/* Cortex-M 特定的設置 */
/* 檢查是否已經定義了中斷優先級位數 */
#ifdef __NVIC_PRIO_BITS/* 如果使用 CMSIS,直接使用系統定義的優先級位數 */#define configPRIO_BITS __NVIC_PRIO_BITS
#else/* 如果沒有使用 CMSIS,默認使用 4 位優先級 */#define configPRIO_BITS 4
#endif/* * 設置最低的中斷優先級,這個優先級可以用來設置中斷的優先級。* 數值越小,優先級越高。*/
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15/* * 設置可以調用 FreeRTOS API 的最高中斷優先級。* 優先級數值越低,優先級越高。*/
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5/* 計算內核的中斷優先級 */
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )/* * 配置系統調用的最大中斷優先級,確保系統調用的優先級不為零。*/
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )/* * 將 FreeRTOS 的中斷處理函數映射到 CMSIS 標準中斷處理函數。* SVC 用于系統調用,PendSV 用于上下文切換。*/
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler/* * 設置是否使用自定義的 SysTick 處理函數,0 表示使用默認處理函數。*/
#define USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION 0
這段配置是 FreeRTOS 和 Cortex-M 中斷優先級對接的重要部分,可以總結為以下幾信息:
configPRIO_BITS
表示中斷優先級一共用了幾位,我們是NVIC_PRIORITYGROUP_4。configLIBRARY_LOWEST_INTERRUPT_PRIORITY
和configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
是 FreeRTOS允許參與調度(或調用 API)的中斷優先級范圍,它只能管5~15
這部分!!- 數值 < 5 的高優中斷:FreeRTOS不控制,也不允許在這些中斷里用任何 FreeRTOS API。這些高優先級的中斷可以寫,比如緊急故障中斷、DMA完成中斷等;
- 數值 ≥ 5 且 ≤ 15 的中斷:可以在中斷里調用 FreeRTOS 的函數(比如發消息、信號量);
- FreeRTOS 自己的調度器用的是優先級 15(也就是最慢的調度中斷);
- 后面的
configKERNEL_INTERRUPT_PRIORITY
和configMAX_SYSCALL_INTERRUPT_PRIORITY
是為了把上面這些優先級轉換成芯片實際使用的格式,Cortex-M 的優先級是左對齊的,所以需要<< (8 - configPRIO_BITS)
來位移。 vPortSVCHandler
和xPortPendSVHandler
這些名字是把 FreeRTOS 的關鍵中斷函數(系統調用和任務切換)映射到 CMSIS 的標準函數名,確保啟動文件能識別。USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION
設置為 0 表示用 FreeRTOS 默認的SysTick_Handler
。如果有特別需求,比如自己控制滴答定時器,設為 1 可以自己寫這個中斷函數。默認即可。
4.2 函數介紹
4.2.1 禁用中斷 portDISABLE_INTERRUPTS()
(1)函數原型
void portDISABLE_INTERRUPTS(void);
- 禁用所有可屏蔽中斷,常用于進入臨界區,保護關鍵代碼不被打斷。
- 禁用中斷期間,FreeRTOS 將不會進行任務切換,也不會響應中斷服務。
4.2.2 啟用中斷 portENABLE_INTERRUPTS()
(1)函數原型
void portENABLE_INTERRUPTS(void);
- 恢復中斷響應,使系統能夠再次處理中斷和任務切換。
- 通常用于臨界區結束后,與
portDISABLE_INTERRUPTS()
配套使用。
(3)示例
portDISABLE_INTERRUPTS(); // 禁用中斷// 臨界區域操作
buffer[index++] = data;
// 臨界區域操作portENABLE_INTERRUPTS(); // 恢復中斷
4.3 編寫例題代碼
正點原子例題
主要就是看FreeRTOS能管理的中斷范圍,是在5~15
之間。
現在打開CubeMX
,增加兩個定時器中斷。
TIM6
和TIM7
,配置相同!
然后在NVIC
中使能,如果TIM6
使能不了,就取消最后一欄的勾(FreeRTOS不允許設置0~4
)
首先,開啟兩個定時器,編寫兩個定時器代碼(注:在main.c
中寫!!)
int main(void)
{... .../* USER CODE BEGIN 2 */HAL_TIM_Base_Start_IT(&htim6);HAL_TIM_Base_Start_IT(&htim7);delay_init(170); // 下一個代碼用,用于阻塞延時/* USER CODE END 2 */... ...while (1){}
}
... ...
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim == &htim6){printf("優先級4--TIM6--中斷開啟!!\r\n");}else if(htim == &htim7){printf("優先級6--TIM7--中斷開啟!!\r\n");}
}
下載到板子,打開串口助手,串口輸出如下:
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
... ...
然后創建一個任務,去控制兩個定時器中斷的開關。
uint8_t Num = 0;/****** (1) 創建一個任務函數 *******/
void Task1(void *argument)
{while (1){if(++Num == 5){Num = 0;portDISABLE_INTERRUPTS(); // 禁用中斷printf("---關掉---中斷啦---\r\n");delay_ms(5000); // 阻塞5sportENABLE_INTERRUPTS(); // 重新啟用中斷printf("---開啟---中斷啦---\r\n");}vTaskDelay(1000);}
}
/* USER CODE END FunctionPrototypes */
... ...
void MX_FREERTOS_Init(void)
{... .../* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//****** (2) 添加任務 *******/xTaskCreate(Task1, "Task1", 128, NULL, 25, NULL);... ...
}
豬豬豬:這里的Delay是移植正點原子的,我已經放在了基礎文件里。
下載,打開串口!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
---關掉--中斷啦---
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
---開啟--中斷啦---
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級6--TIM7--中斷開啟!!
---關掉--中斷啦---
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
優先級4--TIM6--中斷開啟!!
由此可知!!FreeRTOS的任務也不能控制0~4
優先級的任務。
優先級數值 | 中斷優先級含義 | 是否受 FreeRTOS 管理 | 能否調用 xxxFromISR |
---|---|---|---|
0~4 | 高優先級 | ? 不受 FreeRTOS 管理 | ? 不可調用 FromISR |
5~15 | 低優先級 | ? 可由 FreeRTOS 管理 | ? 可調用 FromISR |
宏configMAX_SYSCALL_INTERRUPT_PRIORITY
通常設置為 5,所以優先級 < 5 的中斷不受 FreeRTOS 管理。
宏 / 指令 | 關閉范圍 | 會關閉 TIM6(優先級4)嗎 |
---|---|---|
portDISABLE_INTERRUPTS() | FreeRTOS 管理的中斷(優先級5~15) | ? 不會 |
__disable_irq() | 所有中斷(包括高優先級) | ? 會 |
__disable_irq()
這個是 芯片級別 的中斷屏蔽,不受 FreeRTOS 限制,也會關閉所有高優先級中斷。
五、臨界段代碼保護及任務調度器的掛起和恢復
豬豬豬:前面我們學習的是任務的掛起和恢復,現在我們要看任務調度器!!
5.1 概念理解
5.1.1 臨界段代碼保護
(1)什么是臨界段??
這段代碼非常敏感,別人不能來打擾我,我要一口氣干完!
(2)哪些場景需要??
- 外設初始化:例如,在初始化 I2C、SPI 等通信外設時,需要確保時序正確。如果在初始化過程中被打斷,可能導致設備處于不一致狀態。
- 數據傳輸:進行數據傳輸或硬件控制時,若操作被打斷,可能會導致數據丟失或損壞。
- 共享資源訪問:多個任務可能同時訪問共享資源,例如全局變量、硬件外設、隊列等。如果沒有適當的同步保護,會導致資源競爭和數據沖突。
(3)什么打斷當前程序的運行
就是中斷和任務調度!
中斷:你正在干活,比如說你在搬磚,結果手機響了(來了個中斷),你就得先放下磚,去接電話。比如:定時器中斷、串口中斷、外部中斷、DMA中斷等,它隨時可能打斷你當前在干的事情!
任務調度:你是個低優先級任務,剛寫一半代碼,結果來了個高優先級任務,FreeRTOS 覺得你不夠重要,于是暫停你,讓別人先跑。
(4)怎么不被打斷
關中斷!禁止任務調度!
- 關中斷:關閉中斷,確保當前任務執行期間不會被其他中斷打擾。
- 禁止任務調度:暫停任務調度,防止低優先級任務被高優先級任務搶占。
臨界區直接屏蔽了中斷,系統任務調度、ISR都得靠中斷!
5.1.2 任務調度器
任務調度器是 FreeRTOS 的大腦,它負責決定系統中哪個任務什么時候運行。簡單來說,它就像一個指揮官,按照任務的優先級和時間安排來指揮各個任務的執行。如果有多個任務在等待,調度器會決定哪個任務先執行,哪個任務稍后執行。
在任務執行過程中,有時候會有一些“臨界段代碼”,這段代碼很重要,必須一氣呵成執行完,不允許被打斷。
但是,問題來了:任務調度器隨時可能打斷當前任務并切換到另一個任務。如果在執行臨界段代碼時被調度器打斷,可能會導致任務未能完成這段關鍵操作。為了避免這種情況,可以使用可以“暫停”任務調度器,確保當前任務不會被打斷,這樣,任務可以放心地執行關鍵代碼,不會因為調度器的干擾而導致錯誤。
5.2 函數介紹
5.2.1 臨界段保護函數(任務級)
(1)函數原型
void taskENTER_CRITICAL(void);
void taskEXIT_CRITICAL(void);
說明:
函數 | 作用 | 使用場景 |
---|---|---|
taskENTER_CRITICAL() | 進入臨界段(任務級) 本質上是關閉中斷,防止任務切換 | 在任務函數中使用 |
taskEXIT_CRITICAL() | 退出臨界段,恢復中斷 | 在任務函數中使用 |
這些函數用于在任務中關閉中斷,保護臨界代碼不被打斷。
(2)示例代碼
taskENTER_CRITICAL();
IIC_Init();
taskEXIT_CRITICAL();
使用 taskENTER_CRITICAL()
和 taskEXIT_CRITICAL()
包裹,確保在初始化期間不會被其他任務或中斷打斷。
5.2.2 臨界段保護函數(中斷級)
(1)函數原型
BaseType_t taskENTER_CRITICAL_FROM_ISR(void);
void taskEXIT_CRITICAL_FROM_ISR(UBaseType_t uxSavedStatusValue);
說明:
函數 | 作用 | 使用場景 |
---|---|---|
taskENTER_CRITICAL_FROM_ISR() | 進入臨界段,返回值為當前中斷狀態,并關閉中斷 | 在中斷服務函數中使用 |
taskEXIT_CRITICAL_FROM_ISR(xxx) | 退出臨界段,恢復之前保存的中斷狀態 | 在中斷服務函數中使用 |
這些函數用于中斷服務函數中保護臨界代碼。
(2)示例代碼
UBaseType_t status; // 定義一個變量,用于保存當前中斷狀態
status = taskENTER_CRITICAL_FROM_ISR(); // 關閉中斷,并保存當前中斷狀態
IIC_WriteByte(0xA5);
taskEXIT_CRITICAL_FROM_ISR(status); // 開啟中斷,并恢復之前保存的中斷狀態
status
變量保存了進入臨界段前的中斷狀態,確保在臨界段內執行完關鍵操作后,可以正確地恢復系統的中斷狀態,避免不必要的中斷丟失或系統行為異常。
5.2.3 任務調度器的掛起和恢復函數
(1)函數原型
void vTaskSuspendAll(void);
BaseType_t xTaskResumeAll(void);
(2)函數說明
函數 | 作用 |
---|---|
vTaskSuspendAll() | 掛起任務調度器,禁止任務調度器進行任務切換 |
xTaskResumeAll() | 恢復任務調度器,允許任務切換繼續進行 |
(3)返回值說明
xTaskResumeAll()
:返回一個 BaseType_t
類型的值。
- 返回值
pdTRUE
表示調度器已經成功恢復。 - 返回值
pdFALSE
表示調度器沒有恢復。
(4)示例代碼
/*****(1)掛起任務調度器 ******/
void Critical_Section(void)
{vTaskSuspendAll(); // 掛起任務調度器,禁止任務調度IIC_WriteByte(0xA5); // 比如在 I2C 總線中寫入數據xTaskResumeAll(); // 恢復任務調度器,允許任務切換
}/*****(2)調用函數******/
void Task_Function(void *pvParameters)
{while (1){Critical_Section(); // 執行掛起調度器保護的臨界段代碼vTaskDelay(100); // 延時100ms}
}
通過這兩個函數,可以確保在某些重要操作中,任務調度不會打斷重要操作。
【這章沒有實驗】
六、列表和列表項
6.1 概念理解
(1)什么是列表?什么是列表項?
列表可以類比為一個“容器”,專門用于存放和排序很多個列表項。它是 FreeRTOS 中管理調度、事件、延時等機制的基礎容器。列表項就是存放在列表中的項目。
列表相當于鏈表,列表項相當于節點,FreeRTOS 中的列表是一個雙向環形鏈表。
-
每一個列表項也就是一個個的任務,如果中途增加任務,就插入到列表項,中途刪掉任務,就從列表中移出。
-
列表項的地址是非連續的,是人為鏈接的,所以數目可以后期改變。
列表項1 <---> 列表項2 <---> 列表項3 <---> ... ... <---> 末尾列表項^ ^| |+------------------------------------------------------------+
假設,我們去醫院看病,醫院有多個科室,比如:
- 內科排隊列表(List_t)
- 外科排隊列表(List_t)
- 急診排隊列表(List_t)
每個科室有自己的一個排隊列表,用于管理等候的病人順序,這就像 FreeRTOS 中的一個列表。
每次來掛號,護士會給你一張掛號單,上面寫著:到你就診的時間(或優先級)、你本人的信息、你現在在哪個隊伍等等,這張掛號單就是 FreeRTOS 中的列表項。
6.2 函數介紹
6.2.1 列表/列表項結構體
首先!我們先看看每個結構體的定義和成員。
(1)列表項
ListItem_t
是 FreeRTOS 鏈表中的單個元素,每個元素就是鏈表中的一個節點。我們來看具體的定義:
struct xLIST_ITEM
{listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 校驗值,確保數據完整性 */configLIST_VOLATILE TickType_t xItemValue; /* 列表項的值, 用于排序 */struct xLIST_ITEM * configLIST_VOLATILE pxNext; /* 指向下一個列表項的指針 */struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* 指向上一個列表項的指針 */void * pvOwner; /* 指向擁有這個列表項的對象(如 TCB),從而形成一個雙向鏈接 */struct xLIST * configLIST_VOLATILE pxContainer; /* 指向包含該列表項的列表 */listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 校驗值,確保數據完整性 */
};·
xItemValue
: 這是列表項的核心值,通常用來決定排序!比如在任務調度中,可以通過xItemValue
來表示任務的優先級,數值較小的任務具有較高的優先級(具體根據 FreeRTOS 的排序規則)。pxNext
、pxPrevious
: 指向列表中前/后一個ListItem_t
元素的指針,這使得鏈表變成了雙向鏈表,每個節點都知道自己前后節點的位置。pvOwner
: 這是一個指向實際擁有該列表項的對象的指針(指向我們的任務)。這樣,可以在列表項和實際任務之間形成雙向關聯。pxContainer
: 這是一個指向該列表項所在列表的指針,表明這個列表項屬于哪個列表。
(2)迷你列表項
struct xMINI_LIST_ITEM
{listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 校驗值,確保數據完整性 */configLIST_VOLATILE TickType_t xItemValue; /* 列表項的值 */struct xLIST_ITEM * configLIST_VOLATILE pxNext; /* 指向下一個列表項的指針 */struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* 指向上一個列表項的指針 */
};
迷你列表項也就是末尾列表項,它是一個精簡版的列表項結構,只有最基本的字段。
(3)列表
typedef struct xLIST
{listFIRST_LIST_INTEGRITY_CHECK_VALUE /* 校驗值,確保數據完整性 */volatile UBaseType_t uxNumberOfItems; /* 列表項的數量 */ListItem_t * configLIST_VOLATILE pxIndex; /* 用于遍歷列表,指向上次訪問的列表項 */MiniListItem_t xListEnd; /* 標記列表結束的特殊節點,始終位于列表的尾部 */listSECOND_LIST_INTEGRITY_CHECK_VALUE /* 校驗值,確保數據完整性 */
} List_t;
uxNumberOfItems
: 列表中的項數。記錄當前列表中有多少個列表項,但不算上末尾列表項!pxIndex
: 用于遍歷列表的指針,它指向上次通過listGET_OWNER_OF_NEXT_ENTRY()
獲取的列表項。它幫助 FreeRTOS 在迭代時跟蹤當前位置。xListEnd
: 這是一個特殊的迷你列表項(MiniListItem_t
),標記了列表的結束。其xItemValue
為最大值,用來作為列表的末尾節點。它不存儲實際的數據,只起到標記作用,確保遍歷時能正確停止。
所以,剛創建時,列表中的列表項的數量是0,但是列表中已經有了迷你列表項!
┌──────────── List_t ────────────┐
│ │
│ ┌───────────────┐ │
│ │ xListEnd │ │
│ │ pxNext → 自己 │?────┐ │
│ │ pxPrev → 自己 │─────┘ │
│ └───────────────┘ │
└────────────────────────────────┘
創建一個列表項之后
┌──────────── List_t ────────────┐
│ │
│ ┌─────────────┐ │
│ │ ListItem A │?────────┐ │
│ │ pxNext → End│ │ │
│ │ pxPrev → End│ │ │
│ └─────────────┘ │ │
│ ▲ ▼ │
│ ┌───────────────┐ │ │
│ │ xListEnd │───────┘ │
│ │ pxNext → A │ │
│ │ pxPrev → A │ │
│ └───────────────┘ │
└────────────────────────────────┘
創建兩個之后
┌────────────┐ ┌────────────┐ ┌────────────┐
│ ListItem A │ ?──?│ ListItem B │ ?──?│ xListEnd │
└────────────┘ └────────────┘ └────────────┘▲ ▲└──────────────────────────────────────┘
以此類推!!
6.2.2 初始化列表
(1)函數原型
void vListInitialise( List_t *pxList );
參數解釋:
參數名 | 類型 | 說明 |
---|---|---|
pxList | List_t* | 指向要初始化的列表結構體指針 |
完整函數:
void vListInitialise( List_t *pxList )
{/* 初始化列表中當前索引為末尾項 */pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );/* 初始化列表項的數量為 0 */pxList->uxNumberOfItems = ( UBaseType_t ) 0U;/* xListEnd 是 MiniListItem 類型,單獨作為末尾項 */pxList->xListEnd.xItemValue = portMAX_DELAY;/* 雙向連接:xListEnd 的前后指針都指向自己 */pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
}
(2)示例代碼
List_t myList;
vListInitialise(&myList);
6.2.3 初始化列表項
(1)函數原型
void vListInitialiseItem( ListItem_t *pxItem );
參數解釋:
參數名 | 類型 | 說明 |
---|---|---|
pxItem | ListItem_t* | 指向要初始化的列表項指針 |
完整函數:
void vListInitialiseItem( ListItem_t *pxItem )
{/* 初始化時不屬于任何列表 */pxItem->pxContainer = NULL;
}
(2)示例代碼
ListItem_t myItem;
vListInitialiseItem(&myItem);
6.2.4 列表項插入列表
(1)函數原型
void vListInsert( List_t *pxList, ListItem_t *pxNewListItem );
參數解釋:
參數名 | 類型 | 說明 |
---|---|---|
pxList | List_t* | 目標列表 |
pxNewListItem | ListItem_t* | 要插入的列表項,需預先設置 xItemValue |
完整函數:
void vListInsert( List_t *pxList, ListItem_t *pxNewListItem )
{ListItem_t *pxIterator;TickType_t xValueOfInsertion = pxNewListItem->xItemValue;/* 從列表的頭開始,遍歷每個列表項,直到找到插入點 */for (pxIterator = (ListItem_t *) &(pxList->xListEnd); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext){/* 繼續尋找插入位置,空循環體 */}/* 現在找到插入點,更新指針 */pxNewListItem->pxNext = pxIterator->pxNext; // 新項的下一個項是當前項的下一個pxNewListItem->pxPrevious = pxIterator; // 新項的前一個項是當前項pxIterator->pxNext->pxPrevious = pxNewListItem; // 更新當前項下一個項的前一個指針pxIterator->pxNext = pxNewListItem; // 更新當前項的下一個指針為新項/* 設置新列表項的容器為目標列表 */pxNewListItem->pxContainer = pxList;/* 列表項數目增加 */(pxList->uxNumberOfItems)++;
}
(2)示例代碼
ListItem_t item;
item.xItemValue = 10;vListInsert(&myList, &item);
6.2.5 列表項末尾插入列表
(1)函數原型
void vListInsertEnd( List_t *pxList, ListItem_t *pxNewListItem );
參數解釋:
參數名 | 類型 | 說明 |
---|---|---|
pxList | List_t* | 目標列表 |
pxNewListItem | ListItem_t* | 要插入的列表項 |
完整函數:
void vListInsertEnd( List_t *pxList, ListItem_t *pxNewListItem )
{/* 獲取當前列表的末尾項,即 xListEnd 前的項 */ListItem_t *pxIndex = pxList->xListEnd.pxPrevious;/* 將新列表項插入到 xListEnd 之前 */pxNewListItem->pxNext = ( ListItem_t * ) &( pxList->xListEnd ); // 下一個是xListEndpxNewListItem->pxPrevious = pxIndex; // 前一個是插入前的末尾項pxIndex->pxNext = pxNewListItem; // 更新當前末尾項的下一個項為新項pxList->xListEnd.pxPrevious = pxNewListItem; // 更新 xListEnd 的前一個項為新項/* 設置新列表項的容器為目標列表 */pxNewListItem->pxContainer = pxList;/* 列表項數目增加 */(pxList->uxNumberOfItems)++;
}
(2)示例代碼
ListItem_t item;
vListInsertEnd(&myList, &item);
6.2.6 列表移出列表項
(1)函數原型
UBaseType_t uxListRemove( ListItem_t *pxItemToRemove );
參數解釋:
參數名 | 類型 | 說明 |
---|---|---|
pxItemToRemove | ListItem_t* | 要從列表中移除的列表項 |
返回值:
- 類型:
UBaseType_t
- 說明:移除操作后列表中剩余的項數量
完整函數:
UBaseType_t uxListRemove( ListItem_t *pxItemToRemove )
{/* 獲取當前列表的容器 */List_t * const pxList = pxItemToRemove->pxContainer;/* 更新前后節點的指針,移除目標項 */pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; // 后一個節點的前指針指向前一個節點pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext; // 前一個節點的下一個指針指向后一個節點/* 清除目標項的容器指針,表示已從列表中移除 */pxItemToRemove->pxContainer = NULL;/* 列表項數目減少 */pxList->uxNumberOfItems--;/* 返回更新后的列表項數量 */return pxList->uxNumberOfItems;
}
(2)示例代碼
uxListRemove(&item);
6.3 編寫例題代碼
正點原子例題
首先,直接把第二章動態任務的創建和刪除代碼復制一份,然后把多余任務刪掉
... ...
/****** (1) 創建三個任務函數 *******/void Task_Start(void *argument)
{printf("Hello!Task Start!\r\n");taskENTER_CRITICAL(); // 進入臨界區xTaskCreate(Task1, "Task1", 128, NULL, 26, &Task1_Handle);xTaskCreate(Task2, "Task2", 128, NULL, 27, &Task2_Handle); printf("Free Heap: %d\r\n", xPortGetFreeHeapSize());// 刪除自己vTaskDelete(NULL); taskEXIT_CRITICAL(); // 退出臨界區
}void Task1(void *argument)
{while (1){ switch(LED1_Flag){case 1: LEDx |= 0x01; LED1_Flag = 2; break;case 2: LEDx &= ~(1 << 0); LED1_Flag = 1; break;default: break;}LED_Disp(LEDx);vTaskDelay(500);}
}void Task2(void *argument)
{// 等會在任務2進行列表和列表項操作while (1){vTaskDelay(1000); }
}
... ...
void MX_FREERTOS_Init(void) {... .../* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//****** (2) 添加任務 *******/xTaskCreate(Task_Start, "TaskStart", 128, NULL, 25, NULL)
}
OK!接下來我們需要做的就是
-
初始化列表和列表項
-
(1) 初始化并添加 列表項1
-
(2) 添加 列表項2
-
(3) 添加 列表項3
-
(4) 刪除 列表項2
-
(5) 重新在末尾添加 列表項2
直接在任務2中添加,以下是完整代碼:
List_t List1; // 創建一個列表 List1
ListItem_t ListItem1; // 創建列表項1 ListItem1
ListItem_t ListItem2; // 創建列表項2 ListItem2
ListItem_t ListItem3; // 創建列表項3 ListItem3void Task2(void *argument)
{/**************** 初始化列表、列表項 ****************/// 初始化列表vListInitialise(&List1);// 初始化列表項 vListInitialiseItem(&ListItem1);vListInitialiseItem(&ListItem2);vListInitialiseItem(&ListItem3);// 設置列表項的數值ListItem1.xItemValue = 40;ListItem2.xItemValue = 60;ListItem3.xItemValue = 50;/**************** 【1】將列表項1插入列表 ****************/vListInsert(&List1, &ListItem1);/**************** 【2】將列表項2插入列表 ****************/vListInsert(&List1, &ListItem2);/**************** 【3】將列表項3插入列表 ****************/vListInsert(&List1, &ListItem3);/**************** 【4】將列表項2移出列表 ****************/uxListRemove(&ListItem2);/**************** 【5】將列表項2插入列表末尾 ****************/List1.pxIndex = List1.pxIndex->pxNext; // pxIndex 后移vListInsertEnd(&List1, &ListItem2);while (1){vTaskDelay(1000); }
}
但是過程我們看不到,需要添加打印代碼!如下:
void Task2(void *argument)
{/**************** 【1】初始化列表、列表項 ****************/// 初始化列表vListInitialise(&List1);// 初始化列表項 vListInitialiseItem(&ListItem1);vListInitialiseItem(&ListItem2);vListInitialiseItem(&ListItem3);// 設置列表項的數值ListItem1.xItemValue = 40;ListItem2.xItemValue = 60;ListItem3.xItemValue = 50;// 打印列表和其他列表項的地址printf("/********* 列表和列表項地址 *********/\r\n");printf("項地址:\r\n");printf("List1: %#x\r\n", (int)&List1); // 打印 TestList 的地址printf("List1->pxIndex: %#x\r\n", (int)(List1.pxIndex)); // 打印 pxIndex 的地址printf("List1->xListEnd: %#x\r\n", (int)&(List1.xListEnd)); // 打印 xListEnd 的地址printf("ListItem1: %#x\r\n", (int)&ListItem1); // 打印 ListItem1 的地址printf("ListItem2: %#x\r\n", (int)&ListItem2); // 打印 ListItem2 的地址printf("ListItem3: %#x\r\n", (int)&ListItem3); // 打印 ListItem3 的地址printf("*************************************/\r\n");/**************** 【1】將列表項1插入列表 ****************/vListInsert(&List1, &ListItem1); // 插入 ListItem1 到 List1 中// 打印添加后的列表項連接情況printf("/******* (1)添加列表項 ListItem1 *******/\r\n"); printf("List1->xListEnd->pxNext: %#x\r\n", (int)(List1.xListEnd.pxNext)); printf("ListItem1->pxNext: %#x\r\n", (int)(ListItem1.pxNext)); printf("/--------- 前后向連接分割線 ----------/\r\n"); printf("List1->xListEnd->pxPrevious: %#x\r\n", (int)(List1.xListEnd.pxPrevious)); printf("ListItem1->pxPrevious: %#x\r\n", (int)(ListItem1.pxPrevious)); printf("**************************************/\r\n"); /**************** 【2】將列表項2插入列表 ****************/vListInsert(&List1, &ListItem2); // 插入 ListItem1 到 List1 中printf("/****** (2)添加列表項 ListItem2 *******/\r\n");printf("List1->xListEnd->pxNext = %#x\r\n", (int)(List1.xListEnd.pxNext));printf("ListItem1->pxNext = %#x\r\n", (int)(ListItem1.pxNext));printf("ListItem2->pxNext = %#x\r\n", (int)(ListItem2.pxNext));printf("/--------- 前后向連接分割線 ----------/\r\n");printf("List1->xListEnd->pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));printf("ListItem1->pxPrevious = %#x\r\n", (int)(ListItem1.pxPrevious));printf("ListItem2->pxPrevious = %#x\r\n", (int)(ListItem2.pxPrevious));printf("/************************************/\r\n");/**************** 【3】將列表項3插入列表 ****************/vListInsert(&List1, &ListItem3);printf("/******** (3)添加列表項 ListItem3 ********/\r\n");printf("List1->xListEnd->pxNext = %#x\r\n", (int)(List1.xListEnd.pxNext));printf("ListItem1->pxNext = %#x\r\n", (int)(ListItem1.pxNext));printf("ListItem2->pxNext = %#x\r\n", (int)(ListItem2.pxNext));printf("ListItem3->pxNext = %#x\r\n", (int)(ListItem3.pxNext));printf("/--------- 前后向連接分割線 ----------/\r\n");printf("List1->xListEnd->pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));printf("ListItem1->pxPrevious = %#x\r\n", (int)(ListItem1.pxPrevious));printf("ListItem2->pxPrevious = %#x\r\n", (int)(ListItem2.pxPrevious));printf("ListItem3->pxPrevious = %#x\r\n", (int)(ListItem3.pxPrevious));printf("/**************************************/\r\n");/**************** 【4】將列表項2移出列表 ****************/uxListRemove(&ListItem2); printf("/********* (4)刪除列表項 ListItem2 *********/\r\n");printf("項目地址:\r\n");printf("List1->xListEnd.pxNext = %#x\r\n", (int)(List1.xListEnd.pxNext));printf("ListItem1->pxNext = %#x\r\n", (int)(ListItem1.pxNext));printf("ListItem3->pxNext = %#x\r\n", (int)(ListItem3.pxNext));printf("/----------- 前后向連接分割線 ------------/\r\n");printf("List1->xListEnd.pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));printf("ListItem1->pxPrevious = %#x\r\n", (int)(ListItem1.pxPrevious));printf("ListItem3->pxPrevious = %#x\r\n", (int)(ListItem3.pxPrevious));printf("/****************************************/\r\n");/**************** 【5】將列表項2插入列表末尾 ****************/vListInsertEnd(&List1, &ListItem2); // 將 ListItem2 添加到鏈表末尾printf("/********* (5)重新在末尾添加列表項 ListItem2 *********/\r\n");printf("項目地址:\r\n");printf("List1->pxIndex = %#x\r\n", (int)List1.pxIndex);printf("List1->xListEnd.pxNext = %#x\r\n", (int)(List1.xListEnd.pxNext));printf("ListItem2->pxNext = %#x\r\n", (int)(ListItem2.pxNext));printf("ListItem1->pxNext = %#x\r\n", (int)(ListItem1.pxNext));printf("ListItem3->pxNext = %#x\r\n", (int)(ListItem3.pxNext));printf("/----------------- 前后向連接分割線 ----------------/\r\n");printf("List1->xListEnd.pxPrevious = %#x\r\n", (int)(List1.xListEnd.pxPrevious));printf("ListItem2->pxPrevious = %#x\r\n", (int)(ListItem2.pxPrevious));printf("ListItem1->pxPrevious = %#x\r\n", (int)(ListItem1.pxPrevious));printf("ListItem3->pxPrevious = %#x\r\n", (int)(ListItem3.pxPrevious));printf("/*************** 鏈表重連后的結構完成 **************/\r\n");while (1){ vTaskDelay(1000); }
}
OK!下載,打開串口輸出:
Hello!Task Start!
Free Heap: 2616
/********* 列表和列表項地址 *********/
項地址:
List1: 0x200000ac
List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4
ListItem1: 0x200000c0
ListItem2: 0x200000d4
ListItem3: 0x200000e8
*************************************/
/******* (1)添加列表項 ListItem1 *******/
List1->xListEnd->pxNext: 0x200000c0
ListItem1->pxNext: 0x200000b4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious: 0x200000c0
ListItem1->pxPrevious: 0x200000b4
**************************************/
/****** (2)添加列表項 ListItem2 *******/
List1->xListEnd->pxNext = 0x200000c0
ListItem1->pxNext = 0x200000d4
ListItem2->pxNext = 0x200000b4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious = 0x200000b4
ListItem2->pxPrevious = 0x200000c0
/************************************/
/******** (3)添加列表項 ListItem3 ********/
List1->xListEnd->pxNext = 0x200000c0
ListItem1->pxNext = 0x200000e8
ListItem2->pxNext = 0x200000b4
ListItem3->pxNext = 0x200000d4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious = 0x200000b4
ListItem2->pxPrevious = 0x200000e8
ListItem3->pxPrevious = 0x200000c0
/**************************************/
/********* (4)刪除列表項 ListItem2 *********/
項目地址:
List1->xListEnd.pxNext = 0x200000c0
ListItem1->pxNext = 0x200000e8
ListItem3->pxNext = 0x200000b4
/----------- 前后向連接分割線 ------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem1->pxPrevious = 0x200000b4
ListItem3->pxPrevious = 0x200000c0
/****************************************/
/********* (5)重新在末尾添加列表項 ListItem2 *********/
項目地址:
List1->pxIndex = 0x200000b4
List1->xListEnd.pxNext = 0x200000c0
ListItem2->pxNext = 0x200000b4
ListItem1->pxNext = 0x200000e8
ListItem3->pxNext = 0x200000d4
/----------------- 前后向連接分割線 ----------------/
List1->xListEnd.pxPrevious = 0x200000d4
ListItem2->pxPrevious = 0x200000e8
ListItem1->pxPrevious = 0x200000b4
ListItem3->pxPrevious = 0x200000c0
/*************** 鏈表重連后的結構完成 **************/
太長啦,我們一個一個分析!!!!!!!
1、初始化列表和列表項
Hello!Task Start!
Free Heap: 2616
/********* 列表和列表項地址 *********/
項地址:
List1: 0x200000ac
List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4
ListItem1: 0x200000c0
ListItem2: 0x200000d4
ListItem3: 0x200000e8
*************************************/
初始化時,List1->xListEnd
的 pxNext
和 pxPrevious
都指向自身。
┌──────────────────────────── List_t ───────────────────────────┐
│ │
│ List1 地址: 0x200000ac │
│ pxIndex → 0x200000b4 (xListEnd) │
│ │
│ ┌──────────────── xListEnd ────────────────────┐ │
│ │ 地址: 0x200000b4 │ │
│ │ pxNext → 0x200000b4 (xListEnd) │ │
│ │ pxPrevious→ 0x200000b4 (xListEnd) │ │
│ └───────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
先注意一個事:
List1->pxIndex: 0x200000b4
List1->xListEnd: 0x200000b4
pxIndex
這個指針用于在鏈表中記錄當前位置。它并不是用來存儲鏈表開始或結束的位置,而是用來標記當前操作或遍歷的節點。默認情況下,pxIndex
指向的是 xListEnd
(鏈表的末尾標記),所以地址相同!!
2、列表項1插入到列表
/******* (1)添加列表項 ListItem1 *******/
List1->xListEnd->pxNext: 0x200000c0
ListItem1->pxNext: 0x200000b4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious: 0x200000c0
ListItem1->pxPrevious: 0x200000b4
**************************************/
列表項1和末尾列表項互指。
┌──────────────────────────── List_t ───────────────────────────┐
│ │
│ List1 地址: 0x200000ac │
│ pxIndex → 0x200000b4 (xListEnd) │
│ │
│ ┌────────────── ListItem1 ───────────────┐ │
│ │ 地址: 0x200000c0 │ │
│ │ pxNext → 0x200000b4 (xListEnd) │ │
│ │ pxPrevious→ 0x200000b4 (xListEnd) │ │
│ └────────────────────────────────────────┘ │
│ ▲ │
│ ▼ │
│ ┌──────────────── xListEnd ────────────────┐ │
│ │ 地址: 0x200000b4 │ │
│ │ pxNext → 0x200000c0 (ListItem1) │ │
│ │ pxPrevious→ 0x200000c0 (ListItem1) │ │
│ └──────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
3、列表項2和3插入到列表
/******* 添加列表項 ListItem2 ********/
List1->xListEnd->pxNext = 0x200000c0
ListItem1->pxNext = 0x200000d4
ListItem2->pxNext = 0x200000b4
/--------- 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious = 0x200000b4
ListItem2->pxPrevious = 0x200000c0
/************************************/
/******** 添加列表項 ListItem3 *********/
List1->xListEnd->pxNext = 0x200000c0
ListItem1->pxNext = 0x200000e8
ListItem2->pxNext = 0x200000b4
ListItem3->pxNext = 0x200000d4
/------------ 前后向連接分割線 ----------/
List1->xListEnd->pxPrevious = 0x200000d4
ListItem1->pxPrevious = 0x200000b4
ListItem2->pxPrevious = 0x200000e8
ListItem3->pxPrevious = 0x200000c0
/**************************************/
可以發現,順序并1-2-3,而是1-3-2。
┌────────────────────────────── List_t ──────────────────────────────┐
│ │
│ List1 地址 : 0x200000ac │
│ pxIndex → 0x200000b4 (xListEnd) │
│ │
│ ┌──────────────────── ListItem1 ─────────────────────┐ │
│ │ 地址 : 0x200000c0 │ │
│ │ pxPrevious → 0x200000b4 (xListEnd) │ │
│ │ pxNext → 0x200000e8 (ListItem3) │?────┐ │
│ └────────────────────────────────────────────────────┘ │ │
│ ▲ │ │
│ ▼ │ │
│ ┌──────────────────── ListItem3 ─────────────────────┐ │ │
│ │ 地址 : 0x200000e8 │ │ │
│ │ pxPrevious → 0x200000c0 (ListItem1) │ │ │
│ │ pxNext → 0x200000d4 (ListItem2) │ │ │
│ └────────────────────────────────────────────────────┘ │ │
│ ▲ │ │
│ ▼ │ │
│ ┌──────────────────── ListItem2 ─────────────────────┐ │ │
│ │ 地址 : 0x200000d4 │ │ │
│ │ pxPrevious → 0x200000e8 (ListItem3) │ │ │
│ │ pxNext → 0x200000b4 (xListEnd) │ │ │
│ └────────────────────────────────────────────────────┘ │ │
│ ▲ │ │
│ ▼ │ │
│ ┌───────────────────── xListEnd ─────────────────────┐ │ │
│ │ 地址 : 0x200000b4 │ │ │
│ │ pxPrevious → 0x200000d4 (ListItem2) │ │ │
│ │ pxNext → 0x200000c0 (ListItem1) │?─────┘ │
│ └────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
這是因為我們剛剛設置的列表項的數值
ListItem1.xItemValue = 40; ListItem2.xItemValue = 60; ListItem3.xItemValue = 50;
當我們將這些列表項插入到列表中時,列表會根據 xItemValue
的值進行排序!!!
3、刪掉列表項2
/********* (4)刪除列表項 ListItem2 *********/
項目地址:
List1->xListEnd.pxNext = 0x200000c0
ListItem1->pxNext = 0x200000e8
ListItem3->pxNext = 0x200000b4
/----------- 前后向連接分割線 ------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem1->pxPrevious = 0x200000b4
ListItem3->pxPrevious = 0x200000c0
/****************************************/
移出列表項2后,列表的結構通過調整 pxNext
和 pxPrevious
指針得以重新連接,使得 ListItem1
和 ListItem3
和xListEnd
又形成了一個連續的雙向鏈表。
┌────────────────────────────── List_t ──────────────────────────────┐
│ │
│ List1 地址 : 0x200000ac │
│ pxIndex → 0x200000b4 (xListEnd) │
│ │
│ ┌──────────────────── ListItem1 ─────────────────────┐ │
│ │ 地址 : 0x200000c0 │ │
│ │ pxPrevious → 0x200000b4 (xListEnd) │ │
│ │ pxNext → 0x200000e8 (ListItem3) │?────┐ │
│ └────────────────────────────────────────────────────┘ │ │
│ ▲ │ │
│ ▼ │ │
│ ┌──────────────────── ListItem3 ─────────────────────┐ │ │
│ │ 地址 : 0x200000e8 │ │ │
│ │ pxPrevious → 0x200000c0 (ListItem1) │ │ │
│ │ pxNext → 0x200000b4 (ListItem2) │ │ │
│ └────────────────────────────────────────────────────┘ │ │
│ ▲ │ │
│ ▼ │ │
│ ┌───────────────────── xListEnd ─────────────────────┐ │ │
│ │ 地址 : 0x200000b4 │ │ │
│ │ pxPrevious → 0x200000e8 (ListItem2) │ │ │
│ │ pxNext → 0x200000c0 (ListItem1) │?─────┘ │
│ └────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
4、列表項2插入列表末尾
/********* (5)重新在末尾添加列表項 ListItem2 *********/
項目地址:
List1->pxIndex = 0x200000b4
List1->xListEnd.pxNext = 0x200000c0
ListItem2->pxNext = 0x200000b4
ListItem1->pxNext = 0x200000e8
ListItem3->pxNext = 0x200000d4
/----------------- 前后向連接分割線 ----------------/
List1->xListEnd.pxPrevious = 0x200000d4
ListItem2->pxPrevious = 0x200000e8
ListItem1->pxPrevious = 0x200000b4
ListItem3->pxPrevious = 0x200000c0
/*************** 鏈表重連后的結構完成 **************/
我們是將 pxNewListItem
插入到 xListEnd.pxPrevious
后面,即鏈表的邏輯尾部(實際是尾前一項),也就是在 xListEnd
前面(因為 xListEnd
是一個固定項,永遠在尾部)。
所以,框圖跟刪掉列表項2之前一模一樣
xListEnd <--> ListItem1 <--> ListItem3 <--> ListItem2 <--> xListEnd
關鍵點:pxIndex
的作用
pxIndex
這個指針用于在鏈表中記錄當前位置。它并不是用來存儲鏈表開始或結束的位置,而是用來標記當前操作或遍歷的節點。- 如果
pxIndex
沒有被改變,默認情況下它指向鏈表的結尾(xListEnd
)。 - 當調用
vListInsertEnd(&List1, &ListItem2)
時,ListItem2
會被插入到xListEnd
之前,也就是pxIndex
之前,即鏈表的末尾。
如果我們改動一下呢??
List1.pxIndex = &ListItem1; // 讓其指向列表項1vListInsertEnd(&List1, &ListItem2); // 將 ListItem2 添加到鏈表末尾
重新下載輸出
/********* (5)重新在末尾添加列表項 ListItem2 *********/
項目地址:
List1->pxIndex = 0x200000c0
List1->xListEnd.pxNext = 0x200000d4
ListItem2->pxNext = 0x200000c0
ListItem1->pxNext = 0x200000e8
ListItem3->pxNext = 0x200000b4
/----------------- 前后向連接分割線 ----------------/
List1->xListEnd.pxPrevious = 0x200000e8
ListItem2->pxPrevious = 0x200000b4
ListItem1->pxPrevious = 0x200000d4
ListItem3->pxPrevious = 0x200000c0
/*************** 鏈表重連后的結構完成 **************/
我們來對比看看
字段 | 第一次(pxIndex = 0x200000b4) | 第二次(pxIndex = 0x200000c0) |
---|---|---|
List1->pxIndex | 0x200000b4 | 0x200000c0 |
List1->xListEnd.pxNext | 0x200000c0 | 0x200000d4 |
List1->xListEnd.pxPrevious | 0x200000d4 | 0x200000e8 |
列表項順序 | 1 → 3 → 2 → xListEnd | 2 → 1 → 3 → xListEnd |
列表項2被插入到列表項1的前面了!!
所以,在末尾插入列表項,是靠List1->pxIndex
決定的!!
OK,一章又完美結束!
七、啟動任務調度器【內容太多,先略】
7.1 概念理解
我們點開main.c
,可以看見
/* Init scheduler */osKernelInitialize();/* Call init function for freertos objects (in cmsis_os2.c) */MX_FREERTOS_Init();/* Start scheduler */osKernelStart();
這個是CubeMX自動生成的
步驟 | 函數名 | 作用 |
---|---|---|
1 | osKernelInitialize() | 初始化 RTOS 內核 |
2 | MX_FREERTOS_Init() | 創建任務、信號量、隊列等 RTOS 對象 |
3 | osKernelStart() | 啟動調度器,開始運行任務 |
第三句就是啟動調度器!
osKernelStart()
是 CMSIS-RTOS v2 接口下的 FreeRTOS 調用方式;- 如果你使用的是原始 FreeRTOS API,則對應函數是
vTaskStartScheduler();
- 調用這個函數后,RTOS 會接管 MCU 的控制權,不會再返回主函數。
我們點進函數看看
osStatus_t osKernelStart (void) {osStatus_t stat;// 檢查當前是否在中斷上下文中執行if (IS_IRQ()) {stat = osErrorISR; // 如果是在中斷中調用 osKernelStart,返回錯誤}else {// 檢查當前內核狀態是否為“就緒”if (KernelState == osKernelReady) {/* 設置 SVC 的優先級為默認值(在 FreeRTOS 中用于上下文切換) */SVC_Setup();/* 更改內核狀態為“正在運行” */KernelState = osKernelRunning;/* 啟動 FreeRTOS 的調度器,開始任務調度 */vTaskStartScheduler();// 啟動成功,返回 osOK(正常)stat = osOK;} else {// 如果不是“就緒”狀態,不允許啟動,返回錯誤stat = osError;}}// 返回啟動結果return (stat);
}
有點迷迷糊糊,再看看
代碼行 | 說明 |
---|---|
IS_IRQ() | 判斷當前代碼是否在中斷上下文中執行。不能在中斷中啟動調度器! |
KernelState == osKernelReady | 啟動調度器之前,內核狀態必須是“就緒”狀態。 |
SVC_Setup() | 配置 SVC(Supervisor Call)優先級。FreeRTOS 利用 SVC 觸發上下文切換。 |
KernelState = osKernelRunning; | 狀態標志位更新,表示 RTOS 現在正在運行。 |
vTaskStartScheduler(); | 核心函數,正式啟動調度器,執行最高優先級任務。 |
stat = osOK / osError / osErrorISR | 返回啟動狀態,供調用者判斷是否成功。 |
最最最重要的是!vTaskStartScheduler()
它的作用是:
- 創建空閑任務 (Idle Task),確保系統始終有任務在運行。
- (可選)創建定時器任務 (Timer Task) ,用于管理軟件定時器。
- 初始化調度器內核相關變量。
- 關閉中斷,調用底層啟動調度函數(啟動系統時鐘節拍中斷和任務切換)。
- 進入任務調度狀態,開始多任務運行。
略==
八、時間片調度
8.1 概念理解
什么是時間片?
- 就是每個任務可以占用 CPU 的時間長度
- 等于系統滴答定時器的中斷周期(
SysTick
,比如 1ms)
假設,我們創建了 3 個優先級相同的任務:Task1
, Task2
, Task3
,系統每 1ms 觸發一次 SysTick
,即每個任務時間片為 1ms。
時間線 →
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│Task1 │Task2 │Task3 │Task1 │Task2 │Task3 │Task1 │Task2 │Task3 │...
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘↑ ↑ ↑1ms 2ms 3ms ...
① 時間片不能“累加”
- 如果一個任務時間片沒用完(比如提前進入阻塞態),它不會“攢時間”留著以后用。
- 系統立刻調度下一個就緒態任務。
② 阻塞狀態打斷調度
- 比如
Task3
運行到一半用vTaskDelay(100)
進入阻塞,即使還有時間也不會等待! - 立馬進入下一個就緒任務(如
Task1
),而不是等到Task3
時間片結束。
② 時間片大小
- 取決于滴答定時器中斷頻率
舉個小例子:
假設,食堂里有三個學生排隊打飯:小明、小紅、小剛。他們都同等重要(優先級一樣)。
食堂規定:每個人只能打飯 30秒鐘,就必須輪到下一個人繼續打飯,這 30 秒就是“時間片”。
特別情況:有人提前走了,如果小紅打飯打了一半,有急事走了(類比阻塞)。
那食堂阿姨不會等他時間30秒用完,而是立刻讓下一個人(小剛)繼續打飯。
8.2 函數介紹
8.3 編寫例題代碼
正點原子例題
總結
自用