3.2 创建任务
3.2.1 定义任务栈
先回想一下,在一个裸机系统中,如果有全局变量,有子函数调用,有中断发生,那么系统在运行时,全局变量放在哪里?子函数调用时,局部变量放在哪里?中断发生时,函数返回地址放在哪里?如果只是单纯的裸机编程,可以不考虑上述问题,但是如果要写一个RTOS,就必须明确这些参数是如何存储的。在裸机系统中,它们统统放在栈中。栈是单片机RAM中一段连续的内存空间,其大小由启动文件中的代码配置,具体参见代码清单3-3,最后由C库函数__main进行初始化。
代码清单3-3 裸机系统中的栈分配
1 Stack_Size EQU 0x00000400 2 3 AREA STACK, NOINIT, READWRITE, ALIGN=3 4 Stack_Mem SPACE Stack_Size 5 __initial_sp
但是,在多任务系统中,每个任务都是独立的、互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组。这些任务栈也存在于RAM中,能够使用的最大的栈尺寸也是由代码清单3-3中的Stack_Size决定的。只是多任务系统中任务的栈就是在一个统一的栈空间里面分配好一个个独立的“房间”,每个任务只能使用各自的房间,而需要在裸机系统中使用栈时,则可以天马行空,在栈里寻找任意空闲空间加以使用。
本章我们要实现两个变量按照一定的频率轮流翻转,需要用两个任务来实现,那么就需要定义两个任务栈,具体参见代码清单3-4。在多任务系统中,有多少个任务就需要定义多少个任务栈。
代码清单3-4 定义任务栈
1 #define TASK1_STK_SIZE 128 (1) 2 #define TASK2_STK_SIZE 128 3 4 static CPU_STK Task1Stk[TASK1_STK_SIZE];(2) 5 static CPU_STK Task2Stk[TASK2_STK_SIZE];
代码清单3-4(1):任务栈的大小由宏定义控制,在μC/OS-III中,空闲任务的栈最小应该大于128,这里的任务栈也暂且配置为128。
代码清单3-4(2):任务栈其实就是一个预先定义好的全局数据,此处数据类型为CPU_STK。在μC/OS-III中,凡是涉及数据类型的地方,μC/OS-III都会将标准的C数据类型用typedef重新设置一个类型名,命名方式则采用见名知义的方式且使用大写字母。凡是与CPU类型相关的数据类型统一在cpu.h中定义,与操作系统相关的数据类型则在os_type.h中定义。CPU_STK就是与CPU相关的数据类型,具体参见代码清单3-5。首次使用cpu.h,需要自行在C-CPU文件夹中新建并添加到工程的C/CPU组中。代码清单3-5中除了CPU_STK外,其他数据类型重定义是本章后面内容中需要用到的,这里统一给出,后面将不再赘述。
代码清单3-5 cpu.h中的数据类型
1 #ifndef CPU_H 2 #define CPU_H 3 4 typedef unsigned short CPU_INT16U; 5 typedef unsigned int CPU_INT32U; 6 typedef unsigned char CPU_INT08U; 7 8 typedef CPU_INT32U CPU_ADDR; 9 10 /* 栈数据类型重定义 */ 11 typedef CPU_INT32U CPU_STK; 12 typedef CPU_ADDR CPU_STK_SIZE; 13 14 typedef volatile CPU_INT32U CPU_REG32; 15 16 #endif/* CPU_H */
3.2.2 定义任务函数
任务是一个独立的函数,函数主体无限循环且不能返回。本章定义的两个任务具体参见代码清单3-6。
代码清单3-6 任务函数
1 /* flag 必须定义成全局变量,才能添加到逻辑分析仪中观察波形 2 ** 在逻辑分析仪中要设置为Bit模式才能看到波形,不能使用默认的模拟量 3 */ 4 uint32_t flag1;(1) 5 uint32_t flag2; 6 7 8 /* 任务1 */ 9 void Task1( void *p_arg )(2) 10 { 11 for ( ;; ) { 12 flag1 = 1; 13 delay( 100 ); 14 flag1 = 0; 15 delay( 100 ); 16 } 17 } 18 19 /* 任务2 */ 20 void Task2( void *p_arg )(3) 21 { 22 for ( ;; ) { 23 flag2 = 1; 24 delay( 100 ); 25 flag2 = 0; 26 delay( 100 ); 27 } 28 }
代码清单3-6(1):如果要在KEIL逻辑分析仪中观察波形的变量,则需要将其定义成全局变量,且要以Bit模式观察,不能使用默认的模拟量。
代码清单3-6(2)(3):正如介绍的那样,任务是一个独立的、无限循环且不能返回的函数。
3.2.3 定义任务控制块
在裸机系统中,程序的主体是CPU按照顺序执行的,而在多任务系统中,任务的执行是由系统调度的。系统为了顺利地调度任务,为每个任务都额外定义了一个任务控制块(Task Control Block,TCB),这个任务控制块相当于任务的身份证,里面存有任务的所有信息,比如任务栈、任务名称、任务形参等。有了TCB,以后系统对任务的全部操作都可以通过这个TCB来实现。TCB是一个新的数据类型,在os.h头文件中声明(第一次使用os.h时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),有关TCB具体的声明参见代码清单3-7,使用它可以为每个任务都定义一个TCB实体。
代码清单3-7 任务控制块类型声明
1 /* 任务控制块重定义 */ 2 typedef struct os_tcb OS_TCB;(1) 3 4 /* 任务控制块数据类型声明 */ 5 struct os_tcb {(2) 6 CPU_STK *StkPtr; 7 CPU_STK_SIZE StkSize; 8 };
代码清单3-7(1):在μC/OS-III中,所有的数据类型都会被重新设置一个名称且用大写字母表示。
代码清单3-7(2):目前TCB里面的成员还比较少,只有栈指针和栈大小。为了以后操作方便,我们把栈指针作为TCB的第一个成员。
此处,在app.c文件中为两个任务定义的TCB具体参见代码清单3-8。
代码清单3-8 定义TCB
1 static OS_TCB Task1TCB; 2 static OS_TCB Task2TCB;
3.2.4 实现任务创建函数
任务栈、任务的函数实体、任务的TCB最终需要联系起来才能由系统进行统一调度,这个联系的工作由任务创建函数OSTaskCreate()实现,该函数在os_task.c中定义(第一次使用os_task.c时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),所有与任务相关的函数都在这个文件中定义。OSTaskCreate()函数的实现具体参见代码清单3-9。
代码清单3-9 OSTaskCreate()函数
1 void OSTaskCreate (OS_TCB *p_tcb,(1) 2 OS_TASK_PTR p_task,(2) 3 void *p_arg,(3) 4 CPU_STK *p_stk_base, (4) 5 CPU_STK_SIZE stk_size, (5) 6 OS_ERR *p_err) (6) 7 { 8 CPU_STK *p_sp; 9 10 p_sp = OSTaskStkInit (p_task,(7) 11 p_arg, 12 p_stk_base, 13 stk_size); 14 p_tcb->StkPtr = p_sp;(8) 15 p_tcb->StkSize = stk_size;(9) 16 17 *p_err = OS_ERR_NONE;(10) 18 }
代码清单3-9:OSTaskCreate()函数遵循μC/OS-III中的函数命名规则,以OS开头,表示这是一个外部函数,可以由用户调用;以OS_开头的函数则表示内部函数,只能在μC/OS-III内部使用。紧接着是文件名,表示该函数放在哪个文件中,最后是函数功能名称。
代码清单3-9(1):p_tcb是任务控制块指针。
代码清单3-9(2):p_task是任务名,类型为OS_TASK_PTR,原型声明在os.h文件中,具体参见代码清单3-10。
代码清单3-10 OS_TASK_PTR原型声明
1 typedef void (*OS_TASK_PTR)(void *p_arg);
代码清单3-9(3):p_arg是任务形参,用于传递任务参数。
代码清单3-9(4):p_stk_base用于指向任务栈的起始地址。
代码清单3-9(5):stk_size表示任务栈的大小。
代码清单3-9(6):p_err用于存储错误码。μC/OS-III中为函数的返回值预先定义了很多错误码,通过这些错误码可以知道函数出现错误的原因。为了方便,我们现在把μC/OS-III中所有的错误码都给出来。错误码是枚举类型的数据,在os.h中定义,具体参见代码清单3-11。
代码清单3-11 错误码枚举定义
1 typedefenum os_err { 2 OS_ERR_NONE = 0u, 3 4 OS_ERR_A = 10000u, 5 OS_ERR_ACCEPT_ISR = 10001u, 6 7 OS_ERR_B = 11000u, 8 9 OS_ERR_C = 12000u, 10 OS_ERR_CREATE_ISR = 12001u, 11 12 /* 限于篇幅,此处将中间部分删除,具体内容可查看本章配套的例程 */ 199 200 OS_ERR_X = 33000u, 201 202 OS_ERR_Y = 34000u, 203 OS_ERR_YIELD_ISR = 34001u, 204 205 OS_ERR_Z = 35000u 206 } OS_ERR;
代码清单3-9(7):OSTaskStkInit()是任务栈初始化函数。当任务第一次运行时,加载到CPU寄存器的参数就放在任务栈中,在任务创建时,预先初始化好栈。OSTaskStkInit()函数在os_cpu_c.c中定义(第一次使用os_cpu_c.c时需要自行在文件夹C-CPU中新建并添加到工程的C/CPU组),具体参见代码清单3-12。
代码清单3-12 OSTaskStkInit()函数
1 CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,(1) 2 void *p_arg,(2) 3 CPU_STK *p_stk_base,(3) 4 CPU_STK_SIZE stk_size)(4) 5 { 6 CPU_STK *p_stk; 7 8 p_stk = &p_stk_base[stk_size];(5) 9 /* 异常发生时自动保存的寄存器 */(6) 10 *--p_stk = (CPU_STK)0x01000000u; /* xPSR的位24必须置1 */ 11 *--p_stk = (CPU_STK)p_task; /* R15(PC)任务的入口地址*/ 12 *--p_stk = (CPU_STK)0x14141414u; /* R14 (LR) */ 13 *--p_stk = (CPU_STK)0x12121212u; /* R12 */ 14 *--p_stk = (CPU_STK)0x03030303u; /* R3 */ 15 *--p_stk = (CPU_STK)0x02020202u; /* R2 */ 16 *--p_stk = (CPU_STK)0x01010101u; /* R1 */ 17 *--p_stk = (CPU_STK)p_arg; /* R0 : 任务形参 */ 18 /* 异常发生时需要手动保存的寄存器 */(7) 19 *--p_stk = (CPU_STK)0x11111111u; /* R11 */ 20 *--p_stk = (CPU_STK)0x10101010u; /* R10 */ 21 *--p_stk = (CPU_STK)0x09090909u; /* R9 */ 22 *--p_stk = (CPU_STK)0x08080808u; /* R8 */ 23 *--p_stk = (CPU_STK)0x07070707u; /* R7 */ 24 *--p_stk = (CPU_STK)0x06060606u; /* R6 */ 25 *--p_stk = (CPU_STK)0x05050505u; /* R5 */ 26 *--p_stk = (CPU_STK)0x04040404u; /* R4 */ 27 28 return (p_stk);(8) 29 }
代码清单3-12(1):p_task是任务名,表示任务的入口地址,在任务切换时,需要加载到R15,即PC寄存器,这样CPU就可以找到要运行的任务。
代码清单3-12(2):p_arg是任务的形参,用于传递参数,在任务切换时,需要加载到寄存器R0。R0寄存器通常用来传递参数。
代码清单3-12(3):p_stk_base表示任务栈的起始地址。
代码清单3-12(4):stk_size表示任务栈的大小,数据类型为CPU_STK_SIZE,在Cortex-M3内核的处理器中等于4字节,即一个字。
代码清单3-12(5):获取任务栈的栈顶地址,ARMCM3处理器的栈是由高地址向低地址生长的,所以在初始化栈之前,要获取栈顶地址,然后将栈地址逐一递减即可。
代码清单3-12(6):任务第一次运行时,加载到CPU寄存器的环境参数要预先初始化好。初始化的顺序固定,首先是异常发生时自动保存的8个寄存器,即xPSR、R15、R14、R12、R3、R2、R1和R0。其中xPSR寄存器的位24必须是1,R15 PC指针必须存储任务的入口地址,R0必须是任务形参。对于R14、R12、R3、R2和R1,为了调试方便,应填入与寄存器号相对应的十六进制数。
代码清单3-12(7):剩下的是8个需要手动加载到CPU寄存器的参数,为了调试方便,应填入与寄存器号相对应的十六进制数。
代码清单3-12(8):返回栈指针p_stk,这时p_stk指向剩余栈的栈顶。
代码清单3-9(8):将剩余栈的栈顶指针p_sp保存到TCB的第一个成员StkPtr中。
代码清单3-9(9):将任务栈的大小保存到TCB的成员StkSize中。
代码清单3-9(10):函数执行到这里表示没有错误,即OS_ERR_NONE。
任务创建好之后,需要把任务添加到就绪列表,表示任务已经就绪,系统随时可以调度。将任务添加到就绪列表的代码具体参见代码清单3-13。
代码清单3-13 将任务添加到就绪列表
1 /* 将任务添加到就绪列表 */ 2 OSRdyList[0].HeadPtr = &Task1TCB;(1) 3 OSRdyList[1].HeadPtr = &Task2TCB;(2)
代码清单3-13(1)(2):把TCB指针放到OSRdyList数组中。OSRdyList是一个类型为OS_RDY_LIST的全局变量,在os.h中定义,具体参见代码清单3-14。
代码清单3-14 全局变量OSRdyList定义
(3) (2) (1) 1 OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX];
代码清单3-14(1):OS_CFG_PRIO_MAX是一个定义,表示这个系统支持多少个优先级(刚开始暂时不支持多个优先级,后面的章节中会支持),目前仅用来表示这个就绪列表可以存储多少个TCB指针。具体的宏在os_cfg.h中定义(第一次使用os_cfg.h时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),具体参见代码清单3-15。
代码清单3-15 OS_CFG_PRIO_MAX宏定义
1 #ifndef OS_CFG_H 2 #define OS_CFG_H 3 4 /* 支持最大的优先级 */ 5 #define OS_CFG_PRIO_MAX 32u 6 7 8 #endif/* OS_CFG_H */
代码清单3-14(2):OS_RDY_LIST是就绪列表的数据类型,在os.h中声明,具体参见代码清单3-16。
代码清单3-16 OS_RDY_LIST数据类型声明
1 typedefstruct os_rdy_list OS_RDY_LIST;(1) 2 3 struct os_rdy_list {(2) 4 OS_TCB *HeadPtr; 5 OS_TCB *TailPtr; 6 };
代码清单3-16(1):μC/OS-III中会为每个数据类型重新设置一个字母大写的名称。
代码清单3-16(2):OS_RDY_LIST中目前只有两个TCB类型的指针,一个是头指针,一个是尾指针。本章实验只用到头指针,用来指向任务的TCB。只有当后面讲到同一个优先级支持多个任务时才需要使用头尾指针来将TCB串成一个双向链表。
代码清单3-14(3):OS_EXT是一个在os.h中定义的宏,具体参见代码清单3-17。
代码清单3-17 OS_EXT宏定义
1 #ifdef OS_GLOBALS 2 #define OS_EXT 3 #else 4 #define OS_EXT extern 5 #endif
这段代码的意思是,如果没有定义OS_GLOBALS这个宏,那么OS_EXT就为空,否则为extern。
在μC/OS-III中,需要使用很多全局变量,这些全局变量都在os.h头文件中定义,但是os.h会被包含进很多文件中,那么编译时os.h中定义的全局变量就会出现重复定义的情况,而我们只想将os.h中的全局变量只定义一次,涉及包含os.h头文件时只是声明。有人提出可以加extern,那么该如何加?
通常采取的做法是在C文件中定义全局变量,然后在头文件中需要使用全局变量的位置添加extern声明,但是μC/OS-III中文件非常多,这种方法可行,但不现实,所以就有了在os.h头文件中定义全局变量,然后在os.h文件的开头加上代码清单3-17中宏定义的方法。但是这样还没有成功,μC/OS-III另外新建了一个os_var.c文件(第一次使用os_var.c时需要自行在文件夹μC/OS-III\Source中新建并添加到工程的μC/OS-III Source组),其中包含了os.h,且只在这个文件中定义OS_GLOBALS这个宏,具体参见代码清单3-18。
代码清单3-18 os_var.c文件内容
1 #define OS_GLOBALS 2 3 #include "os.h"
经过这样的处理之后,在编译整个工程时,只有var.c中os.h的OS_EXT才会被替换为空,即变量的定义,其他包含os.h的文件因为没有定义OS_GLOBALS这个宏,所以OS_EXT会被替换成extern,即变成了变量的声明。这样就实现了在头文件中定义变量。
在μC/OS-III中,将任务添加到就绪列表其实是在OSTaskCreate()函数中完成的。每当任务创建好就把任务添加到就绪列表,表示任务已经就绪,只是目前这里的就绪列表的实现还比较简单,不支持优先级,也不支持双向链表,只是简单地将TCB放到就绪列表的数组中。第8章将专门讲解就绪列表,等完善就绪列表之后,再把这部分的操作放回OSTaskCreate()函数中。