进程间通信

当 ADC 中断 ISR 向缓冲区写入数据后,若检测到“缓冲区已满”,会发送一个“数据就绪”信号(如信号量、队列通知等,具体由 RTOS 的 IPC 机制实现)。

此前因“等待数据就绪”而处于阻塞状态的数据处理任务,收到信号后会退出阻塞,被 RTOS 调度器唤醒为“运行状态”,进而从缓冲区读取数据并处理。

消息队列

动态分配(xQueueCreate():由FreeRTOS自动从系统的堆空间分配队列所需内存。代码简洁但依赖堆可用空间,若堆不足可能创建失败。

静态分配(xQueueCreateStatic():需开发者手动准备两块内存:存储数据的数组(对应队列存储单元)、存储队列元信息的结构体变量(如长度、元素数等)。不依赖堆内存,适合内存安全要求高或资源紧张场景,但需手动管理内存生命周期。

静态创建的时候,需要填写控制块的名称,队列的句柄就是控制块的指针。

xQueueCreate()本质是宏函数,调用更通用的 xQueueGenericCreate()(该函数同时用于队列、信号量、互斥量等IPC对象创建,体现代码复用)。

QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType );

uxQueueLength:队列存储单元的数量;

uxItemSize:每个存储单元的字节数;

ucQueueType:IPC对象的类型常数(如队列、二进制信号量、计数信号量等,不同类型对应不同常数);

有对应的宏定义,查书

储存单元内如果数据较大的话,可以存储数据对象的指针,通过指针去查数据

向队列中写数据

核心函数xQueueGenericSend,底层函数 注意返回函数,传入的参数等等

衍生出三个函数

xQueueSendToFront前端写入,LIFO逻辑

xQueueSendToBack(后端写入,FIFO逻辑)

xQueueOverwrite(长度为1的队列满时覆盖)

从队列中读数据

读取数据总是从队列首段读取,读出后删除这个单元的数据,后面如果还有未读取的数据,就依次向队列首端移动

BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );

基本逻辑

阻塞等待的任务状态逻辑

当任务调用 xQueueReceive()时,若设置了 xTicksToWait > 0但队列当前无数据,任务会转入阻塞状态,并等待指定节拍数。在此期间:

  • 若队列中被写入新数据,该任务会被唤醒(退出阻塞态,进入就绪态),之后被调度器选中即可重新运行,完成数据读取;
  • 若等待超时(达到 xTicksToWait节拍数)队列仍无数据,任务也会退出阻塞态(进入就绪态),但 xQueueReceive()会返回 pdFALSE表示读取失败。

xQueuePeek()(读取但不删除数据)

相关函数

image-20251013222413338

在ISR中也可以操作队列,但要使用对应函数

具体案例

按键枚举提高代码可读性

枚举方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
定义  枚举成员值必须是整数,可以用 十进制16进制都可以
隐式赋值
enum 枚举名 {
成员1, // 默认值为0
成员2, // 默认值为1(前一个成员值+1)
...
成员N // 默认值为N-1
};
显式赋值
enum Status {
STATUS_READY = 10,
STATUS_BUSY= 20,
STATUS_ERROR= 30 // 自动为21
};
使用
enum Status nowsta; // 声明Status类型的变量nowsta
nowsta = STATUS_READY; // 合法,赋值为枚举成员STATUS_READY(值为10)

GPIO_PinStateSTM32 HAL 库中专门用于表示 GPIO 引脚状态的数据类型

取值包括:

  • GPIO_PIN_RESET:表示引脚输出低电平(通常对应数值 0);
  • GPIO_PIN_SET:表示引脚输出高电平(通常对应数值 1)。

信号量

有的时候,进程间需要传递的只是一个标志,用于进程间同步或对一个共享资源的互斥性访问,这时就可以使用信号量或互斥量。信号量和互斥量的实现都是基于队列的,

信号量更适用于进程间同步,

互斥量更适用于共享资源的互斥性访问。

二值信号量

image-20251014083807198

记数信号量

ADC连续数据采集时,会使用双缓存区,可以使用计数信号量来管理

计数信号量(counting semaphore)是一种带固定长度队列的同步机制,队列中每个项为“标志”,主要用于控制多个共享资源的访问(避免资源竞争、保障访问有序性)。

  • 初始化:信号量创建时设置“初值”(如图中初值为4),该值对应可用共享资源的数量(类比餐馆里初始的4张餐桌)。
  • take操作(获取资源):当任务/中断(类比“客人进店”)需要占用资源时,执行take操作:若信号量当前值 > 0:值减1,表示占用1个资源(如1张餐桌被占用),任务继续执行。若信号量当前值 = 0:表示所有资源已被占用,执行take的任务需进入阻塞状态(等待其他任务释放资源),直到有资源被释放。
  • give操作(释放资源):当任务/中断(类比“客人用餐结束离开”)释放资源时,执行give操作:信号量值加1,表示可用资源数量增加(如1张空餐桌释放),此前因take阻塞的任务会被唤醒,重新尝试获取资源。

    关键特性与场景

  • 资源计数本质:信号量的“值”直接反映剩余可用资源数,通过“减1获取、加1释放”的逻辑,保障多任务/中断对资源的互斥访问。

  • 超时等待(可选):任务申请信号量(take)时,可设置“等待超时时间”——若超时前仍未获取资源,任务不再阻塞,按预设逻辑处理(避免永久等待)

互斥量

针对优先级翻转问题,采用优先级继承机制

用于互斥资源的访问

image-20251014084925820

优先级翻转与优先级继承(互斥量的核心机制)

  • 优先级翻转问题:当高优先级任务需访问被低优先级任务占用的资源时,会被中优先级任务抢占,导致高优先级任务长时间等待,系统实时性下降。
  • 优先级继承机制(互斥量的解决思路):当高优先级任务请求互斥量时,当前持有互斥量的低优先级任务会临时继承高优先级的优先级,避免被中优先级任务抢占。待持有任务的资源访问完成、释放互斥量后,优先级恢复原状态。

互斥量不能在ISR(中断服务程序)中使用,原因有二:

  • ISR不是“任务”,而优先级继承是任务间的机制,ISR无法参与。
  • ISR执行时不能“阻塞等待”(获取互斥量通常需要等待资源释放),但ISR需快速响应、不可长时间阻塞,因此不适用。

递归互斥量(recursive mutex)

是一种特殊的互斥量,用于支持需递归调用的函数场景。

普通互斥量被任务获取后,该任务无法再次获取;递归互斥量允许同一任务多次获取,但每次获取必须与一次释放配对(即获取和释放的总次数需相等)。此外,递归互斥量和普通互斥量一致,不能在中断服务程序(ISR)中使用

相关函数

image-20251014085346225

image-20251014085358762

Semaphore 信号量,棋语

以介绍动态内存创建方式为主

函数使用注意事项

普通释放函数 xSemaphoreGive():支持释放「二值信号量、计数信号量、互斥量」;

ISR 版本 xSemaphoreGiveFromISR()仅能释放「二值信号量、计数信号量」—— 因为互斥量不能在 ISR(中断服务函数)中使用,同类操作(如 xSemaphoreTake()xSemaphoreTakeFromISR())的限制逻辑一致。

递归互斥量专属函数:递归互斥量的释放/获取需用专门函数 xSemaphoreGiveRecursive()/ xSemaphoreTakeRecursive(),且递归互斥量也不能在 ISR 中使用

信号量值的查询函数

uxSemaphoreGetCount(xSemaphore)用于获取信号量当前值:

  • 若为二值信号量:返回 1(信号量有效,资源可用)或 0(信号量无效,资源不可用);
  • 若为计数信号量:返回值即「剩余可用资源的个数」。

互斥量持有者查询函数

xSemaphoreGetMutexHolder(xMutex)用于在任务中查询「互斥量 xMutex的当前持有者」(即已获取但未释放该互斥量的任务句柄),常用于判断当前任务是否持有该互斥量

cubemx中的使用设置

image-20251014090837794

二值信号量使用程序

创建二值信号量

直接调用 xQueueGenericCreate()来创建队列,并通过参数配置让这个队列表现为“二值信号量”。

释放原型:通过宏定义封装队列操作,等价于 xQueueGenericSend(...),原型展开为:

1
#define xSemaphoreGive( xSemaphore )  xQueueGenericSend( ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

xSemaphore:二值信号量的句柄(也可用于计数信号量、互斥量,因句柄类型通用)。

NULL:二值信号量无需向队列写数据,FreeRTOS 内部处理。

semGIVE_BLOCK_TIME:等待节拍数,释放操作无需等待,故值为0。

queueSEND_TO_BACK:指定数据写入队列的方向(队尾)。

释放成功返回 pdTRUE,失败返回 pdFALSE

中断服务程序(ISR)中释放:xSemaphoreGiveFromISR()函数

封装队列中断级操作,等价于 xQueueGiveFromISR(...),原型展开为:

1
2
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )  \  
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWok

第1个参数 xQueue:仅支持二值信号量或计数信号量的句柄(ISR 中禁止使用互斥量,因互斥量依赖优先级继承机制,ISR 无任务优先级上下文,会导致逻辑错误)。

第2个参数 pxHigherPriorityTaskWoken:BaseType_t类型指针,指示“释放信号量后是否唤醒更高优先级任务”。若为 pdTRUE,表示有高优先级任务就绪,ISR 退出前需主动触发任务调度;若为 pdFALSE,则无需调度。

详细解释:

在中断服务程序(ISR)中释放信号量时,可能触发一个关键行为:让“等待该信号量”的任务从「阻塞态」转为「就绪态」

ISR 退出后,该执行哪个任务取决于 任务 A 和任务 B 的优先级

pxHigherPriorityTaskWoken的角色:“是否有高优先级任务就绪

xSemaphoreGiveFromISR()执行时,会检查被释放信号量唤醒的任务的优先级,并与当前被中断的任务(即 ISR 抢占的那个任务)的优先级比较。若存在“优先级更高的就绪任务” → 函数内部会将 pxHigherPriorityTaskWoken指向的内存设为pdTRUE`。所以传入的是个指针,也就是将对应标志设为pdTRUE

所以执行完xSemaphoreGiveFromISR()会改变唤醒标志的值

最后再条件判断启用任务调度portYIELD_FROM_ISR

ISR 中使用示例代码(逻辑解析)

示例代码展示了“检查信号量句柄有效性 → 调用中断版释放函数 → 根据唤醒标志触发调度”的流程:

1
2
3
4
5
6
BaseType_t highTaskWoken = pdFALSE;  // 初始化唤醒标志为pdFALSE  
if (BinSem_DataReadyHandle != NULL) // 检查二值信号量句柄是否有效
{
xSemaphoreGiveFromISR(BinSem_DataReadyHandle, &highTaskWoken); // 释放信号量,更新唤醒标志
portYIELD_FROM_ISR(highTaskWoken); // 若唤醒了高优先级任务,触发调度
}

运行实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BaseType_t highTaskWoken = pdFALSE;  // 初始化为pdFALSE,表示“暂无高优先级任务就绪”

void USART1_IRQHandler(void) // 假设是串口接收中断服务函数
{
BaseType_t xHigherPriorityTaskWoken = &highTaskWoken; // 取地址传给函数

if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 1. 处理串口接收数据...

// 2. 释放二值信号量(告知任务“数据已接收”) 注意这里传入的是指针
xSemaphoreGiveFromISR(BinSem_DataReadyHandle, xHigherPriorityTaskWoken);

// 3. 检查是否需要调度:若xHigherPriorityTaskWoken变为pdTRUE,说明有高优先级任务就绪
if (highTaskWoken == pdTRUE)
{
portYIELD_FROM_ISR(highTaskWoken); // 触发任务调度,让高优先级任务运行
}
}
}

获取二值信号值

xSemaphoreTake()

#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

xSemaphore二值信号量的句柄(需先通过 xSemaphoreCreateBinary()等函数创建获得)。

xBlockTime:阻塞等待的“节拍数”(FreeRTOS 时间单位)。信号量无效时的等待策略:

  • portMAX_DELAY无限等待(直到信号量被释放);
  • 0不等待,立即返回结果(信号量无效则直接失败);
  • 其他正整数:最多等待对应节拍数,超时则放弃获取。

成功获取信号量 → 返回 pdTRUE

失败(如超时、信号量句柄无效等) → 返回 pdFALSE

该函数不局限于二值信号量,还支持获取「计数信号量」和「互斥量」(因此 xSemaphore参数可传入这三种对象的句柄)。

中断中获取二值信号量:xSemaphoreTakeFromISR()

xSemaphore:需为二值信号量或计数信号量的句柄(注意:不能用互斥量!因为互斥量的“优先级继承”机制在 ISR 中无意义,且会引发优先级混乱)。

具体程序实例

二值信号量释放后会变成1 ,take后会变成0

1表示信号量“可用”(可被任务获取)

0表示信号量“被占用”(无可用资源)

二值信号量为1时,继续give,仍然是pdtrue

二值信号量双缓存区

记数信号量

创建计数信号量

  • 函数xSemaphoreCreateCounting()(宏定义,内部调用 xQueueCreateCountingSemaphore()实现)。
  • 参数uxMaxCount:信号量能达到的最大计数值(表示资源总量的上限);uxInitialCount:信号量的初始计数值(表示初始化时可用的资源数量)。
  • 返回值:信号量的句柄(QueueHandle_t类型,用于后续操作该信号量)。
  • 初始值通常设为最大值(如示例 semb = xSemaphoreCreateCounting(5, 5);,表示初始化时有5个可用资源)。

获取计数信号量

  • 函数xSemaphoreTake()(与“二值信号量”的获取函数复用)。
  • 作用逻辑:申请资源。成功获取后,信号量计数值减1(表示可用资源减少1个);若计数值已为0(无可用资源),申请任务将进入等待状态。

释放计数信号量

  • 函数xSemaphoreGive()(与“二值信号量”的释放函数复用)。
  • 作用逻辑:释放资源。执行后,信号量计数值加1(表示可用资源增加1个)。

获取计数信号量当前计数值

  • 函数uxSemaphoreGetCount()(宏定义,内部调用 uxQueueMessagesWaiting()实现)。
  • 作用逻辑:查询信号量当前的计数值(本质是通过查询对应队列的“等待消息数”,间接获取信号量计数)。

记数信号量如果在中断中释放的话,释放之后会进行一次 portYIELD_FORM_ISR来启动任务调度(判断是返回当前函数还是优先级更高的函数)

当记数信号量

注意 xSemaphoreGive()

  • 若当前计数值 < uxMaxCount:允许释放,计数值 +1,函数返回 pdTRUE(表示释放成功);
  • 若当前计数值 = uxMaxCount:计数值 +1 会超过最大值,释放操作失败,函数返回 pdFALSE(表示释放失败)。

信号量类型不一样导致处理结果不一样

  • 当二值信号量计数值为 1时,xSemaphoreGive()允许调用,且返回 pdTRUE(即使计数值保持 1);
  • 若为「普通计数信号量(uxMaxCount>1)」且计数值达最大值,xSemaphoreGive()才会返回 pdFALSE

互斥量

优先级翻转 问题

高优先级任务需要等待低优先级任务释放二值信号量之后才可以运行,但是不需要使用信号量的低优先级任务会抢占正在等待信号量的高优先级任务提前运行,有损任务的实时性

优先级继承

争抢互斥量的任务获得其最高优先级,知道释放后,任务恢复至原先的优先级

价值:通过“临时提升低优先级任务优先级”,阻止中间优先级任务抢占,保证高优先级任务(TaskHP)能及时执行,满足实时性要求;是互斥型资源(如共享硬件、临界区)访问控制的典型方案。

局限性:优先级继承只能“缓解”优先级翻转,无法完全消除。中优先级任务如果提前抢占,在高优先级阻塞等待互斥量的时候还是会先执行中优先级任务,导致cpu占用 也就是高优先级还是要等待低优先级把资源释放

创建互斥量

QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )

参数 ucQueueType:指定队列类型,如 queueQUEUE_TYPE_MUTEX(互斥量)或 queueQUEUE_TYPE_RECURSIVE_MUTEX(递归互斥量)。

获取和释放互斥量的函数

  • 获取互斥量:使用 xSemaphoreTake()函数。任务调用此函数来获取互斥量所有权。如果互斥量已被其他任务占用,任务可能会阻塞等待(取决于阻塞时间参数)。
  • 释放互斥量:使用 xSemaphoreGive()函数。任务完成资源访问后,调用此函数释放互斥量,允许其他任务获取。

串口发送后需要短暂延时?否则不能正常输出换行符\n

事件组

  • 多事件等待与组合:允许任务同时等待多个事件(通过位掩码配置),支持“与”关系(所有指定事件发生)或“或”关系(任一指定事件发生)的触发条件。
  • 广播式通知:事件发生时,可同时解除所有等待该事件的任务的阻塞状态(非排他性),而队列/信号量仅唤醒最高优先级任务。
  • 同步多任务:适用于需要多个任务对同一事件做出协同响应的场景(如报警事件触发LED闪烁和蜂鸣器同时工作)

与之前队列信号量互斥量不同

特性 队列/信号量 事件组
事件处理 一次仅处理一个事件 支持多事件组合(位掩码操作)
任务唤醒机制 仅唤醒最高优先级任务(排他性) 同时唤醒所有等待同一事件的任务
适用场景 单事件同步、资源互斥 多事件响应、事件广播、多任务同步

事件组的内部变量类型为 EventBits_t,其位数取决于FreeRTOS配置参数

对于STM32,事件组变量通常是32位,但实际可用事件位为24个(位23至位0),位31至24保留未用。

  • 每个事件标志对应一个位(例如,位0代表事件A,位1代表事件B)。事件发生时,通过函数 xEventGroupSetBits()将相应位置1。
  • 任务可以通过 xEventGroupWaitBits()等待特定事件位组合(例如,等待位0和位2同时为1,或任一为1)。
  • 事件组具有“广播”功能:当多个任务等待同一事件时,设置事件位可同时解除所有等待任务的阻塞状态(而队列或信号量通常只唤醒一个任务)

函数API

无法通过CUBEMX可视化创建时间组

三类

image-20251014125001979

清零的时间位用掩码表示

创建时间组:

  • xEventGroupCreate()**动态内存分配:FreeRTOS 自动从堆(heap)中分配事件组所需的内存。函数原型EventGroupHandle_t xEventGroupCreate( void );参数:无(无需传入任何参数)。返回值**:成功创建时返回事件组的句柄(EventGroupHandle_t类型),失败返回 NULL

    动态创建无需参数

  • xEventGroupCreateStatic()**静态内存分配:需开发者预先分配事件组所需的内存(通常为全局变量或静态变量)。函数原型EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *pxEventGroupBuffer );参数:指向 StaticEventGroup_t类型变量的指针(需预先定义该变量)。返回值**:成功创建时返回事件组句柄,失败返回 NULL

置位函数

xEventGroupSetBits()

1
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet );
  • xEventGroup:要操作的事件组的句柄(通过 xEventGroupCreate()创建)。
  • uxBitsToSet:要置位的事件位掩码(就是对应的十六进制数,可以直接对32位进行操作0x00010000)(类型为 EventBits_t,在STM32上等同于 uint32_t)。
  • 返回值:函数执行成功后事件组当前的值(类型为 EventBits_t)。

xEventGroupSetBitsFromISR()

1
BaseType_t xEventGroupSetBitsFromISR(     EventGroupHandle_t xEventGroup,     const EventBits_t uxBitsToSet,     BaseType_t *pxHigherPriorityTaskWoken  );
  • xEventGroup:事件组句柄。
  • uxBitsToSet:要置位的事件位掩码(规则同任务版)。
  • pxHigherPriorityTaskWoken:输出参数(指针),用于返回是否需要触发任务调度:若被置为 pdTRUE,表示有更高优先级任务被唤醒,需在ISR退出前手动触发上下文切换;必须传入一个变量地址(如 &xHigherPriorityTaskWoken),不可直接传入常量 pdTRUE/pdFALSE

定时器守护任务

  • 定时器守护任务是 FreeRTOS 内部的一个后台任务(优先级通常较低),负责处理与时间相关的异步操作,如软件定时器回调函数和延后的系统任务。
  • 在事件组上下文中,当在中断服务程序(ISR)中调用 xEventGroupSetBitsFromISR()时,FreeRTOS 不允许在 ISR 中直接执行置位操作(因为操作可能不确定时长,会破坏中断的实时性)。因此,系统会将置位请求封装成一个消息,并发送到定时器守护任务的消息队列中。
  • 定时器守护任务在后台从队列中取出消息,并实际执行事件位的设置操作。这确保了 ISR 的快速响应,同时维护了系统的稳定性。

这意味着在 ISR 中调用 xEventGroupSetBitsFromISR()时,置位操作不会立即执行

他会

发送消息:将置位请求(包括事件组句柄和掩码)作为消息发送给定时器守护任务。

异步处理:定时器守护任务在任务上下文中(而非 ISR 中)处理这个消息,实际调用 xEventGroupSetBits()来设置事件位。

这种延后机制避免了在 ISR 中执行复杂操作(如任务唤醒和调度),确保了中断处理程序的确定性和快速性(ISR 应该尽快退出)。同时,它保证了事件组操作的安全性(避免多任务竞争)。

多任务竞争:

多个任务同时调用 xEventGroupSetBits()任务和中断同时操作事件组,可能会发生:

  • 操作覆盖:任务A设置了位0,任务B几乎同时设置了位1,但由于任务调度或执行时序,可能导致只有一位的设置生效,另一位被覆盖。
  • 状态不一致:任务A读取事件组状态后,任务B立刻修改了事件位,导致任务A基于过期状态做出错误决策。

触发任务调度(比较的是定时器守护任务相对于当前中断所打断的任务的优先级)

  • 当定时器守护任务收到置位消息后,它会从消息队列中取出请求并执行xEventGroupSetBits()。这个过程可能会唤醒其他等待该事件的任务(例如,如果事件位满足某些任务的等待条件)。
  • 关键点:如果定时器守护任务的优先级高于当前被中断抢占的任务(即 ISR 发生前正在运行的任务),那么一旦定时器守护任务就绪(收到消息),它应该立即抢占 CPU,以确保延后操作被快速处理,从而避免高优先级任务被延迟。
  • 因此,在 ISR 中调用xEventGroupSetBitsFromISR()时,FreeRTOS 会检查定时器守护任务的优先级是否高于当前被中断任务的优先级:如果更高,则参数pxHigherPriorityTaskWoken会被设置为pdTRUE,表示需要在 ISR 退出前触发上下文切换(调用portYIELD_FROM_ISR()),让定时器守护任务立即运行。如果更低或相等,则pxHigherPriorityTaskWoken保持pdFALSE,无需立即调度,ISR 退出后会继续运行原任务。

注意:

当中断服务程序(ISR)中调用 xEventGroupSetBitsFromISR()向定时器守护任务发出请求时,定时器守护任务不会立即收到请求并进入就绪状态

核心机制:延后处理而非立即唤醒

  • 消息队列机制:当在 ISR 中调用 xEventGroupSetBitsFromISR()时,FreeRTOS 会将置位请求封装成一个消息,并发送到定时器守护任务的消息队列中。这是一个异步操作,消息只是被存入队列,但定时器守护任务本身不会立即被唤醒或进入就绪状态。
  • 定时器守护任务的调度:定时器守护任务是一个独立的 FreeRTOS 任务,它通常以较低的优先级运行。只有当它被调度器选中时(即轮到它运行时),才会从消息队列中取出请求并执行实际的置位操作(调用 xEventGroupSetBits())。因此,请求的处理是“延后”的,而不是立即的。

事件位置零

1
2
3
4
EventBits_t xEventGroupClearBits( 
EventGroupHandle_t xEventGroup, // 事件组句柄(通过xEventGroupCreate创建)
const EventBits_t uxBitsToClear // 需清零的事件位掩码
);
  • xEventGroup:目标事件组的句柄。uxBitsToClear位掩码,指定需清零的位(例如 0x01清零位0,0x03清零位0和位1)。
  • 返回值:清零的事件组值(EventBits_t类型,即所有事件位的状态)。

在ISR中的运行机制同上

读取事件组当前值

xEventGroupGetBits()

1
#define xEventGroupGetBits(xEventGroup) xEventGroupClearBits(xEventGroup, 0)

传入的事件位掩码为 0(即 uxBitsToClear = 0),表示不清除任何事件位,仅返回事件组的当前值。

这里传入的掩码是你向让某位置零,则选中它,而不是直接修改他的值

事件组条件等待

xEventGroupWaitBits()用于使当前任务进入阻塞状态,等待事件组中指定的事件条件成立。任务可等待多个事件位的组合(“逻辑与”或“逻辑或”),条件满足时自动唤醒,适用于复杂事件同步场景。

1
2
3
4
5
6
7
EventBits_t xEventGroupWaitBits(
EventGroupHandle_t xEventGroup, // 事件组句柄(通过xEventGroupCreate创建)
const EventBits_t uxBitsToWaitFor, // 需等待的事件位掩码(如0x03表示等待位0和位1)
const BaseType_t xClearOnExit, // 退出时是否清零事件位(pdTRUE/pdFALSE)
const BaseType_t xWaitForAllBits, // 等待条件:全部位置1(pdTRUE)或任一位置1(pdFALSE或运算,发生一个即执行)
TickType_t xTicksToWait // 阻塞超时时间(单位:系统节拍)
);

返回值

  • 条件成立时:返回事件组当前值(可通过位掩码判断具体事件位);
  • 超时返回:返回超时时刻的事件值(可能不满足条件)。

同步函数 xEventGroupSync()

xEventGroupWaitBits()的区别:

xEventGroupSync()强调“所有任务就位后同步执行”,而 xEventGroupWaitBits()侧重“等待外部事件条件成立”。

具体程序示例

首先要声明全局变量句柄

一个任务set,另外的任务wait

多任务同步

原子操作xEventGroupSync()

EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait );

  • xEventGroup:事件组对象的句柄(通过 xEventGroupCreate()创建),用于标识要操作的事件组。
  • uxBitsToSet:任务要设置的事件位掩码。例如,如果 TaskA 需要设置 Bit2(对应二进制 100),则掩码为 0x04;TaskB 设置 Bit1(010),掩码为 0x02;TaskC 设置 Bit0(001),掩码为 0x01
  • uxBitsToWaitFor:需要等待的同步条件掩码,指定哪些事件位必须全部置 1 才能触发同步。例如,如果所有任务需要等待 Bit2、Bit1、Bit0 都置 1(即所有任务都到达同步点),则掩码为 0x07(二进制 111)。
  • xTicksToWait:任务阻塞等待的最大时间(单位:系统节拍)。如果设置为 portMAX_DELAY,任务会无限等待直到条件满足;如果设置为 0,则立即返回当前事件值(不阻塞)。
  • 返回值:函数退出时事件组的当前值。如果同步条件满足,返回的值通常包含 uxBitsToWaitFor掩码;如果超时,返回的值可能不满足条件。

原子操作的优势

图片中强调,如果使用 xEventGroupSetBits()xEventGroupWaitBits()两个函数 separately 来实现同步,这不是原子操作:任务在设置事件位后,可能被其他任务或中断抢占,导致同步条件检查出现竞态条件(race condition)。例如,TaskA 设置 Bit2 后,在等待其他位时被抢占,TaskB 和 TaskC 可能无法及时设置自己的位,导致同步失败或延迟。

原子性:设置事件位和等待条件在同一个函数调用中完成,不会被中断或任务切换打断,确保了操作的完整性。

xEventGroupSync()不会再退出时候自动清空bit值xEventGroupWaitBits可以

所以可以让xEventGroupSync()对一次性任务进行设置,比如在初始化的时候。

任务通知

无需创建中间对象

启用条件:需在FreeRTOS配置中将 configUSE_TASK_NOTIFICATIONS设为 1(默认值即为1,可在STM32CubeMX中设置)。

  • 核心组件:每个任务的控制块(TCB)中增加一个 uint32_t类型的通知值变量(notification value),以及一个通知状态标志(挂起pending或非挂起not-pending)。
  • 工作流程发送者(任务或中断服务程序ISR)向接收者(只能是任务)发送通知,可附带一个通知值或指定通知值的计算方法(如加1)。接收者可以阻塞等待通知,收到通知后退出阻塞状态并处理数据。通信过程直接通过任务控制块完成,无需中间对象,如图8-2所示(进程间直接通信)。

任务通知的优点

  • 高性能:比队列、信号量等传统IPC更快,因为避免了中间对象的开销,直接操作任务控制块。
  • 低内存开销:仅需在任务控制块中增加少量变量(通知值和状态标志),无需额外分配内存创建对象。
  • 灵活性:支持带值通知或修改通知值(如递增、覆盖),适用于多种场景。

任务通知的局限性

  • 接收者限制:只能向任务发送通知,不能向ISR发送通知(ISR可作为发送者,但不能作为接收者)。
  • 无广播功能:通知只能发送给特定任务(指定接收者),无法同时广播给多个任务。
  • 数据限制:一次只能传递一个 uint32_t类型的数据,无法发送大型或复杂数据(如结构体、数组)。
  • 状态管理:通知值只有一个,且状态为二进制(挂起/非挂起),复杂同步场景可能需额外处理。

相关函数

发送通知

xTaskNotify()

功能:向任务发送通知,并设置通知值。支持三种传递方式(通过参数eAction控制):

  • eNoAction:仅发送通知,不修改通知值(接收任务的通知值不变)。
  • eSetBits:将通知值的指定位置1(按位或操作)。当作事件组来用
  • eIncrement:将通知值加1(模拟计数信号量)。二值或者记数信号量
  • eSetValueWithOverwrite:覆盖通知值(即使任务未处理前一个通知)。
  • eSetValueWithoutOverwrite:仅当任务未处理通知时才覆盖通知值(避免丢失数据)。

返回值

返回值具体含义

  • pdPASS(通常定义为 1):表示通知成功发送给目标任务。目标任务的通知值已按照指定的 eAction参数成功更新
  • pdFAIL(通常定义为 0):表示通知发送失败最常见的原因:传入的任务句柄 xTaskToNotify无效(例如为 NULL,或指向已删除的任务)。

同理

xTaskNotifyFromISR()函数参数基本一致

除了最后一个pxHigherPriorityTaskWoken若为 pdTRUE:表示有更高优先级任务被唤醒,需在退出ISR前调用 portYIELD_FROM_ISR() 触发上下文切换;

xTaskNotifyAndQuery()

xTaskNotifyAndQuery()xTaskNotify()的增强版本,不仅能够向指定任务发送通知,还能返回接收任务在通知值改变前的原始值

xTaskNotifyGive()

功能更专一,专门来模拟信号量的

xTaskNotifyWait 通用接受

uint32_t ulNotifiedValue; // 用于存储接收到的通知值

需要自定义接受值 ,然后传入地址 &ulNotifiedValue

等待任务通知并获取通知值

1
2
3
4
5
6
BaseType_t xTaskNotifyWait( 
uint32_t ulBitsToClearOnEntry, // 进入时清零的位掩码
uint32_t ulBitsToClearOnExit, // 退出时清零的位掩码
uint32_t *pulNotificationValue, // 返回接收到的通知值
TickType_t xTicksToWait // 阻塞等待时间
);

返回值

pdTRUE:成功接收到通知

pdFALSE:等待超时或未接收到通知

任务状态处理逻辑:

  1. 有挂起通知(任务处于挂起状态):立即读取通知值并返回pdTRUE执行ulBitsToClearOnExit清零操作(如果设置)不执行ulBitsToClearOnEntry操作
  2. 无挂起通知(任务处于未挂起状态):执行ulBitsToClearOnEntry清零操作(如果设置)进入阻塞状态等待通知超时前收到通知:执行ulBitsToClearOnExit操作,返回pdTRUE超时未收到通知:返回pdFALSE不执行ulBitsToClearOnExit操作

专门类似于二值信号量或者记数信号量的接受

ulTaskNotifyTake()

uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);

  • xClearCountOnExit:类型:BaseType_t(通常为整数)取值:pdTRUEpdFALSE作用:控制函数退出时如何修改通知值:pdTRUE:退出时将通知值清零(模拟二值信号量)。适用于需要一次性事件通知的场景。pdFALSE:退出时将通知值减1(模拟计数信号量)。适用于资源计数场景,如管理多个可用资源。

  • xTicksToWait:类型:TickType_t(通常为无符号整数)作用:指定任务在阻塞状态下等待通知的最大时间(单位:系统节拍)。特殊值:portMAX_DELAY:无限期等待,直到收到通知(需确保 configUSE_TASK_NOTIFICATIONSINCLUDE_vTaskSuspend配置为1)。0:不等待,立即返回当前通知值(非阻塞模式)。

  • 返回值:
    • 类型:uint32_t
    • 含义:返回修改前的通知值(即减1或清零之前的原始值)

具体示例

CUBEMX中将configUSE_TASK_NOTIFICATIONS设置为1

对于接受方,需要定义uint32_t 数值,然后传入数值指针

ulTaskNotifyTake()在通知值减到 0 时

任务会阻塞等待,直到:

  • 有其他任务或中断调用 xTaskNotifyGive()等函数增加通知值(使通知值大于 0),此时任务被唤醒,执行减一操作后返回。

连续发送 vTaskNotifyGive()

如果接收任务尚未处理(即未调用 ulTaskNotifyTake()),通知值会持续累加(例如,连续调用3次,通知值变为3)。

  • 通知状态标志(pending)只有两种状态:挂起(有通知待处理)非挂起(无通知)
  • 第一次调用 vTaskNotifyGive() 会将状态设置为挂起(pending),表示有通知待处理。
  • 后续调用(无论多少次)不会改变状态标志(因为已经是挂起状态)。除非消息被处理完,才会回到非挂起状态,此时表示无待处理消息

通知挂起和任务挂起

任务通知的“挂起状态”(Pending)

  • 本质:是任务控制块(TCB)中的一个二进制标志位uint8_t),表示该任务是否有待处理的通知
  • 触发条件:当其他任务或中断调用 vTaskNotifyGive()xTaskNotify()发送通知时,该标志位被设为 1(挂起)。
  • 清除条件:接收任务调用 ulTaskNotifyTake()xTaskNotifyWait()成功获取通知后自动清除。手动调用 xTaskNotifyStateClear()强制清除。
  • 对任务的影响不阻塞任务运行:即使通知处于挂起状态,任务仍可正常执行(除非任务主动阻塞等待通知)。仅作为通知提醒:用于唤醒因等待通知而阻塞的任务。

任务的“挂起状态”(Suspended) 这里挂起不等于阻塞

  • 本质:是任务生命周期中的一种状态(属于调度器管理),表示任务被强制暂停执行,不参与调度。
  • 触发条件:调用 vTaskSuspend(taskHandle)挂起指定任务。
  • 清除条件:调用 vTaskResume(taskHandle)恢复任务运行。
  • 对任务的影响完全停止运行:被挂起的任务不会占用 CPU 时间,即使有更高优先级也不会执行。无法响应事件:被挂起的任务无法接收通知、信号量等事件(但通知值仍可被修改,状态标志仍可被设置)。

流缓冲区和消息缓冲区

特性 队列(Queue) 流缓冲区(Stream Buffer)
数据单位 固定格式的项(item),如结构体、整数 无结构字节流(byte stream)
数据边界 每次读写一个完整项 可读写任意长度字节(无需完整项)
适用场景 结构化数据交换(如消息、命令) 原始字节流传输(如串口数据、文件流)
多读写支持 原生支持多任务读写 需临界段保护(设计为单读写者)
内存效率 每个项需单独存储 连续字节存储,更紧凑

无法在CUBEMX中设置

image-20251015091058401

创建流缓存区

xStreamBufferCreate()

通用底层函数

1
StreamBufferHandle_t xStreamBufferGenericCreate(    size_t xBufferSizeBytes,      *// 缓冲区大小(字节)*    size_t xTriggerLevelBytes,    *// 触发水平(字节)*    BaseType_t xIsMessageBuffer   *// 缓冲区类型标识* );
  • 关键参数xBufferSizeBytes:流缓冲区的总容量(字节数),决定可存储数据的最大长度。xTriggerLevelBytes:触发水平(字节数),用于控制阻塞任务的唤醒条件(见下文详解)。xIsMessageBuffer:区分创建流缓冲区(pdFALSE)或消息缓冲区(pdTRUE)。流缓冲区固定传递pdFALSE
  • 返回值:成功创建时返回StreamBufferHandle_t类型(流缓冲区句柄,本质是结构体指针),失败返回NULL(通常因内存分配失败)。

触发水平

定义:触发水平是流缓冲区的独特机制,指定解除读取任务阻塞所需的最小数据量(字节数)

  • 当读取任务因缓冲区空而阻塞时,仅当缓冲区中积累的数据量达到或超过触发水平,任务才会被唤醒。
  • 示例:触发水平设为5(如代码xStreamBufferCreate(20, 5)),则需至少5字节数据到达缓冲区,才能唤醒阻塞的读取任务。
  • 最小值:可为1(表示有1字节数据即可唤醒任务)。
  • 最大值:不能超过缓冲区大小(如示例中缓冲区大小为20,触发水平最大只能设20)。
  • 特殊值0:等效于1(系统自动处理)。

重新设置指定流缓冲区的触发水平(Trigger Level)。

1
2
3
4
BaseType_t xStreamBufferSetTriggerLevel(
StreamBufferHandle_t xStreamBuffer, // 流缓冲区句柄
size_t xTriggerLevel // 新设置的触发水平(字节数)
);

写入数据流

1
size_t xStreamBufferSend(StreamBufferHandle_t xStreamBuffer, const void* pvTxData, size_t xDataLengthBytes, TickType_t xTicksToWait);
  • xStreamBuffer:流缓冲区的句柄(通过 xStreamBufferCreate()创建),指定要写入的目标缓冲区。
  • pvTxData:指向要写入数据的源缓冲区指针(数据会被复制到流缓冲区)。
  • xDataLengthBytes:要写入的数据长度(字节数)。
  • xTicksToWait:超时等待时间(单位:系统节拍)。如果流缓冲区空间不足,任务会阻塞等待最多此时间。特殊值:0:不等待,立即返回。portMAX_DELAY:无限期等待,直到空间可用。

示例

1
2
3
4
5
6
7
8
9
// 创建流缓冲区
StreamBufferHandle_t xStreamBuf = xStreamBufferCreate(100, 1);
uint8_t data[20] = "Hello, FreeRTOS!";

// 尝试写入数据,最多等待100个节拍
size_t bytesWritten = xStreamBufferSend(xStreamBuf, data, sizeof(data), 100);
if (bytesWritten < sizeof(data)) {
// 处理写入不完全的情况(如超时或空间不足)
}

一对一可以安全使用

多对一需要放在临界代码段且超时设置为0

  • 情景模拟任务A 进入临界段,调用 xStreamBufferSend()尝试写入发现流缓冲区空间不足,但因 xTicksToWait非零而阻塞等待**问题:任务A阻塞时仍在临界段内(未执行到 taskEXIT_CRITICAL()任务B 尝试进入同一个临界段,但因任务A持有锁而被阻塞**结果:任务A在等缓冲区空间(需其他任务读取数据),任务B在等临界段锁(需任务A退出临界段)→ 死锁

    为什么设置 xTicksToWait = 0能避免死锁

将超时时间设为0(非阻塞模式)后:

  • 任务调用 xStreamBufferSend()立即返回(不等待)
  • 无论写入是否成功,任务都会快速执行到 taskEXIT_CRITICAL(),释放临界段锁
  • 其他任务可以立即获取锁并尝试写入
  • 即使写入失败,任务也可在临界段外重试或处理背压

xStreamBufferSendFromISR同样多一个调度判断

读取数据流

返回值是实际读取的字节个数

1
size_t xStreamBufferReceiveFromISR(StreamBufferHandle_t xStreamBuffer,                                  void *pvRxData,                                  size_t xBufferLengthBytes,                                  BaseType_t *pxHigherPriorityTaskWoken);

和写入类似,不多赘述

流缓存区状态查询

都以流缓冲区句柄作为输入参数

xStreamBufferBytesAvailable() uint32_t 查询当前存储的数据量(字节数)
xStreamBufferSpacesAvailable() uint32_t 查询剩余可用空间(字节数)
xStreamBufferIsEmpty() BaseType_t 检查缓冲区是否为空
xStreamBufferIsFull() BaseType_t 检查缓冲区是否已满

流缓存区复位

返回是否复位成功

只有没有任务在阻塞状态下的时候才能用

包括

  1. 没有任务因等待读取而阻塞即没有任务正在调用 xStreamBufferReceive()并因缓冲区为空而进入阻塞状态,等待数据到来。
  2. 没有任务因等待写入而阻塞即没有任务正在调用 xStreamBufferSend()并因缓冲区已满而进入阻塞状态,等待空闲空间。

发送完成和接受完成的宏

多核应用:只有在真正需要跨核通信时才应重定义这些宏

单核不用管

具体实例

ADC中断中向缓存区写数据,程序一次读取多个数据后取均值显示

注意需要自己定义

消息缓冲区

并非按字节流写入或读出

需要写入和读出相同字节

消息头是消息字节数unint32_t

消息缓存区没有触发水平,写入和读取都是以一条单位为消息

其他和流缓存区一致

创建消息缓存区

1
2
3
4
5
6
#define xMessageBufferCreate(xBufferSizeBytes) \
(MessageBufferHandle_t)xStreamBufferGenericCreate(
(xBufferSizeBytes), // 缓冲区大小
(size_t)0, // 触发水平(固定为0)
pdTRUE // 创建消息缓冲区标志
)
  • xBufferSizeBytes作用:指定消息缓冲区的总容量(字节数)注意事项:实际可用空间小于此值(每条消息需额外4字节存储长度信息)
  • 触发水平参数(固定为0):消息缓冲区不需要触发水平机制与流缓冲区不同,消息缓冲区以完整消息为单位唤醒等待任务即使只有一条消息(无论大小),也会立即唤醒接收任务
  • pdTRUE参数关键标识:告知底层函数创建的是消息缓冲区(而非流缓冲区)内部会根据此参数设置不同的处理逻辑和数据结构

底层实现:xStreamBufferGenericCreate()

返回值说明 句柄

  • 类型MessageBufferHandle_t
  • 本质:指向消息缓冲区控制结构的指针
  • 使用:后续所有操作(发送、接收、删除)都需使用此句柄
  • 错误处理:创建失败返回NULL(通常因内存不足)

[4字节长度信息] [消息数据内容]

  • 长度头:32位小端格式,存储消息实际长度
  • 自动管理:发送/接收函数自动处理长度信息,对用户透明

按消息唤醒:只要有一条完整消息,立即唤醒接收任务 无触发水平

写入消息

核心函数:xMessageBufferSend()

函数原型(宏定义):

1
2
#define xMessageBufferSend(xMessageBuffer, pvTxData, xDataLengthBytes, xTicksToWait) \
xStreamBufferSend((StreamBufferHandle_t)xMessageBuffer, pvTxData, xDataLengthBytes, xTicksToWait)

参数详解

  • xMessageBuffer:消息缓冲区句柄(通过 xMessageBufferCreate()创建)
  • pvTxData:指向待发送消息数据的指针
  • xDataLengthBytes消息数据的实际长度(字节数)不包括4字节的消息头
  • xTicksToWait:阻塞等待时间0:不等待,立即返回portMAX_DELAY:无限期等待直到有足够空间其他值:等待指定系统节拍数

关键特性

  • 自动添加消息头:函数内部自动在消息数据前添加4字节的uint32_t类型长度头
  • 消息完整性:保证要么写入完整消息,要么完全不写入(与流缓冲区不同)
  • 阻塞行为:空间不足时任务进入阻塞状态,等待其他任务读取数据释放空间

返回值

  • 成功:返回实际写入的消息数据字节数(不包括4字节头)
  • 超时:返回0(与流缓冲区不同,不会部分写入)

    ISR版本:xMessageBufferSendFromISR()

函数原型

1
2
#define xMessageBufferSendFromISR(xMessageBuffer, pvTxData, xDataLengthBytes, pxHigherPriorityTaskWoken) \
xStreamBufferSendFromISR((StreamBufferHandle_t)xMessageBuffer, pvTxData, xDataLengthBytes, pxHigherPriorityTaskWoken)

特殊参数

  • pxHigherPriorityTaskWoken:输出参数,指示是否有更高优先级任务被唤醒如果为pdTRUE,应在ISR退出前调用portYIELD_FROM_ISR()

ISR限制

  • 非阻塞:在中断中立即返回,无视超时参数
  • 必须检查调度标志:根据pxHigherPriorityTaskWoken决定是否触发任务切换
1
2
#define xMessageBufferReceive(xMessageBuffer, pvRxData, xBufferLengthBytes, xTicksToWait) \
xStreamBufferReceive((StreamBufferHandle_t)xMessageBuffer, pvRxData, xBufferLengthBytes, xTicksToWait)
  • xMessageBuffer:消息缓冲区句柄(通过 xMessageBufferCreate()创建),指定要读取的缓冲区。
  • pvRxData:指向接收数据缓冲区的指针,读取的消息数据将复制到此。
  • xBufferLengthBytes:接收缓冲区的最大容量(字节数),限制每次读取的最大数据量。
  • xTicksToWait:阻塞等待时间(单位:系统节拍):0:不等待,立即返回(非阻塞)。portMAX_DELAY:无限期等待,直到有消息可用。其他值:等待指定节拍数后超时。

工作原理

  1. 自动处理消息头:函数内部先读取4字节的消息长度头(uint32_t类型),获取消息的实际长度。
  2. 数据读取:根据长度头的信息,从缓冲区中读取相应字节数的消息数据到 pvRxData
  3. 阻塞行为:如果缓冲区中没有消息,任务进入阻塞状态,等待其他任务发送消息。
  4. 消息移除:成功读取后,该消息从缓冲区中移除(FIFO顺序)。

返回值

  • 成功:返回实际读取的消息数据字节数(不包括4字节长度头)。
  • 超时:返回 0(表示在等待时间内没有消息到达)。
  • 缓冲区太小:如果消息长度超过 xBufferLengthBytes,则只读取部分数据(但返回实际长度,提示用户需要更大缓冲区)。

消息缓存区状态查询

几个函数看书吧

具体实例

读取的消息字节长度不包含自动增加的消息头4字节

​ ·