μC/OS-III
里面两个地方用到了环形表,时钟节拍任务,定时器列表,通过排序后,效率是非常高的。
嵌入式实时操作系统uC/OS-Ⅲ
2023/12/21 18:04:16
(1) 该时钟节拍列表由一个数据表(见 os_cfg_app.c
中的 OSCfg_TickWheel[]
) 和一个计数器(OSTickCtr
)构成。
(2) OSCfg_TickWheel[]
数据表包含的表项(entry)数目是由 os_cfg_app.h
文件中的 OS_CFG_TICK_WHEEL_SIZE
设定的,可在编译时进行配置。表项的具体数目要根据处理器中可用的 RAM 存储量和应用程序中的最大任务数来配置。一般可以把 OS_CFG_TICK_WHEEL_SIZE
的值设置为任务数目的1/4左右。建议不要使 OS_CFG_TICK_WHEEL_SIZE
的数值与时钟节拍的频率成倍数关系。如果时钟节拍频率是1000 Hz并且在用户的应用中有50个任务,那么用户应当避免把 OS_CFG_TICK_WHEEL_SIZE
的数值设置为10或20,而应当改为使用11或23。实际上,最好使用素数来设置 OS_CFG_TICK_WHEEL_SIZE
。虽然在编译时很难预计运行时将会发生的事情,但理论上可以使在每个表项上等待的任务的数目均匀分布。
(3) 该数据表中的每个表项包含3个成员:
.NbrEntriesMax
表示在该表项上等待的任务的最大数目。应用程序可以通过调用函数 OSStatReset()
来重置 .NbrEntriesMax
的值。
.NbrEntries
表示在该表项上等待的任务的数目。
.FirstPtr
是一个指针变量,在表头上并属于该表,指向在该表项上的等待任务构成的双向链表(通过各任务的 TCB
结构体)。
每当任务 OS_TickTask()
接收到时钟节拍中断发送的信号时,它会将 OSTickCtr
加1。
当应用程序中的某个任务调用 OSTimeDlyXXX()
函数或者使用非零的超时值调用 OSXXXPend()
函数时,该任务会被自动插入到时钟节拍列表中。
MatchValue = 10 + 13
OSCfg_TickWheel[] spoke number = (10 + 13) * 2
或
MatchValue = 23
OSCfg_TickWheel[] spoke number = 11
第二个任务会插入到与第一个任务相同的表项中,如图 F5-11 所示。在同一个表项上等待的多个任务按照升序排列,因此,剩余等待时间最少的任务会放在时钟节拍列表的最前面。
在时钟节拍任务执行时(见 os_tick.c
文件中的 OSTickTask()
和 OSTickListUpdate()
),它首先会递增 OSTickCtr
的数值,然后判断应该处理哪个表项。如果位于该表项上的时钟节拍列表中有任务存在(即 .FirstPtr
非空),则时钟节拍任务会检查相关任务的 .TickCtrMatch
数值是否与 OSTickCtr
数值相同,如果相同,就会把相应的 OS_TCB
从时钟节拍列表中删除。如果该任务只是等待延时结束,则将会被放入任务就绪表中(后面会描述)。如果该任务在等待某个事件,则不仅需要把它从时钟节拍列表中删除,还需要把它从该事件的任务等待表中删除。在搜索时钟节拍列表时,一旦发现 OSTickCtr
的数值与任务的 .TickCtrMatch
数值不相等,就会立即结束该搜索操作。这是因为延时最先结束的任务总是放在表的最前面。
如果前面的任务延时还没有结束,那么后面的任务延时时间显然也没有到,也就没有必要再进一步搜索了。
注意,OS TickTask()
在更新时钟节拍列表时所做的大部分工作都是在临界段代码中完成的。不过,由于时钟节拍列表是按照顺序排列的,因此,可以把临界区控制得相当短
时钟节拍中断服务程序仅需唤醒时钟节拍任务,可大大缩短中断处理时间。时钟节拍任务使用了一个由N个表项(即辐条)构成的环形数据表(即时钟节拍轮),N的数值由用户配置。所有延时的任务按照延时结束时刻分配到各个表项上,每次节拍中断发生时,只有其中一个表项上的任务可能延时结束。在各个表项上,任务按照延时结束的先后顺序排序。因此,时钟节拍任务每次被节拍中断唤醒后,只处理一个表项,从该表项中的第一个任务开始判断任务延时是否结束,延时结束则继续判断下一个任务,否则停止判断并返回,从而大大节省时间节拍处理时间。
F12-8(1) 定时器列表结构中含有一个表 (OSCfg_TmrWheel[]
,在 os_cfg_app.c
中声明) 和一个计数器 (OSTmrTickCtr
,在 os.h
中声明)。
F12-8(2) 这个表可以容纳的条目数的最大值由参数 OS_CFG_TMR_WHEEL_SIZE
决定,可以在编译时设定(见 os_cfg_app.h
)。实际的条目数取决于处理器可用的 RAM 大小和应用程序中的最大定时器数量。建议将 OS_CFG_TMR_WHEEL_SIZE
设为定时器数量的 1/4 左右。不建议将 OS_CFG_TMR_WHEEL_SIZE
设为定时器任务率的偶数倍。例如,如果定时器任务率是10Hz,请避免将 OS_CFG_TMR_WHEEL_SIZE
设为10或100(可以使用11或101)。最好使用素数。尽管在编译时很难预测运行时的情况,但在理想情况下,每个表项中定时器的数量应该是均匀分布的。
F12-8(3) 表中的每个条目包括三个字段:.NbrEntriesMax
、 NbrEntries
和 .FirstPtr
。
.NbrEntries
表示链接到这个条目的定时器数量。.NbrEntriesMax
用来跟踪表中的最大条目数。.FirstPtr
包括一个指向当前位置上定时器双向链表的指针(通过 OS_TMR
)。每次时钟节拍中断服务程序 (ISR) 发送信号给定时器任务时,OSTmrTickCtr
增加1。
要将定时器插入定时器列表中,需要调用函数 OSTmrStart()
。当然,定时器必须在使用之前先创建。
μC/OS-II将计算匹配值和序号,如下所示:
即
MatchValue = 22
OSCfg_TmrWheel[]中的序号 = 4
如图 F12-10 所示,第二个定时器插入到同一个条目指向的链表中,并且系统将根据定时器的剩余时间排序,将剩余时间较少的定时器排在链表头,而剩余时间最长的定时器排在链表尾。
当定时器任务执行时(见 os_tmr.c
的 OS_TmrTask()
),OSTmrTickCtr
开始加 1 并指向下一个辐条格子。如果此条中含有定时器(即 .FirstPtr
非空),那么第一个 S_TMR
的 .Match
都会被检查是否与 OSTmrTickCtr
匹配。如果找到匹配的 OS_TMR
,则将其从列表中移除,并且检查下一个,然后 OS_TmrTask()
将调用定时器回调函数(假设定时器创建时已定义)。反之,若 OSTmrTickCtr
不匹配定时器的 .Match
值,则查找过程立即结束。原因是链表已经排序,不需要再继续向后查找。
注意,OS_TmrTask()
执行时调度器是上了锁的。由于链表已经排序,并且查找过程在找不到匹配值后立即结束,因此临界代码段比较短。
备注:由于 OS_TMR
链表按照定时器的剩余时间从小到大排序,所以它们的 .Match
值也是按照相同顺序排列的。如果 OSTmrTickCtr
小于链表中某个定时器的 .Match
值,那么后面的定时器也一定都不匹配,可以停止查找并等待下一次比较。正是这样的轮设计和排序链表,使得系统大大减少了遍历每个定时器所需的时间。