RTOS System
RTOS 实时操作系统
MOSI/MISO loopback
MOSI(Master Out Slave In)和 MISO(Master In Slave Out)是 SPI 总线的两条数据线。
Loopback 指的是将 MOSI 和 MISO 物理上短接(或在软件中模拟),这样主机发送的数据会直接回到主机的接收端。
在没有真实 SPI 从设备的情况下,可以用 loopback 测试 SPI 主机的收发流程、FIFO、回调等功能是否正常。
任务优先级设置
|
tskIDLE_PRIORITY 是 FreeRTOS 的空闲任务优先级,值为 0。TASK_PRIORITY_PAYLOAD 设为 4,比空闲任务高 4 级,表示 payload 任务优先级较高,能更快被调度执行。TASK_PRIORITY_HEARTBEAT 设为 1,比空闲任务高 1 级,表示 heartbeat 任务优先级较低,但仍高于空闲任务。
栈块大小
一个 stack block(栈块)以 StackType_t 类型为单位分配。FreeRTOS 的栈以 StackType_t 类型为单位分配,而在 ARM Cortex-M 等平台上,StackType_t 通常定义为 uint32_t,即 4 字节。
模块通信机制
任务通知(Task Notifications)—— 最轻量、最高效
从 FreeRTOS v8.20 开始引入,并在后续版本中大幅增强(支持每个任务拥有一个或多个通知值)
原理:每个任务在创建时,内核都会为其预留一个“任务通知”数组(现代版本支持多路通知)。发送方可以直接向目标任务发送通知、修改其通知值或状态。
特点:
- 速度极快:比通过队列或信号量传递数据快得多,且不需要单独创建队列或信号量对象(省去了内存动态申请的开销)。
- 一对一限制:任务通知是直接发给特定任务的(必须知道目标任务的句柄)。因此它适用于“单任务接收,多任务/中断发送”的场景,无法实现“多个任务等待同一个信号量”的广播效果。
常用 API:
发送:xTaskNotify() / xTaskNotifyGive() / vTaskNotifyGiveFromISR()
接收:xTaskNotifyWait() / ulTaskNotifyTake()
ulTaskNotifyTake
在 FreeRTOS 中,ulTaskNotifyTake() 是一个非常高效且常用的任务通知接收函数,常用于替代传统的二值信号量、计数信号量或事件标志组
函数原型:
/* xClearCountOnExit : 1 -> 当任务成功获取到通知并退出函数时,会将任务的通知值清零(类似于二值信号量的“自动重置”机制) |
二值信号量(Binary Semaphores)—— 简单的事件触发
原理:本质是一个长度为 1 的队列。其值只有 0 和 1 两种状态。
特点:用于实现二进制的事件同步(例如:中断发生后释放信号量,任务获取信号量开始处理)。支持多任务同时等待同一个信号量。
常用 API:xSemaphoreCreateBinary()、xSemaphoreTake()、xSemaphoreGiveFromISR()
计数信号量(Counting Semaphores)—— 资源计数与限流
原理:类似于二值信号量,但其计数值可以大于 1。
特点:常用于管理有限数量的共享资源(例如:资源池、可用的缓冲区个数),或者记录某个事件发生的次数。
常用 API:xSemaphoreCreateCounting()
互斥量(Mutexes)—— 独占式临界资源保护
原理:一种特殊的二值信号量,带有优先级继承机制(Priority Inheritance)。
特点:专用于解决多任务并发访问共享资源时的优先级翻转(Priority Inversion)问题。只有获取了互斥量的任务才有资格释放它。
常用 API:xSemaphoreCreateMutex()、xSemaphoreTake()、xSemaphoreGive()
队列(Queues)—— 安全的数据传递
原理:先进先出(FIFO)的数据缓冲区,支持在任务与任务、任务与中断之间安全地拷贝真实数据。
特点:不仅能用于同步,还能用于传递具体的数据包、结构体或指针。
常用 API:xQueueCreate()、xQueueSend()、xQueueReceive()
事件标志组(Event Groups)—— 多条件组合同步
原理:使用一组二进制位(Flags)来代表多个独立的事件。
特点:支持“与(AND)”、“或(OR)”逻辑组合等待。例如:“必须同时满足事件 A 发生 AND 事件 B 发生,任务才被唤醒”。
常用 API:xEventGroupCreate()、xEventGroupSetBits()、xEventGroupWaitBits()
核心机制对比与选型
| 同步机制 | 是否能传递数据 | 资源开销 | 支持的接收方 | 主要应用场景 |
|---|---|---|---|---|
| 任务通知 | 是(可传32位整数/指针) | 极低(每个任务自带) | 仅限指定单任务 | 高性能的中断到任务同步、简单的状态传递 |
| 二值/计数信号量 | 否(仅传状态) | 较低(需创建对象) | 多个任务均可等待 | 外设中断同步、资源计数、简单事件通知 |
| 互斥量 | 否 | 较低(需创建对象) | 独占式 | 多任务并发访问临界资源、防优先级翻转 |
| 队列 | 是(深度拷贝数据) | 较高(依队列大小而定) | ,多个任务均可读取 | 任务间跨线程数据流传输、协议解析 |
| 事件标志组 | 否(仅标志位组合) | 较高(需创建对象) | 多个任务均可等待 | 多事件协同、多状态联合触发点 |
interrupt
操作临界区
taskENTER_CRITICAL()
工作机制
在 FreeRTOS 中,taskENTER_CRITICAL() 并不是一个普通的 C 语言函数,而是一个宏(Macro)。它的定义通常在 task.h 或底层架构相关的移植文件(如 portmacro.h)中。
以最常见的 ARM Cortex-M 架构(使用 FreeRTOS 移植层)为例,它的核心逻辑如下:
关闭任务调度器(针对抢占式调度):
确保在此期间不会发生任务切换(Context Switch)。
屏蔽中断:
通过修改处理器的中断屏蔽寄存器(例如 Cortex-M 的 BASEPRI 寄存器),将特定优先级以下的中断全部屏蔽。
注意:它不会屏蔽具有最高优先级的硬实时中断(由 configMAX_SYSCALL_INTERRUPT_PRIORITY 界定),以保证系统的最高实时响应能力。
核心特性:支持嵌套(Nested)
FreeRTOS 的临界区机制设计得非常健壮,支持嵌套调用。这意味着你可以在函数 A 中调用一次 taskENTER_CRITICAL(),而在函数 A 内部又调用了函数 B,而函数 B 内部也包含临界区。
它是如何实现的?
底层通常维护了一个嵌套计数器(例如 uxCriticalNesting)。
每调用一次 taskENTER_CRITICAL(),嵌套计数器加 1。
每调用一次 taskEXIT_CRITICAL(),嵌套计数器减 1。
只有当嵌套计数器减到 0 时,系统才会真正恢复中断和调度器。
好处:这极大地简化了模块化编程。底层的驱动函数和上层的业务逻辑都可以安全地加锁,而不必担心某一层过早释放中断。
注意事项
因为 taskENTER_CRITICAL() 会关闭中断或调度器,所以在编写临界区代码时必须遵守以下铁律:
- 代码必须极其简短:只把涉及共享资源访问的那几行核心代码包进去,切忌把大段逻辑、延时、复杂的数学运算放进临界区。
- 绝对不能调用会导致阻塞(Blocked)或挂起的 API:
- 禁止调用:vTaskDelay()、xQueueReceive()(带阻塞时间)、vTaskSuspend() 等。
- 后果:如果任务在临界区内进入阻塞状态,整个操作系统的调度器和中断都被关了,系统将直接死锁(Deadlock)或彻底瘫痪。
- 避免在临界区内进行耗时的外设操作:例如通过慢速的软件模拟 I2C 读写、printf 打印调试等(串口打印会消耗大量时间,导致系统实时性崩塌)。

