SMARTWATCH 学习手册有 C 基础 · 嵌入式从零开始
第二章 / FreeRTOS / 第 1 节

第一个 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"用于调试查看任务,不是靠这个名字寻找入口。
栈深度256256 个 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 端口核对接口。图解描述执行关系,任务耗时和硬件行为需在实际工程观察。