FreeRTOS菜鸟入门(五)·空闲任务与阻塞延时的实现

目录

[1. 实现空闲任务](#1. 实现空闲任务)

[1.1 定义空闲任务的栈](#1.1 定义空闲任务的栈)

[1.2 定义空闲任务的任务控制块](#1.2 定义空闲任务的任务控制块)

[1.3 创建空闲任务](#1.3 创建空闲任务)

[2. 实现阻塞延时](#2. 实现阻塞延时)

[2.1 vTaskDelay()函数](#2.1 vTaskDelay()函数)

[2.2 修改 vTaskSwitchContext()函数](#2.2 修改 vTaskSwitchContext()函数)

[3. SysTick 中断服务函数](#3. SysTick 中断服务函数)

[4. SysTick 初始化函数](#4. SysTick 初始化函数)


通过之前我们了解知道,任务体内的延时使用的是软件延时,也就是还是让CPU空等待来达到延时的效果,也就是delay函数,或者使用一个大的循环,只计数不进行任务处理,但是我们在使用 RTOS 的很大优势就是榨干 CPU 的性能,永远不然它闲着,任务如果需要延时也不能让 CPU 空等待来实现延时的效果。

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

当任务需要延时,进入阻塞状态,那 CPU 又去干什么事情了?

如果没有其它任务可以运行,RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。在 FreeRTOS 中,空闲任务是系统在启动调度器的时候创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作。

在实际应用中,当系统进入空闲任务的时候,可在空闲任务中让单片机进入休眠或者低功耗等操作。

1. 实现空闲任务

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

1.1 定义空闲任务的栈

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

cpp 复制代码
/* 定义空闲任务的栈 */
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) 
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE]; 

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

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

cpp 复制代码
/* 定义空闲任务的任务控制块 */
TCB_t IdleTaskTCB;

1.3 创建空闲任务

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

cpp 复制代码
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() 来实现,该函数需要用户自定义。

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

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

2. 实现阻塞延时

2.1 vTaskDelay()函数

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

cpp 复制代码
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 定义:

cpp 复制代码
typedef struct tskTaskControlBlock
{
    volatile StackType_t *pxTopOfStack; /* 栈顶 */

    ListItem_t xStateListItem; /* 任务节点 */

    StackType_t *pxStack; /* 任务栈起始地址 */
    /* 任务名称,字符串形式 */
    char pcTaskName[ configMAX_TASK_NAME_LEN ];

    TickType_t xTicksToDelay; /* 用于延时 */ 
} tskTCB;

2.2 修改 vTaskSwitchContext()函数

taskYIELD();:任务切换。调用 tashYIELD()会产生 PendSV中断,在 PendSV中断服务函数中会调用上下文切换函数 vTaskSwitchContext(),该函数的作用是寻找最高优先级的就绪任务,然后更新 pxCurrentTCB。

这里我们创建两个任务以及一个空闲任务进行在这三个任务之间进行切换。

cpp 复制代码
#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

3. SysTick 中断服务函数

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

SysTick 中断服务函数:

cpp 复制代码
void xPortSysTickHandler( void )
{
    /* 关中断 */
    vPortRaiseBASEPRI(); 

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

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

xTaskIncrementTick()函数:

cpp 复制代码
void xTaskIncrementTick( void )
{
    TCB_t *pxTCB = NULL;
    BaseType_t i = 0;

    /* 更新系统时基计数器 xTickCount,xTickCount 是一个在 port.c 中定义的全局变量 */
    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();
}

4. SysTick 初始化函数

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

vPortSetupTimerInterrupt()函数:

cpp 复制代码
/* SysTick 控制寄存器 */
#define portNVIC_SYSTICK_CTRL_REG			( * ( ( volatile uint32_t * ) 0xe000e010 ) )

/* SysTick 重装载寄存器寄存器 */
#define portNVIC_SYSTICK_LOAD_REG			( * ( ( volatile uint32_t * ) 0xe000e014 ) )

#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 ); 
}

xPortStartScheduler()函数中调用 vPortSetupTimerInterrupt():

cpp 复制代码
BaseType_t xPortStartScheduler( void )
{
    /* 配置 PendSV 和 SysTick 的中断优先级为最低 */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    
    /* 初始化 SysTick */
    vPortSetupTimerInterrupt();

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

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

FreeRTOS实时操作系统_时光の尘的博客-CSDN博客

相关推荐
芯岭技术19 分钟前
普冉MS32C001单片机,国产32位单片机,芯片特性和功能介绍
单片机·嵌入式硬件
wtsafe30 分钟前
仓储车间安全革命:AI叉车防撞装置系统如何化解操作风险
人工智能·物联网·安全
吃货界的硬件攻城狮2 小时前
【STM32 学习笔记】ADC数模转换器
笔记·stm32·单片机·学习
阿杜杜不是阿木木2 小时前
使用ESPHome烧录固件到ESP32-C3并接入HomeAssistant
物联网·esp32·iot·homeassistant·esp32-c3·esphome
喜欢吃燃面3 小时前
C++:扫雷游戏
c语言·c++·学习
小郝 小郝4 小时前
【C语言】五一回归,复习动脑
c语言·开发语言
天夏已微凉5 小时前
1.2 Linux音频系统发展历程与关键技术演进
linux·c语言·驱动开发·音视频
小昭dedug6 小时前
功能安全的关键——MCU锁步核技术全解析(含真实应用方案)
单片机·嵌入式硬件
负里556 小时前
STM32-模电
嵌入式硬件
BW.SU7 小时前
单片机 + 图像处理芯片 + TFT彩屏 指示灯控件
单片机·嵌入式硬件·人机交互·控件·触摸屏设计·指示灯·液晶屏