使用 CUBE MX 初始化工程
这里无法用文字叙述,建议看一遍原视频,尤其是FreeRTOS配置部分。
我将基础工程命名为“00BASE”,00 表示这是第 001 号项目,BASE 为项目名称表示这是一个基础工程。
不过在这里我遇到了一个坑:在FREERTOS->Advanced settings->USE_NEWLIB_REENTRANT选择为enable后,编译项目会报错声称找不到“reent.h”这个文件。
这是因为我选择的编译环境是kile,生成的代码使用标准C++库,而不是newlib库,所以找不到专门为newlib库优化的reent文件。
这里介绍一下这个文件是干嘛的:
reent.h 头文件是 Newlib C 标准库的关键部分。Newlib 是一个专为嵌入式系统设计的轻量级 C 库。在像 FreeRTOS 这样的多任务实时操作系统 (RTOS) 中,为了确保多个任务可以安全地调用同一个 C 库函数(如 sprintf, malloc 等)而不会导致数据冲突,就需要实现“可重入性”(Reentrancy)。reent.h 便是实现这种线程安全机制的核心。
换句话说,reent负责实现调用非原子操作函数(如printf)时的线程安全。
不过我们也不用担心取消该选项后的安全问题,FreeRTOS 的官方移植文件中,通常会包含一个文件(有时是 syscalls.c 或在 portable 目录下),它使用 FreeRTOS 的互斥锁(Mutex)或调度器锁(Scheduler Lock)重新实现了这些安全机制。
熟悉代码
程序入口
解决所有问题后,我们可以看到项目在core/src里有一个freertos.c的文件,我们将在这个文件里创建任务、信号量以及一系列其它东西。
回到main.c,在第101 行(如果你没初始化别的外设的话)调用了一个叫osKernelStart()的函数;这个函数的作用是启动rtos的任务调度器。它的下面还有这样一句注释:
/* We should never get here as control is now taken by the scheduler */
也就是说,这句话下面的所有内容,包括while循环都不会进入,因为系统接下来已经是任务调度器在控制了。(简单来说可以理解为任务调度器里也有一个循环,程序一直在那个循环里运行不会退出)
系统初始化: MX_FREERTOS_Init()
我们再来看看上面的MX_FREERTOS_Init()函数,按住 Ctrl 单击函数名,可以跳转到函数声明文件:

freertos.c 文件没有对应的头文件,所有函数的声明都在该文件里完成
我们从头看这个文件。

在第 51 行,代码定义了一个 osThreadId_t 类型的变量,而 osThreadId_t 通过查看声明可知其本质为 void* 类型。
紧接着,程序又定义了同类型的结构体 defaultTask_attributes,并在第 95 行初始化系统时将该任务通过函数赋值给defaultTaskHandle 句柄。

为什么要定义这些东西?
RTOS 系统依靠任务运行,没有任务就谈不上运行程序。所以代码初始化时自动给我们初始化了一个默认任务并在 Init 函数里运行它。
创建新任务: osThreadNew()
下面我们来看看程序时如何创建新任务的。
查看函数原型:
osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr)它有三个参数:第一个是任务名称,第二个是想任务函数里传递的参数,第三个是任务属性结构体指针。
先是第一个参数:osThreadFunc_t func:

查看定义可知,它是一个普通的函数指针,规定了任务函数的调用方式。
关于函数指针,可以查看【C/C++编程技术】十分钟搞懂指针函数和函数指针!
第二个参数是传入第一个参数的参数(怎么这句话这么绕)。
第三个是任务属性, 这是 CMSIS-RTOS 预定义的结构体,专门用于存储线程的属性。通过这种方式,可以将任务的配置信息与任务的逻辑代码解耦。
| 字段 | 含义 | 说明 |
.name | 任务名称 | 字符串格式,主要用于调试(Debug)时识别任务,不影响运行逻辑。 |
.stack_size | 栈空间大小 | 这里的 128 * 4 表示分配 512 字节。在 32 位架构(如 ARM Cortex-M)中,栈通常以字(Word)为单位,乘 4 是为了将其转换为字节数。 |
.priority | 任务优先级 | 使用 osPriorityNormal 表示普通优先级。强制类型转换 (osPriority_t) 是为了确保枚举类型匹配。 |
想要创建更多任务,可以在 Cube MX 里设置。
更多属性设置
1. 基础识别与调度属性
const char *name
- 作用:任务的名称字符串。
- 应用:主要用于 RTOS 感知型调试器(如 Ozone, Keil Event Recorder)或日志打印。它不参与逻辑运算,但能显著提高调试效率。
osPriority_t priority
- 作用:任务的初始优先级。
- 应用:数值越大,优先级越高。RTOS 调度器根据此值决定 CPU 的使用权。如果未指定,通常默认为
osPriorityNormal。
2. 控制位与内存管理
uint32_t attr_bits
- 作用:属性标志位。
- 应用:用于设置任务的特殊行为。例如:
osThreadDetached:任务运行完后自动释放资源。osThreadJoinable:任务退出后需被其他任务回收。void *cb_mem&uint32_t cb_size
- 作用:控制块(Control Block)的内存指针与大小。
- 应用:静态内存分配的关键。如果你不希望从动态堆(Heap)中分配任务管理所需的 TCB 空间,可以预先定义一个静态数组,并将指针和大小传给这两个成员。如果为
NULL,则系统自动从堆分配。
3. 栈空间配置 (Stack)
void *stack_mem
- 作用:任务栈的内存起始地址。
- 应用:同样用于静态分配。如果手动管理内存,可将此项指向一个全局数组。如果为
NULL,则由 RTOS 自动分配。 uint32_t stack_size
- 作用:栈的大小(以字节为单位)。
- 应用:存放局部变量、函数调用记录、寄存器上下文。注意:在 ARM 环境下,栈通常需要 8 字节对齐。
4. 高级与保留字段
TZ_ModuleId_t tz_module
- 作用:TrustZone 模块标识符。
- 应用:在支持 ARM TrustZone(如 Cortex-M33)的安全架构中使用。用于指定该任务是否允许调用非安全区域(Non-secure)的代码或资源。
uint32_t reserved
注意:必须设置为 0。这是为了向后兼容未来版本的 API 扩展。
作用:保留字段。
任务函数
每一个任务函数就是包含一个初始化结构和死循环的普通函数。

这一部分也可以在 Cube MX 被设置成“as week”,即生成的函数为弱函数,用户可以在其它地方写真正的任务函数。

up 主比较推荐这种写法,因为 freertos.c 文件会被 cube mx 更新,此时你写在这个文件里的中文注释都会变成乱码……(可恶,为什么不支持 UTF 编码啊!)
其它生成选项…
内存分配方式

可选静态(static)和动态(dynamic),默认为动态。选择静态后,后两项可以设置创建的缓冲区和控制块的变量名:

(要麻烦一点啊…)
除了任务属性不同外,osThreadNew 函数对静态任务的创建过程也不同:


可以看到当属性配置了 cb_mem 等属性后,程序会以静态方式去创建任务(xTaskCreateStatic),否则以动态方式创建(xTaskCreate)。
编写任务函数
将原本用于测试的 while 循环内部的代码复制到任务函数里:

注意将 HAL_Delay 删掉,改用 osDelay。

osDelay 会调用 vTaskDelay,这个函数被调用后会交出 CPU 的控制权,让认为调度器吧 CPU 调度给其它任务。
而 HAL_Delay 会阻塞程序。因此在任务中使用这类语句是禁止的。
hint:可以将任务依赖的头文件添加到 main.h 里,这个头文件是每个文件都会调用的,这样就可以避免反复调用。
- 用两位数是为了防止出现12号排在2号前面这种情况 ↩︎


发表回复