梗概
内容:针对实时操作系统 uc/os-ii的课程相关笔记
作者:电科Lee
参考:[1].BV1SK411H7YY
[2].课本
日期:2024/6/18
一、任务
任务五种状态
睡眠状态 | 当任务只有代码存在但是没有在main函数中OSTaskCreate时,任务是睡眠状态 |
---|---|
就绪状态 | 当任务被create了且具备执行的所有条件,只是在等待CPU的使用权 |
运行状态 | 任务正在占用CPU |
等待状态 | 执行中的任务等待延时、信号量等而暂时等待的状态 |
中断服务状态 | 运行中的任务被中断打断的状态 |
系统任务
空闲任务:
空闲任务即是无可用的用户任务运行时的一个状态。否则操作系统空转会死机
统计任务:
统计任务是每秒计算一次CPU在单位时间内被使用的时间,并将结果放在变量OSCPUsage中,以便其他程序了解CPU的执行情况。结果为百分比的形式。
如果用户程序决定使用统计任务,则必须定义在系统头文件OS_CFG.H中的系统配置常数OS_TASK_STAT_EN设置为一,并且要调用函数OSStatInit()对统计任务进行初始化。
任务控制块
任务就绪表
就绪表有横纵之分
纵向为 uint8_t OSRdyGrp,即一个8bit的数,
横向为就绪表的第二维度,因为:
uint8_t OSRdyGrp[8],即8个组,每个组都是8bit,对应横坐标的位置
操作系统查找最高优先级任务
此过程将分为2个步骤解读:1、新任务加入就绪表
2、从当前就绪表中找出最高优先级的任务
新任务加入就绪表
当我们写代码时,会在0~63这64个优先级中挑选一个给我们的任务,例如给新任务的优先级是20
把任务加入就绪表就是把任务控制块中的 OSTCBBitY、OSTCBBitX赋值给 OSRdyGrp和OSRdyTbl[OSRdyGrp]:
我们由OSRdyGrp和OSRdyTbl可以得到64为的就绪表,所以需要把新任务的“标识”插入进去,
ptcb->OSTCBY是OSRdyGrp的第几位,即就绪表的第几行
ptcb->OSTCBX是OSRdyTbl[OSRdyGrp]=OSRdyTbl[ptcb->OSTCBY]的第几位,由行列坐标(8*8)可得到任务在就绪表中的位置
然后把哪一个的第几位的“几”由二进制位移变成真正的位
1向右平移
ptcb->OSTCBBitY = (OS_PRIO)(1uL << ptcb->OSTCBY);
ptcb->OSTCBBitX = (OS_PRIO)(1uL << ptcb->OSTCBX);
OSRdyGrp|= ptcb->OSTCBBitY; (task.c OS_TCBInit( ) )
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
//OSTCBBitX,OSTCBBitY在之的任务控制块(TCB)前中有提到过,其计算方式为:
ptcb->OSTCBY = (INT8U)(prio >> 3u);//右移3位,相当于除8,结果就是该优先级对应于哪一个组
ptcb->OSTCBX = (INT8U)(prio & 0x07u);
ptcb->OSTCBBitY = (OS_PRIO)(1uL << ptcb->OSTCBY); //对应于OSTCBY位置的位置为1,其余位为0,用于在之后更新就绪表和就绪数组时候用
ptcb->OSTCBBitX = (OS_PRIO)(1uL << ptcb->OSTCBX);//对应于OSTCBX位置的位置为1,其余位为0,用于在之后更新就绪表时候用
OSRdyGrp|= ptcb->OSTCBBitY; //通过OR运算,将对应组的位置为1
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;//通过OR运算,将该优先级在数组中对应位置的位置为1。
原文链接:https://blog.csdn.net/mcu_tian/article/details/48215139
20 = 0001 0100 = 010 100
ptcb->OSTCBY = 010 //2
ptcb->OSTCBX = 100 //4
ptcb->OSTCBBitY = (1<<2) //0100
ptcb->OSTCBBitX = (1<<4) //1 0000
OSRdyGrp|= ptcb->OSTCBBitY //与运算,把ptcb->OSTCBBitY添加到OSRdyGrp中
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX //与运算,把ptcb->OSTCBBitX添加到OSRdyTbl[ptcb->OSTCBY]中
究其本质来看,是把10进制的优先级 20 转换成 横纵坐标的二进制 100、010,此时的二进制的含义是“第几位”,然后通过移位运算、取或运算真正添加到OSRdyGrp、OSRdyTbl[ptcb->OSTCBY]的对应bit位上
从当前就绪表中找出最高优先级的任务
我们首次添加任务时,可能是 OSRdyGrp :0000 0100
OSRdyTbl[x]:0000 1000
多次添加后可能是: OSRdyGrp : 1000 0101
OSRdyTbl[0000 0001]:0000 1000
OSRdyTbl[0000 0100]:0100 0000
OSRdyTbl[1000 0000]:1000 0110
即第一行的第四列、第三行的第七列、第八行的第二三八列均有任务,从中找到最高优先级的任务就是我们的目标
UCOS ii 中采取 unmaptbl(反映射表)的方式来快速定位
unmaptbl是一个256个成员的数组,可以利用其找到8位二进制数出现1的最低位
y = OSUnMapTbl[OSRdyGrp]; //(os_core.c OS_SchedNew())
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
通过对OSRdyGrp和对应的OSRdyTbl[OSRdyGrp]反映射,可以得到最低位的优先级,也就是最高优先级
如当前就绪表中最低优先级的任务的prio是 12 即 001 100
对应的OSUnMapTbl[OSRdyGrp] = 1
OSUnMapTbl[OSRdyTbl[1]] = 4
OSPrioHighRdy = (1<<3)+ 4 =12
这样就取出了最高优先级
unmaptbl:
方向:------------------------------------------------------------------->
|
|
|
|
|
V
INT8U const OSUnMapTbl[256] = {
0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x00 to 0x0F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x10 to 0x1F */
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x20 to 0x2F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x30 to 0x3F */
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x40 to 0x4F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x50 to 0x5F */
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x60 to 0x6F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x70 to 0x7F */
7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x80 to 0x8F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x90 to 0x9F */
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xA0 to 0xAF */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xB0 to 0xBF */
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xC0 to 0xCF */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xD0 to 0xDF */
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xE0 to 0xEF */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0 /* 0xF0 to 0xFF */
};
为什么OSUnMapTbl是256位?
因为查找时,OSRdyGrp不一定只有一位由任务 0000 0010,可能是0100 0010、1111 1111,最大七组全有任务,为255,0~255共256个数
OSRdyTbl同理。
为什么OSUnMapTbl可以快速定位8bit数的最低有1的位?
提前遍历的所有结果,如OSRdyGrp = 0001 1001 = 25,可以找到OSUnMapTbl[25] = 0
二、ucos中断与时钟
中断相关概念
中断:在任务运行过程之中,应内部、外部异步事件的请求终止当前任务,而去处理异步事件所要求的任务的过程
中断服务子程序:应中断请求而运行的程序叫中断服务子程序
中断向量:中断服务程序的入口地址
中断过程
大体过程:系统接收到中断请求后中止当前任务,进入中断向量指向的中断服务程序,执行完后中断返回,进行一次中断级调度
中断响应流程:
中断服务程序流程:
中断退出流程:
中断响应流程中,进入中断和退出中断近似对称,先转到中断向量、再保存CPU、再通知内核进入ISR,结束时通知内核退出ISR、恢复CPU、中断返回
在中断返回时会判断是否还在中断嵌套中,是否锁定了调度器,如果都不是则进行中断级任务调度,运行最高优先级的任务
临界段
临界段:处于关中断与开中断之间的函数
三种实现方法:
1:直接进行关中断 asm(DI)、asm(EI)
2:先保存CPU的状态字PSW再进行开关中断,可以保证开中断后恢复状态字后CPU不变,即增加栈入栈出PSW的过程
3:当C编译器具有扩展功能时可以利用c语言直接把PSW赋值给变量,再利用C语言开关中断
应明确都是在汇编的基础上实现的
时钟相关
时钟节拍 :time ticks
最小的时钟单位就是两次中断之间间隔的时间,这个最小的时钟单位就叫做 时钟节拍
在每个OSTickISR()中断服务函数中调用OSTimeTick(),其作用是1、给宏定义的OSTime+1.2、遍历任务控制块链表中用来存放延时信息的OSTCBDly,令其值-1
时间管理
相关函数有 OSTimeDly(); OSTimeDlyHMSM(); … …
每个函数的最后会进行调度
还可以取消任务延时 OSTimeDlyResume(INT8U Prio)
三、任务间的同步与事件
理论依据
在UCOS中的任务是以并发的形式存在的,有时候任务之间需要的资源需要共享,需要一种机制来分配资源,制约任务之间的关系,会产生两种制约关系:1、直接制约 2、间接制约
直接制约是指两个任务具有合作关系,一个任务的完成依赖于另一个任务的完成
间接制约是指两个或多个任务对资源的共享,得不到资源的任务需要等待其他任务释放资源
表现为: 1、 同步 2、 互斥
事件
信号量:信号量算是一种对资源的标记,根据值的不同具有互斥型信号量与信号量之分
消息邮箱:通过传递消息缓冲区的指针来传输信息
消息队列:当传递的信息的 缓冲区指针 是指针队列时,队列内每个成员都是一个缓存区的地址
事件控制块
#if (OS_EVENT_EN) && (OS_MAX_EVENTS > 0u)
typedef struct os_event {
INT8U OSEventType;
void *OSEventPtr;
INT16U OSEventCnt;
OS_PRIO OSEventGrp;
OS_PRIO OSEventTbl[OS_EVENT_TBL_SIZE];
#if OS_EVENT_NAME_EN > 0u
INT8U *OSEventName;
#endif
} OS_EVENT;
OSEventType: 事件控制块类型,包括OS_EVENT_TYPE_SEM,MUTEX,MBOX,Q(queue),UNUSED
OSEventPtr: 指向缓冲区地址/队列地址
OSEventCnt: 信号量值
OSEventTbl: 等待任务表,与TCB不同,每个事件都具有一个,记录等待此事件的任务
OSEventGrp: 等待任务表分组,类似OSTCBGrp的作用
空任务控制块链表
static void OS_InitEventList (void)
{
#if (OS_EVENT_EN) && (OS_MAX_EVENTS > 0u)
#if (OS_MAX_EVENTS > 1u)
INT16U ix;
INT16U ix_next;
OS_EVENT *pevent1;
OS_EVENT *pevent2;
OS_MemClr((INT8U *)&OSEventTbl[0], sizeof(OSEventTbl)); /* Clear the event table */
for (ix = 0u; ix < (OS_MAX_EVENTS - 1u); ix++) { /* Init. list of free EVENT control blocks */
ix_next = ix + 1u;
pevent1 = &OSEventTbl[ix];
pevent2 = &OSEventTbl[ix_next];
pevent1->OSEventType = OS_EVENT_TYPE_UNUSED;
pevent1->OSEventPtr = pevent2;
#if OS_EVENT_NAME_EN > 0u
pevent1->OSEventName = (INT8U *)(void *)"?"; /* Unknown name */
#endif
}
pevent1 = &OSEventTbl[ix];
pevent1->OSEventType = OS_EVENT_TYPE_UNUSED;
pevent1->OSEventPtr = (OS_EVENT *)0;
#if OS_EVENT_NAME_EN > 0u
pevent1->OSEventName = (INT8U *)(void *)"?"; /* Unknown name */
#endif
OSEventFreeList = &OSEventTbl[0];
}
可知初始化时的空任务控制块链表有OS_MAX_EVENTS个,即最多有OS_MAX_EVENTS个事件
pevent1->OSEventPtr = pevent2;
这句可以知道OSEVENTPtr还有在初始化时指向下一个空任务链表的作用
信号量
OSEventType 为 OS_EVENT_TYPE_SEM
创建信号量:
OS_EVENT *OSSemCreate (INT16U cnt);
OSSemCreate(int16u cnt),返回ECB的指针,则接收OSSemCreate的变量为 OS_EVENT *
中断中不可创建
请求信号量:
void OSSemPend (OS_EVENT *pevent,
INT32U timeout, //0时表示无限等待
INT8U *perr);
主要执行OSEventCnt - 1的操作,如果OSEventCnt = 0,则把当前任务加入该信号量的等待任务列表
OSSemAccept()也是请求信号量,不过它即使信号量不可请求也不等待,继续执行
发送信号量:
INT8U OSSemPost (OS_EVENT *pevent);
主要执行OSEventCnt + 1的操作,如果还有等待该信号量的任务,则进行调度,不再+1,直接去运行等待任务列表中最高优先级的任务
删除信号量:
OS_EVENT *OSSemDel (OS_EVENT *pevent,
INT8U opt,
INT8U *perr);
opt:删除信号量的条件,可选的有OS_DEL_NO_PEND,没有等待任务时删除、OS_DEL_ALWAYS,直接删除
不可在中断中删除
查询信号量的状态:
INT8U OSSemQuery (OS_EVENT *pevent,
OS_SEM_DATA *p_sem_data);
优先级反转
优先级:A>B>C
信号量:只能被一个任务占用
1:程序运行到某一时刻A、B处于delay状态,C运行且占用了信号量,
2:A结束delay,接管CPU
3:A运行中需要信号量,但是被C占用,只好等待
4:CPU转到C的手上,C继续运行
5:B结束delay,接管CPU
6:B运行结束后(未请求信号量)释放CPU
7:C得到B的CPU使用权,一段时间后释放信号量
8:在 B、C之后,A得到信号量而运行
从6、7、8来看,只有B运行完后C才有机会释放信号量,只有释放信号量A才能运行
实际的优先级变成了 B>A,优先级反转
出现此现象的原因的中间优先级打断了低优先级释放信号量,使得其优先级高于高优先级任务,解决方法可以是提高低优先级任务的优先级,使其不被打断尽快完成信号量释放
互斥型信号量
为二值信号量,其OSEventCnt不计数用,而是分为高低8位,
高八位记录要改变的优先级,避免出现优先级反转
低八位为0XFF时有效,否则MUTEX无效
相关函数:
OS_EVENT *OSMutexCreate (INT8U prio,
INT8U *perr);
OS_EVENT *OSMutexDel (OS_EVENT *pevent,
INT8U opt,
INT8U *perr);
BOOLEAN OSMutexAccept (OS_EVENT *pevent,
INT8U *perr);
void OSMutexPend (OS_EVENT *pevent,
INT32U timeout,
INT8U *perr);
INT8U OSMutexPost (OS_EVENT *pevent);
INT8U OSMutexQuery (OS_EVENT *pevent, OS_MUTEX_DATA *p_mutex_data);
要接收OSMutexQuery,则需要定义一个OS_MUTEX_DATA的结构体接收*p_mutex_data
信号量集机制
如果某一任务需要很多的临界资源,如果所需要的临界资源全部准备就绪,那么所有需要的资源全部分配,又缺少则全不分配给该任务
信号量集的相关参数设置
相关函数:
四、任务间的通信
使用场景
ucos提供的通信机制
ucos ii 提供两种通信方式:邮箱 、 队列
消息邮箱机制
消息邮箱传递消息缓冲区(存储要传输的信息)的指针,那么接收的任务只需要按照消息邮箱中附带的指针去访问消息缓冲区就可以得知传输的信息是什么
消息邮箱事件
相关函数:
void *OSMboxAccept (OS_EVENT *pevent);
OS_EVENT *OSMboxCreate (void *pmsg);
OS_EVENT *OSMboxDel (OS_EVENT *pevent,
INT8U opt,
INT8U *perr);
void *OSMboxPend (OS_EVENT *pevent,
INT32U timeout,
INT8U *perr);
INT8U OSMboxPendAbort (OS_EVENT *pevent,
INT8U opt,
INT8U *perr);
INT8U OSMboxPost (OS_EVENT *pevent,
void *pmsg);
INT8U OSMboxPostOpt (OS_EVENT *pevent,
void *pmsg,
INT8U opt);
INT8U OSMboxQuery (OS_EVENT *pevent,
OS_MBOX_DATA *p_mbox_data);
任务与中断服务对函数的使用
消息队列机制
由三部分构成:
ECB、消息队列、消息
OSEventPtr指针指向一个队列,队列分别指向不同的需要传输的消息缓冲区
消息队列事件:
OSEventPtr指向OS_Q
OSQStart 头
OSQSize 消息队列的大小
OSQOut 表示数据出栈的地方
OSQIn 表示数据入栈的地方
OSEntries 当前MsgTbl中指针的个数
MsgTbl[]只保存指向消息缓存区的指针
实际上,在OSIn指向末尾后仍然栈入数据的话,会刷新掉第一个位置的指针构成一种循环队列
队列控制块(OS_QList)
相关函数
函数的使用:
五、信号量集
基本概念
信号往往是设置的程序能否运行的条件
if()
......
else()
......
而信号量实现了多个任务申请同一资源时对资源使用的管理,增加了“量”的概念,由OSEventCnt指定
信号量集则体现了更加现实的情况,完成一个任务可能需要的资源或者说需要满足的前提不止一个,这些前提可能满足一个即可可能需要全部满足更甚者只有在这些条件全不满足时任务才能运行,信号量集体现了任务运行需要满足的条件的逻辑运算
由上可知,实现信号量集的操作,应该有一个结构体储存当前信号量集中各个信号的状态,还应该有一个结构体储存任务请求时所要求的信号量集的状态,最后,要指向请求信号量集的任务:
OSFlagFlags:存储信号量集中各信号的状态
OSFlagNodeFlagGrp:指定任务需要的哪几个信号,1有效0无效
(OSFlagFlags与OSFlagNodeFlagGrp位数相等)
OSFlagNodeWaitType:对指定信号的逻辑操作,见下表
信号量集的操作
OS_FLAG_GRP *OSFlagCreate (OS_FLAGS flags,
INT8U *perr);
OS_FLAGS OSFlagPend (OS_FLAG_GRP *pgrp,
OS_FLAGS flags,
INT8U wait_type,
INT32U timeout,
INT8U *perr);
OS_FLAGS OSFlagPost (OS_FLAG_GRP *pgrp,
OS_FLAGS flags,
INT8U opt,
INT8U *perr);
OS_FLAG_GRP *OSFlagDel (OS_FLAG_GRP *pgrp,
INT8U opt,
INT8U *perr);
OS_FLAGS OSFlagAccept (OS_FLAG_GRP *pgrp,
OS_FLAGS flags,
INT8U wait_type,
INT8U *perr);
INT8U OSFlagNameGet (OS_FLAG_GRP *pgrp,
INT8U **pname,
INT8U *perr);
void OSFlagNameSet (OS_FLAG_GRP *pgrp,
INT8U *pname,
INT8U *perr);
OS_FLAGS OSFlagPendGetFlagsRdy (void);
OS_FLAGS OSFlagQuery (OS_FLAG_GRP *pgrp,
INT8U *perr);
OSFlagCreate:创建一个空标志组,从空标志组链表中取一个下来,最大数量由OS_MAX_FLAGS确定
OSFlagPend:申请一个信号量集,相当于添加一个FlagNode到 OSFlagWaitList,同时把TCB链接上
OSFlagPost:向信号量集发送数据,即改变OSFlagFLags中的值
OSFlagDel:删除信号量集
OSFlagUnlink:把FLagNode与标志组FLagGrp解挂,在OSFLagPost中调用
六、动态内存管理
堆空间为了实时性可靠性,不适用malloc动态内存分配,故做一套内存管理的机制
内存管理
内存管理的就是原来应该由动态分配内存函数malloc分配的 剩下的内存空间
内存管理方法
固定分区:
动态分区法:
静态分区不能做到量体裁衣,造成内存空间的浪费
内存管理机制
ucos-ii中使用的时固态分区法
先把内存空间分区,再对每个分区分块,每块大小相等
我们创建时,只能创建一个分区的内存空间,定义的是一个分区的块数和块的字节数
OS_MEM *CommTxBuffer;
INT8U CommTxPart[5][8];
......
......
CommTxBuffer = OSMEMCreate(CommTxPart,//内存块首地址,内存分区地址
5,//内存分区中块数
8,//内存分区每块大小
&err);
分配后,每个内存块都有用来存放指向下一个内存块地址的空间,即内存块至少有两块(一块怎么指向下一块?),每个内存块空间至少能够存放一个指针
内存分区控制块:
相关函数
OS_MEM *OSMemCreate (void *addr,
INT32U nblks,
INT32U blksize,
INT8U *perr);
void *OSMemGet (OS_MEM *pmem,
INT8U *perr);
INT8U OSMemNameGet (OS_MEM *pmem,
INT8U **pname,
INT8U *perr);
void OSMemNameSet (OS_MEM *pmem,
INT8U *pname,
INT8U *perr);
INT8U OSMemPut (OS_MEM *pmem,
void *pblk);
INT8U OSMemQuery (OS_MEM *pmem,
OS_MEM_DATA *p_mem_data);
OSMemGet:返回一个有效内存块的地址,要创建一个指针类型变量接收,释放时传入这个指针
OSMemPut:释放内存块,必须要释放到对应的内存分区中
七、移植
移植时只修改三个文件:
os_cpu.h
os_cpu_A.asm
os_cpu_c.c
ucos体系结构
移植文件作用
INT8U *perr);
void OSMemNameSet (OS_MEM *pmem,
INT8U *pname,
INT8U *perr);
INT8U OSMemPut (OS_MEM *pmem,
void *pblk);
INT8U OSMemQuery (OS_MEM *pmem,
OS_MEM_DATA *p_mem_data);
[外链图片转存中...(img-kAIocUgu-1721902945560)]
OSMemGet:返回一个有效内存块的地址,要创建一个指针类型变量接收,释放时传入这个指针
OSMemPut:释放内存块,必须要释放到对应的内存分区中
## 七、移植
移植时只修改三个文件:
os_cpu.h
os_cpu_A.asm
os_cpu_c.c
[外链图片转存中...(img-eiVd2zzX-1721902945561)]
### ucos体系结构
[外链图片转存中...(img-MVBZ34Pn-1721902945561)]
### 移植文件作用
[外链图片转存中...(img-tNF2o1Ra-1721902945561)]