FreeRTOS高效应用实战
基于STM32CubeIDE生成对芯片移植好的FreeRTOS工程,使用HAL库编写FreeRTOS应用程序,实现FreeRTOS高效应用实战
引入函数句柄的概念
函数句柄(Function Handle)是编程中用于间接引用和操作函数的一种机制,其本质是将函数作为数据来传递和存储。以下是关于函数句柄的详细说明:
核心概念解析
特性描述间接调用通过变量调用函数,而非直接使用函数名运行时绑定允许在程序运行时动态确定要执行的函数数据化函数函数可像普通变量一样被赋值、传递和返回在FreeRTOS中函数句柄大量使用,需对其有一定理解
任务
FreeRTOS 中的 任务(Task) 是系统调度的基本单元,类似于操作系统中的线程。每个任务代表一个独立的执行流程,拥有自己的堆栈空间和优先级
任务的核心特性
特性说明独立性每个任务拥有独立堆栈和程序计数器(PC)优先级驱动系统基于优先级进行抢占式调度(0 最低,configMAX_PRIORITIES-1 最高)状态管理任务在运行、就绪、阻塞、挂起状态间转换资源隔离任务通过信号量、队列等机制安全共享资源低延迟切换FreeRTOS 任务切换时间通常在微秒级(取决于硬件)任务的五种状态
- 运行(Running):当前正在 CPU 上执行的任务
- 就绪(Ready):已准备好执行,等待调度器分配 CPU
- 阻塞(Blocked):因等待事件(如信号量、延迟)暂停执行
- 挂起(Suspended):被显式挂起,不参与调度(需手动恢复)
- 删除(Deleted):任务已终止,等待资源回收
任务调度机制
FreeRTOS 采用 抢占式调度 和 时间片轮转 的混合策略:
- 抢占规则
- 高优先级任务就绪时 立即抢占 低优先级任务
- 同优先级任务按 时间片轮转(默认为 1 个系统节拍)
- 调度触发条件
- 系统节拍中断(Tick Interrupt)
- 任务主动释放 CPU(taskYIELD())
- 资源释放(如发送信号量、队列)
声明任务头文件
- #include "FreeRTOS.h"
- #include "task.h"
复制代码 声明任务句柄(x_Handle),定义任务属性(任务名称,堆栈大小,优先级)(x_attributes)
- /* myTask_01_led1 的定义 */
- osThreadId_t myTask_01_led1Handle; // 声明myTask_01_led1 的句柄
- const osThreadAttr_t myTask_01_led1_attributes = { // myTask_01_led1 的属性
- .name = "myTask_01_led1", // 任务名称
- .stack_size = 128 * 4, // 堆栈大小为 512 字节
- .priority = (osPriority_t) osPriorityLow, // 优先级为低
- };
- /* myTask02_led2_ 的定义 */
- osThreadId_t myTask02_led2_Handle; // myTask02_led2_ 的句柄
- const osThreadAttr_t myTask02_led2__attributes = { // myTask02_led2_ 的属性
- .name = "myTask02_led2_", // 任务名称
- .stack_size = 128 * 4, // 堆栈大小为 512 字节
- .priority = (osPriority_t) osPriorityLow, // 优先级为低
- };
复制代码 任务执行函数(for死循环或while死循环)
- /* USER CODE END Header_myTask01_led1 */
- void myTask01_led1(void *argument)
- {
- /* USER CODE BEGIN myTask01_led1 */
- /* Infinite loop */
- for(;;)
- {
- /*用户编写执行程序
- HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1);
- osDelay(200);
- */
- }
- /* USER CODE END myTask01_led1 */
- }
复制代码 创建任务
使用osThreadNew()函数创建任务,传入任务入口函数指针,任务入口函数参数指针(无则NULL),任务属性结构体指针
osThreadNew()返回值保存到任务句柄(x_Handle)
- /* creation of myTask_01_led1 */
- myTask_01_led1Handle = osThreadNew(myTask01_led1, NULL, &myTask_01_led1_attributes);
- //任务句柄 // 任务函数 任务属性
- /* creation of myTask02_led2_ */
- myTask02_led2_Handle = osThreadNew(myTask02_led2, NULL, &myTask02_led2__attributes);
复制代码 FreeRTOS初始化末尾创建任务,在初始化结束后,相同优先级任务按照时间片轮转执行,宏观则表现为任务同时执行
信号量
二值信号量:
二值信号量,可以理解为标志位
二值信号量(Binary Semaphore)在 FreeRTOS 中通常用于任务间的同步和互斥。
它的值只能是 0 或 1,适合用于实现简单的同步机制。以下是二值信号量的使用方法:
声明信号量头文件
- #include "semphr.h"//操作信号量的头文件
复制代码 声明二值信号量,创建二值信号量
- SemaphoreHandle_t xBinarySemaphore;
- xBinarySemaphore = xSemaphoreCreateBinary();
复制代码 获取信号量
任务函数中:- if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {//
- // 成功获取信号量,执行相关操作
- // ...
- }
复制代码 释放信号量
- xSemaphoreGive(xBinarySemaphore);
复制代码 使用示例
- //1. **任务 A**:等待信号量
- void vTaskA(void *pvParameters) {
- while (1) {
- if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {
- // 处理任务
- // ...
- // 释放信号量,通知其他任务
- xSemaphoreGive(xBinarySemaphore);
- }
- }
- }
- //2. **任务 B**:释放信号量
- void vTaskB(void *pvParameters) {
- while (1) {
- // 执行某些操作
- // ...
- // 释放信号量,通知任务 A
- xSemaphoreGive(xBinarySemaphore);
- // 等待一段时间
- vTaskDelay(pdMS_TO_TICKS(1000));
- }
- }
复制代码注意事项
- 初始化: 二值信号量需要在使用前通过 xSemaphoreCreateBinary() 创建。
- 首次释放: 在创建后,信号量默认是不可用的(值为0)。可以使用 xSemaphoreGive() 初始化为可用状态(值为1)。
- 同步: 使用二值信号量可以确保任务的同步,比如在任务 A 中等待任务 B 释放信号量来继续执行。
这种信号量在实现任务间的简单同步时非常有用。
但可能出现优先级翻转问题
优先级翻转现象图解
- TEXT任务优先级:Task_H(高) > Task_M(中) > Task_L(低)
- 时间轴 | 事件流
- ----------------------------------------
- t0 Task_L 获取信号量
- t1 Task_H 请求信号量 → 阻塞
- t2 Task_M 就绪 → 抢占Task_L
- t3 Task_M 长时间运行
- t4 Task_L 无法继续执行 → 不能释放信号量
- t5 Task_H 持续阻塞 → 系统实时性破坏
复制代码 互斥信号量(Mutex)
优化方案:使用互斥信号量可以减少优先级翻转问题
解决办法:
使用互斥量替代二值信号量
定义互斥信号量句柄,属性
- osMutexId_t tokenHandle;// 定义一个互斥锁句柄变量,用于后续操作互斥锁
- const osMutexAttr_t token_attributes = {// 定义一个常量结构体,用于设置互斥锁的属性
- // 设置互斥锁的名称为 "token",便于调试和识别
- .name = "token"
- };
复制代码 创建互斥信号量
- // 创建一个新的互斥锁,并返回其句柄
- tokenHandle = osMutexNew(&token_attributes);
- // tokenHandle 变量,用于存储互斥锁的句柄
- // osMutexNew 函数,用于创建一个新的互斥锁
- // &token_attributes 指向互斥锁属性的指针,用于配置互斥锁的行为
复制代码 获取信号量,释放信号量- // 尝试获取信号量tokenHandle,等待时间为portMAX_DELAY(无限等待直到获取成功)
- if (xSemaphoreTake(tokenHandle, portMAX_DELAY) == pdTRUE) //
- {
- USART1_printf("%s\r\n",strHigh);
- HAL_Delay(10);
- // 释放信号量tokenHandle
- xSemaphoreGive(tokenHandle);
- }
复制代码 二值信号量与互斥量对比
特性二值信号量互斥量优先级继承不支持支持递归获取不可可初始状态空可用适用场景事件通知、简单同步资源互斥访问计数信号量
定义计数信号量
- /* Definitions for Sem_Tables */
- osSemaphoreId_t Sem_TablesHandle;// 假设 Sem_Tables_attributes 是预定义的信号量属性结构体
- const osSemaphoreAttr_t Sem_Tables_attributes = {
- .name = "Sem_Tables" // 设置信号量的名称
- };
复制代码 创建计数信号量
- CountingSemHandle = osSemaphoreNew(5, 5, &CountingSem_attributes);
复制代码
- 第一个参数 5:信号量的最大计数值。
- 第二个参数 5:信号量的初始计数值。
- 第三个参数 &CountingSem_attributes:信号量的属性。
- CountingSemHandle:这是信号量的句柄,用于在代码中引用该信号量。
从计数信号量内获取一个信号量
- xSemaphoreTake(CountingSemHandle, pdMS_TO_TICKS(100)//(计数信号量(剩余)数值减少)
复制代码 释放计数信号量
- xSemaphoreGiveFromISR(CountingSemHandle, &highTaskWoken);//(计数信号量(剩余)数值增加)
复制代码注意:_FromISR后缀函数是在中断服务程序(ISR)中运行的函数,确保中断安全
事件组(EvenGroup)
声明事件组头文件
- #include "event_groups.h"
复制代码 先设置事件组掩码(二进制),相当于标志位
[code]#define BITMASK_KEY_LEFT (0b00000001 |