• FreeRTOS学习笔记——四、任务的定义与任务切换的实现


    0 前言

    1. 本章是我们真正从从0 到1 写FreeRTOS 的第一章,属于基础中的基础
    2. 必须要学会创建任务,并重点掌握任务是如何切换
    3. 因为任务的切换是由汇编代码来完成的,所以代码看起来比较难懂,但是会尽力把代码讲得透彻
    4. 如果本章内容学不会,后面的内容根本无从下手
    5. 在这章中:
      • 我们会创建两个任务,并让这两个任务不断地切换,任务的主体都是让一个变量按照一定的频率翻转
      • 通过KEIL 的软件仿真功能,在逻辑分析仪中观察变量的波形变化,最终的波形图具体见图
      • 在这里插入图片描述
    6. 上图的波形图的效果,并不是真正的多任务系统中任务切换的效果图,这个效果其实可以完全由裸机代码来实现,具体见代码
    1 /* flag 必须定义成全局变量才能添加到逻辑分析仪里面观察波形
    2 * 在逻辑分析仪中要设置以 bit 的模式才能看到波形,不能用默认的模拟量
    3 */
    4 uint32_t flag1;
    5 uint32_t flag2;
    6
    7
    8 /* 软件延时,不必纠结具体的时间 */
    9 void delay( uint32_t count )
    10 {
    11 		for (; count!=0; count--);
    12 }
    13
    14 int main(void)
    15 {
    16 		/* 无限循环,顺序执行 */
    17 		for (;;) {
    18 		    flag1 = 1;
    19 		    delay( 100 );
    20 		    flag1 = 0;
    21 		    delay( 100 );
    22
    23         flag2 = 1;
    24         delay( 100 );
    25         flag2 = 0;
    26         delay( 100 );
    27   }
    28 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    1. 多任务系统中,两个任务不断切换的效果图应该像下图所示那样,即两个变量的波形是完全一样的,就好像CPU在同时干两件事一样,这才是多任务的意义。
    • 在这里插入图片描述
    1. 这章只是开始,我们先掌握好任务是如何切换,在后面章节中,我们会陆续的完善功能代码,加入系统调度,实现真正的多任务。
    2. 千里之行,始于本章节,不要急

    1 什么是任务

    1. 裸机系统中:
    • 系统的主体就是main 函数里面顺序执行的无限循环
    • 这个无限循环里面CPU 按照顺序完成各种事情
    1. 多任务系统中:
    • 我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务
    • 任务的大概形式具体见代码
      1 void task_entry (void *parg)
      2 {
      3 	/* 任务主体,无限循环且不能返回 */
      4 	for (;;) {
      5 		/* 任务主体代码 */
      6 	}
      7 }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    2 创建任务

    2.1 定义任务栈

    1. 在一个裸机系统中:

      • 如果有全局变量,有子函数调用,有中断发生
      • 那么系统在运行的时候,全局变量放在哪里,子函数调用时,局部变量放在哪里,中断发生时,函数返回地址放哪里
      • 如果只是单纯的裸机编程,它们放哪里我们不用管
      • 写一个RTOS,这些种种环境参数,我们必须弄清楚他们是如何存储的
    2. 裸机系统中,他们统统放在一个叫的地方

      • 栈是单片机RAM 里面一段连续的内存空间,栈的大小一般在启动文件或者链接脚本里面指定
      • 最后由C 库函数_main 进行初始化
    3. 多任务系统中:

      • 每个任务都是独立的,互不干扰的
      • 要为每个任务都分配独立的栈空间
        • 这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于RAM 中
    4. 本章我们要实现两个变量按照一定的频率轮流的翻转:

      • 每个变量对应一个任务,那么就需要定义两个任务栈,具体见代码清单
      • 在多任务系统中,有多少个任务就需要定义多少个任务栈
      /* main.c 中定义任务栈 */
      1 #define TASK1_STACK_SIZE 128 							(2)
      2 StackType_t Task1Stack[TASK1_STACK_SIZE]; 	(1)
      3
      4 #define TASK2_STACK_SIZE 128
      5 StackType_t Task2Stack[TASK2_STACK_SIZE];
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • (1):任务栈:
        • 预先定义好的全局数据
        • 数据类型为:StackType_t
          • #define portSTACK_TYPE uint32_t;
          • typedef portSTACK_TYPE StackType_t;
        • 大小由TASK1_STACK_SIZE 这个宏来定义
          • 默认为128,单位为字,即512字节, 是FreeRTOS 推荐的最小的任务栈
        • 在FreeRTOS 中,凡是涉及到数据类型的地方,FreeRTOS 都会将标准的C 数据类型用typedef 重新取一个类型名, 经过重定义的数据类型放在portmacro.h这个头文件
      0 /* portmacro.h 中的数据类型 */
      1 #ifndef PORTMACRO_H
      2 #define PORTMACRO_H
      3
      4 /* 包含标准库头文件 */
      5 #include "stdint.h"
      6 #include "stddef.h"
      7
      8
      9 /* 数据类型重定义 */
      10 #define portCHAR char
      11 #define portFLOAT float
      12 #define portDOUBLE double
      13 #define portLONG long
      14 #define portSHORT short
      15 #define portSTACK_TYPE uint32_t
      16 #define portBASE_TYPE long
      17
      18 typedef portSTACK_TYPE StackType_t;
      19 typedef long BaseType_t;
      20 typedef unsigned long UBaseType_t;
      21
      22
      23 #endif /* PORTMACRO_H */
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24

    2.2 定义任务函数

    1. 任务是一个独立的函数,函数主体无限循环不能返回
    2. 本章我们在main.c 中定义的两个任务具体见代码清单
    0 /* main.c 中的任务函数 */
    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 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    2.3 定义任务控制块

    1. 裸机系统中,程序的主体是CPU 按照顺序执行的
    2. 多任务系统中,任务的执行是由系统调度的
    3. 系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块
      • 这个任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈指针,任务名称,任务的形参等
      • 有了这个任务控制块之后,以后系统对任务的全部操作都可以通过这个任务控制块来实现
    4. 定义一个任务控制块需要一个新的数据类型:
      • 该数据类型在task.c 这C 头文件中声明,具体的声明见代码清单
      • 使用它可以为每个任务都定义一个任务控制块实体
      0 /* task.c 中定义任务控制块 */
      1 typedef struct tskTaskControlBlock
      2 {
      3 		volatile StackType_t *pxTopOfStack; 	/* 栈顶 */ 						(1)
      4
      5 		ListItem_t xStateListItem; 					/* 任务节点 */ 				(2)
      6
      7 		StackType_t *pxStack; 							/* 任务栈起始地址 */ 		(3)
      8 																			/* 任务名称,字符串形式 */(4)
      9 		char pcTaskName[ configMAX_TASK_NAME_LEN ];
      10 } tskTCB;
      11 typedef tskTCB TCB_t; 																			(5)
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • (1):栈顶指针,作为TCB(TaskControlBlock) 的第一个成员
      • (2):任务节点,这是一个内置在TCB 控制块中的链表节点
        • 通过这个节点,可以将任务控制块挂接到各种链表中
      • (3):任务栈起始地址
      • (4):任务名称,字符串形式,长度由宏configMAX_TASK_NAME_LEN来控制,该宏在FreeRTOSConfig.h 中定义,默认为16
      • (5):数据类型重定义
    5. 在main.c 文件中为两个任务定义的任务控制块,具体见代码清单
    /* main.c 中定义任务控制块 */
    1 /* 定义任务控制块 */
    2 TCB_t Task1TCB;
    3 TCB_t Task2TCB;
    
    • 1
    • 2
    • 3
    • 4

    2.4 实现任务创建函数

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

    2.4.1 任务创建函数 —— xTaskCreateStatic()函数

    1. xTaskCreateStatic()函数的实现见代码清单
    1 #if( configSUPPORT_STATIC_ALLOCATION == 1 ) 												(1)
    2
    3 TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, 					(2)
    4 															const char * const pcName, 					(3)
    5 															const uint32_t ulStackDepth, 				(4)
    6 															void * const pvParameters, 					(5)
    7 															StackType_t * const puxStackBuffer, 	(6)
    8 															TCB_t * const pxTaskBuffer ) 				(7)
    9 {
    10 		TCB_t *pxNewTCB;
    11 		TaskHandle_t xReturn; 																				(8)
    12
    13 		if ( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
    14 		{
    15				pxNewTCB = ( TCB_t * ) pxTaskBuffer;
    16 				pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;
    17
    18 				/* 创建新的任务 */ 																					(9)
    19 				prvInitialiseNewTask( pxTaskCode, /* 任务入口 */
    20 				pcName, /* 任务名称,字符串形式 */
    21 				ulStackDepth, /* 任务栈大小,单位为字 */
    22 				pvParameters, /* 任务形参 */
    23 				&xReturn, /* 任务句柄 */
    24 				pxNewTCB); /* 任务栈起始地址 */
    25
    26 		}
    27 		else
    28 		{
    29 				xReturn = NULL;
    30 		}
    31
    32 		/* 返回任务句柄,如果任务创建成功,此时xReturn 应该指向任务控制块 */
    33 		return xReturn; 																						(10)
    34 }
    35
    36 #endif /* configSUPPORT_STATIC_ALLOCATION */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • (1):FreeRTOS 中,任务的创建有两种方法:
      • 一种是使用动态创建,一种是使用静态创建
      • 动态创建时,任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放
      • 静态创建时,任务控制块和栈的内存需要事先定义好,是静态的内存, 任务删除时, 内存不能释放
      • 目前我们以静态创建为例来讲解
      • configSUPPORT_STATIC_ALLOCATION 在FreeRTOSConfig.h 中定义,我们配置为1
    • (2):任务入口,即任务的函数名称
      • TaskFunction_t 是在projdefs.h中重定义的一个数据类型,实际就是空指针
      • 具体实现见代码清单 TaskFunction_t 定义
      0 /* projdefs.h 中TaskFunction_t定义 */
      1 #ifndef PROJDEFS_H
      2 #define PROJDEFS_H
      3
      4 typedef void (*TaskFunction_t)( void * );
      5
      6 #define pdFALSE ( ( BaseType_t ) 0 )
      7 #define pdTRUE ( ( BaseType_t ) 1 )
      8
      9 #define pdPASS ( pdTRUE )
      10 #define pdFAIL ( pdFALSE )
      11
      12
      13 #endif /* PROJDEFS_H */
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    • (3):任务名称,字符串形式,方便调试
    • (4):任务栈大小,单位为字
    • (5):任务形参
    • (6):任务栈起始地址
    • (7):任务控制块指针
    • (8):定义一个任务句柄xReturn
      • 任务句柄用于指向任务的TCB
      • 任务句柄的数据类型为TaskHandle_t,在task.h 中定义,实际上就是一个空指针
      • 具体实现见代码清单
    0 /* task.h 中 TaskHandle_t 定义 */
    1 /* 任务句柄 */
    2 typedef void * TaskHandle_t;
    
    • 1
    • 2
    • 3
    • (9):调用prvInitialiseNewTask()函数,创建新任务,该函数在task.c 实现,具体实现见代码清单
    • (10):返回任务句柄
      • 如果任务创建成功,此时xReturn 应该指向任务控制块,xReturn 作为形参传入到prvInitialiseNewTask 函数

    2.4.2 创建新任务——prvInitialiseNewTask()函数

    • (9):调用prvInitialiseNewTask()函数,创建新任务,该函数在task.c 实现,具体实现见代码清单
    0 /* task.c 中 prvInitialiseNewTask() 定义*/
    1 static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, 						(1)
    2 																const char * const pcName, 					(2)
    3 																const uint32_t ulStackDepth, 				(3)
    4 																void * const pvParameters, 					(4)
    5 																TaskHandle_t * const pxCreatedTask, 	(5)
    6 																TCB_t *pxNewTCB ) 									(6)
    7
    8 {
    9 		StackType_t *pxTopOfStack;
    10 		UBaseType_t x;
    11
    12 		/* 获取栈顶地址 */ 																								(7)
    13 		pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
    14 		/* 向下做8 字节对齐 */ 																						(8)
    15 		pxTopOfStack = ( StackType_t * ) \
    16 					( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );
    17
    18 		/* 将任务的名字存储在TCB 中 */ 																			(9)
    19 		for ( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
    20 		{
    21 				pxNewTCB->pcTaskName[ x ] = pcName[ x ];
    22
    23 				if ( pcName[ x ] == 0x00 )
    24 				{
    25 						break;
    26 				}
    27 		}
    28 		/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */ 									(10)
    29 		pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';
    30
    31 		/* 初始化TCB 中的xStateListItem 节点 */ 														(11)
    32 		vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    33 		/* 设置xStateListItem 节点的拥有者 */ 															(12)
    34 		listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    35
    36
    37 		/* 初始化任务栈 */ 																								(13)
    38 		pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack,
    39 		pxTaskCode,
    40 		pvParameters );
    41
    42
    43 		/* 让任务句柄指向任务控制块 */ 																			(14)
    44 		if ( ( void * ) pxCreatedTask != NULL )
    45 		{
    46 				*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
    47 		}
    48 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • (1):任务入口
    • (2):任务名称,字符串形式
    • (3):任务栈大小,单位为字
    • (4):任务形参
    • (5):任务句柄
    • (6):任务控制块指针
    • (7):获取栈顶地址
    • (8):将栈顶指针向下做8 字节对齐
      • 在Cortex-M3(Cortex-M4 或Cortex-M7)内核的单片机中,因为总线宽度是32 位的,通常只要栈保持4 字节对齐就行,可这样为啥要8 字节?难道有哪些操作是64 位的?确实有,那就是浮点运算,所以要8 字节对齐(但是目前我们都还没有涉及到浮点运算,只是为了后续兼容浮点运行的考虑
      • 如果栈顶指针是8 字节对齐的,在进行向下8 字节对齐的时候,指针不会移动,如果不是8 字节对齐的,在做向下8 字节对齐的时候,就会空出几个字节,不会使用,比如当pxTopOfStack 是33,明显不能整除8,进行向下8 字节对齐就是32,那么就会空出一个字节不使用。
    • (9):将任务的名字存储在TCB 中
    • (10):任务名字的长度不能超过configMAX_TASK_NAME_LEN,并以’\0’结尾
    • (11):初始化TCB 中的xStateListItem 节点,即初始化该节点所在的链表为空,表示节点还没有插入任何链表
    • (12):设置xStateListItem 节点的拥有者,即拥有这个节点本身的TCB
    • (13):调用pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中
    • (14):让任务句柄指向任务控制块

    2.4.3 初始化任务栈——pxPortInitialiseStack()函数

    • (13):调用pxPortInitialiseStack()函数初始化任务栈,并更新栈顶指针,任务第一次运行的环境参数就存在任务栈中
      • 该函数在port.c中定义,具体实现见代码清单
      • 任务栈初始化完毕之后,栈空间内部分布图具体见图
    0 /* port.c 中prvTaskExitError函数定义 */
    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 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • (1):异常发生时,CPU 自动从栈中加载到CPU 寄存器的内容
      • 包括8个寄存器,分别为R0、R1、R2、R3、R12、R14、R15 和xPSR 的位24,且顺序不能变。
    • (2):xPSR 的bit24 必须置1,即0x01000000
      • port.c中定义: #define portINITIAL_XPSR ( 0x01000000 )
      • portINITIAL_XPSR是一个32位的寄存器,它是ARM Cortex-M处理器中的一个特殊寄存器,用于存储初始程序状态寄存器的值。它的值在启动时被加载到程序状态寄存器(PSR)中,以设置处理器的初始状态
    • (3):任务的入口地址
    • (4):任务的返回地址,通常任务是不会返回的,如果返回了就跳转到prvTaskExitError,该函数是一个无限循环
    • (5):R12, R3, R2 and R1 默认初始化为0
    • (6):异常发生时,需要手动加载到CPU 寄存器的内容,总共有8 个,分别为R4、R5、R6、R7、R8、R9、R10 和R11,默认初始化为0
    • (7):返回栈顶指针,此时pxTopOfStack 指向具体见图
      • 任务第一次运行时,就是从这个栈指针开始手动加载 8 个字的内容到CPU 寄存器:R4、R5、R6、R7、R8、R9、R10 和R11
      • 当退出异常时,栈中剩下的8 个字的内容会自动加载 到CPU 寄存器:R0、R1、R2、R3、R12、R14、R15 和xPSR 的位24
      • 此时PC 指针就指向了任务入口地址,从而成功跳转到第一个任务
        在这里插入图片描述

    3 实现就绪列表

    3.1 定义就绪列表

    1. 任务创建好之后,我们需要把任务添加到就绪列表里面,表示任务已经就绪,系统随时可以调度
    2. 就绪列表在task.c 中定义,具体见代码清单
    0 /* task.c 中定义就绪列表 */
    1 /* 任务就绪列表 */
    2 List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
    
    • 1
    • 2
    • 3
    • (1):就绪列表实际上就是一个List_t 类型的数组
      • 数组的大小由决定最大任务优先级的宏configMAX_PRIORITIES 决定, configMAX_PRIORITIES 在FreeRTOSConfig.h 中默认定义为5,最大支持256 个优先级。
      • 数组的下标对应了任务的优先级,同一优先级的任务统一插入到就绪列表的同一条链表中
      • 一个空的就绪列表具体见图
        在这里插入图片描述

    3.2 就绪列表初始化

    1. 就绪列表在使用前需要先初始化
    • 就绪列表初始化的工作在函数prvInitialiseTaskLists()里面实现
    • 具体见代码清单
    • 就绪列表初始化完毕之后,示意图见
    1 void prvInitialiseTaskLists( void )
    2 {
    3 		UBaseType_t uxPriority;
    4
    5
    6 		for ( uxPriority = ( UBaseType_t ) 0U;
    7 		uxPriority < ( UBaseType_t ) configMAX_PRIORITIES;
    8 		uxPriority++ )
    9 		{
    10 				vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
    11 		}
    12 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    3.3 将任务插入到就绪列表

    1. 任务控制块里面有一个xStateListItem 成员,数据类型为ListItem_t,我们将任务插入到就绪列表里面,就是通过将任务控制块的xStateListItem 这个节点插入到就绪列表中来实现的
    • 如果把就绪列表比作是晾衣架,任务是衣服,那xStateListItem 就是晾衣架上面的钩子,每个任务都自带晾衣架钩子,就是为了把自己挂在各种不同的链表中
    • 我们在任务创建好之后,紧跟着将任务插入到就绪列表,具体实现见代码清单 7-15 的加粗部分。
    1 /* 初始化与任务相关的列表,如就绪列表 */
    2 prvInitialiseTaskLists();
    3
    4 Task1_Handle = /* 任务句柄 */
    5 xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
    6 									(char *)"Task1", /* 任务名称,字符串形式 */
    7 									(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
    8 									(void *) NULL, /* 任务形参 */
    9 									(StackType_t *)Task1Stack, /* 任务栈起始地址 */
    10 									(TCB_t *)&Task1TCB ); /* 任务控制块 */
    11
    12 /* 将任务添加到就绪列表 */
    13 vListInsertEnd( &( pxReadyTasksLists[1] ),
    14 &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
    15
    16 Task2_Handle = /* 任务句柄 */
    17 xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
    18 									(char *)"Task2", /* 任务名称,字符串形式 */
    19 									(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
    20 									(void *) NULL, /* 任务形参 */
    21 									(StackType_t *)Task2Stack, /* 任务栈起始地址 */
    22 									(TCB_t *)&Task2TCB ); /* 任务控制块 */
    23 /* 将任务添加到就绪列表 */
    24 vListInsertEnd( &( pxReadyTasksLists[2] ),
    25 &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 就绪列表的下标对应的是任务的优先级
    • 但是目前我们的任务还不支持优先级,有关支持多优先级的知识点我们后面会讲到
    • 所以Task1 和Task2 任务在插入到就绪列表的时候,可以随便选择插入的位置
    • 在代码清单中,我们选择将Task1 任务插入到就绪列表下标为1 的链表中Task2 任务插入到就绪列表下标为2 的链表中,具体的示意图见图
      在这里插入图片描述

    4 实现调度器

    1. 调度器是操作系统的核心
    • 主要功能就是实现任务的切换,即从就绪列表里面找到优先级最高的任务,然后去执行该任务
    • 从代码上来看,调度器无非也就是由几个全局变量和一些可以实现任务切换的函数组成,全部都在task.c 文件中实现

    4.1 启动调度器

    1. 调度器的启动由vTaskStartScheduler()函数来完成,该函数在task.c 中定义,具体实现见代码清单

    4.1.1 调度器启动——vTaskStartScheduler()函数

    1. 调度器的启动由vTaskStartScheduler()函数来完成,该函数在task.c 中定义,具体实现见代码清单
    0 /* task.c 中 vTaskStartScheduler()函数 */
    1 void vTaskStartScheduler( void )
    2 {
    3 		/* 手动指定第一个运行的任务 */
    4 		pxCurrentTCB = &Task1TCB; 										(1)
    5
    6 		/* 启动调度器 */
    7 		if ( xPortStartScheduler() != pdFALSE )
    8 		{
    9 				/* 调度器启动成功,则不会返回,即不会来到这里 */ 	(2)
    10		 }
    11 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • (1):pxCurrentTCB 是一个在task.c 定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块
      • 目前我们还不支持优先级,则手动指定第一个要运行的任务
    • (2):调用函数xPortStartScheduler()启动调度器,调度器启动成功,则不会返回。该函数在port.c 中实现,具体见代码清单

    4.1.2 启动调度器——xPortStartScheduler()函数

    • (2):调用函数xPortStartScheduler()启动调度器,调度器启动成功,则不会返回。该函数在port.c 中实现,具体见代码清单
    0 /* port.c 中 xPortStartScheduler()函数 */
    1 /*
    2 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.7,百度搜索“PM0056”即可找到这个文档
    3 * 在Cortex-M 中,内核外设SCB 中SHPR3 寄存器用于设置SysTick 和PendSV 的异常优先级
    4 * System handler priority register 3 (SCB_SHPR3) SCB_SHPR3:0xE000 ED20
    5 * Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception
    6 * Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV
    7 */
    8 #define portNVIC_SYSPRI2_REG (*(( volatile uint32_t *) 0xe000ed20))
    9
    10 #define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 16UL)
    11 #define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
    12
    13 BaseType_t xPortStartScheduler( void )
    14 {
    15 		/* 配置PendSV 和 SysTick 的中断优先级为最低 */ 						(1)
    16 		portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
    17 		portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
    18
    19 		/* 启动第一个任务,不再返回 */
    20 		prvStartFirstTask(); 																(2)
    21
    22 		/* 不应该运行到这里 */
    23 		return 0;
    24 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • (1):配置 PendSV 和 SysTick 的中断优先级为最低
      • SysTick 和PendSV 都会涉及到系统调度
      • 系统调度的优先级要低于系统的其它硬件中断优先级
        • 优先响应系统中的外部硬件中断,所以SysTick 和PendSV 的中断优先级配置为最低
        • portNVIC_PENDSV_PRI是一个宏定义,用于设置PendSV中断的优先级。在FreeRTOS中,PendSV中断用于任务切换。
        • portNVIC_SYSTICK_PRI是一个宏定义,用于设置SysTick中断的优先级。在FreeRTOS中,SysTick定时器被用作系统时基,因此其中断优先级需要设置为最高。
    • (2):调用函数prvStartFirstTask()启动第一个任务,启动成功后,则不再返回,该函数由汇编编写,在port.c 实现,具体代码见代码清单

    4.1.3 启动第一个任务——prvStartFirstTask()函数

    1. (2):调用函数prvStartFirstTask()启动第一个任务,启动成功后,则不再返回,该函数由汇编编写,在port.c 实现,具体代码见代码清单
    2. prvStartFirstTask()函数用于开始第一个任务
      • 主要做了两个动作
        • 一个是更新MSP 的值
        • 二是产生SVC 系统调用
        • 然后去到SVC 的中断服务函数里面真正切换到第一个任务
      • 该函数的具体实现见代码清单
    1 /*
    2 * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到这个文档
    3 * 在Cortex-M 中,内核外设SCB 的地址范围为:0xE000ED00-0xE000ED3F
    4 * 0xE000ED008 为SCB 外设中SCB_VTOR 这个寄存器的地址,里面存放的是向量表的起始地址,即MSP 的地址
    5 */
    6
    7 																											(1)
    8 __asm void prvStartFirstTask( void )
    9 {
    10 		PRESERVE8 																					(2)
    11
    12 		/* 在Cortex-M 中,0xE000ED08 是SCB_VTOR 这个寄存器的地址, (3)
    13 				里面存放的是向量表的起始地址,即MSP 的地址 */
    14 		ldr r0, =0xE000ED08 																(4)
    15 		ldr r0, [r0] 																			(5)
    16 		ldr r0, [r0] 																			(6)
    17
    18 		/* 设置主堆栈指针msp 的值 */
    19 		msr msp, r0 																				(7)
    20
    21 		/* 使能全局中断 */ 																		(8)
    22 		cpsie i
    23 		cpsie f
    24 		dsb
    25 		isb
    26
    27 		/* 调用SVC 去启动第一个任务 */
    28 		svc 0 																							(9)
    29 		nop
    30 		nop
    31 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • (1):
    • (2):当前栈需按照8 字节对齐,如果都是32 位的操作则4 个字节对齐即可。在Cortex-M中浮点运算是8 字节的。
    • (3):在Cortex-M 中,0xE000ED08 是SCB_VTOR 寄存器的地址
      • 里面存放的是向量表的起始地址,即MSP 的地址
      • 向量表通常是从内部FLASH 的起始地址开始存放,那么可知memory:0x00000000 处存放的就是MSP 的值
      • 这个可以通过仿真时查看内存的值证实,具体见图
        在这里插入图片描述
        • SCB_VTOR寄存器是ARM Cortex-M处理器中的一个系统控制寄存器,用于存储向量表的地址。当处理器复位或发生异常时,处理器会从该地址处读取向量表的起始地址。
        • MSP(主堆栈指针(Main Stack Pointer,MSP))
    • (4):将0xE000ED08 这个立即数加载到寄存器R0
    • (5):将0xE000ED08 这个地址指向的内容加载到寄存器R0,此时R0等于SCB_VTOR寄存器的值,等于0x00000000,即memory 的起始地址
    • (6):将0x00000000 这个地址指向的内容加载到R0,此时R0 等于0x200008DB,与图查询到的值吻合。
    • (7):将R0 的值存储到MSP,此时MSP 等于0x200008DB,这是主堆栈的栈顶指针
      • 起始这一步操作有点多余,因为当系统启动的时候,执行完Reset_Handler的时候,向量表已经初始化完毕,MSP 的值就已经更新为向量表的起始值,即指向主堆栈的栈顶指针。
    • (8):使用CPS 指令把全局中断打开
      • 为了快速地开关中断, Cortex-M内核 专门设置了一条 CPS 指令,有 4 种用法,具体见代码清单
    0 /* CPS指令用法 */
    1 CPSID I ;PRIMASK=1 ;关中断
    2 CPSIE I ;PRIMASK=0 ;开中断
    3 CPSID F ;FAULTMASK=1 ;关异常
    4 CPSIE F ;FAULTMASK=0 ;开异常
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • PRIMASK 和FAULTMAST 是Cortex-M内核 里面三个中断屏蔽寄存器中的两个,还有一个是BASEPRI,有关这三个寄存器的详细用法见表格
      在这里插入图片描述
    • (9):产生系统调用,服务号0 表示SVC 中断,接下来将会执行SVC 中断服务函数
    • **SVC(Supervisor Call)**中断是一种特殊的中断,通常在嵌入式系统中使用。它是由程序或硬件产生的异常或错误条件触发的,可以引起程序从当前执行点跳转到预设的中断处理程序(或称为异常处理程序)所在地址执行特定的中断处理程序。
    • SVC中断通常被用于以下情况:
      • 系统调用:当应用程序需要操作系统提供服务时,可以触发SVC中断,将控制权交给操作系统,然后执 行相应的系统调用服务例程,完成特定的操作。
      • 异常处理:当系统遇到一些异常情况时,比如非法指令、栈溢出等,可以触发SVC中断,将控制权交给异常处理程序,进行处理并决定如何恢复程序运行。
      • 任务切换:在一些实时操作系统中,当高优先级任务就绪时,可以通过触发SVC中断来切换任务,将当前任务挂起,转而执行高优先级任务。
      • 中断处理:在一些系统中,SVC中断也可以用于处理其他中断源的中断处理程序,以便在中断处理结束后返回到被中断的程序中继续执行。

    4.1.4 vPortSVCHandler()函数

    1. (9):产生系统调用,服务号0 表示SVC 中断,接下来将会执行SVC 中断服务函数
    2. SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致
    • 在启动文件的向量表中,SVC 的中断服务函数注册的名称是SVC_Handler,所以SVC 中断服务函数的名称我们应该写成SVC_Handler
    • 但是在FreeRTOS 中,官方版本写的是vPortSVCHandler()
    • 为了能够顺利的响应SVC 中断,我们有两个选择:
      • 改中断向量表中SVC 的注册的函数名称或者改FreeRTOS 中SVC 的中断服务名称
      • 这里,我们采取第二种方法,即在FreeRTOSConfig.h 中添加添加宏定义的方法来修改
    • 具体见代码清单,顺便把PendSV 和SysTick 的中断服务函数名也改成与向量表的一致
    0 /* FreeRTOSConfig.h 中 修改FreeRos 中SVC、PendSV 和SysTick 中断服务函数的名称 */
    1 #define xPortPendSVHandler PendSV_Handler
    2 #define xPortSysTickHandler SysTick_Handler
    3 #define vPortSVCHandler SVC_Handler
    
    • 1
    • 2
    • 3
    • 4
    1. vPortSVCHandler()函数开始真正启动第一个任务,不再返回,实现具体见代码清单
    0 /* port.c 中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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • (1):声明 外部变量 pxCurrentTCB,pxCurrentTCB 是一个在task.c 中定义的全局指针,用于指向当前正在运行或者即将要运行的任务的任务控制块。
    • (2):加载pxCurrentTCB 的地址到r3。
    • (3):加载pxCurrentTCB 到r3。
    • (4):加载pxCurrentTCB 指向的任务控制块到 r0,任务控制块的第一个成员就是栈顶指针,所以此时r0 等于栈顶指针
      • 一个刚刚被创建还没有运行过的任务的栈空间分布具体如图所示,即r0 等于图中pxTopOfStack
        在这里插入图片描述
    • (5):以r0 为基地址,将栈中向上增长的8 个字的内容加载到CPU 寄存器r4~r11,同时r0 也会跟着自增。
    • (6):将新的栈顶指针r0 更新到psp,任务执行的时候使用的堆栈指针是psp。此时psp 的指向具体见。
    • (7):将寄存器r0 清0。
    • (8):设置basepri 寄存器的值为0,即打开所有中断。basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽。
    • (9):当从SVC 中断服务退出前,通过向r14 寄存器最后4 位按位或上0x0D,使得硬件在退出时使用进程堆栈指针PSP 完成出栈操作并返回后进入任务模式、返回Thumb 状态。在SVC 中断服务里面,使用的是MSP 堆栈指针,是处在ARM 状态。
      当r14 为0xFFFFFFFX,执行是中断返回指令,cortext-m3 的做法,X的bit0 为1 表示返回thumb 状态,bit1 和bit2 分别表示返回后sp 用msp 还是psp、以及返回到特权模式还是用户模式
    • (10):异常返回,这个时候出栈使用的是PSP 指针,自动将栈中的剩下内容加载到CPU 寄存器: xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)同时PSP 的值也将更新,即指向任务栈的栈顶,具体指向见图。
      在这里插入图片描述

    4.2 任务切换

    1. 任务切换就是在就绪列表中寻找优先级最高就绪任务,然后去执行该任务
    • 但是目前我们还不支持优先级,仅实现两个任务轮流切换
    • 任务切换函数taskYIELD(),具体实现见代码清单。

    4.2.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 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • (1):portYIELD 的实现很简单,实际就是将PendSV 的悬起位置1,当没有其它中断运行的时候响应PendSV 中断,去执行我们写好的PendSV中断服务函数,在里面实现任务切换

    4.2.2 中断服务函数——xPortPendSVHandler()函数

    1. PendSV 中断服务函数是真正实现任务切换的地方,具体实现见代码清单。
    0 /* port.c 中真正实现任务切换函数实现 */
    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 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • (1):声明外部变量 pxCurrentTCB,pxCurrentTCB 是一个在task.c 中定义的全局指针,用于指向当前正在运行 或者即 将要运行的任务的任务控制块
    • (2):声明 外部函数 vTaskSwitchContext,等下会用到。
    • (3):当前栈需按照8 字节对齐,如果都是32 位的操作则4 个字节对齐即可。在Cortex-M中浮点运算是8 字节的。
    • (4):将PSP 的值存储到r0。当进入PendSVC Handler 时,上一个任务运行的环境即:
      • xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)这些CPU 寄存器的值会自动存储到任务的栈中,剩下的r4~r11 需要手动保存,同时PSP 会自动更新(在更新之前PSP 指向任务栈的栈顶),此时PSP 具体指向见图
        在这里插入图片描述
    • (5):加载pxCurrentTCB 的地址到r3。
    • (6):加载r3 指向的内容到r2,即r2 等于pxCurrentTCB。
    • (7):以r0 作为基址(指针先递减,再操作,STMDB 的DB 表示Decrease Befor),将CPU 寄存器r4~r11 的值存储到任务栈,同时更新r0 的值,此时r0 的指向具体见。
      在这里插入图片描述
    • (8):将r0 的值存储到r2 指向的内容,r2 等于pxCurrentTCB。具体为将r0 的值存储到上一个任务的栈顶指针pxTopOfStack,具体指向如上图的r0 指向一样。到此,上下文切换中上文保存就完成了。
    • (9):将R3 和R14 临时压入堆栈(在整个系统中,中断使用的是主堆栈,栈指针使用的是MSP)
      • 因为接下来要调用函数vTaskSwitchContext,调用函数时,返回地址自动保存到R14 中,所以一旦调用发生,R14 的值会被覆盖(PendSV 中断服务函数执行完毕后,返回的时候需要根据R14 的值来决定返回处理器模式还是任务模式,出栈时使用的是PSP 还是MSP),因此需要入栈保护
      • R3 保存的是当前正在运行的任务(准确来说是上文,因为接下来即将要切换到新的任务)的TCB 指针(pxCurrentTCB)地址,函数调用后pxCurrentTCB 的值会被更新,后面我们还需要通过R3 来操作pxCurrentTCB,但是运行函数vTaskSwitchContext 时不确定会不会使用R3 寄存器作为中间变量,所以为了保险起见,R3 也入栈保护起来。
    • (10):将configMAX_SYSCALL_INTERRUPT_PRIORITY 的值存储到r0,该宏在FreeRTOSConfig.h 中定义,用来配置中断屏蔽寄存器BASEPRI 的值,高四位有效。目前配置为191,因为是高四位有效,所以实际值等于11,即优先级高于或者等于11 的中断都将被屏蔽。在关中断方面,FreeRTOS 与其它的RTOS 关中断不同,而是操作BASEPRI 寄存器来预留一部分中断,并不像μC/OS 或者RT-Thread 那样直接操作PRIMASK 把所有中断都关闭掉(除了硬FAULT)。
    • (11):关中断,进入临界段,因为接下来要更新全局指针pxCurrentTCB的值。
    • (12):调用函数vTaskSwitchContext。该函数在task.c 中定义,作用只有一个,选择优先级最高的任务,然后更新pxCurrentTCB。目前我们还不支持优先级,则手动切换,不是任务1 就是任务2,该函数的具体实现见代码清单 7-24 vTaskSwitchContext()函数。
    • (13):退出临界段,开中断,直接往BASEPRI 写0。
    • (14):从主堆栈中恢复寄存器r3 和r14 的值,此时的sp 使用的是MSP。
    • (15):加载r3 指向的内容到r1。r3 存放的是pxCurrentTCB 的地址,即让r1 等于pxCurrentTCB。pxCurrentTCB 在上面的vTaskSwitchContext 函数中被更新,指向了下一个将要运行的任务的TCB。
    • (16):加载r1 指向的内容到r0,即下一个要运行的任务的栈顶指针
    • (17):以r0 作为基地址(先取值,再递增指针,LDMIA 的IA 表示Increase After),将下一个要运行的任务的任务栈的内容加载到CPU 寄存器r4~r11
    • (18):更新psp 的值,等下异常退出时,会以psp 作为基地址,将任务栈中剩下的内容自动加载到CPU 寄存器
    • (19):异常发生时,R14 中保存异常返回标志,包括返回后进入任务模式还是处理器模式、使用PSP 堆栈指针还是MSP 堆栈指针。此时的r14 等于0xfffffffd,最表示异常返回后进入任务模式,SP 以PSP 作为堆栈指针出栈,出栈完毕后PSP 指向任务栈的栈顶。当调用 bx r14 指令后,系统以PSP 作为SP 指针出栈,把接下来要运行的新任务的任务栈中剩下的内容加载到CPU 寄存器:R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和xPSR,从而切换到新的任务。

    4.2.3 vTaskSwitchContext()函数

    • (12):调用函数vTaskSwitchContext。该函数在task.c 中定义,作用只有一个,选择优先级最高的任务,然后更新pxCurrentTCB。目前我们还不支持优先级,则手动切换,不是任务1 就是任务2,该函数的具体实现见代码清单 7-24 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • (1):如果当前任务为任务1,则把下一个要运行的任务改为任务2。
    • (2):如果当前任务为任务2,则把下一个要运行的任务改为任务1。

    5 main函数

    1. 任务的创建,就绪列表的实现,调度器的实现均已经讲完,现在我们把全部的测试代
      码都放到main.c 里面,具体见代码清单。
    1 /**
    2 ***********************************************************************
    3 * @file main.c
    4 * @author fire
    5 * @version V1.0
    6 * @date 2018-xx-xx
    7 * @brief 《FreeRTOS 内核实现与应用开发实战指南》书籍例程
    8 * 任务的定义与任务切换的实现
    9 ***********************************************************************
    10 * @attention
    11 *
    12 * 实验平台:野火 STM32 系列 开发板
    13 *
    14 * 官网 :www.embedfire.com
    15 * 论坛 :http://www.firebbs.cn
    16 * 淘宝 :https://fire-stm32.taobao.com
    17 *
    18 ***********************************************************************
    19 */
    20
    21 /*
    22 *************************************************************************
    23 * 包含的头文件
    24 *************************************************************************
    25 */
    26 #include "FreeRTOS.h"
    27 #include "task.h"
    28
    29 /*
    30 *************************************************************************
    31 * 全局变量
    32 *************************************************************************
    33 */
    34 portCHAR flag1;
    35 portCHAR flag2;
    36
    37 extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
    38
    39
    40 /*
    41 *************************************************************************
    42 * 任务控制块 & STACK
    43 *************************************************************************
    44 */
    45 TaskHandle_t Task1_Handle;
    46 #define TASK1_STACK_SIZE 128
    47 StackType_t Task1Stack[TASK1_STACK_SIZE];
    48 TCB_t Task1TCB;
    49
    50 TaskHandle_t Task2_Handle;
    51 #define TASK2_STACK_SIZE 128
    52 StackType_t Task2Stack[TASK2_STACK_SIZE];
    53 TCB_t Task2TCB;
    54
    55
    56 /*
    57 *************************************************************************
    58 * 函数声明
    59 *************************************************************************
    60 */
    61 void delay (uint32_t count);
    62 void Task1_Entry( void *p_arg );
    63 void Task2_Entry( void *p_arg );
    64
    65 /*
    66 ************************************************************************
    67 * main 函数
    68 ************************************************************************
    69 */
    70 /*
    71 * 注意事项:1、该工程使用软件仿真,debug 需选择 Ude Simulator
    72 * 2、在Target 选项卡里面把晶振Xtal(Mhz)的值改为25,默认是12,
    73 * 改成25 是为了跟system_ARMCM3.c 中定义的__SYSTEM_CLOCK 相同,
    74 * 确保仿真的时候时钟一致
    75 */
    76 int main(void)
    77 {
    78 		/* 硬件初始化 */
    79 		/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
    80
    81 		/* 初始化与任务相关的列表,如就绪列表 */
    82 		prvInitialiseTaskLists();
    83
    84 		/* 创建任务 */
    85 		Task1_Handle =
    86 		xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */
    87 											(char *)"Task1", /* 任务名称,字符串形式 */
    88 											(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */
    89 											(void *) NULL, /* 任务形参 */
    90 											(StackType_t *)Task1Stack, /* 任务栈起始地址 */
    91 											(TCB_t *)&Task1TCB ); /* 任务控制块 */
    92 		/* 将任务添加到就绪列表 */
    93 		vListInsertEnd( &( pxReadyTasksLists[1] ),
    94 									&( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
    95
    96 		Task2_Handle =
    97 		xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */
    98 											(char *)"Task2", /* 任务名称,字符串形式 */
    99 											(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */
    100 										(void *) NULL, /* 任务形参 */
    101 										(StackType_t *)Task2Stack, /* 任务栈起始地址 */
    102 										(TCB_t *)&Task2TCB ); /* 任务控制块 */
    103 		/* 将任务添加到就绪列表 */
    104 		vListInsertEnd( &( pxReadyTasksLists[2] ),
    105 									&( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
    106
    107 		/* 启动调度器,开始多任务调度,启动成功则不返回 */
    108 		vTaskStartScheduler();
    109
    110 		for (;;)
    111 		{
    112 				/* 系统启动成功不会到达这里 */
    113 		}
    114 }
    115
    116 /*
    117 ***********************************************************************
    118 * 函数实现
    119 ***********************************************************************
    120 */
    121 /* 软件延时 */
    122 void delay (uint32_t count)
    123 {
    124 		for (; count!=0; count--);
    125 }
    126 /* 任务1 */
    127 void Task1_Entry( void *p_arg )
    128 {
    129 		for ( ;; )
    130 		{
    131 				flag1 = 1;
    132 				delay( 100 );
    133 				flag1 = 0;
    134 				delay( 100 );
    135
    136 				/* 任务切换,这里是手动切换 */
    137 				taskYIELD(); (注意)
    138 		}
    139 }
    140
    141 /* 任务2 */
    142 void Task2_Entry( void *p_arg )
    143 {
    144 		for ( ;; )
    145 		{
    146 				flag2 = 1;
    147 				delay( 100 );
    148 				flag2 = 0;
    149 				delay( 100 );
    150
    151 				/* 任务切换,这里是手动切换 */
    152 				taskYIELD(); (注意)
    153 		}
    154 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 每个局部的代码均已经讲解过,剩下的看代码注释即可。
    • (注意):因为目前还不支持优先级,每个任务执行完毕之后都主动调用任务切换函数taskYIELD()来实现任务的切换。

    6 实验现象

    • 本章代码讲解完毕,接下来是软件调试仿真,具体过程见图
      在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
    • 本章讲解完毕。但是,只是把本章的内容看完,然后再仿真看看波形是远远不够的
    • 应该是把当前任务控制块指针pxCurrentTCB、就绪列表pxReadyTaskLists、每个任务的控制块和任务的栈这些变量统统添加到观察窗口,然后单步执行程序,看看这些变量是怎么变化的
    • 特别是任务切换时,CPU 寄存器、任务栈和PSP 这些是怎么变化的,让机器执行代码的过程在自己的脑子里面过一遍。
    • 下图就是我在仿真调试时的观察窗口。
      在这里插入图片描述

    7 本章涉及到的汇编指令

    • 本章中有些函数是用汇编编写的,涉及到的ARM 汇编指令具体参考表格

    在这里插入图片描述在这里插入图片描述

  • 相关阅读:
    Spring 6面向切面编程aop详解
    蓝桥杯 Java 括号序列
    使用 Monaco Editor 开发 SQL 编辑器
    现在完成进行时习题
    tomcat+idea--如何在idea上发布项目
    BUUCTF misc 专题(121)[watevrCTF 2019]Evil Cuteness
    java基础之重载与重写
    Java8:Effectively final
    JAVA堆中的对象结构
    【C语言练习】C语言如何操作内存(重中之重!!!)
  • 原文地址:https://blog.csdn.net/weixin_45029825/article/details/133759839