目录
uCOSIII启动过程:
stm32的启动过程:
uCOSIII的启动过程:
任务状态:
任务控制块:
任务堆栈:
任务就绪表:
优先级位映射表//OSPrioTbl[]
位映射表:
查找优先级:
什么是前导零指令呢?
就绪任务列表OSRdyList[]
任务的调度与切换
调度基础:
任务调度器:
中断级调度器:
时间片轮转调度:
uCOSIII启动过程:
stm32的启动过程:
在系统上电的时候第一个执行的是启动文件(.s文件)里边由汇编语言编写的复位函数Reset_Handler.复位函数的最后会调用C库函数_main,_main()函数的主要工作是初始化系统的堆和栈,最后调用C中的main()函数,进入C的编译环境下。
uCOSIII的启动过程:
uCOSIII启动是有一定顺序的所以在使用的时候按照顺序打开
- 调用OSInit()初始化UCOSIII系统。
- 创建任务。一般来说只在main()函数中创建一个开始(start_task)任务,其他任务在该任务中进行创建。创建任务之前,使用OS_CRITICAL_ENTER()函数进入临界区域,再利用OSTaskCreate()函数创建任务,任务创建完之后利用OS_CRITICAL_EXIT()退出临界状态。(RT-Thread和FreeRTOS则默认使用这种形式)。
- 当任务创建好后,就是处于任务就绪。在就绪态的任务可以参加操作系统的调度,任务调度器只启动一次,之后就不会在执行了,uCO/OS中启动任务调度器的函数是OSStart(),并且启动任务调度器的时候就不回返回了,从此任务都是由uCOS管理。调用OSStart()函数开启UCOSIII系统。
流程图可表示为:
打开main函数,代码如下:
int main(void) {OS_ERR err; CPU_SR_ALLOC(); /***********STM32硬件初始化***********/ delay_init(168); //时钟初始化 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组配置 uart_init(115200); //串口初始化 LED_Init(); //LED初始化 /***************END******************///第一步://初始化UCOSIII OSInit(&err); //第二步://进入临界区 OS_CRITICAL_ENTER();//第三步://创建开始任务 OSTaskCreate((OS_TCB * )&StartTaskTCB, //任务控制块 (CPU_CHAR * )"start task", //任务名字 (OS_TASK_PTR )start_task, //任务函数 (void * )0, //传递给任务函数的参数 (OS_PRIO )START_TASK_PRIO, //任务优先级 (CPU_STK * )&START_TASK_STK[0], //任务堆栈基地址 (CPU_STK_SIZE)START_STK_SIZE/10, //任务堆栈深度限位 (CPU_STK_SIZE)START_STK_SIZE, //任务堆栈大小 (OS_MSG_QTY )0, //任务内部消息队列能够接收的最大消息数目,为0时禁止接收消息 (OS_TICK )0, //当使能时间片轮转时的时间片长度,为0时为默认长度, (void * )0, //用户补充的存储区 (OS_OPT )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR, //任务选项 (OS_ERR * )&err); //存放该函数错误时的返回值//第四步:退出临界区 OS_CRITICAL_EXIT(); //退出临界区//第五步:开启UCOSIII OSStart(&err); //开启UCOSIII while(1); }
任务状态:
UCOSIII支持的是单核CPU,不支持多核CPU,这样某一时刻只有一个任务会获得CPU的使用权进入运行态,其他任务就会进入其他状态,
UCOSIII有多个任务态 参考该篇文章的任务状态 http://t.csdnimg.cn/5OTXd
任务控制块:
引入:在裸机系统中,程序的主体是 CPU 按照顺序执行的。而在多任务系统中,任务的执行是由系统调度的。
系统为了顺利的调度任务使用OSTaskCreate()函数来创建任务的时候,为每一个任务定义了一个控制块TCB(Task ConTrol Block),这个任务控制块就相当于任务的身份证,里边含有任务的所有信息,比如任务的堆栈,任务名称,任务的形参等。有了这个控制块之后,以后系统对任务的全部操作都可以通过这个TCB来实现。
任务堆栈:
暂停一个任务,以后又能恢复运行,必须考虑将这个运行的信息保存,而回复运行的时候需要将这些信息恢复到运行环境。所以任务切换必须做环境的保护和恢复操作。
因此每个任务都应该有自己的堆栈,下面创建一个堆栈:
1、定义一个CPU_STK变量,在UCOSIII中CPU_STK数据类型用来定义任务堆栈。
//任务堆栈大小 #define LED0_STK_SIZE 128 //任务控制块 OS_TCB Led0TaskTCB; //任务堆栈 CPU_STK LED0_TASK_STK[LED0_STK_SIZE];
2、在使用OSTaskCreat()函数创建任务的时候将堆栈地址转递给OSTaskCreate函数的参数p_stk_base,将堆数据深度传递给参数stk_limit,堆栈深度通常为堆栈大小的十分之一,主要用来检测堆栈是否为空,将堆栈大小转递给stk_size。
(CPU_STK * )&START_TASK_STK[0], //任务堆栈基地址 (CPU_STK_SIZE)START_STK_SIZE/10, //任务堆栈深度限位 (CPU_STK_SIZE)START_STK_SIZE, //任务堆栈大小
任务就绪表:
引入:多任务操作系统的主要工作是为系统中处于就绪状态的任务分配CPU资源。
其中涉及到的两个关键:
- 判断那些任务处于就绪状态(找优先级)。
- 确定那个任务应该马上得到执行(找任务)。
在UCOSIII中,所有已经就绪等待的任务都会被放在一个所谓的就绪表当中,任务就绪表包括两部分:
- 一个优先级位映射表OSPrioTbl[],用来标注那个优先级下有任务就绪。
- 一个是就绪任务列表OSRdyList[],用来记录每一个优先级下所有的就绪任务,其中包括指向各个就绪任务的指针。
优先级位映射表//OSPrioTbl[]
OSPrioTbl[]在os_prio.c中有定义:
CPU_DATA OSPrioTbl[OS_PRIO_TBL_SIZE]; //UCOSIII_CORE中os_prio.c中41行
根据CPU_DATA的值,该表的成员元素的宽度可以为8,16,32位,跳转可知:
在STM32中CPU_DATA为unsigned int,有4个字节,32位。因此表OSPrioTbl每个参数有32位。其中每个位对应一个优先级,当某个任务就绪之后就会将优先级映射表中相应的位置1。
UCOSIII 中任务数目由宏 OS_CFG_PRIO_MAX 配置的(见 os_cfg.h)。
UCOS-III和逻辑一样,数值越小,优先级越高,因此优先级0是优先级最高的。 优先级OS_CFG_PRIO_MAX-1 的优先级最低(也就是63)。uCOSIII将最小优先级唯一的分配给空闲任务,其他任务不允许被设置为这个优先级。当任务准备好运行了,根据任务的优先级,位映像表中相应位就会被设置为 1。(如果处理器支持位清零指令 CLZ,这个指令会加快位映像表的设置过程。)
位映射表:
根据映射表可以知道OSPrioTb[]的大小是32位(unsigned int)在UCOSIII中咱们得中断有64个优先级,所以两个OSPrioTb[]既可以撑起咱们64个优先级。
想要验证OSPrioTb[]的个数是否为2通过OS_PRIO_TBL_SIZE转跳进入在os.h中
查找优先级:
UCOSIII中优先级是怎么查找的?
- 遍历OSPrioTb[]
- 判断OSPrioTb[N]是否等于0
- 若不为0找出不为零的那位(从左往右)
在UCOSIII中定义了函数OS_PrioGetHighest()用于找到就绪了的最高优先级的任务(位置:os_prio.c中85行 )
//源码分析 OS_PRIO OS_PrioGetHighest (void) {CPU_DATA *p_tbl;OS_PRIO prio;prio = (OS_PRIO)0;//从OSPrioTbl[0]开始扫描映射表,一直遇到非零项。p_tbl = &OSPrioTbl[0];while (*p_tbl == (CPU_DATA)0) { //当数组OSPrioTbl[]中的某个元素为零的时候,就继续扫描下一个数组的元素, prio 加 DEF_INT_CPU_NBR_BITS(32) prio += DEF_INT_CPU_NBR_BITS; //p_tbl加一,继续寻找OSProTb[]中的下一个元素 p_tbl++;}//一旦找到一个非0项,再加上该项的前导0的数量(从左到右第一为1的位置),就找到了最高优先级任务了。prio += (OS_PRIO)CPU_CntLeadZeros(*p_tbl); return (prio); }
注意:一个操作系统如果只是具备了高优先级任务能够“立即”获得处理器并得到执行的特点,那么它仍然不算是实时操作系统。因为这个查找最高优先级任务的过程决定了调度时间是否具有实时性。
例如一个包含 n 个就绪任务的系统中,如果仅仅从头找到尾,那么这个时间将直接和 n 相关,而下一个就绪任务抉择时间的长短将会极大的影响系统的实时性。
μC/OS 内核中采用两种方法寻找最高优先级的任务,第一种是通用的方法,因为 μC/OS 防止 CPU平台不支持前导零指令,就采用 C 语言模仿前导零指令的效果实现了快速查找到最高优先级任务的方法。而第二种方法则是特殊方法,利用硬件计算前导零指令 CLZ,这样子一次就能知道哪一个优先级任务能够运行,这种调度算法比普通方法更快捷,但受限于平台(在 STM32 中我们
就使用这种方法)。
什么是前导零指令呢?
如果分别建立了优先级3,5,8,11这个四个任务,任务创建成功之后,调用CPU_CntLeadZeros()我们可以计算出 OSPrioTbl[0] 第一个置 1 的位前面有 3 个 0,那么这个 3 就是我们要查找的最高优先级,至于后面还有多少个位置 1 我们都不用管,只需要找到第一个 1 即可。
就绪任务列表OSRdyList[]
通过上一步我们已经知道了哪一个优先级的任务已经就绪,但是UCOSIII支持时间片轮转调度,同一个优先级下可以有多个任务。而就绪任务列表OSRdyList[]就是用来记录优先级每个优先级所有的就绪任务。
准备运行好的任务放到就绪列表当中。就绪列表是一个数组OSRdyList[],它一共有 OS_CFG_PRIO_MAX 条记录,记录的数据类型为 OS_RDY_LIST( 见 OS.H)。如下图所示
转跳进该数据类型之后就绪列表中的每条记录都包含了三个变量NbrEntries,TailPtr,HeadPtr。
NbrEntries:表示的是该优先级就绪任务数。当优先级没有就绪任务的时候,该变量的值被设置为0;
.TailPtr 和.HeadPtr 用亍该优先级就绪任务的建立双向列表。.HeadPtr 指向列表的头部,TailPtr 指向列表的尾部。
下图当有两个任务的时候逻辑图如下,此时NbrEntries=2
任务的调度与切换
调度基础:
调度器,决定了任务的运行顺序,UCOSIII是一个可以抢占优先级的,基于优先级内核。根据起重要性每个任务分配一个优先级。UCOSIII支持多个任务拥有相同的优先级(这一点UCOSII中没有)。
操作系统通过两个函数完成任务的调度功能:
- OSSched():在任务级被调用
- OSIntExit():在中断级被调用
确定哪个任务优先级最高,下面该哪个任务运行了的工作是由调度器(Scheduler)完成的
任务调度器:
源码的位置:os_core.c:
源码分析:
//OSSched为任务级调度器,在中断服务函数中不能使用 void OSSched (void) {CPU_SR_ALLOC();如果在中断服务子程序中调用OSSched(),此时中断嵌套层数>0。函数return直接返回if (OSIntNestingCtr > (OS_NESTING_CTR)0) { return; }//用户至少调用了一次给任务调度上锁函数OSSchedLock(),使OSSchedLockNestingCtr>0。函数return直接返回if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0){ return; }在任务执行的时候不希望中断打断任务执行,所以先关闭中断CPU_INT_DIS();//从后边的注释可以知道是获取准备好的最高优先级,(从任务就绪表中获取)OSPrioHighRdy = OS_PrioGetHighest(); /* Find the highest priority ready *///获取下次任务切换要运行的任务,OSTCBHighRdyPtr指向将要切换任务的OS_TCB(任务块)OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;//判断要运行的任务是否是正在运行的任务,是就不用切换if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* Current task is still highest priority task? */CPU_INT_EN(); /* Yes ... no need to context switch */return;}#if OS_CFG_TASK_PROFILE_EN > 0uOSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches to this task */ #endifOSTaskCtxSwCtr++; /* Increment context switch counter */#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)OS_TLS_TaskSw(); #endif//调用OS_TASK_SW()来完成实际上的任务切换OS_TASK_SW(); /* Perform a task level context switch *///开启中断CPU_INT_EN(); }
有时候我们并不希望发生任务调度,因为始终有一些代码的执行过程是不能被打断的。此时我们就可以使用函数OSSchedLock()对调度器加锁,当我们想要恢复任务调度的时候就可以使用函数OSSchedUnlock()给已经上锁的任务调度器解锁。
注意:在OSSched()中真正执行任务切换的宏OS_TASK_SW()(在os_cpu.h中定义),宏OS_TASK_SW()就是函数OSCtxSW(),OSCtxSW()是os_cpu_a.asm中汇编编写的一段代码,OSCtxSW()要做的就是将当前任务的CPU寄存器的值保存在堆栈当中,也就是现场保护,保存完当前任务的的现场后将新的任务OS_TCB中保存任务堆栈指针加载到CPU的堆栈指针寄存器中,最后还要从新任务的堆栈重恢复CPU寄存器的值。
中断级调度器:
OSIntExit();位置:os_core.c中第300行
//中断级任务调度器 void OSIntExit (void) {CPU_SR_ALLOC();//进入临界段//判断UCOSIII是否执行if (OSRunning != OS_STATE_OS_RUNNING) { /* Has the OS started? */return; /* No */}//关闭中断防止被中断打断的代码CPU_INT_DIS();OSIntNestingCtr中断嵌套计数器,判断是否为0if (OSIntNestingCtr == (OS_NESTING_CTR)0) { /* Prevent OSIntNestingCtr from wrapping */CPU_INT_EN();return;}//中断层数减一OSIntNestingCtr--;//因为OSIntExit()是在退出中断服务函数时调用的,因此中断嵌套计数器要减1///若 OSIntNestingCtr大于0时,说明还有其他的中断发生,那么跳回到中断服务程序中不需要任务切换if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /* ISRs still nested? */CPU_INT_EN(); /* Yes */return;}//检查调度器是否加锁,如果加锁的话就直接跳出,不需要做任何任务切换if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* Scheduler still locked? */CPU_INT_EN(); /* Yes */return;}//接下来的五行程序和任务级调度器是一样的,从OSRdyList[]中取出最高优先级的任务控制块OS_TCBOSPrioHighRdy = OS_PrioGetHighest(); /* Find highest priority */OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; /* Get highest priority task ready-to-run */if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* Current task still the highest priority? */CPU_INT_EN(); /* Yes */return;}#if OS_CFG_TASK_PROFILE_EN > 0uOSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches for this new task */ #endifOSTaskCtxSwCtr++; /* Keep track of the total number of ctx switches */#if defined(OS_CFG_TLS_TBL_SIZE) && (OS_CFG_TLS_TBL_SIZE > 0u)OS_TLS_TaskSw(); #endif//调用中断级任务切换函数OSIntCtxSw(); /* Perform interrupt level ctx switch *///开启中断CPU_INT_EN(); }
注意:
在中断级调度器中真正完成任务切换的就是中断级任务切换函数OSIntCtxSWO(),与任务级切换函数 OSCtxSWO()不同的是,由于进入中断的时候现场已经保存过了,所以 OSIntCtxSWO()不需要像OSCtxSWO()一样先保存当前任务现场,只需要做 OSCtxSWO()的后半部分工作,也就是从将要执行的任务堆栈中恢复 CPU 寄存器的值。
时间片轮转调度:
UCOSIII 是支持多个任务拥有同一个优先级的,当多个任务有相同的优先级的时候,这些任务采用时间片轮转调度方法进行任务调度,uC/OS-III 允许每个任务运行规定的时间片,当任务没有用完分配给他的时间片它可以自愿地放弃 CPU。uC/OS-III 允许任务在运行时开启戒者关闭循环轮转调度。在 os_cfg.h 文件中有个宏 OS_CFG_SCHED_ROUND_ROBIN_EN,我们要想使用时间片轮转调度就需要将OS_CFG_SCHED_ROUND_ROBIN_EN 定义为 1这样 UCOSIII中有关时间片轮转调度的代码才会被编译,否则不能使用时间片轮转调度。
下边是正点原子对时间片轮转调度的解释:
UCOSIII通调用OS_SchedRoundRobin()函数来完成就会切换到该优先级对应的下一任务中。
该函数的位置:
源码解释:
//源码解析 #if OS_CFG_SCHED_ROUND_ROBIN_EN > 0u void OS_SchedRoundRobin (OS_RDY_LIST *p_rdy_list) {OS_TCB *p_tcb;//任务控制块变量CPU_SR_ALLOC();//进入临界段//检查是否开启了时间片轮转调度if (OSSchedRoundRobinEn != DEF_TRUE) { /* Make sure round-robin has been enabled */return;}CPU_CRITICAL_ENTER();//关闭中断//获得最高优先级的就绪任务(指针地址)p_tcb = p_rdy_list->HeadPtr; /* Decrement time quanta counter *///无任务if (p_tcb == (OS_TCB *)0) {CPU_CRITICAL_EXIT();return;}//判断是否为空闲任务if (p_tcb == &OSIdleTaskTCB) {CPU_CRITICAL_EXIT();return;}//判断当前任务的时间片是否还有if (p_tcb->TimeQuantaCtr > (OS_TICK)0) {p_tcb->TimeQuantaCtr--;}//是否还剩余时间片,有的话就跳出去返回if (p_tcb->TimeQuantaCtr > (OS_TICK)0) { /* Task not done with its time quanta */CPU_CRITICAL_EXIT();return;}//NbrEntries是代表的某个优先级下的任务,这里判断是否小于2,小于2就不用进行任务切换了直接返回if (p_rdy_list->NbrEntries < (OS_OBJ_QTY)2) { /* See if it's time to time slice current task */CPU_CRITICAL_EXIT(); /* ... only if multiple tasks at same priority */return;}//判断调度器是否上锁上锁的话直接返回if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* Can't round-robin if the scheduler is locked */CPU_CRITICAL_EXIT();return;}//当执行到这一步的时候说明当前任务的时间片已经用完了,将当前任务的任务块OS_TCB从双向链表的链表头移到链表尾()OS_RdyListMoveHeadToTail(p_rdy_list); /* Move current OS_TCB to the end of the list */ //从这开始都是在为下一次任务做铺垫//重新获取新的双向链表表头也就是下一个要执行的任务p_tcb = p_rdy_list->HeadPtr; /* Point to new OS_TCB at head of the list *///是否使用默认时间片长度第一个默认,第二个用户指定的值if (p_tcb->TimeQuanta == (OS_TICK)0) { /* See if we need to use the default time slice */p_tcb->TimeQuantaCtr = OSSchedRoundRobinDfltTimeQuanta;} else {p_tcb->TimeQuantaCtr = p_tcb->TimeQuanta; /* Load time slice counter with new time */}CPU_CRITICAL_EXIT();//开启中断 } #endif