为什么会对操作系统有需求
数据传输过程中,如果数据流较大,前一个数据尚未发出下一个数据就来了,会导致“漏执行”现象。故需要一个实时性强,串行运行多任务的操作系统实现数据流的有序收发
APP端,中控A,终端B
操作系统简介
前后台系统
直接操作裸机,通常把程序分为前台系统和后台系统。
简单的小系统通常是前后台系统,包含了一个死循环和若干个中断服务程序,死循环就是后台系统,中断服务程序就是前台系统。
前台是中断级,后台是任务级
RTOS系统
RTOS:Real Time OS,即实时操作系统。
实时操作系统又分为硬实时和软实时,硬实时系统不允许超时,软实时对超时的限制就没那么严格。
可剥夺型内核
RTOS的内核负责管理所有的任务,决定何时运行那个任务,何时停止当前任务切换到其他任务,这个是内核的多任务管理能力。
而UCOS的内核就是可剥夺型的内核
可剥夺内核顾名思义就是可以剥夺其他任务的CPU使用权,他总是运行就绪任务中的优先级最高的那个任务
可剥夺型任务图解
Ps:任务都是死循环
UCOS系统
UCOS系统简介
UCOS是Micrium公司出品的RTOS类实时操作系统,UCOS目前有两个版本:UCOSII和UCOSIII。
UCOSIII是一个可裁剪,可剥夺型的多任务内核,而且没有任务数限制。UCOSIII提供了实时操作系统所需的所有功能,包括资源管理、同步、任务通信等。
**可剥夺多任务管理:**UCOSIII 和 UCOSII 一样都属于可剥夺的多任务内核,总是执行当前就绪的最高优先级任务。
**同优先级任务的时间片轮转调度:**这个是 UCOSIII 和 UCOSII 一个比较大的区别,UCOSIII
允许一个任务优先级被多个任务使用,当这个优先级处于最高就绪态的时候,UCOSIII 就会轮流调度处于这个优先级的所有任务,让每个任务运行一段由用户指定的时间长度,叫做时间片。
**极短的关中断时间:**UCOSIII 可以采用锁定内核调度的方式而不是关中断的方式来保护临界段代码,这样就可以将关中断的时间降到最低,使得 UCOSIII 能够非常快速的响应中断请求。
**任务数目不受限制:**UCOSIII 本身是没有任务数目限制的,但是从实际应用角度考虑,任务数目会受到 CPU 所使用的存储空间的限制,包括代码空间和数据空间。
**优先级数量不受限制:**UCOSIII 支持无限多的任务优先级。
内核对象数目不受限制:UCOSIII 允许定义任意数目的内核对象。内核对象指任务、信号量、互斥信号量、事件标志组、消息队列、定时器和存储快等。
**软件定时器:**用户可以任意定义“单次”和“周期”型定时器,定时器是一个递减计数器,递减到零就会执行预先定义好的操作。每个定时器都可以指定所需操作,周期型定时器在递减到零时会执行指定操作,并自动重置计数器值。
**同时等待多个内核对象:**UCOSIII 允许一个任务同时等待多个事件。也就是说,一个任务能够挂起在多个信号量或消息队列上,当其中任何一个等待的事件发生时,等待任务就会被唤醒。
**直接向任务发送信号:**UCOSIII 允许中断或任务直接给另一个任务发送信号,避免创建和使用诸如信号量或事件标志等内核对象作为向其他任务发送信号的中介,该特性有效地提高了系统性能。
**直接向任务发送消息:**UCOSIII 允许中断或任务直接给另一个任务发送消息,避免创建和使用消息队列作为中介。
**任务寄存器:**每个任务都可以设定若干个“任务寄存器”,任务寄存器和 CPU 硬件寄存器是不同的,主要用来保存各个任务的错误信息,ID 识别信息,中断关闭时间的测量结果等。
**任务级时钟节拍处理:**UCOSIII 的时钟节拍是通过一个专门任务完成的,定时中断仅触发该任务。将延迟处理和超时判断放在任务级代码完成,能极大地减少中断延迟时间。
**防止死锁:**所有 UCOSIII 的“等待”功能都提供了超时检测机制,有效地避免了死锁。
**时间戳:**UCOSIII 需要一个 16 位或 32 位的自由运行计数器(时基计数器)来实现时间测量,在系统运行时,可以通过读取该计数器来测量某一个事件的时间信息。
例如,当 ISR 给任务发送消息时,会自动读取该计数器的数值并将其附加在消息中。当任务读取消息时,可得到该消息携带的时标,这样,再通过读取当前的时标,并计算两个时标的差值,就可以确定传递这条消息所花费的确切时间。
UCOS相关资料查找
UCOS参考书籍
- 《嵌入式实时操作系统uc/os-III》,作者:Jean J. Labrosse
- 《嵌入式实时操作系统uc/os-III应用开发》,作者:Jean J. Labrosse
- 《嵌入式实时操作系统uc/os-II原理及应用》,作者:任哲
UCOSIII 移植
参考正点原子开发板参考资料
UCOSIII启动和初始化
UCOS初始化有严格的顺序:
- 首先调用OSInit()初始化UCOSIII
- 创建任务,一般在main函数中只创建一个start_task任务,其他任务都在start_task任务中创建,在调用 OSTaskCreate()函数创建任务的时候一定要调用 OS_CRITICAL_ENTER()函数进入临界区,任务创建完以后调用 OS_CRITICAL_EXIT()函数退出临界区
- 最后调用OSStart()函数开启UCOSIII
注意:我们在调用 OSStart()开启 UCOSIII 之前一定要至少创建一个任务,其实我们在调用OSInit()函数初始化 UCOSIII 的时候已经创建了一个空闲任务。
范例:
OS_ERR err;//创建一个err变量来保存执行每个API函数的返回值(错误类型)
CPU_SR_ALLOC();//申请一个变量保存中断开关状态
OSInit(&err); //初始化UCOSIII
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(); //退出临界区
OSStart(&err); //开启UCOSIII
UCOSIII任务管理
任务状态
UCOSIII 支持的是单核 CPU,不支持多核 CPU,这样在某一时刻只有一个任务会获得 CPU 使用权进入运行态,其他的任务就会进入其他状态,UCOSIII 中的任务有多个状态,如下表所示:
在 UCOSIII 中任务可以在这 5 个状态中转换,转换关系如下表所示:
任务控制块
UCOSIII中有个重要的数据结构——OS_TCB
任务控制块TCB用来保存任务的信息,使用OSTaskCreate()函数创建任务的时候就会给任务分配一个任务控制块。
任务控制块是一个结构体,如下
struct os_tcb {
CPU_STK *StkPtr; //指向当前任务堆栈的栈顶
void *ExtPtr; //指向用户可定义的数据区
CPU_STK *StkLimitPtr; //可指向任务堆栈中的某个位置
OS_TCB *NextPtr; //NexPtr 和 PrevPtr 用于在任务就绪表建立 OS_TCB
OS_TCB *PrevPtr; //双向链表
OS_TCB *TickNextPtr; // TickNextPtr 和 TickPrevPtr 可把正在延时或在指定时
OS_TCB *TickPrevPtr; //间内等待某个事件的任务的 OS_TCB 构成双向链表
OS_TICK_SPOKE *TickSpokePtr; //通过该指针可知道该任务在时钟节拍轮的哪个spoke 上
CPU_CHAR *NamePtr; //任务名
CPU_STK *StkBasePtr; //任务堆栈基地址
OS_TASK_PTR TaskEntryAddr;//任务代码入口地址
void *TaskEntryArg; //传递给任务的参数
OS_PEND_DATA *PendDataTblPtr;//指向一个表,包含有任务等待的所有事件对象的信息
OS_STATE PendOn; //任务正在等待的事件的类型
OS_STATUS PendStatus; //任务等待的结果
OS_STATE TaskState; //任务的当前状态
OS_PRIO Prio; //任务优先级
CPU_STK_SIZE StkSize; //任务堆栈大小
OS_OPT Opt; //保存调用 OSTaskCreat()创建任务时的可选参数options 的值
OS_OBJ_QTY PendDataTblEntries; //任务同时等待的事件对象的数目
CPU_TS TS; //存储事件发生时的时间戳
OS_SEM_CTR SemCtr; //任务内建的计数型信号量的计数值
OS_TICK TickCtrPrev; //存储 OSTickCtr 之前的数值 OS_TICK
TickCtrMatch; //任务等待延时结束时,当 TickCtrMatch 和 OSTickCtr 的数值相匹配时,任务延时结束
OS_TICK TickRemain; //任务还要等待延时的节拍数
OS_TICK TimeQuanta; // TimeQuanta 和 TimeQuantaCtr 与时间片有关
OS_TICK TimeQuantaCtr;
void *MsgPtr; //指向任务接收到的消息
OS_MSG_SIZE MsgSize; //任务接收到消息的长度
OS_MSG_Q MsgQ; //UCOSIII 允许任务或 ISR 向任务直接发送消息,MsgQ 就为这个消息队列
CPU_TS MsgQPendTime; //记录一条消息到达所花费的时间
CPU_TS MsgQPendTimeMax; //记录一条消息到达所花费的最长时间
OS_REG RegTbl[OS_CFG_TASK_REG_TBL_SIZE]; //寄存器表,和 CPU 寄存器不同
OS_FLAGS FlagsPend; //任务正在等待的事件的标志位
OS_FLAGS FlagsRdy; //任务在等待的事件标志中有哪些已经就绪
OS_OPT FlagsOpt; //任务等待事件标志组时的等待类型
OS_NESTING_CTR SuspendCtr; //任务被挂起的次数
OS_CPU_USAGE CPUUsage; //CPU 使用率
OS_CPU_USAGE CPUUsageMax; //CPU 使用率峰值
OS_CTX_SW_CTR CtxSwCtr; //任务执行的频繁程度
CPU_TS CyclesDelta; //改成员被调试器或运行监视器利用
CPU_TS CyclesStart; //任务已经占用 CPU 多长时间
OS_CYCLES CyclesTotal; //表示一个任务总的执行时间
OS_CYCLES CyclesTotalPrev;
CPU_TS SemPendTime; //记录信号量发送所花费的时间
CPU_TS SemPendTimeMax; //记录信号量发送到一个任务所花费的最长时间
CPU_STK_SIZE StkUsed; //任务堆栈使用量
CPU_STK_SIZE StkFree; //任务堆栈剩余量
CPU_TS IntDisTimeMax; //该成员记录任务的最大中断关闭时间
CPU_TS SchedLockTimeMax; //该成员记录锁定调度器的最长时间
//下面 3 个成语变量用于调试
OS_TCB *DbgPrevPtr;
OS_TCB *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
};
从上面的 os_tcb 结构体中可以看出 UCOSIII 的任务控制块要比 UCOSII 的要复杂的多,这也间接的说明了 UCOSIII 要比 UCOSII 功能要强大得多。
任务堆栈
在 UCOSIII 中任务堆栈是一个非常重要的概念,任务堆栈用来在切换任务和调用其它函数的时候保存现场,因此每个任务都应该有自己的堆栈,按照下面的步骤创建一个堆栈:
-
定义一个CPU_STK变量,在UCOSIII中用CPU_STK数据类型来定义任务堆栈,CPU_STK 在 cpu.h 中有定义,其实 CPU_STK 就是 CPU_INT32U,可以看出一个 CPU_STK 变量为 4 字节,因此任务的实际堆栈大小应该为我们定义的 4 倍。下面代码就是我们定义了一个任务堆栈 TASK_STK,堆栈大小为 64*4=256 字节。
CPU_STK TASK_STK[64]; //定义一个任务堆栈
可以使用下面的方法定义一个堆栈,这样代码比较清晰,正点原子所有例程都使用下面的方法定义堆栈。
#define TASK_STK_SIZE 64 //任务堆栈大小 CPU_STK TASK_STK[LED1_STK_SIZE]; //任务堆栈
我们使用 OSTaskCreat()函数创建任务的时候就可以把创建的堆栈传递给任务,
如下6、7、8行将创建的堆栈传递给任务,
将堆栈的基地址传递给 OSTaskCreate()函数的参数p_stk_base,
将堆栈深度传递给参数 stk_limit,堆栈深度通常为堆栈大小的十分之一,主要用来检测堆栈是否为空,
将堆栈大小传递给参数 stk_size。
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); //存放该函数错误时的返回值
创建任务的时候会初始化任务的堆栈,我们需要提前将 CPU 的寄存器保存在任务堆栈中,完成这个任务的是 OSTaskStkInit()函数,这个函数应该不陌生的,正点原子在移植 UCOSIII的时候专门讲解过这个函数,用户不能调用这个函数,这个函数是被 OSTaskCreate()函数在创建任务的时候调用的。
任务就绪表
UCOSIII 中将已经就绪的任务放到任务就绪表里,任务就绪表有两部分:
优先级位映射表 OSPrioTbl[] 和就绪任务列表 OSRdyList[]
优先级位映射表
当某一个任务就绪以后就会将优先级位映射表中相应的位置 1, 优先级位映射表如下图所示,
该表元素的位宽度可以是 8 位,16 位或 32 位,根据 CPU_DATA(见 cpu.h)的不同而不同,在 STM32F407 中我们定义 CPU_DATA 为 CPU_INT32U 类型的,即 32 位宽。UCOSIII 中任务数目由宏 OS_CFG_PRIO_MAX 配置的(见 os_cfg.h)。
有关于优先级的操作有 3 个函数:OS_PrioGetHighest()为获取就绪表中最高优先级任务、 OS_PrioInsert()和 OS_PrioRemove()分别为将指定优先级任务相对应的优先级映射表中的位置 1 和清零,
就绪任务列表
OSRdyList[]是用来记录每一个优先级下所有就绪的任务,OSRdyList[]在 os.h 中有定义,数组元素的类型为 OS_RDY_LIST,OS_RDY_LIST 为一个结构体,结构体定义如下:
struct os_rdy_list {
OS_TCB *HeadPtr; //用于创建链表,指向链表头
OS_TCB *TailPtr; //用于创建链表,指向链表尾
OS_OBJ_QTY NbrEntries; //此优先级下的任务数量
};
UCOSIII 支持时间片轮转调度,因此在一个优先级下会有多个任务,那么我们就要对这些任务做一个管理,这里使用 OSRdyList[]数组管理这些任务。OSRdyList[]数组中的每个元素对应一个优先级,比如 OSRdyList[0] 就用来管理优先级 0 下的所有任务。
针对任务就绪列表的操作有以下 6 个函数,如下表所示,这些函数都在 os_core.c 这个文件中,这几个函数是 UCOSIII 内部使用的,用户程序不能使用。
任务调度和切换
可剥夺型调度
任务调度和切换就是让就绪表中优先级最高的任务获得 CPU 的使用权,UCOSIII 是可剥夺型,抢占式的,可以抢了低优先级任务的 CPU 使用权,任务的调度是由一个叫做任务调度器的东西来完成的,任务调度器有两种:一种是任务级调度器,一种是中断级调度器。
任务级调度器
任务级调度器为 OSSched(),OSSched()函数代码在 os_core.c 文件中
OSSched函数一般不被直接调用,是被UCOS中的别的API调用
OSSched()为任务级调度器,如果是在中断服务函数中不能使用!
在 OSSched() 中真正执行任务切换的是宏 OS_TASK_SW() (在 os_cpu.h 中定义) ,宏OS_TASK_SW()就是函数 OSCtxSW(),OSCtxSW()是 os_cpu_a.asm 中用汇编写的一段代码,OSCtxSW()要做的就是将当前任务的 CPU 寄存器的值保存在任务堆栈中,也就是保存现场,保存完当前任务的现场后将新任务的 OS_TCB 中保存的任务堆栈指针的值加载到 CPU 的堆栈指针寄存器中,最后还要从新任务的堆栈中恢复 CPU 寄存器的值。
任务级调度点
-
释放信号量或发送消息,也可通过配置相应的蚕食不发生任务调度
-
使用延时函数OSTimeDly()或者OSTimeDlyHMSM()
-
任务等待的事情还没发生(等待信号量,消息列队)
-
任务取消等待
-
创建任务
-
删除任务
-
删除一个内核对象
-
任务改变自身的优先级或者其他任务的优先级
-
任务通过调用OSTaskSuspend()将自身挂起
-
任务解挂某个挂起的任务
-
退出所有的嵌套中断
-
通过OSSchedUnlock()给调度器解锁
-
任务调用OSSchedRoundRobinYield()放弃期执行时间片
-
用户调用OSSched()
加粗为重点关注的任务调度点
中断级调度器
中断级调度器为 OSIntExit(),当退出外部中断服务函数的时候使用中断级任务调度,调用 OSIntExit()时,中断应该是关闭的。
在中断级调度器中真正完成任务切换的就是中断级任务切换函数 OSIntCtxSW(),与任务级切换函数 OSCtxSW()不同的是,由于进入中断的时候现场已经保存过了,所以 OSIntCtxSW()不需要像 OSCtxSW()一样先保存当前任务现场,只需要做 OSCtxSW()的后半部分工作,也就是从将要执行的任务堆栈中恢复 CPU 寄存器的值。
时间片轮转调度
UCOSIII 支持多个任务同时拥有一个优先级,要使用这个功能我们需要定义OS_CFG_SCHED_ROUND_ROBIN_EN 为 1
在 UCOSIII 中允许一个任务运行一段时间**(时间片)**后让出 CPU 的使用权,让拥有同优先级的下一个任务运行,这种任务调度方法就是时间片轮转调度。
如果当前任务的时间片已经运行完,但是同一优先级下有多个任务,那么 UCOSIII 就会切换到该优先级对应的下一个任务,通过调用 OS_SchedRoundRobin() 函数来完成,这个函数由OSTimeTick()或者 OS_IntQTask()调用
调度器上锁和解锁
有些时候我们并不希望发生任务调度,因为有一些代码的执行过程是不能被打断的。此时我们就可以使用函数**OSSchedLock()**对调度器加锁,当我们想要恢复任务调度的时候就可以使用函数OSSchedUnlock()给已经上锁的任务调度器解锁
任务切换
任务切换分为两种:任务级切换和中断级切换
任务级:OSCtxSw()
中断级:OSIntCtxSw()
任务相关API函数使用
任务创建和删除
OSTaskCreate()函数
在 UCOSIII 中我们通过函数 OSTaskCreate() 来创建任务,OSTaskCreate() 函数原型在 os_task.c 中有定义。
调用OSTaskCreat()创建一个任务以后,刚创建的任务就会进入就绪态
注意!不能在中断服务程序中调用 OSTaskCreat()函数来创建任务。
范例:
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); //存放该函数错误时的返回值
OSTaskDel()函数
OSTaskDel()函数用来删除任务,当一个任务不需要运行的话,我们就可以将其删除掉,删除任务不是说删除任务代码,而是 UCOSIII 不再管理这个任务,在有些应用中我们只需要某个任务只运行一次,运行完成后就将其删除掉,比如外设初始化任务
OSTaskDel()函数原型如下:
void OSTaskDel (OS_TCB *p_tcb,//指向要删除的任务TCB,也可以传递一个NULL指针来删除调用 OSTaskDel()函数的任务自身。
OS_ERR *p_err)//指向一个变量用来保存调用 OSTaskDel()函数后返回的错误码。
虽然 UCOSIII 允许用户在系统运行的时候来删除任务,但是应该尽量的避免这样的操作,如果多个任务使用同一个共享资源,这个时候任务 A 正在使用这个共享资源,如果删除了任务A,这个资源并没有得到释放,那么其他任务就得不到这个共享资源的使用权,会出现各种奇怪的结果。
我们调用 OSTaskDel()删除一个任务后,这个任务的任务堆栈、OS_TCB 所占用的内存并没有释放掉,因此我们可以利用他们用于其他的任务,当然我们也可以使用内存管理的方法给任务堆栈和 OS_TCB 分配内存,这样当我们删除掉某个任务后我们就可以使用内存释放函数将这个任务的任务堆栈和 OS_TCB 所占用的内存空间释放掉。
任务挂起和恢复
待更新……
OS_ERR *p_err)//指向一个变量用来保存调用 OSTaskDel()函数后返回的错误码。
虽然 UCOSIII 允许用户在系统运行的时候来删除任务,但是应该尽量的避免这样的操作,如果多个任务使用同一个共享资源,这个时候任务 A 正在使用这个共享资源,如果删除了任务A,这个资源并没有得到释放,那么其他任务就得不到这个共享资源的使用权,会出现各种奇怪的结果。
我们调用 OSTaskDel()删除一个任务后,这个任务的任务堆栈、OS_TCB 所占用的内存并没有释放掉,因此我们可以利用他们用于其他的任务,当然我们也可以使用内存管理的方法给任务堆栈和 OS_TCB 分配内存,这样当我们删除掉某个任务后我们就可以使用内存释放函数将这个任务的任务堆栈和 OS_TCB 所占用的内存空间释放掉。
## 任务挂起和恢复
待更新……
学习源为正点原子STM32F407开发资料