使用Arduino在FreeRTOS中实现信号量和互斥量的方式
使用Arduino在FreeRTOS中实现信号量和互斥量的方式
在嵌入式系统开发中,信号量和互斥量是用于同步、资源管理和保护资源免受损坏的重要机制。本文将详细介绍如何在Arduino的FreeRTOS系统中实现信号量和互斥量,包括其基本概念、类型以及具体的应用场景。
什么是信号量?
信号量是一种信号机制,其中处于等待状态的任务由另一个任务发出信号以执行。换句话说,当一个task1完成它的工作时,它会显示一个标志或将一个标志加1,然后另一个任务(task2)收到这个标志,表明它现在可以执行它的工作了。当 task2 完成其工作时,标志将减 1。
因此,基本上,它是一种“给予”和“接受”机制,而信号量是一个整数变量,用于同步对资源的访问。
FreeRTOS 中的信号量类型
信号量有两种类型:
- Binary Semaphore:它有两个整数值0和1。有点类似于长度为1的Queue。比如我们有两个task,task1和task2。task1向task2发送数据,因此task2不断检查队列项,如果有1,那么它可以读取数据,否则它必须等到它变成1。取完数据后,task2将队列递减,使其为0,即task1再次可以将数据发送到task2。
从上面的例子可以说,二进制信号量是用于任务之间或任务与中断之间的同步。
- Counting Semaphore:它的值大于0,可以认为是长度大于1的队列。这个semaphore用于对事件进行计数。在这种使用场景中,事件处理程序将在每次事件发生时“给予”一个信号量(增加信号量计数值),而处理程序任务将在每次处理事件时“获取”一个信号量(减少信号量计数值) 。
因此,计数值是已发生的事件数与已处理的事件数之差。
如何在 FreeRTOS 中使用信号量?
FreeRTOS 支持用于创建信号量、获取信号量和提供信号量的不同 API。在本教程中,我们将使用二进制信号量,因为它易于理解和实现。由于此处使用了中断功能,因此您需要在 ISR 功能中使用受中断保护的 API。当我们说将任务与中断同步时,这意味着在 ISR 之后立即将任务置于运行状态。
创建信号量
要使用任何内核对象,我们必须首先创建它。要创建二进制信号量,请使用vSemaphoreCreateBinary()
。
此 API 不接受任何参数,并返回 SemaphoreHandle_t
类型的变量。创建一个全局变量名sema_v
来存储信号量。
SemaphoreHandle_t sema_v;
sema_v = xSemaphoreCreateBinary();
给出信号量
对于提供信号量,有两种版本——一种用于中断,另一种用于正常任务。
xSemaphoreGive()
:这个 API 只接受一个参数,它是信号量的变量名,如上面在创建信号量时给出的sema_v
。它可以从您想要同步的任何正常任务中调用。xSemaphoreGiveFromISR()
:这是xSemaphoreGive()
的受中断保护的 API 版本。当我们需要同步 ISR 和普通任务时,应该从 ISR 函数中使用xSemaphoreGiveFromISR()
。
获取信号量
要获取信号量,请使用 API 函数xSemaphoreTake()
。这个 API 有两个参数。
xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
xSemaphore
:在我们的案例sema_v
中要采用的信号量的名称。xTicksToWait
:这是任务在阻塞状态下等待信号量变为可用的最长时间。在我们的项目中,我们将xTicksToWait
设置为portMAX_DELAY
以使task_1
无限期地等待阻塞状态,直到sema_v
可用。
信号量代码实现
这里连接了一个按钮和两个LED。按钮将充当连接到Arduino Uno 引脚 2 的中断按钮。按下此按钮时将产生中断,连接到引脚 8 的 LED 将打开,再次按下时将关闭。
因此,当按下按钮时,将从 ISR 函数调用xSemaphoreGiveFromISR ()
,从 TaskLED
函数调用 xSemaphoreTake()
函数。为了使系统看起来多任务,将其他 LED 连接到引脚 7,引脚 7 将始终处于闪烁状态。
代码实现步骤
- 首先,包含Arduino_FreeRTOS.h头文件。现在,如果使用任何内核对象,如队列信号量,则还必须包含一个头文件。
#include <Arduino_FreeRTOS.h>
- 声明一个
SemaphoreHandle_t
类型的变量来存储信号量的值。
SemaphoreHandle_t interruptSemaphore;
- 在
void setup()
中,使用xTaskCreate()
API 创建两个任务(TaskLED
和TaskBlink
),然后使用xSemaphoreCreateBinary()
创建一个信号量。创建一个具有相同优先级的任务,然后尝试使用这个数字。此外,将引脚 2 配置为输入并启用内部上拉电阻并连接中断引脚。最后,启动调度程序,如下所示。
void setup() {
pinMode(2, INPUT_PULLUP);
xTaskCreate(TaskLED, "TaskLED", 128, NULL, 1, NULL);
xTaskCreate(TaskBlink, "TaskBlink", 128, NULL, 1, NULL);
interruptSemaphore = xSemaphoreCreateBinary();
attachInterrupt(digitalPinToInterrupt(2), debounceInterrupt, FALLING);
vTaskStartScheduler();
}
- 现在,实现 ISR 功能。创建一个函数并将其命名为与
attachInterrupt()
函数的第二个参数相同。为了使中断正常工作,您需要使用millis
或micros
功能并通过调整去抖时间来消除按钮的去抖问题。从此函数调用interruptHandler()
函数,如下所示。
long debounceTime = 150;
volatile unsigned long lastMicros;
void debounceInterrupt() {
if ((long)(micros() - lastMicros) >= debounceTime * 1000) {
interruptHandler();
lastMicros = micros();
}
}
void interruptHandler() {
xSemaphoreGiveFromISR(interruptSemaphore, NULL);
}
这个函数会给TaskLed
一个信号量来打开LED。
- 创建一个
TaskLed
函数并在while
循环中调用xSemaphoreTake()
API 并检查信号量是否被成功获取。如果它等于pdPASS
(即 1),则使 LED 切换如下所示。
void TaskLed(void *pvParameters) {
pinMode(8, OUTPUT);
while (1) {
if (xSemaphoreTake(interruptSemaphore, portMAX_DELAY) == pdPASS) {
digitalWrite(8, HIGH);
vTaskDelay(pdMS_TO_TICKS(500));
digitalWrite(8, LOW);
}
}
}
- 另外,创建一个函数来闪烁连接到引脚 7 的其他 LED。
void TaskBlink(void *pvParameters) {
pinMode(7, OUTPUT);
while (1) {
digitalWrite(7, HIGH);
vTaskDelay(pdMS_TO_TICKS(200));
digitalWrite(7, LOW);
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void loop()
函数将保持为空。不要忘记它。
void loop() {}
电路原理图
上传代码后,您会看到一个 LED 在 200 毫秒后闪烁,当按下按钮时,第二个 LED 会立即发光。
什么是互斥锁?
如上所述,信号量是一种信号机制,类似地,Mutex 是一种锁定机制,与信号量不同,信号量具有单独的递增和递减函数,但在 Mutex 中,函数本身接受和给出。这是一种避免共享资源损坏的技术。
为了保护共享资源,需要为资源分配一个令牌卡(互斥体)。拥有这张卡的人可以访问其他资源。其他人应该等到卡归还。这样,只有一个资源可以访问任务,其他资源等待机会。
让我们通过一个例子来了解FreeRTOS 中的 Mutex 。这里我们有三个任务,一个用于在 LCD 上打印数据,第二个用于将 LDR 数据发送到 LCD 任务,最后一个任务用于在 LCD 上发送温度数据。所以这里两个任务共享相同的资源,即 LCD。如果 LDR 任务和温度任务同时发送数据,则其中一个数据可能损坏或丢失。
因此,为了防止数据丢失,我们需要锁定 task1 的 LCD 资源,直到它完成显示任务。然后 LCD 任务将解锁,然后 task2 可以执行其工作。
如何在 FreeRTOS 中使用互斥锁?
互斥量的使用方式也与信号量相同。首先,创建它,然后使用各自的 API 提供和获取。
创建互斥锁
要创建互斥体,请使用xSemaphoreCreateMutex()
API。顾名思义,互斥量是一种二进制信号量。它们用于不同的上下文和目的。二进制信号量用于同步任务,而 Mutex 用于保护共享资源。
此 API 不接受任何参数并返回SemaphoreHandle_t
类型的变量。如果无法创建互斥锁,则xSemaphoreCreateMutex()
返回 NULL。
SemaphoreHandle_t mutex_v;
mutex_v = xSemaphoreCreateMutex();
采取互斥锁
当任务想要访问资源时,它将使用xSemaphoreTake()
API 获取 Mutex。它与二进制信号量相同。它还需要两个参数。
xSemaphore
:在我们的例子中使用的 Mutex 的名称mutex_v
。xTicksToWait
:这是任务在阻塞状态下等待 Mutex 可用的最长时间。在我们的项目中,我们将xTicksToWait
设置为portMAX_DELAY
以使task_1
在 Blocked 状态下无限期等待,直到mutex_v
可用。
提供互斥锁
访问共享资源后,任务应该返回 Mutex,以便其他任务可以访问它。xSemaphoreGive()
API 用于返回 Mutex。
xSemaphoreGive()
函数只接受一个参数,即在我们的案例 mutex_v
中给出的 Mutex。
互斥代码实现
这部分的目标是使用串行监视器作为共享资源和两个不同的任务来访问串行监视器以打印一些消息。
- 头文件将保持与信号量相同。
#include <Arduino_FreeRTOS.h>
- 声明一个
SemaphoreHandle_t
类型的变量来存储 Mutex 的值。
SemaphoreHandle_t mutex_v;
- 在
void setup()
中,以 9600 波特率初始化串行监视器,并使用xTaskCreate()
API 创建两个任务(Task1
和Task2
)。然后使用xSemaphoreCreateMutex()
创建一个 Mutex 。创建一个具有相同优先级的任务,然后尝试使用这个数字。
void setup() {
Serial.begin(9600);
mutex_v = xSemaphoreCreateMutex();
if (mutex_v == NULL) {
Serial.println("无法创建互斥锁");
}
xTaskCreate(Task1, "任务 1", 128, NULL, 1, NULL);
xTaskCreate(Task2, "任务 2", 128, NULL, 1, NULL);
}
- 现在,为
Task1
和Task2
制作任务函数。在任务函数的while
循环中,在串行监视器上打印消息之前,我们必须使用xSemaphoreTake()
获取 Mutex ,然后打印消息,然后使用xSemaphoreGive()
返回 Mutex。然后再拖延一些时间。
void Task1(void *pvParameters) {
while (1) {
xSemaphoreTake(mutex_v, portMAX_DELAY);
Serial.println("来自 Task1 的您好");
xSemaphoreGive(mutex_v);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task2(void *pvParameters) {
while (1) {
xSemaphoreTake(mutex_v, portMAX_DELAY);
Serial.println("来自 Task2 的您好");
xSemaphoreGive(mutex_v);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void loop()
将保持为空。
现在,将此代码上传到 Arduino UNO 并打开串行监视器。您将看到正在从 task1
和 task2
打印消息。
要测试 Mutex 的工作,只需注释xSemaphoreGive(mutex_v);
从任何任务。您可以看到程序挂在最后一条打印消息上。
这就是使用 Arduino 在 FreeRTOS 中实现信号量和互斥量的方式。