FreeRTOS的详细学习笔记配备代码适合新手学习
FreeRTOS是一个基于STM32 HAL库的免费实时操作系统(RTOS),其核心目标是确保特定外部事件在规定时间内被响应。该系统采用优先级调度和时间片轮询策略,通过任务队列管理就绪、阻塞和挂起状态的任务。关键特性包括:任务切换通过systick中断触发上下文切换;提供xTaskCreate动态创建和xTaskCreateStatic静态创建任务两种API;支持临界区保护代码完整性。与裸机开发相
FreeRTOS(基于是STM32HAL库)
文章目录
免费实时操作系统
计算机分为三部分 :硬件 操作系统 应用程序
操作系统 :通用(Linux,windows) 和 实时(rtos)
实时操作系统(RTOS-Real Time Operating System)中实时(Real Time)指的是任务(Task)或者说实现一个功能的线程(Thread)必须在给定的时间(Deadline)内完成。
分成多个时间片。每个时间片切换成不同的程序运行,实现多个任务同时运行的现象,提高效率。
调度器启动后,调度器后面的代码将没有意义,通常后面无代
vTaskStartScheduler(); // 启动调度器
调度策略:时间片轮询(均匀执行),优先级策略(抢占执行)
FreeRTOS的目标:确保某些特定的外部事件在规定的时间内被响应,也就是处理这些外部事件的任务在规定时间内被执行。
rtos调度原理


任务队列:就绪,阻塞,挂起
就绪态是调度的核心,相当于二级数组 先按优先级分组,再按先后顺序分
任务切换时间
systick中断:每次中断,FreeRTOS都会判断是否需要切换任务,如果有,
则会进行任务的上下文切换(Context Swtich)
上下文切换
上下文指的是一个任务在运行时,CPU的当前状态,包括通用寄存器,程序计数
器,堆栈指针等等。所谓的上下文切换,就是将当前任务的CPU状态保存下来,
再将新任务的CPU状态恢复回去。
rtos系统移植
无,需自己找一下资源,不难
rtos中断



操作系统的启动入口
//封装一个函数进行调用调度器
void FreeRTOS_Start(void)
{
//创建任务
xTaskCreate();
// 最后一行代码一定是启动调度器
// 调度器会启动一个空闲任务 没事干就去空闲任务
vTaskStartScheduler(); //开启任务调度去
}
创建任务API实际使用
xTaskCreate(); //动态创建 RAM由系统自定分配
xTaskCreateStatic();//静态创建 需手动自己分配
/*
pvTaskCode
参数1:指向任务入口函数的指针;任务以无限循环的形式实现;实现任务的函数不可以返回或退出,可以删除
pcName
参数2:任务的名字
uxStackDepth
参数3:要分配用作任务堆栈的字数(不是字节数!)。例如,如果堆栈宽度为16位,uxStackDepth为100,则将分配200字节用作任务堆栈。再举一例,如果堆栈宽度为32位,uxStackDepth为400,则将分配1600字节用作任务堆栈。堆栈深度与堆栈宽度的乘积不得超过size_t类型变量所能包含的最大值
pvParameters
参数4:作为参数传递给所创建任务的值。如果pvParameters设置为某变量的地址,则在创
建的任务执行时,该变量必须仍然存在,因此,不能传递堆栈变量的地址。
uxPriority
参数5:优先级 0-4 数字越大越优先
pxCreatedTask
参数6:用于将句柄传递至由xTaskCreate()函数创建的任务。pxCreatedTask是可选参数,可设置为NULL。(标识符)
*/
//特定类型的函数指针
TaskHandle_t task1_handle; //结构体
void task1(void* pvParam);
{
while(1)
{
led_Toggle(); //灯翻转
delay_ms(500);
}
}
void FreeRTOS_Start(void)
{
//创建任务
xTaskCreate(task1,"TASK1",128,NULL,4,&task1_handle);
vTaskStartScheduler(); //开启任务调度去
}
裸机开发和RTOS的区别
裸机的:
current_tick 是当前函数运行后的毫秒
例子采用的是hal库
uint32_t tick1 = 0;
uint32_t tick2 = 0;
while (1)
{
uint32_t current_tick = HAL_GetTick();
if (current_tick - tick1 >= 500)
{
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
tick1 = current_tick;
}
if (current_tick - tick2 >= 800)
{
Int_LED_Toggle(LED2_GPIO_Port, LED2_Pin);
tick2 = current_tick;
}
}
rtos:
TaskHandle_t task1_handle; //结构体
TaskHandle_t task2_handle; //结构体
void task1(void* pvParam);
{
while(1)
{
led_Toggle(led1); //灯翻转
//delay_ms(500);
//阻塞状态 =》释放cpu
vTaskDelay(500); // 应该使用rtos内置的延时函数,他是时间片轮询,用外置的是错的。
}
}
void task2(void* pvParam);
{
while(1)
{
led_Toggle(led2); //灯翻转
//delay_ms(500);
vTaskDelay(500);
}
}
void FreeRTOS_Start(void)
{
//创建任务
xTaskCreate(task1,128,NULL,4,&task1_handle);
xTaskCreate(task2,128,NULL,4,&task2_handle);
vTaskStartScheduler(); //开启任务调度去
}
调度策略
TaskHandle_t task1_handle; //结构体
TaskHandle_t task2_handle; //结构体
TaskHandle_t task3_handle; //结构体
void task1(void* pvParam);
{
while(1)
{
led_Toggle(led1); //灯翻转
delay_ms(500);
//vTaskDelay(500);
}
}
void task2(void* pvParam);
{
while(1)
{
led_Toggle(led2); //灯翻转
//delay_ms(500);
vTaskDelay(500);
}
}
void task3(void* pvParam);
{
while(1)
{
led_Toggle(led3); //灯翻转
// freeRTOS是优先级调度 =>如果最高优先级的任务不会知道释放CPU 会永久只执行一个函数
//delay_ms(500);
vTaskDelay(500);
}
}
void FreeRTOS_Start(void)
{
//创建任务
xTaskCreate(task1,"TASK1",128,NULL,4,&task1_handle);
xTaskCreate(task2,"TASK2",128,NULL,3,&task2_handle);
xTaskCreate(task3,"TASK3",128,NULL,3,&task3_handle);
vTaskStartScheduler(); //开启任务调度去
}
// 如果最高优先级为delay_ms();最终效果只有最高优先级的任务进行运行,低优先级一直等待
//如果都是delay_ms(); 那么是时间片轮询
taskENTER_CRITICAL();// 进入临界区
//保护中间的代码运行完整,不被打断
taskEXIT_CRITICAL();//突出临界区

静态创建任务
参数 6:StackType_t * const puxStackBuffer(静态创建核心参数)
-
作用:传入用户「静态分配的任务栈空间」,是静态创建与动态创建的关键区别(动态创建无需用户分配,RTOS 从堆申请)。
-
类型说明:
StackType_t是 FreeRTOS 定义的栈元素类型(通常为uint32_t),因此栈空间需定义为StackType_t数组。 -
配置要求
- 必须是静态分配(
static修饰)或全局数组(避免栈空间被编译器回收); - 数组长度必须 ≥ 参数 3(
ulStackDepth),否则实际可用栈小于配置值,易导致栈溢出
- 必须是静态分配(
参数 7:StaticTask_t * const pxTaskBuffer(静态创建核心参数)
- 作用:传入用户「静态分配的任务控制块(TCB)」,TCB 是 RTOS 管理任务的核心数据结构(存储任务状态、优先级、栈指针等信息)。
- 类型说明:
StaticTask_t是 FreeRTOS 定义的静态 TCB 类型(动态创建时用TCB_t,由 RTOS 动态分配)。 - 配置要求
- 必须是静态分配(
static修饰)或全局变量(不能是局部变量); - 无需用户初始化,RTOS 会在创建任务时自动填充 TCB 信息。
- 必须是静态分配(
#define configSUPPORT_STATIC_ALLOCATION 1
//配置静态创建
#define LED1_TASK_STACK_SIZE 128
#define LED1_TASK_PRIORITY 3
TaskHandle_t xTask1Handle;
StackType_t xTask1Stack[LED1_TASK_STACK_SIZE];
StaticTask_t xTask1TCB;
// 空闲任务分配空间
#define IDLE_TASK_STACK_SIZE 128
StackType_t xIdleTaskStack[IDLE_TASK_STACK_SIZE];
StaticTask_t xIdleTaskTCB;
void vApplicationGetIdleTaskMemory(StaticTask_t ** ppxIdleTaskTCBBuffer,StackType_t ** ppxIdleTaskStackBuffer,uint32_t *pulIdleTaskStackSize)
{
*pulIdleTaskStackSize = IDLE_TASK_STACK_SIZE;
*ppxIdleTaskStackBuffer = xIdleTaskStack;
*ppxIdleTaskTCBBuffer = &xIdleTaskTCB;
}
void task1(void* pvParam);
{
while(1)
{
led_Toggle(led1); //灯翻转
// delay_ms(500);
vTaskDelay(500);
}
}
void FreeRTOS_Start(void)
{
//创建任务
xTask1handle = xTaskCreateStatic(task1,"TASK1",LED1_TASK_STACK_SIZE,NULL,LED1_TASK_PRIORITY, xTask1Stack,&xTask1TCB);
vTaskStartScheduler(); //开启任务调度去
}
// 一般使用动态,不推荐使用静态(需要自己添加的东西太多)
删除任务
删除任务的核心逻辑(调度器层面)
无论静态还是动态任务,删除时调度器都会执行以下核心操作:
-
移除任务状态:将目标任务从「就绪态、阻塞态、挂起态」等对应列表中移除,确保调度器不再选择该任务执行;
-
标记任务无效:将任务控制块(TCB)标记为「已删除」,避免其他 API(如
vTaskSuspend())误操作; -
触发调度器切换:如果删除的是「当前运行的任务」或「高优先级任务」,调度器会立即重新选择最高优先级的就绪任务执行(避免 CPU 无任务可运行);
-
资源释放:这是最关键的差异点 —— 动态任务会自动释放资源,静态任务需手动处理。
注意:
空闲任务负责释放由RTOS内核分配给已删除任务的内存。因此,如果应用程序调用了 vTaskDelete()分配的内存不会自动释放,应在任务删除之前手动释放。请务必确保空闲任务获得足够的微控制器处理时间。任务代码
void task1(void *pvParameters);
#define LED1_TASK_STACK_SIZE 128
#define LED1_TASK_PRIORITY 3
TaskHandle_t xTask1Handle;
// task2 LED2灯闪烁
void task2(void *pvParameters);
#define LED2_TASK_STACK_SIZE 128
#define LED2_TASK_PRIORITY 3
TaskHandle_t xTask2Handle;
// taks3 按键删除任务
void task3(void *pvParameters);
#define KEY_TASK_STACK_SIZE 128
#define KEY_TASK_PRIORITY 3
TaskHandle_t xTask3Handle;
void task1(void* pvParam);
{
while(1)
{
led_Toggle(led1); //灯翻转
delay_ms(500);
//vTaskDelay(500);
}
}
void task2(void* pvParam);
{
while(1)
{
led_Toggle(led2); //灯翻转
//delay_ms(500);
vTaskDelay(500);
}
}
void task3(void* pvParam);
{
while(1)
{
Key_Type key = Int_Key_Scan();
if (key == KEY1)
{
// 删除任务需要填写句柄
vTaskDelete(xTask1Handle);
}
else if (key == KEY2)
{
vTaskDelete(xTask2Handle);
}
}
}
void FreeRTOS_Start(void)
{
//创建任务
xTaskCreate(task1, "task1", LED1_TASK_STACK_SIZE, NULL, LED1_TASK_PRIORITY, &xTask1Handle);
xTaskCreate(task2, "task2", LED2_TASK_STACK_SIZE, NULL, LED2_TASK_PRIORITY, &xTask2Handle);
xTaskCreate(task3, "task3", KEY_TASK_STACK_SIZE, NULL, KEY_TASK_PRIORITY, &xTask3Handle);
vTaskStartScheduler(); //开启任务调度去
}
常见rtos代码格式
void task1(void *pvParameters);
#define LED1_TASK_STACK_SIZE 128
#define LED1_TASK_PRIORITY 3
TaskHandle_t xTask1Handle;
// task2 LED2灯闪烁
void task2(void *pvParameters);
#define LED2_TASK_STACK_SIZE 128
#define LED2_TASK_PRIORITY 3
TaskHandle_t xTask2Handle;
// taks3 按键删除任务
void task3(void *pvParameters);
#define KEY_TASK_STACK_SIZE 128
#define KEY_TASK_PRIORITY 3
TaskHandle_t xTask3Handle;
// 启动任务
void start_task(void *pvParameters);
#define START_TASK_STACK_SIZE 128
#define START_TASK_PRIORITY 5
TaskHandle_t xStartTaskHandle;
I
void task1(void* pvParam);
{
while(1)
{
led_Toggle(led1); //灯翻转
delay_ms(500);
//vTaskDelay(500);
}
}
void task2(void* pvParam);
{
while(1)
{
led_Toggle(led2); //灯翻转
//delay_ms(500);
vTaskDelay(500);
}
}
void task3(void* pvParam);
{
while(1)
{
Key_Type key = Int_Key_Scan();
if (key == KEY1)
{
// 删除任务需要填写句柄
vTaskDelete(xTask1Handle);
}
else if (key == KEY2)
{
vTaskDelete(xTask2Handle);
}
}
}
void start_task(void *pvParameters)
{
xTaskCreate(task1, "task1", LED1_TASK_STACK_SIZE, NULL, LED1_TASK_PRIORITY, &xTask1Handle);
xTaskCreate(task2, "task2", LED2_TASK_STACK_SIZE, NULL, LED2_TASK_PRIORITY, &xTask2Handle);
xTaskCreate(task3, "task3", KEY_TASK_STACK_SIZE, NULL, KEY_TASK_PRIORITY, &xTask3Handle);
//删除任务
vTaskDelete(NULL); //创建完另外三个任务后,将自己删除
}
void FreeRTOS_Start(void)
{
// 启动任务
xTaskCreate(start_task, "start_task", START_TASK_STACK_SIZE, NULL,START_TASK_PRIORITY, &xStartTaskHandle);
vTaskStartScheduler(); //开启任务调度去
}
任务的恢复和挂起
API
//挂起是按条件,阻塞是按时间
vTaskSuspend();
vTaskReaume();
实例代码
void task1(void *pvParameters);
#define LED1_TASK_STACK_SIZE 128
#define LED1_TASK_PRIORITY 3
TaskHandle_t xTask1Handle;
// task2 LED2灯闪烁
void task2(void *pvParameters);
#define LED2_TASK_STACK_SIZE 128
#define LED2_TASK_PRIORITY 3
TaskHandle_t xTask2Handle;
// taks3 按键删除任务
void task3(void *pvParameters);
#define KEY_TASK_STACK_SIZE 128
#define KEY_TASK_PRIORITY 3
TaskHandle_t xTask3Handle;
// 启动任务
void start_task(void *pvParameters);
#define START_TASK_STACK_SIZE 128
#define START_TASK_PRIORITY 5
TaskHandle_t xStartTaskHandle;
I
void task1(void* pvParam);
{
while(1)
{
led_Toggle(led1); //灯翻转
delay_ms(500);
//vTaskDelay(500);
}
}
void task2(void* pvParam);
{
while(1)
{
led_Toggle(led2); //灯翻转
//delay_ms(500);
vTaskDelay(500);
}
}
void task3(void* pvParam);
{
while(1)
{
Key_Type key = Int_Key_Scan();
if (key == KEY1)
{
//挂起任务
vTaskSuspend(xTask1Handle);
}
else if (key == KEY2)
{
//恢复任务
vTaskReaume(xTask1Handle);
}
//省略其他几个灯的操作
}
}
void start_task(void *pvParameters)
{
xTaskCreate(task1, "task1", LED1_TASK_STACK_SIZE, NULL, LED1_TASK_PRIORITY, &xTask1Handle);
xTaskCreate(task2, "task2", LED2_TASK_STACK_SIZE, NULL, LED2_TASK_PRIORITY, &xTask2Handle);
xTaskCreate(task3, "task3", KEY_TASK_STACK_SIZE, NULL, KEY_TASK_PRIORITY, &xTask3Handle);
//删除任务
vTaskDelete(NULL); //创建完另外三个任务后,将自己删除
}
void FreeRTOS_Start(void)
{
// 启动任务
xTaskCreate(start_task, "start_task", START_TASK_STACK_SIZE, NULL,START_TASK_PRIORITY, &xStartTaskHandle);
vTaskStartScheduler(); //开启任务调度去
}
进出临界区!
API
taskENTER_CRITICAL();// 进入临界区
//保护中间的代码运行完整,不被打断
taskEXIT_CRITICAL();//突出临界区

代码
//其他代码同上
void task3(void* pvParam);
{
while(1)
{
Key_Type key = Int_Key_Scan();
if (key == KEY1)
{
/*
1. portDISABLE_INTERRUPTS();关闭中断 屏蔽的中断优先级 11-15
主要屏蔽的中断 systick =>无法执行任务切换
2. 将临界区的标记+1
*/
//可以多次进入临界区,嵌套
taskENTER_CRITICAL();
printf("key1\r\n");
// 退出临界区
/*
1. 将临界区的标记-1
2. 判断标记为0 打开中断
正常切换
*/
taskEXIT_CRITICAL();
}
else if (key == KEY2)
{
}
//省略其他几个灯的操作
}
}
挂起恢复调度器
//挂起和临界区的作用差不多一致,都是保证代码运行完整。但是挂起不会关闭中断(区别)。
// 挂起调度器
vTaskSuspendAll();
printf("挂起调度器\r\n");
delay_us(5000000);
// 恢复调度器
printf("恢复调度器\r\n");
xTaskResumeAll();
任务相关的实用API

一般的
//第一个 调用一个句柄,返回一个优先级
// 获取优先级
UBaseType_t task1_pri = uxTaskPriorityGet(xTask1Handle);
printf("优先级 =%d\r\n", task1_pri);
//第二个 调用一个句柄,设置优先级
vTaskPrioritySet(xTask1Handle,1);
//第三个 获取任务的数量
UBaseType_t nums = uxTaskGetNumberOfTasks();
printf(" =%d\r\n", nums);
// 第四个 获取当前任务的句柄
TaskHandle_t cur_task_handle = xTaskGetCurrentTaskHandle();
// 第五个 根据任务名称获取句柄
TaskHandle_t task_handle = xTaskGetHandle("task1");
重要的
//注意在使用这些函数时,有些需要在配置文件中进行配置使能
//获取任务状态
// 获取task1的状态
eTaskState task1_state = eTaskGetState(xTask1Handle);
printf("task1 =%d\r\n", task1_state);
// 获取task1的取小剩余空间
UBaseType_t task1_stack = uxTaskGetStackHighWaterMark(xTask1Handle);
printf("task1的最小剩余空间=%d\r\n",task1_stack);|
// 获取任务运行时间
// 统计运行时间的宏定义 比较复杂,需要看手册进行配置
//注意使用之前需要一个定时器,为0.01ms的定时器中断,需自己配置
#define configGENERATE_RUN_TIME_STATS 1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() HAL_TIM_Base_Start_IT(&htim6)
#define portGET RUN_TIME_COUNTER_VALUE() (tim_tick)
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
#define configSUPPORT_DYNAMIC_ALLOCATION 1
uint8_t runTimeBuff[256] ;
vTaskGetRunTimeStats((char *)runTimeBuff);
printf("%s", runTimeBuff);
下图为获取运行时间配置选项:
//以表格形式获取任务所有信息
// 获取任务表格信息
uint8_t runTimeBuff[256] ;
vTaskList((char *)runTimeBuff);
printf("\n%s", runTimeBuff);
//获取指定单个任务的信息
// 获取task1Info
TaskStatus_t task1Info;
// 句柄 接收信息的结构体 是否获取最小内存空间 需要获取任务的状态
vTaskGetInfo(xTask1Handle,&task1Info,pdTRUE,eInvalid);
printf("task1的优先级=%d,最小剩余内存空间:%d,状态为:%d\r\n",task1Info.
uxCurrentPriority, task1Info. usStackHighWaterMark, task1Info.eCurrentState);
下图vTaskGetInfo的后两个参数
TaskStatus_t pxTaskStatusArray[6]; //数组
uint32_t ulTotalRunTime; //时间
// 获取多个任务的信息
uxTaskGetSystemState(pxTaskStatusArray, uxTaskGetNumberOfTasks(),&
ulTotalRunTime);
printf(“总运行时间=%d\r\n",ulTotalRunTime);
for (int i = 0; i < uxTaskGetNumberOfTasks(); i++)
{
printf("task%d的优先级=%d,最小剩余内存空间:%d,状态为:%d\r\n",i,
pxTaskStatusArray[i].uxCurrentPriority, pxTaskStatusArray[i].
usStackHighWaterMark, pxTaskStatusArray[i].eCurrentState);
}
延时函数的选择
vTaskDelay(); //使用时不能做到精准时间,例如整点秒杀,八点,九点准时做什么,但是程序运行是有时间波动的,导致延时有误差,虽然前面几次误差可忽略,但是他会累计直到误差很大
xTaskDelayUntil();//绝对时间,非常精准
//参数 标准值 周期
实例
void task1(void *pvParameters)
{
while (1)
{
// 周期是550ms
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
// 延时50
delay_ms(50);
// 延时500
vTaskDelay(500);
}
}
void task2(void *pvParameters)
{
// 初始值 还没有+50ms
TickType_t tick_count =xTaskGetTickCount();
while (1)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
// 延时50
delay_ms(50);
// 初始值 +500 作为下次唤醒的时间 实际阻塞450ms
vTaskDelayUntil(&tick_count,500);
}
}
*/
/*
场景1 唤醒芯片需要按下3s =>(按下)vTaskDelay(3000);
场景2 3s周期检查任务状态 重新上电 => vTaskDelayUntil(起始时间,3000)
*/
消息队列
//是rtos任务通信的主要方式,通常作为缓冲区使用
/*
特点:
先进先出
线程安全(读写操作均使用临界区)
支持阻塞操作(任务可以在读取空队列或写入满队列时进入阻塞状态(不会占用CPU资源)等待,直到有数据
可读或队列有空问。)
*/
API
//创建任务队列
//动态创建
/*
xQueueCreate();
参数. uxQueueLength
队列一次可存储的最大项目数。
· uxltemSize
存储队列中每个项目所需的大小(以字节为单位)。
项目通过复制而非引用的方式入队,因此该参数值是每个入队项目将复制的字节数。
队列中的每个项目必须具有相同的大小。
*/
xQueueCreateStatic();// 静态创建 一般不使用,如果要用去官网查看
/*
#### 创建队列
- `xQueueCreate():动态方式创建队列
- `xQueueCreateStatic():静态方式创建队列
#### 写入操作
- `xQueueSend():同`xQueueSendToBack()
- `xQueueSendToBack():往队列的尾部写入消息
- `xQueueSendToFront():往队列的头部写入消息
- `xQueueOverwrite():覆写队列消息(只用于队列长度为1的情况)
- `xQueueSendFromISR():同`xQueueSendToBackFromISR()`
- `xQueueSendToBackFromISR():在中断中往队列的尾部写入消息
- `xQueueSendToFrontFromISR():在中断中往队列的头部写入消息
- `xQueueOverwriteFromISR():在中断中覆写队列消息(只用于队列长度为1的情况)
#### 读取操作
- `xQueueReceive():从队列头部读取消息,并删除消息
- `xQueuePeek():从队列头部读取消息
- `xQueueReceiveFromISR()
- `xQueuePeekFromISR()
*/
代码实例
下面为不使用队列时进行发送数据的读写操作,会极度占用cpu资源:
// 发送数据任务
void sendMsg(void *pvParameters);
#define Send_TASK_STACK_SIZE 128
#define Send_TASK_PRIORITY 3
TaskHandle_t Send_Task_handle;
// 接收数据任务
void recvMsg(void *pvParameters);
#define Recv_TASK_STACK_SIZE 128
#define Recv_TASK_PRIORITY 3
TaskHandle_t Recv_Task_handle;
// 查看任务运行时间
void timeTask(void *pvParameters);
#define Time_TASK_STACK_SIZE 128
#define Time_TASK_PRIORITY 3
TaskHandle_t Time_Task_handle;
uint8_t msg_buff[1];
uint8_t len;
void FreeRTOS_start(void)
{
xTaskCreate(sendMsg, "sendMsg", Send_TASK_STACK_SIZE, NULL, Send_TASK_PRIORITY, &Send_Task_handle);
xTaskCreate(recvMsg, "recvMsg", Recv_TASK_STACK_SIZE, NULL, Recv_TASK_PRIORITY, &Recv_Task_handle);
xTaskCreate(timeTask, "timeTask", Time_TASK_STACK_SIZE, NULL, Time_TASK_PRIORITY, &Time_Task_handle);
vTaskStartScheduler();
}
void sendMsg(void *pvParameters)
{
while (1)
{
taskENTER_CRITICAL();// 进入临界区
if (len == 0)
{
// 写入数据
msg_buff[0] = 'a';
len = 1;
}
taskEXIT_CRITICAL();//突出临界区
}
}
void recvMsg(void *pvParameters)
{
while (1)
{
// 1. 使用算法逻辑可以实现先进先出
// 2. 使用临界区可以实现线程安全
// 3. 不能实现阻塞效果 会一直占用 CPU
taskENTER_CRITICAL();// 进入临界区
// 读取数据
if (len > 0)
{
// 读取数据
printf("%c\n", msg_buff[0]);
len = 0;
}
taskEXIT_CRITICAL();//突出临界区
}
}
uint8_t taskTime[128];
void timeTask(void *pvParameters)
{
while (1)
{
vTaskGetRunTimeStats(taskTime);
printf("%s\n", taskTime);
vTaskDelay(2000);
}
}
下面为使用队列的代码:(基本类型)
// 发送数据任务
void sendMsg(void *pvParameters);
#define Send_TASK_STACK_SIZE 128
#define Send_TASK_PRIORITY 3
TaskHandle_t Send_Task_handle;
// 接收数据任务
void recvMsg(void *pvParameters);
#define Recv_TASK_STACK_SIZE 128
#define Recv_TASK_PRIORITY 3
TaskHandle_t Recv_Task_handle;
// 查看任务运行时间
void timeTask(void *pvParameters);
#define Time_TASK_STACK_SIZE 128
#define Time_TASK_PRIORITY 3
TaskHandle_t Time_Task_handle;
QueueHandle_t queue1;
void FreeRTOS_start(void)
{
queue = xQueueCreate(10,sizeof(uint8_t))
xTaskCreate(sendMsg, "sendMsg", Send_TASK_STACK_SIZE, NULL, Send_TASK_PRIORITY, &Send_Task_handle);
xTaskCreate(recvMsg, "recvMsg", Recv_TASK_STACK_SIZE, NULL, Recv_TASK_PRIORITY, &Recv_Task_handle);
xTaskCreate(timeTask, "timeTask", Time_TASK_STACK_SIZE, NULL, Time_TASK_PRIORITY, &Time_Task_handle);
vTaskStartScheduler();
}
void sendMsg(void *pvParameters)
{
while (1)
{
uint8_t item = 10;
//句柄 要发送数据的指针 等待时间
xQueueSend(queue1, &item, portMAX_DELAY);
vTaskDelay(1000);
}
}
void recvMsg(void *pvParameters)
{
while (1)
{
uint8_t item = 0;
BaseType_t res = xQueueReceive(queue1, &item, portMAX_DELAY);
if (res == pdTRUE)
{
printf("%d\n", item);
}
}
}
uint8_t taskTime[128];
void timeTask(void *pvParameters)
{
while (1)
{
//收集系统中所有任务的 CPU 占用率、运行时长等关键数据
vTaskGetRunTimeStats(taskTime);
printf("%s\n", taskTime);
vTaskDelay(2000);
}
}
结构体类型队列
void FreeRTOS_start(void)
{
//创建结构体队列
queue = xQueueCreate(10,sizeof(key_type));//开饭几位这个几位
xTaskCreate(sendMsg, "sendMsg", Send_TASK_STACK_SIZE, NULL, Send_TASK_PRIORITY, &Send_Task_handle);
xTaskCreate(recvMsg, "recvMsg", Recv_TASK_STACK_SIZE, NULL, Recv_TASK_PRIORITY, &Recv_Task_handle);
xTaskCreate(timeTask, "timeTask", Time_TASK_STACK_SIZE, NULL, Time_TASK_PRIORITY, &Time_Task_handle);
vTaskStartScheduler();
}
void sendMsg(void *pvParameters)
{
while (1)
{
// 队列存储结构体类型
Key_Type item = Int_Key_Scan();
if (item != KEY_NONE)
{
xQueueSend(queue1, &item, portMAX_DELAY);
}
vTaskDelay(10);
}
}
void recvMsg(void *pvParameters)
{
while (1)
{
// 结构体类型队列
Key_Type item ;
BaseType_t res = xQueueReceive(queue1, &item, portMAX_DELAY);
if (res == pdTRUE)
{
switch (item)
{
case KEY1:
printf("按键1被按下");
break;
case KEY2:
printf("按键2被按下");
break;
case KEY3:
printf("按键3被按下");
break;
case KEY4:
printf("按键4被按下");
break;
default:
break;
}
}
}
}
信号量
概念: 信号量可以看作是一个特殊的“计数器”,该“计数器”的数值可用于表示共享资源的数量。当多个任务需要共享某些资源时,信号量可以确保在同一时刻只有一定数量的任务能够访问该资源,防止资源冲突。
获取(Take)当任务访问共享资源时,它需要先执行一次信号量的“获取”操作。如果信号量的数值大于0,表示有资源可用,任务就可以继续执行,同时信号量的数值减1。如果信号量为0,表示资源已经被占用,任务就进入阻塞状态,等待直到信号量数值大于0时再继续执行。
释放(Give)当任务使用完共享资源后,它需要执行一次信号量的“释放”操作,使得信号量的数值加1,表示资源可用,其他阻塞等待的任务此时就可以获得资源并继续执行了。
分类
/*
二值信号量:
定义:最大值是 1,也就是只能为 0 或者 1 的信号量称为二值信号量。
用途:
任务同步:如果一个任务需要等待另一个任务完成后执行,或者需要等待某个外部事件发生之后执行,那么就可以使用二值信号量作为通知信号。
任务互斥:可用于确保同一时刻只有一个任务访问共享资源,防止资源冲突(基于二值信号量特性推导)。
*/
/*
计数型信号量:
定义:最大值可以为任意指定值的信号量,称为计数型信号量。
用途:
事件计数:用于统计某类事件发生的次数。
例如:1.统计按键被按下的次数、传感器触发的次数,或中断事件的发生次数。2.任务可通过 “获取信号量” 的操作,得知事件发生的次数,进而执行对应的逻辑(如事件发生 N 次后执行某段处理代码)。
资源管理:用于管理多个相同类型的共享资源(如多个串口、多个缓冲区、多个网络连接等)。
例如:1.若系统有 3 个可用串口,可将计数型信号量初始值设为 3。任务需使用串口时,“获取信号量”(信号量数值减 1);使用完毕后 “释放信号量”(数值加 1)。这样能确保最多 3 个任务同时使用串口,避免资源冲突,实现对多资源的高效管理。
*/
二值信号量
API
/*
- **创建信号量**
- `xSemaphoreCreateBinary()
- `xSemaphoreCreateBinaryStatic()
- **获取信号量**
- `xSemaphoreTake()
- `xSemaphoreTakeFromISR()
- **释放信号量**
- `xSemaphoreGive()
- `xSemaphoreGiveFromISR()
*/
示例代码(串口收发数据)
下面为不使用信号量时,但是会极度占cpu资源
// 接收数据任务
void recvMsg(void *pvParameters);
#define Recv_TASK_STACK_SIZE 128
#define Recv_TASK_PRIORITY 3
TaskHandle_t Recv_Task_handle;
// 查看任务运行时间
void timeTask(void *pvParameters);
#define Time_TASK_STACK_SIZE 128
#define Time_TASK_PRIORITY 3
TaskHandle_t Time_Task_handle;
uint8_t recvBuf[16];
uint16_t real_len ;
void FreeRTOS_start(void)
{
xTaskCreate(recvMsg, "recvMsg", Recv_TASK_STACK_SIZE, NULL, Recv_TASK_PRIORITY, &Recv_Task_handle);
// xTaskCreate(timeTask, "timeTask", Time_TASK_STACK_SIZE, NULL, Time_TASK_PRIORITY, &Time_Task_handle);
vTaskStartScheduler();
}
void recvMsg(void *pvParameters)
{
while (1)
{
// 串口接收数据 => 只能轮询等待
HAL_UARTEx_ReceiveToIdle(&huart1, recvBuf, 16, &real_len, 0xffff);
if (real_len > 0)
{
recvBuf[real_len] = '\0';
printf("recv:%s\r\n", recvBuf);
real_len = 0;
}
}
}
uint8_t taskTime[128];
void timeTask(void *pvParameters)
{
while (1)
{
//收集系统中所有任务的 CPU 占用率、运行时长等关键数据
vTaskGetRunTimeStats(taskTime);
printf("%s\n", taskTime);
vTaskDelay(2000);
}
下面为使用信号量的代码:
// 接收数据任务
void recvMsg(void *pvParameters);
#define Recv_TASK_STACK_SIZE 128
#define Recv_TASK_PRIORITY 3
TaskHandle_t Recv_Task_handle;
// 查看任务运行时间
void timeTask(void *pvParameters);
#define Time_TASK_STACK_SIZE 128
#define Time_TASK_PRIORITY 3
TaskHandle_t Time_Task_handle;
uint8_t recvBuf[16];
uint16_t real_len ;
QueueHandle_t sem;
void FreeRTOS_start(void)
{
sem = xSemaphoreCreateBinary();
xTaskCreate(recvMsg, "recvMsg", Recv_TASK_STACK_SIZE, NULL, Recv_TASK_PRIORITY, &Recv_Task_handle);
// xTaskCreate(timeTask, "timeTask", Time_TASK_STACK_SIZE, NULL, Time_TASK_PRIORITY, &Time_Task_handle);
vTaskStartScheduler();
}
void recvMsg(void *pvParameters)
{
//开启中断
HAL_UARTEx_ReceiveToIdle_IT(&huart1, recvBuf, 16);
//HAL 库中串口的 “中断模式空闲接收” 函数,核心作用是以 “中断驱动” 的非阻塞方式,接收串口数据
while (1)
{
// 不要使用轮询 使用获取信号量来管理硬件资源
//收到数据的时候才释放信号量
BaseType_t res = xSemaphoreTake(sem, portMAX_DELAY);
if (res == pdTRUE)
{
// 接收到数据
printf("recv:%s\r\n", recvBuf);
memset(recvBuf, 0, 16);
}
}
HAL_UARTEx_ReceiveToIdle_IT(&huart1, recvBuf, 16);
}
uint8_t taskTime[128];
void timeTask(void *pvParameters)
{
while (1)
{
//收集系统中所有任务的 CPU 占用率、运行时长等关键数据
vTaskGetRunTimeStats(taskTime);
printf("%s\n", taskTime);
vTaskDelay(2000);
}
//中断处理函数 收到数据产生中断释放信号量
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1) // 判断是否为USART1的串口事件
{
BaseType_t pxHigherPriorityTaskWoken = pdFALSE; // 高优先级任务唤醒标志,初始为pdFALSE
// 中断中释放FreeRTOS二值信号量,pxHigherPriorityTaskWoken会被FreeRTOS自动更新
xSemaphoreGiveFromISR(sem, &pxHigherPriorityTaskWoken);
// 若有高优先级任务被唤醒,立即触发上下文切换,保证高优先级任务实时执行
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); 是 FreeRTOS 专门用于 中断处理函数(ISR)的 “任务切换触发函数”,核心作用是:中断处理完成后,根据传入的标志,决定是否立即切换到被唤醒的高优先级任务,保证系统实时性。
//不使用hal库时的stm32代码
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
uint8_t recvBuf[16] = {0}; // 串口接收缓存
QueueHandle_t sem; // FreeRTOS二值信号量
void usart_init()
{
GPIO_InitTypeDef init;
USART_InitTypeDef uInit;
nvic_init(); // 中断优先级配置
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// 2. GPIO配置(PA9-TX复用推挽,PA10-RX浮空输入)
init.GPIO_Pin = GPIO_Pin_9;
init.GPIO_Speed = GPIO_Speed_50MHz;
init.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &init);
init.GPIO_Pin = GPIO_Pin_10;
init.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &init);
// 3. 串口参数配置(波特率115200、8N1)
uInit.USART_BaudRate = 115200;
uInit.USART_WordLength = USART_WordLength_8b;
uInit.USART_StopBits = USART_StopBits_1;
uInit.USART_Parity = USART_Parity_No;
uInit.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
uInit.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &uInit);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收中断
USART_Cmd(USART1, ENABLE); // 使能串口
}
void nvic_init()
{
NVIC_InitTypeDef init;
init.NVIC_IRQChannel = USART1_IRQn;
init.NVIC_IRQChannelPreemptionPriority = 0;
init.NVIC_IRQChannelSubPriority = 1;
init.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&init);
}
void USART1_IRQHandler()
{
static int index = 0;
BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 高优先级任务唤醒标志
if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET){
recvBuf[index] = USART_ReceiveData(USART1); // 读取串口数据
// 检测换行符(自定义接收结束标志)
if(recvBuf[index] == '\n'){
recvBuf[index] = '\0'; // 字符串结尾
// 中断中释放信号量(必须用中断安全版本)
xSemaphoreGiveFromISR(sem, &xHigherPriorityTaskWoken);
index = 0; // 重置接收索引
} else {
index++;
if(index >= 16) index = 0; // 防止缓存溢出
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清除中断标志
}
// 若有高优先级任务被唤醒,立即切换任务
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
int main(void)
{
// 系统基础初始化(时钟、SysTick等)
usart_init(); // 初始化串口(标准外设库)
// 创建二值信号量
sem = xSemaphoreCreateBinary();
// 创建接收任务
xTaskCreate(recvMsg, "recvMsg", 128, NULL, 3, NULL);
vTaskStartScheduler(); // 启动FreeRTOS调度
while(1); // 调度器启动后不会执行到此处
}
void recvMsg(void *pvParameters)
{
while (1)
{
// 获取抢占信号量(阻塞等待串口中断释放信号量)
BaseType_t res = xSemaphoreTake(sem, portMAX_DELAY);
if (res == pdTRUE)
{
printf("recv:%s\r\n", recvBuf); // 打印接收数据
memset(recvBuf, 0, 16); // 清空缓存
}
}
}
示例代码(多个串口打印时任务争抢)
不加的话,当两个同优先级的串口打印任务出现时,会导致打印出现混乱,一人打印一截
注意:使用临界区虽然也可以保证程序运行完整,但是如果在运行时出现更高优先级的任务时,无法执行任务
// 查看任务运行时间
void timeTask(void *pvParameters);
#define Time_TASK_STACK_SIZE 128
#define Time_TASK_PRIORITY 3
TaskHandle_t Time_Task_handle;
// 串口打印任务1
void printfTask1(void *pvParameters);
#define PRINTF1_TASK_STACK_SIZE 128
#define PRINTF1_TASK_PRIORITY 3
TaskHandle_t printf1_Task_handle;
// 串口打印任务2
void printfTask2(void *pvParameters);
#define PRINTF2_TASK_STACK_SIZE 128
#define PRINTF2_TASK_PRIORITY 3
TaskHandle_t printf2_Task_handle;
QueueHandle_t sem;
void FreeRTOS_start(void)
{
// 使用二值信号量管理单一资源
sem = xSemaphoreCreateBinary();
// 二值信号量的初始值默认是0 => 管理资源的时候需要先释放一次
xSemaphoreGive(sem);
xTaskCreate(printfTask1, "printfTask1", PRINTF1_TASK_STACK_SIZE, NULL, PRINTF1_TASK_PRIORITY, &printf1_Task_handle);
xTaskCreate(printfTask2, "printfTask2", PRINTF2_TASK_STACK_SIZE, NULL, PRINTF2_TASK_PRIORITY, &printf2_Task_handle);
// xTaskCreate(timeTask, "timeTask", Time_TASK_STACK_SIZE, NULL, Time_TASK_PRIORITY, &Time_Task_handle);
vTaskStartScheduler();
}
void printfTask1(void *pvParameters)
{
while (1)
{
// 打印资源获取信号量 => 排斥别的串口使用的任务
xSemaphoreTake(sem, portMAX_DELAY);
printf("hello world===============\n");
// 打印完成之后释放信号量 => 让别的任务再来使用
xSemaphoreGive(sem);
vTaskDelay(1000);
}
}
void printfTask2(void *pvParameters)
{
while (1)
{
// 打印资源获取信号量 => 排斥别的串口使用的任务 抢占资源
xSemaphoreTake(sem, portMAX_DELAY);
printf("hello world+++++++++++++++\n");
// 打印完成之后释放信号量 => 让别的任务再来使用
xSemaphoreGive(sem);
vTaskDelay(1000);
}
}
int fputc(int ch, FILE *f)
{
/* 1. 等待发送寄存器为空 */
while ((USART1->SR & USART_SR_TXE) == 0);
/* 2. 数据写出到数据寄存器 */
USART1->DR = ch;
return ch;
}
/* USER CODE END 1 */
uint8_t taskTime[128];
void timeTask(void *pvParameters)
{
while (1)
{
//收集系统中所有任务的 CPU 占用率、运行时长等关键数据
vTaskGetRunTimeStats(taskTime);
printf("%s\n", taskTime);
vTaskDelay(2000);
}
互斥信号量
/*互斥信号量
概述:互斥信号量是一种特殊的二值信号量,与普通的二值信号量相比,其具有优先级继承机制。
优先级继承机制
概述:优先级继承是用来缓解互斥任务可能会出现优先级反转问题的一种机制。
优先级翻转:实时操作系统中多任务环境下的一种异常调度现象:高优先级任务因等待低优先级任务持有的互斥资源,反而被中等优先级任务抢占,导致高优先级任务的执行被延迟
代码示例(不使用互斥时)
// 高优先级任务
void High_task(void *pvParameters);
#define HIGH_TASK_PRIORITY 4
#define HIGH_TASK_STACK_SIZE 128
TaskHandle_t High_Task_handle;
// 中优先级任务
void Middle_task(void *pvParameters);
#define MIDDLE_TASK_PRIORITY 3
#define MIDDLE_TASK_STACK_SIZE 128
TaskHandle_t Middle_Task_handle;
// 低优先级任务
void Low_task(void *pvParameters);
#define LOW_TASK_PRIORITY 2
#define LOW_TASK_STACK_SIZE 128
TaskHandle_t Low_Task_handle;
QueueHandle_t sem;
void FreeRTOS_Start(void)
{
// 使用二值信号量管理单一资源
sem = xSemaphoreCreateBinary();
// 二值信号量的初始值默认是0 => 管理资源的时候需要先释放一次
xSemaphoreGive(sem);
xTaskCreate(High_task, "High_task", HIGH_TASK_STACK_SIZE, NULL, HIGH_TASK_PRIORITY, &High_Task_handle);
xTaskCreate(Middle_task, "Middle_task", MIDDLE_TASK_STACK_SIZE, NULL, MIDDLE_TASK_PRIORITY, &Middle_Task_handle);
xTaskCreate(Low_task, "Low_task", LOW_TASK_STACK_SIZE, NULL, LOW_TASK_PRIORITY, &Low_Task_handle);
// 如果配置了运行时间统计 => 会在下面启动调度器的时候 启动portCONFIGURE_TIMER_FOR_RUN_TIME_STA...
vTaskStartScheduler();
}
void High_task(void *pvParameters)
{
vTaskDelay(50);//阻塞一下让低优先级先用
xSemaphoreTake(sem, portMAX_DELAY);
for (uint8_t i = 0; i < 10; i++)
{
printf("High_task\n");
}
// 释放信号量
xSemaphoreGive(sem);
vTaskDelete(NULL);
}
// 中优先级任务
void Middle_task(void *pvParameters)
{
vTaskDelay(25);
// 抢占低优先级任务
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
delay_ms(500);
vTaskDelete(NULL);
}
// 低优先级任务
void Low_task(void *pvParameters)
{
// 获取信号量,独占共享资源
xSemaphoreTake(sem, portMAX_DELAY);
for (uint8_t i = 0; i < 10; i++)
{
printf("Low_task 执行中...\n");
}
// 释放信号量
xSemaphoreGive(sem);
vTaskDelete(NULL);
}
/**
* 高优先级的任务因为信号量需要等待低优先级的任务执行
* 过程当中有一个中等优先级的打断了低优先级的任务
* 不合理的情况:中优先级的任务执行很久才能释放任务 最后才能执行高优先级的任务
*/
使用互斥信号量代码示例
- API
- 创建信号量
- xSemaphoreCreateMutex()
- xSemaphoreCreateMutexStatic()
- 获取信号量
- xSemaphoreTake()
- 释放信号量
- xSemaphoreGive()
// 就修改一下信号量就可以了 相当于低优先级的任务会临时获得高优先级的优先级等级(揣着鸡毛当令箭)
void FreeRTOS_Start(void)
{
// 使用二值信号量管理单一资源
// sem = xSemaphoreCreateBinary();
sem = xSemaphoreCreateMutex(); //就修改一下信号量就可以了 相当于
// 二值信号量的初始值默认是0 => 管理资源的时候需要先释放一次
xSemaphoreGive(sem);
xTaskCreate(High_task, "High_task", HIGH_TASK_STACK_SIZE, NULL, HIGH_TASK_PRIORITY, &High_Task_handle);
xTaskCreate(Middle_task, "Middle_task", MIDDLE_TASK_STACK_SIZE, NULL, MIDDLE_TASK_PRIORITY, &Middle_Task_handle);
xTaskCreate(Low_task, "Low_task", LOW_TASK_STACK_SIZE, NULL, LOW_TASK_PRIORITY, &Low_Task_handle);
// 如果配置了运行时间统计 => 会在下面启动调度器的时候 启动portCONFIGURE_TIMER_FOR_RUN_TIME_STA...
vTaskStartScheduler();
}
计数型信号量
API
- 计数型信号量
- API
- 创建信号量
- xSemaphoreCreateCounting()
- xSemaphoreCreateCountingStatic()
- 获取信号量
- xSemaphoreTake()
- xSemaphoreTakeFromISR()
- 释放信号量
- xSemaphoreGive()
- xSemaphoreGiveFromISR()
- 获取信号量数值
- uxSemaphoreGetCount()
代码
void KEY_task(void *pvParameters);
#define KEY_TASK_PRIORITY 4
#define KEY_TASK_STACK_SIZE 128
TaskHandle_t KEY_Task_handle;
void LED_task(void *pvParameters);
#define LED_TASK_PRIORITY 3
#define LED_TASK_STACK_SIZE 128
TaskHandle_t LED_Task_handle
void FreeRTOS_Start(void)
{
sem =xSemaphoreCreateCounting(3,0); //计数信号量 参数 最大值 初始值
xTaskCreate(KEY_task, "KEY_task", KEY_TASK_STACK_SIZE, NULL, KEY_TASK_PRIORITY, &KEY_Task_handle);
xTaskCreate(LED_task, "LED_task", LED_TASK_STACK_SIZE, NULL, LED_TASK_PRIORITY, &LED_Task_handle);
// 如果配置了运行时间统计 => 会在下面启动调度器的时候 启动portCONFIGURE_TIMER_FOR_RUN_TIME_STA...
vTaskStartScheduler();
}
void LED_task(void *pvParameters)
{
while (1)
{
//获取信号量的值
uint32_t cnt = uxSemaphoreGetCount(sem);
if (cnt == 3)
{
// 灯亮
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
delay_ms(500);
// 灯灭
LED_OFF(LED1);
delay_ms(500);
// 释放信号量
for (uint8_t i = 0; i < COUNT_MAX; i++)
{
xSemaphoreGive(sem);
}
}
}
}
void KEY_task(void *pvParameters)
{
while (1)
{
Key_Type key = Int_Key_Scan();
if (key == KEY1)
{
// 获取信号量 计数值会累加,但当计数值达到创建时指定的最大值后,后续的释放操作将不再改变计数值
xSemaphoreTask(sem,portMAX_DELAY);
}
}
}
队列集
概念:队列的集合,队列集允许一个任务同时等待多个多个队列的消息,适用于一个任务需要接收多个来源的数据的场景。
API 函数 功能描述 典型应用场景
xQueueCreateSet()-创建队列集-系统初始化时,创建可管理多个队列的集合,例如需要同时监听按键、串口队列的场景。
xQueueAddToSet()-将队列添加到队列集中-把 “按键队列”“串口队列” 加入队列集,使任务能同时监听这两个队列的消息。
xQueueRemoveFromSet() 从队列集中移除队列 当某队列不再需要被监听时(如串口功能临时关闭),将其从队列集中移除。
xQueueSelectFromSet() 获取队列集中有有效消息的队列 任务中调用该函数,阻塞等待队列集中任意队列有消息,从而高效处理多源数据。
xQueueSelectFromSetFromISR() 中断中获取队列集中有有效消息的队列 串口中断、按键中断中,判断队列集内哪个队列可操作,保证中断处理的实时性。
不使用队列集
// 按键队列写入数据
void key_queue_write(void *pvParameter);
#define KEY_QUEUE_WRITE_PRIORITY 3
#define KEY_QUEUE_WRITE_STACK_SIZE 128
TaskHandle_t key_queue_write_task_handle;
// 按键队列读取数据
void key_queue_read(void *pvParameter);
#define KEY_QUEUE_READ_PRIORITY 3
#define KEY_QUEUE_READ_STACK_SIZE 128
TaskHandle_t key_queue_read_task_handle;
// 串口队列写入数据
void uart_queue_write(void *pvParameter);
#define UART_QUEUE_WRITE_PRIORITY 3
#define UART_QUEUE_WRITE_STACK_SIZE 128
TaskHandle_t uart_queue_write_task_handle;
// 串口队列读取数据
void uart_queue_read(void *pvParameter);
#define UART_QUEUE_READ_PRIORITY 3
#define UART_QUEUE_READ_STACK_SIZE 128
TaskHandle_t uart_queue_read_task_handle;
QueueHandle_t key_queue;
QueueHandle_t UART_queue;
// 串口接收数据的缓存
uint8_t uart_queue_data[128];
uint8_t uart_queue_data_p = uart_queue_data;
QueueHandle_t uart_sem ;
void FreeRTOS_Start(void)
{
// 1.1 创建队列
key_queue = xQueueCreate(3, sizeof(Key_Type));
// 1.2 创建串口队列
UART_queue = xQueueCreate(3, sizeof(char *));
// 1.3 创建信号量
uart_sem = xSemaphoreCreateBinary();
// 2. 创建任务
xTaskCreate(key_queue_write, "key_queue_write", KEY_QUEUE_WRITE_STACK_SIZE, NULL, KEY_QUEUE_WRITE_PRIORITY, &key_queue_write_task_handle);
xTaskCreate(key_queue_read, "key_queue_read", KEY_QUEUE_READ_STACK_SIZE, NULL, KEY_QUEUE_READ_PRIORITY, &key_queue_read_task_handle);
// 创建串口队列的读写任务
xTaskCreate(uart_queue_write, "uart_queue_write", UART_QUEUE_WRITE_STACK_SIZE, NULL, UART_QUEUE_WRITE_PRIORITY, &uart_queue_write_task_handle);
xTaskCreate(uart_queue_read, "uart_queue_read", UART_QUEUE_READ_STACK_SIZE, NULL, UART_QUEUE_READ_PRIORITY, &uart_queue_read_task_handle);
// 如果配置了运行时间统计 => 会在下面启动调度器的时候 启动portCONFIGURE_TIMER_FOR_RUN_TIME_STAT...
vTaskStartScheduler();
}
}
void key_queue_write(void *pvParameter)
{
// 扫描外部的按键按下 => 将按下的对应按键的值写入到QUEUE
while (1)
{
Key_Type key = Int_Key_Scan();
if (key != KEY_NONE)
{
// 将按键的值写入到队列
xQueueSend(key_queue, &key, portMAX_DELAY);
}
delay_ms(10);
}
}
void key_queue_read(void *pvParameter)
{
// 读取按键队列中的数据
// KEY1 => 翻转LED1
// KEY2 => 翻转LED2
// KEY3 => 翻转LED3
// KEY4 => 翻转所有LED
while (1)
{
Key_Type key;
// 读取队列 阻塞等待有数据
xQueueReceive(key_queue, &key, portMAX_DELAY);
// 根据按键类型执行LED控制逻辑
switch (key) {
case KEY1:
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
break;
case KEY2:
Int_LED_Toggle(LED2_GPIO_Port, LED2_Pin);
break;
case KEY3:
Int_LED_Toggle(LED3_GPIO_Port, LED3_Pin);
break;
case KEY4:
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
Int_LED_Toggle(LED2_GPIO_Port, LED2_Pin);
Int_LED_Toggle(LED3_GPIO_Port, LED3_Pin);
break;
default:
break;
}
}
}
// 串口队列写任务:负责STM32串口接收电脑数据,并将数据写入队列
// 功能:接收电脑发送的串口数据,通过信号量+队列机制实现可靠的数据传递
void uart_queue_write(void *pvParameter)
{
// 步骤1:启动串口接收中断(使用HAL库的空闲中断接收模式,支持不定长数据接收)
HAL_UARTEx_ReceiveToIdle_IT(&huart1, uart_queue_data, sizeof(uart_queue_data));
while (1)
{
// 步骤2:获取二值信号量,阻塞等待串口中断通知(数据接收完成)
xSemaphoreTake(uart_sem, portMAX_DELAY);
// 步骤3:将完整数据写入队列(UART_queue),实现任务间数据传递
xQueueSend(UART_queue, &uart_queue_data_p, portMAX_DELAY); // 注意地址问题
// 步骤4:重新启动串口接收中断,持续监听下一次数据
HAL_UARTEx_ReceiveToIdle_IT(&huart1, uart_queue_data, sizeof(uart_queue_data));
}
}
// 串口中断回调函数:HAL库串口空闲中断触发后,执行该函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
// 标记:用于判断是否需要触发任务调度
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 步骤1:从中断中释放信号量,通知uart_queue_write任务“数据已接收”
xSemaphoreGiveFromISR(uart_sem, &xHigherPriorityTaskWoken);
// 步骤2:若释放信号量后唤醒了更高优先级的任务,立即触发任务调度
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 串口队列读任务:从队列读取数据,并执行串口回传或指令解析
void uart_queue_read(void *pvParameter)
{
// 读取串口队列的数据 => 设计功能
// LED1 => 翻转LED1
// LED2 => 翻转LED2
// LED3 => 翻转LED3
// ALL => 翻转所有LED
while (1)
{
char *data;
// 阻塞读取UART队列
xQueueReceive(UART_queue, &data, portMAX_DELAY);
if (strstr(data, "LED1") != NULL)
{
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
}
else if (strstr(data, "LED2") != NULL)
{
Int_LED_Toggle(LED2_GPIO_Port, LED2_Pin);
}
else if (strstr(data, "LED3") != NULL)
{
Int_LED_Toggle(LED3_GPIO_Port, LED3_Pin);
}
else if (strstr(data, "ALL") != NULL)
{
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
Int_LED_Toggle(LED2_GPIO_Port, LED2_Pin);
Int_LED_Toggle(LED3_GPIO_Port, LED3_Pin);
}
}
}
使用队列集的代码
// 按键队列写入数据
void key_queue_write(void *pvParameter);
#define KEY_QUEUE_WRITE_PRIORITY 3
#define KEY_QUEUE_WRITE_STACK_SIZE 128
TaskHandle_t key_queue_write_task_handle;
// 按键队列读取数据
void key_queue_read(void *pvParameter);
#define KEY_QUEUE_READ_PRIORITY 3
#define KEY_QUEUE_READ_STACK_SIZE 128
TaskHandle_t key_queue_read_task_handle;
// 串口队列写入数据
void uart_queue_write(void *pvParameter);
#define UART_QUEUE_WRITE_PRIORITY 3
#define UART_QUEUE_WRITE_STACK_SIZE 128
TaskHandle_t uart_queue_write_task_handle;
// 串口队列读取数据
void uart_queue_read(void *pvParameter);
#define UART_QUEUE_READ_PRIORITY 3
#define UART_QUEUE_READ_STACK_SIZE 128
TaskHandle_t uart_queue_read_task_handle;
QueueHandle_t key_queue;
QueueHandle_t UART_queue;
// 串口接收数据的缓存
uint8_t uart_queue_data[128];
uint8_t uart_queue_data_p = uart_queue_data;
QueueHandle_t uart_sem ;
// 队列集 只有读数据的api获取,没有写数据的
QueueSetHandle_t queue_set;
void FreeRTOS_Start(void)
{
// 1.1 创建队列
key_queue = xQueueCreate(3, sizeof(Key_Type));
// 1.2 创建串口队列
UART_queue = xQueueCreate(3, sizeof(char *));
// 1.3 创建信号量
uart_sem = xSemaphoreCreateBinary();
// 1.4 创建队列集管理多个队列
queue_set = xQueueCreateSet(2);
// 1.5 添加队列到队列集中
xQueueAddToSet(key_queue, queue_set);
xQueueAddToSet(UART_queue, queue_set);
// 2. 创建任务
xTaskCreate(key_queue_write, "key_queue_write", KEY_QUEUE_WRITE_STACK_SIZE, NULL, KEY_QUEUE_WRITE_PRIORITY, &key_queue_write_task_handle);
// 创建串口队列的读写任务
xTaskCreate(uart_queue_write, "uart_queue_write", UART_QUEUE_WRITE_STACK_SIZE, NULL, UART_QUEUE_WRITE_PRIORITY, &uart_queue_write_task_handle);
// 创建队列集任务
xTaskCreate(queue_set_task, "queue_set_task", QUEUE_SET_STACK_SIZE, NULL, QUEUE_SET_PRIORITY, &queue_set_task_handle);
// 如果配置了运行时间统计 => 会在下面启动调度器的时候 启动portCONFIGURE_TIMER_FOR_RUN_TIME_STAT...
vTaskStartScheduler();
}
}
void key_queue_write(void *pvParameter)
{
// 扫描外部的按键按下 => 将按下的对应按键的值写入到QUEUE
while (1)
{
Key_Type key = Int_Key_Scan();
if (key != KEY_NONE)
{
// 将按键的值写入到队列
xQueueSend(key_queue, &key, portMAX_DELAY);
}
delay_ms(10);
}
}
// 串口队列写任务:负责STM32串口接收电脑数据,并将数据写入队列
// 功能:接收电脑发送的串口数据,通过信号量+队列机制实现可靠的数据传递
void uart_queue_write(void *pvParameter)
{
// 步骤1:启动串口接收中断(使用HAL库的空闲中断接收模式,支持不定长数据接收)
HAL_UARTEx_ReceiveToIdle_IT(&huart1, uart_queue_data, sizeof(uart_queue_data));
while (1)
{
// 步骤2:获取二值信号量,阻塞等待串口中断通知(数据接收完成)
xSemaphoreTake(uart_sem, portMAX_DELAY);
// 步骤3:将完整数据写入队列(UART_queue),实现任务间数据传递
xQueueSend(UART_queue, &uart_queue_data_p, portMAX_DELAY); // 注意地址问题
// 步骤4:重新启动串口接收中断,持续监听下一次数据
HAL_UARTEx_ReceiveToIdle_IT(&huart1, uart_queue_data, sizeof(uart_queue_data));
}
}
// 串口中断回调函数:HAL库串口空闲中断触发后,执行该函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
// 标记:用于判断是否需要触发任务调度
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 步骤1:从中断中释放信号量,通知uart_queue_write任务“数据已接收”
xSemaphoreGiveFromISR(uart_sem, &xHigherPriorityTaskWoken);
// 步骤2:若释放信号量后唤醒了更高优先级的任务,立即触发任务调度
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void queue_set_task(void *pvParameter)
{
// 一个管理队列集的任务 可以代替多个单独读取队列的任务
while (1)
{
// 返回有消息的队列的句柄 => 代替多个队列的读取等待 => 返回值表示队列的句柄
QueueSetMemberHandle_t queue_handle = xQueueSelectFromSet(queue_set, portMAX_DELAY);
if (queue_handle == key_queue)
{
// 说明key_queue队列有数据
Key_Type key;
xQueueReceive(key_queue, &key, portMAX_DELAY);
// 执行按键数据处理逻辑(如之前的LED控制)
switch (key) {
case KEY1:
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
break;
// 其他按键逻辑...
default:
break;
}
}
else if (queue_handle == UART_queue)
{
// 说明串口队列有数据
char *data;
xQueueReceive(UART_queue, &data, portMAX_DELAY);
// 执行串口数据处理逻辑(如之前的LED控制或指令解析)
if (strstr(data, "LED1") != NULL) {
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
}
// 其他串口指令逻辑...
}
}
}
/*不同点: 用一个任务代替多个读任务,根据返回句柄的不同,来执行不同的代码 */
事件标志组
事件标志组是一种灵活的多任务同步机制,与信号量相比具有以下优势:
- 支持多个事件的组合:信号量仅允许任务以阻塞状态等待单个事件;事件标志组允许任务以阻塞状态等待多个事件的组合,只有当多个事件都发生后(条件),任务才会解锁。
- 支持解锁多个任务:信号量只能解锁一个任务;事件标志组可以在事件发生时,解锁多个任务。
API
| 分类 | API 函数 | 功能描述 | 典型应用场景 |
|---|---|---|---|
| 创建类 | xEventGroupCreate() |
动态创建事件标志组 | 系统运行时按需创建事件标志组,适用于资源灵活分配的场景。 |
xEventGroupCreateStatic() |
静态创建事件标志组 | 系统初始化阶段创建,需提前分配内存,适用于资源确定性要求高的场景。 | |
| 设置类 | xEventGroupSetBits() |
设置事件标志位 | 任务中触发事件,例如传感器数据采集完成后设置标志位,通知等待任务。 |
xEventGroupSetBitsFromISR() |
中断中设置事件标志位 | 串口中断、定时器中断中触发事件,保证中断处理的实时性。 | |
| 等待类 | xEventGroupWaitBits() |
等待事件标志位 | 任务中阻塞等待多个事件组合,例如需同时满足 “按键按下” 和 “串口指令到达” 才执行逻辑。 |
| 清零类 | xEventGroupClearBits() |
清零事件标志位 | 任务处理完事件后,清除标志位,避免重复触发。 |
xEventGroupClearBitsFromISR() |
中断中清零事件标志位 | 中断处理完事件后,清除标志位,保证中断逻辑的完整性。 | |
| 同步类 | xEventGroupSync() |
设置事件标志位并等待事件 | 多任务同步场景,例如任务 A 设置标志位后,等待任务 B 也设置标志位,再共同执行逻辑。 |
| 参数 | configUSE_16_BIT_TICKS |
影响事件标志组位数 | 设置为 1 时,事件标志组长度为 8 位;设置为 0 时,长度为 24 位,需根据系统事件数量选择。 |
示例代码
// 按键任务
void Key_task(void *pvParameters);
#define KEY_TASK_PRIORITY 3
#define KEY_TASK_STACK_SIZE 128
TaskHandle_t Key_task_handle;
// LED任务
void Led_task(void *pvParameters);
#define LED_TASK_PRIORITY 3
#define LED_TASK_STACK_SIZE 128
TaskHandle_t Led_task_handle;
// 事件标志组
EventGroupHandle_t event_group;
void FreeRTOS_Start(void)
{
// 创建事件标志组
event_group = xEventGroupCreate();
// 创建任务
xTaskCreate(Key_task, "Key_task", KEY_TASK_STACK_SIZE, NULL, KEY_TASK_PRIORITY, &Key_task_handle);
xTaskCreate(Led_task, "Led_task", LED_TASK_STACK_SIZE, NULL, LED_TASK_PRIORITY, &Led_task_handle);
// 如果配置了运行时间统计 => 会在下面启动调度器的时候 启动
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); 启动定时器
vTaskStartScheduler();
}
void Key_task(void *pvParameters)
{
// 扫描按键 => 按下不同的按钮 将事件标志组中的对应位设置为1
while (1)
{
Key_Type key = Int_Key_Scan();
if (key == KEY1)
{
// 在事件标志组的第0位设置为1
xEventGroupSetBits(event_group, 1);
}
else if (key == KEY2)
{
// 在事件标志组的第1位设置为1
xEventGroupSetBits(event_group, 2);
}
else if (key == KEY3)
{
// 在事件标志组的第2位设置为1
xEventGroupSetBits(event_group, 4);
}
else if (key == KEY4)
{
// 在事件标志组的第3位设置为1
xEventGroupSetBits(event_group, 8);
}
}
}
void Led_task(void *pvParameters)
{
// 等待4个条件全部满足 => LED1闪烁一下
while (1)
{
// 事件标志组句柄 等待条件拼接成int值
xEventGroupWaitBits(event_group, 0x0f, pdFALSE, pdTRUE, portMAX_DELAY);
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
delay_ms(500);
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
delay_ms(500);
}
}
/*
EventBits_t xEventGroupWaitBits(
EventGroupHandle_t xEventGroup, // 事件标志组句柄
const EventBits_t uxBitsToWaitFor, // 要等待的事件标志位掩码
const BaseType_t xClearOnExit, // 事件标志位是否在退出时清零
const BaseType_t xWaitForAllBits, // 是否等待所有标志位(pdTRUE:逻辑与;pdFALSE:逻辑或)
TickType_t xTicksToWait // 阻塞等待的超时时间
);
*/
直接任务通知
概念
| 类别 | 特点描述 |
|---|---|
| 传统任务间通信 | 需借助队列、信号量、事件标志组等中间对象传递消息,流程相对繁琐。 |
| 直接任务通知 | 是一种轻量高效的任务间通信机制,发送方任务可直接将消息送至接收方任务,无需中间对象,简化了通信流程。 |
任务通知实现原理
| 维度 | 说明 |
|---|---|
| 通知数组结构 | 每个任务创建时自带一个通知数组,数组每个元素用于存储一个通知信息,单个元素占用 4 个字节(32 位),其中 1 位存储通知状态(二值信号),其余 31 位存储通知值(事件标志组)。 |
| 数组长度配置 | 数组长度可通过configTASK_NOTIFICATION_ARRAY_ENTRIES参数设置,默认值为 1。 |
任务通知的使用逻辑与特点
| 分类 | 说明 |
|---|---|
| 使用逻辑 | 通知数组中多个元素用于接收不同类型的通知,而非同一类型的多个通知。 |
| 特点 - 阻塞等待 | 允许接收方以阻塞状态等待任务通知,类似队列、信号量的同步机制。 |
| 特点 - 发送方非阻塞 | 不允许发送方以阻塞方式等待发送任务通知(接收方未处理前一通知时),用户可选择四种处理方式:- 直接覆盖原值,无论接收方是否读取;- 接收方读取原值则覆盖,否则丢弃当前通知;- 在值中设置一个或多个标志位;- 对值进行加操作。 |
FreeRTOS 任务通知 API 分类与功能
| 分类 | API 函数 | 功能描述 |
|---|---|---|
| 发送通知 | xTaskNotify() |
发送任务通知(带通知值) |
xTaskNotifyGive() |
发送通知(不带通知值) | |
xTaskNotifyAndQuery() |
发送通知并查询当前通知状态 | |
xTaskNotifyFromISR() |
中断中发送任务通知(带通知值) | |
vTaskNotifyGiveFromISR() |
中断中发送通知(不带通知值) | |
xTaskNotifyAndQueryFromISR() |
中断中发送通知并查询状态 | |
| 接收通知 | xTaskNotifyWait() |
等待并接收任务通知 |
ulTaskNotifyTake() |
获取任务通知(无通知值) | |
| 清除通知 | xTaskNotifyStateClear |
清除任务通知状态 |
ulTaskNotifyValueClear |
清除任务通知值 |
替换二值信号量
// 接收数据任务
void recvMsg(void *pvParameters);
#define Recv_TASK_STACK_SIZE 128
#define Recv_TASK_PRIORITY 3
TaskHandle_t Recv_Task_handle;
// 查看任务运行时间
void timeTask(void *pvParameters);
#define Time_TASK_STACK_SIZE 128
#define Time_TASK_PRIORITY 3
TaskHandle_t Time_Task_handle;
uint8_t recvBuf[16];
uint16_t real_len ;
void FreeRTOS_start(void)
{
xTaskCreate(recvMsg, "recvMsg", Recv_TASK_STACK_SIZE, NULL, Recv_TASK_PRIORITY, &Recv_Task_handle);
xTaskCreate(timeTask, "timeTask", Time_TASK_STACK_SIZE, NULL, Time_TASK_PRIORITY, &Time_Task_handle);
vTaskStartScheduler();
}
void recvMsg(void *pvParameters)
{
//开启中断
HAL_UARTEx_ReceiveToIdle_IT(&huart1, recvBuf, 16);
//HAL 库中串口的 “中断模式空闲接收” 函数,核心作用是以 “中断驱动” 的非阻塞方式,接收串口数据
while (1)
{
// 接收任务通知 替代信号量 进入阻塞状态(等待通知)。
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (res == pdTRUE)
{
// 接收到数据
printf("recv:%s\r\n", recvBuf);
memset(recvBuf, 0, 16);
}
}
HAL_UARTEx_ReceiveToIdle_IT(&huart1, recvBuf, 16);
}
uint8_t taskTime[128];
void timeTask(void *pvParameters)
{
while (1)
{
//收集系统中所有任务的 CPU 占用率、运行时长等关键数据
vTaskGetRunTimeStats(taskTime);
printf("%s\n", taskTime);
vTaskDelay(2000);
}
//中断处理函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1) // 判断是否为USART1的串口事件
{
BaseType_t pxHigherPriorityTaskWoken = pdFALSE; // 高优先级任务唤醒标志,初始为pdFALSE
// 使用任务间通知替代二值信号量
// 填写的参数是:接收通知的任务句柄
//找到 Recv_Task_handle 对应的任务,将其 “通知数组第 0 个元素” 的通知状态置为 “有通知”
vTaskNotifyGiveFromISR(Recv_Task_handle, pxHigherPriorityTaskWoken);
// 若有高优先级任务被唤醒,立即触发上下文切换,保证高优先级任务实时执行
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}
}
替换事件标志组
// 按键任务
void Key_task(void *pvParameters);
#define KEY_TASK_PRIORITY 3
#define KEY_TASK_STACK_SIZE 128
TaskHandle_t Key_task_handle;
// LED任务
void Led_task(void *pvParameters);
#define LED_TASK_PRIORITY 3
#define LED_TASK_STACK_SIZE 128
TaskHandle_t Led_task_handle;
// 事件标志组
EventGroupHandle_t event_group;
void FreeRTOS_Start(void)
{
// 创建事件标志组
event_group = xEventGroupCreate();
// 创建任务
xTaskCreate(Key_task, "Key_task", KEY_TASK_STACK_SIZE, NULL, KEY_TASK_PRIORITY, &Key_task_handle);
xTaskCreate(Led_task, "Led_task", LED_TASK_STACK_SIZE, NULL, LED_TASK_PRIORITY, &Led_task_handle);
// 如果配置了运行时间统计 => 会在下面启动调度器的时候 启动
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); 启动定时器
vTaskStartScheduler();
}
void Key_task(void *pvParameters)
{
// 扫描按键 => 按下不同的按钮
while (1)
{
Key_Type key = Int_Key_Scan();
if (key == KEY1)
{
xTaskNotify(Led_task_handle, 1 << 0, eSetBits); //变化
}
else if (key == KEY2)
{
xTaskNotify(Led_task_handle, 1 << 1, eSetBits);
}
else if (key == KEY3)
{
xTaskNotify(Led_task_handle, 1 << 2, eSetBits);
}
else if (key == KEY4)
{
xTaskNotify(Led_task_handle, 1 << 3, eSetBits);
}
}
}
void Led_task(void *pvParameters)
{
while (1)
{
uint32_t notify_value = 0;
xTaskNotifyWait(0x0, 0x0, ¬ify_value, portMAX_DELAY);
if (notify_value == 0x0f)
{
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
delay_ms(500);
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
delay_ms(500);
// 可以手动清除标志位
// xEventGroupClearBits(event_group, 0x0f);
// 只能触发之后手动清除
xTaskNotifyStateClear(NULL);
ulTaskNotifyValueClear(NULL, 0x0f);
}
}
}
/*
BaseType_t xTaskNotifyWait(
uint32_t ulBitsToClearOnEntry, // 进入函数时要清除的通知位掩码
uint32_t ulBitsToClearOnExit, // 退出函数时要清除的通知位掩码
uint32_t *pulNotificationValue, // 存储接收到的通知值
TickType_t xTicksToWait // 阻塞等待的超时时间
);
*/
软件定时器
软件定时器概述与 API 分类
| 类别 | 说明 |
|---|---|
| 概述 | 软件定时器可在指定时间后或按固定频率周期性执行回调函数,与硬件定时器无关,是 FreeRTOS 实现定时逻辑的轻量工具。 |
| 创建定时器 | - xTimerCreate():动态创建软件定时器;- xTimerCreateStatic():静态创建软件定时器(编译期分配内存)。 |
| 启动定时器 | - xTimerStart():任务上下文启动定时器;- xTimerStartFromISR():中断上下文启动定时器。 |
| 停止定时器 | - xTimerStop():任务上下文停止定时器;- xTimerStopFromISR():中断上下文停止定时器。 |
| 复位定时器 | - xTimerReset():任务上下文复位定时器(重新开始计时);- xTimerResetFromISR():中断上下文复位定时器。 |
| 修改定时器周期 | - xTimerChangePeriod():任务上下文修改定时器周期;- xTimerChangePeriodFromISR():中断上下文修改定时器周期。 |
软件定时器相关参数说明
| 参数名称 | 功能描述 |
|---|---|
configUSE_TIMERS |
用于启用软件定时器功能,配置为1启用,0禁用。 |
configTIMER_TASK_PRIORITY |
设置定时器任务的优先级,优先级越高,定时器回调函数的执行实时性越强。 |
configTIMER_QUEUE_LENGTH |
配置定时器命令队列的长度,决定可同时处理的定时器命令数量,避免命令丢失。 |
configTIMER_TASK_STACK_DEPTH |
配置定时器服务任务的堆栈大小,需根据定时器回调函数的复杂度合理设置,防止栈溢出。 |
示例代码(基本使用)
// 软件定时器1
void timer1_callback( TimerHandle_t xTimer );
#define TIMER1_PERIOD 500
TimerHandle_t timer1; //句柄
// 软件定时器2
void timer2_callback( TimerHandle_t xTimer );
#define TIMER1_PERIOD 1000
TimerHandle_t timer2; //句柄
void FreeRTOS_Start(void)
{
/* 使用软件定时器 实现LED灯闪烁 */
xTimerCreate("time1", TIMER1_PERIOD, pdTRUE, NULL, timer1_callback);
xTimerCreate("time2", TIMER2_PERIOD, pdTRUE, NULL, timer2_callback);
/*
TimerHandle_t xTimerCreate(
const char * const pcTimerName, // 定时器名称(仅用于调试)
TickType_t xTimerPeriod, // 定时器周期(单位:系统时基滴答数)
UBaseType_t uxAutoReload, // 触发模式:pdTRUE(周期触发)/ pdFALSE(单次触发)
void * pvTimerID, // 用户自定义参数(传递给回调函数)
TimerCallbackFunction_t pxCallbackFunction); // 定时器到期后的回调函数
*/
//启动定时器
xTimerStart(timer1,0);
xTimerStart(timer2,0);
vTaskStartScheduler();
}
void timer1_callback(TimerHandle_t xTimer)
{
if (xTimer == timer1)
{
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
}
}
void timer2_callback(TimerHandle_t xTimer)
{
if (xTimer == timer2)
{
Int_LED_Toggle(LED2_GPIO_Port, LED2_Pin);
}
}
示例代码(结合按键使用)
// 软件定时器1
void timer1_callback( TimerHandle_t xTimer );
#define TIMER1_PERIOD 500
TimerHandle_t timer1; //句柄
// 软件定时器2
void timer2_callback( TimerHandle_t xTimer );
#define TIMER1_PERIOD 1000
TimerHandle_t timer2; //句柄
// 按键任务
void Key_task(void *pvParameters);
#define KEY_TASK_PRIORITY 1
#define KEY_TASK_STACK_SIZE 128
TaskHandle_t Key_task_handle;
void FreeRTOS_Start(void)
{
/* 使用软件定时器 实现LED灯闪烁 */
xTimerCreate("time1", TIMER1_PERIOD, pdTRUE, NULL, timer1_callback);
xTimerCreate("time2", TIMER2_PERIOD, pdTRUE, NULL, timer2_callback);
//启动定时器
xTimerStart(timer1,0);
xTimerStart(timer2,0);
xTaskCreate(Key_task, "Key_task", KEY_TASK_STACK_SIZE, NULL, KEY_TASK_PRIORITY, &Key_task_han);
vTaskStartScheduler();
}
void timer1_callback(TimerHandle_t xTimer)
{
if (xTimer == timer1)
{
Int_LED_Toggle(LED1_GPIO_Port, LED1_Pin);
}
}
void timer2_callback(TimerHandle_t xTimer)
{
if (xTimer == timer2)
{
Int_LED_Toggle(LED2_GPIO_Port, LED2_Pin);
}
}
void Key_task(void *pvParameters)
{
while (1)
{
Key_Type key = Int_Key_Scan();
if (key == KEY1)
{
// 按键1按下 => 启动定时器1
xTimerStart(timer1, 100);
}
else if (key == KEY2)
{
xTimerStop(timer1, 100);
}
else if (key == KEY3)
{
// 按键3按下 => 修改定时器1的周期
xTimerChangePeriod(timer1, 1000, 100); // 100 为阻塞时间
}
}
}
低功耗模式
FreeRTOS 低功耗模式概述与设计
| 类别 | 说明 |
|---|---|
| 概述 | FreeRTOS 提供低功耗模式,系统在所有任务阻塞或挂起时进入该模式,此时周期性 SysTick 中断停止以节省功耗;当任务需唤醒(如延时到期)或有中断发生时,系统自动恢复正常状态。 |
| 硬件支持 | 以表格形式明确不同低功耗模式(睡眠、停机、待机)的进入条件、唤醒方式,以及对 1.8V 区域时钟、XtI/XtR 时钟、电压调节器的影响,为硬件级低功耗实现提供参考。 |
| 设计思想 | - 低功耗时长计算:根据任务阻塞时间计算低功耗时长;- SysTick 计时处理:将 SysTick 中断周期设置为计算出的低功耗时长,解决 SysTick 中断停止后的计时问题。 |
该低功耗模式是 FreeRTOS 在嵌入式设备节能场景的关键设计,通过软硬件协同(系统任务调度 + 硬件低功耗模式),在保证任务实时性的前提下,大幅降低空闲时的功耗,适用于电池供电的物联网设备、可穿戴设备等对续航要求高的场景。
FreeRTOS 低功耗模式相关参数说明
| 参数名称 | 功能描述 |
|---|---|
configUSE_TICKLESS_IDLE |
用于使能低功耗 Tickless 模式,默认值为 0,配置为 1 时启用该模式以节省功耗。 |
configEXPECTED_IDLE_TIME_BEFORE_SLEEP |
系统进入低功耗模式的最短时长,默认值为 2,用于避免频繁进入退出低功耗模式带来的开销。 |
configPRE_SLEEP_PROCESSING(x) |
在系统进入低功耗模式前执行的事务,例如关闭外设时钟等操作,为低功耗做好硬件准备。 |
configPOST_SLEEP_PROCESSING(x) |
系统退出低功耗模式后执行的事务,例如开启之前关闭的外设时钟,使系统恢复正常运行状态。 |
内存管理
FreeRTOS 堆内存管理概述与分析
| 维度 | 说明 |
|---|---|
| 内存分配场景 | 每次创建任务、队列、互斥锁、软件定时器、信号量或事件组时,RTOS 内核需从 RAM 分配内存;内存可从 RTOS 堆自动动态分配,或由应用开发者手动提供。 |
| 动态分配的不足 | 若使用标准 C 库malloc()/free()动态创建 RTOS 对象,存在以下问题:1. 嵌入式系统中不总是可用;2. 占用宝贵代码空间;3. 非线程安全;4. 执行时间不确定(非确定性)。 |
| 替代方案提示 | 可参考静态内存分配与动态内存分配页面,了解静态(不使用 FreeRTOS 堆)或动态分配 RTOS 对象的利弊,以及configAPPLICATION_ALLOCATED_HEAP常量的配置(在FreeRTOSConfig.h中定义),还可学习无堆实现时 FreeRTOS 的使用方法。 |
FreeRTOS 堆内存管理是保障系统资源高效利用与稳定性的关键环节,其核心是通过 RTOS 自身的堆机制规避标准 C 库动态分配的缺陷,适用于对内存确定性、线程安全和资源开销敏感的嵌入式实时系统场景(如工业控制、汽车电子)。
FreeRTOS heap_1 内存管理概述
| 维度 | 说明 |
|---|---|
| 适用场景 | 适用于大量嵌入式应用程序,尤其是小型、深度嵌入式场景 —— 系统启动时创建所有任务、队列、信号量等对象,生命周期内不删除,无需释放内存。 |
| 实现特点 | 是最简单的堆实现方式,内存分配后不允许释放;通过将单个数组细分为小块来管理 RAM,堆总大小由configTOTAL_HEAP_SIZE(在FreeRTOSConfig.h中定义)设置,也可通过configAPPLICATION_ALLOCATED_HEAP配置堆的特定地址。 |
| 工具与优化 | 提供xPortGetFreeHeapSize() API 函数,可返回未分配的堆空间总量,用于优化configTOTAL_HEAP_SIZE的设置。 |
heap_1 虽因 “不支持内存释放” 在功能灵活性上受限,但在 “对象创建后长期使用、无需动态销毁” 的嵌入式场景中(如简单传感器节点、传统工业控制器),凭借其实现简单、确定性高的特点,仍具有实用价值。
FreeRTOS heap_2 内存管理概述
| 维度 | 说明 |
|---|---|
| 版本定位 | 现为旧版,heap_4 是首选方案。 |
| 算法与特性 | 使用最佳适应算法,支持内存释放,但不合并相邻空闲块;适用于重复创建 / 删除任务、队列、信号量等对象的场景,但内存随机大小分配时不建议使用(易碎片化)。 |
| 配置与工具 | 堆总大小由configTOTAL_HEAP_SIZE(在FreeRTOSConfig.h中定义)设置,可通过configAPPLICATION_ALLOCATED_HEAP指定堆地址;提供xPortGetFreeHeapSize()查询未分配堆空间,pvPortCalloc()可分配并初始化内存为零。 |
heap_2 虽能满足动态内存分配 / 释放的基本需求,但因不合并空闲块易产生碎片,在对内存利用率要求高的场景中已逐渐被 heap_4 替代,仅在一些旧项目或简单动态内存场景中仍有使用价值。
FreeRTOS heap_4 内存管理概述
| 维度 | 说明 |
|---|---|
| 算法与特性 | 使用第一适应算法,支持相邻空闲内存块合并(包含合并算法),可有效减少内存碎片,是 FreeRTOS 堆内存管理的首选方案。 |
| 配置与工具 | 堆总大小由configTOTAL_HEAP_SIZE(在FreeRTOSConfig.h中定义)设置,可通过configAPPLICATION_ALLOCATED_HEAP指定堆地址;提供xPortGetFreeHeapSize()查询未分配堆空间总量、xPortGetMinimumEverFreeHeapSize()查询系统启动后最小空闲堆空间量,vPortGetHeapStats()可填充heap_t结构体获取堆详细统计信息。 |
| 适用场景 | 适用于需要频繁动态创建 / 删除 RTOS 对象(任务、队列、信号量等)的场景,尤其在内存碎片化敏感的嵌入式系统中(如复杂物联网设备、工业控制器),能保障内存利用率与系统稳定性。 |
heap_4 凭借空闲块合并算法和丰富的内存统计工具,成为 FreeRTOS 堆内存管理的主流选择,在动态内存分配场景中兼具实用性与可靠性,是平衡内存利用率、碎片化控制与功能灵活性的优选方案。
-------------------------- |
| 内存分配场景 | 每次创建任务、队列、互斥锁、软件定时器、信号量或事件组时,RTOS 内核需从 RAM 分配内存;内存可从 RTOS 堆自动动态分配,或由应用开发者手动提供。 |
| 动态分配的不足 | 若使用标准 C 库malloc()/free()动态创建 RTOS 对象,存在以下问题:1. 嵌入式系统中不总是可用;2. 占用宝贵代码空间;3. 非线程安全;4. 执行时间不确定(非确定性)。 |
| 替代方案提示 | 可参考静态内存分配与动态内存分配页面,了解静态(不使用 FreeRTOS 堆)或动态分配 RTOS 对象的利弊,以及configAPPLICATION_ALLOCATED_HEAP常量的配置(在FreeRTOSConfig.h中定义),还可学习无堆实现时 FreeRTOS 的使用方法。 |
FreeRTOS 堆内存管理是保障系统资源高效利用与稳定性的关键环节,其核心是通过 RTOS 自身的堆机制规避标准 C 库动态分配的缺陷,适用于对内存确定性、线程安全和资源开销敏感的嵌入式实时系统场景(如工业控制、汽车电子)。
FreeRTOS heap_1 内存管理概述
| 维度 | 说明 |
|---|---|
| 适用场景 | 适用于大量嵌入式应用程序,尤其是小型、深度嵌入式场景 —— 系统启动时创建所有任务、队列、信号量等对象,生命周期内不删除,无需释放内存。 |
| 实现特点 | 是最简单的堆实现方式,内存分配后不允许释放;通过将单个数组细分为小块来管理 RAM,堆总大小由configTOTAL_HEAP_SIZE(在FreeRTOSConfig.h中定义)设置,也可通过configAPPLICATION_ALLOCATED_HEAP配置堆的特定地址。 |
| 工具与优化 | 提供xPortGetFreeHeapSize() API 函数,可返回未分配的堆空间总量,用于优化configTOTAL_HEAP_SIZE的设置。 |
heap_1 虽因 “不支持内存释放” 在功能灵活性上受限,但在 “对象创建后长期使用、无需动态销毁” 的嵌入式场景中(如简单传感器节点、传统工业控制器),凭借其实现简单、确定性高的特点,仍具有实用价值。
FreeRTOS heap_2 内存管理概述
| 维度 | 说明 |
|---|---|
| 版本定位 | 现为旧版,heap_4 是首选方案。 |
| 算法与特性 | 使用最佳适应算法,支持内存释放,但不合并相邻空闲块;适用于重复创建 / 删除任务、队列、信号量等对象的场景,但内存随机大小分配时不建议使用(易碎片化)。 |
| 配置与工具 | 堆总大小由configTOTAL_HEAP_SIZE(在FreeRTOSConfig.h中定义)设置,可通过configAPPLICATION_ALLOCATED_HEAP指定堆地址;提供xPortGetFreeHeapSize()查询未分配堆空间,pvPortCalloc()可分配并初始化内存为零。 |
heap_2 虽能满足动态内存分配 / 释放的基本需求,但因不合并空闲块易产生碎片,在对内存利用率要求高的场景中已逐渐被 heap_4 替代,仅在一些旧项目或简单动态内存场景中仍有使用价值。
FreeRTOS heap_4 内存管理概述
| 维度 | 说明 |
|---|---|
| 算法与特性 | 使用第一适应算法,支持相邻空闲内存块合并(包含合并算法),可有效减少内存碎片,是 FreeRTOS 堆内存管理的首选方案。 |
| 配置与工具 | 堆总大小由configTOTAL_HEAP_SIZE(在FreeRTOSConfig.h中定义)设置,可通过configAPPLICATION_ALLOCATED_HEAP指定堆地址;提供xPortGetFreeHeapSize()查询未分配堆空间总量、xPortGetMinimumEverFreeHeapSize()查询系统启动后最小空闲堆空间量,vPortGetHeapStats()可填充heap_t结构体获取堆详细统计信息。 |
| 适用场景 | 适用于需要频繁动态创建 / 删除 RTOS 对象(任务、队列、信号量等)的场景,尤其在内存碎片化敏感的嵌入式系统中(如复杂物联网设备、工业控制器),能保障内存利用率与系统稳定性。 |
heap_4 凭借空闲块合并算法和丰富的内存统计工具,成为 FreeRTOS 堆内存管理的主流选择,在动态内存分配场景中兼具实用性与可靠性,是平衡内存利用率、碎片化控制与功能灵活性的优选方案。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)