Toriyung

导航

阻塞延时

阻塞延时是当任务进入延时后,该任务的CPU使用权被剥夺进入阻塞状态(阻塞状态可以理解为保持状态不变,ps:惯性)。此时CPU可以进行其他任务的调度等,这样一来大大提升了CPU的使用效率。

而当所有任务都进入阻塞状态时,此时CPU就调度空闲任务执行。

 

阻塞延时

  阻塞延时和普通CPU延时不同,普通CPU延时是CPU进行设置好的时长等待,此时下面要进行的所有任务执行都将一并延时,直到延时结束,CPU再继续执行,这样子的一个系统称不上一个实时操作系统,因为他无法很好实现“实时”效果;而阻塞延时的核心,就是当上一个任务进入延时后,CPU就“放开”对其的控制,让其自己保持状态不变,CPU进而对下面的指令进行执行,在rtos中,这种方式使得延时对于任务的切换几乎没有影响,进而实现“实时”效果。

  我们定义阻塞延时TaskDelay,它将完成两件事情:1. 设置延时时长; 2. 切换任务

  1. 设置延时时长

    首先要明白,阻塞延时的时长如何实现? 是通过SysTick中断进行计数值的定时递减!

    首先先确定系统的时钟源频率是多少,知道了频率我们便知道系统SYS一秒数多少下,那么我们设置给他数一定的次数中断一次,就可以控制SYS每多长时间中断一次,然后,我们设置一个值-任务的延时计数值,当SYS中断一次,该值递减一次,直到值为0,则进行任务阻塞状态结束,这样一来,就可以实现控制任务阻塞时间。

    SysTick中断服务函数为SysTick_Handler,我们宏定义为xPortSysTickHandler,当产生SysTick中断时进入这个函数。按照上面的理论,我们知道,首先要确定SYS一秒数多少下,要数多少下中断一次,数完是否真的要产生中断(所以你确实可以选择数完就数完,啥事没有)等,对SysTick进行初始化,而初始化其实就是通过两个寄存器进行这几个问题进行确定,如图


图1 两个寄存器的位段说明

 

 根据图片,可以写代码如下,初始化函数为vPortSetupTimerInterrupt

 

#define portNVIC_SYSTICK_CTRL_REG    (*((volatile uint32_t *)0xe000e010))            //SYS控制及状态寄存器

#define portNVIC_SYSTICK_LOAD_REG    (*((volatile uint32_t *)0xe000e014))            //重装载值,数完后中断一次

#ifndef configSYSTICK_CLOCK_HZ                                                                //SYSTICK时钟频率
    #define configSYSTICK_CLOCK_HZ                configCPU_CLOCK_HZ        //假如定为系统时钟↓
    #define portNVIC_SYSTICK_CLK_BIT        (1UL << 2UL)                        //控制寄存器第2位段-选择时钟源:1为内核时钟 
#else
    #define portNVIC_SYSTICK_CLK_BIT        (0)                                            //控制寄存器第2位段-选择时钟源:0为外部时钟 
#endif

#define portNVIC_SYSTICK_INT_BIT            (1UL << 1UL)                        //控制寄存器第1位段-SYS倒数到0时是否产生异常
#define portNVIC_SYSTICK_ENABLE_BIT        (1UL << 0UL)                        //控制寄存器第0位段-SYS使能

void vPortSetupTimerInterrupt(void)
{
    /* 初始化SYSTICK,即初始化两个寄存器 */
    portNVIC_SYSTICK_LOAD_REG = (configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ) - 1UL;            
    
    portNVIC_SYSTICK_CTRL_REG = (portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT);
}

     

    确定完SysTick的一些参数后,我们就可以定义一下,产生中断后,我们要做些什么,同样由上面理论知,我们需要设置,中断发生时,任务延时计数值减1,并判断这个值是否为0,如果为0,表示任务阻塞状态结束,则切换到这个任务。所以代码如下

void xPortSysTickHandler(void)
{
    /* SysTick中断服务函数 */
    vPortRaiseBASEPRI();                //进临界区
    
    xTaskIncrementTick();                //进入计数器切换
    
    vPortClearBASEPRIFromISR();    //出临界区
}


void xTaskIncrementTick(void)
{
    /* 进入计数器递减,并在每一次递减后判断是否有超时任务,进行切换 */
    TCB_t *pxTCB = NULL;
    BaseType_t i = 0;
    
        /*注释码段暂时用不到*/
    //extern TickType_t xTickCount;
        
    //const TickType_t xConstTickCount = xTickCount + 1;
    //xTickCount = xConstTickCount;
    
    
    /*    扫描就绪列表中所有优先级链表的第一个任务的xTicksToDelay,如不为0,则减1    */
    for(i=0;i<configMAX_PRIORITIES;i++)
    {
        pxTCB = (TCB_t *)listGET_OWNER_OF_HEAD_ENTRY((&pxReadyTasksLists[i]));
        if(pxTCB->xTicksToDelay > 0)
        {
            pxTCB->xTicksToDelay--;
        }
    }    
    portYIELD();    //任务切换
}

    

    通过上面我们知道了给定某个任务他自己的一个任务延时计数值,就可以让它进行我们想要的延时,那么剩下的就是这个任务延时计数值应该怎么赋予任务?什么时候赋予?

    第一个问题简单,和任务相关的,一律被我们集合在任务的TCB中,于是在原有的TCB结构中,加入任务延时计数值xTicksToDelay

typedef struct tasTaskControlerBlock
{
    volatile StackType_t *pxTopOfStack;                        //栈顶
    ListItem_t xStateListItem;                                 //任务节点(链表项)
    StackType_t *pxStack;                                      //任务栈起始地址
    char pcTaskName[configMAX_TASK_NAME_LEN];                //任务名称
    TickType_t xTicksToDelay;                                  //任务延时计数值
}tskTCB;    
typedef tskTCB    TCB_t;

    第二个问题,文章的一开头就已经说明了,阻塞延时函数TaskDelay的第一件事:设置延时

void vTaskDelay(const TickType_t xTicksToDelay)
{
    TCB_t *pxTCB = NULL;
    
    //设置当前任务的延时计数
    pxTCB = pxCurrentTCB;
    pxTCB->xTicksToDelay = xTicksToDelay;
    
    //任务切换    
    taskYIELD();        
} 

 

  2. 切换任务

    由上面的vTaskDelay函数的代码可知,第二件事就是切换任务,通过前面文章的调度器的实现(https://www.cnblogs.com/toriyung/p/16837371.html)我们知道,任务切换是通过触发PendSV中断,进入PendSVHandler中断服务函数,里面有着任务切换函数vTaskSwitchContext,但之前的文章并没有涉及延时,所以需要加入对任务当下延时计数值(xTicksToDelay)的判断

void vTaskSwitchContext(void)
{
    /* 上下文切换(任务切换)*/
    
    if(pxCurrentTCB == &IdleTaskTCB)            //当前任务为空闲任务        
    {
        if(Task1TCB.xTicksToDelay == 0)                                //Task1延迟结束,切换为Task1
        {            
            pxCurrentTCB =&Task1TCB;            
        }            
        else if(Task2TCB.xTicksToDelay == 0)                    //Task2延迟结束,切换为Task2
        {            
            pxCurrentTCB =&Task2TCB;            
        }            
        else                                                                                    //Task1和Task2都在延迟,不进行切换
        {            
            return;
        }
    }
    else
    {
        if(pxCurrentTCB == &Task1TCB)            //当前任务为Task1
        {
            if(Task2TCB.xTicksToDelay == 0)                            //Task2延迟结束,切换为Task2
            {    
                pxCurrentTCB =&Task2TCB;    
            }    
            else if(pxCurrentTCB->xTicksToDelay != 0)        //Task1和Task2都在延迟,切换为空闲任务
            {    
                pxCurrentTCB = &IdleTaskTCB;    
            }    
            else    
            {    
                return;                                                                        //Task1没有延迟,保持Task1
            }    
        }
        if(pxCurrentTCB == &Task2TCB)            //当前任务为Task2
        {
            if(Task1TCB.xTicksToDelay == 0)                            //Task1延迟结束,切换为Task1
            {    
                pxCurrentTCB =&Task1TCB;    
            }    
            else if(pxCurrentTCB->xTicksToDelay != 0)        //Task1和Task2都在延迟,切换为空闲任务
            {    
                pxCurrentTCB = &IdleTaskTCB;    
            }    
            else    
            {    
                return;                                                                        //Task2没有延迟,保持Task2
            }
        }
    }
}

 

  至此,阻塞延时的实现搞定!

 

结构框图

 

 图2 结构框图

 

仿真效果

  

图3 普通延迟

 

 

图4 阻塞延迟

 

 

 

  可以看到,普通延迟的情况下,当任务1拉高电平进入延迟和拉低电平进入延迟期间,CPU根本没有空出手来去调度任务2,所以任务2一直保持低电平不变,直到任务1第二次延迟结束,CPU才进行任务2的调度;而阻塞延迟的情况下,当任务1拉高电平后立马进入阻塞状态,CPU停止对任务1控制,立马调度任务2,所以任务2以十分快的速度追上任务1的步伐拉高电平,肉眼看起来几乎是同步的。

 

posted on 2022-11-18 23:55  Toriyung  阅读(298)  评论(0编辑  收藏  举报