FreeRTOS学习记录(六):空闲任务与阻塞延时的实现

2022-04-24

依据:[野火]《FreeRTOS内核实现与应用开发实战指南》


目录

一、实现空闲任务

1、实现空闲任务

定义空闲任务的任务控制块

创建空闲任务

二、阻塞延时

1、vTaskDelay()函数

2、修改 vTaskSwitchContext()函数

三、SysTick中断服务函数

1、xTaskIncrementTick()函数,更新系统时基。

四、SysTick初始化函数

五、实验


RTOS 中的延时叫阻塞延时,即任务需要延时的时候,任务会放弃 CPU 的使用权,CPU 可以去干其它的事情,当任务延时时间到,重新获取 CPU 使用权,任务继续运行,这样就充分地利用了 CPU 的资源,而不是干等着。

如果没有其它任务可以 运行,RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。

在 FreeRTOS 中,空闲任务是系统在【启动调度器】的时候创建的优先级最低的任务,空闲任 务主体主要是做一些系统内存的清理工作

一、实现空闲任务

为了简单起见,我们本章实现的空闲任务只是对一个全局变量进行计数。鉴于空闲任务的这种特性,在实际应用中,当系统进入空闲任务的时候,可在空闲任务中让单片机进入休眠或者低功耗等操作

1、实现空闲任务

目前我们在创建任务时使用的栈和 TCB 都使用的是静态的内存,即需要预先定义好内存,空闲任务也不例外。有关空闲任务的栈和 TCB 需要用到的内存空间均在 main.c 中定义。

//定义空闲任务的栈

#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )     //(2)

StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];     //(1)

(1)空闲任务的栈是一个定义好的数组,大小由 FreeRTOSConfig.h 中 定义的宏 configMINIMAL_STACK_SIZE 控制,默认为 128,单位为字,即 512个字节。

定义空闲任务的任务控制块

任务控制块是每一个任务必须的,空闲任务的的任务控制块我们在 main.c 中定义,是 一个全局变量

idle:懒惰的; 闲置的; 没有工作的; 闲散的

//定义空闲任务的任务控制块
TCB_t IdleTaskTCB;

创建空闲任务

当定义好空闲任务的栈,任务控制块后,就可以创建空闲任务。空闲任务在调度器启动函数 vTaskStartScheduler()中创建。

//创建空闲任务

extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
                                    StackType_t **ppxIdleTaskStackBuffer,
                                    uint32_t *pulIdleTaskStackSize );
void vTaskStartScheduler( void )
{
    /*=======================创建空闲任务 start=======================*/ 
    TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用于指向空闲任务控制块 */ 
    StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */ 
    uint32_t ulIdleTaskStackSize; 

    /* 获取空闲任务的内存:任务栈和任务 TCB */ //(1) 
    vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, 
                                   &pxIdleTaskStackBuffer, 
                                   &ulIdleTaskStackSize ); 

    /* 创建空闲任务 */ //(2) 
    xIdleTaskHandle = 
        xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */ 
                           (char *)"IDLE", /* 任务名称,字符串形式 */ 
                           (uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */ 
                           (void *) NULL, /* 任务形参 */ 
                           (StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */ 
                           (TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */ 

    /* 将任务添加到就绪列表 */ //(3) 
    vListInsertEnd( &( pxReadyTasksLists[0] ), 
                    &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) ); 
    /*==========================创建空闲任务 end=====================*/ 

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

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

(1)获取空闲任务的内存 , 即将 pxIdleTaskTCBBuffer pxIdleTaskStackBuffer 这两个接下来要作为形参传到 xTaskCreateStatic()函数的指针分别指 向空闲任务的 TCB 和栈的起始地址,这个操作由函数 vApplicationGetIdleTaskMemory()来 实现,该函数需要用户自定义,目前我们在 main.c 中实现

vApplicationGetIdleTaskMemory()函数

void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
                                    StackType_t **ppxIdleTaskStackBuffer,
                                    uint32_t *pulIdleTaskStackSize )
{
    *ppxIdleTaskTCBBuffer = &IdleTaskTCB;    //空闲任务控制块
    *ppxIdleTaskStackBuffer = IdleTaskStack;    //空闲任务堆栈
    *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;    //最小堆栈大小
}

(2)调用 xTaskCreateStatic()函数创建空闲任务。

(3)将空闲任务插入到就绪列表的开头。在以后会支持优先级, 空闲任务默认的优先级是最低的,即排在就绪列表的开头。

二、阻塞延时

1、vTaskDelay()函数

阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻 塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时 间,CPU 可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU 就将运行空闲 任务。阻塞延时函数task.c 中定义。

 // vTaskDelay()函数

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

    /* 获取当前任务的 TCB */
    pxTCB = pxCurrentTCB;     //(1)

    /* 设置延时时间 */
    pxTCB->xTicksToDelay = xTicksToDelay;     //(2)
 
    /* 任务切换 */
    taskYIELD();     //(3)
}

(1)获取当前任务的任务控制块。pxCurrentTCB 是一个在 task.c 定义的全局指针用于指向当前正在运行或者即将要运行的任务的任务控制块

(2)xTicksToDelay 是任务控制块的一个成员,用于记录任务需要延时的时间,单位为 SysTick 的中断周期。比如我们本书当中 SysTick 的中断周期为 10ms,调 用 vTaskDelay( 2 )则完成 2*10ms 的延时。

xTicksToDelay 定义

typedef struct tskTaskControlBlock
{
    volatile StackType_t *pxTopOfStack; /* 栈顶 */

    ListItem_t xStateListItem; /* 任务节点 */

    StackType_t *pxStack; /* 任务栈起始地址 */
                          /* 任务名称,字符串形式 */
    char pcTaskName[ configMAX_TASK_NAME_LEN ];
 
    //=======================================
    TickType_t xTicksToDelay; /* 用于延时 */         //延时延时延时延时延时延时延时**********
    //=======================================
} tskTCB;

(3)任务切换。调用 tashYIELD()会产生 PendSV中断,在 PendSV中断 服务函数中会调用上下文切换函数 vTaskSwitchContext(),该函数的作用是寻找最高优先级 的就绪任务,然后更新 pxCurrentTCB。上一章我们只有两个任务,则 pxCurrentTCB 不是 指向任务 1 就是指向任务 2,本章节开始我们多增加了一个空闲任务,则需要让 pxCurrentTCB 在这三个任务中切换,算法需要改变

2、修改 vTaskSwitchContext()函数

#if 0
    void vTaskSwitchContext( void )
    { /* 两个任务轮流切换 */
        if ( pxCurrentTCB == &Task1TCB )
        {
            pxCurrentTCB = &Task2TCB;
        }
        else
        {
            pxCurrentTCB = &Task1TCB;
        }
    }
#else

    void vTaskSwitchContext( void ) 
    { 
        /* 如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2, 
        看看他们的延时时间是否结束,如果任务的延时时间均没有到期, 
        那就返回继续执行空闲任务 */ 
        if ( pxCurrentTCB == &IdleTaskTCB )         //(1) 
        { 
            if (Task1TCB.xTicksToDelay == 0) 
            { 
                pxCurrentTCB =&Task1TCB; 
            } 
            else if (Task2TCB.xTicksToDelay == 0) 
            { 
                pxCurrentTCB =&Task2TCB; 
            } 
            else 
            { 
                return; /* 任务延时均没有到期则返回,继续执行空闲任务 */ 
            } 
        } 
        else /* 当前任务不是空闲任务则会执行到这里 */         //(2) 
        { 
            /*如果当前任务是任务 1 或者任务 2 的话,检查下另外一个任务, 
            如果另外的任务不在延时中,就切换到该任务 
            否则,判断下当前任务是否应该进入延时状态, 
            如果是的话,就切换到空闲任务。否则就不进行任何切换 */ 
            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; /* 返回,不进行切换,因为两个任务都处于延时中     */ 
                } 
            } 
        } 
    } 

#endif

(1)如果当前任务是空闲任务,那么就去尝试执行任务 1 或者任务 2, 看看他们的延时时间是否结束,如果任务的延时时间均没有到期,那就返回继续执行空闲任务。

(2)如果当前任务是任务 1 或者任务 2 的话,检查下另外一个任务,如果另外的任务不在延时中,就切换到该任务。否则,判断下当前任务是否应该进入延时状 态,如果是的话,就切换到空闲任务,否则就不进行任何切换 。

三、SysTick中断服务函数

任务上下文切换函数 vTaskSwitchContext ()中,会判断每个任务的任务控制块中的 延时成员 xTicksToDelay 的值是否为 0,如果为 0就要将对应的任务就绪,如果不为 0 就继续延时。如果一个任务要延时,一开始 xTicksToDelay 肯定不为 0,当 xTicksToDelay 变为 0 的时候表示延时结束,那么 xTicksToDelay 是以什么周期在递减?在哪里递减?在 FreeRTOS 中,这个周期由 SysTick 中断提供,操作系统里面的最小的时间单位就是 SysTick 的中断周期,我们称之为一个 tick,SysTick 中断服务函数在 port.c 中实现

//SysTick 中断服务函数

void xPortSysTickHandler( void )
{
    /* 关中断 */
    vPortRaiseBASEPRI();     //(1)

    /* 更新系统时基 */
    xTaskIncrementTick();     //(2)

    /* 开中断 */
    vPortClearBASEPRIFromISR();     //(3)
}

 (2)更新系统时基,该函数在 task.c 中定义

1、xTaskIncrementTick()函数,更新系统时基。

void xTaskIncrementTick( void )
{
    TCB_t *pxTCB = NULL;
    BaseType_t i = 0;

    /* 更新系统时基计数器 xTickCount,xTickCount 是一个在 port.c 中定义的全局变量 */    //(1)
    const TickType_t xConstTickCount = xTickCount + 1;
    xTickCount = xConstTickCount;

 
    /* 扫描就绪列表中所有任务的 xTicksToDelay,如果不为 0,则减 1 */    //(2)
    for (i=0; i<configMAX_PRIORITIES; i++)
    {
        pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
        if (pxTCB->xTicksToDelay > 0)
        {
            pxTCB->xTicksToDelay --;
        }
    }

    /* 任务切换 */    //(3)
    portYIELD();
}

(1)更新系统时基计数器 xTickCount,加一操作。xTickCount 是一个在 port.c 中定义的全局变量,在函数任务启动调度器vTaskStartScheduler()中调用启动调度器 xPortStartScheduler()函数前初始化。

(2)扫描就绪列表中所有任务的 xTicksToDelay,如果不为 0,则减 1。

(3)执行一次任务切换。

四、SysTick初始化函数

SysTick 的中断服务函数要想被顺利执行,则 SysTick 必须先初始化。SysTick 初始化 函数在 port.c 中定义

// vPortSetupTimerInterrupt()函数

/* SysTick 控制寄存器 */     //(1)
#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 )     //(2)
{
    /* 设置重装载寄存器的值 */     //(3)
    portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;

    /* 设置系统定时器的时钟等于内核时钟     //(4)
       使能 SysTick 定时器中断
       使能 SysTick 定时器 */
    portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
                                  portNVIC_SYSTICK_INT_BIT |
                                  portNVIC_SYSTICK_ENABLE_BIT );
}

(1)配置 SysTick 需要用到的寄存器宏定义,在 port.c 中实现。

(2)SysTick 初始化函数 vPortSetupTimerInterrupt() , 在 xPortStartScheduler()中被调用

//xPortStartScheduler() 中调用  vPortSetupTimerInterrupt()


BaseType_t xPortStartScheduler( void )
{
    /* 配置 PendSV 和 SysTick 的中断优先级为最低 */
    portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; 

    /* 初始化 SysTick */ 
    vPortSetupTimerInterrupt(); 

    /* 启动第一个任务,不再返回 */
    prvStartFirstTask();

    /* 不应该运行到这里 */
    return 0;
}

(3)设置重装载寄存器的值,决定 SysTick 的中断周期。从代码清单 (1)可以知道:如果没有定义 configSYSTICK_CLOCK_HZ 那么 configSYSTICK_CLOCK_HZ 就等于configCPU_CLOCK_HZconfigSYSTICK_CLOCK_HZ 确实没有定义 ,则 configSYSTICK_CLOCK_HZ 由在 FreeRTOSConfig.h 中定义的 configCPU_CLOCK_HZ 决定,同时 configTICK_RATE_HZ 也 在 FreeRTOSConfig.h 中定义

//configCPU_CLOCK_HZ 与 configTICK_RATE_HZ 宏定义

1 #define configCPU_CLOCK_HZ (( unsigned long ) 25000000)     //(1)
2 #define configTICK_RATE_HZ (( TickType_t ) 100)     //(2)

        (3)、(1)系统时钟的大小, 因为目前是软件仿真,需要配置成 与 system_ARMCM3.c(system_ARMCM4.c 或 system_ARMCM7.c)文件中的 SYSTEM_CLOCK 的一样,即等于 25M。如果有具体的硬件,则配置成与硬件的系统时钟一样。

        (3)、(2)SysTick 每秒中断多少次,目前配置为 100,即每 10ms 中断一次。

(4)设置系统定时器的时钟等于内核时钟,使能 SysTick 定时器中断,使能 SysTick 定时器。

五、实验

/*
 *************************************************************************
 * 包含的头文件
 *************************************************************************
 */
#include "FreeRTOS.h"
#include "task.h"
 
/*
 *************************************************************************
 * 全局变量
 *************************************************************************
 */
portCHAR flag1;
portCHAR flag2;

extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
 
 
/*
 *************************************************************************
 * 任务控制块 & STACK
 *************************************************************************
 */
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
 
TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;
 
 
/*
 *************************************************************************
 * 函数声明
 *************************************************************************
 */
void delay (uint32_t count);
void Task1_Entry( void *p_arg );
void Task2_Entry( void *p_arg );
 
/*
 ************************************************************************
 * main 函数
 ************************************************************************
 */
 
int main(void)
{
    /* 硬件初始化 */
    /* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */

    /* 初始化与任务相关的列表,如就绪列表 */
    prvInitialiseTaskLists();

    /* 创建任务 */
    Task1_Handle =
        xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
                           (char *)"Task1", /* 任务名称,字符串形式 */
                           (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
                           (void *) NULL, /* 任务形参 */
                           (StackType_t *)Task1Stack, /* 任务栈起始地址 */
                           (TCB_t *)&Task1TCB ); /* 任务控制块 */
    /* 将任务添加到就绪列表 */
    vListInsertEnd( &( pxReadyTasksLists[1] ),
                    &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );

    Task2_Handle =
        xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
                           (char *)"Task2", /* 任务名称,字符串形式 */
                           (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
                           (void *) NULL, /* 任务形参 */
                           (StackType_t *)Task2Stack, /* 任务栈起始地址 */
                           (TCB_t *)&Task2TCB ); /* 任务控制块 */
    /* 将任务添加到就绪列表 */
    vListInsertEnd( &( pxReadyTasksLists[2] ),
                    &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );

    /* 启动调度器,开始多任务调度,启动成功则不返回 */
    vTaskStartScheduler();

    for (;;)
    {
        /* 系统启动成功不会到达这里 */
    }
}
 
/*
 *************************************************************************
 * 函数实现
 *************************************************************************
 */
/* 软件延时 */
void delay (uint32_t count)
{
    for (; count!=0; count--);
}
/* 任务 1 */
void Task1_Entry( void *p_arg )
{
    for ( ;; )
    {
#if 0
        flag1 = 1;
        delay( 100 );
        flag1 = 0;
        delay( 100 );

        /* 任务切换,这里是手动切换 */
        portYIELD();
#else
        flag1 = 1; 
        vTaskDelay( 2 );         //(1) 
        flag1 = 0; 
        vTaskDelay( 2 ); 
#endif
    }
}

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

        /* 任务切换,这里是手动切换 */
        portYIELD();
#else
        flag2 = 1; 
        vTaskDelay( 2 );     //(2) 
        flag2 = 0; 
        vTaskDelay( 2 ); 
#endif
    }
}
 
/* 获取空闲任务的内存 */ 
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];     //(3) 
TCB_t IdleTaskTCB; 
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer, 
                                    StackType_t **ppxIdleTaskStackBuffer, 
                                    uint32_t *pulIdleTaskStackSize ) 
{ 
    *ppxIdleTaskTCBBuffer=&IdleTaskTCB; 
    *ppxIdleTaskStackBuffer=IdleTaskStack; 
    *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE; 
}

(1)、(2):延时函数均由原来的软件延时替代为阻塞延时,延时时间均 为 2 个 SysTick 中断周期,即 20ms。

(3)定义空闲任务的栈和 TCB

每天加油、好好学习、多看多学多想!!!!加油!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值