RTOS 实时操作系统

MOSI/MISO loopback

MOSI(Master Out Slave In)和 MISO(Master In Slave Out)是 SPI 总线的两条数据线。
Loopback 指的是将 MOSI 和 MISO 物理上短接(或在软件中模拟),这样主机发送的数据会直接回到主机的接收端。
在没有真实 SPI 从设备的情况下,可以用 loopback 测试 SPI 主机的收发流程、FIFO、回调等功能是否正常。

任务优先级设置

#define TASK_PRIORITY_PAYLOAD     (tskIDLE_PRIORITY + 4)
#define TASK_PRIORITY_HEARTBEAT (tskIDLE_PRIORITY + 1)
/* ... */
int userTaskStack = TASK_PRIORITY_PAYLOAD;
TaskHandle_t userTask = xTaskCreateStatic(
&user_task_entry, // task proc
"tcpip", // task name
LOCAL_STACK_SIZE, // stack depth
NULL, // task parameters
params->task_priority, // priority
userTaskStack,
&user_task_control);
(void) userTask; // 消除未使用变量警告的常用写法。

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 开始引入,并在后续版本中大幅增强(支持每个任务拥有一个或多个通知值)

原理:每个任务在创建时,内核都会为其预留一个“任务通知”数组(现代版本支持多路通知)。发送方可以直接向目标任务发送通知、修改其通知值或状态。
特点:

  1. 速度极快:比通过队列或信号量传递数据快得多,且不需要单独创建队列或信号量对象(省去了内存动态申请的开销)。
  2. 一对一限制:任务通知是直接发给特定任务的(必须知道目标任务的句柄)。因此它适用于“单任务接收,多任务/中断发送”的场景,无法实现“多个任务等待同一个信号量”的广播效果。
    常用 API:
    发送:xTaskNotify() / xTaskNotifyGive() / vTaskNotifyGiveFromISR()
    接收:xTaskNotifyWait() / ulTaskNotifyTake()

ulTaskNotifyTake

在 FreeRTOS 中,ulTaskNotifyTake() 是一个非常高效且常用的任务通知接收函数,常用于替代传统的二值信号量、计数信号量或事件标志组

函数原型:

/*  xClearCountOnExit : 1 -> 当任务成功获取到通知并退出函数时,会将任务的通知值清零(类似于二值信号量的“自动重置”机制)
0 -> 退出时不会将通知值清零,而是将通知值减 1(类似于计数信号量)
xTicksToWait: 阻塞超时时间。传入 0U 表示非阻塞(立即返回)
如果当前没有收到通知,函数会立刻返回 0,而不会让当前任务进入阻塞状态去等待
*/
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );

二值信号量(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

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() 会关闭中断或调度器,所以在编写临界区代码时必须遵守以下铁律:

  1. 代码必须极其简短:只把涉及共享资源访问的那几行核心代码包进去,切忌把大段逻辑、延时、复杂的数学运算放进临界区。
  2. 绝对不能调用会导致阻塞(Blocked)或挂起的 API:
    • 禁止调用:vTaskDelay()、xQueueReceive()(带阻塞时间)、vTaskSuspend() 等。
    • 后果:如果任务在临界区内进入阻塞状态,整个操作系统的调度器和中断都被关了,系统将直接死锁(Deadlock)或彻底瘫痪。
  3. 避免在临界区内进行耗时的外设操作:例如通过慢速的软件模拟 I2C 读写、printf 打印调试等(串口打印会消耗大量时间,导致系统实时性崩塌)。