【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