本文是FreeRTOS教程系列的第三篇,将介绍“二值信号量”的使用。
信号量用于将任务与系统中的其他事件同步。在FreeRTOS中,信号量是基于队列机制(后面会有一节专门讲队列)实现的。在FreeRTOS中有4种类型的信号量:
- 二值信号量;
- 计数信号量;
- 互斥锁
- 递归
二值信号量,顾名思义,是只有两个值,“0”和“1”的信号量。一个任务要么有信号量,有么没有信号量,没有第三种情况(比如有两个信号量)。引入信号量的原因是:某些情况下单纯依靠任务优先级进行任务调度,不能满足应用要求。假设有如下场景:一个低优先级任务(LTask)负责处理数据,得到的数据将被另一个高优先级任务(HTask)使用。根据任务调度规则,LTask随时可能被HTask抢断。一旦LTask中关键代码部分被抢断,HTask可能会拿到”半成品“数据,导致计算错误。但是如果LTask拥有信号量,执行完关键部分的代码才释放信号量。在此期间,HTask即使准备就绪,也必须等待LTask释放信号量以后才能抢断LTask,潜在的错误就可以避免。
CubeMX设置
首先在CubeMX中创建三个不同的任务,任务的属性设置如下
图1.创建任务
接着,创建一个二值信号量,如下图所示
图2.创建信号量
其余设置与前两节类似,不再赘述。
代码部分
在CubeMX中已经创建了三个不同的任务,下面实现任务内容
首先是中等优先级任务(MEDIUM Task)
void StartMediumTask(void const *argument)
{
/* USER CODE BEGIN StartMediumTask */
/* Infinite loop */
for (;;)
{
printf("Entered MEDIUM Task\n");
printf("Leaving MEDIUM Task\n\n");
osDelay(500);
}
/* USER CODE END StartMediumTask */
}
其实在这个示例中,运行中等优先级任务不需要信号量。我创建这个任务是为了说明,即使创建了信号量,未必每个任务都要使用它,有些任务还是可以和信号量没有任何关系。这个MEDIUM Task不会等待任何信号量,所以它可以独立运行,在需要的时候抢低优先级任务LOW Task。
其次是高优先级任务(HIGH Task)
void StartHighTask(void const *argument)
{
/* USER CODE BEGIN StartHighTask */
/* Infinite loop */
for (;;)
{
printf("Entered HighTask and waiting for Semaphore\n");
osSemaphoreWait(myBinarySem01Handle, osWaitForever);
printf("Semaphore acquired by HIGH Task\n");
printf("Leaving HighTask and releasing Semaphore\n\n");
osSemaphoreRelease(myBinarySem01Handle);
osDelay(500);
}
/* USER CODE END StartHighTask */
}
在第8行代码,osSemaphoreWait(myBinarySem01Handle, osWaitForever);
,高优先级任务会在此处一直等待取得二值信号量,然后才能继续向下执行。函数的第一个参数是信号量句柄,第二个参数是等待时间。在这里,任务将永远等待。一旦任务执行完毕,在第11行,二元信号量被释放osSemaphoreRelease(myBinarySem01Handle);
。信号量被释放以后,其他等待信号量的任务得以继续执行。
再者是低优先级任务(LOW Task)
void StartLowTask(void const *argument)
{
/* USER CODE BEGIN StartLowTask */
/* Infinite loop */
for (;;)
{
printf("Entered LOWTask and waiting for semaphore\n");
osSemaphoreWait(myBinarySem01Handle, osWaitForever);
printf("Semaphore acquired by LOW Task, waitting the key being pressed \n");
while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13))
; // 一直等待按键按下
printf("Leaving LOWTask and releasing Semaphore\n\n");
osSemaphoreRelease(myBinarySem01Handle);
osDelay(500);
}
与高优先级任务类似,LOW Task也在等待信号量。一旦其获得信号量,就会等待按钮被按下,然后通过串口发送消息,最后释放信号量。
接下来看一下程序运行效果
实验结果
图3.程序运行效果
- 开始时,调度器有3个任务要执行,HIGH Task首先被执行;
- HIGH Task首先获得信号量,自身执行完成后,释放信号量;
- 然后,MEDIUM Task执行,在任务中通过延时函数挂起500毫秒;
- 接着,LOW Task开始执行。其获得信号量以后,等待按钮被按下;
- HIGH Task延时结束,结束被挂起的状态,抢断LOW Task,在任务中持续等待获得信号量,但是此时信号量被LOW Task占有,HIGH Task只能等待;
- MEDIUM Task结束挂起状态,抢断LOW Task。由于无需等待获取信号量,MEDIUM Task能够按自己的节奏,每500ms执行一次;
如果不按下按钮,只有MEDIUM Task在持续执行,当按下按钮时,如下图所示,情况发生了变化
图4.按钮按下后程序运行情况
上图表明按钮按下后发生了以下过程
- MEDIUM Task持续运行,抢断LOW Task;
- 按钮按下,LOW Task得以继续执行,随后释放信号量;
- HIGH Task随后获得信号量,得以继续执行剩余代码,在任务的最后释放信号量;
- MEDIUM Task执行;
- HIGH Task和LOW Task等待执行,调度器让HIGH Task先执行。在任务中先获取信号量然后又释放;
- MEDIUM Task被延时函数挂起500ms,还未恢复,此时只有LOW Task需要执行。在任务中获取信号量然后等待按钮被按下;
- HIGH Task被延时函数挂起500ms,还未恢复。MEDIUM Task从挂起状态恢复,抢断LOW Task执行;
- HIGH Task从挂起状态恢复,等待信号量,但是此时信号量被LOW Task占有;
- MEDIUM Task从挂起状态恢复,抢断LOW Task执行,保持每500ms执行一次。情况又回到了按钮被按下之前的状态;
由以上分析可以看出,如果HIGH Task未能获取信号量便值能一直等待,不能能继续执行。
回到本节开头的情景,如果LOW Task有关键的数据处理任务要执行,而HIGH Task又需要使用处理完成的数据。就可以添加信号量,LOW Task在数据处理前获取信号量,数据处理完成后释放信号量。HIGH Task在使用数据前,需要先获取信号量,如果获取不到便会持续等待数据处理完成。
二值信号量带来的优先级翻转(高优先级任务需要等待低优先级任务),某些情况下会带来问题。同时二值信号量也有其局限性,不足以应对所有使用需求。我们后面会介绍其他手段,解决实际应用中可能遇到的其他问题。
【本节教程结束】
欢迎关注我的同名公众号**“早点儿毕业”**,我会在公众号同步发布教程(笔记)内容。
版权声明:本文为CSDN博主「早点儿毕业」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35913527/article/details/122021032
本文是FreeRTOS教程系列的第三篇,将介绍“二值信号量”的使用。
信号量用于将任务与系统中的其他事件同步。在FreeRTOS中,信号量是基于队列机制(后面会有一节专门讲队列)实现的。在FreeRTOS中有4种类型的信号量:
- 二值信号量;
- 计数信号量;
- 互斥锁
- 递归
二值信号量,顾名思义,是只有两个值,“0”和“1”的信号量。一个任务要么有信号量,有么没有信号量,没有第三种情况(比如有两个信号量)。引入信号量的原因是:某些情况下单纯依靠任务优先级进行任务调度,不能满足应用要求。假设有如下场景:一个低优先级任务(LTask)负责处理数据,得到的数据将被另一个高优先级任务(HTask)使用。根据任务调度规则,LTask随时可能被HTask抢断。一旦LTask中关键代码部分被抢断,HTask可能会拿到”半成品“数据,导致计算错误。但是如果LTask拥有信号量,执行完关键部分的代码才释放信号量。在此期间,HTask即使准备就绪,也必须等待LTask释放信号量以后才能抢断LTask,潜在的错误就可以避免。
CubeMX设置
首先在CubeMX中创建三个不同的任务,任务的属性设置如下
图1.创建任务
接着,创建一个二值信号量,如下图所示
图2.创建信号量
其余设置与前两节类似,不再赘述。
代码部分
在CubeMX中已经创建了三个不同的任务,下面实现任务内容
首先是中等优先级任务(MEDIUM Task)
void StartMediumTask(void const *argument)
{
/* USER CODE BEGIN StartMediumTask */
/* Infinite loop */
for (;;)
{
printf("Entered MEDIUM Task\n");
printf("Leaving MEDIUM Task\n\n");
osDelay(500);
}
/* USER CODE END StartMediumTask */
}
其实在这个示例中,运行中等优先级任务不需要信号量。我创建这个任务是为了说明,即使创建了信号量,未必每个任务都要使用它,有些任务还是可以和信号量没有任何关系。这个MEDIUM Task不会等待任何信号量,所以它可以独立运行,在需要的时候抢低优先级任务LOW Task。
其次是高优先级任务(HIGH Task)
void StartHighTask(void const *argument)
{
/* USER CODE BEGIN StartHighTask */
/* Infinite loop */
for (;;)
{
printf("Entered HighTask and waiting for Semaphore\n");
osSemaphoreWait(myBinarySem01Handle, osWaitForever);
printf("Semaphore acquired by HIGH Task\n");
printf("Leaving HighTask and releasing Semaphore\n\n");
osSemaphoreRelease(myBinarySem01Handle);
osDelay(500);
}
/* USER CODE END StartHighTask */
}
在第8行代码,osSemaphoreWait(myBinarySem01Handle, osWaitForever);
,高优先级任务会在此处一直等待取得二值信号量,然后才能继续向下执行。函数的第一个参数是信号量句柄,第二个参数是等待时间。在这里,任务将永远等待。一旦任务执行完毕,在第11行,二元信号量被释放osSemaphoreRelease(myBinarySem01Handle);
。信号量被释放以后,其他等待信号量的任务得以继续执行。
再者是低优先级任务(LOW Task)
void StartLowTask(void const *argument)
{
/* USER CODE BEGIN StartLowTask */
/* Infinite loop */
for (;;)
{
printf("Entered LOWTask and waiting for semaphore\n");
osSemaphoreWait(myBinarySem01Handle, osWaitForever);
printf("Semaphore acquired by LOW Task, waitting the key being pressed \n");
while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13))
; // 一直等待按键按下
printf("Leaving LOWTask and releasing Semaphore\n\n");
osSemaphoreRelease(myBinarySem01Handle);
osDelay(500);
}
与高优先级任务类似,LOW Task也在等待信号量。一旦其获得信号量,就会等待按钮被按下,然后通过串口发送消息,最后释放信号量。
接下来看一下程序运行效果
实验结果
图3.程序运行效果
- 开始时,调度器有3个任务要执行,HIGH Task首先被执行;
- HIGH Task首先获得信号量,自身执行完成后,释放信号量;
- 然后,MEDIUM Task执行,在任务中通过延时函数挂起500毫秒;
- 接着,LOW Task开始执行。其获得信号量以后,等待按钮被按下;
- HIGH Task延时结束,结束被挂起的状态,抢断LOW Task,在任务中持续等待获得信号量,但是此时信号量被LOW Task占有,HIGH Task只能等待;
- MEDIUM Task结束挂起状态,抢断LOW Task。由于无需等待获取信号量,MEDIUM Task能够按自己的节奏,每500ms执行一次;
如果不按下按钮,只有MEDIUM Task在持续执行,当按下按钮时,如下图所示,情况发生了变化
图4.按钮按下后程序运行情况
上图表明按钮按下后发生了以下过程
- MEDIUM Task持续运行,抢断LOW Task;
- 按钮按下,LOW Task得以继续执行,随后释放信号量;
- HIGH Task随后获得信号量,得以继续执行剩余代码,在任务的最后释放信号量;
- MEDIUM Task执行;
- HIGH Task和LOW Task等待执行,调度器让HIGH Task先执行。在任务中先获取信号量然后又释放;
- MEDIUM Task被延时函数挂起500ms,还未恢复,此时只有LOW Task需要执行。在任务中获取信号量然后等待按钮被按下;
- HIGH Task被延时函数挂起500ms,还未恢复。MEDIUM Task从挂起状态恢复,抢断LOW Task执行;
- HIGH Task从挂起状态恢复,等待信号量,但是此时信号量被LOW Task占有;
- MEDIUM Task从挂起状态恢复,抢断LOW Task执行,保持每500ms执行一次。情况又回到了按钮被按下之前的状态;
由以上分析可以看出,如果HIGH Task未能获取信号量便值能一直等待,不能能继续执行。
回到本节开头的情景,如果LOW Task有关键的数据处理任务要执行,而HIGH Task又需要使用处理完成的数据。就可以添加信号量,LOW Task在数据处理前获取信号量,数据处理完成后释放信号量。HIGH Task在使用数据前,需要先获取信号量,如果获取不到便会持续等待数据处理完成。
二值信号量带来的优先级翻转(高优先级任务需要等待低优先级任务),某些情况下会带来问题。同时二值信号量也有其局限性,不足以应对所有使用需求。我们后面会介绍其他手段,解决实际应用中可能遇到的其他问题。
【本节教程结束】
欢迎关注我的同名公众号**“早点儿毕业”**,我会在公众号同步发布教程(笔记)内容。
版权声明:本文为CSDN博主「早点儿毕业」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_35913527/article/details/122021032
暂无评论