在操作系统系统中,信号量通常用于控制对共享资源的访问和任务之间进行同步,信号量在操作系统中是很常用的,也是学习freeRTOS操作系统必须要掌握的。
freeRTOS中最常用到的信号量有:二值信号量、计数信号量、互斥信号量。
有关这几个信号量分别如下:
1、二值信号量
(1)二值信号量
二值信号量是指所创建的信号量只有两个值(0 和 1),通常用于互斥访问或者同步。
二值信号量在某处被占有使用之后,其他地方想要申请这个二值信号量是无法成功申请的,只有当这个被占有的二值信号量被使用完毕并释放之后,才能被再次申请占有使用!
总而言之,二值信号量被使用之后会变为无效状态,需要被重新释放才能进入有效状态。
二值信号量就像是路口的红绿灯一样,只有两个状态:能通行和不能通行。在freeRTOS中就是能使用和不能使用。如下图示例:
在freeRTOS中,二值信号量的创建和使用的API管理函数分别如下:
(2)创建二值信号量
函数原型:
SemaphoreHandle_t xSemaphoreCreateBinary(void)
函数描述:函数 xSemaphoreCreateBinary 用于创建二值信号量。
返回值:如果创建成功会返回二值信号量的句柄,创建失败会返回 NULL。
(3)等待二值信号量
在freeRTOS中,信号量的获取是进行了区分的,在任务或者函数中获取与在中断中是不一样的,freeRTOS中给出了不同API函数。
1)在任务代码中等待信号量
函数原型:
xSemaphoreTake(
SemaphoreHandle_t xSemaphore, /* 信号量句柄 */
TickType_t xTicksToWait ); /* 等待信号量可用的最大等待时间 */
函数描述:
函数 xSemaphoreTake 用于在任务代码中获取信号量。
第 1 个参数是信号量句柄。
第 2 个参数是没有信号量可用时,等待信号量可用的最大等待时间,单位系统时钟节拍。
返回值:如果创建成功会获取信号量返回 pdTRUE,否则返回 pdFALSE。
使用这个函数要注意以下问题:
此函数是用于任务代码中调用的,不可以在中断服务程序中调用此函数,中断服务程序使用的是xSemaphoreTakeFromISR。
2)在中断中等待信号量
xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )
函数描述:函数xSemaphoreTakeFromISR用于在中断中获取信号量。
第 1 个参数是要获取的信号量的句柄。 这是创建信号量时返回的句柄。
第 2 个参数是如果采用信号量导致任务取消阻止,并且未阻止的任务的优先级高于当前运行的任务,则xSemaphoreTakeFromISR()会将pxHigherPriorityTaskWoken设置为pdTRUE。 如果xSemaphoreTakeFromISR()将此值设置为pdTRUE,则应在退出中断之前请求上下文切换。
返回值:如果创建成功会获取信号量返回 pdTRUE,否则返回 pdFALSE。
(4)释放二值信号量
1)用于在任务代码中释放二值信号量
函数原型:
xSemaphoreGive( SemaphoreHandle_t xSemaphore ); /* 信号量句柄 */
函数描述:释放信号量。函数 xSemaphoreGive 用于在任务代码中释放信号量。
xSemaphore:是信号量句柄。
返回值,如果信号量释放成功返回 pdTRUE,否则返回 pdFALSE,因为信号量的实现是基于消息队列,返回失败的主要原因是消息队列已经满了。
注意:此函数是用于任务代码中调用的,不可以在中断服务程序中调用此函数。
2)用于在中断中释放二值信号量
函数原型:
xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore,
signed BaseType_t *pxHigherPriorityTaskWoken)
函数描述:函数 xSemaphoreGiveFromISR 用于中断服务程序中释放信号量。
xSemaphore:是信号量句柄。
pxHigherPriorityTaskWoken:用于保存是否有高优先级任务准备就绪。如果函数执行完毕后,此参数的数值是pdTRUE,说明有高优先级任务要执行,否则没有。
返回值:如果信号量释放成功返回 pdTRUE,否则返回 errQUEUE_FULL。
2、计数信号量
计数信号量是一个相当于长度大于1的队列,用于任务之间的同步和共享资源的保护。
计数信号量与二值信号量的不同在于,二值信号量只能被一个地方申请使用,只有在这个申请使用的地方了释放了才能被其他处申请使用。而计数信号量是可以创建一定数量的信号量的,多个地方可以同时申请使用,直到达到最大的计数信号量的阈值。
计数信号量相关的API函数:
(1)创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCounting(
UBaseType_t uxMaxCount, /* 支持的最大计数值 */
UBaseType_t uxInitialCount); /* 初始计数值 */
参数说明:
uxMaxCount:设置此计数信号量支持的最大计数值。
uxInitialCount:设置计数信号量的初始值。(为0则不起作用)
返回值:如果创建成功会返回消息队列的句柄,创建失败会返回 NULL。
(2)获取信号量
1)在任务代码中获取信号量
xSemaphoreTake(
SemaphoreHandle_t xSemaphore, /* 信号量句柄 */
TickType_t xTicksToWait ); /* 等待信号量可用的最大等待时间 */
说明:函数 xSemaphoreTake 用于在任务代码中获取信号量。
xSemaphore:是信号量句柄。
xTicksToWait:是没有信号量可用时,等待信号量可用的最大等待时间,单位系统时钟节拍。
返回值:如果信号量获取成功会返回 pdTRUE,否则返回 pdFALSE。
2)在中断中获取信号量
xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )
函数描述:函数 xSemaphoreTakeFromISR 用于在中断中获取信号量。
xSemaphore:是要获取的信号量的句柄。 这是创建信号量时返回的句柄。
pxHigherPriorityTaskWoken:是如果采用信号量导致任务取消阻止,并且未阻止的任务的优先级高于当前运行的任务,则xSemaphoreTakeFromISR()会将pxHigherPriorityTaskWoken设置为pdTRUE。 如果xSemaphoreTakeFromISR()将此值设置为pdTRUE,则应在退出中断之前请求上下文切换。
返回值:如果创建成功会获取信号量返回 pdTRUE,否则返回 pdFALSE。
(3)释放信号量
1)在任务代码中释放信号量
xSemaphoreGive( SemaphoreHandle_t xSemaphore ); /* 信号量句柄 */
函数 xSemaphoreGive 用于在任务代码中释放信号量。
xSemaphore:是信号量句柄。
返回值:如果信号量释放成功返回 pdTRUE,否则返回 pdFALSE,因为计数信号量的实现是基于消息队列,返回失败的主要原因是消息队列已经满了。
2)在中断中释放信号量
xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore, /* 信号量句柄 */
signed BaseType_t *pxHigherPriorityTaskWoken /* 高优先级任务是否被唤醒的状态保存 */ )
xSemaphore:是信号量句柄。
pxHigherPriorityTaskWoken:用于保存是否有高优先级任务准备就绪。如果函数执行完毕后,此参数的数值是pdTRUE,说明有高优先级任务要执行,否则没有。
返回值:如果信号量释放成功返回 pdTRUE,否则返回 errQUEUE_FULL。
3、优先级反转 & 互斥信号量
在实时操作系统中,优先级反转的问题是不容忽视的,程序设计的过程中,也是要充分考虑这个问题的。
那优先级反转到底是什么呢?
优先反转是指:
假如一个系统中有高(H)、中(M)、低(L)三个优先级的任务,并有一个二值信号量。
在某一个时刻二值信号量被低(L)优先级的任务使用了,并在运行过程中,高优先级任务(H)抢占了低优先级(L)的CPU使用权,但是也想要获取二值信号量被低优先(L)的任务占有着,高优先级任务(H)由此被挂起等待了,中优先级任务(M)因为不需要二值信号量,会抢占低优先级(L)任务的执行而得到运行,而高优先级任务(H)依然只能等到低优先级任务(L)释放二值信号量才能得到执行。
由此造成了高优先级任务得不到及时的执行,而低优先级任务却能比高优先级任务更多的得到执行。这就是优先级反转。
优先级互斥的示意图如下:
解决优先级反转的问题最好的办法是使用互斥信号量。
互斥信号量和二值信号量比较相似,不同之处在于互斥信号量具有优先级继承的特性,如果一个互斥信号量正在被一个低优先级的任务使用,而此时这个高优先级的任务也希望获取这个互斥信号量的话就会被阻塞。
互斥信号量能够解决优先级反转的原因:
使用互斥信号量时,高优先级的任务会把低优先级的任务的优先级先提高到和自己相同的优先级,保证低优先级的任务能够继续运行至结束这样极大减少了因为高优先级获取不到信号量被阻塞过长时间的问题。
互斥信号量的API函数:
(1)创建互斥信号量
函数原型:
SemaphoreHandle_t xSemaphoreCreateMutex(void)
函数描述:函数 xSemaphoreCreateMutex 用于创建互斥信号量。
返回值:如果创建成功会返回互斥信号量的句柄,失败会返回 NULL。
(2)获取互斥信号量
函数原型:
xSemaphoreTake(
SemaphoreHandle_t xSemaphore, /* 信号量句柄 */
TickType_t xTicksToWait ); /* 等待信号量可用的最大等待时间 */
函数描述:函数 xSemaphoreTake 用于在任务代码中获取信号量。
xSemaphore:是信号量句柄。
xTicksToWait:是没有信号量可用时,等待信号量可用的最大等待时间,单位系统时钟节拍。
返回值:如果创建成功会获取信号量返回 pdTRUE,否则返回 pdFALSE。
(3)释放互斥信号量
函数原型:
xSemaphoreGive( SemaphoreHandle_t xSemaphore ); /* 信号量句柄 */
函数描述:函数 xSemaphoreGive 用于在任务代码中释放信号量。
xSemaphore:是信号量句柄。
返回值:如果信号量释放成功返回 pdTRUE,否则返回 pdFALSE,因为信号量的实现是基于消息队列,返回失败的主要原因是消息队列已经满了。
4、二值信号量和计数信号量的使用示例
二值信号量和计数信号量的使用思路如下:
1)通过任务start_task创建两个任务led0_task、led2_task,一个二值信号量binary_sem,一个计数信号量count_sem。
2)信号量创建成功之后,在led0_task任务中等到信号量,如果能获取到就翻转LED2的状态。
3)在led2_task任务中定时翻转LED3的状态,表明系统正在运行中。
4)通过外部中断获取按键的按下,更新信号量的值。
注意:本例程使用的是GD32E103进行演示,通过按键更新信号量,从而改变LED灯的状态。
(1)创建任务和二值信号量、计数信号量
/*MCU:GD32E103Vx
* RTOS:freeRTOS*/
void start_task(void *pvParameters)
{
pvParameters = pvParameters;
taskENTER_CRITICAL(); //进入临界区
binary_sem = xSemaphoreCreateBinary(); //创建二值信号量
count_sem = xSemaphoreCreateCounting(10,10);//创建计数信号量
xTaskCreate((TaskFunction_t) led0_task,
(const char*) "led0_task",
(uint16_t) TASK_STK_LED0_SIZE,
(void*) NULL,
(UBaseType_t) TASK_LED0_PRIO,
NULL );
xTaskCreate((TaskFunction_t) led2_task,
(const char*) "led2_task",
(uint16_t) TASK_STK_LED2_SIZE,
(void*) NULL,
(UBaseType_t) TASK_LED2_PRIO,
NULL );
vTaskDelete(StartTask_Handler); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
}
(2)任务函数
void led0_task(void *pvParameters)
{
//pvParameters = pvParameters;
BaseType_t err = pdFALSE;
for(;;)
{
/* 二值信号量
if(binary_sem != NULL)
{
err = xSemaphoreTake(binary_sem,portMAX_DELAY);
if(err == pdTRUE)
{
gd_eval_led_toggle(LED2);
}
}*/
if(count_sem != NULL)
{
err = xSemaphoreTake(count_sem,portMAX_DELAY);
if(err == pdTRUE)
{
gd_eval_led_toggle(LED2);
}
}
vTaskDelay(100);
}
}
void led2_task(void *pvParameters)
{
pvParameters = pvParameters;
for(;;)
{
gd_eval_led_toggle(LED3);
vTaskDelay(500);
}
}
(3)中断处理函数
void EXTI10_15_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken;
if(exti_interrupt_flag_get(EXTI_13) != RESET)
{
exti_interrupt_flag_clear(EXTI_13); //清除中断标志位
/*if(binary_sem != NULL)
{
//释放二值信号量
xSemaphoreGiveFromISR(binary_sem,&xHigherPriorityTaskWoken);
//必要时进行任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}*/
if(count_sem != NULL)
{
xSemaphoreGiveFromISR(count_sem,&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
}