FreeRTOS 快速入门
1 学习资料
1.1资源下载
1.2学习路线
1.3课程文档
2 程序设计模式
本章目标
- 理解裸机程序设计模式
- 了解多任务系统中程序设计的不同
2.1裸机程序设计模式
裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。
2.1.1轮询模式
在主循环中不断查询外设状态并执行对应操作,结构简单但效率低。
2.1.2 前后台
前台处理中断,后台执行主循环任务,实现基本的任务调度。
2.1.3 定时器驱动
通过定时器中断触发任务执行,实现周期性操作。
2.1.3 基于状态机
根据不同状态执行相应操作,通过状态转移管理复杂逻辑。
2.2多任务系统
2.2.1多任务模式
一次性完成一个任务,任务与任务之间间隔较长,让人感觉任务执行不连贯、响应迟缓。
一次完成一小段任务,快速切换任务,任务与任务之间间隔较短,让人感觉任务执行连贯、响应快。
多任务系统会依次给这些任务分配时间,从而实现一次完成一小段任务的操作。
2.2.2互斥操作
多任务系统中,多个任务可能需要同时访问某些资源,需要增加保护措施以防止混乱。
这时要考虑访问公用的资源的“互斥操作”。
2.2.3同步操作
任务之间有依赖关系,比如说任务 B 要在任务 A 完成之后执行。
这种时候就可以阻塞 B,先让任务A处理完复杂事情,再执行 B。
3 开发板的使用
3.1硬件连接
3.1.1连接ST-Link
3.1.2连接USB串口
3.2 测试外设
3.3 注意事项
4 模块使用与软件配置
4.1硬件模块和驱动对应关系
模块 | 驱动 |
板载单色LED | driver_led.cdriver_led.h |
按键(K1) | driver_key.cdriver_key.h |
蜂鸣器模块(有源) | driver_active_buzzer.cdriver_active_buzzer.h |
蜂鸣器模块(无源) | driver_passive_buzzer.cdriver_passive_buzzer.h |
温湿度模块(DHT11) | driver_dht11.cdriver_dht11.h |
温度模块(DS17B20) | driver_ds17b20.cdriver_ds17b20.h |
红外避障模块(LM393) | driver_ir_obstacle.cdriver_ir_obstacle.h |
超声波测距模块(HC-SR03) | driver_ultrasonic_sr04.cdriver_ultrasonic_sr04.h |
旋转编码器模块(EC11) | driver_rotary_encoder.cdriver_rotary_encoder.h |
红外接收模块(1738) | driver_ir_receiver.cdriver_ir_receiver.h |
红外发射模块(38KHz) | driver_ir_sender.cdriver_ir_sender.h |
RGB全彩LED模块 | driver_color_led.cdriver_color_led.h |
光敏电阻模块 | driver_light_sensor.cdriver_light_sensor.h |
舵机(SG90) | |
IIC OLED屏幕(SSD1306) | driver_oled.cdriver_oled.h |
IIC 陀螺仪加速度计模块(MPU6050) | driver_mpu6050.cdriver_mpu6050.h |
SPI FLASH模块(W25Q64) | driver_spiflash_w25q64.cdriver_spiflash_w25q64.h |
直流电机(DRV8833) | driver_motor.cdriver_motor.h |
步进电机(ULN2003) |
4.2调试引脚与定时器
5 创建 FreeRTOS 工程
5.1 创建STM32CubeMX工程
芯片选型
5.2 配置时钟
配置时钟
切换基准时钟
配置系统时钟频率
5.3 配置外设
这里以 GPIO 为例
板载LED的使用的GPIO是PC13,如下图所示:
在引脚配置界面,找到PC13:
配置引脚模式
这里选择GPIO Output,让PC13配置为通用输出IO,以便用来驱动LED的亮灭。
5.4 配置FreeRTOS
CMSIS-RTOS 选择 V2,因为 V2 功能更多。
5.4.1 配置参数
配置FreeRTOS的参数和功能
FreeRTOS的参数包括时基频率、任务堆栈大小、是否使能互斥锁等。
5.4.2 添加任务
使用默认任务
5.5 生成Keil MDK的工程
配置工程
配置代码生成
点击“Open Folder”可以打开工程目录,看到如下文件:
5.6 添加用户代码
STM32CubeMX 完成了初始化配置,要实现什么功能,需要自己添加代码。
5.6.1 打开工程
使用Keil打开工程:
5.6.2 修改文件
打开freertos.c文件,找到StartDefaultTask函数里的循环。
如下图加入代码:
6FreeRTOS 源码
6.1 FreeRTOS目录结构
6.2 头文件相关
6.2.1头文件目录
FreeRTOS需要3个头文件目录:
- FreeRTOS本身的头文件:
Middlewares\Third_Party\FreeRTOS\Source\include
- 移植时用到的头文件:
Middlewares\Third_Party\FreeRTOS\Source\portable[compiler][architecture]
- 含有配置文件FreeRTOSConfig.h的目录:Core\Inc
6.2.2头文件
6.3 入口函数
/* Init scheduler */
osKernelInitialize(); /* 初始化FreeRTOS运行环境 */
MX_FREERTOS_Init(); /* 创建任务 */
/* Start scheduler */
osKernelStart(); /* 启动调度器 */
6.4 数据类型和编程规范
6.4.1数据类型
TickType_t
:决定系统时钟的计量精度和上限,需根据节拍频率和运行时长选择位数BaseType_t
:与架构字长对齐的高效基础类型,用于提升关键路径代码性能
6.4.2变量名
在命名之前通过前缀提醒其类型方便阅读
6.4.3函数名
6.4.4宏的名
7 内存管理
7.1ARM 架构相关知识
7.1.1 硬件架构与汇编指令
整体框架
- 对内存只有读、写指令
- 对于数据的运算是在CPU内部实现
CPU 内部结构
R0~R12:临时存储数据
R13:SP(Stack Pointer),栈指针,保存栈指针
MSP:主栈指针,全局任务执行使用
PSP:进程栈指针,普通任务执行使用
R14:LR(Link Register),链接寄存器,保存返回地址
R15:PC(Program Counter),程序计数器,保存当前指令地址
汇编指令
读内存: Load
LDR R0, [R1, #4] ; 读地址 "R1+4",得到的 4 字节数据存入 R0
写内存: Stroe
STR R0, [R1, #4] ; 把 R0 的 4 字节数据写入地址 "R1+4"
7.1.2 汇编实例
工作流程
运算a-a*b
- 读内存a
- 读内存 b
- 计算a*b
- 结果写入内存
代码
PUSH {LR} ; 保存返回地址
SUB SP, SP, #8 ; 为a和b分配8字节栈空间
0x1000
+────────+
| |
+────────+
0x0FFC
+────────+
| 0x8000 | ← LR 值
+────────+
0x0FF8
+────────+
| | ← 预留给变量 a 的空间(SP+4)
+────────+
0x0FF4
SP ───────────→ +────────+
| | ← 预留给变量 b 的空间(SP)
+────────+
0x0FF0
MOV R0, #10 ; R0 = 10
STR R0, [SP, #4] ; a = 10(SP+4位置)
MOV R0, #20 ; R0 = 20
STR R0, [SP] ; b = 20(SP位置)
0x1000
+────────+
| |
+────────+
0x0FFC
+────────+
| 0x8000 | ← LR 值
+────────+
0x0FF8
+────────+
| 10 | ← 变量 a 的值(SP+4 = 0xFF8)
+────────+
0x0FF4
SP ───────────→ +────────+ ← SP 为栈顶
| 20 | ← 变量 b 的值(SP = 0xFF4)
+────────+
0x0FF0
ADD SP, SP, #8 ; 释放栈空间
POP {PC} ; 返回
0x1000
SP ───────────→ +────────+
| | ← 栈恢复初始状态
| 空闲 |
| |
+────────+
0x0FF8
7.2栈和堆
7.2.1 栈的知识点
- 特殊寄存器:用LR保存返回地址,通过压栈/弹栈避免覆盖。
- 局部变量分配:在任务栈顶动态分配,函数返回时释放。
- 独立任务栈:保证任务间数据隔离、上下文独立保存。
7.2.2 堆的知识点
定义:由程序员手动管理的动态内存空间(如用malloc
申请、free
释放),用于存储生存期较长的数据(如动态数组)。
特点:
- 分配灵活,大小可变;
- 需手动释放,易内存泄漏;
- 分配效率较低,可能导致内存碎片化。
7.3 内存管理简介
7.3.1 层级关系图解
计算机存储体系(从CPU到外部存储):
┌─────────────────────────────────────────────────────────────────────────────┐
│ CPU │
│ ┌─────────────────────┐ │
│ │ 寄存器(Register) │ │
│ └─────────────────────┘ │
│ ▲ │
│ │ │
├───────────────────────────────────┼─────────────────────────────────────────┤
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 高速缓存(Cache) │ │
│ └─────────────────────┘ │
│ ▲ │
│ │ │
├───────────────────────────────────┼─────────────────────────────────────────┤
│ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 内存 │ │ 只读存储器 │ |
│ │ (RAM) │ │ (ROM) │ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ 栈 | │ │ │ BIOS │ │ │
│ │ └───────────────┘ │ │ └───────────────┘ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ 堆 | │ │ │ 固件程序 │ │ │
│ │ └───────────────┘ │ │ └───────────────┘ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ 全局变量区 │ │ │ │ 系统配置 │ │ │
│ │ └───────────────┘ │ │ └───────────────┘ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ ▲ │
│ │ │
├───────────────────────────────────┼─────────────────────────────────────────┤
│ ▼ │
│ ┌─────────────────────┐ │
│ │ 外存(Storage) │ │
│ │ ┌───────────────┐ │ │
│ │ │ FLASH │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │ SSD/HDD | │ │ │
│ │ │ └──────────┘ │ │ │
│ │ │ ┌──────────┐ │ │ │
│ │ │ │ U盘/SDD | | │ │
│ │ │ └──────────┘ │ │ │
│ │ └───────────────┘ │ │
│ │ ┌───────────────┐ │ │
│ │ │ 传统硬盘 │ │ │
│ │ │ (HDD) │ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
数据流向:
1. CPU直接访问寄存器和缓存
2. 缓存从RAM读取数据(局部性原理)
3. RAM从外存(FLASH/硬盘)加载程序和数据
4. 外存(硬盘/FLASH)作为最终存储介质保存持久化数据
7.3.2 概念对比
差不多得了😅
术语 | 所属范畴 | 数据特性 |
栈(Stack) | RAM | 临时数据 |
堆(Heap) | RAM | 动态数据 |
全局变量区(Global Variable Area) | RAM | 全局数据 |
随机存取存储器(RAM) | 内存 | 运行时的数据 |
只读存储器(ROM) | 内存 | 系统初始化的数据和程序 |
内存(Memory) | 硬件组件 | 正在运行的程序和数据 |
硬盘(HDD/SSD) | 硬件组件 | 长期数据 |
7.3.3 总结
- 栈 vs 堆 vs 全局变量区:
-
- 栈 自动管理,适合短期、固定大小的数据。
- 堆 手动管理,适合动态、长期存活的数据。
- 全局变量区 自动管理、适合存储全局作用域的数据。
- 层级协作:
-
- 程序运行时,数据从 硬盘→内存→缓存→寄存器 流动。
- 栈和堆是 RAM 中的不同管理区域,共同支撑程序执行。
7.4 内存管理法详解
内存管理算法
文件 | 优点 | 缺点 |
heap_1.c | 分配简单,时间确定性高 | 仅支持静态分配,不支持释放 |
heap_2.c | 支持动态分配和释放 | 可能产生内存碎片 |
heap_3.c | 封装标准库 malloc/free | 时间不确定性高,依赖 libc |
heap_4.c | 支持内存块合并,减少碎片 | 分配时间不确定 |
heap_5.c | 支持不连续内存区域管理 | 分配时间不确定 |
一般用算法 4,有多个 RAM 段用算法 5。
7.4.1 Heap_1
- 适用场景:
-
- 系统运行期间不删除任务 / 队列的场景。
7.4.2 Heap_4
- 适用场景:
-
- 需频繁分配 / 释放不同大小内存的场景。
7.4.3 Heap_5
- 适用场景:
-
- 内存地址不连续的嵌入式系统。
7.5 Heap相关的函数
函数 | 作用 |
| 分配内存 |
| 释放内存 |
| 查询当前空闲内存大小 |
| 查询历史最小空闲内存 |
| 分配失败时的回调函数 |
8 任务管理
在本章中,会涉及如下内容:
- 如何给每个任务分配CPU时间
- 如何选择某个任务来运行
- 任务优先级如何起作用
- 任务有哪些状态
- 如何实现任务
- 如何使用任务参数
- 怎么修改任务优先级
- 怎么删除任务
- 怎么实现周期性的任务
- 如何使用空闲任务
8.1基本概念
概念 | 定义与说明 |
应用程序(Application) | 指整个单片机程序,包含所有功能逻辑。 |
任务(Task)/ 线程(Thread) | 可独立运行的最小执行单元,用于实现程序的并行功能。每个任务可视为一个 “子功能模块”。 |
任务状态(State) | 描述任务当前的执行状态,如: |
优先级(Priority) | 任务执行的优先等级: |
事件驱动(Event-Driven) | 任务通过等待特定事件触发后续操作,而非持续占用 CPU 资源,提高效率。 |
协助式调度(Co-operative Scheduling) | 任务主动让出 CPU 控制权,需等待当前任务主动释放资源后,其他任务才能运行。 |
8.1.1 总结
- 一个程序由多个任务组成
- 状态与调度决定执行流程
- 栈保障任务数据独立
- 事件驱动优化资源利用
8.2任务创建与删除
8.2.1什么是任务
任务是个特定函数,为void ATaskFunction( void *pvParameters )
,该函数无返回值,并且一个函数能用来创建多个任务,可以多个任务可运行同一个函数。
8.2.2创建任务
动态分配内存创建任务(自动分配)
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 任务函数
const char * const pcName, // 任务名
const configSTACK_DEPTH_TYPE usStackDepth,// 栈大小(字)
void * const pvParameters, // 任务函数参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄
第一个 const,不能通过这个指针去修改所指向的字符内容。
const char *p = "hello";
// p = "world";这是可以的,指针p可以指向其他字符串
// *p = 'x'; 这是不允许的,因为p指向的内容是常量,不能被修改
第二个 const, 不能改变pcName 这个指针变量的值。
即对 pcName 进行修改指向内容或者改变指针指向的操作都会导致编译错误。
静态分配内存的创建任务(手动分配)
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); // 任务控制块(TCB)
8.2.2.1 总结
- 动态和静态分配的区别在于是否要手动分配内存空间
- 静态要手动分配,从而操作栈空间和任务控制块。
- 动态会自动分配,通过操作句柄从而操作栈空间和任务控制块。
注意:动态数组和动态分配的动态不是一个意思
动态数组的 “动态”
含义:数组的大小可改变。
手动分配:通过malloc()/realloc()手动管理内存。
FreeRTOS 动态分配的 “动态”
含义:任务 / 队列的内存在运行时自动分配(从堆中分配)。
调用xTaskCreate()
/xQueueCreate()
时,FreeRTOS 自动从堆分配内存。
8.2.3 示例 1:估算栈的大小
最复杂的嵌套函数的层*36+局部变量*4+保存现场(64)(字节)=栈*4(字节)
8.2.4 示例 2:创建任务
05_create_task:使用动态、静态分配内存的方式,分别创建多个任务。
8.2.5任务的删除
自杀:vTaskDelete(NULL)
被杀:其他任务执行vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄
杀人:vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄
注意:只有运行状态的任务才能删除任务
8.2.6示例3:删除任务
07_delete_task:当监测到遥控器的Power按键被按下后,删除音乐播放任务。
while (1)
{
/* 读取红外遥控器 */
if (0 == IRReceiver_Read(&dev, &data))
{
if (data == 0xa8) /* play */
{
/* 创建播放音乐的任务 */
extern void PlayMusic(void *params);//声明一个外部函数PlayMusic
if (xSoundTaskHandle == NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Create Task");
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);
}
}
else if (data == 0xa2) /* power */
{
/* 删除播放音乐的任务 */
if (xSoundTaskHandle != NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Delete Task");
vTaskDelete(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
xSoundTaskHandle = NULL;
}
}
}
}
8.3任务优先级和Tick
8.3.1任务优先级
调度器通过两种算法找出优先级最高的任务并执行
1. 通用方法(configUSE_PORT_OPTIMISED_TASK_SELECTION=0)
- 实现:遍历所有优先级,逐个检查是否有就绪任务。
- 限制:优先级无上限,但值越大越耗时。
2. 架构优化方法(configUSE_PORT_OPTIMISED_TASK_SELECTION=1)
- 实现:汇编指令从 32 位位图中快速定位最高优先级。
- 限制:优先级最多 32。
8.3.2 Tick
每隔一段时间触发中断发起一次调度,选择并执行下一个的任务
- 先确保最高优先级的、可运行的任务,马上执行
- 对于相同优先级的、可运行的任务,轮流执行
8.3.3示例4:优先级实验
08_task_priority:提高音乐播放任务的优先级,使用vTaskDelay进行延时
while (1)
{
/* 读取红外遥控器 */
if (0 == IRReceiver_Read(&dev, &data))
{
if (data == 0xa8) /* play */
{
/* 创建播放音乐的任务 */
extern void PlayMusic(void *params);
if (xSoundTaskHandle == NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Create Task");
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal+1, &xSoundTaskHandle);
}
}
else if (data == 0xa2) /* power */
{
/* 删除播放音乐的任务 */
if (xSoundTaskHandle != NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Delete Task");
vTaskDelete(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
xSoundTaskHandle = NULL;
}
}
}
}
8.3.4修改优先级
//用uxTaskPriorityGet来获得任务的优先级:
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
//用vTaskPrioritySet 来设置任务的优先级:
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
xTask指定任务句柄,NULL表示获取自己的优先级。
uxNewPriority表示新的优先级,取值范围是0~(configMAX_PRIORITIES – 1)。
加了const意味着,不能在函数修改其值,没加就可以修改其值,故可以设置优先级
8.4任务状态
8.4.1阻塞状态(Blocked)
vTaskDelay(2); // 等待2个Tick,假设configTICK_RATE_HZ=100, Tick周期时10ms, 等待20ms
vTaskDelay(pdMS_TO_TICKS(20)); //还可以使用pdMS_TO_TICKS宏把ms转换为tick,等待20ms
8.4.2暂停状态(Suspended)
进入暂停:
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
xTaskToSuspend 指定任务句柄,NULL表示暂停自己。
退出暂停:退出暂停状态,只能由别人来操作
void vTaskResume( TaskHandle_t xTaskToResume ); //其他任务调用
BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume );//中断程序调用
xTaskToResume 要恢复的任务句柄
8.4.3就绪状态(Ready)
任务准备好了还没轮到它运行
8.4.4完整的状态转换图
8.4.5示例5:任务暂停
09_task_suspend:使用vTaskSuspend暂停音乐播放任务,使用vTaskResume恢复它,实现音乐的暂停播放、继续播放功能。
while (1)
{
/* 读取红外遥控器 */
if (0 == IRReceiver_Read(&dev, &data))
{
if (data == 0xa8) /* play */
{
/* 创建播放音乐的任务 */
extern void PlayMusic(void *params);
if (xSoundTaskHandle == NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Create Task");
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal+1, &xSoundTaskHandle);
bRunning = 1;
}
else
{
/* 要么suspend要么resume */
if (bRunning)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Suspend Task");
vTaskSuspend(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
bRunning = 0;
}
else
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Resume Task");
vTaskResume(xSoundTaskHandle);
bRunning = 1;
}
}
}
else if (data == 0xa2) /* power */
{
/* 删除播放音乐的任务 */
if (xSoundTaskHandle != NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Delete Task");
vTaskDelete(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
xSoundTaskHandle = NULL;
}
}
}
}
8.6 Delay函数
8.6.1两个Delay函数
void vTaskDelay( const TickType_t xTicksToDelay );
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement );
xTicksToDelay 阻塞固定的时间
pxPreviousWakeTime 上一次被唤醒的时间
xTimeIncrement 阻塞固定的时间(pxPreviousWakeTime + xTimeIncrement)
8.6.2示例5:Delay
11_taskdelay:比较vTaskDelay和vTaskDelayUntil实际阻塞的时间,并在LCD上打印出来。
void LcdPrintTask(void *params)
{
struct TaskPrintInfo *pInfo = params;
uint32_t cnt = 0;
int len;
BaseType_t preTime;
uint64_t t1, t2;
preTime = xTaskGetTickCount();
while (1)
{
/* 打印信息 */
if (g_LCDCanUse)
{
g_LCDCanUse = 0;
len = LCD_PrintString(pInfo->x, pInfo->y, pInfo->name);
len += LCD_PrintString(len, pInfo->y, ":");
LCD_PrintSignedVal(len, pInfo->y, cnt++);
g_LCDCanUse = 1;
mdelay(cnt & 0x3);
}
t1 = system_get_ns();
//vTaskDelay(500); // 500000000
vTaskDelayUntil(&preTime, 500);
t2 = system_get_ns();
LCD_ClearLine(pInfo->x, pInfo->y+2);
LCD_PrintSignedVal(pInfo->x, pInfo->y+2, t2-t1);
}
}
8.7空闲任务及其钩子函数
8.7.1 开启钩子函数
- 先将configUSE_IDLE_HOOK 定义为 1
- 在后面添加vApplicationIdleHook函数
8.7.2 介绍
空闲任务
- 没事情干时调用空闲函数
- 空闲函数负责释放被删除任务的内存,执行低功耗操作,维持系统运行
钩子函数(回调函数)
- 空闲任务执行时被调用 or 其他特定任务执行时被调用
- 钩子函数实现后台操作,比如监控系统状态、测量 CPU 使用率等。
两者关系
- 钩子函数是可选的扩展功能
- 空闲任务是系统必须执行的任务
- 钩子函数可以在空闲任务执行时被调用
8.8调度算法
8.8.1重要概念
正在运行的任务:运行(Running)。
非运行状态的任务:阻塞(Blocked)、暂停(Suspended)、就绪(Ready)。
8.8.2配置调度算法
调度算法:即确定哪个就绪态的任务可以切换为运行状态。
通过配置FreeRTOSConfig.h中的 configUSE_PREEMPTION、configUSE_TIME_SLICING 配置调度算法
FreeRTOS调度模式决策树
├─ configUSE_PREEMPTION=0 → 合作调度
│ └─ 特点:高优先级不抢占,任务需主动让出CPU,同优先级按顺序执行
│
└─ configUSE_PREEMPTION=1 → 可抢占调度
├─ configUSE_TIME_SLICING=0 → 无时间片轮转
│ ├─ 特点:高优先级可抢占,同优先级持续运行直到主动放弃或被抢占
│ └─ configIDLE_SHOULD_YIELD=1 → 空闲任务主动让位同优先级用户任务
│
└─ configUSE_TIME_SLICING=1 → 时间片轮转
├─ 特点:高优先级可抢占,同优先级按时间片轮流执行(默认1tick)
└─ configIDLE_SHOULD_YIELD=1 → 空闲任务主动让位同优先级用户任务
├─ 是 → 空闲任务每次循环后检查并让位
└─ 否 → 空闲任务与用户任务平等竞争
辨析
配置项 | A | B | C | D | E |
configUSE_PREEMPTION | 1 | 1 | 1 | 1 | 0 |
configUSE_TIME_SLICING | 1 | 1 | 0 | 0 | x |
configIDLE_SHOULD_YIELD | 1 | 0 | 1 | 0 | x |
说明 | 常用 | 很少用 | 很少用 | 很少用 | 几乎不用 |
- A:可抢占+时间片轮转+空闲任务让步
- B:可抢占+时间片轮转+空闲任务不让步
- C:可抢占+非时间片轮转+空闲任务让步
- D:可抢占+非时间片轮转+空闲任务不让步
- E:合作调度
8.8.3 总结
配置项 | 启用效果 | 禁用效果 |
| 高优先级任务可立即抢占 | 任务须主动让出 CPU,优先级无意义 |
| 同优先级任务按时间轮转,Tick 中断触发切换 | 同优先级任务持续运行,仅在高优先级任务就绪或结束时切换 |
| 空闲任务主动让位 | 空闲任务与用户任务比划比划 |
9 同步互斥与通信
9.1同步与互斥的概念
临界资源:同一时间只能有一任务使用的资源。
同步(Synchronization):确保任务按预定顺序执行,一个任务的执行依赖另一个任务的完成
互斥(Mutual Exclusion):确保临界资源在同一时间仅被一个任务访问,防止数据竞争
9.2 各类方法的对比
概念
内核对象 | 传递信息具体的数据内容 | 传递状态(标志位) 状态变化或事件发生的信号 | 使用权限 | 一对多 | 独占释放 | 应用场景 |
队列 | ✅ | ✅ | 所有任务 / 中断 | ✅ | ❌ | 任务间数据传递(如传感器数据→处理任务) |
信号量 | ❌ | ✅ | 所有任务 / 中断 | ✅ | ❌ | 资源计数(如可用缓冲区数量)、同步(任务 A 完成后唤醒任务 B) |
互斥量 | ❌ | ✅ | 所有任务 | ❌ | ✅ | 保护共享资源(如多个任务读写同一全局变量) |
事件组 | ❌ | ✅ | 所有任务 / 中断 | ✅ | ❌ | 多事件触发(如 “初始化完成”“数据就绪” 同时满足时执行任务) |
任务通知 | ✅ | ✅ | 仅目标任务 | ❌ | ❌ | 高效的一对一通信(如中断直接通知特定任务) |
- 队列:
-
- 里面可以放任意数据,可以放多个数据
- 任务、ISR都可以放入数据;任务、ISR都可以从中读出数据
- 事件组:
-
- 一个事件用一bit表示,1表示事件发生了,0表示事件没发生
- 可以用来表示事件、事件的组合发生了,不能传递数据
- 有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
- 信号量:
-
- 核心是"计数值"
- 任务、ISR释放信号量时让计数值加1
- 任务、ISR获得信号量时让计数值减1
- 任务通知:
-
- 核心是任务的TCB里的数值
- 会被覆盖
- 发通知给谁?必须指定接收任务
- 只能由接收任务本身获取该通知
- 互斥量:
-
- 数值只有0或1
- 谁获得互斥量,就必须由谁释放同一个互斥量
选取原则
├─ 是否保护共享资源?
│ ├─ 是 → 是否需要优先级继承?
│ │ ├─ 是 → 互斥量
│ │ └─ 否 → 二值信号量/临界区
│ │
│ └─ 否 → 是否传递数据?
│ ├─ 是 → 是否高效/一对一?
│ │ ├─ 是 → 任务通知
│ │ └─ 否 → 队列
│ │
│ └─ 否 → 是否多事件组合?
│ ├─ 是 → 事件组
│ └─ 否 → 是否统计资源?
│ ├─ 是 → 计数信号量
│ └─ 否 → 二值信号量
│
└─ 是否在中断中使用?
└─ 是 → 使用带FromISR的API(任务通知优先)
10 队列
本章涉及如下内容:
- 怎么创建、清除、删除队列
- 队列中消息如何保存
- 怎么向队列发送数据、怎么从队列读取数据、怎么覆盖队列的数据
- 在队列上阻塞是什么意思
- 怎么在多个队列上阻塞
- 读写队列时如何影响任务的优先级
队列(queue)可以用于"任务到任务"、"任务到中断"、"中断到任务"直接传输信息。
10.1队列的特性
10.1.1常规操作
- 基本构成:包含若干数据项,数据大小固定。
- 创建要求:创建时需指定队列长度和数据大小。
- 操作规则:
-
- 默认规则:写数据至尾部,读数据从头部,遵循先进先出原则。
- 特殊操作:可强制写数据至头部,覆盖原有头部数据。
10.1.2 队列的阻塞访问
10.2队列函数
使用队列的流程:创建队列、写队列、读队列、删除队列
10.2.1创建
创建有两种方法:动态分配内存、静态分配内存
- 动态分配内存:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
uxQueueLength队列长度(数据项)
uxItemSize每个数据的大小(字节)
返回值非0:成功,返回句柄。NULL:失败,因为内存不足
- 静态分配内存:
QueueHandle_t xQueueCreateStatic(*
UBaseType_t uxQueueLength,*
UBaseType_t uxItemSize,*
uint8_t *pucQueueStorageBuffer,*
StaticQueue_t *pxQueueBuffer*
);
pucQueueStorageBuffer | uint8_t数组, 此数组大小至少为uxQueueLength * uxItemSize |
pxQueueBuffer | StaticQueue_t结构体,用于静态分配队列所需的内存空间,包括队列的控制块(如队列状态、读写指针等)和数据缓冲区(存储实际数据项)。 |
看看结构体的成员类型是什么
10.2.2复位
BaseType_t xQueueReset( QueueHandle_t pxQueue);//操作句柄
10.2.3删除
void vQueueDelete( QueueHandle_t xQueue );
10.2.4写队列
往队列尾部写入数据
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
往队列尾部写入数据,此函数可以在中断函数中使用
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
往队列头部写入数据
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
往队列头部写入数据,此函数可以在中断函数中使用
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
参数 | 说明 |
xQueue | 句柄 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列,数据大小在创建队列时已经确定了 |
xTicksToWait | 表示阻塞的最大时间(Tick Count)。 0,即无法写入数据时函数会立刻返回; portMAX_DELAY,即一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入, errQUEUE_FULL:写入失败,因为队列满了。 |
10.2.5读队列
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken );
参数 | 说明 |
xQueue | 句柄 |
pvItemToQueue | 数据指针,这个数据的值会被复制进队列,数据大小在创建队列时已经确定了 |
xTicksToWait | 表示阻塞的最大时间(Tick Count)。 0,即无法写入数据时函数会立刻返回; portMAX_DELAY,即一直阻塞直到有空间可写 |
返回值 | pdPASS:数据成功写入, errQUEUE_FULL:写入失败,因为队列满了。 |
10.2.6查询
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
10.2.7 覆盖
注意使用时队列长度必须为1
/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void * pvItemToQueue
);
BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
10.2.8 复制
/* 复制队列
* xQueue: 复制哪个队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 没有数据的话阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);
10.3示例:队列的基本使用
13_queue_game。以前使用环形缓冲区传输红外遥控器的数据,本程序改为使用队列。
10.3.1程序框架
10.3.2源码分析
/* 挡球板任务 - 处理输入数据并控制挡球板移动 */
static void platform_task(void *params)
{
byte platformXtmp = platformX; /* 保存当前挡球板X坐标 */
uint8_t dev, data, last_data = 0;
struct input_data idata
/* 初始化显示:绘制初始位置的挡球板 */
draw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
while (1) {
/* 从队列接收输入数据(阻塞等待) */
if (pdPASS == xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY)) {
data = idata.val; /* 获取按键值 */
/* 处理无效数据:使用上一次有效数据 */
if (data == 0x00) {
data = last_data;
}
/* 处理左移命令 */
if (data == 0xe0) { /* Left */
btnLeft(); /* 调用左移函数 */
}
/* 处理右移命令 */
if (data == 0x90) { /* Right */
btnRight(); /* 调用右移函数 */
}
/* 保存本次有效数据 */
last_data = data;
}
}
}
10.3.3上机实验
10.4示例:使用队列实现多设备输入
14_queue_game_multi_input。
10.5输入设备驱动与任务解耦设计及队列集应用
驱动设计原则
- 职责分离:驱动仅采集硬件数据(如红外键值、编码器脉冲)
- 复用性:驱动不包含业务代码(如游戏控制转换),切换场景时可直接复用。
任务解耦设计(InputTask)
- 统一管理所有输入设备队列,将原始数据转换为业务所需控制指令(如将红外键值
0xe0
转为 “左移” 命令)。
队列集(Queue Set)核心逻辑
- 作用:统一管理多个设备队列,InputTask 通过队列集监听数据来源。
- 流程:
-
- 创建设备队列(如红外队列 A、编码器队列 B)。
- 创建队列集 S,长度为各设备队列长度之和。
- 加入队列集:将 A、B 的句柄加入 S。
- 数据写入:向 A/B 写入数据时,自动将其句柄写入 S。
- 数据读取:InputTask 从 S 读取句柄,判断来源后从对应队列取数据并转换为业务指令。
10.5.1创建队列集
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength )
参数 | 说明 |
uxQueueLength | 队列集长度,最多能存放多少个句柄 |
返回值 | 非0:成功,返回句柄,以后使用句柄来操作队列NULL:失败,因为内存不足 |
10.5.2把队列加入队列集
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );
参数 | 说明 |
xQueueOrSemaphore | 要加入队列集的队列句柄 |
xQueueSet | 队列集句柄 |
返回值 | pdTRUE:成功pdFALSE:失败 |
10.5.3读取队列集
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
TickType_t const xTicksToWait );
参数 | 说明 |
xQueueSet | 队列集句柄 |
返回值 | NULL:失败,队列句柄:成功 |
10.6示例:使用队列集改善程序框架
15_queueset_game
10.7示例12:遥控器数据分发给多个任务
17_queue_car_dispatch
10.7.1程序框架
10.7.2源码分析
/**
* 汽车控制任务 - 处理红外遥控输入并控制汽车移动
* @param params 指向汽车结构体的指针
*/
static void CarTask(void *params)
{
struct car *pcar = (struct car *)params;
struct ir_data idata;
/* 创建用于接收红外数据的队列 */
QueueHandle_t xQueueIR = xQueueCreate(10, sizeof(struct ir_data));
/* 注册队列到红外驱动,使驱动能向此队列发送数据 */
RegisterQueueHandle(xQueueIR);
/* 初始化显示汽车 */
ShowCar(pcar);
while (1) {
/* 阻塞等待红外数据 */
if (pdPASS == xQueueReceive(xQueueIR, &idata, portMAX_DELAY)) {
/* 判断是否为控制当前汽车的按键 */
if (idata.val == pcar->control_key) {
/* 确保汽车不超出右边界 */
if (pcar->x < g_xres - CAR_LENGTH) {
/* 更新汽车位置 */
HideCar(pcar); // 隐藏原位置汽车
pcar->x += 20; // 向右移动20像素
/* 边界检查,防止越界 */
if (pcar->x > g_xres - CAR_LENGTH) {
pcar->x = g_xres - CAR_LENGTH;
}
ShowCar(pcar); // 在新位置显示汽车
}
}
}
}
}
/* 创建3个汽车任务实例 */
void CreateCarTasks(void)
{
xTaskCreate(CarTask, "car1", 128, &g_cars[0], osPriorityNormal, NULL);
xTaskCreate(CarTask, "car2", 128, &g_cars[1], osPriorityNormal, NULL);
xTaskCreate(CarTask, "car3", 128, &g_cars[2], osPriorityNormal, NULL);
}
10.7.3上机实验
11 信号量
本章涉及如下内容:
- 怎么创建、删除信号量
- 怎么发送、获得信号量
- 什么是计数型信号量?什么是二进制信号量?
11.1信号量的特性
11.1.1信号量的常规操作
基本概念
- 信号量本质:兼具“通知”(信号)与“资源数量表示”(量)功能的同步机制。
核心操作
- give(释放):计数值加1,唤醒等待任务。
- take(获取):计数值减1,若失败可选择阻塞或立即返回。
典型场景
- 计数型信号量:
-
- 事件计数:产生时计数+1,处理时计数-1。
- 资源管理:适合多任务竞争有限资源的情况。
- 二进制信号量:
-
- 事件标志:表示事件是否发生(0未发生,1已发生)。
- 互斥场景:保证仅一个任务访问临界资源的情况。
11.1.2 两种信号量的对比
二进制信号量 | 技术型信号量 |
被创建时初始值为0 | 被创建时初始值可以设定 |
其他操作是一样的 | 其他操作是一样的 |
11.2信号量函数
使用句柄来表示一个信号量,使用信号量时,先创建、然后去添加资源、获得资源。
11.2.1创建
| 计数型信号量 | |
动态创建 | xSemaphoreCreateBinary 计数值初始值为0 | xSemaphoreCreateCounting |
静态创建 | xSemaphoreCreateBinaryStatic | xSemaphoreCreateCountingStatic |
创建二进制信号量
/* 创建一个二进制信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinary( void );
/* 创建一个二进制信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer );
创建计数型信号量
/* 创建一个计数型信号量,返回它的句柄。
* 此函数内部会分配信号量结构体
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);
/* 创建一个计数型信号量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* uxMaxCount: 最大计数值
* uxInitialCount: 初始计数值
* pxSemaphoreBuffer: StaticSemaphore_t结构体指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t *pxSemaphoreBuffer );
11.2.2删除
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
11.2.3 give/take
在任务中使用 | 在ISR中使用 | |
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
参数 | 说明 |
xSemaphore | 信号量句柄,释放哪个信号量 |
pxHigherPriorityTaskWoken | 如果释放信号量导致更高优先级的任务变为了就绪态, 则*pxHigherPriorityTaskWoken = pdTRUE |
返回值 | pdTRUE表示成功 如果二进制信号量的计数值已经是1,再次调用此函数则返回失败 如果计数型信号量的计数值已经是最大值,再次调用此函数则返回 |
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
BaseType_t xSemaphoreTakeFromISR(
SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken
);
参数 | 说明 |
xSemaphore | 信号量句柄,获取哪个信号量 |
xTicksToWait | 0:不阻塞,马上返回 portMAX_DELAY: 一直阻塞直到成功 其他值: 阻塞的Tick个数 可以使用*pdMS_TO_TICKS()*来指定阻塞时间为若干ms |
返回值 | pdTRUE表示成功 |
12 互斥量
本章涉及如下内容:
- 为什么要实现互斥操作
- 怎么使用互斥量
- 互斥量导致的优先级反转、优先级继承
12.1互斥量的使用场合
多线程写竞争,避免死锁
12.2互斥量函数
12.2.1创建
在配置文件FreeRTOSConfig.h中定义:
#define configUSE_MUTEXES 1
以使用互斥量。
互斥量是一种特殊的二进制信号量。
互斥量不能在ISR中使用。
/* 创建一个互斥量,返回它的句柄。
* 此函数内部会分配互斥量结构体
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/* 创建一个互斥量,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticSemaphore_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer );
12.2.2其他函数
/*
* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
/* 释放 */
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
/* 获得 */
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
12.3示例:优先级继承
22_mutex_priority_inversion,主要看nwatch\game2.c。
12.4递归锁
12.4.1死锁的概念
假设有2个互斥量M1、M2,2个任务A、B:
- A获得了互斥量M1
- B获得了互斥量M2
- A还要获得互斥量M2才能运行,结果A阻塞
- B还要获得互斥量M1才能运行,结果B阻塞
- A、B都阻塞,再无法释放它们持有的互斥量
- 死锁发生
12.4.2 递归锁函数
| 一般互斥量 | |
创建 | xSemaphoreCreateRecursiveMutex | xSemaphoreCreateMutex |
获得 | xSemaphoreTakeRecursive | xSemaphoreTake |
释放 | xSemaphoreGiveRecursive | xSemaphoreGive |
/* 创建一个递归锁,返回它的句柄。*
* 此函数内部会分配互斥量结构体*
* 返回值: 返回句柄,非NULL表示成功*
*/
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
*/ 释放 */
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xSemaphore );
*/ 获得 */
BaseType_t xSemaphoreTakeRecursive(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
12.5 总结
是否需要控制资源访问数量?
├─ 是 → 计数信号量(设置资源数量上限)
└─ 否 → 是否需要保护共享资源?
├─ 是 → 是否存在任务优先级差异?
│ ├─ 是 → 互斥量(含优先级继承机制)
│ └─ 否 → 二值信号量(轻量级锁)
└─ 否 → 是否需要任务间事件通知?
├─ 是 → 二值信号量(事件标志)
└─ 否 → 无需同步机制
13 事件组
本章涉及如下内容:
- 事件组的概念与操作函数
- 事件组的优缺点
- 怎么设置、等待、清除事件组中的位
- 使用事件组来同步多个任务
13.1事件组概念与操作
13.1.1事件组的概念
事件组本质上是一个整数,其每一位代表一个事件(含义由程序员定义,1 表示事件发生,0 表示未发生),可被多个任务或 ISR 读写,支持等待单 / 多事件。
13.1.2事件组的操作
就一个函数置位,多个函数接受,就完了
13.2事件组函数
13.2.1创建
/* 创建一个事件组,返回它的句柄。
* 此函数内部会分配事件组结构体
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreate( void );
/* 创建一个事件组,返回它的句柄。
* 此函数无需动态分配内存,所以需要先有一个StaticEventGroup_t结构体,并传入它的指针
* 返回值: 返回句柄,非NULL表示成功
*/
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );
13.2.2删除
/*
* xEventGroup: 事件组句柄,你要删除哪个事件组
*/
void vEventGroupDelete( EventGroupHandle_t xEventGroup )
13.2.3设置事件
/* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* 返回值: 返回原来的事件值(没什么意义, 因为很可能已经被其他任务修改了)
*/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
/* 设置事件组中的位
* xEventGroup: 哪个事件组
* uxBitsToSet: 设置哪些位?
* 如果uxBitsToSet的bitX, bitY为1, 那么事件组中的bitX, bitY被设置为1
* 可以用来设置多个位,比如 0x15 就表示设置bit4, bit2, bit0
* pxHigherPriorityTaskWoken: 有没有导致更高优先级的任务进入就绪态? pdTRUE-有, pdFALSE-没有
* 返回值: pdPASS-成功, pdFALSE-失败
*/
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken );
ISR中的函数,比如队列函数xQueueSendToBackFromISR,最多只会唤醒1个任务。
13.2.4等待事件
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
参数 | 说明 |
xEventGroup | 等待哪个事件组? |
uxBitsToWaitFor | 等待哪些位?哪些位要被测试? |
xWaitForAllBits | 怎么测试?是"AND"还是"OR"? pdTRUE: 等待的位,全部为1; pdFALSE: 等待的位,某一个为1即可 |
xClearOnExit | 函数提出前是否要清除事件? pdTRUE: 清除uxBitsToWaitFor指定的位 pdFALSE: 不清除 |
xTicksToWait | 如果期待的事件未发生,阻塞多久。 可以设置为0:判断后即刻返回; 可设置为portMAX_DELAY:一定等到成功才返回; 可以设置为期望的Tick Count,一般用*pdMS_TO_TICKS()*把ms转换为Tick Count |
返回值 | 返回的是事件值, 如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值; 如果是超时退出,返回的是超时时刻的事件值。 |
举例
事件组的值 | uxBitsToWaitFor | xWaitForAllBits | 说明 |
0100 | 0101 | pdTRUE | 任务期望bit0,bit2都为1, 当前值只有bit2满足,任务进入阻塞态; 当事件组中bit0,bit2都为1时退出阻塞态 |
0100 | 0110 | pdFALSE | 任务期望bit0,bit2某一个为1, 当前值满足,所以任务成功退出 |
0100 | 0110 | pdTRUE | 任务期望bit1,bit2都为1, 当前值不满足,任务进入阻塞态; 当事件组中bit1,bit2都为1时退出阻塞态 |
13.2.5同步点
使用 xEventGroupSync() 函数可以同步多个任务:
- 可以设置某位、某些位,表示自己做了什么事
- 可以等待某位、某些位,表示要等等其他任务
- 期望的时间发生后, xEventGroupSync() 才会成功返回。
- xEventGroupSync成功返回后,会清除事件
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );
参数列表如下:
参数 | 说明 |
xEventGroup | 哪个事件组? |
uxBitsToSet | 要设置哪些事件?我完成了哪些事件? 比如0x05(二进制为0101)会导致事件组的bit0,bit2被设置为1 |
uxBitsToWaitFor | 等待那个位、哪些位? 比如0x15(二级制10101),表示要等待bit0,bit2,bit4都为1 |
xTicksToWait | 如果期待的事件未发生,阻塞多久。 可以设置为0:判断后即刻返回; 可设置为portMAX_DELAY:一定等到成功才返回; 可以设置为期望的Tick Count,一般用*pdMS_TO_TICKS()*把ms转换为Tick Count |
返回值 | 返回的是事件值, 如果期待的事件发生了,返回的是"非阻塞条件成立"时的事件值; 如果是超时退出,返回的是超时时刻的事件值。 |
13.3示例:广播
23_eventgroup_broadcast
void car_game(void)
{
int x;
int i, j;
g_framebuffer = LCD_GetFrameBuffer(&g_xres, &g_yres, &g_bpp);
draw_init();
draw_end();
}
//g_xSemTicks = xSemaphoreCreateCounting(1, 1);
//g_xSemTicks = xSemaphoreCreateMutex();
g_xEventCar = xEventGroupCreate();
/* 等待事件:bit0 */
xEventGroupWaitBits(g_xEventCar, (1<<0), pdTRUE, pdFALSE, portMAX_DELAY);
/* 设置事件组: bit0 */
xEventGroupSetBits(g_xEventCar, (1<<0));
vTaskDelete(NULL);
13.4示例:等待任意一个事件
24_eventgroup_or
/* 设置事件组: bit0 */
xEventGroupSetBits(g_xEventCar, (1<<0));
vTaskDelete(NULL);
car2运行到终点后,设置事件,代码如下:
/* 设置事件组: bit1 */
xEventGroupSetBits(g_xEventCar, (1<<1));
car3等待bit0、bit1事件,实验“或”的关系(倒数第2个参数),代码如下:
/* 等待事件:bit0 or bit1 */
xEventGroupWaitBits(g_xEventCar, (1<<0)|(1<<1), pdTRUE, pdFALSE, portMAX_DELAY);
13.5示例:等待多个事件都发生
25_eventgroup_and
/* 等待事件:bit0 or bit1 */
xEventGroupWaitBits(g_xEventCar, (1<<0)|(1<<1), pdTRUE, pdTRUE, portMAX_DELAY);
14 任务通知
本章涉及如下内容:
- 任务通知:通知状态、通知值
- 任务通知的使用场合
- 任务通知的优势
14.1任务通知的特性
任务通知与队列、信号量、事件组等通信方式的区别在于:
- 指向性:任务通知可明确指定通知目标任务,而其他方式不知道对方是谁。 (效率高)
- 中间结构:队列等需事先创建结构体实现双方通信,任务通知无需中间结构体。(节约内存)
14.1.1 优势及限制
- 优势:效率高、省内存(无需额外结构体)。
- 限制:不能发给 ISR、数据独享、无缓冲、无法广播、发送方不阻塞。
14.1.2 通知状态和通知值
- 任务 TCB 成员:
-
- 通知状态(uint8_t):初始为
taskNOT_WAITING_NOTIFICATION
,还可处于等待或已接收状态。 - 通知值(uint32_t):可存储计数、位信号等。
- 通知状态(uint8_t):初始为
14.2 任务通知的使用
- 功能:实现轻量级队列、邮箱、信号量、事件组。
- 函数分类:
-
- 简化版:
xTaskNotifyGive
(任务)、vTaskNotifyGiveFromISR
(中断)、ulTaskNotifyTake
(取通知)。 - 专业版:
xTaskNotify
(发通知,支持位操作 / 覆盖等)、xTaskNotifyWait
(取通知,可清指定位)。
- 简化版:
14.3 示例基本操作
- 任务创建时记录句柄,通过
xTaskNotifyGive
发信号量,xTaskNotify
发数值,接收方用ulTaskNotifyTake
或xTaskNotifyWait
阻塞等待。
19 补充
我给你补个蛋
19.1 数据类型
19.2 看看代码
看不了一点
19.3 引用
- extern 声明:适合单次使用外部函数,直接在当前文件声明,但多个文件使用时需重复声明。
- 头文件引用:适合多次使用同一函数,将声明写在头文件中,通过
#include
引用,避免重复声明。
20 单片机工程师的核心能力
基础能力
GPIO、UART、I2C、SPI:基础能力
RTOS:基础能力
WIFI、Zigbee、Lora、Modbus:业务能力
核心能力
1.对程序的整体理解
2.阅读源码、理解源码的能力
3.调试能力
学习路线
零基础入门
项目路线
完成入门的学习后学习相应的项目
单片机招聘需求