上次实验我们了解了信号量,这次实验我们将学会互斥量的使用。本次实验采用STM32F103ZET6主芯片的开发板,使用HAL库开发。
目录
互斥量的优点
在使用信号量进行互斥型资源访问控制时(互斥性资源:指线程之间由于竞争某些共享资源存在的与相互制约关系,就像一把钥匙同时只能用于开一把锁,想开另外一把得等这里用完才可以),有可能会出现优先级翻转。互斥量可以比较好的避免这种问题,因为它增加了优先级继承机制。
什么是优先级翻转
优先级翻转就是本来应该先运行高优先级然后再运行低优先级,但是因为在使用信号量进行控制时,有可能会使优先级翻转,即低优先级先于高优先级运行。
我们通过一个实验来了解优先级翻转问题
实验目的:在这个实验中我们使用一个二值信号量和三个优先级不同的任务,通过串口输出来实现优先级翻转。
串口就是一种互斥型资源。
实验过程:设置三个任务优先级分别为高(TaskHP)、中(TaskMP)、低(TaskLP)如图:
然后在它们运行时分别通过串口输出消息,高和低分别都检测二值信号来判断是否运行并且在运行完成后输出二值信号量,中不使用二值信号量,采用优先级抢占运行。开始TaskLP运行然后TaskHP抢占准备运行然后没有检测到二值信号量,就将CPU使用释放,在t4时刻TaskLP被TaskMP抢占,然后等待TaskMP运行完,TaskLP运行到t6释放信号量,这时TaskHP才开始运行。可以看到中间部分中优先级的任务比高优先级的任务先执行。我们接下来使用实验完成这个现象。
CobeMX设置
这部分我们系统时钟配置和之前一样,这里不再赘述,这里打开串口1,(我的开发板自带了一个串口转USB接口(CH340),我这里直接将其连接到电脑即可,如果自己的开发板没有带这个功能,需要自己购买一个串口转USB接口,然后在自己的电脑上下载CH340驱动。)将其配置成Asynchronous模式,波特率设置为115200,具体配置如图:
图表 1 串口设置
这里需要注意其他的设置需要和串口调试助手配置一样,不然会导致通讯出现异常。
然后我们需要打开freertos,设置模式为V2,然后生成三个优先级不同的任务,如图所示:
图表 2RTOS任务配置
再在Timers and Semaphore 页面创建一个二值信号量如图:
图表 3RTOS二值信号量设置
配置完成后就可以生成代码
代码实现部分
这次我们不需要在主函数中操作,直接进入freertos.c,在头文件中添加我们需要用到的头文件:
#include "usart.h" // 串口相关配置
#include "semphr.h" // 信号量中部分函数的来源
#include <stdio.h> // 实现串口printf转换
然后我们添加一段代码以实现串口可以直接使用printf函数输出:
int fputc(int ch, FILE *f)
{
uint8_t temp[1] = {ch};
HAL_UART_Transmit(&huart1, temp, 1, 2); // huart1是串口1,如果使用其他串口需要更改这个变量
return ch;
}
基本设置完成,找到我们配置的三个任务,实现任务代码
首先Task_low任务中我们需要检测信号量,检测成功后在其中使用库函数的延时函数占用信号量,最后完成后释放信号量,具体代码如下:
void Task_low(void *argument)
{
/* USER CODE BEGIN Task_low */
/* Infinite loop */
for(;;)
{
if(xSemaphoreTake(tokenHandle,200) == pdTRUE) // 获取到信号量
{
printf("this is low task!\n"); // 串口输出
HAL_Delay(1000);
xSemaphoreGive(tokenHandle);
// portYIELD_FROM_ISR(HighTaskWoken); // 进行任务调度
}
}
/* USER CODE END Task_low */
}
在Task_middle任务中只是简单的串口输出和延时作用,具体代码如下:
void Task_middle(void *argument)
{
/* USER CODE BEGIN Task_middle */
/* Infinite loop */
for(;;)
{
printf("this is middle task!\n"); // 串口输出
vTaskDelay(500); /* 刚好为低等级延时的一般,
让低等级串口输出一次后中等级输出两次 */
}
/* USER CODE END Task_middle */
}
在Task_high中我们也需要检测信号量,检测成功后在其中使用RTOS的延时函数占用信号量,最后完成后释放信号量,具体代码如下:
void Task_high(void *argument)
{
/* USER CODE BEGIN Task_high */
/* Infinite loop */
for(;;)
{
if(xSemaphoreTake(tokenHandle,portMAX_DELAY) == pdTRUE) // 获取到信号量
{
printf("this is high task!\n"); // 串口输出
xSemaphoreGive(tokenHandle); // 释放信号量
}
vTaskDelay(500);
}
/* USER CODE END Task_high */
}
代码书写完成后,烧录进开发板,使用串口调试助手查看输出结果,如果发现没有输出,回到keil中,打开魔术棒,在TarGer中有个USE MicroLIB选项,将这个选项选上,如图:
然后重新编译和烧录,之后要是输出乱码,就查看自己的串口设置和串口调试助手的设置是否一致,最后检查两者的编码方式是否一致:
这些修改完成后基本就没什么问题了(笔者暂时就遇到过这些问题)
查看输出结果,如图:
可以看到在低优先级占用CPU时,被中等优先级的抢占了两次,然后才到高优先级运行。
优先级继承
互斥量可以很好的解决之前的问题,这就是优先级继承,即我们不希望低优先级完成前被高优先级抢占,我们可以利用互斥量将低优先级先暂时提高到另一个申请互斥量的高优先级一样的优先级,等到运行完成后再将优先级改回来。
互斥量的基本函数和二值信号量一样,之前的设置基本上都可以用,我们这里直接将上一次的CobeMX设置复制,然后将之前的二值信号量删除,在Mutex 页面创建一个互斥量,如图:
图表 4RTOS互斥量设置
完成设置后生成代码,将我们刚才书写的代码直接复制,然后编译、烧录进开发板,打开串口调试助手,查看实验现象:
这次运行就按照之前设定的优先级运行了。
注意:互斥量并不能彻底解决优先级翻转问题,但是至少可以缓解优先级翻转问题
而且因为互斥量是通过更改任务优先级来实现优先级翻转问题的,ISR并不是任务,或者说中断的优先级是高于所有任务的,所以在中断中不能使用互斥量。