知不足而奋进 望远山而前行
目录
目标
- 理解信号量的概念
- 掌握信号量发流程
- 掌握二进制信号量
- 熟悉计数型信号量
- 掌握互斥信号量
- 熟悉递归互斥信号量
内容
概念
在 FreeRTOS 中,信号量(Semaphore)是一种用于实现任务之间同步和资源共享的机制。它是一种计数型的同步原语,用于控制对共享资源的访问和保护。利用信号量可以减少系统资源浪费,提高任务运行效率。
在FreeRTOS中,包含4种类型的信号量:
- 二进制信号量(Binary Semaphore):
二进制信号量是最基本的信号量类型。它的计数值要么为0(表示信号量已被获取),要么为1(表示信号量可用)。二进制信号量常用于实现互斥访问共享资源的场景,只允许一个任务访问资源。
在 FreeRTOS 中,你可以使用 xSemaphoreCreateBinary()
函数创建一个二进制信号量。任务可以通过 xSemaphoreTake()
函数获取信号量,通过 xSemaphoreGive()
函数释放信号量。
- 计数型信号量(Counting Semaphore):
计数型信号量允许指定初始计数值,并支持多个任务同时获取信号量。计数型信号量的计数值表示可用的资源数量。
在 FreeRTOS 中,你可以使用 xSemaphoreCreateCounting()
函数创建一个计数型信号量。任务可以使用 xSemaphoreTake()
函数获取信号量,使用 xSemaphoreGive()
函数释放信号量。
- 互斥信号量(Mutex Semaphore):
互斥信号量也用于实现资源的互斥访问,类似于二进制信号量。但与二进制信号量不同的是,互斥信号量允许同一个任务多次获取信号量,而不会导致死锁。具有优先级继承的特性。在任务持有互斥信号量时,其他任务无法获取该信号量,必须等待该任务释放信号量,优先级最高的任务会优先获取到信号量。
在 FreeRTOS 中,你可以使用 xSemaphoreCreateMutex()
函数创建一个互斥信号量。任务可以使用 xSemaphoreTake()
函数获取信号量,使用 xSemaphoreGive()
函数释放信号量。
- 递归互斥信号量(Recursive Mutex Semaphore):
递归互斥信号量是一种特殊的信号量类型,用于解决任务在嵌套调用中对资源的重复获取。它允许同一个任务多次获取信号量而不会导致死锁。
在 FreeRTOS 中,你可以使用 xSemaphoreCreateRecursiveMutex()
函数创建一个递归互斥信号量。任务可以使用 xSemaphoreTakeRecursive()
函数多次获取信号量,并使用 xSemaphoreGiveRecursive()
函数相应地释放信号量。
开发流程
- 创建信号量
- 开启一个任务,用来等待信号量
- 开启一个任务,用来发送信号量
1. 二进制信号量
功能 | 描述 |
xSemaphoreCreateBinary | 创建二进制信号量 |
xSemaphoreTake | 等待信号 |
xSemaphoreGive | 发送信号 |
xSemaphoreTakeFromISR | 中断里等待信号 |
xSemaphoreGiveFromISR | 中断里发送信号 |
二进制信号量必须现发送(Give)一次,才可以进行获取(Take)
信号量的创建
SemaphoreHandle_t xSemaphoreCreateBinary();
返回值为信号量的句柄,如果创建失败则返回NULL
。
等待信号操作
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime );
SemaphoreHandle_t xSemaphore
表示要等待的哪个信号量句柄。TickType_t xBlockTime
表示要等待的时间。通常我们一直等到有信号到来,这里我们可以填写portMAX_DELAY
- 返回值类型为
BaseType_t
,表示成功或者失败,取值为pdPASS
和pdFAIL
发送信号操作
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
SemaphoreHandle_t xSemaphore
表示要等待的哪个信号量句柄。- 返回值类型为
BaseType_t
,表示成功或者失败,取值为pdPASS
和pdFAIL
案例一
- 开启两个任务,分别去等待信号。
- 开启按键扫描任务,当点击按键时,发送信号
#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);
参数说明:
const UBaseType_t uxMaxCount
最大计数值。const UBaseType_t uxInitialCount
初始化当前计数值。
返回值为信号量的句柄,如果创建失败则返回NULL
。
等待信号操作
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xBlockTime );
SemaphoreHandle_t xSemaphore
表示要等待的哪个信号量句柄。TickType_t xBlockTime
表示要等待的时间。通常我们一直等到有信号到来,这里我们可以填写portMAX_DELAY
- 返回值类型为
BaseType_t
,表示成功或者失败,取值为pdPASS
和pdFAIL
发送信号操作
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
SemaphoreHandle_t xSemaphore
表示要等待的哪个信号量句柄。- 返回值类型为
BaseType_t
,表示成功或者失败,取值为pdPASS
和pdFAIL
案例一
- 开启两个任务,等待信号,接收到信号后,处理耗时操作
- 开启按键扫描,点击按键时发送信号
#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 );
SemaphoreHandle_t xSemaphore
表示要等待的哪个信号量句柄。TickType_t xBlockTime
表示要等待的时间。通常我们一直等到有信号到来,这里我们可以填写portMAX_DELAY
- 返回值类型为
BaseType_t
,表示成功或者失败,取值为pdPASS
和pdFAIL
发送信号操作
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
SemaphoreHandle_t xSemaphore
表示要等待的哪个信号量句柄。- 返回值类型为
BaseType_t
,表示成功或者失败,取值为pdPASS
和pdFAIL
案例一
开启两个任务,同时等待和发送信号,观察任务调用
#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 );
SemaphoreHandle_t xSemaphore
表示要等待的哪个信号量句柄。TickType_t xBlockTime
表示要等待的时间。通常我们一直等到有信号到来,这里我们可以填写portMAX_DELAY
- 返回值类型为
BaseType_t
,表示成功或者失败,取值为pdPASS
和pdFAIL
发送信号操作
BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xSemaphore);
SemaphoreHandle_t xSemaphore
表示要等待的哪个信号量句柄。- 返回值类型为
BaseType_t
,表示成功或者失败,取值为pdPASS
和pdFAIL
示例代码:
#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) {}
}
比较
四种信号量类型的常见应用场景的比较:
- 二进制信号量:
应用场景:二进制信号量适用于任务间的互斥、同步和事件通知。例如,当多个任务需要共享一个资源时,可以使用二进制信号量来保证同一时间只有一个任务访问该资源。
示例应用:多个任务竞争访问共享打印机资源。
- 计数信号量:
应用场景:计数信号量适用于任务间的资源共享和限制、任务同步和事件通知。它可以表示一组可用资源的数量,任务可以通过获取计数信号量来申请和释放这些资源。
示例应用:限制同时执行的任务数量、任务间的生产者-消费者模式。
- 互斥信号量:
应用场景:互斥信号量用于互斥访问共享资源的场景。它确保在任意给定时间只有一个任务可以访问共享资源,避免了数据竞争和不一致性。
示例应用:多个任务竞争访问共享数据结构、临界区保护。
- 递归互斥信号量:
应用场景:递归互斥信号量适用于同一任务需要多次获取互斥资源的场景。它允许同一任务在获取资源后再次获取,而不会引起死锁。
示例应用:任务递归调用、嵌套临界区保护。
需要根据具体的应用需求选择合适的信号量类型。如果需要简单的互斥访问,互斥信号量可能是最合适的选择。如果需要限制资源数量或任务同步,计数信号量可以派上用场。而对于同一任务需要多次获取资源的情况,递归互斥信号量提供了便利。
联想记忆
考虑一个共享图书馆的场景,其中图书是资源,而计数型信号量用于记录可用图书的数量。
- 图书管理员任务(give信号): 图书管理员负责维护图书馆的图书状态,包括添加新的图书。每当图书管理员添加一本新书时,他会使用 xSemaphoreGive 函数给信号通知系统,表示有新的图书可用。
- 读者任务(give信号和take信号): 读者既能选择借阅图书,还图书,也能选择将自己的书捐献给图书馆。每当读者完成借书时,他们会使用 xSemaphoreTake 函数通知系统,系统会更新计数器。同时,读者在归还或捐献新书后,可以使用 xSemaphoreGive 函数给信号通知系统,表示提供了新的图书。
在这个例子中,xSemaphoreGive 函数用于给信号通知系统,表示有新的图书可用。xSemaphoreTake 函数用于等待信号,表示获取图书。计数型信号量在这里仍然充当了图书计数器的角色,记录着可用图书的数量。这样,读者也能参与到提供图书的过程中。