04_空闲任务与阻塞延时

前言

在上一节中,任务中延时函数使用的软件延时,即让CPU等待来达到延时的效果,但这样与裸机编程是一样,还没有达到RTOS的效果,使用RTOS的目的就是充分发挥RTOS的性能,永远不让它闲着。如果任务需要延时,不能以CPU空等的方式来实现。RTOS中的延时叫做阻塞延时,即任务需要延时时,会放弃CPU的使用权,CPU可以去做其他事情,当任务延时时间时,重新获取CPU使用权,继续执行任务。FreeRTOS中至少有一个任务叫做空闲任务,如果没有可以执行的任务,CPU就执行空闲任务,空闲任务是优先级最低的任务。

一、实现空闲任务

空闲任务的创建和普通任务的创建是一致的,只是空闲任务优先级最低。

1.定于空闲任务的栈

/* Idle Task */
#define confgiMINIMAL_STACK_SIZE		128
StackType_t	IdleTasKStack[confgiMINIMAL_STACK_SIZE];

2.定义任务控制块

TCB_t	IdleTaskTCB;

3.创建空闲任务

由于空闲任务是必须有的,所以空闲任务的创建放在vTaskStartScheduler()函数中。在vTaskStartScheduler()继续添加代码:

void vTaskStartScheduler(void)
{
    /* 创建空闲任务 */
    TCB_t *pxIdleTaskTCBBuffer = NULL;  /* 用于指向空闲任务的控制块 */
    StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务的栈起始地址 */
    uint32_t ulIdleTaskStackSize;
    /* 获取空闲任务的内存:任务栈和任务TCB */
    vApplicationGetIdleTaskMemory(&pxIdleTaskTCBBuffer,&pxIdleTaskStackBuffer,&ulIdleTaskStackSize);
    /* 创建空闲任务 */
    TaskHandle_t xIdleTaskHandle = 
    xTaskCreateStatic(  (TaskFunction_t)prvIdleTask,    /* 任务入口 */
                        (char*)"IDLE",                  /* 任务名称,字符串形式 */
                        (uint32_t)ulIdleTaskStackSize,  /* 任务栈大小,单位为字 */
                        (void*)NULL,                    /* 任务形参 */
                        (StackType_t*)pxIdleTaskStackBuffer,    /* 任务栈起始地址 */
                        (TCB_t*)pxIdleTaskTCBBuffer);   /* 任务控制块 */

    /* 将任务添加到就序列表 */
    vListInsertEnd(&(pxReadyTasksLists[0]),&(((TCB_t*)pxIdleTaskStackBuffer)->xStateListItem));

    /* 手动指定第一个任务 */
    pxCurrentTCB = &Task1TCB;

    /* 启动调度器 */
    if(xPortStartScheduler() != pdFALSE)
    {
        /* 调度器启动成功则不会来到这里 */
    }
}

空闲任务prvIdleTask()这里是是一个空的死循环函数。vApplicationGetIdleTaskMemory()函数的实现如下:

void vApplicationGetIdleTaskMemory(TCB_t **ppxIdleTaskTCBBuffer,
									StackType_t **ppxIdleTaskStackBuffer,
									uint32_t *pulIdleTaskStackSize)
{
	*ppxIdleTaskTCBBuffer = &IdleTaskTCB;
	*ppxIdleTaskStackBuffer = IdleTasKStack;
	*pulIdleTaskStackSize = confgiMINIMAL_STACK_SIZE;
}

二、实现阻塞延时

1.vTaskDelay()函数

阻塞延时的阻塞指的是任务调用该延时函数之后,任务会被剥夺CPU的使用权,然后进入阻塞状态,直到延时结束,任务重新获取CPU使用权才可以继续运行。

void vTaskDelay(const TickType_t xTicksToDelay)
{
    TCB_t *pxTCB = NULL;

    /* 获取当前任务的TCB */
    pxTCB = pxCurrentTCB;
    /* 设置延时时间 */
    pxTCB->xTicksToDelay = xTicksToDelay;
    /* 任务切换 */
    taskYIELD();
}

这个函数有两个功能,一是设置当前函数的延时时间,而是进行任务切换。在TCB结构体中新增成员xTicksToDelay用来延时,如下:

typedef struct tskTaskControlBlock
{
    volatile StackType_t *pxTopOfStack;          /* 栈顶 */
    ListItem_t  xStateListItem;                 /* 任务节点 */
    StackType_t *pxStack;                       /* 任务栈起始地址 */
    char pcTaskName[configMAX_TASK_NAME_LEN];   /* 任务名称,为字符串形式 */
    TickType_t  xTicksToDelay;                  /* 延时时间 */
}tskTCB;

2.修改vTaskSwitchContext()函数

在之前该函数的作用仅是切换任务1和任务2,不考虑阻塞和优先级情况,由于目前已经加入了一个空闲任务以及阻塞延时,因此任务切换时应该考虑任务1和任务2的延时情况,比如如果任务1在处于延时状态,那么就执行任务2,如果任务2也处于延时状态,那么就执行空闲任务。

void vTaskSwitchContext(void)
{
    /* 如果当前任务是空闲任务,那么就尝试去执行任务1或者任务2,看它们的延时是否已经结束,
    如果任务的延时还没有到期,则返回,继续执行空闲任务 */
    if(pxCurrentTCB == &IdleTaskTCB)
    {
        if(Task1TCB.xTicksToDelay == 0)
        {
            pxCurrentTCB = &Task1TCB;
        }
        else if(Task2TCB.xTicksToDelay == 0)
        {
            pxCurrentTCB = &Task2TCB;
        }
        else
        {
            return;
        }
    }
    else        /* 当前任务不是空闲任务 */
    {
        if(pxCurrentTCB == &Task1TCB)
        {
            if(Task2TCB.xTicksToDelay == 0)
            {
                pxCurrentTCB = &Task2TCB;
            }
            else if(pxCurrentTCB->xTicksToDelay != 0)
            {
                pxCurrentTCB = &IdleTaskTCB;
            }
            else
            {
                return;
            }
        }
        else if(pxCurrentTCB == &Task2TCB)
        {
            if(Task1TCB.xTicksToDelay == 0)
            {
                pxCurrentTCB = &Task1TCB;
            }
            else if(pxCurrentTCB->xTicksToDelay != 0)
            {
                pxCurrentTCB = &IdleTaskTCB;
            }
            else
            {
                return;
            }
        }
    }
}

代码实现无非就是寻找一个可以执行的任务来执行,很容易理解。

3.SysTick中断服务函数

在任务上下文切换函数中,会判断每个任务的任务控制块中的延时成员xTicksToDelay的值是否为0,如果为0就要将对应的任务就绪,如果不为0就继续延时。延时肯定是要有时基的,RTOS一般用SysTick来提供时基,当然也可以用普通定时器来提供时基。具体是在SysTick的中断服务函数中对延时值xTicksToDelay递减,每中断一次,xTicksToDelay就递减一次,因此任务的延时时间的最小间隔是由定时器的中断频率来决定,一般1ms中断一次。SysTick中断服务函数的内容如下:

/* 在port.c中实现 */
void xPortSysTickHandler(void)
{
    /* 关中断 */
    vPortRaiseBASEPRI();
    /* 更新系统时基 */
    xTaskIncrementTick();
    /* 开中断 */
    vPortClearBASEPRIFromISR();
}

这个函数的作用就是更新延时时间,更新系统时间的xTaskIncrementTick()函数的代码如下:

/* 在task.c中实现 */
void xTaskIncrementTick(void)
{
    TCB_t *pxTCB = NULL;
    BaseType_t i=0;
    /* 更新系统时基计数器xTickCount,xTickCount是一个早port.c中定义的全局变量 */
    const TickType_t xConstTickCount = xTickCount + 1;
    xTickCount = xConstTickCount;
    /* 扫描就序列表中所有任务的xTicksDelay,如果不为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();
}

如果任务的xTicksToDelay值不为0,就进行减1,然后产生PendSV中断进行任务切换。

4.SysTick初始化

/* SysTick Initialization */
/* SysTick控制寄存器 */
#define portNVIC_SYSTICK_CTRL_REG       (*((volatile uint32_t*)0xe000e010))
/* SysTick重装载寄存器 */
#define portNVIC_SYSTICK_LOAD_REG       (*((volatile uint32_t*)0xe000e014))
/* SysTick时钟源选择 */
#ifndef configSYSTICK_CLOCK_HZ
    #define configSYSTICK_CLOCK_HZ  configCPU_CLOCK_HZ
    /* 确保SysTick时钟与内核时钟一致 */
    #define portNVIC_SYSTICK_CLK_BIT    (1UL << 2UL)
#else
#define portNVIC_SYSTICK_CLK_BIT        (0)
#endif

#define portNVIC_SYSTICK_INT_BIT        (1UL << 1UL)
#define portNVIC_SYSTICK_ENABLE_BIT     (1UL << 0UL)

void vPortSetupTimerInterrupt(void)
{
    /* 设置重装载寄存器的值 */
    portNVIC_SYSTICK_LOAD_REG = (configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ) - 1UL;
    /* 设置系统定时器的时钟等于内核时钟
        使能SysTick定时器中断
        使能SysTick定时器 */
    portNVIC_SYSTICK_CTRL_REG = (portNVIC_SYSTICK_CLK_BIT |
                                portNVIC_SYSTICK_INT_BIT |
                                portNVIC_SYSTICK_ENABLE_BIT);
}

其中的configSYSTICK_CLOCK_HZ设置时钟频率,这里用软件仿真,设置为25MHz;configTICK_RATE_HZ是定时器的中断频率,这里设置为 100Hz,即10ms中断一次。代码定义如下:

#define configCPU_CLOCK_HZ                  ((unsigned long)25000000)
#define configTICK_RATE_HZ                  ((TickType_t)100)

三、实验验证

将之前的两个任务中的延时函数替换为阻塞延时,代码如下:

/* 任务1 */
void Task1_Entry(void *p_arg)
{
	for(;;)
	{
#if 0
		flag1 = 1;
		delay(100);
		flag1 = 0;
		delay(100);
#else
		flag1 = 1;
		vTaskDelay(2);
		flag1 = 0;
		vTaskDelay(2);
#endif
		
		taskYIELD();
	}
}

/* 任务2 */
void Task2_Entry(void *p_arg)
{
	for(;;)
	{
#if 0
		flag2 = 1;
		delay(100);
		flag2 = 0;
		delay(100);
#else
		flag2 = 1;
		vTaskDelay(2);
		flag2 = 0;
		vTaskDelay(2);
#endif
		taskYIELD();
	}
}

运行结果:

QQ截图20220811193443

可以看出,flag1和flag2几乎是同步变化的,已经看到了RTOS的影子了,这就是RTOS想到达到的效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

忆昔z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值