学习FreeRTOS之路(三):任务的定义与任务切换的实现

什么是任务

在裸机系统中,系统的主体就是 main 函数里面顺序执行的无限循环,这个无限循环里面 CPU 按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。

 void task_entry (void *parg)
 {
	 /* 任务主体,无限循环且不能返回 */
	 for (;;) {
		 /* 任务主体代码 */
	 }
 }

程序

 #include "FreeRTOS.h"
 #include "task.h"
 
 //全局变量
 portCHAR flag1;          
 portCHAR flag2;
 extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
 
 //任务控制块
 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 );

 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 ( ;; )
 {
	 flag1 = 1;
	 delay( 100 );
	 flag1 = 0;
	 delay( 100 );
	 /* 任务切换,这里是手动切换 */
	 taskYIELD(); ( 注意)
 }
 }
 /* 任务 2 */
 void Task2_Entry( void *p_arg )
 {
	for ( ;; )
 {
	 flag2 = 1;
	 delay( 100 );
	 flag2 = 0;
	 delay( 100 );
 
	 /* 任务切换,这里是手动切换 */
	 taskYIELD(); ( 注意)
 }
 }

注:下面是相关函数的具体分析,我看到汇编就蒙了,留待以后水平提升再战

创建任务

定义任务栈

在一个裸机系统中,如果有全局变量,有子函数调用,有中断发生。那么系统在运行的时候,全局变量放在哪里,子函数调用时,局部变量放在哪里,中断发生时,函数返回地址放哪里。

如果只是单纯的裸机编程,它们放哪里我们不用管,但是如果要写一个 RTOS,这些种种环境参数,我们必须弄清楚他们是如何存储的。在裸机系统中,他们统统放在一个叫栈的地方,栈是单片机 RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定,最后由 C库函数_main 进行初始化。

但是,在多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM中。

本章我们要实现两个变量按照一定的频率轮流的翻转,每个变量对应一个任务,那么就需要定义两个任务栈

IDE采用的是KEIL 5

1 #define TASK1_STACK_SIZE 128 
2 StackType_t Task1Stack[TASK1_STACK_SIZE]; 
3 
4 #define TASK2_STACK_SIZE 128
5 StackType_t Task2Stack[TASK2_STACK_SIZE];

任务栈其实就是一个预先定义好的全局数据,数据类型为StackType_t(uint_32 4字节),大小由 TASK1_STACK_SIZE 这个宏来定义,默认为 128,即 512字节,这也是 FreeRTOS 推荐的最小的任务栈。

在 FreeRTOS 中,凡是涉及到数据类型的地方,FreeRTOS 都会将标准的 C 数据类型用 typedef 重新取一个类型名。这些经过重定义的数据类型放在 portmacro.h

定义任务函数

任务是一个独立的函数,函数主体无限循环且不能返回。

1 /* 软件延时 */
2 void delay (uint32_t count)
3 {
4 for (; count!=0; count--);
5 }
6 /* 任务 1 */
7 void Task1_Entry( void *p_arg ) (1)
8 {
9 for ( ;; )
10 {
11 flag1 = 1;
12 delay( 100 );
13 flag1 = 0;
14 delay( 100 );
15 }
16 }
17 
18 /* 任务 2 */
19 void Task2_Entry( void *p_arg ) (2)
20 {
21 for ( ;; )
22 {
23 flag2 = 1;
24 delay( 100 );
25 flag2 = 0;
26 delay( 100 );
27 }
28 }

定义任务控制块

在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。

系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块,这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等。

有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现。

定义一个任务控制块需要一个新的数据类型,该数据类型在task.c 这 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 } tskTCB;

栈顶指针,作为 TCB的第一个成员。

任务节点,这是一个内置在 TCB 控制块中的链表节点,通过这个节点,可以将任务控制块挂接到各种链表中。

任务栈起始地址。

任务名称,字符串形式,长度由宏 configMAX_TASK_NAME_LEN来控制,该宏在 FreeRTOSConfig.h 中定义,默认为 16。

1 /* 定义任务控制块 */
2 TCB_t Task1TCB;
3 TCB_t Task2TCB;

实现任务创建函数

任务的栈,任务的函数实体,任务的控制块最终需要联系起来才能由系统进行统一调度。那么这个联系的工作就由任务创建函数 xTaskCreateStatic()来实现,该函数在 task.c中定义,在 task.h 中声明,所有跟任务相关的函数都在这个文件定义。

  • 1.xTaskCreateStatic()静态创建函数
TaskHandle_t xTaskCreateStatic( 
 TaskFunction_t pxTaskCode,   /* 任务入口 */
 const char * const pcName,  /* 任务名称,字符串形式 */
 const uint32_t ulStackDepth,   /* 任务栈大小,单位为字 */
 void * const pvParameters,    /* 任务形参 */
 StackType_t * const puxStackBuffer,  /* 任务栈起始地址 */
 TCB_t * const pxTaskBuffer )  /* 任务控制块指针 */

FreeRTOS 中,任务的创建有两种方法,一种是使用动态创建,一种是使用静态创建。动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内 存 , 任 务 删 除 时 , 内 存 不 能 释 放 。 目 前 我 们 以 静 态 创 建

xTaskCreateStatic()调用 prvInitialiseNewTask()函数创建新任务,该函数在 task.c 实现

  • 2.prvInitialiseNewTask()函数
 static void prvInitialiseNewTask(
	 TaskFunction_t pxTaskCode,    //任务入口
	 const char * const pcName,   //任务名称,字符串形式
	 const uint32_t ulStackDepth,   //任务栈大小,单位为字
	 void * const pvParameters,   //任务形参
	 TaskHandle_t * const pxCreatedTask,   //任务句柄
	 TCB_t *pxNewTCB )  //任务控制块指针

prvInitialiseNewTask()函数调用 pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中。

  • 3.pxPortInitialiseStack()函数
1 #define portINITIAL_XPSR  ( 0x01000000 )
2 #define portSTART_ADDRESS_MASK  ( ( StackType_t ) 0xfffffffeUL )
3 
4 static void prvTaskExitError( void )
5 {
6 /* 函数停止在这里 */
7 for (;;);
8 }
9 
10 StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack,
11 TaskFunction_t pxCode,
12 void *pvParameters )
13 {
14 /* 异常发生时,自动加载到 CPU 寄存器的内容 */ (1)
15 pxTopOfStack--;
16 *pxTopOfStack = portINITIAL_XPSR; (2)
17 pxTopOfStack--;
18 *pxTopOfStack =  ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; (3)
19 pxTopOfStack--;
20 *pxTopOfStack = ( StackType_t ) prvTaskExitError; (4) 
21 pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为 0 */
22 *pxTopOfStack = ( StackType_t ) pvParameters;  (5) 
23 
24 /* 异常发生时,手动加载到 CPU 寄存器的内容 */ (6)
25 pxTopOfStack -= 8; 
26 
27 /* 返回栈顶指针,此时 pxTopOfStack 指向空闲栈 */
28 return pxTopOfStack; (7)
29 }

实现就绪列表

  • 定义就绪列表

任务创建好之后,我们需要把任务添加到就绪列表里面,表示任务已经就绪,系统随时可以调度。就绪列表在 task.c中定义

/* 任务就绪列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

就绪列表实际上就是一个 List_t 类型的数组,数组的大小由决定
最 大 任 务 优 先 级 的 宏 configMAX_PRIORITIES 决 定

configMAX_PRIORITIES 在FreeRTOSConfig.h 中默认定义为 5,最大支持 256 个优先级。

数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中。

  • 就绪列表初始化
void prvInitialiseTaskLists( void )
 {
 UBaseType_t uxPriority;
 
 
 for ( uxPriority = ( UBaseType_t ) 0U;
 uxPriority < ( UBaseType_t ) configMAX_PRIORITIES;
 uxPriority++ )
 {
 vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
 }
 }
  • 将任务插入到就绪列表

任务控制块里面有一个 xStateListItem 成员,数据类型为ListItem_t,我们将任务插入到就绪列表里面,就是通过将任务控制块的 xStateListItem 这个节点插入到就绪列表中来实现的。

如果把就绪列表比作是晾衣架,任务是衣服,那 xStateListItem 就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种不同的链表中。

实现调度器

调度器是操作系统的核心,其主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务。

从代码上来看,调度器无非也就是由几个全局变量和一些可以实现任务切换的函数组成,全部都在 task.c文件中实现。

启动调度器

调度器的启动由 vTaskStartScheduler()函数来完成,该函数在 task.c 中定义

  • 1.vTaskStartScheduler()函数
 void vTaskStartScheduler( void )
 {
	 /* 手动指定第一个运行的任务 */
	 pxCurrentTCB = &Task1TCB; (1)
	 
	 /* 启动调度器 */
	 if ( xPortStartScheduler() != pdFALSE )
	 {
		 /* 调度器启动成功,则不会返回,即不会来到这里 */ (2)
	 }
 }

pxCurrentTCB 是一个在 task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。目前我们还不支持优先级,则手动指定第一个要运行的任务。

调用函数 xPortStartScheduler()启动调度器,调度器启动成功,则不会返回。该函数在 port.c 中实现

  • 2.xPortStartScheduler()函数
  #define portNVIC_SYSPRI2_REG  (*(( volatile uint32_t *) 0xe000ed20))
 
  #define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 16UL)
  #define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
 
 BaseType_t xPortStartScheduler( void )
 {
 /* 配置 PendSV 和 SysTick 的中断优先级为最低 */ (1)
 portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
 portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
 
 /* 启动第一个任务,不再返回 */
 prvStartFirstTask(); (2)
 
 /* 不应该运行到这里 */
 return 0;
}

配置 PendSV 和 SysTick 的中断优先级为最低。SysTick 和PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级,即优先相应系统中的外部硬件中断,所以 SysTick 和 PendSV 的中断优先级配置为最低。

调用函数 prvStartFirstTask()启动第一个任务,启动成功后,则不再返回,该函数由汇编编写,在 port.c实现

  • 3.prvStartFirstTask()函数
asm void prvStartFirstTask( void )
 {
 PRESERVE8 (2)
 
 /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 这个寄存器的地址, (3)
 里面存放的是向量表的起始地址,即 MSP 的地址 */
 ldr r0, =0xE000ED08 (4)
 ldr r0, [r0] (5)
 ldr r0, [r0] (6)
 
 /* 设置主堆栈指针 msp 的值 */
 msr msp, r0 (7)
 
 /* 使能全局中断 */ (8)
 cpsie i
 cpsie f
 dsb
 isb
 
 /* 调用 SVC 去启动第一个任务 */
 svc 0 (9)
 nop
 nop
 }
  • 4.vPortSVCHandler()函数
    vPortSVCHandler()函数开始真正启动第一个任务,不再返回
1 __asm void vPortSVCHandler( void )
2 {
3 extern pxCurrentTCB; (1)
4 
5 PRESERVE8
6 
7 ldr r3, =pxCurrentTCB (2)
8 ldr r1, [r3] (3)
9 ldr r0, [r1] (4)
10 ldmia r0!, {r4-r11} (5)
11 msr psp, r0 (6)
12 isb
13 mov r0, #0 (7)
14 msr basepri, r0 (8)
15 orr r14, #0xd (9)
16 
17 bx r14 (10)
18 }

任务切换

  • 1.taskYIELD()
1 /* 在 task.h 中定义 */
2 #define taskYIELD()  portYIELD()
3 
4 
5 /* 在 portmacro.h 中定义 */
6 /* 中断控制状态寄存器:0xe000ed04
7 * Bit 28 PENDSVSET: PendSV 悬起位
8 */
9  #define portNVIC_INT_CTRL_REG  (*(( volatile uint32_t *) 0xe000ed04))
10 #define portNVIC_PENDSVSET_BIT ( 1UL << 28UL )
11 
12 #define portSY_FULL_READ_WRITE ( 15 )
13 
14 #define portYIELD() \  \
15 {  \  \
16 /* 触发 PendSV,产生上下文切换 */ \ \
17 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; (1) \  \
18 __dsb( portSY_FULL_READ_WRITE );  \  \
19 __isb( portSY_FULL_READ_WRITE ); \  \
20 }

portYIELD 的实现很简单,实际就是将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV中断,去执行我们写好的 PendSV 中断服务函数,在里面实现任务切换。

  • 2.xPortPendSVHandler()函数

PendSV中断服务函数是真正实现任务切换的地方

1 __asm void xPortPendSVHandler( void )
2 {
3 extern pxCurrentTCB; (1)
4 extern vTaskSwitchContext; (2)
5 
6 PRESERVE8 (3)
7 
8 mrs r0, psp (4)
9 isb
10 
11 ldr r3, =pxCurrentTCB (5)
12 ldr r2, [r3] (6)
13 
14 stmdb r0!, {r4-r11} (7)
15 str r0, [r2] (8)
16 
17 stmdb sp!, {r3, r14} (9)
18 mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY (10)
19 msr basepri, r0 (11)
20 dsb
21 isb
22 bl vTaskSwitchContext (12)
23 mov r0, #0 (13)
24 msr basepri, r0
25 ldmia sp!, {r3, r14} (14)
26 
27 ldr r1, [r3] (15)
28 ldr r0, [r1] (16)
29 ldmia r0!, {r4-r11} (17)
30 msr psp, r0 (18)
31 isb
32 bx r14 (19)
33 nop
34 }
  • 3.vTaskSwitchContext()函数
1 void vTaskSwitchContext( void )
2 {
3 /* 两个任务轮流切换 */
4 if ( pxCurrentTCB == &Task1TCB ) (1)
5 {
6 pxCurrentTCB = &Task2TCB;
7 }
8 else (2)
9 {
10 pxCurrentTCB = &Task1TCB;
11 }
12 }

(1) :如果当前任务为任务 1,则把下一个要运行的任务改为任务 2。
(2) :如果当前任务为任务 2,则把下一个要运行的任务改为任务 1。

依旧参考、感谢:

[野火®]《FreeRTOS 内核实现与应用开发实战—基于STM32》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值