FreeRTOS信号量

本文档主要介绍FreeRTOS操作系统的信号量概念及其应用,通过正点原子的FreeRTOS源码详解与应用开发进行学习,旨在帮助读者掌握这一实时操作系统的关键组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

——————(正点原子FreeRTOS学习笔记)

开始学习FreeRTOS,学习参考书籍和视频来自正点原子FreeRTOS源码详解与应用开发,北京航空航天大学出版社



1  二值信号量
1.1  二值信号量简介
        二值信号量通常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是还是有一 些细微的差别,互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号 另更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问。
        和队列一样,信号量 API 函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时 候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同 一个信号量上的话那么优先级最高的哪个任务优先获得信号量,这样当信号量有效的时候高优 先级的任务就会解除阻塞状态。
        二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空 的,这不正好就是二值的吗? 任务和中断使用这个特殊队列不用在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。
        使用二值信号量来完成中断与任务同步的这个机制中,任务优先级确保了外设能够得到及 时的处理,这样做相当于推迟了中断处理过程。也可以使用队列来替代二值信号量,在外设事 件的中断服务函数中获取相关数据,并将相关的数据通过队列发送给任务。如果队列无效的话 任务就进入阻塞态,直至队列中有数据,任务接收到数据以后就开始相关的处理过程。下面几 个步骤演示了二值信号量的工作过程。
1 、二值信号量无效
                    
        在图 14.2.1.1 中任务 Task 通过函数 xSemaphoreTake()获取信号量,但是此时二值信号量无
效,所以任务 Task 进入阻塞态。

2 、中断 释放 信号量
                
        此时中断发生了,在中断服务函数中通过函数 xSemaphoreGiveFromISR()释放信号量,因此信 号量变为有效。

3、任务获取信号量成功
                
        由于信号量已经有效了,所以任务 Task 获取信号量成功,任务从阻塞态解除,开始执行相 关的处理过程

4、任务再次进入阻塞态
        由于任务函数一般都是一个大循环,所以在任务做完相关的处理以后就会再次调用函数 xSemaphoreTake()获取信号量。在执行完第三步以后二值信号量就已经变为无效的了,所以任务 将再次进入阻塞态,和第一步一样,直至中断再次发生并且调用函数 xSemaphoreGiveFromISR() 释放信号量。

1.2  创建二值信号量
        同队列一样,要想使用二值信号量就必须先创建二值信号量,二值信号量创建函数 如表 14.2.2 所示:
    

1 、函数 vSemaphoreCreateBinary ()
        此函数是老版本 FreeRTOS 中的创建二值信号量函数,新版本已经不再使用了,新版本的 FreeRTOS 使用 xSemaphoreCreateBinary()来替代此函数,这里还保留这个函数是为了兼容那些 基于老版本 FreeRTOS 而做的应用层代码。此函数是个宏,具体创建过程是由函数 xQueueGenericCreate()来完成的,在文件 semphr.h 中有如下定义:
     void vSemaphoreCreateBinary( SemaphoreHandle_t xSemaphore )
参数:
    xSemaphore:保存创建成功的二值信号量句柄。
返回值:
    NULL: 二值信号量创建失败。
    其他值: 二值信号量创建成功。

2 、函数 xSemaphoreCreateBinary()
    此函数是 vSemaphoreCreateBinary()的新版本,新版本的 FreeRTOS 中统一用此函数来创建 二值信号量。使用此函数创建二值信号量的话信号量所需要的 RAM 是由 FreeRTOS 的内存管 理部分来动态分配的。此函数创建好的二值信号量默认是空的,也就是说刚创建好的二值信号 量使用函数 xSemaphoreTake()是获取不到的,此函数也是个宏,具体创建过程是由函数 xQueueGenericCreate()来完成的,函数原型如下:
     SemaphoreHandle_t xSemaphoreCreateBinary( void )
参数:
    无。
返回值:
    NULL: 二值信号量创建失败。
    其他值: 创建成功的二值信号量的句柄。

3 、函数 xSemaphoreCreateBinaryStatic()
      此函数也是创建二值信号量的,只不过使用此函数创建二值信号量的话信号量所需要的 RAM 需要由用户来分配,此函数是个宏,具体创建过程是通过函数 xQueueGenericCreateStatic() 来完成的,函数原型如下:
     SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )
参数:
    pxSemaphoreBuffer: :此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。
返回值:
    NULL: 二值信号量创建失败。
    其他值: 创建成功的二值信号量句柄。


1.3  二值信号量创建过程分析
        上一小节讲了三个用于二值信号量创建的函数,两个动态的创建函数和一个静态的创建函 数。本节就来分析一下这两个动态的创建函数,静态创建函数和动态的类似,就不做分析了。 首先来看一下老版本的二值信号量动态创建函数 vSemaphoreCreateBinary(),函数代码如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define vSemaphoreCreateBinary( xSemaphore ) \
{  \
    ( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, \  (1)
    semSEMAPHORE_QUEUE_ITEM_LENGTH, \
    queueQUEUE_TYPE_BINARY_SEMAPHORE );  \
    if( ( xSemaphore ) != NULL ) \
    {  \
        ( void ) xSemaphoreGive( ( xSemaphore ) ); \  (2)
    }  \
}
#endif
(1)、上面说了二值信号量是在队列的基础上实现的,所以创建二值信号量就是创建队列的 过程。这里使用函数 xQueueGenericCreate()创建了一个队列,队列长度为 1,队列项长度为 0, 队列类型为 queueQUEUE_TYPE_BINARY_SEMAPHORE,也就是二值信号量。
(2)、当二值信号量创建成功以后立即调用函数 xSemaphoreGive()释放二值信号量,此时新
创建的二值信号量有效。 在来看一下新版本的二值信号量创建函数 xSemaphoreCreateBinary(),函数代码如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary() \
    xQueueGenericCreate( ( UBaseType_t ) 1,  \
    semSEMAPHORE_QUEUE_ITEM_LENGTH,  \
    queueQUEUE_TYPE_BINARY_SEMAPHORE ) \
#endif
    可以看出新版本的二值信号量创建函数也是使用函数 xQueueGenericCreate()来创建一个类 型为queueQUEUE_TYPE_BINARY_SEMAPHORE、长度为 1、队列项长度为 0 的队列。这一步 和老版本的二值信号量创建函数一样,唯一不同的就是新版本的函数在成功创建二值信号量以 后不会立即调用函数 xSemaphoreGive()释放二值信号量。也就是说新版函数创建的二值信号量 默认是无效的,而老版本是有效的。
    大家注意看,创建的队列是个没有存储区的队列,前面说了使用队列是否为空来表示二值 信号量,而队列是否为空可以通过队列结构体的成员变量 uxMessagesWaiting 来判断。

1.4  释放信号量
    释放信号量的函数有两个,如表 14.2.4.1 所示:
    
        同队列一样,释放信号量也分为任务级和中断级。还有!不管是二值信号量、计数型信号 量还是互斥信号量,它们都使用表 14.2.4.1 中的函数释放信号量,递归互斥信号量有专用的释 放函数。

1、数 函数 xSemaphoreGive()
    此函数用于释放二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正释放信 号量的过程是由函数   xQueueGenericSend()来完成的,函数原型如下:
     BaseType_t xSemaphoreGive( xSemaphore )
参数:
    xSemaphore :要释放的信号量句柄。
返回值:
    pdPASS: 释放信号量成功。
    errQUEUE_FULL: 释放信号量失败。

        我们再来看一下函数 xSemaphoreGive()的具体内容,此函数在文件 semphr.h 中有如下定义:
#define xSemaphoreGive( xSemaphore )  \
    xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \
    NULL, \
    semGIVE_BLOCK_TIME, \
    queueSEND_TO_BACK )  \
        可以看出任务级释放信号量就是向队列发送消息的过程,只是这里并没有发送具体的消息, 阻塞时间为 0(宏 semGIVE_BLOCK_TIME 为 0),入队方式采用的后向入队。具体入队过程第十 三章已经做了详细的讲解,入队的时候队列结构体成员变量 uxMessagesWaiting 会加一,对于二 值信号量通过判断 uxMessagesWaiting 就可以知道信号量是否有效了,当 uxMessagesWaiting 为 1 的话说明二值信号量有效,为 0 就无效。如果队列满的话就返回错误值 errQUEUE_FULL,提 示队列满,入队失败。

2 、函数 xSemaphoreGiveFromISR()
        此函数用于在中断中释放信号量,此函数只能用来释放二值信号量和计数型信号量,绝对 不能用来在中断服务函数中释放互斥信号量!此函数是一个宏,真正执行的是函数
xQueueGiveFromISR(),此函数原型如下:
    BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
                                                                BaseType_t * pxHigherPriorityTaskWoken)
参数:
    xSemaphore:  要释放的信号量句柄。
    pxHigherPriorityTaskWoken:  标记退出此函数以后是否进行任务切换,这个变量的值由这
        三个函数来设置的,用户不用进行设置,用户只需要提供一 个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退 出中断服务函数之前一定要进行一次任务切换。
返回值:
    pdPASS: 释放信号量成功。
    errQUEUE_FULL: 释放信号量失败。
        在中断中释放信号量真正使用的是函数 xQueueGiveFromISR(),此函数和中断级通用入队 函数 xQueueGenericSendFromISR()极其类似!只是针对信号量做了微小的改动。函数 xSemaphoreGiveFromISR()不能用于在中断中释放互斥信号量,因为互斥信号量涉及到优先级继 承的问题,而中断不属于任务,没法处理中断优先级继承。大家可以参考第十三章分析函数
xQueueGenericSendFromISR()的过程来分析 xQueueGiveFromISR()。

1.5  获取信号量
        获取信号量也有两个函数,如表 14.2.5.1 所示:

        同释放信号量的 API 函数一样,不管是二值信号量、计数型信号量还是互斥信号量,它们 都使用表 14.2.5.1 中的函数获取信号量。

1 、函数 xSemaphoreTake()
        此函数用于获取二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正获取信 号量的过程是由函数 xQueueGenericReceive ()来完成的,函数原型如下:
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
                                                TickType_t xBlockTime)
参数:
    xSemaphore :要获取的信号量句柄。
    xBlockTime: 阻塞时间。
返回值:
    pdTRUE: 获取信号量成功。
    pdFALSE: 超时,获取信号量失败。
    再来看一下函数 xSemaphoreTake ()的具体内容,此函数在文件 semphr.h 中有如下定义:
#define xSemaphoreTake( xSemaphore, xBlockTime ) \
    xQueueGenericReceive( ( QueueHandle_t ) ( xSemaphore ),  \
    NULL, \
    ( xBlockTime ), \
    pdFALSE ) \
    获取信号量的过程其实就是读取队列的过程,只是这里并不是为了读取队列中的消息。在 第十三章讲解函数 xQueueGenericReceive()的时候说过如果队列为空并且阻塞时间为 0 的话就 立即返回 errQUEUE_EMPTY,表示队列满。如果队列为空并且阻塞时间不为 0 的话就将任务 添加到延时列表中。如果队列不为空的话就从队列中读取数据(获取信号量不执行这一步),数 据读取完成以后还需要将队列结构体成员变量 uxMessagesWaiting 减一,然后解除某些因为入 队而阻塞的任务,最后返回 pdPASS 表示出对成功。互斥信号量涉及到优先级继承,处理方式 不同,后面讲解互斥信号量的时候在详细的讲解。
<think>我们正在讨论FreeRTOS信号量的使用。根据引用内容,信号量FreeRTOS中用于任务间同步和资源管理的重要机制。引用[1]提到信号量可以解决资源共享和任务同步问题,引用[4]详细解释了信号量的P、V操作(即获取和释放)。此外,引用[3]和引用[5]提供了创建和删除信号量的函数示例。用户要求提供使用教程和示例代码。因此,我将按照以下步骤组织回答:1.介绍信号量的基本概念和类型(二进制信号量、计数信号量)。2.说明如何创建信号量。3.展示如何在任务中获取(take)和释放(give)信号量。4.提供示例代码,展示信号量用于资源管理和任务同步的两种典型场景。5.注意事项(如避免优先级反转、正确删除信号量等)。根据引用[2],需要注意互斥信号量和二进制信号量的区别:互斥信号量具有优先级继承,且必须归还;而用于同步的信号量通常获取后不归还(取决于设计)。引用[5]提供了删除信号量的函数。下面开始逐步回答:</think>### FreeRTOS 信号量使用教程 #### 一、信号量基础概念 信号量FreeRTOS 实现**任务同步**和**资源共享**的核心机制,本质是带保护的计数器。主要类型包括: 1. **二进制信号量**:值仅 0 或 1,用于互斥访问(如共享外设) 2. **计数信号量**:值 ≥0,用于资源池管理(如缓冲区槽位) 3. **互斥信号量**:带优先级继承的二进制信号量,解决优先级反转问题[^2] 信号量操作遵循 **P/V 操作**原则: - **P 操作 (xSemaphoreTake)**:申请资源,信号量值减 1 - **V 操作 (xSemaphoreGive)**:释放资源,信号量值加 1 当信号量为负时,表示等待资源的任务数量[^4] --- #### 二、关键 API 函数 | 函数 | 作用 | 参数说明 | |------|------|----------| | `xSemaphoreCreateBinary()` | 创建二进制信号量 | 返回信号量句柄 | | `xSemaphoreCreateCounting(uxMaxCount, uxInitialCount)` | 创建计数信号量 | 最大计数值/初始值 | | `xSemaphoreTake(xSemaphore, xBlockTime)` | 获取信号量 | 阻塞时间(ticks) | | `xSemaphoreGive(xSemaphore)` | 释放信号量 | - | | `vSemaphoreDelete(xSemaphore)` | 删除信号量 | - | --- #### 三、典型应用场景与示例代码 ##### 场景 1:资源互斥(二进制信号量) 保护 UART 串口避免多任务同时写入: ```c SemaphoreHandle_t uartMutex; // 全局信号量句柄 void Task1(void *params) { while(1) { if(xSemaphoreTake(uartMutex, portMAX_DELAY)) { HAL_UART_Transmit(&huart1, "Task1\n", 6, 100); xSemaphoreGive(uartMutex); // 必须释放! } vTaskDelay(pdMS_TO_TICKS(100)); } } void main() { uartMutex = xSemaphoreCreateBinary(); xSemaphoreGive(uartMutex); // 初始化为可用状态 xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL); vTaskStartScheduler(); } ``` > 📌 **关键点**:互斥信号量使用后必须释放[^2],否则会导致死锁 ##### 场景 2:任务同步(计数信号量) 生产者-消费者模型: ```c #define BUFFER_SIZE 5 SemaphoreHandle_t dataSemaphore; void ProducerTask(void *params) { while(1) { produce_data(); // 生产数据 xSemaphoreGive(dataSemaphore); // 释放信号量(资源+1) vTaskDelay(pdMS_TO_TICKS(200)); } } void ConsumerTask(void *params) { while(1) { if(xSemaphoreTake(dataSemaphore, portMAX_DELAY)) { consume_data(); // 消费数据 } } } void app_main() { // 创建计数信号量(最大5,初始0) dataSemaphore = xSemaphoreCreateCounting(BUFFER_SIZE, 0); xTaskCreate(ProducerTask, "Prod", 128, NULL, 1, NULL); xTaskCreate(ConsumerTask, "Cons", 128, NULL, 2, NULL); } ``` > ⚡ **同步机制**:生产者释放信号量唤醒消费者,初始值 0 使消费者阻塞直至数据就绪[^3] --- #### 四、最佳实践 1. **超时设置**:避免永久阻塞,例如 `xSemaphoreTake(sem, pdMS_TO_TICKS(100))` 2. **优先级继承**:互斥场景优先使用 `xSemaphoreCreateMutex()` 3. **中断安全**: - 中断中释放信号量用 `xSemaphoreGiveFromISR()` - 配合 `portYIELD_FROM_ISR()` 触发任务切换 4. **删除保护**:确保无任务等待时再调用 `vSemaphoreDelete()`[^5] > 💡 **调试技巧**:FreeRTOS 的 `uxSemaphoreGetCount()` 可实时查看信号量计数值
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值