信号量简介

FreeRTOS中的信号量是一种用于任务间同步和资源管理的机制,信号量可以是二进制的(只能取0或1),也可以是技术型的(可以是任意正整数),信号量的基本操作包括“获取”和“释放”

比如一个房间一次只能容纳一人,由指示灯来表示是否有人在使用,当我们想使用房间的时候,有一下的过程:

  • 判断房间是否有人使用(判断信号量是否有资源)
  • 房间空闲(信号量有空闲),那么可以直接进入房间(获取信号量成功)
  • 房间使用中(信号量没有资源),那么这个人可以选择不进入房间(获取信号量失败),也可以在门口等待(任务阻塞)

在实际代码中信号量API函数底层就是用队列实现的,不过它是特殊的队列,长度只有一个

信号量 队列

主要用于管理对共享资源的访问,确保在同一时刻只有一个任务可以访问共享资源

用于任务之间的数据通信,通过在任务之间传递信息,实现信息传递和同步
可以是二进制信号量或及计数信号量 存储和传递消息的数据通信,任务可以发送消息到队列,也可以从队列接收消息
适用于对资源互斥访问,控制任务的执行顺序,或者限制同时访问某一资源的任务数量 适用于在任务之间传递数据,实现解耦和通信

就比如说有task1和task2都要对一个变量a进行++操作,因为实际上++操作是先++然后再将++后的值赋值给a,如果在++后任务切换,还没有进行赋值,切换到task2,然后task2对a++后a的值为1,然后切换回task1,task1就执行剩余的操作,将一开始a++之后的值赋值给a,所以a还是等于1,但是用户是希望a++两次得到2的但是这里就出现了问题,但是如果使用信号量的话,就比如task1++后获取信号量,还没有赋值就切换为task2了,但是此时信号量还没有释放,然后task2无法获取信号量就进入阻塞切换回task1进行完为赋值操作后再切换回task2,这样就实现了a的两次++

二值信号量

        二值信号量简介

二值信号量(Binary Semaphore)是一种特殊类型的信号量,它只有0和1两种值,这种信号量主要用于实现对资源共享的互斥访问或者任务之间的同步

信号量 API 函数允许指定阻塞时间, 阻塞时间表示当一个任务试图“获取”信号量时, 如果信号不是立即可用,那么该任务进入阻塞状态的最大 “tick” 数, 如果多个任务在同一个信号量上阻塞,那么具有最高优先级的任务将在下次信号量可用时最先解除阻塞,如果是相同优先级的任务等待同一个信号量,就看这两个任务在任务列表的先后顺序

        二值信号量的特点

  • 两个状态:二值信号量只能处于两个状态之一,通常用0和1表示,当信号量的值为0时,表示资源不可用,当值为1时,表示资源可用,不会记录多个事件,只能表示事件是否发生过一次
  • 互斥访问:常用于控制对共享资源的互斥访问,确保在同一时刻只有一个任务可以访问共享资源,任务在访问资源之前会尝试获取信号量,成功则继续执行,失败则等待
  • 任务同步:也可以用于任务之间的同步,例如一个任务等待另一个任务完成某个操作 

        二值信号量函数API

函数 描述

xSemaphoreCreateBinary()

使用动态方式创建二值信号量(初始0状态)

vSemaphoreCreateBinary()

也是动态创建,但是创建完会主动释放一次信号量(初始1状态)

xSemaphoreCreateBinaryStatic()

使用静态方式创建二值信号量

xSemaphoreGive()

释放信号量

xSemaphoreGiveFromISR()

在中断中释放信号量

xSemaphoreTake()

获取信号量

xSemaphoreTakeFromISR()

在中断中获取信号量

        二值信号量演示代码

本质上就是相当于在切换任务时,如果在上一个任务释放了信号量要完成什么操作,没有释放信号量就要完成另外什么操作,将上个任务的操作于切换后的任务的操作进行了绑定,能够实现任务之间的同步,也可以实现对共享资源的互斥访问

#include "FreeRTOS_demo.h"
#include "FreeRTOS.h"//这两个头文件是必须要引入的
#include "task.h"
#include "LED.h"
#include "Key.h"
#include "stdio.h"
#include "semphr.h"
//启动任务的配置
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void * pvParameters);//启动任务

//任务1的配置
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void * pvParameters);//启动任务

//任务2的配置
#define TASK2_STACK 128
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void * pvParameters);//启动任务



QueueHandle_t sem_hangdle;
void freertos_start(void)//启动FreeRTOS
{
    /*创建一个二值信号量*/
    //xSemaphoreCreateBinary();而这个函数只是单纯的创建,下面那个函数创建完后会先释放一次
    vSemaphoreCreateBinary(sem_hangdle);//传入一个队列句柄
    //1.创建一个启动任务
    xTaskCreate((TaskFunction_t) start_task,                //任务函数地址,前面是类型强转
                (char*) "start_task",                       //取任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK,  //任务栈大小,最小是128字节
                (void *) NULL,                              //不需要形参就给一个NULL
                (UBaseType_t) START_TASK_PRIORITY,          //任务优先级,这里的优先级默认为5,有5个优先级
                (TaskHandle_t *) &start_task_handle);        //任务句柄
                //任务的相关信息会保存到一个tcb地址,然后这个地址会赋值给这个任务句柄,所以这只用创建一个任务句柄即可
    //2.启动调度器
    vTaskStartScheduler();//这是必须要启动的,会自动创建空闲函数
}
void start_task(void * pvParameters)//启动任务,用来创建其他task
{
    //进入临界区,临界区里面的代码不会被打断
    taskENTER_CRITICAL();
    
    //创建3个任务
    xTaskCreate((TaskFunction_t) task1,                
                (char*) "task1",                       
                (configSTACK_DEPTH_TYPE) TASK1_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK1_PRIORITY,          
                (TaskHandle_t *) &task1_handle);
    xTaskCreate((TaskFunction_t) task2,                
                (char*) "task2",                       
                (configSTACK_DEPTH_TYPE) TASK2_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK2_PRIORITY,          
                (TaskHandle_t *) &task2_handle);
    
    //退出临界区
    taskEXIT_CRITICAL();
    //启动任务只需要执行一次,用完就删除自己
    vTaskDelete(NULL);
}

void task1(void * pvParameters)//任务1:用于按键扫描,当检测到按键KEY1被按下时,释放二值信号量
{
   uint8_t key = 0;
   BaseType_t res = 0;
   while(1)
   {
        key = Key_Detect();
        if(key = KEY1_PRESS)
        {
            /*释放二值信号量*/
            res = xSemaphoreGive(sem_hangdle);//也是传入信号量的句柄
            if(res == pdPASS)
            {
                printf("释放成功");
            }
            else
            {
                printf("释放失败");
            }
        }
        vTaskDelay(500);//这里不能用HAL库的延时函数要用FreeRTOS库中的延时函数这样延时的时候才会让出CPU资源
   }
}

void task2(void * pvParameters)//任务2:获取二值信号量,当成功获取后打印提示信息
{

   while(1)
   {
        BaseType_t res = 0;
        res = xSemaphoreTake(sem_hangdle,portMAX_DELAY);//队列句柄,阻塞时间
        if(res == pdPASS)
        {
            printf("获取成功");
        }
        else 
        {
            printf("获取失败");
        }

    }
}

计数型信号量

        计数型信号量简介

计数型信号量不同于二值信号量的只有0和1,本质上是一个带计数功能的事件记录器,值可以在0到某个最大值之间变化,每“释放”一次计数值加 1,每“获取”一次计数值减 1,如果计数值已经是0,再去获取就会阻塞或失败,如果已经达到最大值,再释放通常就会失败,二值信号量是表示"有没有",计数型信号量是表示"有多少个",功能更加强大,可以保存多个资源数量,或者累计多个事件

        计数型信号量的应用

计数信号量通常用于两种情况:

  • 事件计数:在此使用方案中,每次事件发生时,事件处理程序将“给出”一个信号量(信号量计数值递增) ,并且 处理程序任务每次处理事件(信号量计数值递减)时“获取”一个信号量,因此,计数值是已发生的事件数与已处理的事件数之间的差值,在这种情况下, 创建信号量时计数值可以为零
  • 资源管理:在此使用情景中,计数值表示可用资源的数量,要获得对资源的控制权,任务必须首先获取 一个信号量——同时递减信号量计数值,当计数值达到零时,表示没有空闲资源可用,当任务使用完资源时, “返还”一个信号量——同时递增信号量计数值,在这种情况下, 创建信号量时计数值可以等于最大计数值

        计数性信号量API

函数 描述

xSemaphoreCreateCounting()

使用动态方法创建计数型信号量

xSemaphoreCreateCountingStatic()

使用静态方法创建计数型信号量

uxSemaphoreGetCount()

获取信号量的计数值

        计数型信号量演示代码

要先在配置文件卡计数型信号量的宏

#define configUSE_COUNTING_SEMAPHORES 1
#include "FreeRTOS_demo.h"
#include "FreeRTOS.h"//这两个头文件是必须要引入的
#include "task.h"
#include "LED.h"
#include "Key.h"
#include "stdio.h"
#include "semphr.h"
//启动任务的配置
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void * pvParameters);//启动任务

//任务1的配置
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void * pvParameters);//启动任务

//任务2的配置
#define TASK2_STACK 128
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void * pvParameters);//启动任务



QueueHandle_t sem_count_hangdle;
void freertos_start(void)//启动FreeRTOS
{
    UBaseType_t count = 0;    
    /*创建一个计数型信号量*/
    sem_count_hangdle = xSemaphoreCreateCounting(100,0);//最大计数值,初始计数值,返回一个队列句柄
    if(sem_count_hangdle == NULL)
    {
        printf("创建信号量失败\n");
    }
    else//创建成功就获取一次计数值并打印出来
    {
        count = uxSemaphoreGetCount(sem_count_hangdle);
        printf("计数值为:%d\n",count);
    }
    //1.创建一个启动任务
    xTaskCreate((TaskFunction_t) start_task,                //任务函数地址,前面是类型强转
                (char*) "start_task",                       //取任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK,  //任务栈大小,最小是128字节
                (void *) NULL,                              //不需要形参就给一个NULL
                (UBaseType_t) START_TASK_PRIORITY,          //任务优先级,这里的优先级默认为5,有5个优先级
                (TaskHandle_t *) &start_task_handle);        //任务句柄
                //任务的相关信息会保存到一个tcb地址,然后这个地址会赋值给这个任务句柄,所以这只用创建一个任务句柄即可
    //2.启动调度器
    vTaskStartScheduler();//这是必须要启动的,会自动创建空闲函数
}
void start_task(void * pvParameters)//启动任务,用来创建其他task
{
    //进入临界区,临界区里面的代码不会被打断
    taskENTER_CRITICAL();
    
    //创建3个任务
    xTaskCreate((TaskFunction_t) task1,                
                (char*) "task1",                       
                (configSTACK_DEPTH_TYPE) TASK1_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK1_PRIORITY,          
                (TaskHandle_t *) &task1_handle);
    xTaskCreate((TaskFunction_t) task2,                
                (char*) "task2",                       
                (configSTACK_DEPTH_TYPE) TASK2_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK2_PRIORITY,          
                (TaskHandle_t *) &task2_handle);
    //退出临界区
    taskEXIT_CRITICAL();
    //启动任务只需要执行一次,用完就删除自己
    vTaskDelete(NULL); 
}

void task1(void * pvParameters)//任务1:用于按键扫描,当检测到按键KEY1被按下时,释放计数型信号量
{
   uint8_t key = 0;
   BaseType_t res = 0;
   while(1)
   {
        key = Key_Detect();
        if(key = KEY1_PRESS)
        {
            /*释放二值信号量*/
            res = xSemaphoreGive(sem_count_hangdle);//也是传入信号量的句柄
            if(res == pdPASS)
            {
                printf("释放成功");
            }
            else
            {
                printf("释放失败");
            }
        }
        vTaskDelay(500);//这里不能用HAL库的延时函数要用FreeRTOS库中的延时函数这样延时的时候才会让出CPU资源
   }
}
void task2(void * pvParameters)//任务2:每过一秒获取一次计数型信号量,当成功获取后打印信号量计数值
{

   while(1)
   {
        BaseType_t res = 0;
        UBaseType_t current_count = 0; 
        res = xSemaphoreTake(sem_count_hangdle,portMAX_DELAY);//队列句柄,阻塞时间
        if(res == pdPASS)
        {
            printf("获取成功");
        }
        else 
        {
            printf("获取失败");
        }
        current_count = uxSemaphoreGetCount(sem_count_hangdle);
        printf("计数值为:%d\n",current_count);
        vTaskDelay(1000);
    }
}

优先级翻转

        什么是优先级翻转

优先级翻转是一个在实时系统中可能出现的问题,特别是在多任务环境中,该问题指的是一个较低优先级的任务阻塞了一个较高优先级任务的执行,从而导致高优先级任务无法及时完成

就是一开始是最低优先级的任务B在执行然后任务B在执行任务的时候获取了信号量,然后还没有释放信号量就被任务A抢占了,但是任务A也需要获取信号量,但是由于任务B还没有释放信号量所以任务A获取不了进入阻塞状态,所以又回到任务B,在任务B还没有释放信号量的时候又被中等优先级的任务打断了,然后等任务C执行完后又到任务B将信号量释放任务A才能执行,所以本来高优先级的任务A是想要有更多的执行时间,但是由于这个信号量的问题,导致任务A进入了很久的阻塞态才能执行

优先级翻转会破坏系统的实时性,导致高优先级的任务不能及时响应,高优先级的任务会被更低优先级的任务拖住,中等优先级的任务频繁出现就会导致低优先级的任务更难完成资源释放,高优先级任务还有可能长时间无法获取资源

避免方法:

  • 使用互斥量而不是普通的二值信号量
  • 避免低优先级任务持有资源时间太久,缩短临界区的长度,拿到资源后值做必要操作,使用完后立即释放
  • 合理设计任务的优先级,真实实时性强的任务设置为高优先级

        互斥信号量

  • 互斥信号量是包含优先级继承机制的二进制信号量二进制信号量能更好实现实现同步(任务间或任务与中断之间), 而互斥信号量有助于更好实现简单互斥(即相互排斥)
  • 优先级继承是一种解决实时系统中任务调度引起的优先级翻转问题的机制,在具体的任务调度中,当一个高优先级任务等待一个低优先级任务所持有的资源时,系统会提升低优先级任务的优先级,以避免高优先级任务长时间等待的情况
  • 优先级继承无法完全解决优先级翻转,只是在某些情况下将影响降至最低

注意不能在中断中使用互斥信号量,原因如下:

  • 互斥信号量使用的优先级继承机制要求从任务中(而不是从中断中)获取和释放互斥信号量
  • 中断无法保持阻塞来等待一个被互斥信号量保护的资源
函数 描述

xSemaphoreCreateMutex()

使用动态方法创建互斥信号量(会主动释放一次)

xSemaphoreCreateMutexStatic()

使用静态方法创建互斥信号量

使用并创建互斥信号量的时候需要在配置文件开启对应的宏

#define configUSE_MUTEXES 1

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐