uC/OSIII时钟节拍处理过程,尤其是调度的过程!


 

时钟节拍涉及到的函数调用过程:
从main开始,创建了第一个任务AppTaskStart,在其一开始执行时,对BSP和CPU进行初始化,调用BSP_CPU_TickInit()函数;
该函数定义在bsp文件夹下的bsp.c文件中,该函数先读取时钟频率,然后按OSCfg_TickRate_Hz值计算应该为CM3的systick定时器设置的到期值。
void BSP_CPU_TickInit (void)
{
    CPU_INT32U cpu_clk_freq;
    CPU_INT32U cnts;
   cpu_clk_freq = BSP_CPU_ClkFreq(); /* Determine SysTick reference freq. */ 
    cnts = (cpu_clk_freq / OSCfg_TickRate_Hz); /* Determine nbr SysTick increments */ 
    OS_CPU_SysTickInit(cnts); /* Initialize the SysTick. */ 
}
这里的OSCfg_TickRate_Hz又在ucosiii\source文件夹的OS_cfg_app.c中被定义为:
OS_RATE_HZ const OSCfg_TickRate_Hz = (OS_RATE_HZ )OS_CFG_TICK_RATE_HZ;
OS_CFG_TICK_RATE_HZ是对用户开放的,在st\eval\ucosiii文件夹的OS_cfg_app.h中被如下定义,如下:
#define OS_CFG_TICK_RATE_HZ 1000u /* Tick rate in Hertz (10 to 1000 Hz) */
在上面的void BSP_CPU_TickInit (void)函数中,调用OS_CPU_SysTickInit(cnts)(这个函数定义在ucosiii\source\MDK的os_cpu_c.c中最后一段),因为该函数直接设置CM3的systick定时器寄存器,属于与编译器相关的:
void OS_CPU_SysTickInit (CPU_INT32U cnts)
{
    CPU_INT32U prio;
    CPU_REG_NVIC_ST_RELOAD = cnts - 1u;  /* Set SysTick handler prio. */
    prio = CPU_REG_NVIC_SHPRI3;
    prio &= DEF_BIT_FIELD(24, 0); /* 将bit32到bit24清0 */
    prio |= DEF_BIT_MASK(OS_CPU_CFG_SYSTICK_PRIO, 24);/* 默认为最高优先级0 */
    CPU_REG_NVIC_SHPRI3 = prio;   /* Enable timer. */
    CPU_REG_NVIC_ST_CTRL |= CPU_REG_NVIC_ST_CTRL_CLKSOURCE |
    CPU_REG_NVIC_ST_CTRL_ENABLE;   /* Enable timer interrupt. */
    CPU_REG_NVIC_ST_CTRL |= CPU_REG_NVIC_ST_CTRL_TICKINT;
}
这里设置完成后,CM3的systick定时器即被开启,会按1000Hz的频率定时产生中断。
中断处理函数在它的同一个文件(os_cpu_c.c)中,它上面几行即是时钟中断处理函数:
void OS_CPU_SysTickHandler (void)
{
    CPU_SR_ALLOC();
   CPU_CRITICAL_ENTER(); 
    OSIntNestingCtr++; /* Tell uC/OS-III that we are starting an ISR */
    CPU_CRITICAL_EXIT();
    OSTimeTick(); /* Call uC/OS-III's OSTimeTick() */
    OSIntExit(); /* Tell uC/OS-III that we are leaving the ISR */
}
可见,时钟到期时,会调用OSTimeTick(),然后会调用OSIntExit()函数。注意OSIntExit()这个函数是专门用于中断退出时做任务调度使用的,它内部做了两个工作:一、找到当前的最高优先级的任务;二、调用PendSV软中断,进行任务切换调度。源码不展开分析了。题外话,PendSV软中断用于任务调度是ucos与CM3珠联璧合的结果。
然后具体分析一下中断处理中的核心OSTimeTick()函数。这个函数被定义在ucosiii\source文件夹下的OS_time.c文件中,位于该文件最后一段。
先讲明该函数的动作:它主要有两部分代码,是选择性编译的。方案一、如果配置了打开中断延迟处理任务(这个开关名字叫OS_CFG_ISR_POST_DEFERRED_EN,在st\eval\ucosiii的os_cfg.h中被定义),就执行 OS_IntQPost(...)向中断延迟处理任务OS_IntQTask()发送一个队列,然后就退出了,不做其他操作,接下来会退出中断。中断延迟处理任务OS_IntQTask() 优先级为0,在中断退出后肯定会被调度,由它再负责向时钟tick处理任务OS_TickTask发信号量,这是后话。方案二、如果没有打开中断延迟处理任务,就要在时钟节拍中断中自己向时钟tick处理任务OS_TickTask发信号量,并且及时更新时间片,和定时器任务,这会占用中断处理时间。
可以看出,方案一的优点就是明显减小了关中断时间,但同时增加了禁止任务调度的时间。这在ucosiii手册中有详细说明,具体是用方案一还是方案二,要看自己的实际需求。另,ucosiii默认为方案二。
void OSTimeTick (void)
{
    OS_ERR err;
    OSTimeTickHook(); /* Call user definable hook */ 
#if OS_CFG_ISR_POST_DEFERRED_EN > 0u   /*如果开启了延迟处理任务,ucosiii特有*/
   ts = OS_TS_GET(); /* Get timestamp */ 
    OS_IntQPost((OS_OBJ_TYPE) OS_OBJ_TYPE_TICK, /* Post to ISR queue */
                (void *)&OSRdyList[OSPrioCur],
                (void *) 0,
                (OS_MSG_SIZE) 0u,
                (OS_FLAGS ) 0u,
                (OS_OPT ) 0u,
                (CPU_TS ) ts,
                (OS_ERR *)&err);
#else         /*如果没有开启延迟处理任务,以下代码全部是在中断中做的,与ucosii相同*/
  (void)OSTaskSemPost((OS_TCB *)&OSTickTaskTCB, /* Signal tick task */ 
                       (OS_OPT ) OS_OPT_POST_NONE,
                       (OS_ERR *)&err);
#if OS_CFG_SCHED_ROUND_ROBIN_EN > 0u
    OS_SchedRoundRobin(&OSRdyList[OSPrioCur]);
#endif
#if OS_CFG_TMR_EN > 0u
    OSTmrUpdateCtr--;
    if (OSTmrUpdateCtr == (OS_CTR)0u) {
        OSTmrUpdateCtr = OSTmrUpdateCnt;
        OSTaskSemPost((OS_TCB *)&OSTmrTaskTCB, /* Signal timer task */
                      (OS_OPT ) OS_OPT_POST_NONE,
                      (OS_ERR *)&err);
    }
#endif
#endif
}
因为方案二是方案一的子集,这里从方案一开始看起:
 OS_IntQPost是从中断中直接向中断延迟处理函数OS_IntQTask (void *p_arg)推送队列。
OS_IntQPost函数被定义在ucosiii\source的os_int.c文件中,(注意这个推送是中断函数普适的,也就是说可以在任何中断中使用,可以传递任意内核对象到任意任务,包括信号量、标志组、队列等等,它里面还为tick处理专门开出一个特例,就是这里重点要讲的东西)它做两件事:一、将要推送的队列消息内容保存到os_int_q队列中,这是一个缓冲队列,可以存放多个推送消息,中断延迟任务将从这里依次取出队列消息进行投递;二、将OS_IntQTask置入就绪队列优先级0的位置上,确保中断退出后接下来就能调度优先级为0的OS_IntQTask处理任务。
源码不贴出来了。下面来看一下方案一从中断退出后执行的操作,退出中断后,因为OS_IntQTask(void *p_arg)处理任务优先级为0是最高,所以一定会执行OS_IntQTask(void *p_arg) (该函数也是在ucosiii\source的os_int.c文件中),注意这里有个比较有意思的事情:通常一个post函数都是对应一个pend函数,但是OS_IntQPost函数向OS_IntQTask中断延迟处理任务推送队列时,在OS_IntQTask中并没有相应的pend函数,这是因为在OS_IntQPost中就将OS_IntQTask置入就绪了,不需要查询任务挂起表(题外话,任务就绪表和任务挂起表,ucosiii中最重要的两个数据结构)。下面看一下OS_IntQTask:
void OS_IntQTask (void *p_arg)
{
    CPU_BOOLEAN done;
    CPU_TS ts_start;
    CPU_TS ts_end;
    CPU_SR_ALLOC();
    p_arg = p_arg; /* Not using 'p_arg', prevent compiler warning */ 
    while (DEF_ON) {
        done = DEF_FALSE;
        while (done == DEF_FALSE) {
            CPU_CRITICAL_ENTER();
            if (OSIntQNbrEntries == (OS_OBJ_QTY)0u) {
                OSRdyList[0].NbrEntries = (OS_OBJ_QTY)0u; /* Remove from ready list */
                OSRdyList[0].HeadPtr = (OS_TCB *)0;
                OSRdyList[0].TailPtr = (OS_TCB *)0;
                OS_PrioRemove(0u); /* Remove from the priority table */
                CPU_CRITICAL_EXIT();
                OSSched();
                done = DEF_TRUE; /* No more entries in the queue, we are done */
            } else {
                CPU_CRITICAL_EXIT();
                ts_start = OS_TS_GET();
                OS_IntQRePost();
                ts_end = OS_TS_GET() - ts_start; /* Measure execution time of tick task */
                if (OSIntQTaskTimeMax < ts_end) {
                    OSIntQTaskTimeMax = ts_end;
                }
                CPU_CRITICAL_ENTER();
                OSIntQOutPtr = OSIntQOutPtr->NextPtr; /* Point to next item in the ISR queue */
                OSIntQNbrEntries--;
                CPU_CRITICAL_EXIT();
            }
        }
    }
}
它的执行过程是:不断的从os_int_q队列中取出消息,执行OS_IntQRePost()函数,该函数也是在ucosiii\source的os_int.c文件中,这个函数os_int_q队列取出消息再按消息type字段和obj字段,将不同的内核对象转发到不同的任务中,直到os_int_q队列为空,把优先级0从就绪表上给摘下来,执行调度OSSched(),接下来就要看哪个就绪的任务优先级高,就调用哪个任务。
OS_IntQRePost()函数从os_int_q队列中取出的消息,本意就是将不同的内核对象转发到不同的任务中,但同时它为时钟tick专门开了一个特殊处理,就是这里重点要讲的,先看一下OS_IntQRePost()函数的源码片断:
switch (OSIntQOutPtr->Type) {
        ...... 
case OS_OBJ_TYPE_TICK:
#if OS_CFG_SCHED_ROUND_ROBIN_EN > 0u
             OS_SchedRoundRobin(&OSRdyList[OSPrioSaved]);
#endif
             (void)OS_TaskSemPost((OS_TCB *)&OSTickTaskTCB, /* Signal tick task */
                                  (OS_OPT ) OS_OPT_POST_NONE,
                                  (CPU_TS ) OSIntQOutPtr->TS,
                                  (OS_ERR *)&err);
#if OS_CFG_TMR_EN > 0u
             OSTmrUpdateCtr--;
             if (OSTmrUpdateCtr == (OS_CTR)0u) {
                 OSTmrUpdateCtr = OSTmrUpdateCnt;
                 ts = OS_TS_GET(); /* Get timestamp */
                 (void)OS_TaskSemPost((OS_TCB *)&OSTmrTaskTCB, /* Signal timer task */
                                      (OS_OPT ) OS_OPT_POST_NONE,
                                      (CPU_TS ) ts,
                                      (OS_ERR *)&err);
             }
#endif
             break;
}
可以看出来,这里的处理过程与方案二中在OS_CPU_SysTickHandler (void)中后半段的处理过程完全一样,向OSTickTaskTCB发送信号量,然后再向OSTmrTaskTCB发送信号量。注意按道理在多个post连用的时候,在第一个post中应当使用OS_OPT_POST_NO_SCHED选项,暂不执行调度,以确保当前任务不被打断,但这里没有这样使用,而是两个post都是使用OS_OPT_POST_NONE选项,这并无问题,因为OS_IntQRePost()函数是在OS_IntQTask (void *p_arg)中执行的,它的优先级是0,没有任务能打断它。至此,两种方案殊途同归了。
再向下分析,就没有难点了,已经向OSTmrTaskTCB发送了信号量,而OSTickTaskTCB对应的是OS_TickTask(void *p_arg)函数(该任务控制块在ucosiii\source文件夹中的os_tick.c中被OS_TickTaskInit (OS_ERR *p_err)函数创建,而该函数在ucosiii\source文件夹中的os_core.c中被调用,同时被调用的还有Idle等系统必备任务),因为是直接向OSTickTaskTCB 任务post了任务信号量,可以大胆推断:该任务将从pend状态转为就绪状态,等待时机被执行。以下是OS_TickTask(void *p_arg)函数的源码,正和猜测的一样(该函数在ucosiii\source文件夹中的os_tick.c中):
void OS_TickTask (void *p_arg)
{
    OS_ERR err;
    CPU_TS ts;
   p_arg = p_arg; /* Prevent compiler warning */ 
   while (DEF_ON) { 
        (void)OSTaskSemPend((OS_TICK )0,
                            (OS_OPT )OS_OPT_PEND_BLOCKING,
                            (CPU_TS *)&ts,
                            (OS_ERR *)&err); /* Wait for signal from tick interrupt */
        if (err == OS_ERR_NONE) {
            if (OSRunning == OS_STATE_OS_RUNNING) {
                OS_TickListUpdate(); /* Update all tasks waiting for time */
            }
        }
    }
}
可以看出,该任务回调函数主要就是调用了在同一文件(os_tick.c)中的OS_TickListUpdate()函数。这个函数负责查询时钟节拍列表中的各个“辐条”,将到期的任务放入任务就绪表中。该函数比较大,这里不列写源码,主要是算法过程,各种查找排序。
经过OS_TickListUpdate()的一顿折腾,该醒来的任务就都就绪了。然后OS_TickTask()遇到花括号,又走入while (DEF_ON)中,继续pend它自己的信号量。这一pend,就进入了ucosIII的调度点,tick处理过程至此结束,可以让位给任务就绪表中优先级最高的其他任务了,这很可能是新就绪的任务。
题外话,提到调度点,ucosiii共有14个调度点,很重要,要写出好的应用,这些调度点应当死记,它们分别是:
1.任务释放信号量给另一个任务,或者向另一个任务发消息(post函数);
2.任务调用延时函数OSTimeDly()或者OSTimeDlyHMSM();
3.任务等待事件发生,而事件还没有发生(pend函数);
4.任务取消等待(pendAbort函数);
5.创建任务;
6.删除任务;
7.删除一个内核对象(信号量、标志组、队列等);
8.任务改变自身的优先级或者其他任务的优先级;
9.任务将自身挂起(OSTaskSuspend函数);
10.解挂某任务(OSTaskResume函数);
11.退出所有的嵌套中断(OSIntExit);
12.调度器解锁(OSSchedUnlock函数);
13.任务放弃自己的时间片(RoundRobinYield);
14.用户自己调用OSSched()函数。
另外,题外话,从ucosiii3.04版本开始,ucos放弃了它一直得意的“辐条”机制,改为回归链条机制。例如一个延迟10tick,一个延迟3tikc,就是一条链上会有两个任务,各自的到期时间使用累加排序算法,为:3+7,这样只从第一个任务每次减1,到期就调走,转入减下一个任务的时间值,确实也很方便,但为啥从ucosii开始它就说是“辐条”机制好呢,自己打自己脸啊。定时器管理的辐条也是这样被无情抛弃了,与时钟节拍辐条改动不同的是,如果一个延迟为10,一个延迟为3,在各自的计数字段里就是10和3,每节拍减1,看谁到期就调用谁,连排序算法都没用。
回归正题,总结一下时钟节拍处理过程:
方案一:OS_CFG_ISR_POST_DEFERRED_EN ==1u
OS_CPU_SysTickHandler ->OSTimeTick-> OS_IntQPost ->OSIntExit退出中断,执行调度 -> OS_IntQTask ->OS_IntQRePost -> OS_TaskSemPost((OS_TCB *)&OSTmrTaskTCB) -> OS_TickTask ->使到期的任务就绪,执行调度。
方案二:OS_CFG_ISR_POST_DEFERRED_EN ==0u
OS_CPU_SysTickHandler -> OSTimeTick->OS_TaskSemPost((OS_TCB *)&OSTmrTaskTCB) -> OS_TickTask ->使到期的任务就绪,因为还在中断中,不会执行调度 ->OSIntExit退出中断,执行调度 。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值