freertos信号量

信号量简介

信号量(Semaphore)是一种非常重要的同步方式,用于任务之间的同步和共享资源的管理。

信号量类型

FreeRTOS 支持多种类型的信号量,每种类型适用于不同的场景:

  1. 二值信号量(Binary Semaphore)

    • 二值信号量是最简单的信号量类型,只有两种状态:0 和 1。
    • 通常用于任务间的同步,确保某一事件的发生。
    • 当信号量值为 1 时,表示资源可用;为 0 时,表示资源不可用。
    • 任务获取信号量时,若信号量值为 1,则将其设为 0 并继续执行;若信号量值为 0,则任务将阻塞,直到信号量值变为 1。
  2. 计数信号量(Counting Semaphore)

    • 计数信号量可以保存任意非负整数值。
    • 通常用于控制对有限数量资源的访问,例如缓冲区或硬件设备。
    • 任务获取信号量时,若信号量值大于 0,则将其减 1 并继续执行;否则任务将阻塞。
    • 信号量可以由任务或中断服务程序来递增其值。
  3. 互斥信号量(Mutex Semaphore)

    • 互斥信号量用于保护临界区代码或共享资源,确保一次只有一个任务能访问。
    • 与二值信号量类似,但它具有优先级继承机制,可以用来解决优先级反转的问题。
    • 一旦任务获得了互斥信号量,它就能保持对该资源的独占访问,直到它释放信号量。
  4. 递归互斥信号量(Recursive Mutex Semaphore)

    • 递归互斥信号量允许一个已经拥有信号量的任务再次获取它而不陷入死锁。
    • 每次获取信号量时都会增加一个计数器,只有当计数器减少到 0 时,信号量才会被释放。
    • 这种信号量适用于需要多次获取和释放同一资源的情况。

信号量的使用场景

信号量在 FreeRTOS 中通常用于以下情况:

  • 任务同步:当一个任务完成某项工作后,可以使用信号量通知另一个任务该工作已完成。
  • 资源共享:多个任务需要访问一个资源时,可以使用信号量来确保一次只有一个任务能访问。
  • 延时中断:考虑使用任务为外围设备提供服务的情形。轮询外围设备将会耗费 CPU 资源, 阻止执行其他任务。因此, 最好让任务大部分时间处于阻塞状态(允许其他任务执行), 只有在确实有事情需要执行时才执行自身。可以通过使用二进制信号量来实现, 方法是“获取”信号量时使任务阻塞。然后为外围设备编写中断例程, 当外围设备需要服务时,只是“提供”信号量。任务 始终“接收”信号量(从队列中读取信号以使队列变空),但从不“提供”信号量。中断 始终“提供”信号量(将写入队列使其为满),但从不获取信号量。
    中断例程可以捕获与外设事件关联的数据并将其发送到任务的队列中。队列数据可用时, 任务将取消阻塞,从队列中检索数据, 然后执行必要的数据处理。此第二种方案要求中断尽可能短, 在一个任务中进行所有后置处理。
    binary-semaphore.gif

信号量 API

在 FreeRTOS 中,信号量的操作通常通过一组 API 函数来完成,这些函数包括但不限于:

  • 创建信号量:二值信号量创建xSemaphoreCreateBinary()、计数信号量创建xSemaphoreCreateCounting()、互斥信号量创建xSemaphoreCreateMutex() 等。
  • 获取信号量xSemaphoreTake() 用于获取信号量。
  • 释放信号量xSemaphoreGive() 用于释放信号量。
  • 获取递归信号量xSemaphoreTakeRecursive()xSemaphoreGiveRecursive() 用于递归互斥信号量。

通过合理使用这些信号量,可以有效地同步任务间的操作,管理对共享资源的访问,从而提高系统的稳定性和效率。

信号量使用

信号量是实时操作系统(RTOS)中常用的一种同步机制,用于协调多个任务之间的访问顺序,特别是在共享资源或临界区代码的情况下。下面通过几个具体的使用案例来说明信号量在不同场景下的应用。

案例 1:任务间同步

  • 场景描述

假设有两个任务,串口中断A 负责收集传感器的数据,Task B 负责处理这些数据。每当 串口中断A 收集到新的数据时,需要通知 Task B 进行处理。

  • 解决方案

使用一个二值信号量来进行任务间的同步。这里我使用到了之前写的环形队列,这样就可以 串口中断A 接收数据存入环形缓冲区,然后当数据到达一定量的时候再去通知 Task B 这样就不用每收到一个字符就去处理一次了,使得串口的执行效率更高了。

  • 代码示例
c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 环形fifo
static Fifo_t U1;
#define maxNum (100)
//static
uint8_t resbuff[maxNum];

// 二值信号量
SemaphoreHandle_t xDataReadySemaphore;

void UART1_RxCpltCallback(UART_HandleTypeDef *huart);
void vTaskB(void *pvParameters);

// 初始化接收
void SemphrInit(void)
{

FifoInit(&U1, (uint8_t *)resbuff, maxNum);
// 创建一个二值信号量,初值为 0,表示没有新数据
xDataReadySemaphore = xSemaphoreCreateBinary();
// 注册串口1接收回调函数
HAL_UART_RegisterCallback(&huart1, HAL_UART_RX_COMPLETE_CB_ID, UART1_RxCpltCallback);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); // 开启串口非空中断
xTaskCreate(vTaskB, "taskb", 128, NULL, 4, NULL);

vTaskStartScheduler();

}

// 中断A
// 串口接收回调函数 此函数在串口非空中断时被hal库自动调用 在次任务中接收数据
void UART1_RxCpltCallback(UART_HandleTypeDef *huart)
{
static uint16_t count = 0;
count += FifoIn(&U1, (uint8_t *)(&huart->Instance->DR), 1); // 数据入队 并累加数量

if (count >= maxNum / 2)
{
count = 0;
BaseType_t pxHigherPriorityTaskWoken; // 有高优先级任务要调度标志
xSemaphoreGiveFromISR(xDataReadySemaphore, &pxHigherPriorityTaskWoken);
portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}
}

uint8_t tmp[maxNum / 2];
// 任务B
void vTaskB(void *pvParameters)
{
for (;;)
{
// 等待数据准备好的信号量
if (xSemaphoreTake(xDataReadySemaphore, portMAX_DELAY) == pdTRUE)
{
// 处理数据

FifoOut(&U1, tmp, maxNum / 2);
for (uint16_t i = 0; i < maxNum / 2; i++)
{
// tmp[i]++;
printf("%c ", tmp[i]);

if (i % 10 == 0)
{
printf("\r\n");

}
}
}

}
}

案例 2:控制有限资源的访问

  • 场景描述

有一个停车场有5个车位,使用计数信号量来管理5个车位的使用情况,保证在车库满的情况下不能继续入库,保证在车库空的情况下不能继续出库。

  • 解决方案

使用一个计数信号量来记录车库数量。

  • 代码示例
c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

void vTaskC(void *pvParameters)
{
static PressEvent btn1_event_val = NONE_PRESS;
static PressEvent btn2_event_val = NONE_PRESS;

BaseType_t re;
while (1)
{
// 判断按键按下
if (btn1_event_val != get_button_event(&MKEY1))
{
btn1_event_val = get_button_event(&MKEY1);
if (btn1_event_val == PRESS_DOWN)
{
re = xSemaphoreTake(xDataReadySemaphorecount, 0);

if (re == pdTRUE)
{
printf("车库出去一辆车\r\n");
}
else
{
printf("车库没车了\r\n");
}
}
}

if (btn2_event_val != get_button_event(&MKEY2))
{
btn2_event_val = get_button_event(&MKEY2);
if (btn2_event_val == PRESS_DOWN)
{
re = xSemaphoreGive(xDataReadySemaphorecount);
if (re == pdTRUE)
{
printf("车库进入一辆车\r\n");
}
else
{
printf("车库车满了\r\n");
}
}
}
vTaskDelay(10);
}
}

通过这些案例,我们可以看到信号量在协调任务之间共享资源、同步操作和保护临界区方面的重要作用。正确的信号量使用可以有效地提高系统的可靠性和实时性。

互斥信号量使用

优先级反转

当高优先级任务因为低优先级任务持有某个资源(如互斥信号量)而无法执行时,就会发生优先级反转现象。
优先级反转(Priority Inversion)是在多任务操作系统(尤其是实时操作系统)中常见的一个问题。当高优先级任务因为低优先级任务持有某个资源(如互斥信号量)而无法执行时,就会发生优先级反转现象。这种现象会导致系统的响应时间变长,影响实时性的保证。

  • 优先级反转的例子

假设在一个系统中有三个任务:Task A、Task B 和 Task C,它们的优先级依次降低,即 Task A > Task B > Task C。

  1. 初始状态

    • Task A 正在运行。
    • Task B 正处于就绪状态,但优先级低于正在运行的 Task A。
    • Task C 正处于就绪状态,但优先级最低,因此也处于等待状态。
  2. 发生优先级反转

    • Task A 需要访问一个共享资源,并尝试获取一个互斥信号量。
    • 与此同时,Task C 开始运行,并成功获取了该互斥信号量。
    • Task A 因为无法获取互斥信号量而阻塞。
    • 此时,Task B 被调度运行,但由于 Task C 持有互斥信号量,Task A 仍然不能运行。

在这种情况下,尽管 Task A 的优先级最高,但由于 Task C 持有资源,Task A 被迫等待。同时,Task B 也无法执行,因为它仍然低于 Task A 的优先级。这就形成了优先级反转的现象。

  • 解决优先级反转的方法

为了防止优先级反转的发生,可以采取以下措施:

  1. 优先级继承(Priority Inheritance)

    • 优先级继承机制允许一个低优先级任务暂时借用高优先级任务的优先级。
    • 在上述例子中,当 Task C 获取互斥信号量时,它的优先级将被提升到与 Task A 相同的优先级,这样 Task C 就会优先于 Task B 运行,而一旦 Task C 释放了互斥信号量,Task A 就可以立即运行。
  2. 优先级天花板(Priority Ceiling)

    • 优先级天花板机制为每个资源分配了一个固定的优先级上限。
    • 在 Task C 获取互斥信号量时,所有可能访问该资源的任务的优先级都会被提升到这个固定的上限。
    • 这样可以确保高优先级任务不会被低优先级任务阻止。
  • 实现优先级继承

在 FreeRTOS 中,优先级继承是通过互斥信号量(Mutex)来实现的。当一个任务获取了一个互斥信号量后,如果发现有更高优先级的任务正等待这个信号量,那么当前任务的优先级就会被提升到那个等待任务的优先级,直到释放信号量为止。

案例 3:保护临界区

  • 场景描述

假设有一个全局变量,多个任务需要读写这个变量,为了避免并发访问时产生冲突,需要保护这个全局变量所在的临界区。

  • 解决方案

使用互斥信号量来保护临界区。

  • 代码示例
c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

SemaphoreHandle_t xMutexSemaphore;
volatile int xGlobalVariable = 0; // 全局变量

void vMutexInit(void) {
// 创建一个互斥信号量
xMutexSemaphore = xSemaphoreCreateMutex();
}

// 任务A
void vTaskA(void *pvParameters) {
for(;;) {
// 获取互斥信号量
if(xSemaphoreTake(xMutexSemaphore, portMAX_DELAY) == pdTRUE) {
// 进入临界区
xGlobalVariable++; // 修改全局变量
// 退出临界区
xSemaphoreGive(xMutexSemaphore);
}
}
}

// 任务B
void vTaskB(void *pvParameters) {
for(;;) {
// 获取互斥信号量
if(xSemaphoreTake(xMutexSemaphore, portMAX_DELAY) == pdTRUE) {
// 进入临界区
xGlobalVariable--; // 修改全局变量
// 退出临界区
xSemaphoreGive(xMutexSemaphore);
}
}
}

在这个例子中,vMutexInit 初始化了一个互斥信号量。vTaskAvTaskB 分别代表两个需要修改全局变量的任务。当一个任务获取到互斥信号量后,可以安全地修改全局变量,修改完成后释放信号量,允许其他任务进入临界区。

这里是这个文章freertos的总仓库 github https://github.com/freedom413/FreeRtosDemo.git