FreeRTOS 计数信号量

时间:2021-11-23 15:12:03

以下转载自安富莱电子: http://forum.armfly.com/forum.php

本章节开始讲解 FreeRTOS 任务间的同步和资源共享机制,计数信号量。 FreeRTOS 中计数信号量的源码实现是基于消息队列实现的。 

信号量的概念及其作用
信号量(semaphores)是 20 世纪 60 年代中期 Edgser Dijkstra 发明的。 使用信号量的最初目的是
为了给共享资源建立一个标志,该标志表示该共享资源被占用情况。这样,当一个任务在访问共享资源之
前,就可以先对这个标志进行查询,从而在了解资源被占用的情况之后,再来决定自己的行为。
实际的应用中,信号量的作用又该如何体现呢?比如有个 30 人的电脑机房,我们就可以创建信号量
的初始化值是 30,表示 30 个可用资源,不理解的初学者表示信号量还有初始值?是的,信号量说白了就
是共享资源的数量。 另外我们要求一个同学使用一台电脑,这样每有一个同学使用一台电脑,那么信号量
的数值就减一,直到 30 台电脑都被占用,此时信号量的数值就是 0。 如果此时还有几个同学没有电脑可
以使用,那么这几个同学就得等待,直到有同学离开。 有一个同学离开,那么信号量的数值就加 1,有两
个就加 2,依此类推。刚才没有电脑用的同学此时就有电脑可以用了,有几个同学用,信号量就减几,直
到再次没有电脑可以用,这么一个过程就是使用信号量来管理共享资源的过程。
平时使用信号量主要实现以下两个功能:
两个任务之间或者中断函数跟任务之间的同步功能,这个和前面章节讲解的事件标志组是类似的。其
实就是共享资源为 1 的时候。
多个共享资源的管理,就像上面举的机房上机的例子。
针对这两种功能,FreeRTOS 分别提供了二值信号量和计数信号量,其中二值信号量可以理解成计数

信号量的一种特殊形式,即初始化为仅有一个资源可以使用,只不过 FreeRTOS 对这两种都提供了 API
函数,而像 RTX,uCOS-II 和 III 是仅提供了一个信号量功能,设置不同的初始值就可以分别实现二值信
号量和计数信号量。 当然,FreeRTOS 使用计数信号量也能够实现同样的效果。
实际上信号量还有很多其它用法,而且极具挑战性,可以大大的开拓大家的视野,有兴趣的同学可以
阅读一下《The Little Book Of Semaphores》 ,作者是 Allen B. Downy。
FreeRTOS 任务间计数信号量的实现
任务间信号量的实现是指各个任务之间使用信号量实现任务的同步或者资源共享功能。 下面我们通过
如下的框图来说明一下 FreeRTOS 计数信号量的实现,让大家有一个形象的认识。
FreeRTOS 计数信号量

运行条件:
创建 2 个任务 Task1 和 Task2。
创建计数信号量可用资源为 1。
运行过程描述如下:
任务 Task1 运行过程中调用函数 xSemaphoreTake 获取信号量资源,如果信号量没有被任务 Task2
占用,Task1 将直接获取资源。 如果信号量被 Task2 占用,任务 Task1 将由运行态转到阻塞状态,
等待资源可用。一旦获取了资源并使用完毕后会通过函数 xSemaphoreGive 释放掉资源。
任务 Task2 运行过程中调用函数 xSemaphoreTake 获取信号量资源,如果信号量没有被任务 Task1
占用,Task2 将直接获取资源。 如果信号量被 Task1 占用,任务 Task2 将由运行态转到阻塞状态,
等待资源可用。一旦获取了资源并使用完毕后会通过函数 xSemaphoreGive 释放掉资源。

 FreeRTOS 中断方式计数信号量的实现
FreeRTOS 中断方式信号量的实现是指中断函数和 FreeRTOS 任务之间使用信号量。 信号量的中断方
式主要是用于实现任务同步,与前面章节讲解事件标志组中断方式是一样的。
下面我们通过如下的框图来说明一下 FreeRTOS 中断方式信号量的实现,让大家有一个形象的认识。

FreeRTOS 计数信号量

 

运行条件:
创建一个任务 Task1 和一个串口接收中断。
信号量的初始值为 0,串口中断调用函数 xSemaphoreGiveFromISR 释放信号量,任务 Task1 调用
函数 xSemaphoreTake 获取信号量资源。
运行过程描述如下:
任务 Task1 运行过程中调用函数 xSemaphoreTake,由于信号量的初始值是 0,没有信号量资源可
用,任务 Task1 由运行态进入到阻塞态。
Task1 阻塞的情况下,串口接收到数据进入到了串口中断服务程序,在串口中断服务程序中调用函数
xSemaphoreGiveFromISR 释放信号量资源,信号量数值加 1,此时信号量计数值为 1,任务 Task1
由阻塞态进入到就绪态,在调度器的作用下由就绪态又进入到运行态,任务 Task1 获得信号量后,信
号量数值减 1,此时信号量计数值又变成了 0。
再次循环执行时,任务 Task1 调用函数 xSemaphoreTake 由于没有资源可用再次进入到阻塞态,等
待串口释放信号量资源,如此往复循环。
上面就是一个简单的 FreeRTOS 中断方式信号量同步过程。 实际应用中,中断方式的消息机制要注意以下
四个问题:
中断函数的执行时间越短越好,防止其它低于这个中断优先级的异常不能得到及时响应。
实际应用中,建议不要在中断中实现消息处理,用户可以在中断服务程序里面发送消息通知任务,在
任务中实现消息处理,这样可以有效地保证中断服务程序的实时响应。同时此任务也需要设置为高优
先级,以便退出中断函数后任务可以得到及时执行。
中断服务程序中一定要调用专用于中断的信号量设置函数,即以 FromISR 结尾的函数。
在操作系统中实现中断服务程序与裸机编程的区别。
   如果 FreeRTOS 工程的中断函数中没有调用 FreeRTOS 的信号量 API 函数,与裸机编程是一样
的。
   如果 FreeRTOS 工程的中断函数中调用了 FreeRTOS 的信号量 API 函数,退出的时候要检测是
否有高优先级任务就绪,如果有就绪的,需要在退出中断后进行任务切换,这点与裸机编程稍有
区别,详见 实验例程说明(中断方式):
   另外强烈推荐用户将 Cortex-M3 内核的 STM32F103 和 Cortex-M4 内核的 STM32F407,F429
的 NVIC 优先级分组设置为 4,即:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);这样中
断优先级的管理将非常方便。
   用户要在 FreeRTOS 多任务开启前就设置好优先级分组,一旦设置好切记不可再修改。
计数信号量 API 函数
使用如下 18 个函数可以实现 FreeRTOS 的信号量(含计数信号量,二值信号量和互斥信号):
xSemaphoreCreateBinary()
xSemaphoreCreateBinaryStatic()
vSemaphoreCreateBinary()
xSemaphoreCreateCounting()
xSemaphoreCreateCountingStatic()
xSemaphoreCreateMutex()
xSemaphoreCreateMutexStatic()
xSem'CreateRecursiveMutex()
xSem'CreateRecursiveMutexStatic()
vSemaphoreDelete()
xSemaphoreGetMutexHolder()
uxSemaphoreGetCount()
xSemaphoreTake()
xSemaphoreTakeFromISR()
xSemaphoreTakeRecursive()
xSemaphoreGive()
xSemaphoreGiveRecursive()
xSemaphoreGiveFromISR()
关于这 18 个函数的讲解及其使用方法可以看 FreeRTOS 在线版手册 。

这里我们重点的说以下 4 个函数:
xSemaphoreCreateCounting ()
xSemaphoreGive ()
xSemaphoreGiveFromISR ()
xSemaphoreTake ()
因为本章节配套的例子使用的是这 4 个函数。

函数 xSemaphoreCreateCounting
FreeRTOS 计数信号量

函数描述:
函数 xSemaphoreCreateCounting 用于创建计数信号量。
第 1 个参数是设置此计数信号量支持的最大计数值。
第 2 个参数是设置计数信号量的初始值。 

返回值,如果创建成功会返回消息队列的句柄,如果由于 FreeRTOSConfig.h 文件中 heap 大小不足,
无法为此消息队列提供所需的空间会返回 NULL。
使用这个函数要注意以下问题:
1. 此函数是基函数 xQueueCreateCountingSemaphore 实现的:
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) \
xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
函数 xQueueCreateCountingSemaphore 的实现是基于消息队列函数 xQueueGenericCreate 实现
的。
2. 使用此函数要在 FreeRTOSConfig.h 文件中使能宏定义:
#define configUSE_COUNTING_SEMAPHORES 1

FreeRTOS 计数信号量

 

函数 xSemaphoreGive

FreeRTOS 计数信号量

函数描述:
函数 xSemaphoreGive 用于在任务代码中释放信号量。
第 1 个参数是信号量句柄。
返回值,如果信号量释放成功返回 pdTRUE,否则返回 pdFALSE,因为计数信号量的实现是基于消
息队列,返回失败的主要原因是消息队列已经满了。
使用这个函数要注意以下问题:
1. 此函数是基于消息队列函数 xQueueGenericSend 实现的:

#define xSemaphoreGive( xSemaphore ) \
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, \
queueSEND_TO_BACK )
2. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是
xSemaphoreGiveFromISR。
3. 使用此函数前,一定要保证用函数 xSemaphoreCreateBinary(), xSemaphoreCreateMutex() 或者
xSemaphoreCreateCounting()创建了信号量。
4. 此函数不支持使用 xSemaphoreCreateRecursiveMutex()创建的信号量。

函数 xSemaphoreGiveFromISR

FreeRTOS 计数信号量

函数描述:
函数 xSemaphoreGiveFromISR 用于中断服务程序中释放信号量。
第 1 个参数是信号量句柄。
第 2 个参数用于保存是否有高优先级任务准备就绪。如果函数执行完毕后,此参数的数值是 pdTRUE,
说明有高优先级任务要执行,否则没有。
返回值,如果信号量释放成功返回 pdTRUE,否则返回 errQUEUE_FULL。
使用这个函数要注意以下问题:
1. 此函数是基于消息队列函数 xQueueGiveFromISR 实现的:
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) \
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
2. 此函数是用于中断服务程序中调用的,故不可以任务代码中调用此函数,任务代码中中使用的是
xSemaphoreGive。
3. 使用此函数前,一定要保证用函数 xSemaphoreCreateBinary()或者 xSemaphoreCreateCounting()
创建了信号量。
4. 此函数不支持使用 xSemaphoreCreateMutex ()创建的信号量

FreeRTOS 计数信号量

   FreeRTOS 计数信号量

函数 xSemaphoreTake

 FreeRTOS 计数信号量

函数描述:
函数 xSemaphoreTake 用于在任务代码中获取信号量。
第 1 个参数是信号量句柄。
第 2 个参数是没有信号量可用时,等待信号量可用的最大等待时间,单位系统时钟节拍。
返回值,如果创建成功会获取信号量返回 pdTRUE,否则返回 pdFALSE。
使用这个函数要注意以下问题:
1. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序使用的是
xSemaphoreTakeFromISR。
2. 如果消息队列为空且第 2 个参数为 0,那么此函数会立即返回。
3. 如果用户将 FreeRTOSConfig.h 文件中的宏定义 INCLUDE_vTaskSuspend 配置为 1 且第 2 个参数配
置为 portMAX_DELAY,那么此函数会永久等待直到信号量可用。

实验例程现象(任务间通信)

FreeRTOS 计数信号量

 

 

FreeRTOS 实验_计数信号量(中断方式)——及其重要

K2 键按下,启动单次定时器中断,50ms 后在定时器中断给任务 vTaskBeep 发送同步信号。

FreeRTOS 计数信号量

 这个中断程序调试了我大半下午,现在总结一下要点:

我的定时器中断服务函数:

void  BASIC_TIM_IRQHandler (void)
{
    if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET ) 
    {    
        ulHighFrequencyTimerTicks++;
        cc++;
        TIM_ClearITPendingBit(BASIC_TIM , TIM_IT_Update);           
    }        
        if(cc==10000)
        {
            cc=0;
            TIM_ClearITPendingBit(BASIC_TIM , TIM_IT_Update);
            BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    LED3_TOGGLE
    /* 发送同步信号 */
        xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);

    /* 如果xHigherPriorityTaskWoken = pdTRUE,那么退出中断后切到当前最高优先级任务执行 */
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
        
        }
        
}

portYIELD_FROM_ISR函数的参数,如果为pdTRUE,退出中断就会切换到高优先级任务执行。那是因为这个portYIELD_FROM_ISR函数在参数为pdTRUE时,会调用portYIELD()函数强制上下文切换。

FreeRTOS有两种方法触发任务切换:

  • 执行系统调用,比如普通任务可以使用taskYIELD()强制任务切换,中断服务程序中使用portYIELD_FROM_ISR()强制任务切换;
  • 系统节拍时钟中断

      对于Cortex-M3,M4平台,这两种方法的实质是一样的,都会使能一个PendSV中断,在PendSV中断服务程序中,找到最高优先级的就绪任务,然后让这个任务获得CPU运行权,从而完成任务切换。
      对于第一种任务切换方法,不管是使用taskYIELD()还是portYIELD_FROM_ISR(),最终都会执行宏portYIELD(),代码如下:

FreeRTOS 计数信号量

 从上面的代码中可以看出,PendSV中断的产生是通过代码:portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT实现的,它向中断状态寄存器bit28位写入1,将PendSV中断设置为挂起状态,等到优先级高于PendSV的中断执行完成后,PendSV中断服务程序将被执行,进行任务切换工作。

PendSV中断服务程序源码是一段汇编,我们这里不去深究,画出这段汇编程序的流程图:

FreeRTOS 计数信号量

 

 (此处摘自:http://blog.csdn.net/zhzht19861011/article/details/51418383)

 

我们只用知道这个中断函数会进入临界区和退出临界区就行了,这也是我刚开始做实验找到了影响实验的因素却没有找到源头,经过上面的流程图就豁然开朗了。

要进入临界区,就要关闭优先级大于 等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY这个宏配置的值的所有中断,这个值我之前配置成3,而我的定时器中断优先级配置成2,造成的实验现象就是,定时器中断进入一次之后,系统就卡死了。从逻辑上讲,这样设置是可以的才对的啊,我不想要os管理我的定时器中断,所以就不屏蔽我的定时器中断,但是FreeRTOS有自己的机制和解释,我们需要遵守别人的规定,继续看下面。卡在这里:

FreeRTOS 计数信号量

 一直在寻找中断优先级

当我把定时器中断的优先级的数学字面值设置成大于等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY这个值时,系统就正常运行了。

这也就是说,在我们的实验中,必须保证定时器中断被freertos管理,毕竟我们的发送同步消息函数是在定时器中断函数中的,也就是我们定时中断设置的优先级,在数学字面值(后来看官方文档,这个数学字面值既然巧合的默契了)上,应该大于等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY宏的值,这样系统才可以正常运行。

上面这一段是我自己在没有看到官方解释时候的理解,果不其然,在卡死函数下方,官方给出了参考链接。现在截取关键部分展示:

FreeRTOS 计数信号量

 译:

数字优先级值与逻辑优先级设置之间的反向关系

Cortex-M硬件细节

接下来要知道的是,在ARM Cortex-M内核中,数字低优先级值用于指定逻辑高的中断优先级。例如,分配数值优先级值为2的中断的逻辑优先级高于分配数字优先级为5的中断的逻辑优先级。换句话说,即使数字2较低,中断优先级2高于中断优先级5.希望能够清楚:分配数字优先级为2的中断可以中断(嵌套)一个分配数字优先级为5的中断,但是分配数字优先级为5的中断不能中断分配了数字中断优先级为2的。
这是ARM Cortex-M中断优先级的最直观的方面,因为它与大多数非ARM Cortex-M3微控制器架构相反。

使用RTOS时的相关性

以“FromISR”结尾的FreeRTOS功能是中断安全的,但是即使这些函数的逻辑优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY(configMAX_SYSCALL_INTERRUPT_PRIORITY在FreeRTOSConfig.h头文件中定义)的优先级,也不能调用这些函数。因此,使用RTOS API函数的任何中断服务程序必须将其优先级手动设置为数值等于或大于configMAX_SYSCALL_INTERRUPT_PRIORITY设置的值。这确保中断的逻辑优先级等于或小于configMAX_SYSCALL_INTERRUPT_PRIORITY设置。(果然和我想的一样,这也就是既然你调用了FreeRTOS的API,你的中断优先级设置就应该是属于FreeRTOS管理的)
Cortex-M中断优先级的默认值为零。零是可能的最高优先级值。因此,不要将使用中断安全RTOS API的中断优先级设置为其默认值。

这也就是说,官方都已告诉我们了,我们既然使用别人的产品,就要遵守别人的规定和约束,终于,解惑了。