第一个 FreeRTOS 程序:让两个任务分别前进
已经知道任务入口是一个函数。这次创建两个任务,用两个计数变量观察它们各自执行,再解释创建、等待和启动之间的关系。
1 · 先准备一个独立实验工程
FreeRTOS 是负责安排任务及其协作的内核。“内核”在这里就是一组参与编译的 C 文件和与处理器配套的底层代码。它需要匹配当前处理器的任务切换实现、系统节拍中断,以及一份 FreeRTOSConfig.h 配置。
实验使用已集成 FreeRTOS 的独立 STM32 工程。首次准备可使用开发工具为该实验工程加入 FreeRTOS 中间件,并确认系统时钟和中断处理已正确接入。随后采用本章的 FreeRTOS 原生接口。工具可能还提供名为 CMSIS-RTOS 的统一接口层;它用另一套名称包装内核功能。本章选择 xTaskCreate、vTaskDelay 这一套接口,便于和仓库对照。
先检查工程是否已存在任务创建和调度器启动代码。实验应只保留一套启动入口。下面的启动片段替换原实验任务创建及启动部分;不要再接着调用另一套调度器启动函数。手表现有工程中的时钟与外设配置沿用仓库版本。
main.c 的用户头文件区
#include "FreeRTOS.h"
#include "task.h"FreeRTOS.h 给出内核通用类型和配置,task.h 声明任务相关接口。main.h 等板级头文件仍由工程提供;本实验不新增外设驱动。
2 · 先写两个任务入口
先用计数器代替外设,避免同时排查接线和调度。两个计数器分别只有一个任务写入,在调试器中观察,不用串口打印干扰运行。
main.c 文件级变量和函数区 · 放在 main() 外
static volatile uint32_t count_a;
static volatile uint32_t count_b;
static void TaskA(void *argument)
{
(void)argument;
for (;;) {
count_a++;
vTaskDelay(pdMS_TO_TICKS(500));
}
}
static void TaskB(void *argument)
{
(void)argument;
for (;;) {
count_b++;
vTaskDelay(pdMS_TO_TICKS(200));
}
}每个循环先把自己的计数器加一,再等待一段时间。pdMS_TO_TICKS(500) 把毫秒换成系统节拍数,vTaskDelay() 让当前任务进入定时等待。暂时记住:这里等待的是当前任务,其他符合运行条件的任务仍能执行。下一篇会拆开它的时间关系。
volatile 让调试器观察计数器时,编译器仍保留相关内存访问;本例不靠它解决多个任务同时修改同一数据的问题。
3 · 把入口交给系统,再启动调度器
把下面代码放在 main() 的硬件初始化之后、原来的裸机无限循环之前。Error_Handler() 是工程已有的错误处理入口。
main() 内 · 外设初始化后,只执行这一套任务启动代码
BaseType_t result;
result = xTaskCreate(TaskA, "TaskA", 256, NULL, 1, NULL);
if (result != pdPASS) Error_Handler();
result = xTaskCreate(TaskB, "TaskB", 256, NULL, 1, NULL);
if (result != pdPASS) Error_Handler();
vTaskStartScheduler();
Error_Handler();xTaskCreate() 会为任务分配管理信息和栈,成功返回 pdPASS。任务创建成功后,vTaskStartScheduler() 启动调度。正常运行时,后面的裸机 while (1) 不再负责轮询应用工作。若启动函数返回,应进入错误处理。
本例任务一直循环;某项任务如果确实完成了全部工作,要调用 vTaskDelete(NULL) 删除自己,不能直接从任务入口返回。这里 NULL 表示当前任务;删除任务不会代替应用归还其另行申请的数据缓冲。
| 参数 | 本例中的值 | 它表示什么 |
|---|---|---|
| 入口 | TaskA | 保存入口地址;此处不写 TaskA()。 |
| 名称 | "TaskA" | 用于调试查看任务,不是靠这个名字寻找入口。 |
| 栈深度 | 256 | 256 个 StackType_t 元素;当前 STM32U5 端口每个 4 字节,共 1024 字节。 |
| 入口参数 | NULL | 本例不传初始对象。 |
| 任务优先级 | 1 | 任务之间选择运行顺序的依据;下一篇解释。 |
| 输出句柄 | NULL | 暂时不保存任务的标识。需要通知指定任务时再保存。 |
句柄是内核返回、用来指明某个对象的标识,像“请操作这个任务”的凭据。这里不用句柄,所以最后一个参数传 NULL。栈深度 256 仅用于这个很小的计数实验,不能直接套到传感器算法或界面任务。
4 · 参数怎样交给任务?
如果希望一份代码用不同间隔执行,就需要传参数。先把第一项任务改成从参数读取间隔:
替换 TaskA,并在文件级保存参数
static uint32_t task_a_interval = 500;
static void TaskA(void *argument)
{
uint32_t interval = *(uint32_t *)argument;
for (;;) {
count_a++;
vTaskDelay(pdMS_TO_TICKS(interval));
}
}同时替换对应的创建语句,不能仍传 NULL
result = xTaskCreate(TaskA, "TaskA", 256,
&task_a_interval, 1, NULL);
if (result != pdPASS) Error_Handler();任务入口收到的是同一个对象的地址,并不是系统自动复制一份整数。入口先读取间隔到局部变量 interval;文件级的 task_a_interval 始终存在。这里也能看出前置篇的函数指针和 void * 为什么有用。
5 · 怎样确认实验真的运行了?
运行后暂停调试,查看 count_a 与 count_b;继续运行一段时间再暂停。两者应持续增加,后者通常增长更快。它们刚启动时都会先加一次,所以短时间观察不必恰好是 2.5 倍。断点暂停也会改变运行时间,不能拿单次暂停数据当作精确调度测量。
如果两个值都是零,先看是否停在错误处理、是否启动调度器、内核节拍是否接入。只有一个增加,则检查另一任务是否创建成功、是否进入断言。不要先把所有任务优先级和栈一起调大。
两个任务有独立循环;创建失败能发现;调度器已经启动。下一篇再修改等待方式和优先级,观察为什么某个任务可能得不到执行。
资料与源码
本章按仓库当前 FreeRTOS V11.2.0 与 STM32U5 端口核对接口。图解描述执行关系,任务耗时和硬件行为需在实际工程观察。