FreeRTOS学习笔记 · 第二讲:建立基础工程

使用 CUBE MX 初始化工程

这里无法用文字叙述,建议看一遍原视频,尤其是FreeRTOS配置部分。


我将基础工程命名为“00BASE”,00 表示这是第 001 号项目,BASE 为项目名称表示这是一个基础工程。

这是因为我选择的编译环境是kile,生成的代码使用标准C++库,而不是newlib库,所以找不到专门为newlib库优化的reent文件。

这里介绍一下这个文件是干嘛的:

reent.h 头文件是 Newlib C 标准库的关键部分。Newlib 是一个专为嵌入式系统设计的轻量级 C 库。在像 FreeRTOS 这样的多任务实时操作系统 (RTOS) 中,为了确保多个任务可以安全地调用同一个 C 库函数(如 sprintf, malloc 等)而不会导致数据冲突,就需要实现“可重入性”(Reentrancy)。reent.h 便是实现这种线程安全机制的核心。

不过我们也不用担心取消该选项后的安全问题,FreeRTOS 的官方移植文件中,通常会包含一个文件(有时是 syscalls.c 或在 portable 目录下),它使用 FreeRTOS 的互斥锁(Mutex)或调度器锁(Scheduler Lock)重新实现了这些安全机制。

熟悉代码

程序入口

解决所有问题后,我们可以看到项目在core/src里有一个freertos.c的文件,我们将在这个文件里创建任务、信号量以及一系列其它东西。

回到main.c,在第101 行(如果你没初始化别的外设的话)调用了一个叫osKernelStart()的函数;这个函数的作用是启动rtos的任务调度器。它的下面还有这样一句注释:

C
/* 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()

下面我们来看看程序时如何创建新任务的。

查看函数原型:

C
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 里,这个头文件是每个文件都会调用的,这样就可以避免反复调用。

  1. 用两位数是为了防止出现12号排在2号前面这种情况 ↩︎

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注


目录