写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。
标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。
点击此处进入学习日记的总目录
2024.03.02
- 九、UCOSIII:创建任务
- 1、任务的定义
- 2、定义任务栈
- 3、定义任务函数
- 4、定义任务控制块TCB
- 5、任务创建函数
- 6、任务栈初始化函数
- 7、将任务添加到就绪列表
- 8、全局变量的声明方法
九、UCOSIII:创建任务
1、任务的定义
在裸机系统中,系统的主体就是main函数里面顺序执行的无限循环,这个无限循环里面CPU按照顺序完成各种事情。
在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务。
void task_entry (void *parg)
{/* 任务主体,无限循环且不能返回 */for (;;){/* 任务主体代码 */}
}
2、定义任务栈
/*
************************************************************************************************************************
* TCB & STACK & 任务声明
************************************************************************************************************************
*/
#define TASK1_STK_SIZE 20
#define TASK2_STK_SIZE 20static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];static OS_TCB Task1TCB;
static OS_TCB Task2TCB;void Task1( void *p_arg );
void Task2( void *p_arg );
在多任务系统中,有多少个任务就需要定义多少个任务栈。
任务栈的大小由宏定义控制,在μC/OS-III中, 空闲任务的栈最小应该大于128,那么我们这里的任务的栈也暂且配置为128。
(但是代码里任务栈数量为20)
任务栈其实就是一个预先定义好的全局数据,数据类型为CPU_STK。
在μC/OS-III中,凡是涉及数据类型的地方, μC/OS-II都会将标准的C数据类型用typedef重新取一个类型名,命名方式则采用见名之义的方式命名且统统大写。
3、定义任务函数
任务是一个独立的函数,函数主体无限循环且不能返回。
任务的代码示意如下:
/* flag 必须定义成全局变量才能添加到逻辑分析仪里面观察波形
** 在逻辑分析仪中要设置以 bit 的模式才能看到波形,不能用默认的模拟量
*/
uint32_t flag1;
uint32_t flag2;/* 任务1 */
void Task1( void *p_arg )(2)
{
for ( ;; ) {flag1 = 1;delay( 100 );flag1 = 0;delay( 100 );}//任务是一个独立的、无限循环且不能返回的函数。
}/* 任务2 */
void Task2( void *p_arg )(3)
{
for ( ;; ) {flag2 = 1;delay( 100 );flag2 = 0;delay( 100 );}
}
4、定义任务控制块TCB
/*
************************************************************************************************************************
************************************************************************************************************************
* 数据类型
************************************************************************************************************************
************************************************************************************************************************
*/typedef struct os_rdy_list OS_RDY_LIST;
typedef void (*OS_TASK_PTR)(void *p_arg);
typedef struct os_tcb OS_TCB;
/*
------------------------------------------------------------------------------------------------------------------------
* 就绪列表
------------------------------------------------------------------------------------------------------------------------
*/
struct os_rdy_list
{OS_TCB *HeadPtr;OS_TCB *TailPtr;
};
在裸机系统中,程序的主体是CPU按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。
系统为了顺利的调度任务, 为每个任务都额外定义了一个任务控制块TCB(Task ControlBlock),这个任务控制块就相当于任务的身份证, 里面存有任务的所有信息,比如任务的栈,任务名称,任务的形参等。
有了这个任务控制块之后, 以后系统对任务的全部操作都可以通过这个TCB来实现。
TCB是一个新的数据类型, 在os.h这个头文件中声明,使用它可以为每个任务都定义一个TCB实体。
目前TCB里面的成员还比较少,只有栈指针和栈大小。其中为了以后操作方便,我们把栈指针作为TCB的第一个成员。
5、任务创建函数
#include "os.h"void OSTaskCreate (OS_TCB *p_tcb, OS_TASK_PTR p_task, void *p_arg,CPU_STK *p_stk_base,CPU_STK_SIZE stk_size,OS_ERR *p_err)
{CPU_STK *p_sp;p_sp = OSTaskStkInit (p_task,p_arg,p_stk_base,stk_size);p_tcb->StkPtr = p_sp;p_tcb->StkSize = stk_size;*p_err = OS_ERR_NONE;
}
任务的栈,任务的函数实体,任务的TCB最终需要联系起来才能由系统进行统一调度。
那么这个联系的工作就由任务创建函数 OSTaskCreate来实现,该函数在os_task.c中定义,所有跟任务相关的函数都在这个文件定义。
- OSTaskCreate函数遵循μC/OS-III中的函数命名规则,以大小的OS开头,表示这是一个外部函数,可以由用户调用,以OS_开头的函数表示内部函数,只能由μC/OS-III内部使用。紧接着是文件名,表示该函数放在哪个文件,最后是函数功能名称。
- p_tcb是任务控制块指针。
- p_task 是任务函数名,类型为OS_TASK_PTR,原型声明在os.h中
- p_arg是任务形参,用于传递任务参数。 p_stk_base 用于指向任务栈的起始地址。 stk_size 表示任务栈的大小。
- p_err 用于存错误码,μC/OS-III中为函数的返回值预先定义了很多错误码,
错误码是枚举类型的数据,在os.h中定义 OSTaskStkInit()是任务栈初始化函数。 当任务第一次运行的时候, 加载到CPU寄存器的参数就放在任务栈里面,在任务创建的时候,预先初始化好栈。 - OSTaskStkInit()函数在os_cpu_c.c中定义
- p_tcb->StkPtr = p_sp;
将剩余栈的栈顶指针p_sp保存到任务控制块TCB的第一个成员StkPtr中。 - p_tcb->StkSize = stk_size;
将任务栈的大小保存到任务控制块TCB的成员StkSize中。 - *p_err = OS_ERR_NONE;
函数执行到这里表示没有错误,即OS_ERR_NONE。
6、任务栈初始化函数
#include "os.h"/* 任务堆栈初始化 */
CPU_STK *OSTaskStkInit (OS_TASK_PTR p_task,void *p_arg,CPU_STK *p_stk_base,CPU_STK_SIZE stk_size)
{CPU_STK *p_stk;p_stk = &p_stk_base[stk_size];/* 异常发生时自动保存的寄存器 */*--p_stk = (CPU_STK)0x01000000u; /* xPSR的bit24必须置1 */*--p_stk = (CPU_STK)p_task; /* 任务的入口地址 */*--p_stk = (CPU_STK)0x14141414u; /* R14 (LR) */*--p_stk = (CPU_STK)0x12121212u; /* R12 */*--p_stk = (CPU_STK)0x03030303u; /* R3 */*--p_stk = (CPU_STK)0x02020202u; /* R2 */*--p_stk = (CPU_STK)0x01010101u; /* R1 */*--p_stk = (CPU_STK)p_arg; /* R0 : 任务形参 *//* 异常发生时需手动保存的寄存器 */*--p_stk = (CPU_STK)0x11111111u; /* R11 */*--p_stk = (CPU_STK)0x10101010u; /* R10 */*--p_stk = (CPU_STK)0x09090909u; /* R9 */*--p_stk = (CPU_STK)0x08080808u; /* R8 */*--p_stk = (CPU_STK)0x07070707u; /* R7 */*--p_stk = (CPU_STK)0x06060606u; /* R6 */*--p_stk = (CPU_STK)0x05050505u; /* R5 */*--p_stk = (CPU_STK)0x04040404u; /* R4 */return (p_stk);
}
-
p_task是任务名,指示着任务的入口地址,在任务切换的时候,需要加载到R15, 即PC寄存器,这样CPU就可以找到要运行的任务。
-
p_arg 是任务的形参,用于传递参数,在任务切换的时候,需要加载到寄存器R0。R0寄存器通常用来传递参数。
-
p_stk_base 表示任务栈的起始地址。
-
stk_size 表示任务栈的大小, 数据类型为CPU_STK_SIZE,在Cortex-M3内核的处理器中等于4个字节,即一个字。
-
p_stk = &p_stk_base[stk_size];
获取任务栈的栈顶地址,ARMCM3处理器的栈是由高地址向低地址生长的。所以初始化栈之前, 要获取到栈顶地址,然后栈地址逐一递减即可。 -
/* 异常发生时自动保存的寄存器 */ 下面的八行
任务第一次运行的时候,加载到CPU寄存器的环境参数我们要预先初始化好。初始化的顺序固定, 首先是异常发生时自动保存的8个寄存器,即xPSR、R15、R14、R12、R3、R2、R1和R0。其中xPSR寄存器的位24必须是1, R15PC指针必须存的是任务的入口地址,R0必须是任务形参,剩下的R14、R12、R3、R2和R1为了调试方便,填入与寄存器号相对应的16进制数。 -
/* 异常发生时需手动保存的寄存器 */ 下面的八行
剩下的是8个需要手动加载到CPU寄存器的参数,为了调试方便填入与寄存器号相对应的16进制数。 -
返回栈指针p_stk,这个时候p_stk指向剩余栈的栈顶。
7、将任务添加到就绪列表
任务创建好之后,我们需要把任务添加到一个叫就绪列表的数组里面,表示任务已经就绪,系统随时可以调度。
int main(void)
{ OS_ERR err;/* 初始化相关的全局变量 */OSInit(&err);/* 创建任务 */OSTaskCreate ((OS_TCB*) &Task1TCB, (OS_TASK_PTR ) Task1, (void *) 0,(CPU_STK*) &Task1Stk[0],(CPU_STK_SIZE) TASK1_STK_SIZE,(OS_ERR *) &err);OSTaskCreate ((OS_TCB*) &Task2TCB, (OS_TASK_PTR ) Task2, (void *) 0,(CPU_STK*) &Task2Stk[0],(CPU_STK_SIZE) TASK2_STK_SIZE,(OS_ERR *) &err);/* 将任务加入到就绪列表 */OSRdyList[0].HeadPtr = &Task1TCB;OSRdyList[1].HeadPtr = &Task2TCB;/* 启动OS,将不再返回 */ OSStart(&err);
}
把任务TCB指针放到OSRDYList数组里面。
OSRdyList定义如下
OS_CFG_PRIO_MAX是一个定义,表示这个系统支持多少个优先级(刚开始暂时不支持多个优先级,往后章节会支持), 目前这里仅用 来表示这个就绪列表可以存多少个任务的TCB指针。
具体的宏在os_cfg.h中定义
OSRdyList是一个类型为OS_RDY_LIST的全局变量, 在os.h中定义
μC/OS-III中中会为每个数据类型重新取一个大写的名字。
OS_RDY_LIST里面目前暂时只有两个TCB类型的指针,一个是头指针,一个是尾指针。
需要使用头尾指针来将TCB串成一个双向链表。
8、全局变量的声明方法
在μC/OS-III中,需要使用很多全局变量,这些全局变量都在os.h这个头文件中定义,但是os.h会被包含进很多的文件中, 那么编译的时候,os.h里面定义的全局变量就会出现重复定义的情况,而我们要的只是os.h里面定义的全局变量只定义一次, 其他包含os.h头文件的时候只是声明。
通常我们的做法都是在C文件里面定义全局变量,然后在头文件里面加extern声明,哪里需要使用就在哪里加extern声明。
但是μC/OS-III中,文件非常多,这种方法可行,但不现实。所以就有了现在在os.h头文件中定义全局变量, 然后在os.h文件的开头加上 代码清单:任务-17 的宏定义的方法。
但是到了这里还没成功, μC/OS-III再另外新建了一个os_var.c的文件,在里面包含os.h, 且只在这个文件里面定义OS_GLOBALS这个宏
如果没有定义OS_GLOBALS这个宏,那么OS_EXT就为空,否则就为extern。
经过这样处理之后,在编译整个工程的时候,只有var.c里面的os.h的OS_EXT才会被替换为空,即变量的定义, 其他包含os.h的文件因为没有定义OS_GLOBALS这个宏,则OS_EXT会被替换成extern,即变成了变量的声明。 这样就实现了在头文件中定义变量。