【FreeRTOS源码学习笔记】空闲任务与阻塞延时

【FreeRTOS源码学习笔记】空闲任务与阻塞延时

  • 前言
  • 一、实现空闲任务
    • [1.1 定义空闲任务的栈](#1.1 定义空闲任务的栈)
    • [1.2 定义空闲任务的任务控制块](#1.2 定义空闲任务的任务控制块)
    • [1.3 创建空闲任务](#1.3 创建空闲任务)
  • 二、实现阻塞延时
    • [2.1 vTaskDelay() 函数](#2.1 vTaskDelay() 函数)
    • [2.2 修改 vTaskSwitchContext() 函数](#2.2 修改 vTaskSwitchContext() 函数)
    • [2.3 SysTick 中断服务函数](#2.3 SysTick 中断服务函数)
    • [2.4 SysTick 初始化函数](#2.4 SysTick 初始化函数)
  • 三、main函数

前言

在前面的学习中,任务中的延时使用的是软件延时,即还是让 CPU 等待来达到延时的效果。使用 RTOS 的优势就是能充分发挥 CPU 的性能,永远不让它闲着。任务如果需要延时,也就不能再让 CPU 空等来实现延时的效果。RTOS 中的延时叫作阻塞延时,即任务需要延时时,会放弃 CPU 的使用权,CPU 可以去做其他的事情,当任务延时时间到,重新获取 CPU 使用权,任务继续运行,这样就充分地利用了 CPU 的资源,而不是空等。

当任务需要延时而进入阻塞状态时,那 CPU 在做什么?如果没有其他任务可以运行,RTOS 都会为 CPU 创建一个空闲任务,这个时候 CPU 就运行空闲任务。在 FreeRTOS 中,空闲任务是系统在启动调度器时创建的优先级最低的任务,空闲任务主体主要是做一些系统内存的清理工作。但是为了简单起见,我们本章实现的空闲任务只是对一个全局变量进行计数。鉴于空闲任务的这种特性,在实际应用中,当系统进入空闲任务时,可在空闲任务中让单片机进入休眠或者低功耗等操作。


一、实现空闲任务

目前我们在创建任务时使用的栈和 TCB 都使用静态内存,即需要预先定义好内存,空闲任务也不例外。

1.1 定义空闲任务的栈

在 main.c 中定义空闲任务的栈

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

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

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

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

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

1.3 创建空闲任务

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

c 复制代码
1 extern TCB_t IdleTaskTCB;
2 void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
3                                     StackType_t **ppxIdleTaskStackBuffer,
4                                     uint32_t *pulIdleTaskStackSize );
5 void vTaskStartScheduler( void )
6 {
7     /*============================= 创建空闲任务 start =============================*/
8     TCB_t *pxIdleTaskTCBBuffer = NULL;        /* 用于指向空闲任务控制块 */
9     StackType_t *pxIdleTaskStackBuffer = NULL; /* 用于空闲任务栈起始地址 */
10    uint32_t ulIdleTaskStackSize;
11
12    /* 获取空闲任务的内存:任务栈和任务 TCB */                              (1)
13    vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
14                                   &pxIdleTaskStackBuffer,
15                                   &ulIdleTaskStackSize );
16    /* 创建空闲任务 */                                                   (2)
17    xIdleTaskHandle =
18    xTaskCreateStatic( (TaskFunction_t)prvIdleTask,   /* 任务入口 */
19                      (char *)"IDLE",                 /* 任务名称,字符串形式 */
20                      (uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */
21                      (void *) NULL,                  /* 任务形参 */
22                      (StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */
23                      (TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */
24    /* 将任务添加到就绪列表 */                                          (3)
25    vListInsertEnd( &( pxReadyTasksLists[0] ),
26                   &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
27    /*============================= 创建空闲任务 end ==============================*/
28
29    /* 手动指定第一个运行的任务 */
30    pxCurrentTCB = &Task1TCB;
31
32    /* 启动调度器 */
33    if ( xPortStartScheduler() != pdFALSE )
34    {
35        /* 调度器启动成功,则不会返回,即不会来到这里 */
36    }
37 }

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

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

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

vApplicationGetIdleTaskMemory() 函数实现:

c 复制代码
1 void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
2                                    StackType_t **ppxIdleTaskStackBuffer,
3                                    uint32_t *pulIdleTaskStackSize )
4 {
5     *ppxIdleTaskTCBBuffer=&IdleTaskTCB;
6     *ppxIdleTaskStackBuffer=IdleTaskStack;
7     *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
8 }

二、实现阻塞延时

2.1 vTaskDelay() 函数

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

c 复制代码
1 void vTaskDelay( const TickType_t xTicksToDelay )
2 {
3     TCB_t *pxTCB = NULL;
4 
5     /* 获取当前任务的 TCB */
6     pxTCB = pxCurrentTCB;                                      (1)
7 
8     /* 设置延时时间 */
9     pxTCB->xTicksToDelay = xTicksToDelay;                      (2)
10
11    /* 任务切换 */
12    taskYIELD();                                               (3)
13 }

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

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

xTicksToDelay 定义

c 复制代码
1 typedef struct tskTaskControlBlock
2 {
3     volatile StackType_t   *pxTopOfStack;      /* 栈顶 */
4 
5     ListItem_t             xStateListItem;     /* 任务节点 */
6 
7     StackType_t            *pxStack;           /* 任务栈起始地址 */
8     /* 任务名称,字符串形式 */
9     char                   pcTaskName[ configMAX_TASK_NAME_LEN ];
10
11    TickType_t xTicksToDelay;                  /* 用于延时 */
12 } tskTCB;

2.2 修改 vTaskSwitchContext() 函数

vTaskDelay() 代码的(3):任务切换。调用 taskYIELD() 会产生 PendSV 中断,在 PendSV 中断服务函数中会调用上下文切换函数 vTaskSwitchContext(),该函数的作用是寻找最高优先级的就绪任务,然后更新 pxCurrentTCB。目前我们学习到现在只有两个任务,则 pxCurrentTCB 不是指向任务 1 就是指向任务 2,本章开始我们增加了一个空闲任务,则需要让 pxCurrentTCB 在这 3 个任务中切换。算法需要改变,具体实现参见代码清单 7-7 中的加粗部分。

vTaskSwitchContext() 函数

c 复制代码
1 #if 0  //(之前的写法)
2 void vTaskSwitchContext( void )
3 {   /* 两个任务轮流切换 */
4     if ( pxCurrentTCB == &Task1TCB )
5     {
6         pxCurrentTCB = &Task2TCB;
7     }
8     else
9     {
10        pxCurrentTCB = &Task1TCB;
11    }
12 }
13 #else
14 // 修改后的写法
15 void vTaskSwitchContext( void )
16 {
17    /* 如果当前任务是空闲任务,那么就去尝试执行任务1或者任务2,
18    看看它们的延时是否结束,如果任务的延时均没有到期,
19    则返回,继续执行空闲任务 */
20    if ( pxCurrentTCB == &IdleTaskTCB )                          (1)
21    {
22        if (Task1TCB.xTicksToDelay == 0)
23    		{
24        		pxCurrentTCB =&Task1TCB;
25    		}
26    		else if (Task2TCB.xTicksToDelay == 0)
27    		{
28        		pxCurrentTCB =&Task2TCB;
29    		}
30    		else
31    		{
32        		return;        /* 任务延时均没有到期则返回,继续执行空闲任务 */
33    		}
34 		}
35 		else/* 当前任务不是空闲任务则会执行到这里 */                                  (2)
36 		{
37    		/* 如果当前任务是任务 1 或者任务 2,检查另外一个任务,
38    		如果另外的任务不在延时中,就切换到该任务,
39    		否则,判断当前任务是否应该进入延时状态,
40    		如果是,就切换到空闲任务,否则不进行任何切换 */
41    		if (pxCurrentTCB == &Task1TCB)
42    		{
43        		if (Task2TCB.xTicksToDelay == 0)
44        		{
45           		 pxCurrentTCB =&Task2TCB;
46        		}
47        		else if (pxCurrentTCB->xTicksToDelay != 0)
48        		{
49            		pxCurrentTCB = &IdleTaskTCB;
50        		}
51      		  else
52     		   {
53      		      return;            /* 返回,不进行切换,因为两个任务都处于延时中 */
54       		 }
55   		 }
56    		else if (pxCurrentTCB == &Task2TCB)
57    		{
58        		if (Task1TCB.xTicksToDelay == 0)
59        		{
60           		 pxCurrentTCB =&Task1TCB;
61      		  }
62      		  else if (pxCurrentTCB->xTicksToDelay != 0)
63       		 {
64      		      pxCurrentTCB = &IdleTaskTCB;
65      		  }
66       		 else
67       		 {
68        		    return;            /* 返回,不进行切换,因为两个任务都处于延时中 */
69       		 }
70   		 }
71 		}
72 }
73 
74 #endif

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

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


2.3 SysTick 中断服务函数

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

SysTick 中断服务函数

c 复制代码
1 void xPortSysTickHandler( void )
2 {
3     /* 关中断 */
4     UBaseType_t uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();  (1)
5 
6     /* 更新系统时基 */
7     xTaskIncrementTick();                              (2)
8 
9     /* 开中断 */
10    portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );      (3)
11 }

( 1 ):进入临界段,关中断。

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

( 3 ):退出临界段,开中断。

xTaskIncrementTick()函数

c 复制代码
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,加 1 操作。xTickCount 是一个在 port.c 中定义的全局变量,在函数 vTaskStartScheduler() 中调用 xPortStartScheduler() 函数前初始化。

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

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


2.4 SysTick 初始化函数

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

c 复制代码
1 /* SysTick 控制寄存器 */                                          (1)
2 #define portNVIC_SYSTICK_CTRL_REG (*((volatile uint32_t *) 0xe000e010 ))
3 /* SysTick 重装载寄存器 */
4 #define portNVIC_SYSTICK_LOAD_REG (*((volatile uint32_t *) 0xe000e014 ))
5 
6 /* SysTick 时钟源选择 */
7 #ifndef configSYSTICK_CLOCK_HZ
8     #define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
9     /* 确保 SysTick 的时钟与内核时钟一致 */
10    #define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )
11 #else
12    #define portNVIC_SYSTICK_CLK_BIT ( 0 )
13 #endif
14 
15 #define portNVIC_SYSTICK_INT_BIT        ( 1UL << 1UL )
16 #define portNVIC_SYSTICK_ENABLE_BIT     ( 1UL << 0UL )
17 
18 void vPortSetupTimerInterrupt( void )                              (2)
19 {
20     /* 设置重装载寄存器的值 */                                     (2)-①
21     portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
22 
23 
24     /* 设置系统定时器的时钟等于内核时钟
25        使能 SysTick 定时器中断
26        使能 SysTick 定时器 */                                       (2)-②
27     portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
28                                   portNVIC_SYSTICK_INT_BIT |
29                                   portNVIC_SYSTICK_ENABLE_BIT );
30 }

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

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

xPortStartScheduler() 函数中调用 vPortSetupTimerInterrupt()

c 复制代码
1 BaseType_t xPortStartScheduler( void )
2 {
3     /* 配置 PendSV 和 SysTick 的中断优先级为最低 */
4     portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
5     portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
6     
7     /* 初始化 SysTick */
8     vPortSetupTimerInterrupt();   // 新增
9 
10    /* 启动第一个任务,不再返回 */
11    prvStartFirstTask();
12 
13    /* 不应该运行到这里 */
14    return 0;
15 }

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

(2)-②:设置系统定时器的时钟等于内核时钟,启用 SysTick 定时器中断和 SysTick 定时器。

configCPU_CLOCK_HZ 与 configTICK_RATE_HZ 宏定义

c 复制代码
1 #define configCPU_CLOCK_HZ      ( ( unsigned long ) 25000000 )        (1)
2 #define configTICK_RATE_HZ      ( ( TickType_t ) 100 )                (2)

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

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


三、main函数

main函数代码改动不大,我把需要改动的代码写在下面。

c 复制代码
101 /* 任务 1 */
102 void Task1_Entry( void *p_arg )
103 {
104     for ( ; ; )
105     {
106 #if 0
107         flag1 = 1;
108         delay( 100 );
109         flag1 = 0;
110         delay( 100 );
111 
112         /* 任务切换,这里是手动切换 */
113         portYIELD();
114 #else
115         flag1 = 1;
116         vTaskDelay( 2 );                                      (1)
117         flag1 = 0;
118         vTaskDelay( 2 );
119 #endif
120     }
121 }
122 
123 /* 任务 2 */
124 void Task2_Entry( void *p_arg )
125 {
126     for ( ; ; )
127     {
128 #if 0
129         flag2 = 1;
130         delay( 100 );
131         flag2 = 0;
132         delay( 100 );
133 
134         /* 任务切换,这里是手动切换 */
135         portYIELD();
136 #else
137         flag2 = 1;
138         vTaskDelay( 2 );                                      (2)
139         flag2 = 0;
140         vTaskDelay( 2 );
141 #endif
142     }
143 }
144 
145 /* 获取空闲任务的内存 */
146 StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
147 TCB_t IdleTaskTCB;                                         (3)
148 void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
149                                     StackType_t **ppxIdleTaskStackBuffer,
150                                     uint32_t *pulIdleTaskStackSize )
151 {
152     *ppxIdleTaskTCBBuffer=&IdleTaskTCB;
153     *ppxIdleTaskStackBuffer=IdleTaskStack;
154     *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
155 }

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

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


实验现象

进入软件进行调试,全速运行程序,从逻辑分析仪中可以看到两个任务的波形是完全同步的,就好像 CPU 在同时做两件事

flag1 和 flag2 的低电平的时间为(8.5630-8.5835)s,刚好等于阻塞延时时的 20ms


相关推荐
蒸蒸yyyyzwd31 分钟前
秋招学习笔记 day106
笔记·学习
小李不想当小白2 小时前
WDG看门狗(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
欢欢李2 小时前
【LTE L3 笔记 04】LTE帧结构
笔记·无线通信·物理层·lte·空口协议
Zhao-Jichao4 小时前
AI Prompting for Everyone 课程笔记 - P20 实验与期末项目 Lab and Final Project
前端·人工智能·笔记
lisw055 小时前
作为人类代理的大语言模型及其角色分类!
人工智能·学习·语言模型
Z21-X5 小时前
云原生运维实战笔记:企业网站综合上云部署与云运维服务全景解析
运维·笔记·学习·云原生
坤坤子吖5 小时前
Linux网络编程:UDP进阶,从字典服务器到简单聊天室
linux·服务器·网络·c++·笔记·学习
Rabitebla5 小时前
【Linux学习笔记】进程篇(九):进程的创建与退出——fork、写时拷贝与退出码
linux·c++·笔记·学习
楚山之石6 小时前
【MRM】CAPLEX 对比剂灌注 MRI 开源术语库:ISMRM OSIPI 共识报告
图像处理·人工智能·学习·mri