FreeRTOS 学习笔记一:使用 CubeMX 创建两个任务
一、简述
FreeRTOS 可以理解为一个运行在单片机上的实时操作系统内核。它本身并不负责帮我们完成具体业务逻辑,而是负责管理多个任务,让这些任务按照一定规则使用 CPU。
FreeRTOS 的主要作用有三个:
任务调度
资源管理
任务通信
简单来说:
FreeRTOS 像一个“管理者”,负责安排任务的运行顺序。
每个任务具体做什么,仍然由我们自己写代码决定。
FreeRTOS 不会修改任务代码,它只是在需要切换任务时保存当前任务的运行现场,然后恢复另一个任务的运行现场。
二、创建两个任务
本次使用 CubeMX 生成 STM32 工程,并通过 CubeMX 配置 FreeRTOS。这样可以少写一部分底层初始化代码,更适合初学者先理解任务创建和任务调度。
本节目标:
创建两个 FreeRTOS 任务:任务 1:控制 PA0 反转电平任务 2:控制 PA1 反转电平
如果 PA0 和 PA1 外接 LED,就可以看到两个 LED 以不同频率闪烁。
(一)CubeMX 相关配置
1. 选择芯片
在 CubeMX 中选择 STM32F103C8T6 这款单片机。实际学习 FreeRTOS 时,也可以选择其他 STM32 芯片,只要支持 FreeRTOS 并且工程能够正常编译即可。
2. 配置调试方式
选择 Serial Wire,也就是 SWD 调试方式。
SWD 通常使用两根主要调试线:
这种方式可以配合 ST-Link 或 J-Link 进行程序下载和在线调试,是 STM32 中最常用的调试方式。
3. 选择 TIM4 作为 HAL 时间基准
在使用 FreeRTOS 时,通常不建议继续让 HAL 使用 SysTick 作为时间基准,因为 FreeRTOS 自己也需要使用系统节拍。
所以这里可以选择 TIM4 作为 HAL 的 Timebase Source。这样可以减少 HAL 延时和 FreeRTOS 系统节拍之间的冲突。
4. 配置外部时钟
根据开发板实际情况配置高速外部时钟 HSE。如果板子上有低速外部晶振,也可以配置 LSE。
5. 配置系统时钟为 72 MHz
STM32F103C8T6 常见配置是使用 8 MHz 外部晶振,然后通过 PLL 倍频到 72 MHz。
本次选择 PA0 和 PA1 作为普通推挽输出。
PA0 -> GPIO_OutputPA1 -> GPIO_Output
这两个引脚可以用于外接 LED,后续在两个不同的任务中分别控制它们反转电平。
7. 配置 FreeRTOS
在 Middleware 中启用 FreeRTOS,并选择 CMSIS_V2 接口。
CubeMX 会自动生成 FreeRTOS 相关文件,不需要自己手动从官网下载 FreeRTOS 源码。没有下载对应软件包时,CubeMX 会提示下载,按照提示下载即可。
本节只学习任务创建,所以重点关注 Tasks and Queues 这一栏。
第一个任务通常会在启用 FreeRTOS 时自动生成,例如:
第二个任务需要点击 Add 手动添加。
8. 配置任务优先级
双击任务后,可以看到任务的配置界面,其中比较重要的是 Priority。
例如:
任务 1:osPriorityNormal任务 2:osPriorityLow
这表示任务 1 的优先级高于任务 2。
这里要注意:
优先级不是任务的创建顺序,也不是固定的执行顺序。
它的含义是:
当多个任务都处于就绪态,且都可以运行时,FreeRTOS 会优先运行优先级更高的任务。
也就是说,高优先级任务只要处于就绪态,就会优先获得 CPU。
9. 配置工程生成方式
设置项目名称,IDE 选择 MDK-ARM,后续可以使用 Keil 打开工程。
在 Code Generator 中,可以选择只复制必须的库文件,这样工程体积更小,编译负担也更低。
三、代码讲解
1. 查看任务配置
生成工程后,打开:
在这个文件中可以看到 CubeMX 为我们生成的任务句柄、任务属性以及任务创建代码。
任务属性一般包括:
例如:
const osThreadAttr_t defaultTask_attributes = { .name = "defaultTask", .stack_size = 128 * 4, .priority = (osPriority_t) osPriorityNormal,};
其中:
表示任务名称。
表示任务栈大小。这里是 512 字节。
.priority = osPriorityNormal
表示任务优先级。
任务创建代码一般类似:
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
这句话的作用是创建一个任务,任务入口函数是 StartDefaultTask。
2. 在两个任务中分别控制 PA0 和 PA1
两个任务中可以分别写 GPIO 反转代码。
任务 1 示例:
voidStartDefaultTask(void *argument){ for(;;) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); osDelay(500); }}
任务 2 示例:
voidStartTask02(void *argument){ for(;;) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1); osDelay(1000); }}
这里需要理解两个关键点:
第一,HAL_GPIO_TogglePin() 是反转电平
例如:
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
表示反转 PA0 的输出电平。
如果 PA0 原来是低电平,执行后变成高电平;如果原来是高电平,执行后变成低电平。
所以它不是“点亮一次 LED”,而是“改变一次电平状态”。
第二,osDelay() 不是普通裸机延时
例如:
它的意思不是让整个系统停止 500 ms,而是让当前任务进入阻塞态 500 ms。
任务进入阻塞态后,暂时不参与 CPU 竞争。此时 FreeRTOS 调度器会去运行其他处于就绪态的任务。
四、任务优先级和任务切换过程
假设有两个任务:
任务 1:osPriorityNormal,控制 PA0任务 2:osPriorityLow,控制 PA1
由于 osPriorityNormal 高于 osPriorityLow,所以当两个任务都处于就绪态时,FreeRTOS 会优先运行任务 1。
但是任务 1 执行到:
之后,任务 1 会进入阻塞态。在这 500 ms 内,任务 1 不再占用 CPU,也不会继续参与调度。
此时,如果任务 2 处于就绪态,调度器就会切换到任务 2 运行。
任务 2 运行后也会执行:
然后任务 2 也进入阻塞态。
如果此时系统中没有其他用户任务可以运行,FreeRTOS 会运行空闲任务,也就是 Idle Task。
当任务 1 的 500 ms 延时时间到了之后,任务 1 会从阻塞态重新变成就绪态。由于任务 1 的优先级高于任务 2,所以只要任务 1 重新就绪,调度器就会优先切换回任务 1。
这里最容易写错的一点是:
任务 2 被高优先级任务打断时,并不是被停止、删除或重新开始。
更准确地说:
任务 2 的运行现场会被保存。等高优先级任务再次进入阻塞态后,任务 2 可以从之前被打断的位置继续运行。
所以,不能理解成:
而应该理解成:
哪个任务处于就绪态,并且优先级最高,调度器就运行哪个任务。
五、两个任务的大致运行过程
以任务 1 延时 500 ms、任务 2 延时 1000 ms 为例,可以这样理解:
任务 1 运行,反转 PA0 ↓任务 1 执行 osDelay(500),进入阻塞态 ↓任务 2 运行,反转 PA1 ↓任务 2 执行 osDelay(1000),进入阻塞态 ↓两个任务都在阻塞时,系统运行 Idle Task ↓500 ms 到,任务 1 重新进入就绪态 ↓任务 1 优先级更高,调度器运行任务 1 ↓任务 1 再次反转 PA0,然后又执行 osDelay(500) ↓调度器继续根据任务状态和优先级切换任务
最终看到的现象是:
PA0 对应的 LED 闪烁较快PA1 对应的 LED 闪烁较慢
如果 PA0 每 500 ms 反转一次,那么完整亮灭周期大约是 1 秒。
如果 PA1 每 1000 ms 反转一次,那么完整亮灭周期大约是 2 秒。
六、本节小结
本节通过两个 LED 闪烁任务,初步理解了 FreeRTOS 的任务调度。
需要重点记住:
FreeRTOS 任务通常写成 for(;;) 无限循环。
任务优先级不是任务创建顺序,而是任务竞争 CPU 时的调度依据。
当多个任务都处于就绪态时,高优先级任务优先运行。
osDelay() 只会阻塞当前任务,不会让整个系统停止。
低优先级任务被高优先级任务抢占时,不是被停止,而是保存上下文,后面可以继续运行。
如果所有用户任务都处于阻塞态,FreeRTOS 会运行空闲任务 Idle Task。
对于 for(;;) 形式的任务,任务通常不会真正“结束”,而是在运行态、就绪态、阻塞态之间不断切换。
这一节最重要的理解是:
FreeRTOS 不是按照代码书写顺序依次执行任务,而是根据任务状态和任务优先级来决定当前运行哪个任务。