讲师博文
深入剖析:FreeRTOS信号量在设备通信中的工程细节 来源 : 华清远见     2026-05-22

在嵌入式多任务系统中,设备通信(如UART、 I2C、SPI、CAN)既是数据交换的通道,也是任务之间、任务与中断之间产生交互与冲突的核心区域。  FreeRTOS提供的二进制信号量、计数信号量和互斥信号量,看似简单,但在实际通信系统中的运用远不止“拿锁放锁”这么浅显。本文将深入三个典型场景,剖析其内部机制、典型代码模式以及工程中容易被忽视的陷阱。

一、二进制信号量:构建“中断-任务” 同步的精确握手

场景:微控制器通过USART接收外部设备数据,每收到一个字节触发中断。中断服务程序(ISR)不应执行复杂解析,但必须尽快将数据交给任务处理。

常规错误做法:在ISR中直接将数据写入任务使用的全局缓冲区,然后通过一个标志变量通知任务。缺乏阻塞机制,导致任务需要不断轮询标志,浪费CPU。

正确模式:创建二进制信号量(初始值为0)。 ISR中把接收到的字节放入队列(或简单变量)后调用  xSemaphoreGiveFromISR()释放信号量;任务则调用 xSemaphoreTake()等待,拿到信号量后再读取数据并处理。

关键工程细节:

1. “一个字节一个信号量” vs “一组数据一个信号量” :若每个字节都释放一次信号量,频繁任务切换会导致效率低下。合理的做法是在ISR中将接收到的字节存入环形缓冲区,当累积到一帧数据(如遇到换行符或达到帧长度)时,才释放一次信号量。任务被唤醒后一次性处理整帧数据。

2. xHigherPriorityTaskWoken  的正确使用: xSemaphoreGiveFromISR 有一个输出参数,若释放操作导致更高优先级任务解除阻塞,该参数会被置为 pdTRUE ,此时应在ISR退出前执行,portYIELD_FROM_ISR() ,立即切换到该任务,避免延迟。

3. 超时机制:任务调用 xSemaphoreTake( sem, timeout ) 时,若超过设定时间未收到信号量(如设备故障、数据丢失),可以执行错误恢复,例如复位外设或发送重试请求。

代码骨架示例:

二、互斥信号量:保护共享通信外设,深度解析优先级继承

场景:三个任务(中等优先级Task_Middle,高优先级Task_High,低优先级Task_Low)共享同一个SPI总线,用于读写外部传感器。 Task_Low先获取互斥量开始传输,中途Task_High抢占并试图获取同一互斥量。

如果不使用互斥量而用二进制信号量:Task_Low持有二进制信号量(此时值为0),Task_High请求该信号量时因无法获得而进入阻塞态(挂起)。但此时存在一个中等优先级Task_Middle(不需要SPI),它会持续运行,导致Task_High一直得不到CPU,直到Task_Low释放信号量。而Task_Low由于被Task_Middle抢占,释放信号量的时间被无限推迟——这就是经典的优先级反转。

互斥量的解决方案:当Task_High因等待被Task_Low持有的互斥量而阻塞时, FreeRTOS会临时将Task_Low的优先级提升到与Task_High相同(继承优先级)。这样Task_Low不会被Task_Middle抢占,能够连续运行尽快释放互斥量,从而大幅缩短Task_High的等待时间。释放互斥量后, Task_Low的优先级恢复原值。

工程实践要点:

1. 不要用于ISR:互斥量的“give”和“take”不允许在中断中使用,因为优先级继承逻辑依赖任务调度上下文,强行使用会触发断言。

2. 递归互斥量:若同一任务需要多次获取同一互斥量(如嵌套调用函数),应使用 xSemaphoreCreateRecursiveMutex() ,避免死锁。

3. 避免“持有互斥量时阻塞” :如果一个任务获取互斥量后又去等待另一个事件(如队列、延迟),这违背了互斥量的设计初衷,容易导致系统难以调试。

三、计数信号量:管理通信资源池与“生产者-消费者”流量控制

场景A:多个同质资源。例如设备有两个独立的DMA通道用于串口发送,最多允许两个任务同时发送数据。计数信号量初始值为2,每个任务发送前 take ,完成后 give。当值为0时,其他任务自动阻塞。这相当于一种轻量级的“资源计数器”。

场景B:生产者-消费者中的可消耗事件计数。例如网络通信协议栈:网卡中断每收到一个数据包就放入接收队列,并 give计数信号量;上层任务 take计数信号量,若计数值大于0,则表示有待处理的数据包,依次处理。这种方法比二进制信号量更高效:二进制信号量只能表示“有数据包” ,而计数信号量可以精确指示累积的未处理包数量。

代码核心:

注意:由于计数信号量只指示“个数” ,具体数据仍需通过队列或环形缓冲区传递,不可将信号量与数据存储混为一谈。

四、综合陷阱与高级调试

1. 优先级天花板:互斥量默认的优先级继承机制能解决反转,但不适用于实时性极高的系统(因为继承过程有短暂延迟)。 FreeRTOS同时支持“优先级天花板”策略——互斥量关联一个最高优先级,任何任务获取后临时提升到该优先级。可以在创建互斥量时通过 xSemaphoreCreateMutexStatic配合参数设定。

2. 信号量与队列的抉择:单纯同步使用信号量;传递数据用队列;既同步又传数据一般用队列(队列自带计数和阻塞机制)。但队列比信号量稍重,对极简场景可用信号量+全局缓冲区的组合。

3. 死锁预防:设备通信中常见死锁:任务A获取互斥量M1去访问设备,等待任务B释放M2;任务B持有M2等待M1。解决:统一资源请求顺序,或使用 xSemaphoreTake 的超时机制返回失败后主动释放已占有的资源。

4. 调试手段: FreeRTOS提供 uxSemaphoreGetCount()可实时查看计数信号量当前值;配合vTaskList()输出的任务状态中,阻塞等待信号量的任务会显示为“B”(Blocked),方便快速定位。

五、信号量起源:消息队列

本文将详细全方位的讲解FreeRTOS的队列消息,其实在FreeRTOS中队列的重要性也不言而喻,与FreeRTOS任务调度同等重要,因为后面的各种信号量基本都是基于队列的。本文主要围绕(队列的创建、队列的出队、入队函数、队列的环形缓冲区的实现(数据的拷贝、先进先出、以及后进先出的实现)、队列锁、以及队列任务级函数与中断级函数的区别)队列的重要知识,本文都会以源码分析的形式进行解析。

一.消息队列 的特点

由于队列的知识也比较多,这里先进行一个简单的总结,把我们即将要学的知识点进行简单的概括。

1.队列的基本概念

消息队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息(而这个消息可以是任意类型的数据),任务能够从队列里面读取消息,也能够向队列发送消息。

基于队列, FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、递归互斥信号量,所以掌握队列就显得十分重要。

如下图所示, 一个队列可以很多任务来写队列也可以很多任务来读队列。但是并不能两个队列同时来写或读队列。就与全局变量不能多个任务来同时读写,那样会出问题,而队列就是通过关中断的方式来保证队列同一时间只能一个任务进行读写。因为关中断:任务无法切换,且一些中断也无法来干扰。(后面源码中会有体现)

队列的特点:

1.一般情况下队列消息是先进先出方式排队(当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据),但同时 FreeRTOS的队列也支持将数据写入到队列的头部,并且还可以指定是否覆盖先前已经在队列头部的数据。  (后面会详细讲解pcHead、pcTail、 pcWriteTo、 pcReadFrom的指向关系,明白了这些就明白了队列环形缓冲区)

2. 队列传输数据时有两种方法: 1. 直接拷贝数据 2.拷贝数据的地址,然后根据地址读取数据。  第二种方法适合传输大数据比如一个大数组, 或者一个结构体变量。

3.队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。

4.读写队列均支持阻塞机制(以读队列为例:在任务从队列读取消息时,可以指定一个阻塞超时时间。如果队列不为空则会读取队列中第一个消息(通过拷贝的方式: memcpy函数),如果队列为空,则看我们自己设置阻塞时间(1.阻塞时间为0:即刻返回队列空错误。 2.阻塞时间不为0:假设阻塞时间为20ms,刚开始队列为空,则任务进入阻塞,如果在20ms内有消息入队了即该任务会被唤醒然后读取消息,如果在20ms内还没有消息则任务就不会再等待直接从阻塞态中唤醒,返回队列空错误。  3.阻塞时间为最大:任务死等进入阻塞态,直到完成读取队列的消息。 写队列过程基本一致,不过一个的队列空一个是队列满。 )

5.当在中断中读写队列时,如果队列空或满,不会进行阻塞,直接返回队列空或队列满错误,因为中断要的就是快进快出。 (后面源码解析会详细阐述任务中使用队列与中断中使用队列的区别,以及与中断息息相关的队列锁)

上述队列特点只是大概介绍一下,更多的细节看下面源码分析,保证搞得透透彻彻、明明白白的。

六、信号量详解

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问,其实信号量主要的功能就是实现任务之间的同步与互斥,实现的方式主要就是依靠队列(信号量是特殊的队列)的任务阻塞机制。

既然队列也可以实现同步与互斥那为什么还要信号量?

答:信号量相比队列更节省空间,因为实现同步与互斥不需要传递数据,所以信号量没有队列后面的环形存储区,信号量主要就是依靠计数值uxMessagesWaiting (在队列中表示队列现有消息个数,在信号量中表示有效信号量个数)。

什么是同步与互斥?

1.同步

比如说,买包子

我要去买包子,如果包子店没有包子了,则需要等待卖包子的把包子做出来我才能买到包子,这个等待的过程就叫做同步。 (在实际应用中: 一个采集数据的传感器任务, 一个处理数据的任务,则处理数据的任务需要等待传感器去采用数据,则在FreeRTOS系统中等待不能干等着,在该任务等待的过程中,  CPU转而可以去执行其他任务,则就可以提高效率,则就是队列的阻塞机制)

2.互斥

比如说,抢厕所

厕所只有一个,一个人进去上了,另一个人也要上,则必须等待前人上完厕所才能上,等待的过程就是同步,而保护厕所的过程叫做互斥,则厕所就是所谓临界资源,同一时间只能一个人使用厕所,当然前人上完厕所应该提醒等待的人,厕所用完了可以上了,其中本质也是阻塞机制。

uxMessagesWaiting作为复用在信号量中表示资源的数量,所有获取信号量的任务都会将该整数减一,当该整数值为零时,则此时想要获取的任务则会进入阻塞态,释放信号量的任务都会将该整数加一,不过当该整数值为最大值时(最大值要看你是什么信号量),则此时想要释信号量的任务则并不会进入阻塞态,直接返回释放信号量失败。

接下来就分别介绍二值信号量、计数信号量、互斥信号量、递归互斥信号量、它们的应用场景、特殊机制、以源码分析的方式,深入理解信号量!!!

创建信号量就对应创建特殊队列,获取信号量就对应队列出队,释放信号量就对应队列入队,学好了队列就基本学好了信号量,所以这一章主要是针对互斥量(优先级反转、优先级继承、递归互斥信号量)。

FreeRTOS信号量在设备通信中主要用于任务同步和资源共享控制,尤其适用于多任务环境下对硬件外设(如串口、网络接口、传感器等)的协调访问。以下是其核心应用场景及实现方式:

七、典型设备通信应用场景

1. 串口通信(不定长数据接收)

使用二值信号量实现串口空闲中断与数据处理任务的同步。

流程:

串口接收中断触发(每收到一个字节)。

收到完整帧后, 空闲中断释放二值信号量( xSemaphoreGiveFromISR() )。

数据处理任务等待信号量( xSemaphoreTake() ),获取后解析数据。

2.网络协议栈(如以太网)

网络任务阻塞等待二值信号量。

MAC层中断收到数据包后释放信号量,唤醒网络任务处理。

3.多设备并发访问

使用计数信号量管理有限资源池(如4个串口、 10个网络连接)。

初始值设为资源总数,任务使用前获取信号量,使用后释放。

4.传感器数据采集

采集任务完成一次采样后释放信号量。

处理任务获取信号量后执行分析,实现生产者-消费者同步

结论

FreeRTOS信号量在设备通信中远不止“锁”的简单角色。二进制信号量是中断与任务之间的精确握手信号,互斥量通过优先级继承解决了共享外设访问中的反转难题,计数信号量则灵活管理多资源池和事件累计。深入理解它们的内部实现、适用边界以及常见陷阱,才能在实时通信系统中做到既高效又可靠。在实际项目中,往往需要将信号量与队列、任务通知(Task Notification)等其他IPC机制结合,才能构造出真正健壮的通信架构。

扫码申领本地嵌入式教学实录全套视频及配套源码

上一篇:SPI通信优化:硬件SPI vs 软件SPI的对比与选型

下一篇:注意力机制深度拆解:从 Soft-Attention 到 Self-Attention 的技术演进

400-611-6270

Copyright © 2004-2024 华清远见教育科技集团 版权所有
京ICP备16055225号-5京公海网安备11010802025203号