0-1write MC/OS __Basics1
2021-07-16 22:23:32 星期五 |
一、裸机系统与多任务系统
裸机系统
- 轮询系统
在裸机编程时先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情,实时性差。 - 前后台系统
在轮询系统的基础上加入了中断。外部事件的响应在中断里完成,事件的处理仍然回到轮询系统中完成,若要处理的事件很简短,则可在中断服务程序中处理。
中断称为前台,main函数里的无限循环称为后台。
事件的响应和处理分开了,但事件的处理还是在后台中顺序执行的,相比轮询系统,前后台系统确保了事件不会丢失,加上中断的具有可嵌套的功能,这大大提高了程序的实时响应能力。
多任务系统
相比前后台系统,多任务系统的事件响应也是在中断中完成的,但是事件的处理是在任务中完成的。在多任务系统中,任务跟中断一样,也具有优先级,优先级高的任务会被优先执行。
当一个紧急事件在中断被标记之后,如果事件对应的任务优先级足够高,就会立马得到响应。相比前后台系统,多任务系统的实时性又被提高了。
什么是任务?
相比前后台系统中后台顺序执行的程序主体,在多任务系统中,根据程序的功能,把程序主体分割成一个个独立的,无限循环且不能返回的小程序,这个小程序被称为任务。
任务由操作系统调度管理。
轮询、前后台、多任务系统三者的区别
2021-07-20 01:01:16 星期二 |
二、任务的定义与任务切换的实现
了解如何创建任务,掌握任务是如何切换的(任务的切换是由汇编代码来完成的)
1、创建任务
1.1、定义任务栈
系统在运行的时候,全局变量、子函数调用时的局部变量、中断发生时的函数返回地址等环境参数,它们都是如何存储的?
在裸机系统中,放在栈中(栈是单片机RAM里一段连续的内存空间),栈的大小由启动文件里面的代码配置,最后由C库函数_main进行初始化。
裸机系统中需要使用栈的时候可以随便在栈里面找个空闲的空间使用,但多任务系统却不行。
在多任务系统中,每个任务都是独立互不干扰的,所以要为每个任务都分配独立的栈空间。(栈空间通常是一个预先定义好的全局数组)能够使用的最大的栈由Stack_Size决定。
多任务系统中任务的栈就是在统一的一个栈空间里面分配好一个个独立的房间,每个任务只能使用各自的房间。
任务栈其实就是一个预先定义好的全局数据,数据类型为CPU_STK
static CPU_STK Task1Stk[TASK1_STK_SIZE];
static CPU_STK Task2Stk[TASK2_STK_SIZE];
补充:
volatile关键字
volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)
1.2、定义任务函数
任务是一个独立的函数,函数主体无限循环且不能返回。
1.3、定义任务控制块TCB
前面已经提到,多任务系统中任务的执行是由系统调度的。系统为了顺利的调度任务,为每个任务都额外定义了一个任务控制块TCB(Task ControlBlock),相当于任务的身份证,里面存有任务的所有信息,比如任务的栈,任务名称,任务的形参等。
系统对任务的全部操作都可以通过这个TCB实现,TCB是一个新的数据类型。
/* 任务控制块数据类型声明 */
struct os_tcb {
CPU_STK *StkPtr;//栈指针
CPU_STK_SIZE StkSize;//栈大小
};
//任务TCB**定义**
static OS_TCB Task1TCB;
static OS_TCB Task2TCB;
1.4、实现任务创建函数
任务的栈,任务的函数实体,任务的TCB最终需要联系起来才能由系统进行统一调度。
这个联系的工作就由任务创建函数OSTaskCreate来实现。
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;
//任务栈初始化函数。当任务第一次运行时,加载到CPU寄存器的参数就放在任务栈里面,在任务创建的时候,预先初始化好栈。
p_sp = OSTaskStkInit (p_task,//任务名,指示着任务的入口地址。任务切换时加载到PC寄存器R15
p_arg,//任务的形参。任务切换时加载到寄存器R0
p_stk_base,//任务栈的起始地址
stk_size);//任务栈的大小
p_tcb->StkPtr = p_sp;
p_tcb->StkSize = stk_size;
*p_err = OS_ERR_NONE;
}
任务创建好之后要添加到就绪列表中,表示任务已经就绪,系统随时可以调度。
typedef struct os_rdy_list OS_RDY_LIST;
struct os_rdy_list {
OS_TCB *HeadPtr;
OS_TCB *TailPtr;
};
OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX];
/* 将任务加入到就绪列表 */
OSRdyList[0].HeadPtr = &Task1TCB;
OSRdyList[1].HeadPtr = &Task2TCB;
2、OS系统初始化
OS系统初始化一般是在硬件初始化完成之后来做的,只要初始化定义的全局变量。
void OSInit (OS_ERR *p_err)
{
OSRunning = OS_STATE_OS_STOPPED;//系统的运行状态
OSTCBCurPtr = (OS_TCB *)0;//系统指向当前正在运行的TCB指针。
OSTCBHighRdyPtr = (OS_TCB *)0;//指向就绪任务中优先级最高的任务的TCB.
OS_RdyListInit();//初始化全局变量OSRdyList[],即初始化就序列表。
*p_err = OS_ERR_NONE;//代码运行到这里表示没有错误。
}
void OS_RdyListInit(void)
{
OS_PRIO i;
OS_RDY_LIST *p_rdy_list;
for ( i=0u; i<OS_CFG_PRIO_MAX; i++ )
{
p_rdy_list = &OSRdyList[i];
p_rdy_list->HeadPtr = (OS_TCB *)0;
p_rdy_list->TailPtr = (OS_TCB *)0;
}
}
3、启动系统
任务创建好,系统初始化完毕,开始启动系统。
void OSStart (OS_ERR *p_err)
{
if ( OSRunning == OS_STATE_OS_STOPPED ) {
/* 手动配置任务1先运行 */
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
/* 启动任务切换,不会返回 ,由汇编语言编写*/
OSStartHighRdy();
/* 不会运行到这里,运行到这里表示发生了致命的错误 */
*p_err = OS_ERR_FATAL_RETURN;
}
else
{
*p_err = OS_STATE_OS_RUNNING;
}
}
4、任务切换
当调用OSStartHighRdy()函数,触发PendSV异常后,就需要编写PendSV异常服务函数,然后在里面进行任务切换。
PendSV异常服务中主要完成两个工作,一是保存上文,即保存当前正在运行的任务的环境参数;二是切换下文, 即把下一个需要运行的任务的环境参数从任务栈中加载到CPU寄存器,从而实现任务的切换。
5、main()函数
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);
}
任务函数Task1和Task2并不会真正 的自动切换,而是在各自的函数体里面加入了OSSched()函数来实现手动切换。
/* 任务切换,实际就是触发PendSV异常,然后在PendSV异常中进行上下文切换 */
void OSSched (void)
{
if ( OSTCBCurPtr == OSRdyList[0].HeadPtr )
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
OS_TASK_SW();
}
OSSched()函数的调度算法很简单,即如果当前任务是任务1,那么下一个任务就是任务2,如果当前任务是任务2,那么下一个任务就是任务1。
然后再调用OS_TASK_SW()函数触发PendSV异常,然后在PendSV异常里面实现任务的切换。
2021-07-20 19:56:48 星期二 |
三、任务时间片运行
在上一个代码实例的基础上,加入SysTick中断,在SysTick中断服务函数里面进行任务切换,从而实现双任务的时间片运行,即每个任务运行的时间都是一样的。
1、什么是SysTick?
RTOS需要一个时基来驱动,系统任务调度的频率等于该时基的频率。通常该时基由一个定时器来提供,也可以从其他周期性的信号源获得。
Cortex-M内核中有一个系统定时器SysTick,它内嵌在NVIC中,是一个24位的递减的计数器,计数器每计数一次的时间为1/SYSCLK。
当重装载数值寄存器的值递减到0的时候,系统定时器就产生一次中断,以此循环往复。
SysTick是最适合给操作系统提供时基, 用于维护系统心跳的定时器。
2、如何使用SysTick?
只需要一个初始化函数。
void OS_CPU_SysTickInit (CPU_INT32U ms)
{
/* 设置重装载寄存器的值 */
SysTick->LOAD = ms * SystemCoreClock / 1000 - 1;
/* 配置中断优先级为最低 */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);
/* 复位当前计数器的值 */
SysTick->VAL = 0;
/* 选择时钟源、启用中断、启用计数器 */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
3、编写SysTick中断服务函数
void SysTick_Handler(void)
{
OSTimeTick();
}
void OSTimeTick (void)
{
/* 任务调度,与上一个代码实例一样无须修改 */
OSSched();
}
4、main()函数
与上一个main()相比仅仅加入了SysTick相关的内容。
int main(void)
{
OS_ERR err;
/* 关闭中断 */
CPU_IntDis();//(1)为什么要关闭中断?
/* 配置SysTick 10ms 中断一次 */
OS_CPU_SysTickInit (10);//(2)任务的调度是在SysTick的中断服务函数中完成的
/* 初始化相关的全局变量 */
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);
}
(1)在OS系统初始化之前启用了SysTick定时器产生10ms的中断,在中断里面触发任务调度,
如果一开始我们不关闭中断,就会在OS还有启动之前就进入SysTick中断,然后发生任务调度,
既然OS都还没启动,那调度是不允许发生的, 所以先关闭中断。
系统启动后,中断由OSStart()函数里面的OSStartHighRdy()重新开启。
这个代码实例中,任务调度将不再在各自的任务里面实现,而是放到了SysTick中断服务函数中,
从而实现每个任务都运行相同的时间片,轮流的占有、平等的享有CPU。
上一个代码中,两个任务也是轮流的占有CPU,也是享有相同的时间片,该时间片是任务单次运行的时间。
不同的是本次代码中,任务的时间片等于SysTick定时器的时基,是很多个任务单次运行时间的综合,
即在这个时间片里面任务运行了非常多次。
2021-07-21 10:17:05 星期三 |
四、阻塞延时与空闲任务
在之前的例子中,任务体内的延时使用的是软件延时,即还是让CPU空等来达到延时的效果。为了榨干CPU的性能,尽量不让它空闲着,这里引入了阻塞延时。
什么是阻塞延时?
任务需要延时的时候,任务会放弃CPU的使用权,CPU可以去干其他的事情,当任务延时时间到,重新获得CPU使用权,任务继续运行,充分利用了CPU的资源。
当任务需要延时,进入阻塞状态,CPU如果没有其他任务可以运行,系统便会为CPU创建一个空闲任务,这个时候CPU就运行空闲任务。
什么是空闲任务?
空闲任务是系统在初始化时创建的优先级最低的任务,空闲任务主体很简单只是对一个全局变量进行计数。
在实际应用中,当系统进入空闲任务时,可在空闲任务中让单片机进入休眠或者低功耗等操作。
1、实现空闲任务
1.1、定义空闲任务栈
CPU_STK OSCfg_IdleTaskStk[OS_CFG_IDLE_TASK_STK_SIZE];
/* 空闲任务栈起始地址 */
CPU_STK * const OSCfg_IdleTaskStkBasePtr = (CPU_STK *)&OSCfg_IdleTaskStk[0];
/* 空闲任务栈大小 */
CPU_STK_SIZE const OSCfg_IdleTaskStkSize = (CPU_STK_SIZE)OS_CFG_IDLE_TASK_STK_SIZE;
//空闲任务的栈的起始地址和大小均被定义成一个常量,不能被修改。
1.2、定义空闲任务TCB
/* 空闲任务TCB */
OS_EXT OS_TCB OSIdleTaskTCB;
1.3、定义空闲任务函数
/* 空闲任务 */
void OS_IdleTask (void *p_arg)
{
p_arg = p_arg;
/* 空闲任务什么都不做,只对全局变量OSIdleTaskCtr ++ 操作 */
for (;;) {
OSIdleTaskCtr++;//空闲任务计数变量
}
}
1.4、空闲任务初始化
空闲任务在系统初始化中完成,意味着在系统还没有启动之前空闲任务就已经创建好。
void OSInit (OS_ERR *p_err)
{
/* 配置OS初始状态为停止态 */
OSRunning = OS_STATE_OS_STOPPED;
/* 初始化两个全局TCB,这两个TCB用于任务切换 */
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
/* 初始化就绪列表 */
OS_RdyListInit();
/* 初始化空闲任务 */
OS_IdleTaskInit(p_err);(1)
if (*p_err != OS_ERR_NONE) {
return;
}
}
/* 空闲任务初始化 */
void OS_IdleTaskInit(OS_ERR *p_err)
{
/* 初始化空闲任务计数器 */
OSIdleTaskCtr = (OS_IDLE_CTR)0;(2)
/* 创建空闲任务 */
OSTaskCreate( (OS_TCB *)&OSIdleTaskTCB,(3)
(OS_TASK_PTR )OS_IdleTask,
(void *)0,
(CPU_STK *)OSCfg_IdleTaskStkBasePtr,
(CPU_STK_SIZE)OSCfg_IdleTaskStkSize,
(OS_ERR *)p_err );
}
2、实现阻塞延时
阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离CPU使用权,然后进入阻塞状态,直到延时结束, 任务重新获取CPU使用权才可以继续运行。在任务阻塞的这段时间,CPU可以去执行其他的任务, 如果其他的任务也在延时状态,那么CPU就将运行空闲任务。
/* 阻塞延时 */
void OSTimeDly(OS_TICK dly)
{
/* 设置延时时间 */
OSTCBCurPtr->TaskDelayTicks = dly;//TaskDelayTicks是任务控制块的一个成员,用于记录任务需要延时的时间,单位为SysTick的中断周期。
/* 进行任务调度 */
OSSched();
}
struct os_tcb {
CPU_STK *StkPtr;
CPU_STK_SIZE StkSize;
/* 任务延时周期个数 */
OS_TICK TaskDelayTicks;
};
void OSSched(void)
{
/* 如果当前任务是空闲任务,那么就去尝试执行任务1或者任务2,
看看他们的延时时间是否结束,如果任务的延时时间均没有到期,
那就返回继续执行空闲任务 */
if ( OSTCBCurPtr == &OSIdleTaskTCB )
{
if (OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
else if (OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else
{
/* 任务延时均没有到期则返回,继续执行空闲任务 */
return;
}
}
else
{
/*如果是task1或者task2的话,检查下另外一个任务,
如果另外的任务不在延时中,就切换到该任务
否则,判断下当前任务是否应该进入延时状态,
如果是的话,就切换到空闲任务。否则就不进行任何切换 */
if (OSTCBCurPtr == OSRdyList[0].HeadPtr)
{
if (OSRdyList[1].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[1].HeadPtr;
}
else if (OSTCBCurPtr->TaskDelayTicks != 0)
{
OSTCBHighRdyPtr = &OSIdleTaskTCB;
}
else
{
/* 返回,不进行切换,因为两个任务都处于延时中 */
return;
}
}
else if (OSTCBCurPtr == OSRdyList[1].HeadPtr)
{
if (OSRdyList[0].HeadPtr->TaskDelayTicks == 0)
{
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
}
else if (OSTCBCurPtr->TaskDelayTicks != 0)
{
OSTCBHighRdyPtr = &OSIdleTaskTCB;
}
else
{
/* 返回,不进行切换,因为两个任务都处于延时中 */
return;
}
}
}
/* 任务切换 触发PendSV异常。*/
OS_TASK_SW();
}
3、main()函数
与上一个实验代码变动不大。
- 空闲任务初始化函数在OSInint中调用,在系统启动之前创建好空闲任务。
- 任务中的延时函数均替代为阻塞延时,延时时间均为2个SysTick中断周期,即20ms。