ARM32开发--FreeRTOS-信号量

知不足而奋进 望远山而前行

目录

目标

内容

概念

开发流程

1. 二进制信号量

案例一

案例二

2. 计数型信号量

案例一

案例二

3. 互斥信号量

案例一

4. 递归互斥信号量

比较

联想记忆


目标

  1. 理解信号量的概念
  2. 掌握信号量发流程
  3. 掌握二进制信号量
  4. 熟悉计数型信号量
  5. 掌握互斥信号量
  6. 熟悉递归互斥信号量

内容

概念

在 FreeRTOS 中,信号量(Semaphore)是一种用于实现任务之间同步和资源共享的机制。它是一种计数型的同步原语,用于控制对共享资源的访问和保护。利用信号量可以减少系统资源浪费,提高任务运行效率。

在FreeRTOS中,包含4种类型的信号量:

  1. 二进制信号量(Binary Semaphore):

二进制信号量是最基本的信号量类型。它的计数值要么为0(表示信号量已被获取),要么为1(表示信号量可用)。二进制信号量常用于实现互斥访问共享资源的场景,只允许一个任务访问资源。

在 FreeRTOS 中,你可以使用 xSemaphoreCreateBinary() 函数创建一个二进制信号量。任务可以通过 xSemaphoreTake() 函数获取信号量,通过 xSemaphoreGive() 函数释放信号量。

  1. 计数型信号量(Counting Semaphore):

计数型信号量允许指定初始计数值,并支持多个任务同时获取信号量。计数型信号量的计数值表示可用的资源数量。

在 FreeRTOS 中,你可以使用 xSemaphoreCreateCounting() 函数创建一个计数型信号量。任务可以使用 xSemaphoreTake() 函数获取信号量,使用 xSemaphoreGive() 函数释放信号量。

  1. 互斥信号量(Mutex Semaphore):

互斥信号量也用于实现资源的互斥访问,类似于二进制信号量。但与二进制信号量不同的是,互斥信号量允许同一个任务多次获取信号量,而不会导致死锁。具有优先级继承的特性。在任务持有互斥信号量时,其他任务无法获取该信号量,必须等待该任务释放信号量,优先级最高的任务会优先获取到信号量。

在 FreeRTOS 中,你可以使用 xSemaphoreCreateMutex() 函数创建一个互斥信号量。任务可以使用 xSemaphoreTake()函数获取信号量,使用 xSemaphoreGive() 函数释放信号量。

  1. 递归互斥信号量(Recursive Mutex Semaphore):

递归互斥信号量是一种特殊的信号量类型,用于解决任务在嵌套调用中对资源的重复获取。它允许同一个任务多次获取信号量而不会导致死锁。

在 FreeRTOS 中,你可以使用 xSemaphoreCreateRecursiveMutex() 函数创建一个递归互斥信号量。任务可以使用 xSemaphoreTakeRecursive() 函数多次获取信号量,并使用 xSemaphoreGiveRecursive() 函数相应地释放信号量。

开发流程

  1. 创建信号量
  2. 开启一个任务,用来等待信号量
  3. 开启一个任务,用来发送信号量

1. 二进制信号量

功能

描述

xSemaphoreCreateBinary

创建二进制信号量

xSemaphoreTake

等待信号

xSemaphoreGive

发送信号

xSemaphoreTakeFromISR

中断里等待信号

xSemaphoreGiveFromISR

中断里发送信号

二进制信号量必须现发送(Give)一次,才可以进行获取(Take)

信号量的创建

SemaphoreHandle_t xSemaphoreCreateBinary();

返回值为信号量的句柄,如果创建失败则返回NULL

等待信号操作

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime );
  1. SemaphoreHandle_t xSemaphore表示要等待的哪个信号量句柄。
  2. TickType_t xBlockTime表示要等待的时间。通常我们一直等到有信号到来,这里我们可以填写portMAX_DELAY
  3. 返回值类型为BaseType_t,表示成功或者失败,取值为pdPASSpdFAIL

发送信号操作

BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
  1. SemaphoreHandle_t xSemaphore表示要等待的哪个信号量句柄。
  2. 返回值类型为BaseType_t,表示成功或者失败,取值为pdPASSpdFAIL

案例一
  1. 开启两个任务,分别去等待信号。
  2. 开启按键扫描任务,当点击按键时,发送信号
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "Usart0.h"

TaskHandle_t            task_handler;
TaskHandle_t            task_key_handler;
TaskHandle_t            task1_handler;
TaskHandle_t            task2_handler;
SemaphoreHandle_t 			sema_handler;

void task1(void *pvParameters) {
    BaseType_t result;
    while(1) {
        result = xSemaphoreTake(sema_handler, portMAX_DELAY);
        if(result == pdTRUE) {
            printf("task1\n");
        } else {
            printf("task1 Error\n");
        }
    }
}

void task2(void *pvParameters) {
    BaseType_t result;
    while(1) {
        result = xSemaphoreTake(sema_handler, portMAX_DELAY);
        if(result == pdTRUE) {
            printf("task2\n");
        } else {
            printf("task2 Error\n");
        }
    }
}

void task_key(void *pvParameters) {
    FlagStatus pre_state = RESET;
    BaseType_t result;
    while(1) {
        FlagStatus state = gpio_input_bit_get(GPIOA, GPIO_PIN_0);
        if(SET == state && pre_state == RESET) {
            // 当前高电平, 上一次为低电平,按下
            pre_state = state;

            result = xSemaphoreGive(sema_handler);
        } else if(RESET == state && pre_state == SET) {
            // 当前高电平, 上一次为低电平,抬起
            pre_state = state;
        }
        vTaskDelay(20);
    }
}

void Usart0_recv(uint8_t *data, uint32_t len)
{
    printf("recv: %s\n", data);
}

static void GPIO_config() {
    // 时钟初始化
    rcu_periph_clock_enable(RCU_GPIOA);
    // 配置GPIO模式
    gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO_PIN_0);
}

void start_task(void *pvParameters) {
    GPIO_config();
    Usart0_init();

    taskENTER_CRITICAL();

    xTaskCreate(task_key, "task_key", 64, NULL, 2, &task_key_handler);
    xTaskCreate(task1, "task1", 64, NULL, 3, &task1_handler);
    xTaskCreate(task2, "task2", 64, NULL, 2, &task2_handler);
    vTaskDelete(task_handler);

    taskEXIT_CRITICAL();
}

int main(void)
{
    NVIC_SetPriorityGrouping(NVIC_PRIGROUP_PRE4_SUB0);
    sema_handler = xSemaphoreCreateBinary();
    xTaskCreate(start_task, "start_task", 128, NULL, 1, &task_handler);
    vTaskStartScheduler();

    while(1) {}
}

观察,两个任务是否获得信号。

改变两个任务的优先级,观察两个任务信号的获取情况。

案例二

在案例1的基础上,通过串口接收,来发送信号。

void USART0_recv(uint8_t *data, uint32_t len)
{
    printf("recv: %s\n", data);
    xSemaphoreGiveFromISR(sema_handler, NULL);
}

xSemaphoreGiveFromISR中断中发送信号

2. 计数型信号量

功能

描述

xSemaphoreCreateCounting

创建计数型信号量

uxSemaphoreGetCount

获取信号量的计数值

xSemaphoreTake

等待信号

xSemaphoreGive

发送信号

xSemaphoreTakeFromISR

中断里等待信号

xSemaphoreGiveFromISR

中断里发送信号

计数型信号量的发送和获取与二进制信号量相同

信号量的创建

SemaphoreHandle_t xSemaphoreCreateCounting(	const UBaseType_t uxMaxCount,
                                        	const UBaseType_t uxInitialCount);

参数说明:

  1. const UBaseType_t uxMaxCount最大计数值。
  2. const UBaseType_t uxInitialCount初始化当前计数值。

返回值为信号量的句柄,如果创建失败则返回NULL

等待信号操作

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime );
  1. SemaphoreHandle_t xSemaphore表示要等待的哪个信号量句柄。
  2. TickType_t xBlockTime表示要等待的时间。通常我们一直等到有信号到来,这里我们可以填写portMAX_DELAY
  3. 返回值类型为BaseType_t,表示成功或者失败,取值为pdPASSpdFAIL

发送信号操作

BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
  1. SemaphoreHandle_t xSemaphore表示要等待的哪个信号量句柄。
  2. 返回值类型为BaseType_t,表示成功或者失败,取值为pdPASSpdFAIL

案例一
  1. 开启两个任务,等待信号,接收到信号后,处理耗时操作
  2. 开启按键扫描,点击按键时发送信号
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "Usart0.h"

TaskHandle_t            task_handler;
TaskHandle_t            task_key_handler;
TaskHandle_t            task1_handler;
TaskHandle_t            task2_handler;
SemaphoreHandle_t 		sema_handler;

void task1(void *pvParameters) {
    BaseType_t result;
    while(1) {
        result = xSemaphoreTake(sema_handler, portMAX_DELAY);
        if(result == pdTRUE) {
            printf("task1 %ld\n", uxSemaphoreGetCount(sema_handler));
        } else {
            printf("task1 Error\n");
        }
        vTaskDelay(2500);
    }
}

void task2(void *pvParameters) {
    BaseType_t result;
    while(1) {
        result = xSemaphoreTake(sema_handler, portMAX_DELAY);
        if(result == pdTRUE) {
            printf("task2 %ld\n", uxSemaphoreGetCount(sema_handler));
        } else {
            printf("task2 Error\n");
        }
        vTaskDelay(2500);
    }
}

void task_key(void *pvParameters) {
    FlagStatus pre_state = RESET;
    BaseType_t result;
    while(1) {
        FlagStatus state = gpio_input_bit_get(GPIOA, GPIO_PIN_0);
        if(SET == state && pre_state == RESET) {
            // 当前高电平, 上一次为低电平,按下
            pre_state = state;

            result = xSemaphoreGive(sema_handler);
//			if(result == pdTRUE) {
//				printf("semaphore give success\n");
//			} else {
//				printf("semaphore give error\n");
//			}
        } else if(RESET == state && pre_state == SET) {
            // 当前高电平, 上一次为低电平,抬起
            pre_state = state;
        }
        vTaskDelay(20);
    }
}

void Usart0_recv(uint8_t *data, uint32_t len)
{
    printf("recv: %s\n", data);
}

static void GPIO_config() {
    // 时钟初始化
    rcu_periph_clock_enable(RCU_GPIOA);
    // 配置GPIO模式
    gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO_PIN_0);
}

void start_task(void *pvParameters) {
    GPIO_config();
    Usart0_init();
    taskENTER_CRITICAL();

    xTaskCreate(task_key, "task_key", 64, NULL, 2, &task_key_handler);
    xTaskCreate(task1, "task1", 64, NULL, 3, &task1_handler);
    xTaskCreate(task2, "task2", 64, NULL, 2, &task2_handler);
    vTaskDelete(task_handler);

    taskEXIT_CRITICAL();
}

int main(void)
{
    NVIC_SetPriorityGrouping(NVIC_PRIGROUP_PRE4_SUB0);

    sema_handler = xSemaphoreCreateCounting(100, 0);
    xTaskCreate(start_task, "start_task", 128, NULL, 1, &task_handler);
    vTaskStartScheduler();

    while(1) {}
}

观察,两个任务是否获得信号。

频繁点击按键,观察效果。

案例二

在案例1的基础上,通过串口接收,来发送信号。

void Usart0_recv(uint8_t *data, uint32_t len)
{
    printf("recv: %s\n", data);
    xSemaphoreGiveFromISR(sema_handler, NULL);
}

xSemaphoreGiveFromISR中断中发送信号

3. 互斥信号量

功能

描述

xSemaphoreCreateMutex

创建互斥信号量

xSemaphoreTake

等待信号

xSemaphoreGive

发送信号

互斥锁是包含优先级继承机制的二进制信号量。 二进制信号量 能更好实现实现同步(任务间或任务与中断之间), 而互斥锁有助于更好实现简单互斥(即相互排斥)。

用于互斥时, 互斥锁就像用于保护资源的令牌。 任务希望访问资源时,必须首先 获取 ('take') 令牌。 使用资源后,必须“返回”令牌,这样其他任务就有机会访问 相同的资源。

互斥锁使用相同的信号量访问 API 函数,因此也能指定模块时间。 阻塞时间表示如果互斥锁不是立即可用, 则在尝试“获取”互斥锁时任务应进入阻塞状态的最大“滴答”数。 然而,与二进制信号量不同 互斥锁采用优先继承。 这意味着,如果高优先级任务在尝试获取当前由较低优先级任务持有的互斥锁(令牌)时阻塞, 则持有令牌的任务的优先级会暂时提高到阻塞任务的优先级。 这一机制 旨在确保较高优先级的任务保持阻塞状态的时间尽可能短, 从而最大限度地减少已经发生的“优先级反转”现象。

优先级继承无法解决优先级反转! 只是在某些情况下将影响降至最低。 硬实时应用程序的设计应首先 确保不会发生优先级反转。

⚠️注意:

  • 需要通过宏开启此功能:#define configUSE_MUTEXES 1
  • 互斥信号量初始化后就有一个信号
  • 互斥信号量不能用于中断服务函数

信号量的创建

SemaphoreHandle_t xSemaphoreCreateMutex();

返回值为信号量的句柄。

等待信号操作

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime );
  1. SemaphoreHandle_t xSemaphore表示要等待的哪个信号量句柄。
  2. TickType_t xBlockTime表示要等待的时间。通常我们一直等到有信号到来,这里我们可以填写portMAX_DELAY
  3. 返回值类型为BaseType_t,表示成功或者失败,取值为pdPASSpdFAIL

发送信号操作

BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
  1. SemaphoreHandle_t xSemaphore表示要等待的哪个信号量句柄。
  2. 返回值类型为BaseType_t,表示成功或者失败,取值为pdPASSpdFAIL
案例一

开启两个任务,同时等待和发送信号,观察任务调用

#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "Usart0.h"

TaskHandle_t            task_handler;
TaskHandle_t            task1_handler;
TaskHandle_t            task2_handler;
SemaphoreHandle_t 		sema_handler;

void task1(void *pvParameters) {
    BaseType_t result;
    while(1) {
        result = xSemaphoreTake(sema_handler, portMAX_DELAY);
        if(result == pdTRUE) {
            printf("task1\n");
        } else {
            printf("task1 Error\n");
        }
        xSemaphoreGive(sema_handler);
        vTaskDelay(2000);
    }
}

void task2(void *pvParameters) {
    BaseType_t result;
    while(1) {
        result = xSemaphoreTake(sema_handler, portMAX_DELAY);
        if(result == pdTRUE) {
            printf("task2\n");
        } else {
            printf("task2 Error\n");
        }
        xSemaphoreGive(sema_handler);
        vTaskDelay(2000);
    }
}

void start_task(void *pvParameters) {
    Usart0_init();
    taskENTER_CRITICAL();
    
    xTaskCreate(task1, "task1", 64, NULL, 2, &task1_handler);
    xTaskCreate(task2, "task2", 64, NULL, 2, &task2_handler);
    vTaskDelete(task_handler);

    taskEXIT_CRITICAL();
}

void Usart0_recv(uint8_t *data, uint32_t len)
{
    printf("recv: %s\n", data);
}


int main(void)
{
    NVIC_SetPriorityGrouping(NVIC_PRIGROUP_PRE4_SUB0);

    sema_handler = xSemaphoreCreateMutex();
    xTaskCreate(start_task, "start_task", 128, NULL, 1, &task_handler);
    vTaskStartScheduler();

    while(1) {}
}

4. 递归互斥信号量

功能

描述

xSemaphoreCreateRecursiveMutex

创建递归互斥信号量

xSemaphoreTakeRecursive

等待信号

xSemaphoreGiveRecursive

发送信号

用户可对一把递归互斥锁重复加锁。只有用户 为每个成功的 xSemaphoreTakeRecursive() 请求调用 xSemaphoreGiveRecursive() 后,互斥锁才会重新变为可用。例如,如果一个任务成功“加锁”相同的互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁,直到任务也把这个互斥锁“解锁”5 次。

这种类型的信号量使用优先级继承机制,因此“加锁”一个信号量的任务必须在不需要此信号量时, 立即将信号量“解锁”。避免死锁

不能从中断服务程序中使用类型是互斥锁的信号量。

信号量的创建

SemaphoreHandle_t xSemaphoreCreateRecursiveMutex();

返回值为信号量的句柄。

等待信号操作

BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime );
  1. SemaphoreHandle_t xSemaphore表示要等待的哪个信号量句柄。
  2. TickType_t xBlockTime表示要等待的时间。通常我们一直等到有信号到来,这里我们可以填写portMAX_DELAY
  3. 返回值类型为BaseType_t,表示成功或者失败,取值为pdPASSpdFAIL

发送信号操作

BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xSemaphore);
  1. SemaphoreHandle_t xSemaphore表示要等待的哪个信号量句柄。
  2. 返回值类型为BaseType_t,表示成功或者失败,取值为pdPASSpdFAIL

示例代码:

#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "USART0.h"

TaskHandle_t            task_Handler;
TaskHandle_t            task1_Handler;
TaskHandle_t            task2_Handler;
SemaphoreHandle_t 		sema_handler;

void task1(void *pvParameters) {
  BaseType_t result;
  while(1) {
    printf("task1	take 0\n");
    xSemaphoreTakeRecursive(sema_handler, portMAX_DELAY);

    printf("task1	take 1\n");
    xSemaphoreTakeRecursive(sema_handler, portMAX_DELAY);

    printf("task1	give\n");
    xSemaphoreGiveRecursive(sema_handler);
		
    printf("task1	give\n");
    xSemaphoreGiveRecursive(sema_handler);
    vTaskDelay(1000);
  }
}

void task2(void *pvParameters) {
  BaseType_t result;
  while(1) {
    printf("task2	take 0\n");
    xSemaphoreTakeRecursive(sema_handler, portMAX_DELAY);

    printf("task2	take 1\n");
    xSemaphoreTakeRecursive(sema_handler, portMAX_DELAY);

    printf("task2	give\n");
    xSemaphoreGiveRecursive(sema_handler);
		
    printf("task2	give\n");
    xSemaphoreGiveRecursive(sema_handler);

    vTaskDelay(1000);
  }
}

static void GPIO_config() {
  // 时钟初始化
  rcu_periph_clock_enable(RCU_GPIOA);
  // 配置GPIO模式
  gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO_PIN_0);
}


void start_task(void *pvParameters) {
  GPIO_config();
  USART0_init();
	
  sema_handler = xSemaphoreCreateRecursiveMutex();
  if(sema_handler == NULL) {
    printf("create error\r\n");
  }
  taskENTER_CRITICAL();

  xTaskCreate(task1, "task1", 128, NULL, 3, &task1_Handler);
  xTaskCreate(task2, "task2", 128, NULL, 2, &task2_Handler);
  vTaskDelete(task_Handler);

  taskEXIT_CRITICAL();
}

void USART0_on_recv(uint8_t *data, uint32_t len)
{
  printf("recv: %s\n", data);
}

int main(void)
{
    NVIC_SetPriorityGrouping(NVIC_PRIGROUP_PRE4_SUB0);
  xTaskCreate(start_task, "start_task", 128, NULL, 1, &task_Handler);
  vTaskStartScheduler();

  while(1) {}
}

比较

四种信号量类型的常见应用场景的比较:

  1. 二进制信号量:

应用场景:二进制信号量适用于任务间的互斥、同步和事件通知。例如,当多个任务需要共享一个资源时,可以使用二进制信号量来保证同一时间只有一个任务访问该资源。

示例应用:多个任务竞争访问共享打印机资源。

  1. 计数信号量:

应用场景:计数信号量适用于任务间的资源共享和限制、任务同步和事件通知。它可以表示一组可用资源的数量,任务可以通过获取计数信号量来申请和释放这些资源。

示例应用:限制同时执行的任务数量、任务间的生产者-消费者模式。

  1. 互斥信号量:

应用场景:互斥信号量用于互斥访问共享资源的场景。它确保在任意给定时间只有一个任务可以访问共享资源,避免了数据竞争和不一致性。

示例应用:多个任务竞争访问共享数据结构、临界区保护。

  1. 递归互斥信号量:

应用场景:递归互斥信号量适用于同一任务需要多次获取互斥资源的场景。它允许同一任务在获取资源后再次获取,而不会引起死锁。

示例应用:任务递归调用、嵌套临界区保护。

需要根据具体的应用需求选择合适的信号量类型。如果需要简单的互斥访问,互斥信号量可能是最合适的选择。如果需要限制资源数量或任务同步,计数信号量可以派上用场。而对于同一任务需要多次获取资源的情况,递归互斥信号量提供了便利。

联想记忆

考虑一个共享图书馆的场景,其中图书是资源,而计数型信号量用于记录可用图书的数量。

  • 图书管理员任务(give信号): 图书管理员负责维护图书馆的图书状态,包括添加新的图书。每当图书管理员添加一本新书时,他会使用 xSemaphoreGive 函数给信号通知系统,表示有新的图书可用。
  • 读者任务(give信号和take信号): 读者既能选择借阅图书,还图书,也能选择将自己的书捐献给图书馆。每当读者完成借书时,他们会使用 xSemaphoreTake 函数通知系统,系统会更新计数器。同时,读者在归还或捐献新书后,可以使用 xSemaphoreGive 函数给信号通知系统,表示提供了新的图书。

在这个例子中,xSemaphoreGive 函数用于给信号通知系统,表示有新的图书可用。xSemaphoreTake 函数用于等待信号,表示获取图书。计数型信号量在这里仍然充当了图书计数器的角色,记录着可用图书的数量。这样,读者也能参与到提供图书的过程中。

  • 36
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛慕昭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值