文章目录
- 前言概述
- 任务通知替代二值信号量
- 其他任务间通信方式(了解)
前言概述
在前面的内容中,我们已经学习了 FreeRTOS 中两种常见的任务间通信机制:
- 消息队列(Queue)
- 信号量(Semaphore)
其中:
- 消息队列 主要用于 任务与任务、任务与中断之间传递数据
- 信号量 主要用于 任务同步以及资源管理
消息队列的作用几乎是 不可替代的。
如果需要在 任务与任务 ,或者 任务与中断之间传递数据 ,那么消息队列通常都是 首选机制。
但在实际应用中,很多时候并不需要传递数据。
例如:
- 中断通知任务
- 一个任务唤醒另一个任务
- 某个事件发生后通知任务继续执行
这种情况,本质上只是 发送通知和接收通知。
在前面的学习中,我们已经知道:二值信号量 就可以完成这种任务同步工作。
不过,如果是 向单一任务发送通知 的场景,使用消息队列或者信号量,其实显得有些 "重量级"。
因此,FreeRTOS 又提供了一种 更加轻量级的通信机制 :任务通知(Task Notification)
任务通知可以理解为:二值信号量在面向单个任务发通知时,一种更加轻量、更高效的实现方式。
接下来,我们就来学习 FreeRTOS 的 任务通知机制。
任务通知替代二值信号量
任务通知的作用实际上有很多,下面会慢慢介绍。
但任务通知最有价值,最常见的用途就是:
在只有一个任务等待事件的情况下,任务通知可以取代二值信号量,而且效率会更高。
我们先来看一下官网对任务通知的介绍:
相关官方文档链接:FreeRTOS 直达任务通知 - FreeRTOS™
相关内容,如下图所示:

在 FreeRTOS 官方文档中有明确说明,相比使用二值信号量,任务通知有两个明显优势:
- 第一,执行效率更高
- 第二,系统开销更小,SRAM占用更少
换句话说:
如果只是为了完成 "通知某一个任务发生了一件事情" 这样的操作,那么使用任务通知往往会比二值信号量更加高效。
任务通知是一种较晚(大约2015年)才引入FreeRTOS的任务间通信机制。
可以说:在上面的场景中,任务通知可以视为二值信号量的升级版本。
下面来介绍一下FreeRTOS的任务通知机制,先来了解一下它的工作原理。
注意事项
在正式学习 使用任务通知替代二值信号量 之前,我们需要先说明一个客观的事实:
任务通知并不是二值信号量的完全替代方案。
更准确地说:
任务通知是在"只有一个任务等待通知"的情况下,对二值信号量的一种性能优化实现。
二值信号量可以做到:
同一个事件同时通知多个任务。
使用二值信号量,只要一个任务想要接收通知,那么它都可以调用:
c
xSemaphoreTake(BinarySem, portMAX_DELAY);
多个任务,都可以同时等待一个通知,等待同一个二值信号量的释放。
而任务通知则不同:
任务通知发送的通知,是直接面向某个任务的直接通知,不能一次性通知多个任务!(官方叫它直接任务通知)
不过在实际嵌入式系统中:
同一个事件同时唤醒多个任务的场景其实非常少见。
因此在多数情况下,任务通知都可以作为 二值信号量的轻量级替代方案 来使用。
也就是说:
如果后面你听到有人说"任务通知是二值信号量的升级版",你心里要清楚,这句话是"对也不对"。
任务通知的工作原理
在上面的描述中,我们知道:
任务通知相比较于二值信号量,有 执行效率更高、资源开销更低 的优势,可谓是"全方面遥遥领先"。
那为什么任务通知有这样的全面优势呢?
其实原因非常简单:
任务通知并不依赖一个独立的内核对象。
我们前面学习消息队列、信号量的时候,都有一个共同特点:
这些机制在使用之前,都需要 先创建对象。
例如:
- 创建消息队列需要调用
xQueueCreate() - 创建二值信号量需要调用
xSemaphoreCreateBinary()
也就是说,这些通信机制本质上都需要 额外创建一个内核对象。
而这些对象在 FreeRTOS 内部,其实都是通过 消息队列(Queue)结构 实现的。
因此在使用这些机制时,内核需要:
- 为对象 分配内存空间
- 维护 队列控制结构体
- 管理 等待该对象的任务列表
这些操作都会带来一定的 时间开销和内存开销。
但你可以仔细想一想:
如果仅仅是面向单一任务发通知,再去创建一个消息队列对象,管理它,这些操作还是太"重量级"了。
于是在大约2015年时,FreeRTOS推出了任务通知机制(直接任务通知)。
任务通知的设计思路完全不同。
FreeRTOS 在设计任务控制块(TCB)的时候,专门为每一个任务预留了一块 任务通知相关 的成员变量。
换句话说:
每一个任务在创建的时候,就已经自带了任务通知机制。
TCB中任务通知相关成员
在FreeRTOS中若想要使用任务通知机制,让任务创建时预留相关成员变量,则需要开启相应宏:
c
#define configUSE_TASK_NOTIFICATIONS 1 // 配置使用任务通知机制,TCB中会存储相应成员
任务通知机制,在FreeRTOS中是默认开启的,如下图所示:

TCB结构体中,这两个核心的成员是:

注意表面上看,这是两个数组,但实际上它们是两个无符号4字节的整型变量。
这是因为两个数组长度的宏,其取值都是1,如下图所示:

这两个无符号4字节的整型成员变量,正是 任务通知机制的核心实现基础。
其中:
ulNotifiedValue用于保存 通知值,也可以叫做通知计数值。ucNotifyState用于记录 通知状态
因此,当任务之间使用任务通知进行通信时:
- 不需要创建额外对象
- 不需要额外分配内存
- 不需要维护额外的队列结构
任务通知实际上只是:
直接修改目标任务 TCB 中的通知状态和通知值。
也就是说:
一个任务想要通知另外一个任务,本质上只是做了这样几件事情:
- 找到目标任务的 TCB
- 修改该任务的 通知值
- 将该任务从 阻塞态移动到就绪态
整个过程非常直接,中间没有复杂的中间结构。
这也正是为什么任务通知会比信号量和队列 执行路径更短、效率更高 的原因。
简单来说可以这样理解:
消息队列、信号量这类机制的结构是:任务/中断 → 消息队列/信号量对象 → 任务
而任务通知则是:任务/中断 → 任务
中间少了一层 内核对象的管理开销。
因此,在很多只需要 向单一任务发事件通知 的简单场景中,任务通知往往是一个更优的选择。
ucNotifyState:通知状态的作用
首先,我们需要知道这两个成员在 TCB 创建之后的初始默认值。
FreeRTOS内核创建任务的函数源码如下:

实际上在该函数内部,包括函数 pvPortMallocStack() 中,都找不到 单独初始化这两个成员变量 的代码。
原因是:
在申请到 TCB_t 内存之后,FreeRTOS 会对整个结构体进行 统一清零处理:

也就是说:
TCB 结构体中的所有成员,在任务创建时默认都会被初始化为 0。
因此可以得到:
- ulNotifiedValue 是一个 4 字节无符号整型,初始值为 0
- ucNotifyState 是一个 1 字节无符号整型,初始值同样为 0
接下来我们先来看一下 ucNotifyState 成员,也就是 通知状态成员 的作用。
当任务通知被当作 二值信号量 使用时,也就是:
向单一任务发送通知,用于任务同步
这种情况下,系统实际上只关心一个问题:
该任务是否已经收到通知。
因此此时主要依赖 ucNotifyState 来记录通知状态,而通常并不关心 ulNotifiedValue 的具体数值。
ucNotifyState 成员用于记录 任务当前的通知状态。
在 FreeRTOS 内核中,这个成员的标准取值是下列三个宏定义:

具体来说,如下表格所示:
| 对应整数值 | 宏 | 含义 | 备注 |
|---|---|---|---|
| 0 | taskNOT_WAITING_NOTIFICATION | 当前任务没有等待通知 | 必须是0,因为该取值依赖初始化内存为0值 |
| 1 | taskWAITING_NOTIFICATION | 当前任务正在等待通知 | 正在等待通知的任务会进入阻塞态,放弃CPU |
| 2 | taskNOTIFICATION_RECEIVED | 当前任务已经收到通知 | 如果任务在阻塞等待通知,那么它会被唤醒 |
任务创建完成之后,ucNotifyState 的默认值是 0,也就是 taskNOT_WAITING_NOTIFICATION。
这表示:当前任务既 没有等待通知,也没有收到通知。
当任务调用等待通知函数,而且当前还没有通知到达,那么内核会:
- 将
ucNotifyState设置为taskWAITING_NOTIFICATION - 随后让当前任务进入 阻塞状态,表示该任务正处于等待通知的阻塞状态。
- 等待其他任务或者中断发送通知
当其他任务或者中断发送通知时,FreeRTOS 内核就会:
- 将任务的
ucNotifyState设置为:taskNOTIFICATION_RECEIVED - 表示该任务 已经收到通知。
- 如果此时任务正处于等待通知的阻塞状态,内核就会把该任务从 阻塞态移动到就绪态,任务随后就可以继续执行。
因此可以理解为:
ucNotifyState 本质上是一个 任务通知状态标志,用于记录任务:
- 是否正在等待通知
- 是否已经收到通知
FreeRTOS 内核正是通过这个状态变量,来判断任务是否需要 进入阻塞 或者 被唤醒执行。
ulNotifiedValue:通知值的作用
ulNotifiedValue 用于保存 通知值,也会叫通知计数值。
也就是说,利用任务通知发通知的同时,还可以"顺手"发一个通知值。
当然,如果把任务通知当成**"轻量版二值信号量"**使用,那么这个通知值的作用并不大。
关于它的具体作用,我们放到后面再讲。
任务通知实现任务同步的相关 API
如果只是使用任务通知实现任务同步(替代二值信号量),实际上常用的 API 只有三个:
c
xTaskNotifyGive()
vTaskNotifyGiveFromISR()
ulTaskNotifyTake()
又看到了 Give 和 Take 这两个单词,它们的含义其实和 二值信号量中的 Give / Take 完全一致:
- Give 表示发送任务通知,相当于产生一个事件信号。
- Take 表示等待接收任务通知,如果当前没有通知,任务会进入 阻塞状态,直到通知到来(前提是会等待阻塞)。
因此,从使用方式上来看,可以把任务通知理解为一种 "轻量级二值信号量"。
下面来详细介绍一下这三个API。
xTaskNotifyGive():向某个任务发送通知
函数原型:
c
void xTaskNotifyGive(TaskHandle_t xTaskToNotify);
作用:向 指定任务发送一个通知信号。
其唯一的参数,通过传参一个任务句柄,指定需要接收通知的任务。
从效果上来看,它非常像二值信号量中的:
c
xSemaphoreGive()
但从内部实现上来看,它并不是"释放一个信号量对象",而是:直接修改目标任务 TCB 中的通知相关成员。
xTaskNotifyGive() 的核心作用可以概括成一句话:
向指定任务发送一次通知,并将该任务的通知值加 1。
也就是说,它至少做了两件事:
- 修改目标任务的通知状态,将
ucNotifyState成员的取值改为taskNOTIFICATION_RECEIVED,表示这个任务已经收到通知了。 - 修改目标任务的通知值,将
ulNotifiedValue成员的取值进行自增1。
需要注意的是,任务在收到通知之前可以处于两种不同的状态:
- taskNOT_WAITING_NOTIFICATION(任务没有等待接收通知)
- taskWAITING_NOTIFICATION(任务在等待接收通知)
这说明:
任务通知不一定非要"先等后发",也可以"先发后等":
- 如果任务在接收通知之前,处于等待接收通知的状态,那么此任务会从阻塞态被唤醒,进入就绪态。
- 如果目标任务没有等待通知,没有阻塞等待,那么任务会在下一次调用接收通知函数时获取该通知,通知并不会丢失。
特别需要注意的是:
- 每调用xTaskNotifyGive()函数1次,发送1次通知,通知值都会累加一次。
- xTaskNotifyGive()函数没有返回值,接收通知的任务可以通过通知值的大小,知道通知已经发送来几次。
- 但如果把任务通知作为二值信号量来使用,这个通知值通常没有太大的意义。
xTaskNotifyTake():当前任务尝试接收通知
函数原型如下:
c
uint32_t ulTaskNotifyTake(
BaseType_t xClearCountOnExit, // 函数返回时,任务通知值如何处理
TickType_t xTicksToWait // 最大阻塞等待时间
);
这个函数的作用是:当前任务主动去等待接收一个通知,并在接收到通知后返回通知值。
它的行为可以简单理解为:
任务通知里的 xTaskNotifyTake() ≈ 二值信号量中的 xSemaphoreTake()
也就是说:
- 如果当前还没有收到通知,那么任务可以进入 阻塞状态
- 如果已经有通知了,那么任务就会立即取走通知,返回通知值,并继续运行。
具体到行为:
如果此时任务还没有收到通知,那么内核会做两件事:
- 把当前任务的
ucNotifyState设为taskWAITING_NOTIFICATION(等待接收通知) - 如果允许阻塞等待,就让任务进入阻塞态
如果调用 ulTaskNotifyTake() 时,通知本身已经到了:
ucNotifyState成员的取值本身就是taskNOTIFICATION_RECEIVED- 那么当前任务就不会阻塞,而是直接收到通知。
- 调用此函数会直接返回结果,也就是返回通知值(处理之前的)。
- 当然无论是哪种模式,处理完通知之后都会将 ucNotifyState 设为 taskWAITING_NOTIFICATION(等待接收通知)。
下面介绍一下函数的形参和返回值。
参数1:xClearCountOnExit
c
BaseType_t xClearCountOnExit
这个参数用于决定:
这个参数用于决定:任务成功取到通知后,通知值(成员ulNotifiedValue)如何处理。
如果传参为:pdTRUE
表示:
取到通知后,直接把通知值(ulNotifiedValue)清零,这更像 二值信号量 的行为。
因为在作为二值信号量使用时,ulNotifiedValue通知值只是作为一个计数器,记录发送了几次通知。
现在任务已经收到了通知,那就不管之前被通知了几次,直接归零即可。
类比一下:
你妈喊你吃饭,喊了你好几次
最终你去吃饭了
如果只把任务通知当通知使用,只要响应了通知,通知值计数就可以归零。
如果传参为:pdFALSE
表示:取到通知后,只把通知值减 1,即把ulNotifiedValue的取值自减1。
如果是把任务通知当成二值信号量使用,建议的传参是:pdTRUE。
参数2:xTicksToWait
此传参用于表示任务等待通知时,最大等待阻塞时间。
该参数我们已经非常熟悉了,不再赘述。
函数的返回值:
函数的返回值类型是一个4字节无符号整型:uint32_t类型
该函数的返回值表示:接收通知之前,当前任务的通知计数值。
返回值可能有两种:
- 0,表示到达阻塞时间,超时等待,仍然没有接收到通知。接收通知失败。
- 大于0,表示接收通知时,通知值的原始取值。
注意,此函数返回之前会修改通知值,但该函数的返回值是通知值的原始值。
类比一下:
你妈叫你去吃饭,喊了3次。
于是通知计数值就是3
你响应了通知,去吃饭,于是这个通知计数值被归0了。
但你仍然知道你妈叫了你3次。
如果把任务通知作为二值信号量使用,该函数的返回值就表示对方通知的次数,在这种情况下,此函数的返回值通常没有意义。
xTaskNotifyGiveFromISR():在中断环境下向某个任务发送通知
vTaskNotifyGiveFromISR() 的作用是:在中断服务函数中向指定任务发送通知。
函数原型如下:
c
void vTaskNotifyGiveFromISR(
TaskHandle_t xTaskToNotify, // 发送通知的任务句柄
BaseType_t *pxHigherPriorityTaskWoken // 是否需要在ISR结束后,触发高优先级任务切换
);
这个函数的行为和xTaskNotifyGive基本类似,无非换到了在中断环境下发送通知,所以不再赘述。
如果希望在ISR结束后,立刻触发高优先级任务切换,可以采用下面的调用方式:
c
BaseType_t Flag = pdFALSE;
vTaskNotifyGiveFromISR(xx, &Flag);
portYIELD_FROM_ISR(Flag);
以上。
代码示例
两个任务之间发通知的一段演示代码。
如下:
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "DebugUSART1.h"
/* 接收任务句柄 */
static TaskHandle_t TaskRecvHandle = NULL;
/* -------------------- 任务1:发送通知任务 -------------------- */
void TaskSend(void *Argument) {
uint32_t Round = 0;
uint8_t i;
while (1) {
Round++;
printf1("\r\n[Send] Round %lu start\r\n", Round);
/* 连续发送5次通知 */
for (i = 0; i < 5; i++) {
xTaskNotifyGive(TaskRecvHandle);
printf1("[Send] Give Notify %d\r\n", i + 1);
}
printf1("[Send] Round %lu end\r\n", Round);
/* 间隔一段时间,方便观察 */
vTaskDelay(5000);
}
}
/* -------------------- 任务2:等待通知任务 -------------------- */
void TaskRecv(void *Argument) {
uint32_t NotifyValue;
while (1) {
printf1("[ Recv ] Wait Notify...\r\n");
/* 等待任务通知,并一次性取出通知计数值 */
NotifyValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
printf1("[ Recv ] Get Notify Count = %lu\r\n", NotifyValue);
}
}
int main(void) {
DebugUSART1_Init();
/* 创建接收任务:优先级低 */
xTaskCreate(TaskRecv,
"TaskRecv",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
&TaskRecvHandle);
/* 创建发送任务:优先级高 */
xTaskCreate(TaskSend,
"TaskSend",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 2,
NULL);
vTaskStartScheduler();
while (1) {}
}
当然,任务通知更常用与中断当中,在中断当中给任务发一个任务通知。
中断中发送任务通知
在前面我们已经基于二值信号量,实现了在中断中给任务发通知。
但实际上在FreeRTOS中,任务通知更适合来完成这样的工作。
任务通知本身同样不负责传递具体数据,它更适合表达这样一种含义:
某个事件发生了,然后通知某一个任务立即去处理。
比如我们之前实现的串口中断接收数据的相关"事件":
消息队列已满,串口中断发送队列失败,系统发生了异常。
因此,我们完全可以这样设计:
- USART1 中断 仍然负责接收串口数据,并尝试把数据发送到消息队列中
- 如果发送成功,说明一切正常,程序继续运行
- 如果发送失败,说明消息队列可能已经满了,也就是系统发生了异常情况
- 此时中断中不直接做复杂处理,而是 通过任务通知通知异常处理任务
- 随后,由一个 更高优先级的异常处理任务 去等待该通知
- 一旦收到通知,说明系统出现了异常,该任务就会被立即唤醒,进行后续处理
这样一来,程序的结构依然非常清晰:
消息队列负责传递正常数据,任务通知负责上报异常事件。
也就是说:
消息队列传数据,任务通知传异常事件。
下面给出代码示例:
USART1.h 头文件:
c
#ifndef __USART1_H__
#define __USART1_H__
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "FreeRTOS.h"
#include "queue.h"
#include "task.h"
/* main.c 里创建的队列句柄 */
extern QueueHandle_t UartRxQueue;
/* 异常处理任务句柄 */
extern TaskHandle_t UartErrTaskHandle;
/**
* @brief 初始化串口 USART1
*/
void USART1_Init(void);
/**
* @brief 调试打印函数(串口版 printf)
*/
void printf1(const char *format, ...);
#endif
USART1.c 源文件:
c
#include "USART1.h"
/* ================= 内部函数(不对外暴露) ================= */
/**
* @brief USART1 发送单字节(阻塞式)
* @note 仅供本文件内部使用
*/
static void DebugUSART1_SendByte(uint8_t Byte) {
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, Byte);
}
/**
* @brief 初始化 USART1 串口
*/
void USART1_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
/* PA9 -> USART1_TX:复用推挽输出 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* PA10 -> USART1_RX:上拉输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* USART 参数配置:115200, 8N1 */
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
/* 开启 USART1 接收中断 */
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
/*
* 使用 FreeRTOS 时:
* 中断优先级建议采用分组4
* 并且用户中断优先级数值不要高于阈值
* 这里设置为 12
*/
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 12;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/**
* @brief 调试打印函数(串口版 printf)
* @param format: 格式化字符串
*/
void printf1(const char *format, ...) {
char buffer[100];
va_list list;
va_start(list, format);
vsprintf(buffer, format, list);
va_end(list);
for (uint16_t i = 0; buffer[i] != '\0'; i++) {
if (buffer[i] == '\n') {
DebugUSART1_SendByte('\r');
}
DebugUSART1_SendByte((uint8_t)buffer[i]);
}
}
/**
* @brief USART1中断:收到 1 字节就塞进 FreeRTOS 队列
*/
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
char ch = (char)USART_ReceiveData(USART1);
BaseType_t xFlag = pdFALSE;
BaseType_t Ret = xQueueSendFromISR(UartRxQueue, &ch, &xFlag);
if (Ret != pdPASS) {
/* 使用任务通知,上报异常事件 */
if (UartErrTaskHandle != NULL) {
vTaskNotifyGiveFromISR(UartErrTaskHandle, &xFlag);
}
}
/*
* 如果 xFlag 被设置为 pdTRUE
* 则 ISR 退出后会触发任务切换
*/
portYIELD_FROM_ISR(xFlag);
}
}
main.c 源文件:
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "LED.h"
#include "USART1.h"
#define BUFFER_SIZE 20
/* 串口接收消息队列 */
QueueHandle_t UartRxQueue = NULL;
/* 异常处理任务句柄 */
TaskHandle_t UartErrTaskHandle = NULL;
/* -------------------- 任务1:接收并解析串口命令 -------------------- */
void TaskRecv(void *Argument) {
char Buffer[BUFFER_SIZE] = { 0 };
uint8_t BufferIndex = 0;
while (1) {
char ch;
/* 无限阻塞等待队列中的串口数据 */
if (xQueueReceive(UartRxQueue, &ch, portMAX_DELAY) == pdPASS) {
/* 丢弃换行符 */
if (ch == '\r' || ch == '\n') {
continue;
}
/* 拼接字符串,始终保证 '\0' 结尾 */
if (BufferIndex < BUFFER_SIZE - 1) {
Buffer[BufferIndex++] = ch;
Buffer[BufferIndex] = '\0';
} else {
/*
* 如果接收过长,超出本地解析缓冲区长度,
* 多余数据不再继续写入
*/
Buffer[BUFFER_SIZE - 1] = '\0';
}
if (strcmp(Buffer, "OK") == 0) {
printf1("Light On \n");
LED_On();
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
} else if (strcmp(Buffer, "ERROR") == 0) {
printf1("Light Off \n");
LED_Off();
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
/* 非法指令,立即清空 */
if (BufferIndex > 0 && Buffer[0] != 'O' && Buffer[0] != 'E') {
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
/* 超过 ERROR 的长度,也清空 */
if (BufferIndex > 5) {
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
}
}
}
/* -------------------- 任务2:异常处理任务 -------------------- */
void TaskUartError(void *Argument) {
while (1) {
/*
* 阻塞等待异常通知
* pdTRUE 表示取到通知后自动清零
*/
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
printf1("Error: USART RX Queue Overflow! \n");
/*
* 这里可以继续扩展异常处理逻辑,例如:
* 1. 点亮告警灯
* 2. 让蜂鸣器工作
* 3. 记录错误计数
* 4. 上报系统状态
*/
}
}
int main(void) {
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
LED_Init();
USART1_Init();
/* 创建消息队列:长度20,每个元素1字节 */
UartRxQueue = xQueueCreate(20, sizeof(char));
if (UartRxQueue == NULL) {
printf1("Error: UartRxQueue create failed. \n");
while (1) {}
}
/* 创建串口接收解析任务 */
xTaskCreate(TaskRecv,
"TaskRecv",
configMINIMAL_STACK_SIZE + 128,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 创建高优先级异常处理任务 */
xTaskCreate(TaskUartError,
"TaskUartError",
configMINIMAL_STACK_SIZE + 128,
NULL,
tskIDLE_PRIORITY + 2,
&UartErrTaskHandle);
vTaskStartScheduler();
while (1) {}
}
至此,我们就在原本 "串口中断 + 消息队列 + 接收任务解析" 的基础上,
将原本的"二值信号量异常通知机制",替换为"任务通知异常机制"。
整个程序的运行逻辑,可以概括为:
- USART1 接收到 1 个字节数据,进入接收中断
- 中断中尝试将该字节发送到 UartRxQueue 消息队列
- 如果发送成功,说明系统当前处理能力正常,程序继续按照原来的逻辑运行
- 如果发送失败,说明消息队列可能已经满了,也就是系统当前出现了异常情况
- 此时中断中不去直接做复杂处理,而是 通过任务通知上报异常
- 高优先级任务 TaskUartError 一直阻塞等待该通知
- 一旦收到通知,说明异常发生,TaskUartError 会被立即唤醒并执行相应处理逻辑
这样写有一个非常明显的好处:
中断中只负责"发现异常并上报异常",而不负责"详细处理异常"。
同时,相比二值信号量:
任务通知不需要额外创建内核对象,开销更小,效率更高,更适合这种"一对一事件通知"的场景。
其他任务间通信方式(了解)
目前,在 FreeRTOS 中,我们已经学习了:
- 消息队列
- 信号量(包括二值信号量、计数信号量以及互斥信号量)
- 任务通知
除了这些方式外,FreeRTOS 还提供了一些其他的任务间通信方式。
例如:
- 事件标志组(Event Group)
- 队列集(Queue Set)
这些机制在 FreeRTOS 中同样属于任务间通信手段,但我们不作为重点内容进行讲解。
下面对其中两个机制做一个简单了解。
事件标志组
事件标志组(Event Group)
事件标志组本质上是一组"位标志(bit)"。
每一位可以表示一个事件是否发生,例如:
- bit0:串口接收完成
- bit1:网络连接成功
- bit2:数据处理完成
任务可以:
- 设置某一位(表示某个事件发生)
- 等待某一位或某几位同时满足
例如:
- 等待"多个事件同时发生"
- 或等待"任意一个事件发生"
因此,事件标志组非常适合:多个事件的组合判断与同步
队列集
队列集(Queue Set)
队列集可以理解为:
把多个队列或信号量组合在一起进行统一监听
正常情况下:一个任务只能阻塞等待一个队列或一个信号量
而使用队列集后,可以实现:一个任务同时等待多个对象中的任意一个
例如:
- 等待"串口队列"或"按键队列"中有数据
- 等待"某个信号量"或"另一个队列"触发
当其中任意一个就绪时,任务就会被唤醒。
但是需要注意:
队列集在实际工程中使用并不多,因为:
- 实现相对复杂
- 可读性较差
- 大多数场景可以通过任务拆分来解决
因此,在实际工程和学习过程中,我们优先掌握:
消息队列、信号量、任务通知
这三种最常用、最核心的通信方式即可。
总结:
FreeRTOS 提供了多种任务间通信机制,但在实际开发中,应优先选择:
简单、清晰、稳定、可维护的通信方式。