EagleBear2002 的博客

这里必须根绝一切犹豫,这里任何怯懦都无济于事

嵌入式系统概论-07-实时内核

本文主要内容来自 SpriCoder的博客,更换了更清晰的图片并对原文的疏漏做了补充和修正。

任务管理

μC/OS-II 是一个可移植的、可固化的、可扩展的、抢占式的、实时确定性的多任务内核,适用于微处理器、微控制器和 DSP。

任务主函数

开源代码用来学习是可以的,但是如果要商用,则需要获取到开源代码所有者的商业许可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void YourTask (void *pdata) {
for (;;) {
/* USER CODE
Call one of uC/OS-II's services:
OSFlagPend();
OSMboxPend();
OSMutexPend();
OSQPend();
OSSemPend();
OSTaskSuspend(OS_PRIO_SELF);
OSTimeDly();
OSTimeDlyHMSM();
/* USER CODE */
}
}
// 或者是
void YourTask (void *pdata) {
/* USER CODE */
OSTaskDel(OS_PRIO_SELF);
}

任务优先级

  1. μC/OS-II 最多可以管理 64 个任务
  2. 尽管 μC/OS-II 保留了四个最高优先级任务和四个最低优先级任务供自己使用。但是,此时,μC/OS-II 实际上仅使用两个优先级:OSTaskCreateOS_LOWEST_PRIO-1(请参阅 OS_CFG.H)。这使您最多可以执行 56 个应用程序任务。
  3. 优先级的值越小,任务的优先级越高。
  4. 在当前版本的 μC/ OS-II 中,任务优先级编号也用作任务标识符。
  5. 任务优先级一致怎么办: 1. 时间片流转:先使用一定的时间片完成,然后将结果给下一个使用 2. 先到先服务

空闲任务和统计任务

内核总是创建一个空闲任务 OSTaskIdle()

  1. 总是设置为最低优先级,OS_LOWEST_PRIOR
  2. 当所有其他任务都未在执行时,空闲任务开始执行
  3. 应用程序不能删除该任务;
  4. 空闲任务的工作就是把 32 位计数器 OSIdleCtr 加 1,该计数器被统计任务所使用;

统计任务 OSTaskStat(),提供运行时间统计。每秒钟运行一次,计算当前的 CPU 利用率。其优先级是 OS_LOWEST_PRIOR-1,可选。

任务控制块 TCB

任务控制块 OS_TCB 是描述一个任务的核心数据结构,存放了任务的各种管理信息,包括任务堆栈指针,任务的状态、优先级,任务链表指针等;

一旦任务建立了,任务控制块 OS_TCB 将被赋值。

1
2
3
4
5
6
7
8
9
10
typedef struct os_tcb{
//栈指针;
//INT16U OSTCBId; /*任务的 ID*/
//链表指针;
//OS_EVENT *OSTCBEventPtr; /*事件指针*/
//void *OSTCBMsg; /*消息指针*/
//INT8U OSTCBStat; /*任务的状态*/
//INT8U OSTCBPrio; /*任务的优先级*/
//其他……
} OS_TCB;

栈指针

  1. OSTCBStkPtr:指向当前任务栈顶的指针,每个任务可以有自己的栈,栈的容量可以是任意的;
  2. OSTCBStkBottom:指向任务栈底的指针;
  3. OSTCBStkSize:栈的容量,用可容纳的指针数目而不是字节数(Byte)来表示。

链表指针

所有的任务控制块分属于两条不同的链表,单向的空闲链表(头指针为 OSTCBFreeList)和双向的使用链表(头指针为 OSTCBList);

OSTCBNextOSTCBPrev:用于将任务控制块插入到空闲链表或使用链表中。每个任务的任务控制块在任务创建的时候被链接到使用链表中,在任务删除的时候从链表中被删除。双向连接的链表使得任一成员都能快速插入或删除。

空闲 TCB 链表

所有的任务控制块都被放置在任务控制块列表数组 OSTCBTbl[] 中,系统初始化时,所有 TCB 被链接成空闲的单向链表,头指针为 OSTCBFreeList。当创建一个任务后,就把 OSTCBFreeList 所指向的 TCB 赋给了该任务,并将它加入到使用链表中,然后把 OSTCBFreeList 指向空闲链表中的下一个节点。

为什么空闲是单项链表,使用是双项链表?因为双向链表有利于将时间复杂度降低为常数。

  1. 遍历链表的时间复杂度是 \(O(n)\)
  2. 期望遍历复杂度是 \(O(1)\),常数,开辟一个数据存放所有任务和 TCB 地址
  3. 用空间换时间

指针数组(指向相应 TCB)

状态的转换

任务就绪表

  1. 每个任务的就绪态标志放入在就绪表中,就绪表中有两个变量 OSRdyGrpOSRdyTbl[]
  2. OSRdyGrp 中,任务按优先级分组,8 个任务为一组。OSRdyGrp 中的每一位表示 8 组任务中每一组中是否有进入就绪态的任务。任务进入就绪态时,就绪表 OSRdyTbl[]中的相应元素的相应位也置位。

(0, 0)是优先级最高的任务,(7, 7)是优先级最低的

根据优先级确定就绪表

假设优先级为 12(优先级为 0)的任务进入就绪状态,12=1100b,则 OSRdyTbl[1] 的第 4 位置 1,且 OSRdyGrp 的第 1 位置 1,相应的数学表达式为:

  1. OSRdyGrp |= 0x02
  2. OSRdyTbl[1] |= 0x10

而优先级为 21 的任务就绪 21=10 101b,则 OSRdyTbl[2]的第 5 位置 1,且 OSRdyGrp 的第 2 位置 1,相应的数学表达式

  1. OSRdyGrp |= 0x04
  2. OSRdyTbl[2] |= 0x20

从上面的计算可知: 若 OSRdyGrpOSRdyTbl[] 的第 n 位置 1,则应该把 OSRdyGrpOSRdyTbl[] 的值与 \(2^n\) 相或。uC/OS 中,把 \(2^n\) 的 n=0-7 的 8 个值先计算好存在数组 OSMapTbl[7]中,也就是:

  1. OSMapTbl[0] = $2^0$ = 0x01(0000 0001)
  2. OSMapTbl[1] = $2^1$ = 0x02(0000 0010)
  3. OSMapTbl[7] = $2^7$ = 0x80(1000 0000)

如果 prio 是任务的优先级,即任务的标识号,则将任务放入就绪表,使任务进入就绪态的方法是:

  1. OSRdyGrp |= OSMapTbl[prio>>3]
  2. OSRdyTbl[prio>>3] |= OSMapTbl[prio&0x07]

假设优先级为 12:1100b

  1. OSRdyGrp |= OSMapTbl[12>>3](0x02)
  2. OSRdyTbl[1] |= 0x10

使任务脱离就绪态

将任务就绪表 OSRdyTbl[prio>>3] 相应元素的相应位清零,而且当 OSRdyTbl[prio>>3] 中的所有位都为零时,即该任务所在组的所有任务中没有一个进入就绪态时,OSRdyGrp 的相应位才为零:

1
2
if((OSRdyTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0)
OSRdyGrp &= ~OSMapTbl[prio >> 3];

任务的调度

  1. μC/OS-II 是可抢占实时多任务内核,它总是运行就绪任务中优先级最高的那一个。
  2. μC/OS-II 中不支持时间片轮转法,每个任务的优先级要求不一样且是唯一的,所以任务调度的工作就是:查找准备就绪的最高优先级的任务并进行上下文切换。
  3. μC/OS-II 任务调度所花的时间为常数,与应用程序中建立的任务数无关。
  4. 确定哪个任务的优先级最高,应该选择哪个任务去运行,这部分的工作是由调度器(Scheduler)来完成的。 1. 任务级的调度是由函数 OSSched() 完成的; 2. 中断级的调度是由另一个函数 OSIntExt() 完成的。

根据就绪表确定最高优先级(为什么右移三位)

两个关键:

  1. 将优先级数分解为高三位和低三位分别确定;
  2. 高优先级有着小的优先级号

根据就绪表确定最高优先级

  1. 通过 OSRdyGrp 值确定高 3 位,假设 OSRdyGrp = 0x08=0x00001000,第 3 位为 1,优先级的高 3 位为 011;
  2. 通过 OSRdyTbl[3] 的值来确定低 3 位,假设 OSRdyTbl[3] = 0x3a,第 1 位为 1,优先级的低 3 位为 001,3*8+2-1=25

任务调度器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void OSSched(void) {
INT8U y;
OS_ENTER_CRITICAL();
// 检查是否中断调用和允许任务调用
if ((OSLockNesting | OSIntNesting) == 0) {
y = OSUnMapTbl[OSRdyGrp];
// 找到优先级最高的任务
OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
// 该任务是否正在运行
if (OSPrioHighRdy != OSPrioCur) {
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
OSCtxSwCtr++;
OS_TASK_SW();
}
}
OS_EXIT_CRITICAL();
}

源代码中使用了查表法

查表法具有确定的时间,增加了系统的可预测性,uC/OS 中所有的系统调用时间都是确定的

  1. Y = OSUnMapTbl[OSRdyGrp]
  2. X = OSUnMapTbl[OSRdyTbl[Y]]
  3. Prio = (Y<<3) + X;

优先级判定表 OSUnMapTbl[256]

  • \(2^8 = 256\):一共有 256 种情况,查表解释即可
  • 空间换时间,用来快速查找当前优先级最高的部分

从 64->256

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void OS_SchedNew(void) {
#if OS_LOWEST_PRIO <= 63 //μC/OS-II v2.7 之前方式
INT8U y;
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
#else
INT8U y;
INT16U* ptbl;
// OSRdyGrp 为 16 位
if ((OSRdyGrp & 0xFF) != 0) {
y = OSUnMapTbl[OSRdyGrp & 0xFF];
} else {
y = OSUnMapTbl[(OSRdyGrp >> 8) & 0xFF] + 8; //矩形组号 y>=8
}
ptbl = &OSRdyTbl[y]; //取出 x 方向的 16bit 数据
if ((*ptbl & 0xFF) != 0) {
OSPrioHighRdy = (INT8U)((y << 4) + OSUnMapTbl[(*ptbl & 0xFF)]); //*16
} else {
OSPrioHighRdy = (INT8U)((y << 4) + OSUnMapTbl[(*ptbl >> 8) & 0xFF] + 8);
}
#endif
}
  1. 未超过 64 位,则用上面的,如果超过了 64 位则使用下半部分
  2. 仔细分析一下:判定低八位是否为 0,如果低八位不为 0,则直接对低八位操作即可,如果低八位为 0,则在高八位,所以需要加 8

任务切换

  1. 将被挂起任务的寄存器内容入栈;
  2. 将较高优先级任务的寄存器内容出栈,恢复到硬件寄存器中。

任务级的任务切换 OS_TASK_SW()

  1. 保护当前任务的现场
  2. 恢复新任务的现场
  3. 执行中断返回指令
  4. 开始执行新的任务
调用 OS_TASK_SW() 前的数据结构 保存当前 CPU 寄存器的值 重新装入要运行的任务

任务切换 OS_TASK_SW() 的代码

1
2
3
4
5
6
7
8
void OSCtxSw(void) {
//将 R1,R2,R3 及 R4 推入当前堆栈;
OSTCBCur->OSTCBStkPtr = SP;
OSTCBCur = OSTCBHighRdy;
SP = OSTCBHighRdy->OSTCBSTKPtr;
//将 R4,R3,R2 及 R1 从新堆栈中弹出;
//执行中断返回指令;
}

给调度器上锁

  1. OSSchedlock():给调度器上锁函数,用于禁止任务调度,保持对 CPU 的控制权(即使有优先级更高的任务进入了就绪态);
  2. OSSchedUnlock():给调度器开锁函数,当任务完成后调用此函数,调度重新得到允许;
  3. 当低优先级的任务要发消息给多任务的邮箱、消息队列、信号量时,它不希望高优先级的任务在邮箱、队列和信号量还没有得到消息之前就取得了 CPU 的控制权,此时,可以使用调度器上锁函数。

任务管理的系统服务

  1. 创建任务
  2. 删除任务
  3. 修改任务的优先级
  4. 挂起和恢复任务
  5. 获得一个任务的有关信息

创建任务

  1. 创建任务的函数 1. OSTaskCreate() 2. OSTaskCreateExt()
  2. OSTaskCreateExt()OSTaskCreate() 的扩展版本,提供了一些附加的功能;
  3. 任务可以在多任务调度开始 (即调用 OSStart()) 之前创建,也可以在其它任务的执行过程中被创建。但在 OSStart() 被调用之前,用户必须创建至少一个任务;
  4. 不能在中断服务程序(ISR)中创建新任务。

OSTaskCreate()

1
2
3
4
5
6
INT8U OSTaskCreate (
void (*task)(void *pd), //任务代码指针
void *pdata, //任务参数指针
OS_STK *ptos, //任务栈的栈顶指针
INT8U prio //任务的优先级
);
  • 返回值 - OS_NO_ERR:函数调用成功; - OS_PRIO_EXIT:任务优先级已经存在; - OS_PRIO_INVALID:任务优先级无效。

OSTaskCreate() 的实现过程

  1. 任务优先级检查 1. 该优先级是否在 0 到 OS_LOWSEST_PRIO 之间? 2. 该优先级是否空闲?
  2. 调用 OSTaskStkInit(),创建任务的栈帧
  3. 调用 OSTCBInit(),从空闲的 OS_TCB 池(即 OSTCBFreeList 链表)中获得一个 TCB 并初始化其内容,然后把它加入到 OSTCBList 链表的开头,并把它设定为就绪状态
  4. 任务个数 OSTaskCtr 加 1
  5. 调用用户自定义的函数 OSTaskCreateHook()
  6. 判断是否需要调度(调用者是正在执行的任务)

OSTaskCreateExt()

1
2
3
4
5
6
7
8
INT8U OSTaskCreateExt(
//前四个参数与 OSTaskCreate 相同,
INT16U id, //任务的 ID
OS_STK *pbos, //指向任务栈底的指针
INT32U stk_size, //栈能容纳的成员数目
void *pext,//指向用户附加数据域的指针
INT16U opt //一些选项信息
);
  • 返回值:与 OSTaskCreate() 相同。

任务的栈空间

  1. 每个任务都有自己的栈空间(Stack),栈必须声明为 OS_STK 类型,并且由连续的内存空间组成;
  2. 栈空间的分配方法 1. 静态分配:在编译的时候分配,例如:static OS_STK MyTaskStack[stack_size];OS_STK MyTaskStack[stack_size]; 2. 动态分配:在任务运行的时候使用 malloc() 函数来动态申请内存空间;

动态分配

1
2
3
4
5
6
OS_STK *pstk;
pstk = (OS_STK *)malloc(stack_size);
/* 确认 malloc()得到足够的内存空间 */
if (pstk != (OS_STK *)0) {
// Create the task;
}

内存碎片问题

在动态分配中,可能存在内存碎片问题。特别是当用户反复地建立和删除任务时,内存堆中可能会出现大量的碎片,导致没有足够大的一块连续内存区域可用作任务栈,这时 malloc() 便无法成功地为任务分配栈空间。

栈的增长方向

栈的增长方向的设置

  1. 从低地址到高地址:在 OS_CPU.H 中,将常量 OS_STK_GROWTH设定为 0;
  2. 从高地址到低地址:在 OS_CPU.H 中,将常量 OS_STK_GROWTH设定为 1;
  3. OS_STK TaskStack[TASK_STACK_SIZE];
  4. OSTaskCreate(task, pdata,&TaskStack[TASK_STACK_SIZE-1],prio);

删除任务

OSTaskDel():删除一个任务,其 TCB 会从所有可能的系统数据结构中移除。任务将返回并处于休眠状态(任务的代码还在)。

  1. 如果任务正处于就绪状态,把它从就绪表中移出,这样以后就不会再被调度执行了;
  2. 如果任务正处于邮箱、消息队列或信号量的等待队列中,也把它移出;
  3. 将任务的 OS_TCBOSTCBList 链表当中移动到 OSTCBFreeList

任务也可以自我删除(并非真的删除,只是内核不再知道该任务)

1
2
3
4
5
void MyTask (void *pdata)
{
...... /* 用户代码 */
OSTaskDel(OS_PRIO_SELF);
}
  • OSTaskChangePrio():在程序运行期间,用户可以通过调用本函数来改变某个任务的优先级。INT8U OSTaskChangePrio(INT8U oldprio, INT8U newprio)
  • OSTaskQuery():获得一个任务的有关信息:获得的是对应任务的 OS_TCB 中内容的拷贝。

挂起和恢复任务

OSTaskSuspend():挂起一个任务

  1. 如果任务处于就绪态,把它从就绪表中移出;
  2. 在任务的 TCB 中设置 OS_STAT_SUSPEND 标志,表明该任务正在被挂起。

OSTaskResume():恢复一个任务

  1. 恢复被 OSTaskSuspend()起的任务;
  2. 清除 TCB 中 OSTCBStat 字段的 OS_STAT_SUSPEND

中断和时间管理

中断处理

中断:由于某种事件的发生而导致程序流程的改变。产生中断的事件称为中断源。

CPU 响应中断的条件:

  1. 至少有一个中断源向 CPU 发出中断信号;
  2. 系统允许中断,且对此中断信号未予屏蔽。

中断服务程序 ISR

中断一旦被识别,CPU 会保存部分(或全部)运行上下文(context,即寄存器的值),然后跳转到专门的子程序去处理此次事件,称为中断服务子程序(ISR)。

μC/OS-Ⅱ 中,中断服务子程序要用汇编语言来编写,然而,如果用户使用的 C 语言编译器支持在线汇编语言的话,用户可以直接将中断服务子程序代码放在 C 语言的程序文件中。

用户 ISR 的框架

  1. 保存全部 CPU 寄存器的值;
  2. 调用 OSIntEnter(),或直接把全局变量 OSIntNesting(中断嵌套层次)加 1;
  3. 执行用户代码做中断服务;
  4. 调用 OSIntExit()
  5. 恢复所有 CPU 寄存器;
  6. 执行中断返回指令。

OSIntEnter()

1
2
3
4
5
6
7
8
/* 在调用本函数之前必须先将中断关闭 */
void OSIntEnter (void){
if (OSRunning == TRUE) {
if (OSIntNesting < 255) {
OSIntNesting++;
}
}
}

OSIntExit()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void OSIntExit(void) {
OS_ENTER_CRITICAL(); //关中断
if ((--OSIntNesting | OSLockNesting) == 0) //判断嵌套是否为零
{ //把高优先级任务装入
OSIntExitY = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((OSIntExitY << 3) +
OSUnMapTbl[OSRdyTbl[OSIntExitY]]);
if (OSPrioHighRdy != OSPrioCur) {
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
OSCtxSwCtr++;
OSIntCtxSw();
}
}
OS_EXIT_CRITICAL(); //开中断返回
}

OSIntCtxSw()

在任务切换时,为什么使用 OSIntCtxSw() 而不是调度函数中的 OS_TASK_SW()

原因如下:

  1. 一半的任务切换工作,即 CPU 寄存器入栈,已经在前面做完了;
  2. 需要保证所有被挂起任务的栈结构是一样的。

调用中断切换函数 OSIntCtxSw() 后的堆栈情况

时钟节拍

  1. 时钟节拍是一种特殊的中断;
  2. μC/OS 需要用户提供周期性信号源,用于实现时间延时和确认超时。节拍率应在 10 到 100Hz 之间,时钟节拍率越高,系统的额外负荷就越重;
  3. 时钟节拍的实际频率取决于用户应用程序的精度。时钟节拍源可以是专门的硬件定时器,或是来自 50/60Hz 交流电源的信号。

时钟节拍 ISR

1
2
3
4
5
6
7
8
void OSTickISR(void) {
//(1)存处理器寄存器的值;
//(2)用 OSIntEnter()将 OSIntNesting 加 1;
//(3)用 OSTimeTick(); /*检查每个任务的时间延时*/
//(4)用 OSIntExit();
//(5)复处理器寄存器的值;
//(6)行中断返回指令;
}

时钟节拍函数 OSTimetick()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void OSTimeTick(void) {
OS_TCB* ptcb;
OSTimeTickHook(); // (1)
ptcb = OSTCBList; // (2)
while (ptcb->OSTCBPrio != OS_IDLE_PRIO) { // (3)
OS_ENTER_CRITICAL();
if (ptcb->OSTCBDly != 0) {
if (--ptcb->OSTCBDly == 0) {
if (!(ptcb->OSTCBStat & OS_STAT_SUSPEND)) { // (4)
OSRdyGrp |= ptcb->OSTCBBitY; // (5)
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
} else {
ptcb->OSTCBDly = 1;
}
}
}
ptcb = ptcb->OSTCBNext;
OS_EXIT_CRITICAL();
}
OS_ENTER_CRITICAL(); // (6)
OSTime++; // (7)
OS_EXIT_CRITICAL();
}

时间管理

  1. 与时间管理相关的系统服务: 1. OSTimeDLY() 2. OSTimeDLYHMSM() 3. OSTimeDlyResume() 4. OSTimeGet() 5. OSTimeSet()

OSTimeDLY()

OSTimeDLY():任务延时函数,申请该服务的任务可以延时一段时间;

调用 OSTimeDLY 后,任务进入等待状态;

使用方法:

  1. void OSTimeDly(INT16U ticks);
  2. ticks 表示需要延时的时间长度,用时钟节拍的个数 来表示。
1
2
3
4
5
6
7
8
9
10
11
12
void OSTimeDly (INT16U ticks){
if (ticks > 0){
OS_ENTER_CRITICAL();
if ((OSRdyTbl[OSTCBCur->OSTCBY] &=
~OSTCBCur->OSTCBBitX) == 0){
OSRdyGrp &= ~OSTCBCur->OSTCBBitY;
}
OSTCBCur->OSTCBDly = ticks;
OS_EXIT_CRITICAL();
OSSched();
}
}

问题

这个问题是指,对于一个 OSTimeDly() 操作而言,其实我们是不能严格延迟一个时间周期的,因为可能出现高优先级的事务,最好是 OSTimeDLY(2),同样对于 OSRTimeDly(1) 而言,其实严格意义上的 dly 时间是不确定的(抖动)。

解决方案

  1. 增加微处理器的时钟频率
  2. 增加时钟节拍的频率
  3. 重新安排任务的优先级
  4. 避免使用浮点运算(如果非使用不可,尽量用单精度数)
  5. 使用能较好地优化程序代码的编译器
  6. 时间要求苛刻的代码用汇编语言写
  7. 如果可能,用同一家族的更快的微处理器做系统升级。如从 8086 向 80186 升级, 从 68000 向 68020 升级等
  8. 不管怎么样,抖动总是存在的

OSTimeDlyHMSM()

  1. OSTimeDlyHMSM()OSTimeDly() 的另一个版本,即按时分秒延时函数;
  2. 使用方法
1
2
3
4
5
6
INT8U OSTimeDlyHMSM(
INT8U hours, // 小时
INT8U minutes, // 分钟
INT8U seconds, // 秒
INT16U milli // 毫秒
);

OSTimeDlyResume()

OSTimeDlyResume():让处在延时期的任务提前结束延时,进入就绪状态;

使用方法:

  1. INT8U OSTimeDlyResume(INT8U prio);
  2. prio 表示需要提前结束延时的任务的优先级/任务 ID。

系统时间

  1. 每隔一个时钟节拍,发生一个时钟中断,将一个 32 位的计数器 OSTime 加 1;
  2. 该计数器在用户调用 OSStart() 初始化多任务和 4,294,967,295 个节拍执行完一遍的时候从 0 开始计数。若时钟节拍的频率等于 100Hz,该计数器每隔 497 天就重新开始计数;
  3. OSTimeGet():获得该计数器的当前值;INT32U OSTimeGet(void)
  4. OSTimeSet():设置该计数器的值;void OSTimeSet(INT32U ticks)

何时启动系统定时器

  1. 如果在 OSStart 之前启动定时器,则系统可能无法正确执行完 OSStartHighRdy
  2. OSStart 函数直接调用 OSStartHighRdy 去执行最高优先级的任务,OSStart 不返回
  3. 系统定时器应该在系统的最高优先级任务中启动
  4. 使用 OSRunning 变量来控制操作系统的运行

时钟节拍的启动

用户必须在多任务系统启动以后再开启时钟节拍器,也就是调用 OSStart() 之后。

在调用 OSStart() 之后做的第一件事是初始化定时器中断:

1
2
3
4
5
6
7
8
void main(void)
{
OSInit(); /* 初始化 uC/OS-II*/
/* 应用程序初始化代码... */
/* 调用 OSTaskCreate()建至少一个任务*/
//允许时钟节拍中断; /* 错误!可能 crash!*/
OSStart(); /* 开始多任务调度 */
}

系统的初始化与启动

在调用 μC/OS-II 的任何其它服务之前,用户必须首先调用系统初始化函数 OSInit() 来初始化 μC/OS 的所有变量和数据结构;

OSInit() 建立空闲任务 OSTaskIdle(),该任务总是处于就绪状态,其优先级一般被设成最低,即 OS_LOWEST_PRIO;如果需要,OSInit() 还建立统计任务 OSTaskStat(),并让其进入就绪状态;

OSInit() 还初始化了 4 个空数据结构缓冲区:空闲 TCB 链表 OSTCBFreeList、空闲事件链表 OSEventFreeList、空闲队列链表 OSQFreeList 和空闲存储链表 OSMemFreeList

μC/OS-II 的启动

多任务的启动是用户通过调用 OSStart() 实现的。然而,启动 μC/OS-Ⅱ 之前,用户至少要建立一个应用任务。

1
2
3
4
5
6
7
8
void main (void) {
OSInit(); /* 初始化 uC/OS-II */
// ...
通过调用 OSTaskCreate() OSTaskCreateExt()
创建至少一个任务;
// ...
OSStart(); /*开始多任务调度! 永不返回*/
}

OSStart()

1
2
3
4
5
6
7
8
9
10
11
12
13
void OSStart(void) {
INT8U Y;
INT8U X;
if (OSRunning == FALSE) {
y = OSUnMapTbl[OSRdyGrp];
x = OSUnMapTbl[OSRdyTbl[y]];
OSPrioHighRdy = (INT8U)((Y << 3) + X);
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
OSTCBCur = OSTCBHighRdy;
OSStartHighRdy();
}
}

统计任务初始化函数 OSStatInit(void)

1
2
3
4
5
6
7
8
9
10
11
void OSStatInit(void) {
OSTimeDly(2);
OS_ENTER_CRITICAL();
OSIdleCtr = 0L;
OS_EXIT_CRITICAL();
OSTimeDly(OS_TICKS_PER_SEC);
OS_ENTER_CRITICAL();
OSIdleCtrMax = OSIdleCtr;
OSStatRdy = TRUE;
OS_EXIT_CRITICAL();
}

任务之间的通信与同步

任务间通信的管理:事件控制块 ECB

同步与互斥:

  1. 临界区(Critical Sections)
  2. 信号量(Semaphores)

任务间通信:

  1. 邮箱(Message Mailboxes)
  2. 消息队列(Message Queues)

事件控制块 ECB

所有的通信信号都被看成是事件(event), μC/OS-II 通过事件控制块(ECB)来管理每一个具体事件。

1
2
3
4
5
6
7
8
// ECB 数据结构
typedef struct {
void *OSEventPtr; /*指向消息或消息队列列的指针*/
INT8U OSEventTbl[OS_EVENT_TBL_SIZE];//等待任务列列表
INT16U OSEventCnt; /*计数器(当事件是信号量时)*/
INT8U OSEventType; /*事件类型:信号量、邮箱等*/
INT8U OSEventGrp; /*等待任务组*/
} OS_EVENT;

任务和 ISR 之间的通信方式

  1. 一个任务或 ISR 可以通过事件控制块 ECB(信号量、邮箱或消息队列)向另外的任务发信号;
  2. 一个任务还可以等待另一个任务或中断服务子程序给它发送信号。对于处于等待状态的任务,还可以给他指定一个最长等待时间。
  3. 多个任务可以同时等待同一个事件的发生。当该事件发生后,在所有等待该事件的任务中,优先级最高的任务得到了该事件并进入就绪状态,准备执行。

等待任务列列表

每个正在等待某个事件的任务被加入到该事件的 ECB 的等待任务列表中,该列表包含两个变量 OSEventGrpOSEventTbl[]

OSEventGrp 中,任务按优先级分组,8 个任务为一组,共 8 组,分别对应 OSEventGrp 当中的 8 位。当某组中有任务处于等待该事件的状态时,对应的位就被置位。同时,OSEventTbl[] 中的相应位也被置位。

使任务进入入/脱离等待状态

将一个任务插入到事件的等待任务列表中:

1
2
pevent->OSEventGrp |= OSMapTbl[prio >> 3];
pevent->OSEventTbl[prio >> 3] |= OSMapTbl[prio & 0x07];

从等待任务列表中删除一个任务:

1
2
3
if ((pevent->OSEventTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0) {
pevent->OSEventGrp &= ~OSMapTbl[prio >> 3];
}

在等待事件的任务列列表中查找优先级最高的

在等待任务列表中查找最高优先级的任务:

1
2
3
y = OSUnMapTbl[pevent->OSEventGrp];
x = OSUnMapTbl[pevent->OSEventTbl[y]];
prio = (y << 3) + x;

空闲 ECB 的管理

ECB 的总数由用户所需要的信号量、邮箱和消息队列的总数决定,由 OS_CFG.H 中的#define OS_MAX_EVENTS 定义。

在调用 OSInit() 初始化系统时,所有的 ECB 被链接成一个单向链表——空闲事件控制块链表;

每当建立一个信号量、邮箱或消息队列时,就从该链表中取出一个空闲事件控制块,并对它进行初始化。

ECB 的基本操作

OSEventWaitListInit()

  1. 初始化一个事件控制块。当创建一个信号量、邮箱或消息队列时,相应的创建函数会调用本函数对 ECB 的内容进行初始化,将 OSEventGrpOSEventTbl[] 数组清零;
  2. OSEventWaitListInit(OS_EVENT *pevent)
  3. pevent:指向需要初始化的事件控制块的指针。

OSEventTaskRdy()

  1. 使一个任务进入就绪态。当一个事件发生时,需要将其等待任务列表中的最高优先级任务置为就绪态;
  2. OSEventTaskRdy(OS_EVENT *pevent, void *msg, INT8U msk)
  3. msg:指向消息的指针;msk:用于设置 TCB 的状态。

OSEventTaskWait()

  1. 使一个任务进入等待状态。当某个任务要等待一个事件的发生时,需要调用本函数将该任务从就绪任务表中删除,并放到相应事件的等待任务表中;
  2. OSEventTaskWait(OS_EVENT *pevent)

同步与互斥

为了实现资源共享,一个操作系统必须提供临界区操作的功能;

μC/OS 采用关闭/打开中断的方式来处理临界区代码,从而避免竞争条件,实现任务间的互斥;

μC/OS 定义两个宏(macros)来开关中断,即:OS_ENTER_CRITICAL()OS_EXIT_CRITICAL()

这两个宏的定义取决于所用的微处理器,每种微处理器都有自己的 OS_CPU.H 文件。

μC/OS-II 中开关中断的方法

当处理临界段代码时,需要关中断,处理完毕后,再开中断;

关中断时间是实时内核最重要的指标之一;

在实际应用中,关中断的时间很大程度中取决于微处理器的结构和编译器生成的代码质量;

μC/OS-II 中采用了 3 种开关中断的方法

  1. OS_CRITICAL_METHOD==1:用处理器指令关中断,执行 OS_ENTER_CRITICAL(),开中断执行 OS_EXIT_CRITICAL()
  2. OS_CRITICAL_METHOD==2:实现 OS_ENTER_CRITICAL() 时,先在堆栈中保存中断的开/关状态,然后再关中断;实现 OS_EXIT_CRITICAL() 时,从堆栈中弹出原来中断的开/关状态;
  3. OS_CRITICAL_METHOD==3:把当前处理器的状态字保存在局部变量中(如 OS_CPU_SR),关中断时保存,开中断时恢复

信号量

  1. 信号量在多任务系统中的功能: 1. 实现对共享资源的互斥访问(包括单个共享资源或多个相同的资源); 2. 实现任务之间的行为同步;
  2. 必须在 OS_CFG.H 中将 OS_SEM_EN 开关常量置为 1,这样 μC/OS 才能支持信号量。
  3. μC/OS 中信号量由两部分组成:信号量的计数值(16 位无符号整数)和等待该信号量的任务所组成的等待任务表;
  4. 信号量系统服务: 1. OSSemCreate() 2. OSSemPend(), OSSemPost() 3. OSSemAccept(), OSSemQuery()

任务、ISR 和信号量的关系

创建一个信号量

OSSemCreate()

  1. 创建一个信号量,并对信号量的初始计数值赋值,该初始值为 0 到 65,535 之间的一个数;
  2. OS_EVENT OSSemCreate(INT16U cnt)
  3. cnt:信号量的初始值。

执行步骤:

  1. 从空闲事件控制块链表中得到一个 ECB;
  2. 初始化 ECB,包括设置信号量的初始值、把等待任务列表清零、设置 ECB 的事件类型等;
  3. 返回一个指向该事件控制块的指针。

OSSemPend()

  1. 等待一个信号量,即操作系统中的 P 操作,将信号量的值减 1;
  2. OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *err)

执行步骤:

  1. 如果信号量的计数值大于 0,将它减 1 并返回;
  2. 如果信号量的值等于 0,则调用本函数的任务将被阻塞起来,等待另一个任务把它唤醒
  3. 调用 OSSched() 函数,调度下一个最高优先级的任务运行。

OSSemPost()

  1. 发送一个信号量,即操作系统中的 V 操作,将信号量的值加 1;
  2. OSSemPost (OS_EVENT *pevent)

执行步骤:

  1. 检查是否有任务在等待该信号量,如果没有,将信号量的计数值加 1 并返回;
  2. 如果有,将优先级最高的任务从等待任务列表中删除,并使它进入就绪状态;
  3. 调用OSSched(),判断是否需要进行任务切换。

无等待地请求一个信号量

OSSemAccept()

  1. 当一个任务请求一个信号量时,如果该信号量暂时无效,则让该任务简单地返回,而不是进入等待状态;
  2. INT16U OSSemAccept(OS_EVENT *pevent);

执行步骤:

  1. 如果该信号量的计数值大于 0,则将它减 1,然后将信号量的原有值返回;
  2. 如果该信号量的值等于 0,直接返回该值(0)。

查询一个信号量的当前状态

OSSemQuery()

  1. 查询一个信号量的当前状态;
  2. INT8U OSSemQuery(OS_EVENT *pevent,OS_SEM_DATA *pdata)
  3. 将指向信号量对应事件控制块的指针 pevent 所指向的 ECB 的内容拷贝到指向用于记录信号量信息的数据结构 OS_SEM_DATA 数据结构的指针 pdata 所指向的缓冲区当中。

任务间通信

低级通信:

  1. 只能传递状态和整数值等控制信息,传送的信息量小;
  2. 例如:信号量

高级通信:

  1. 能够传送任意数量的数据;
  2. 例如:共享内存、邮箱、消息队列

共享内存

在 μC/OS-II 中如何实现共享内存?

  1. 内存地址空间只有一个,为所有的任务所共享!
  2. 为了避免竞争状态,需要使用信号量来实现互斥访问。

消息邮箱

邮箱(MailBox):一个任务或 ISR 可以通过邮箱向另一个任务发送一个指针型的变量,该指针指向一个包含了特定“消息”(message)的数据结构;

必须在 OS_CFG.H 中将 OS_MBOX_EN 开关常量置为 1,这样 μC/OS 才能支持邮箱。

一个邮箱可能处于两种状态:

  1. 满的状态:邮箱包含一个非空指针型变量;
  2. 空的状态:邮箱的内容为空指针 NULL

邮箱的系统服务:

  1. OSMboxCreate()
  2. OSMboxPost()
  3. OSMboxPend()
  4. OSMboxAccept()
  5. OSMboxQuery()

任务、ISR 和消息邮箱的关系

邮箱的系统服务

OSMboxCreate():创建一个邮箱

  1. 在创建邮箱时,须分配一个 ECB,并使用其中的字段 OSEventPtr 指针来存放消息的地址;
  2. OS_EVENT *OSMboxCreate(void *msg)
  3. msg:指针的初始值,一般情形下为 NULL

OSMboxPend():等待一个邮箱中的消息

  1. 若邮箱为满,将其内容(某消息的地址)返回;若邮箱为空,当前任务将被阻塞,直到邮箱中有了消息或等待超时
  2. OSMboxPend (OS_EVENT *pevent,INT16U timeout, INT8U *err)

OSMboxPost():发送一个消息到邮箱中

  1. 如果有任务在等待该消息,将其中的最高优先级任务从等待列表中删除,变为就绪状态;
  2. OSMboxPost(OS_EVENT *pevent, void *msg)

OSMboxAccept():无等待地请求邮箱内容

  1. 若邮箱为满,返回它的当前内容;若邮箱为空,返回空指针;
  2. OSMboxAccept (OS_EVENT *pevent)

OSMboxQuery():查询一个邮箱的状态

  1. OSMboxQuery (OS_EVENT *pevent,OS_MBOX_DATA *pdata)

消息队列

  1. 消息队列(Message Queue):消息队列可以使一个任务或 ISR 向另一个任务发送多个以指针方式定义的变量;
  2. 为了使 μC/OS 能够支持消息队列,必须在 OS_CFG.H 中将 OS_Q_EN 开关常量置为 1,并且通过常量 OS_MAX_QS 来决定系统支持的最多消息队列数。
  3. 一个消息队列可以容纳多个不同的消息,因此可把它看作是由多个邮箱组成的数组,只是它们共用一个等待任务列表:
  4. 消息队列的系统服务 1. OSQCreate() 2. OSQPend()、OSQAccept() 3. OSQPost()、OSQPostFront() 4. OSQFlush() 5. OSQQuery()

消息队列列的体系结构

队列列控制块

队列控制块数据结构

1
2
3
4
5
6
7
8
9
typedef struct os_q {
struct os_q *OSQPtr; //空闲队列控制块指针
void **OSQStart; //指向消息队列的起始地址
void **OSQEnd; //指向消息队列的结束地址
void **OSQIn; //指向消息队列中下一个插入消息的位置
void **OSQOut; //指向消息队列中下一个取出消息的位置
INT16U OSQSize; //消息队列中总的单元数
INT16U OSQEntries; //消息队列中当前的消息数量
} OS_EVENT;

空闲队列控制块的管理

  1. 每一个消息队列都要用到一个队列控制块。在 μC/OS 中,队列控制块的总数由 OS_CFG.H 中的常量 OS_MAX_QS 定义。
  2. 在系统初始化时,所有的队列控制块被链接成一个单向链表——空闲队列控制块链表 OSQFreeList。

消息缓冲区

创建一个消息队列列

OSQCreate()

  1. OS_EVENT *OSQCreate (void **start, INT16U size)
  2. start:指针数组,用来存放各个消息的地址
  3. size:数组的大小(即消息队列的元素个数)

执行步骤

  1. 从空闲事件控制块链表中取得一个 ECB;
  2. 从空闲队列控制块列表中取出一个队列控制块,并对其进行初始化;
  3. 初始化 ECB 的内容(事件类型、等待任务列表),并将 OSEventPtr 指针指向队列控制块。

队列列控制块与事件控制块

请求消息队列列中的消息

OSQPend():等待一个消息队列中的消息

  1. void *OSQPend (OS_EVENT *pevent, INT16U timeout, INT8U *err)
  2. 如果消息队列中有至少一条消息,返回消息的地址;
  3. 如果没有消息,相应任务进入等待状态。

OSQAccept():无等待地请求消息队列中的消息

  1. void *OSQAccept(OS_EVENT *pevent);
  2. 如果消息队列中有消息,返回消息的地址;
  3. 如果消息队列中没有消息,返回 NULL

向消息队列列发送一个消息

OSQPost():以 FIFO 方式向消息队列发送一个消息

  1. INT8U OSQPost (OS_EVENT *pevent, void *msg)
  2. 如果有任务在等待该消息队列,唤醒其中优先级最高的任务,并重新调度;
  3. 如果没有任务在等待该消息队列,而且此时消息队列未满,则以 FIFO 方式插入这个消息。

OSQPostFront():以 LIFO 方式向消息队列发送一个消息:INT8U OSQPostFront(OS_EVENT *pevent, void *msg)

清空操作与查询操作

OSQFlush():清空一个消息队列

  1. INT8U OSQFlush (OS_EVENT *pevent);
  2. 删除一个消息队列中的所有消息;

OSQQuery():查询一个消息队列的状态

  • INT8U OSQQuery (OS_EVENT *pevent,OS_Q_DATA *pdata)

存储管理

概述

μC/OS 中是实模式存储管理

  1. 不划分内核空间和用户空间,整个系统只有一个地址空间,即物理内存空间,应用程序和内核程序都能直接对所有的内存单元进行访问;
  2. 系统中的“任务”,实际上都是线程---只有运行上下文和栈是独享的,其他资源都是共享的。

内存布局:代码段(text)、数据段(data)、bss 段、堆空间、栈空间;

malloc/free?

在 ANSI C 中可以用 malloc()free() 两个函数动态地分配内存和释放内存。在嵌入式实时操作系统中,容易产生碎片。

由于内存管理算法的原因,malloc()free() 函数执行时间是不确定的。μC/OS-II 对 malloc()free() 函数进行了改进,使得它们可以分配和释放固定大小的内存块。这样一来,malloc()free() 函数的执行时间也是固定的了

μC/OS 中的存储管理理

  1. μC/OS 采用的是固定分区的存储管理方法 1. μC/OS 把连续的大块内存按分区来管理,每个分区包含有整数个大小相同的块; 2. 在一个系统中可以有多个内存分区,这样,用户的应用程序就可以从不同的内存分区中得到不同大小的内存块。但是,特定的内存块在释放时必须重新放回它以前所属于的内存分区; 3. 采用这样的内存管理算法,上面的内存碎片问题就得到了解决。

内存控制块

为了便于管理,在 μC/OS 中使用内存控制块 MCB(Memory Control Block)来跟踪每一个内存分区,系统中的每个内存分区都有它自己的 MCB。

1
2
3
4
5
6
7
typedef struct {
void *OSMemAddr; /*分区起始地址*/
void *OSMemFreeList;//下一个空闲内存块
INT32U OSMemBlkSize; /*内存块的大小*/
INT32U OSMemNBlks; /*内存块数量*/
INT32U OSMemNFree; /*空闲内存块数量*/
} OS_MEM;

内存管理初始化

如果要在 μC/OS-II 中使用内存管理,需要在 OS_CFG.H 文件中将开关量 OS_MEM_EN 设置为 1。这样 μC/OS-II 在系统初始化 OSInit() 时就会调用 OSMemInit(),对内存管理器进行初始化,建立空闲的内存控制块链表。

创建一个内存分区

OSMemCreate()

1
2
3
4
5
OS_MEM *OSMemCreate (
void *addr, // 内存分区的起始地址
INT32U nblks, // 分区内的内存块数
INT32U blksize,// 每个内存块的字节数
INT8U *err); // 指向错误码的指针

例子

1
2
3
OS_MEM *CommTxBuf;
INT8U CommTxPart[100][32];
CommTxBuf = OSMemCreate(CommTxPart, 100, 32, &err);

OSMemCreate()

  1. 从系统的空闲内存控制块中取得一个 MCB;
  2. 将这个内存分区中的所有内存块链接成一个单向链表;
  3. 在对应的 MCB 中填写相应的信息。

分配一个内存块

  1. void *OSMemGet(OS_MEM *pmem, INT8U *err);
  2. 功能:从已经建立的内存分区中申请一个内存块。该函数的唯一参数是指向特定内存分区的指针。如果没有空闲的内存块可用,返回 NULL 指针。
  3. 应用程序必须知道内存块的大小,并且在使用时不能超过该容量。

释放一个内存块

  1. INT8U OSMemPut(OS_MEM *pmem, void *pblk)
  2. 功能:将一个内存块释放并放回到相应的内存分区中。
  3. 注意:用户应用程序必须确认将内存块放回到了正确的内存分区中,因为 OSMemPut() 并不知道一个内存块是属于哪个内存分区的。

等待一个内存块

  1. 如果没有空闲的内存块,OSMemGet() 立即返回 NULL。能否在没有空闲内存块的时候让任务进入等待状态?
  2. μC/OS-II 本身在内存管理上并不支持这项功能,如果需要的话,可以通过为特定内存分区增加信号量的方法,来实现此功能。
  3. 基本思路:当应用程序需要申请内存块时,首先要得到一个相应的信号量,然后才能调用 OSMemGet() 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
OS_EVENT* SemaphorePtr;
OS_MEM* PartitionPtr;
INT8U Partition[100][32];
OS_STK TaskStk[1000];
void main(void) {
INT8U err;
OSInit();
// ...
SemaphorePtr = OSSemCreate(100);
PartitionPtr = OSMemCreate(Partition, 100, 32, &err);
OSTaskCreate(Task, (void*)0, &TaskStk[999], &err);
OSStart();
}
void Task(void* pdata) {
INT8U err;
INT8U* pblock;
for (;;) {
OSSemPend(SemaphorePtr, 0, &err);
pblock = OSMemGet(PartitionPtr, &err);
/* 使用内存块 */
// ...
OSMemPut(PartitionPtr, pblock);
OSSemPost(SemaphorePtr);
}
}

freertos 内存管理理

三种 pvPortMalloc()vPortFree() 的实现范例

Heap_1.c

  1. 其实现了一个非常基本的 pvPortMalloc() 版本,而没有实现 vPortFree()。如果应用程序不需要删除任务,队列或者信号量,则其具有使用 heap_1 的潜质。其具有确定性。
  2. 这种分配方案将 FreeRTOS 的内存堆空间堪称一个简单的数组。当调用 pvPortMalloc() 时,则将数组又简单的细分成为更小的内存块。数组大小在 FreeRTOSConfig.h 中由 configTOTAL_HEAP_SIZE 定义。

Heap_2.c

  1. 其采用了一个最佳匹配算法来分配内存,并支持内存释放。由于声明了一个静态数组,所以会让整个应用程序看起来耗费了很多内存,即使是在数组没有进行任何实际分配之前。
  2. 最佳匹配算法保证 pvPortMalloc() 会使用最接近请求大小的空间块。例如: 1. 对空间包含了三个空闲内存块,分别为 5 字节,25 字节和 100 字节。 2. pvPortMalloc() 被调用用以请求分配 20 字节大小的内存空间。
  3. Heap_2.c 不会把相邻的空闲块合并成一个更大的内存块,所以会产生内存碎片如果分配和释放的总是相同大小的内存块,则内存碎片不会称为一个问题。所以 Heap_2.c 适合于那些重复创建与删除具有相同空间任务的应用程序。

Heap_3.c

简单的调用了标准库 malloc()free(),但是通过暂时挂起调度器使得函数调用具备了线程安全特性。