FreeRTOS的学习(二)——任务优先级问题

FreeRTOS的学习系列文章目录

FreeRTOS的学习(一)——STM32上的移植问题
FreeRTOS的学习(二)——任务优先级问题
FreeRTOS的学习(三)——中断机制
FreeRTOS的学习(四)——列表
FreeRTOS的学习(五)——系统延时
FreeRTOS的学习(六)——系统时钟
FreeRTOS的学习(七)——1.队列概念
FreeRTOS的学习(七)——2.队列入队源码分析
FreeRTOS的学习(七)——3.队列出队源码分析
FreeRTOS的学习(八)——1.二值信号量
FreeRTOS的学习(八)——2.计数型信号量
FreeRTOS的学习(八)——3.优先级翻转问题
FreeRTOS的学习(八)——4.互斥信号量
FreeRTOS的学习(九)——软件定时器
FreeRTOS的学习(十)——事件标志组
FreeRTOS的学习(十一)——任务通知


目录

  • FreeRTOS的学习系列文章目录
  • 前言
  • 1 可使用的最大优先级
  • 2 寻找问题
    • 2.1 通用查找方式
    • 2.2 硬件查找方式
    • 2.3 关于两种查找方式的区别
  • 总结


前言

今天开始学了FreeRTOS的配置文件,也就是系列文章(一)里的FreeRTOSConfig,h,系统学习了里面的配置选项的含义。


1 可使用的最大优先级

这是我今天看完配置文件后存在的一个主要的疑惑,为什么设置的最大是32个优先级,虽然一般十来个优先级就足够使用了,因为多个任务能共用一个优先级,但是依旧让我存在一些疑惑:最大优先级能不能更多?有什么限制条件?

2 寻找问题

通过查询配置的宏configMAX_PRIORITIES的reference,在portmacro.h中查找到了以下代码。
代码如下:

#if configUSE_PORT_OPTIMISED_TASK_SELECTION == 1

    /* Check the configuration. */
#if ( configMAX_PRIORITIES > 32 )
#error configUSE_PORT_OPTIMISED_TASK_SELECTION can only be set to 1 when configMAX_PRIORITIES is less than or equal to 32.  It is very rare that a system requires more than 10 to 15 difference priorities as tasks that share a priority will time slice.
#endif

在这个代码里我们可以发现,进入这段程序的条件是configUSE_PORT_OPTIMISED_TASK_SELECTION==1,这也是个宏定义,意思大概是任务调度器查找下一个就绪状态且优先级高,可以进入运行状态的任务的方式。先不管它是啥,进入这个条件后,出现了关于最大优先级的条件,configMAX_PRIORITIES > 32,也就是说,当优先级多余32个时,会执行下面的报错,翻译过来就是,当configUSE_PORT_OPTIMISED_TASK_SELECTION,也就是查找下一个运行任务的方式为1时,优先级必须小于32,并且一般程序用10-15个优先级就够了,一个优先级可以有多个任务。
接下来我开始详细分析configUSE_PORT_OPTIMISED_TASK_SELECTION到底是什么作用,通过查找原子的说明,我发现这个查找下一个运行任务的方式分为软件方式(通用方式),硬件方式(与MCU有关)。那么问题来了,为什么原子得意思是通用方式可以设置无限个优先级,然而硬件方式最多32位,这个与你使用的硬件设备相关,比如我现在测试的引用平台是f103,作为32位的单片机,最多只支持32个优先级,从我们上面找到的这个条件语句来说,确实如此,但是这里他讲的有点含糊,我不是很明白,接下来准备继续查找为什么硬件方式只能是32个。

2.1 通用查找方式

通过搜索configUSE_PORT_OPTIMISED_TASK_SELECTION,我又找到了一段函数片,位于task.c里。
这段代码有点长,我只留下头尾,另外我们大致梳理一下过程。
代码如下:

#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )
//省略
#endif /* configUSE_PORT_OPTIMISED_TASK_SELECTION */

首先看条件(configUSE_PORT_OPTIMISED_TASK_SELECTION == 0),哦吼,有点意思?也就是说选择软件查找的时候会进入这个条件,NEXT?
先看对应的注释,我这边直接翻译了:如果进入了这个条件,那么说明任务选择的方式是通用方式,这种方式不是用于特定的微控制器体系的。另外,注释中给出了一个uxTopReadyPriority,这东西很关键,查找其定义。这是个无符号长整形变量,它拥有着当前就绪的最高优先级任务的优先级大小。另外还有个关键形参uxPriority,同样是优先级相关,这个东西我们在创建任务的函数中就可以看到,就是普通的用来表征优先级的局部参数。

PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;

理解了这两个变量,第一个宏定义的函数(taskRECORD_READY_PRIORITY)就可以看懂了,大概的意思就是如果形参uxPriority大于当前的全局就绪的最大的优先级uxTopReadyPriority,那么就吧当前优先级赋值给uxTopReadyPriority。至此,这个函数大概就是更新就绪状态的最大优先级给uxTopReadyPriority。

/* uxTopReadyPriority holds the priority of the highest priority ready
 * state task. */
#define taskRECORD_READY_PRIORITY( uxPriority ) \
    {                                               \
        if( ( uxPriority ) > uxTopReadyPriority )   \
        {                                           \
            uxTopReadyPriority = ( uxPriority );    \
        }                                           \
    } /* taskRECORD_READY_PRIORITY */

那么这个更新优先级的函数是什么时候被调用的呢?可以看下面的代码(具体位于task.c),可以看到定义了prvAddTaskToReadyList( pxTCB )可以表征的函数中就包括了taskRECORD_READY_PRIORITY,所以当prvAddTaskToReadyList被调用的时候,那么最高就绪优先级就会被重载比较。所以这种情况一般可以分为一开始创建任务的时候(任务创建后,会将其优先级插入到对应优先级的就绪列表中)以及当有任务重新进入就绪状态的时候,uxTopReadyPriority会重新获取当前就绪任务的最高优先级。

/*
 * Place the task represented by pxTCB into the appropriate ready list for
 * the task.  It is inserted at the end of the list.
 */
#define prvAddTaskToReadyList( pxTCB )                                                                 \
    traceMOVED_TASK_TO_READY_STATE( pxTCB );                                                           \
    taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );                                                \
    listINSERT_END( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
    tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )
/*-----------------------------------------------------------*/

抬走,下一个!
第二个函数(taskSELECT_HIGHEST_PRIORITY_TASK),顾名思义,寻找优先级最高的就绪任务,这里涉及到了另一个关键的变量pxReadyTasksLists这个是存储就绪任务优先级的列表,也就是供调度器挑选的后宫,一个优先级对应一个列表,同优先级的就绪任务都挂接在对应的列表上。程序会先从创建的任务中优先级最高的那个就绪任务的列表开始查找,如果找到了就跳出while,将该找到的任务给TCB任务控制块去执行,然后把找到的优先级传递给uxTopReadyPriority。至于这个uxTopReadyPriority什么时候会改变,上面的函数已经进行了说明。

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                \
    {                                                                         \
        UBaseType_t uxTopPriority = uxTopReadyPriority;                       \                                                                             \
        /* Find the highest priority queue that contains ready tasks. */      \
        while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
        {                                                                     \
            configASSERT( uxTopPriority );                                    \
            --uxTopPriority;                                                  \
        }                                                                     \                                                                              \
        /* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of \
         * the  same priority get an equal share of the processor time. */                    \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
        uxTopReadyPriority = uxTopPriority;                                                   \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK */

至此,通用方式下查找下一个运行任务的方式已经基本搞定了,具体细节还需仔细推敲。可以发现这种方式效率确实地下,其实复杂度及基本是查找数组的最大值嘛,相对来说也还好,但是对于资源有限的arm来说,占用还是越少越好啊。
硬件查找方式应运而生。

2.2 硬件查找方式

继续往下看代码:

#else /* configUSE_PORT_OPTIMISED_TASK_SELECTION */
/* If configUSE_PORT_OPTIMISED_TASK_SELECTION is 1 then task selection is
 * performed in a way that is tailored to the particular microcontroller
 * architecture being used. */

到这,很显然就是configUSE_PORT_OPTIMISED_TASK_SELECTION=1时进行的定义部分了,也就是硬件查找下一个运行任务的方式。
可以看到这部分有三个函数,其中taskRESET_READY_PRIORITY是之前软件查找方式没有的。我们先来看看他的功能。

/* A port optimised version is provided, call it only if the TCB being reset
 * is being referenced from a ready list.  If it is referenced from a delayed
 * or suspended list then it won't be in a ready list. */
#define taskRESET_READY_PRIORITY( uxPriority )                                                     \
    {                                                                                                  \
        if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) \
        {                                                                                              \
            portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );                        \
        }                                                                                              \
    }

翻译过来的大意是:仅当从就绪列表中引用正在重置的TCB时,才调用它。如果它是从阻塞态或挂起态的列表中引用的,那么它将不在就绪列表中,所以也就不会调用该taskRESET函数了。 首先该函数进入了一个条件,listCURRENT_LIST_LENGTH宏的作用是访问它会返回当前列表中的项目数。那么这里查找的是那个列表呢?可以看到后面写道:

listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0

uxPriority 就是个形参,是调用这个taskRESET函数时的写入的优先级,通过输入该优先级查找该优先级下的就绪列表。然后取该列表的首地址,查找该列表内部是否还有任务,如果任务数==0,则条件成立。因此可以看到,task RESET函数只有在就绪列表里没有任务时才能工作,其具体的工作内容以及portRESET_READY_PRIORITY的宏定义如下:

//工作内容
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );
//宏定义
#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities )     ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )

该宏的具体操作是对于uxTopReadyPriority而言的,实质上述的所有操作都是在为这个全局变量服务。该函数的内容就是通过 将1左移uxPriority,再取反,与uxTopReadyPriority取与。这顿操作猛如虎,我竟一时没看懂。不怕,先放放。
下一个函数!

/* A port optimised version is provided.  Call the port defined macros. */
#define taskRECORD_READY_PRIORITY( uxPriority )    portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )
//portRECORD_READY_PRIORITY的宏定义
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities )    ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

这里的taskRECORD宏显然是与taskRESET相对的,只是使用的条件和场合并不同。一个是用来存储,一个是用来清除。portRECORD_READY_PRIORITY的操作与前面套路的宏操作也是相对的。
到这儿我想不得不去深入看看硬件查找方式的原理了。查找资料后可以了解以下。

Cortex-M 内核有一个计算前导零的指令CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32 位)从高位开始第一次出现 1 的位的前面的零的个数。 比如: 一个 32 位的变量 uxTopReadyPriority, 其位 0、位 24 和 位 25 均 置 1 , 其 余 位 为 0 , 具 体 见 下图。 那 么 使 用 前 导 零 指 令 __CLZ(uxTopReadyPriority)可以很快的计算出 uxTopReadyPriority 的前导零的个数为 6。
在这里插入图片描述
如果 uxTopReadyPriority 的每个位号对应的是任务的优先级,任务就绪时,则将对应的位置 1,反之则清零。那么上图就表示优先级 0、优先级 24 和优先级 25 这三个任务就绪,其中优先级为 25 的任务优先级最高。

到这,我相信前两个问题就可以得到解决了。再次把两个位操作的宏拿出来比较:

#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities )     ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities )    ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

首先,uxPriority 按照定义最大优先级为32,那么uxPriority 应该是一个0-31的数字,然而uxReadyPriorities 也就是最后传递到uxTopReadyPriority应该是一个32位 的变量,这个位数的大小与硬件处理器的位数有关,也正好解决了我一开始的疑问。这个变量的每一位上的1代表着一个优先级。所以可以发现上述的两个位操作,实际上就是把当前uxPriority 代表的优先级(数值0-31),在uxReadyPriorities (位数0-31)上作对应的RESET(清0)以及RECORD(置1)。相信到这边,大家已经基本明白硬件查找方式的优先级是如何工作的了。实质上依旧是软件上的位操作,但是不能忽略硬件设计的CLZ指令,没有他就无法快速计算uxTopReadyPriority上位上有1对应的是多少优先级。
接下来还有最后一个宏定义函数,也就是taskSELECT_HIGHEST_PRIORITY_TASK,请看下面的代码:

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                                  \
    {                                                                                           \
        UBaseType_t uxTopPriority;                                                              \                                                                                                \
        /* Find the highest priority list that contains ready tasks. */                         \
        portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );                          \
        configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
        listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );   \
    } /* taskSELECT_HIGHEST_PRIORITY_TASK() */

该宏功能就是在就绪任务列表中选择最高的优先级,内部第一个函数就是得到最高优先级的port宏:portGET_HIGHEST_PRIORITY,其代码段如下:

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities )    uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

这里就用到了__clz指令,也就是从高位求取用位1表示的uxReadyPriorities,计算得到位上为1的最高位前的0数目,例如,0x00102040,则其最高是在位23上有个1,那么__clz(0x00102040)的结果就是8,用31UL-8得到的也就是对应的优先级23,赋值给上述的局部变量uxTopPriority。接下来的configASSERT是一个断言,相当于做了一个判断,该判断要求该上面找到的最大优先级对应的就绪列表中是有任务的,如果没有任务呢?就会触发断言对应的结果,按照默认FreeConfig,h的配置就会打印出错误。
最后的一个函数listGET_OWNER_OF_NEXT_ENTRY意思就是将找到的任务的任务块赋值给pxCurrentTCB,这样就确定了下一个要运行的任务,接下来会去执行。

2.3 关于两种查找方式的区别

软件查找的方式遇到找到的最高的优先级对应的就绪任务列表中没有任务,他会-1继续向下寻找,然而硬件查找方式却并没有这个操作。
首先我以我个人的角度还解释一下这种情况,可能是有紧急情况调度了高优先级的任务,而uxTopReadyPriority对于通用的查找方式,taskRESET_READY_PRIORITY宏函数似乎并没有起作用(见下面的代码段),所以这种情况下最高就绪优先级并没有更新,因此如果出现对应优先级的列表里没有东西时,需要**–uxTopPriority**,然而硬件查找方式的RESET函数可以起到及时更新uxTopReadyPriority对应优先级的位的状态(1/0)。当然,具体我还没有深究,也希望能与相关方面的专家同行一起讨论一下,能直接告诉我原因结论就最好了。嘻嘻嘻。

/* Define away taskRESET_READY_PRIORITY() and portRESET_READY_PRIORITY() as
 * they are only required when a port optimised method of task selection is
 * being used. */
 //由于通用方式并非是优化的方法,所以下面两种宏定义是非必要的,并没有实际意义。
#define taskRESET_READY_PRIORITY( uxPriority )
#define portRESET_READY_PRIORITY( uxPriority, uxTopReadyPriority )

总结

洋洋洒洒写了很多,大概就是梳理了我从遇到一个不解的概念到逐步找到问题,深入探索问题的原因,这其中会不知不觉学到其他的概念,也就在无意间拓宽了学习的道路,经过这个过程,也大概了梳理了FreeRTOS的源码是如何工作的,希望这篇自我探索的博客能带来更多的意义。

你可能感兴趣的:(STM32,FreeRTOS,单片机,嵌入式硬件,stm32,c语言,FreeRTOS)