目录
介绍
本文的目的是介绍使用ESP32和Arduino支持的FreeRTOS计数信号量。
我们将开发一个简单的应用程序,我们将使用计数信号量作为执行功能。请注意,这只是一个简单的入门示例,我们可以使用其他机制更有效地实现此类功能,例如任务通知。
信号量通常用于资源访问中的同步和互斥。我将在相关内容部分的信号量上留下更多内容。在我们的示例程序中,我们将使用FreeRTOS 计数信号量。
因此,在我们的程序中,我们将使用setup函数启动可配置数量的任务,然后它将等待信号量以完成所有任务。这种协调将使用前面提到的计数信号量来实现。
因此,在启动任务之后,setup函数将尝试保持信号量与启动的任务一样多次,并且仅在该点之后继续执行。请注意,当任务尝试获取没有单位的信号量(信号量计数器等于0)时,它将停止,直到信号量计数被其他任务递增或发生定义的超时。
代码
我们将通过声明一个全局变量来保持要启动的任务数来启动我们的代码。这将为我们的用例提供更多灵活性。我们将使用4个任务执行示例,但您可以将其更改为其他数字。
然后,我们将计数信号量声明为全局变量,因此可以通过setup函数和任务来访问它。我们使用xSemaphoreCreateCounting 函数创建信号量 。
该函数接收信号量可以达到的最大计数和初始值作为输入参数。由于我们将此用于同步目的,因此我们将指定最大计数等于要启动的任务数,并将其初始化为0。
int nTasks=4;
SemaphoreHandle_t barrierSemaphore = xSemaphoreCreateCounting( nTasks, 0 );
接下来,在设置功能上,我们将首先打开串行连接并打印一个简单的调试消息,这样我们现在就可以执行代码了。
接下来,我们将执行一个循环来启动我们的任务。请注意,我们可以使用相同的任务函数来启动多个任务,因此我们不需要为每个新任务实现代码。我们将检查后者的代码。
请注意,我们将创建固定到ESP32的核心1的任务。这是因为,在看到这个以前的文章中,设置功能被固定到核心1.这将是我们更容易理解信号灯的功能不引入多核执行的复杂性。如果您需要了解推出固定到特定的ESP32核心任务的更多信息,请查看之前的教程。
此外,我们将创建优先级为0的任务。我们将这样做,因为,如本教程所示,setup函数以1的优先级运行(请记住,更高的数字意味着更高的优先级)。因此,由于任务将具有较低的优先级,它们将仅在初始化停止某处时运行,并且我们期望它阻止我们的信号量。
只是为了区分任务,我们将循环的迭代次数作为输入参数传递,因此他们可以在代码中打印它。检查此之前的教程如果您需要了解参数传递给FreeRTOS的任务的详细信息。
在原始代码中,我们通过引用传递变量,这导致了并发问题。我们将按值传递任务参数:
Serial.begin(112500);
delay(1000);
Serial.println("Starting to launch tasks..");
int i;
for(i= 0; i< nTasks; i++){
xTaskCreatePinnedToCore(
genericTask, /* Function to implement the task */
"genericTask", /* Name of the task */
10000, /* Stack size in words */
(void *)i, /* Task input parameter */
0, /* Priority of the task */
NULL, /* Task handle. */
1); /* Core where the task should run */
}
启动任务后,我们将执行for循环尝试获取信号量与启动任务的次数一样多。因此,当信号量中的单元数量与任务数量相同时,设置函数只应从该执行点传递。由于会有增加信号量的任务,我们应该保证Arduino设置功能只有在所有任务完成后才能完成。
要从信号量中获取单位,我们将调用xSemaphoreTake函数。该函数接收信号量的句柄(我们在开头声明和初始化的全局变量)和要等待的最大FreeRTOS滴答数 作为第一个参数。因为我们希望它无限期地阻塞直到它能够获得信号量,所以我们将值传递给最后一个参数值portMAX_DELAY 。
之后,我们将执行最后一条消息,指示setup函数已通过该执行点:
for(i= 0; i< nTasks; i++){
xSemaphoreTake(barrierSemaphore, portMAX_DELAY);
}
Serial.println("Tasks launched and semaphore passed...");
这样就完成了初始化功能。现在我们将实现主循环,作为示例,我们将使用一个名为vTaskSuspend的函数 来暂停主循环任务。这将阻止其执行,无论其优先级是什么。该函数接收要挂起的任务的句柄作为输入。在这种情况下,由于我们想要暂停调用任务,我们传递NULL :
void loop() {
vTaskSuspend(NULL);
}
最后,我们将为我们的任务编写代码。他们将简单地开始,打印他们的数字(从他们被启动时传递的参数),增加信号量然后完成。
因此,我们首先构建并打印指示任务编号的字符串。然后,我们将使用xSemaphoreGive 函数递增信号量。该函数接收信号量的句柄作为输入。请注意,还有一个xSemaphoreGiveFromISR 将在中断服务程序的上下文中使用。
检查以下函数的代码,该函数已包含执行后删除任务的调用。
void genericTask( void * parameter ){
String taskMessage = "Task number:";
taskMessage = taskMessage + ((int)parameter);
Serial.println(taskMessage);
xSemaphoreGive(barrierSemaphore);
vTaskDelete(NULL);
}
请查看本教程的完整源代码:
测试结果
要测试程序结果,只需使用Arduino IDE将程序上传到ESP32板。然后打开串行监视器以检查结果。你可以得到类似于图1的输出。
图1 – 信号量同步程序的输出
请注意,尽管设置功能的优先级高于任何已启动的任务,但它只执行了所有任务执行其代码(打印其编号)后执行(打印“启动的任务和信号量传递…”消息)。
举个例子,如果我们在Arduino初始化函数中注释信号量获取代码,图2显示了输出。
图2 – 没有信号量同步的代码执行
请注意,现在setup函数首先执行所有代码,因为它具有更高的优先级而且从不阻塞。任务只能在安装完成后运行(请记住,我们暂停了主循环的执行,因此,虽然它具有更高的优先级,但它会释放CPU)。
除了不再具有所需的同步之外,我们还可以看到任务现在输出错误的任务编号。这是因为它们在设置功能结束后访问参数。因此,它们访问的内存位置不再有效,因为迭代变量是setup函数的局部变量。