信号量

FreeRTOS 中的信号量是一种重要的同步机制,用于实现任务之间或任务与中断之间的通信资源共享管理。信号量最初是为解决并发编程中互斥访问共享资源的问题而设计的,但在 FreeRTOS 中还可以用于任务间的同步。

FreeRTOS 提供了三种主要类型的信号量:

  1. 二值信号量(Binary Semaphore)
    • 只有两个状态:空(0) 和 满(1)。
    • 常用于任务间同步中断与任务的同步
    • 在创建时,二值信号量可以通过 xSemaphoreCreateBinary() 或 xSemaphoreCreateBinaryStatic() 创建。
  2. 计数信号量(Counting Semaphore)
    • 允许多个任务或中断访问资源,类似于一个计数器。
    • 计数信号量的计数值可以大于 1,表示同一时间允许多个任务获取信号量。
    • 常用于管理多个等价资源实现事件计数
    • 在创建时,可以通过 xSemaphoreCreateCounting() 或 xSemaphoreCreateCountingStatic() 创建。
  3. 互斥量(Mutex)
    • 专门用于保护共享资源,实现任务间的互斥访问。
    • 互斥量具有优先级继承机制,用于防止优先级反转问题。
    • 互斥量始终处于二值信号量的状态,但为互斥访问场景进行了特殊优化。
    • 在创建时,可以通过 xSemaphoreCreateMutex() 或 xSemaphoreCreateMutexStatic() 创建。

提示:在许多使用场景中,使用直达任务通知要比使用信号量的速度更快,内存效率更高。

二进制信号量

二进制信号量用于互斥和同步目的。

二进制信号量和互斥锁极为相似,但存在一些细微差别:互斥锁包括优先继承机制, 而二进制信号量则不然。因此,二进制信号量是实现同步的更好选择(任务之间或任务与中断之间), 而互斥锁是实现简单互斥的更好选择

信号量相关函数允许指定阻塞时间。阻塞时间表示在尝试“获取”信号量时, 如果信号量不是立即可用, 任务应进入阻塞状态的最大“滴答”数。如果多个任务在同一信号量上阻塞, 则具有最高优先级的任务将成为下次信号量可用时解除阻塞的任务。

可将二进制信号量视为仅能容纳一个项目的队列。因此,队列只能为空或满 (因此称为二进制)。

使用信号量同步任务与中断。中断仅“提供” 信号量,而任务仅“获取”信号量。

freertosbinSemaphore

xSemaphoreGiveFromISR 用于释放信号量

xSemaphoreTake 用于获取信号量的

信号量的“给出”和“获取”机制

  • 二进制信号量的计数值最多只能为 1(表示信号量可用)。
  • 当一个任务成功调用 xSemaphoreTake 时,信号量的计数值立即变为 0,表示信号量被“占用”。
  • 在信号量变为 0 后,其他试图获取信号量的任务都会进入阻塞状态,直到信号量被重新“给出”并变为 1。

API

xSemaphoreCreateBinary

创建一个二进制信号量,并返回一个可以引用该信号量的句柄。

每个二进制信号量需要少量 RAM,用于保存信号量状态。如果 使用 xSemaphoreCreateBinary() 创建了二进制信号量,那么所需的 RAM 将自动从 FreeRTOS 堆中分配。如果二进制信号量 是使用 xSemaphoreCreateBinaryStatic() 创建的,则 RAM 由应用程序写入器提供,这需要一个附加参数,但允许 RAM 在编译时进行静态分配 。

1
SemaphoreHandle_t xSemaphoreCreateBinary( void );

信号量是在“空”状态下创建的,这意味着必须先用 xSemaphoreGive() API 函数给出信号量, 然后才能使用 xSemaphoreTake() 函数来获取(获得)该信号量。

二值信号量并不需要在得到后立即释放, 因此,任务同步可以通过一个任务/中断持续释放信号量 而另外一个持续获得信号量来实现。

与互斥锁不同, 二进制信号量可用于中断服务程序。

返回值:

  • NULL

    因为可用 FreeRTOS 堆不足,所以无法创建信号量。

  • 其他任何值

    信号量已成功创建。返回值是一个句柄, 通过该句柄可以引用信号量。

示例

1
2
3
4
5
xSemaphore = xSemaphoreCreateBinary();

if( xSemaphore == NULL ){
} else {
}

xSemaphoreTake

用于获取信号量的。

不得从 ISR 调用此宏。如果需要,xQueueReceiveFromISR() 可用来从中断中获取一个信号量, 尽管这不是正常操作。信号量使用队列作为 其底层机制,因此函数在某种程度上可互操作。

1
xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);

参数:

  • xSemaphore

    正在获取的信号量的句柄——在创建信号量时获得。

  • xTicksToWait

    等待信号量变为可用的时间(以滴答为单位)。

    如果 INCLUDE_vTaskSuspend 设置为 1,则将阻塞时间指定为 portMAX_DELAY 会导致任务无限期地阻塞(没有超时限制)。

返回:

  • 如果获得信号量,则返回 pdTRUE
  • 如果 xTicksToWait 过期,信号量不可用,则返回 pdFALSE

示例

1
2
3
4
5
if( xSemaphoreTake(xSemaphore, ( TickType_t )10) == pdTRUE ) {
// 访问共享资源
// ...
} else {
}

xSemaphoreGive

用于释放信号量的。释放前信号量必须已经创建

参数:

  • xSemaphore

    要释放的信号量的句柄。这是创建信号量时返回的句柄。 |

返回:

  • 如果信号量被释放,则返回 pdTRUE
  • 如果发生错误,则返回 pdFALSE。信号量是使用队列实现的。发布消息时,如果队列上没有空间, 那么可能会发生错误,这表明最初未能正确获取信号量。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
if( xSemaphoreGive( xSemaphore ) != pdTRUE ) {
// 释放失败的处理
}

if( xSemaphoreTake( xSemaphore, ( TickType_t ) 0 ) )
{
// 获取信号量后就可以访问共享资源
// ...
// 操作玩抽释放信号量
if( xSemaphoreGive( xSemaphore ) != pdTRUE ) {
// 释放失败的处理
}
}

xSemaphoreTakeFromISR

可从 ISR 调用的 xSemaphoreTake() 版本。与 xSemaphoreTake() 不同,xSemaphoreTakeFromISR() 不允许指定阻塞时间。

1
xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken)

参数:

  • xSemaphore

    信号量被“获取”。信号量由 SemaphoreHandle_t 类型的变量引用, 必须在使用之前显式创建。

  • pxHigherPriorityTaskWoken

    信号量由于其类型可能阻塞一个或多个任务,等待给出信号量。调用 xSemaphoreTakeFromISR() 会尝试获取信号量,如果成功获取信号量,则会唤醒一个正在等待该信号量的任务,使其离开阻塞状态。

    如果调用函数导致任务离开阻塞状态, 且未阻塞任务的优先级等于或高于当前正在执行的任务(被中断的任务),那么 API 函数将从内部把 *pxHigherPriorityTaskWoken 设置为 pdTRUE。

    如果 xSemaphoreTakeFromISR() 将 pxHigherPriorityTaskWoken 设置为 pdTRUE,则应在退出中断之前执行上下文切换。这将确保中断直接返回到最高优先级的就绪状态任务(返回优先级最高的任务,而不是被中断的任务) 。该机制与 xQueueReceiveFromISR() 函数中使用的机制相同, pxHigherPriorityTaskWoken 是一个可选参数,可设置为 NULL。

返回:

如果成功获取信号量,则返回 pdTRUE。如果因为信号量不可用而未成功获取信号量, 则返回 pdFALSE。

xSemaphoreGiveFromISR

用于释放信号量的。释放前信号量必须已经 通过调用 xSemaphoreCreateBinary() 或 xSemaphoreCreateCounting() 创建。

互斥锁型信号量(那些调用 xSemaphoreCreateMutex() 创建的信号量) 不得与此宏一起使用。

1
xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken)

参数:

  • xSemaphore

    要释放的信号量的句柄。这是创建信号量时返回的句柄。

  • pxHigherPriorityTaskWoken

    如果给出信号量会导致任务解除阻塞,并且解除阻塞的任务的优先级高于当前正在运行的任务, 则 xSemaphoreGiveFromISR() 会将 *pxHigherPriorityTaskWoken 设置为 pdTRUE。

    如果 xSemaphoreGiveFromISR() 将此值设置为 pdTRUE,则应在退出中断之前请求上下文切换。

    从 FreeRTOS V7.3.0 开始,pxHigherPriorityTaskWoken 为可选参数, 可设置为 NULL。

返回:

如果成功给出信号量,则返回 pdTRUE,否则 errQUEUE_FULL。

实现

开启两个任务,分别去等待信号。

开启按键扫描任务,当点击按键时,发送信号

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#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 的队列那样, 计数信号量也可以被认为是长度大于 1 的队列。同样,信号量的使用者对存储在队列中的数据并不感兴趣, 他们只关心队列是否为空。

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

  1. 盘点事件。

    在此使用场景中,事件处理程序将在每次事件发生时“提供”信号量(递增信号量计数值), 而处理程序任务将在每次处理事件时“获取”信号量 (递减信号量计数值)。因此,计数值是已发生的事件数与已处理的事件数之间的差值。在这种情况下, 创建信号量时希望计数值为零。

  2. 资源管理。

    在此使用情景中,计数值表示可用资源的数量。为了获得对资源的控制,任务必须首先获得信号量——递减信号量计数值。当计数值达到零时, 表示没有空闲资源可用。当任务结束使用资源时, 它会“返还”一个信号量——同时递增信号量计数值。在这种情况下, 创建信号量时希望计数值等于最大计数值。

计数信号量的“给出”和“获取”机制

给出机制:

“给出”信号量表示资源或事件的增加,可以理解为将信号量的计数值递增。

  • 当调用 xSemaphoreGive() 或 xSemaphoreGiveFromISR() 时:

    如果当前信号量的计数值小于最大允许值,则计数值增加 1。

    如果计数值已达到最大值(通常在创建信号量时定义),进一步的“给出”操作不会改变计数值,函数返回 pdFAIL

    如果有任务正在等待此信号量且任务的优先级足够高,则调度器会将该任务从阻塞状态移到就绪状态。

获取机制:

“获取”信号量表示使用资源或响应事件,可以理解为将信号量的计数值递减。

  • 当调用 xSemaphoreTake() 或 xSemaphoreTakeFromISR() 时:

    如果当前信号量的计数值大于零,则计数值减少 1,函数返回 pdPASS。

    如果计数值等于零:

    • 如果指定了阻塞时间(xTicksToWait),任务进入阻塞状态,等待计数值变为大于零。
    • 如果阻塞时间为 0,函数立即返回 pdFAIL,表示获取失败。

API

xSemaphoreCreateCounting

创建一个计数信号量, 并返回一个可以引用该新建信号量的句柄。

configSUPPORT_DYNAMIC_ALLOCATION 必须在 FreeRTOSConfig.h 中设置为 1,或处于未定义状态(在这种情况下,默认为 1),该 RTOS API 函数 才可用。

每个计数信号量需要少量 RAM ,用于保存信号量的状态。如果使用 xSemaphoreCreateCounting() 创建计数信号量则所需的 RAM 将从 FreeRTOS 堆自动分配。 如果使用 xSemaphoreCreateCountingStatic() 创建计数信号量, 则 RAM会由应用程序编写器提供,这需要其他的 但允许在编译时静态分配 RAM 。

1
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount);

参数:

  • uxMaxCount

    可以达到的最大计数值。当信号量达到此值时,它不能再被“给定”。

  • uxInitialCount

    创建信号量时分配给信号量的计数值。

返回:

  • 如果已成功创建信号量,则将返回该信号量的句柄 。
  • 如果因为保留信号量所需的 RAM 无法分配而无法创建信号量, 则会返回 NULL。

示例

1
2
3
4
5
xSemaphore = xSemaphoreCreateCounting( 10, 0 );

if( xSemaphore != NULL ) {

}

uxSemaphoreGetCount

获取信号量的计数值。

1
UBaseType_t uxSemaphoreGetCount( SemaphoreHandle_t xSemaphore );

参数:

  • xSemaphore

    正在查询的信号量的句柄。

返回:

如果信号量是计数信号量,则返回信号量的当前计数值 。如果信号量是二进制信号量, 则当信号量可用时,返回 1,当信号量不可用时, 返回 0。

xSemaphoreTake

同上

xSemaphoreGive

同上

xSemaphoreTakeFromISR

同上

xSemaphoreGiveFromISR

同上

实现

  1. 开启两个任务,等待信号,接收到信号后,处理耗时操作
  2. 开启按键扫描,点击按键时发送信号
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#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) {}
}

互斥锁

互斥锁是包含优先级继承机制的二进制信号量。鉴于二进制信号量是实现同步(任务之间或任务与中断之间) 的更好方式,因此互斥锁更适合实现简单的 相互排斥(即互斥)。

用于互斥时,互斥锁就像用于保护资源的令牌。当一个 任务希望访问资源时,必须首先获得(“获取”)该令牌。使用完资源后, 任务必须“返还”令牌,以便其他任务有机会访问 相同的资源。

优先级继承机制

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

优先级继承无法解决优先级反转!只是在某些情况下将影响降至最低。应用程序的设计应从一开始就避免发生优先级反转。

不能从中断中使用互斥锁

  • 互斥锁使用的优先级继承机制要求从任务中(而不是从中断中)拿走和放入互斥锁。
  • 中断无法保持阻塞来等待一个被互斥锁保护的资源变为可用。

mutexes

互斥锁的“给出”和“获取”机制

任务请求使用共享资源时,需要通过“获取”互斥锁来确保独占访问权。

  • 调用 xSemaphoreTake() 或 xSemaphoreTakeRecursive() 时:

    锁可用:如果互斥锁当前未被其他任务占用,则获取成功,并将锁的“拥有者”设置为当前任务,函数返回 pdPASS。

    锁不可用:如果互斥锁已被其他任务占用,则任务进入阻塞状态,直到:

    • 锁被释放。
    • 超过了指定的阻塞时间(xTicksToWait)。

    递归支持(Recursive Mutex):如果当前任务已获取了互斥锁,递归获取是允许的,计数会递增(只适用于递归互斥锁)。

当任务完成资源的使用后,必须通过“给出”互斥锁将其释放,允许其他任务继续使用。

  • 调用 xSemaphoreGive()xSemaphoreGiveRecursive() 时:

    当前任务为锁的拥有者:互斥锁被释放,允许其他等待的任务获取锁。

    当前任务不是锁的拥有者:释放操作无效(仅在调试模式下可能触发断言错误)。

    递归支持(Recursive Mutex):如果任务递归获取了多次互斥锁,则每次“给出”都会使计数递减,直到完全释放为止。

API

xSemaphoreCreateMutex

创建互斥锁,并返回 一个该互斥锁可以引用的句柄。中断服务例程中, 不能使用互斥锁。

configSUPPORT_DYNAMIC_ALLOCATION 和 configUSE_MUTEXES 必须同时在 FreeRTOSConfig.h 中设置为 1,xSemaphoreCreateMutex() 才可用。(configSUPPORT_DYNAMIC_ALLOCATION 也可以不定义, 在这种情况下,它将默认为 1。)

每个互斥锁需要少量 RAM , 以此来保持互斥锁的状态。如果互斥锁是使用 xSemaphoreCreateMutex() 创建的, 则所需的 RAM 将从 FreeRTOS 堆自动分配。 如果互斥锁是使用 xSemaphoreCreateMutexStatic() 创建的, 那么应由应用程序写入器提供 RAM, 但允许在编译时静态分配 RAM 。

使用 xSemaphoreTake() 获取互斥锁, 并使用 xSemaphoreGive() 给出互斥锁。

1
SemaphoreHandle_t xSemaphoreCreateMutex( void )

返回:

  • 如果已成功创建互斥锁型信号量,则返回创建的互斥锁的句柄。
  • 如果由于无法分配保存互斥锁所需的内存而未创建互斥锁,则返回 NULL。
1
2
3
4
5
xSemaphore = xSemaphoreCreateMutex();

if( xSemaphore != NULL ) {
// TODO
}

xSemaphoreTake

同上

xSemaphoreGive

同上

实现

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

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include "gd32f4xx.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "USART0.h"

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
/******
需求:创建3个任务
1. task1:等待Take信号
2. task2:每隔1秒执行一次任务
3. task3:等待Take信号
优先级逐个增加
------------------------------- 信号量
a. 创建互斥信号量
b. 默认发送了一个信号
******/
TaskHandle_t xStartTask_Handler;
//TaskHandle_t xTaskKey_Handler;
TaskHandle_t xTask1_Handler;
TaskHandle_t xTask2_Handler;
TaskHandle_t xTask3_Handler;

SemaphoreHandle_t xSemaphore = NULL;

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

// 同学:优先级低
void task1(void *pvParameters) {
printf("task_Low run!\n");
uint32_t cnt = 0;
while(1){
// 无限阻塞在此函数,直到获取到信号
printf("task_Low try take\n");
xSemaphoreTake(xSemaphore, portMAX_DELAY);
printf("task_Low running!\n");
delay_1ms(3000); // 模拟耗时操作
printf("task_Low give!\n");
xSemaphoreGive(xSemaphore);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

// 坏同学:优先级中
void task2(void *pvParameters) {
printf("task_Middle run!\n");
uint32_t cnt = 0;
while(1){
printf("task_Middle! %d\n", cnt++);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

// 老师:优先级高
void task3(void *pvParameters) {
printf("task_High run!\n");
uint32_t cnt = 0;
BaseType_t xReturn; // pdTRUE, pdFALSE
while(1){
// 无限阻塞在此函数,直到获取到信号
printf("task_High try take\n");
xSemaphoreTake(xSemaphore, portMAX_DELAY);
printf("task_High running!\n");

delay_1ms(1000); // 模拟耗时操作

printf("task_High give!\n");
xSemaphoreGive(xSemaphore);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

void GPIO_config() {
// GPIO input PA0
rcu_periph_clock_enable(RCU_GPIOA);
// input
gpio_mode_set(GPIOA, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO_PIN_0);
}
void sys_init() {
GPIO_config();
USART0_init();
}
void start_task(void *pvParameters) {

// 1. 在start任务中初始化所有外设
sys_init();
printf("Start!\n");

// FreeRTOS 创建互斥信号量, 默认会放一个信号
xSemaphore = xSemaphoreCreateMutex();

if(xSemaphore != NULL){
printf("xSemaphore create successful!\n");
}

// 2. 其他任务,进入临界区,等所有任务都创建完,再退出,一起运行
taskENTER_CRITICAL();

xTaskCreate((TaskFunction_t)task1, // 指向任务函数的指针
(const char* )"task1", // 任务名称
64, // 任务栈大小
NULL, // 任务函数的参数
3, // 任务优先级, 数值越大,优先级越高
(TaskHandle_t* )&xTask1_Handler); // 任务句柄

xTaskCreate((TaskFunction_t)task2, // 指向任务函数的指针
(const char* )"task2", // 任务名称
64, // 任务栈大小
NULL, // 任务函数的参数
4, // 任务优先级, 数值越大,优先级越高
(TaskHandle_t* )&xTask2_Handler); // 任务句柄

xTaskCreate((TaskFunction_t)task3, // 指向任务函数的指针
(const char* )"task3", // 任务名称
64, // 任务栈大小
NULL, // 任务函数的参数
5, // 任务优先级, 数值越大,优先级越高
(TaskHandle_t* )&xTask3_Handler); // 任务句柄
// Exti退出临界区
taskEXIT_CRITICAL();
// 3. 删除自己
vTaskDelete(xStartTask_Handler);
}

int main(void) {
nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0);
systick_config();
// 开始第一个任务
xTaskCreate((TaskFunction_t)start_task, // 任务的函数指针,函数名
(const char* )"start_task", // 任务名称,最大长度configMAX_TASK_NAME_LEN
(uint16_t )128, // 任务栈大小,单位: 字Word / Half Word 128 x 4
(void* )NULL, // 任务函数的参数,通常用NULL
(UBaseType_t )1, // 任务优先级,数值越大,优先级越高
(TaskHandle_t* )&xStartTask_Handler); // 任务句柄
// 开启任务调度
vTaskStartScheduler();
while(1) {
}
}

递归互斥锁

用户可对一把递归互斥锁重复加锁。只有用户为每成功的 xSemaphoreTakeRecursive() 请求调用 xSemaphoreGiveRecursive() 后,互斥锁才会重新变为可用。

例如,如果一个任务成功“加锁”相同的互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁,直到任务也把这个互斥锁“解锁”5 次。

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

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

不能从中断中使用互斥锁的原因是:

  • 互斥锁使用的优先级继承机制要求 从任务中(而不是从中断中)拿走和放入互斥锁。
  • 中断无法保持阻塞来等待一个被互斥锁保护的资源 变得可用。

递归互斥锁的“给出”和“获取”机制

递归互斥锁的基本思想是,每当一个任务成功获取锁时,锁的计数值会增加;当任务释放锁时,锁的计数值会减少。当计数值为零时,锁才会被真正释放,并允许其他任务获取该锁。

获取锁(Take)机制

  • 当一个任务首次请求获取递归互斥锁时,如果锁当前没有被占用,任务会立即获得锁,并将锁的计数器设为 1。
  • 如果该任务再次请求获取该锁,它会成功获取锁(锁的计数器加 1),并继续执行嵌套的代码段。
  • 锁的计数器会随着任务的每次获取递增,这样任务就可以在嵌套的调用中多次获取该锁,而不会发生死锁。

释放锁(Give)机制

  • 当任务释放锁时,锁的计数器会减少。如果计数器的值大于 1,表示任务还有其他嵌套的调用持有锁,释放锁只是减少计数器的值,锁仍然被当前任务持有。
  • 当任务释放锁时,如果计数器的值减至 0,表示当前任务已经没有

API

xSemaphoreCreateRecursiveMutex

创建一个递归互斥锁,并返回一个可以引用该互斥锁的句柄 。不能在中断服务程序中使用递归互斥锁。

1
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void )

configSUPPORT_DYNAMIC_ALLOCATION 和 configUSE_RECURSIVE_MUTEXES 必须在 FreeRTOSConfig.h 中设置为 1,xSemaphoreCreateRecursiveMutex() 才可用(configSUPPORT_DYNAMIC_ALLOCATION 也可以不定义,这种情况下 默认为 1)。

每个递归互斥锁都需要少量 RAM,用于保存递归互斥锁的状态。 如果互斥锁是使用 xSemaphoreCreateRecursiveMutex() 创建的, 则所需的 RAM 将从 FreeRTOS 堆自动分配。 如果一个递归互斥锁是使用 xSemaphoreCreateRecursiveMutexStatic() 创建的, 那么RAM 由应用程序写入器提供,这需要用到一个附加参数, 但允许在编译时静态分配 RAM 。

递归互斥锁分别使用 xSemaphoreTakeRecursive() 和 xSemaphoreGiveRecursive() API 函数“获取”和“释放”。 不得使用 xSemaphoreTake() 和 xSemaphoreGive()。

xSemaphoreCreateMutex()和 xSemaphoreCreateMutexStatic()用于创建非递归互斥锁。非递归互斥锁只能被一个任务获取一次(在释放前),如果同一个任务想再次获取则会失败, 因为当任务第一次释放互斥锁时,互斥锁就一直处于释放状态。

与非递归互斥锁相反,递归互斥锁可以被同一个任务获取很多次, 获取多少次就需要释放多少次, 此时才会返回递归互斥锁。

返回:

如果已成功创建递归互斥锁,则返回创建的互斥锁的句柄。

如果由于无法分配保递归互斥锁所需的内存而未创建递归互斥锁,则返回 NULL。

示例

1
2
3
4
xMutex = xSemaphoreCreateRecursiveMutex();

if( xMutex != NULL ){
}

xSemaphoreTakeRecursive

递归的“获取”一个互斥锁型信号量。 此互斥锁必须已经事先通过调用 xSemaphoreCreateRecursiveMutex() 完成创建;

1
xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex, TickType_t xTicksToWait );

必须在 FreeRTOSConfig.h 中将 configUSE_RECURSIVE_MUTEXES 设置为 1, 此宏才可用。

不得在使用 xSemaphoreCreateMutex() 创建的互斥锁上使用此宏。

所有者可以反复“获取”递归使用的互斥锁。在所有者 为每个成功的“获取”请求调用 xSemaphoreGiveRecursive() 之前,该互斥锁不会再次变得可用。例如, 如果一个任务成功地“获取”了同一个互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁, 直到任务也把这个互斥锁“解锁”5 次。

参数:

  • xMutex

    正在获得的互斥锁的句柄。这是由 xSemaphoreCreateRecursiveMutex() 返回的句柄。

  • xTicksToWait

    等待信号量变为可用的时间(以滴答为单位)。可以使用 portTICK_PERIOD_MS 宏 将其转换为实际时间。可以用一个为零的阻塞时间来轮询信号量。如果 任务已有信号量,则无论 xTicksToWait 的值是多少, xSemaphoreTakeRecursive() 都将立即返回。

返回:

如果获得信号量,则返回 pdTRUE;如果 xTicksToWait 过期,信号量不可用,则返回 pdFALSE。

1
2
3
4
5
6
7
8
9
10
11
12
if( xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 ) == pdTRUE ) {

xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 );
xSemaphoreTakeRecursive( xMutex, ( TickType_t ) 10 );

xSemaphoreGiveRecursive( xMutex );
xSemaphoreGiveRecursive( xMutex );
xSemaphoreGiveRecursive( xMutex );
// 这时候其他任务可以获取这个锁
}
else {
}

xSemaphoreGiveRecursive

递归地释放或“给出”一个互斥锁型信号量的。 此互斥锁必须已经事先通过调用 xSemaphoreCreateRecursiveMutex() 完成创建;

1
xSemaphoreGiveRecursive( SemaphoreHandle_t xMutex )

必须在 FreeRTOSConfig.h 中将 configUSE_RECURSIVE_MUTEXES 设置为 1, 此宏才可用。

不得在使用 xSemaphoreCreateMutex() 创建的互斥锁上使用此宏。

所有者可以反复“获取”递归互斥锁。在所有者为每个成功的“获取”请求调用 xSemaphoreGiveRecursive() 之前,该互斥锁不会再次变得可用。

例如, 如果一个任务成功地“获取”了同一个互斥锁 5 次, 那么任何其他任务都无法使用此互斥锁, 直到任务也把这个互斥锁“解锁”5 次。

参数:

  • xMutex

    正在释放或“给出”的互斥锁的句柄。这是由 xSemaphoreCreateRecursiveMutex() 返回的句柄。

返回:

如果成功给出信号量,则返回 pdTRUE。

实现

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#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) {}
}

优先级继承

FreeRTOS 实现了基本的优先级继承机制,旨在优化 空间和执行周期。完全的优先级继承机制需要多得多的数据和处理器 周期来确定任何时刻的继承优先级,特别是在任务同时占用超过一个互斥锁时 。

请牢记优先级继承机制的这些特定行为:

  • 如果一个任务在占用一个互斥锁时没有先释放它已占用的互斥锁, 则可以进一步提升其继承优先级。
  • 任务在释放其占有的所有互斥锁之前,一直保持最高继承优先级。 这与释放互斥锁的顺序无关。
  • 如果多个互斥锁被占用,无论在任何一个被占用的互斥锁上等待的任务是否完成等待(超时), 则任务将保持最高继承优先级 。