第一章 μCOS 简介
1.1 初识 μCOS
实际上,一个 CPU 核心在某一时刻只能运行一个任务,由于切换处理任务的速度非常快,因此给人造成了一种同一时刻有多个任务同时运行的错觉。
操作系统的分类方式可以由任务调度器的工作方式决定,比如有的操作系统给每个任务分配同样的运行时间,时间到了就切换到下一个任务,Unix 操作系统就是这样的。像 μCOS这种传统的 RTOS 类操作系统是由用户给每个任务分配一个任务优先级,任务调度器根据此优先级来决定任务运行顺序。
1.2 µC/OS-III 的特点
µC/OS-III 是一个可裁剪、可固化到 ROM 的抢占式实时内核,并且可管理任务的数量不受限制。µC/OS-III 是第三代的 µC/OS 内核,具有以下几个重要的特性:
抢占式多任务管理:µC/OS-III 是一个支持多任务抢占的内核,因此总是优先执行任务优先级高的任务。
时间片调度:如果处于就绪状态的任务有多个相同优先级,那么 µC/OS-III 将根据用户指定的时间(时间片)轮流调度这些任务。
极短的中断禁用时间:µC/OS-III 通过锁定任务调度器代替禁用中断来保护临界区,确保了 µC/OS-III 能够快速地响应中断。
任务数量不限:µC/OS-III 理论上支持不受限制的任务数量,但实际上,系统中任务的最大
数量受处理器内存大小限制。
任务优先级数量不限:µC/OS-III 支持的任务优先级数量不受限制,但对于大多数应用场景
而言,使用 32~256 个任务优先级就绰绰有余了。
内核对象数量不限:µC/OS-III 提供了多种内核对象,如任务、信号量、事件标志、消息队
列、软件定时器和内存区等,并且在不考虑处理器内存限制的情况下,用户可以无限制的创建这些内核对象。
时间戳:µC/OS-III 提供了时间戳功能,便于测量处理器处理某些事件所消耗的时间,以方便用户对系统进行针对性的优化。
自定义钩子函数:µC/OS-III 提供了一些在内核执行操作之前、之后或过程中的钩子函数,
这样可以方便用户扩展 µC/OS-III 的功能。
防死锁:µC/OS-III 允许设置任务等待的最大超时时间,可以有效地防止死锁的发生。
软件定时器:在 µC/OS-III 中,用户可以创建任意数量的“单次”和“周期”软件定时器,并且每个软件定时器都可以有独立的超时回调函数。
任务内嵌信号量:µC/OS-III 提供了任务的内嵌信号量功能,这使得任务可以直接获取来自其他任务或中断的信号,不需要任何的中间内核对象,大大地提高了系统的运行效率。
任务内嵌消息队列:µC/OS-III 提供了任务的内嵌消息队列,这使得任务可以直接接收来自其他任务或中断的消息,而不需要任何的中间内核对象,大大地提高了系统的运行效率。
第二章 µC/OS-III 中断管理
任务切换、系统时钟节拍等等,都是利用中断来完成的。
2.1 ARM Cortex-M 中断
2.1.1 ARM Cortex-M 中断简介
中断是 CPU 的一种常见特性,中断一般由硬件产生,当中断发生后,会中断 CPU 当前正在执行的程序而跳转到中断对应的服务程序中去执行,ARM Cortex-M 内核的 MCU 具有一个用于中断管理的嵌套向量中断控制器(NVIC,全称:Nested Vectored Interrupt Controller)。ARM Cortex-M 的 NVIC 最大可支持 256 个中断源,其中包括 16 个系统中断和 240 个外部中断。
2.1.2 ARM Cortex-M 中断优先级管理
ARM Cortex-M 使用 NVIC 对不同优先级的中断进行管理,首先看一下 NVIC 在 CMSIS 中的结构体定义,如下所示:
typedef struct
{
__IOM uint32_t ISER[8U] /* 中断使能寄存器 */
uint32_tRESERVED0[24U];
__IOM uint32_t ICER[8U]; /* 中断除能寄存器 */
uint32_tRSERVED1[24U];
__IOM uint32_t ISPR[8U]; /* 中断使能挂起寄存器 */
uint32_tRESERVED2[24U];
__IOM uint32_t ICPR[8U]; /* 中断除能挂起寄存器 */
uint32_tRESERVED3[24U];
__IOM uint32_t IABR[8U]; /* 中断有效位寄存器 */
uint32_tRESERVED4[56U];
__IOM uint8_t IP[240U]; /* 中断优先级寄存器 */
uint32_tRESERVED5[644U];
__OM uint32_t STIR; /* 软件触发中断寄存器 */
} NVIC_Type;
可以看到成员变量 中断优先级寄存器IP 是一个 uint8_t 类型的数组,数组一共有 240 个元素,数组中每一个元组就用来配置一个对应的外部中断的优先级。,因此中断优先级的配置范围就应该是 0~255。但是芯片厂商一般用不完这些资源,对于 STM32,只用到了中断优先级寄存器的高四位[7:4],低四位[3:0]取零处理,因此 STM32 提供了最大 16 级(0~15)的中断优先级等级。中断优先级配置寄存器的值越小,中断的优先等级就越高。
STM32 的中断优先级可分为抢占优先级和子优先级,抢占优先级支持中断嵌套,也就是高的可以打断优先级低的,子优先级决定不考虑打断情况下中断执行顺序。
优先级分组0~4,分别0~4bit用于抢占优先级,4~0bit用于子优先级。
建议在 STM32 上使用 µC/OS-III 时,使用中断优先级分组 4(NVIC_PriorityGroup_4)即优先级配置寄存器的高四位全部用于抢占优先级,不使用子优先级,那么这么一来只需要设置中断的抢占优先级即可。
2.1.3 三个中断优先级配置寄存器
系统中断有独立的中断优先级配置寄存器,分别为 SHPR1、SHPR2、SHPR3。
SHPR1 寄存器
SHPR1 寄存器用于配置 内存管理错误中断 MemManage、总线错误中断 BusFault、使用错误 UsageFault 的中断优先级。该寄存器24位,内存、总线、使用 这三种错误中断优先级各占 8 位,最高的8位保留。
SHPR2 寄存器
SHPR2 寄存器用于配置 特权调用 SVCall 的中断优级。"SVCall"代表Supervisor Call,通常用于实现操作系统的系统调用。该寄存器24位,SVCALL中断优先级使用高8位。
当处理器执行SVCall指令时,会触发特权级别的切换,使处理器从用户态切换到特权态,以便访问特权级别的资源或者执行特权级别的指令。SVCall指令的使用通常由操作系统来管理,用于实现系统级功能的调用和管理。
SHPR3 寄存器
SHPR3 寄存器用于配置 可挂起系统调用中断 PendSV、滴答定时器 SysTick 的中断优先级。24位寄存器,PendSV和Systic中断优先级共用到高16位。
PendSV是一个用于实现低优先级任务切换的中断。当发生PendSV中断时,处理器会挂起当前执行的任务,然后执行PendSV中断处理程序。
SysTick定时器是一个24位的倒计时器,可以用于实现系统级的定时和延时功能。SysTick定时器通常用于实时操作系统(RTOS)中,作为任务调度器的基准时钟,也可以用于生成周期性的系统定时器中断。
2.1.4 三个中断屏蔽寄存器
ARM Cortex-M 有三个用于屏蔽中断的寄存器,分别为 PRIMASK、FAULTMASK 和BASEPRI。
PRIMASK 寄存器
作用:PRIMASK 寄存器有 32bit,但只有 bit0 有效,可读可写,PRIMASK 寄存器设置为 1 用于屏蔽除 非屏蔽中断 NMI 和 硬件错误 HardFault 外的所有异常和中断,将 PRIMASK 寄存器清 0 用于使能中断。
FAULTMASK 寄存器
作用:FAULTMASK 寄存器有 32bit,但只有 bit0 有效,也是可读可写的,将 FAULTMASK
寄存器设置为 1 用于屏蔽除 NMI 外的所有异常和中断,清零用于使能中断。
BASEPRI 寄存器
作用:BASEPRI 有 32bit,但只有低 8 位[7:0]有效,也是可读可写的。BASEPRI 用于设置一个中断屏蔽的阈值,设置好 BASEPRI 后,中断优先级低于阈值的就都会被屏蔽掉
µC/OS-III 就是使用 BASEPRI 寄存器来管理受 µC/OS-III 管理的中断的,而不受 µC/OS-III 管理的中断,则不受 µC/OS-III 的影响。
2.1.5 中断控制状态寄存器
中断控制状态寄存器(ICSR)用于设置和清除异常的挂起状态,以及获取当前系统正在执行的异常编号。32bits寄存器,读取低8位的 VECAVTIVE 段可判断当前执行的代码是否在中断里。
2.2 µC/OS-III 中断配置项
1. CPU_CFG_NVIC_PRIO_BITS
此宏用于定义中断优先级配置寄存器的实际使用位数,中断优先级配置寄存器实际使用到多少比特位,这个宏就应该定义成多少,因为 STM32 的优先级配置寄存器都只使用到了高四比特位,因此对于 STM32 而言,这个宏应该配置为 4。
2. CPU_CFG_KA_IPL_BOUNDARY
此宏用于定义BASEPRI寄存器的值,中断优先级低于此宏定义值(中断优先级数值大于此宏定义值)的中断受 µC/OS-III 管理。
2.3 µC/OS-III 中断管理详解
2.3.1 PendSV 和 SysTick 中断优先级配置
1. PendSV 中断优先级配置
PendSV 主要用于任务切换,因此在 µC/OS-III 内核开始进行多任务处理前,也就是在 µC/OS-III 内核启动之前,就需要配置好 PendSV。 OSStart() 函数中通过地址向 SHPR3 寄存器写值设置 PendSV中断优先级,给的是0xFF,也就是最低优先级。
2. SysTick 中断优先级配置
SysTick 主要用于为 µC/OS-III 内核提供时钟节拍,在调用函数 OSStart()后,需要调用函数OS_CPU_SysTickInit()对 SysTick 进行配置。通过设置 SHPR3 寄存器配置SysTick中断优先级,给的最高优先级。
3. µC/OS-III 开关中断
在文件 cpu_a.asm 中提供了两组用于开关中断的标号
CPU_IntDis ; 关闭所有中断
CPU_IntEn ; 打开所有中断 操作PRIMASK寄存器开关中断
CPU_SR_Save ; 保存中断状态,并关闭受 µC/OS-III 管理的中断
CPU_SR_Restore ; 恢复中断状态 读取BASEPRI寄存器并更新值
4. µC/OS-III 临界区
临界区是指代码中的一些关键部分。临界区中的代码运行时要求不能被打断。由于中断或任务切换都有可能打断临界区代码,因此必须通过屏蔽中断来保护临界区中代码的执行。
CPU_CRITICAL_ENTER() //进入临界区前屏蔽中断
其实是调用了 CPU_SR_Save 保留并设置 BASEPRI 寄存器,来屏蔽掉所有中断号低于 BASEPRI 寄存器值的中断。
CPU_CRITICAL_EXIT() //退出临界区后恢复中断
其实是调用了 CPU_SR_Restore 来恢复BASEPRI 寄存器。
通常在创建任务、挂起任务之类的任务操作都使用临界区CRITICAL的进入和退出防止被中断打断。
在 CRITICAL_ENTER() 和 CRITICAL_EXIT() 的底层可以发现,这俩宏分别调用 SR_Save 和 SR_Restore 来将 BASEPRI 寄存器的值保存到变量 CPU_SR 中。
CPU_SR 需要在调用CRITICAL_ENTER()和CRITIAL_EXIT()之前使用宏 CPU_SR_ALLOC() 来手动定义。SR_ALLOC必须定义在所有局部变量之后。
#define CPU_SR_ALLOC() \
CPU_SR cpu_sr = (CPU_SR)0
5. µC/OS-III 中断嵌套计数器
在UCOS-3中,中断嵌套的次数由全局变量 OSIntNestingCtr 记录。
OSIntNestingCtr > 0 的时候,表示当前处于中断状态;OSIntNestingCtr < 0 时,表示当前不处于中断状态。
全局变量 OSIntNestringCtr 是在中断服务函数中更新的。
UCOS-3提供了 OSIntEnter() 和 OSIntExit() 用于中断服务函数前后,
其中 OSIntEnter() 只是简单地更新了全局变量 OSIntNestingCtr ,
而 OSIntExit() 不仅更新了 OSIntNestingCtr,还会根据需要进行任务切换。
第三章 任务基础知识
任务和任务管理是RTOS的核心。
3.1 单任务和多任务系统
单任务就是一个main函数,跑一个无限循环,依次调用各种函数和触发中断。大循环中的函数没有优先级之分,实时性差。
多任务系统在宏观角度可以看成多个任务同时执行,任务调度器根据任务调度算法据将CPU的使用权分配给任务,有更强的实时性。
3.2 任务状态
UCOS-3中任务存在五种状态,分别是 休眠、就绪、运行、挂起 和 中断。
休眠态:
存在于内存中,但是尚未提供给UCOS-3内核的任务处于休眠态。
调用函数 OSTaskCreate()对一个处于休眠态的任务进行创建。从休眠进入就绪。
调用函数 OSTaskDel()删除这个任务。任务进入休眠。
被删除的任务就会重新回到休眠态,这里所说的删除仅仅指通知 µC/OS-III 内核不再管理这个任务。任务仍然存在于代码空间。
就绪态:
当任务被函数 OSTaskCreate()创建后,任务未被执行,就处于就绪态。
UCOS-3 中有一个就绪态任务链表,按照任务的优先级对就绪态任务进行排序。
运行态:
正在执行的任务处于运行态。
挂起态:
处于运行态的任务需要等待某一条件时,会让出CPU的使用权,切换到挂起态等待事件发生。等待条件满足后,挂起的任务切换到就绪,等待任务调度器调度运行。
中断态:
处于运行态的任务被中断打断,原本处于运行态的任务就会处于中断态。直到中断结束,切换回运行态。
3.3 任务优先级
任务优先级
每一个任务都被分配一个任务优先级。不同任务可以有相同的优先级。
任务优先级范围:配置文件os_cfg.h中 ,0~(OS_CFG_PRIO_MAX-1) 。
0优先级最高,OS_CFG_PRIO_MAX-1 优先级最低。
可以通过 OS_PrioGetHighest() 获取就绪态最高的任务优先级。
就绪态的任务优先级以位图的形式记录在优先级数组 OSPrioTbl 中。通过前导零的方式可以很方便地获取就绪态任务中最高的任务优先级。
优先级表
OSPrioTbl 在文件 os_prio.c 中定义优先级表,它是一个数组:
CPU_DATA OSPrioTbl[OS_PRIO_TBL_SIZE]; /* 定义优先级表 */
CPU_DATA 的类型为 unsigned int,也就是一个数组元素为 32 位。如果 MCU 的字长类型是 16 位、8 位或者 64 位,只需要把优先级表的数据类型 CPU_DATA 改成相应的位数即可。
#define OS_PRIO_TBL_SIZE ( ((OS_CFG_PRIO_MAX - 1u) / DEF_INT_CPU_NBR_BITS) + 1u )
位图:
OSPriTbl 就是一个数组,数组一个元素32位,把数组换成位,1代表存在该优先级的任务。
优先级表用二进制排列,就是位图,从高位到低位,优先级从高到低。
使用前导零可以方便地获取就绪态任务中最高的任务优先级,前导零就是按行遍历优先级数组,找到最前面的1前面有几个零,有几个零就代表最高的任务优先级是几
PriTbl 优先级表这个数组的大小是宏定义的,计算方式是
(PRIO_MAX-1)/DEF_INT_CPU_NBR_BITS + 1,也就是
PriTbl 的元素个数 =( (最大优先级 PriMAX-1)/一个整型的位数 INT_NBR_BITS) +1
OS中整型的位数,字节的位数等也都是可以通过宏定义设置的,一般默认字节8,整型32
![](https://i-blog.csdnimg.cn/direct/533f187babd048e181f555de51c689a4.png)
3.4 任务调度方式
抢占式调度
任务有优先级,优先级高的任务可以在优先级低的任务执行期间,抢占 CPU 的使用权。
时间片调度
时间片调度是针对优先级相同的任务而言的。多个具有相同优先级的任务就绪,任务调度器根据任务时间片轮流地运行这些任务,这些任务仍然遵循抢占优先级的调度方式。
时间片是以一次系统时间节拍位单位的。例如UCOS-3默认的任务时间片为100,则当前任务运行100个系统时钟节拍后,切换到另一个相同任务优先级的任务中运行。
3.5 任务控制块
每个任务都有独自的任务控制块。
任务控制块是用来存放任务信息的数据结构。
任务控制块所需的内存空间由用户手动分配,并在调用任务相关的函数时,通常将任务控制块的首地址作为参数。
任务控制块结构体定义在os.h文件中,包含了大量成员变量,可以通过配置文件进行裁剪。
3.6 任务栈
在创建一个任务前,需要为任务准备好一块内存空间。这一内存空间将作为任务的栈空间进行使用。栈首地址和栈大小会作为任务创建函数OSTaskCreate()的参数传递进去。这里任务栈的大小单位为字,1 个字为 4 字节。
第四章 任务相关 API 函数
4.1 创建和删除任务相关API
OSTaskCreate() 创建任务
OSTaskDel() 删除任务
4.1.1 OSTaskCreate()
此函数用于创建任务,需要由用户手动分配任务控制块以及任务栈空间所需内存。当任务被创建好后,就会立马处于就绪态,此时只要任务调度器处于正常工作状态,那么创建好的任务就会由任务调度器调度。要注意的是,不能在中断服务函数中创建任务。任务栈大小单位为字(32bit)。
//创建开始任务
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 //存放该函数错误时的返回值
);
注意:
1. 任务栈必须定义为 CPU_STK 类型。
2. 任务函数中必须调用 µC/OS-III 提供的用来让任务挂起的函数,例如延时函数、任务挂起函数或等待内核对象(等待消息队列、事件标志、信号量、互斥信号量等)的函数,这样才能够让其他任务获得 CPU 的使用权。
3. 创建的任务不能够使用数值为 0 、 1 、 OS_CFG_PRIO_MAX-2 和
OS_CFG_PRIO_MAX-1 的任务优先级,因为这些任务优先级是保留给 µC/OS-III 使用的。
OS_TCB MyTaskTCB; /* (1) 保存任务的任务控制块 */
CPU_STK MyTaskStk[200];
void MyTask (void *p_arg) /* (3) 任务函数的地址就是任务函数的函数名 */
{
while (DEF_ON) {
/* 仍待一个事件 */
/* 任务体 */
}
}
void SomeCode (void)
{
OS_ERR err;
:
:
OSTaskCreate( &MyTaskTCB, /* (1) 任务控制块 */
"My Task", /* (2) 任务名 */
MyTask, /* (3) 任务函数地址 */
(void *)0, /* (4) 任务函数参数未用到 */
12, /* (5) 任务优先级 */
&MyTaskStk[0], /* (6) 任务栈 */
10, /* (7) 栈生长的“水位”限制 */
200, /* (8) 任务栈大小 */
5, /* (9) 任务消息队列的大小 */
10, /* (10) 任务时间片 */
(void *)0, /* (11) 扩展指针 */
OS_OPT_TASK_STK_CHK + OS_OPT_TASK_STK_CLR, /* (12) 选项 */
&err); /* (13) 错误代码*/
/* 检查错误代码 (14) */
:
:
}
4.1.2 OSTaskDel()
删除任务不会删除任务的代码或释放任务栈,删除任务指示意味着,任务的代码和任务栈都不再由 µC/OS-III 内核管理,被删除任务的任务代码和任务栈依然可以在创建其他任务时使用。
要使用任务删除函数需要将配置文件os_cfg.h文件中的 OS_CFG_TASK_DEL_EN 配置为1。
注意:
不能删除空闲任务和 µC/OS-III 的中断服务任务。
OS_TCB MyTaskTCB;
void TaskX (void *p_arg)
{
OS_ERR err;
while (DEF_ON) {
:
:
OSTaskDel( &MyTaskTCB,
&err);
/* 检查错误代码 */
:
:
}
}
4.2 挂起和恢复任务相关函数
4.2.1 函数 OSTaskSuspend()
此函数用于无条件地挂起任务,被挂起的任务不会参与任务调度。
void OSTaskSuspend( OS_TCB* p_tcb,//任务块指针
OS_ERR* p_err //接收错误码的变量指针
)
4.2.2 函数 OSTaskResume()
此函数用于恢复被OSTaskSuspend()挂起的任务。
void OSTaskResume( OS_TCB* p_tcb,//任务块指针
OS_ERR* p_err //错误变量指针
)
第五章 UCOS-3 启动流程
5.1 启动流程
首先要理解一个东西,单片机有跑裸机和跑操作系统两个说法,操作系统说白了就是个任务的调度工具。要先把裸机跑起来,再去跑RTOS去帮助调度任务。
CPU_IntDis()
屏蔽所有中断,确保 µC/OS-III 的启动不被打断
OSInit()
初始化 µC/OS-III 内核
OSTaskCreate()
创建第一个应用任务
OSStart()
启动 µC/OS-III 内核,开始进行任务调度
5.2 屏蔽所有中断
调用 CPU_IntDis(),该宏操作寄存器 PRIMASK 开关所有中断。
确保RTOS的启动过程不被中断打断。
5.3 内核初始化
调用 OSInit() 初始化RTOS内核。
OSInit()只有一个参数用来接收返回的错误。内核初始化函数会初始化各种确保内核正常运行的全局变量和功能,初始化完成后全局变量 OSInitialized。
void OSInit(OS_ERR* p_err);
5.4 创建第一个应用任务
使用 OSTaskCreate() 创建应用任务。
完成内核初始化后,开启任务调度之前,至少要创建一个应用任务。
虽然函数OSInit()初始化内核的过程中必定会创建空闲任务,根据配置文件选择性创建任务统计任务、软件定时器任务。但这三个任务都属于内核任务,应用程序无法访问。因此在开始调度之前,至少要创建一个应用程序任务,用来初始化应用程序。同时这个初始化任务的优先级应该设置为2,也就是最高优先级。
5.5 开始任务调度
OSStart() 开始任务调度是内核开始前的最后一步
void OSStart(OS_ERR* p_err)
第六章 UCOS-3 任务切换
6.1 PendSV 异常
PendSV 异常是一个非常重要的可挂起异常。可挂起异常指的是可以被挂起从而延迟执行。
通常将 PendSV 设置成最低中断优先级,也就是在其他所有中断处理完成后才执行。RTOS就是将任务切换的过程放到 PendSV 异常的中断服务函数中处理的。
要挂起 PendSV 异常(触发 PendSV 异常)也非常简单,只需将 ICSR 寄存器(中断控制状
态寄存器)中断的 PENDSVSET 为置 1,即可挂起 PendSV 异常。
在挂起 PendSV 异常后,PendSV 的中断服务函数会等 CPU 处理完所有中断优先级不小于 PendSV 异常中断优先级的中断后,再处理 PendSV 异常的中断处理函数。
6.2 触发任务切换的时机
RTOS属于抢占式内核,系统会保证当前当前执行的任务一定是优先级最高的就绪态任务。RTOS会根据任务的就绪、挂起、恢复、延时、优先级改变、任务被删除等时机调度任务。
6.2.1 函数 OSSched()
函数 OSSched() 用于在任务中触发任务切换,一般情况下,该函数会被内核自动调用。
当然,应用程序也可以在任务中调用 OSSched(),手动触发任务切换。函数 OSSched()定义在文件 os_core.c 中。
该函数内部:
会判断 中断嵌套计数器OSIntNestingCtr 的大小,
如果OSIntNestingCtr>0,代表在执行中断函数,不可以触发任务切换。
会判断 任务调度器锁定嵌套计数器OSSchedLockNestingCtr 的大小,
如果OSSchedLockNestingCtr>0,代表任务调度器被锁定,不可以触发任务切换。
OSIntNestingCtr 和 OSSchedLockNestingCtr 都为零,代表可以执行任务切换。
INT_DIS() 屏蔽全部中断,相当于进入临界区。
PrioGetHighest() 读取 PriTbl 获取就绪态任务中最高的任务优先级。
判断 空闲任务使能 TASK_IDLE_EN,
根据任务优先级从 就绪态任务链表OSRdyList 中获取 任务控制块TCB
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
如果获取到的 最高优先级就绪态任务控制块OSTCBHignRdyPtr
就是 当前任务的任务控制块OSTCBCurPtr
if (OSTCBHighRdyPtr == OSTCBCurPtr)
那么无需进行任务切换,直接 恢复中断使能 INT_EN()。
再往后就是诸如根据 空闲任务使能 判断对 空闲任务 的处理,和任务的切换
值得注意的是,
任务优先级表PriTbl 和 就绪态任务链表RydyList 搭配使用
就绪态任务链表 中存放 优先级表 对应任务的 任务控制块TCB指针
要切换任务的时候,是利用 最高优先就绪态任务控制块指针TCBHighRdyPtr 将
指向的最高级就绪态任务的 上下文切换计数器CtxSwCtr 自增。
然后 TLS指针 指向切换后的任务的本地存储寄存器
Thread Local Storage,线程局部存储
所谓任务切换,其实就是挂起PendSV中断。
PendSV中断就是专门用来支持上下文切换的,负责保存寄存器、恢复堆栈指针等工作。
PendSV中断挂起,其他中断执行完以后,执行PendSV中断服务函数,保存寄存器,恢复堆栈指针。
CPU执行TLS指针指向的任务。
切换完成。
6.2.2 函数 OSIntExit()
OSIntExit() 用来告诉RTOS 一个中断的中断服务函数执行完毕,可在需要的时候执行任务切换了。
除了中断结束调用IntExit(),最常见的是时间片调度中 Systic 溢出调用 OsTimeTick() 中断,然后通过 IntExit() 调用 PendSV 进行任务切换。 看下面的代码:
/* 初始化 CPU 库 */
CPU_Init();
/* 根据配置的节拍频率配置 SysTick
通过获取系统时钟频率 `HAL_RCC_GetSysClockFreq()`
并除以配置的节拍频率 `OSCfg_TickRate_Hz`,得到一个计数值。
这个计数值决定了 SysTick 定时器的计数周期,从而决定了系统节拍的间隔。
*/
cnts = (CPU_INT32U)(HAL_RCC_GetSysClockFreq() / OSCfg_TickRate_Hz);
OS_CPU_SysTickInit(cnts);
这里 SysTickInit(cnts)的作用是设置滴答时钟的计数周期。
在RTOS中,Systic定时器通常被用作系统的时钟节拍源。
Systic定时器的计数周期决定了时间片调度的时间间隔,也就是时间片长度。
Systic 溢出后 SysticHandler() 会调用 OSTimeTick()来处理系统时钟节拍事务,
然后会调用 OSIntExit() 函数。
OSIntExit() 内部做了两个工作:
一、找到当前的最高优先级的任务;
二、调用PendSV软中断,进行任务切换调度。
6.2.3 PendSVHandler
PendSV中断服务函数主要就是处理了任务栈中数据的入栈和出栈。入栈保存当前任务切换前的上下文信息,出栈恢复上下文信息。也就是保存寄存器,恢复堆栈指针。
第七章 其他任务相关API函数
任务相关API函数总结
SchedRoundRobinCfg() 配置时间片调度功能
SchedRoundRobinYield() 时间片到期之前强制进行任务调度
TaskChangePrio() 修改任务优先级
TaskCreate() 创建任务
TaskCreateHook() 任务创建钩子函数
TaskDel() 删除任务
TaskDelHook() 任务删除钩子函数TaskRegGet() 获取任务本地存储寄存器的值
TaskRegGetID() 获取当前任务本地存储寄存器的索引值
TaskRegSet() 修改任务本地存储寄存器的值
TaskResume() 恢复任务
TaskReturnHook() 任务意外返回钩子函数
TaskStkChk() 计算任务栈余量
TaskStkInit() 初始化任务栈
TaskSuspend() 挂起任务
TaskSwHook() 任务切换钩子函数
TaskTimeQuantaSet() 修改任务时间片
IdleTaskHook() 空闲任务钩子函数
StatTaskCPUUsageInit() 初始化 CPU 使用率统计功能
任务相关API函数简要
1. 函数 OSSchedRoundRobinCfg()
该函数用于配置时间片调度功能,配置内容为是否开启时间片调度以及时间片的默认长度。
void OSSchedRoundRobinCfg( CPU_BOOLEAN en,
OS_TICK dflt_time_quanta, //1代表时间片设为默认值
OS_ERR* p_err)
//Round Robin是指时间片调度算法
2. 函数 OSSchedRoundRobinYield()
该函数用于在任务时间片到期前强制进行任务调度。
void OSSchedRoundRobinYield(OS_ERR* p_err)
3. 函数 OSTaskChangePrio()
该函数用于修改任务的优先级。
void OSTaskChangePrio( OS_TCB* p_tcb,
OS_PRIO prio_new,
OS_ERR* p_err)
4. 函数 OSTaskCreateHook()
该函数为任务创建的钩子函数,会在新创建的任务被添加到就绪态任务链表前调用。
void OSTaskCreateHook(OS_TCB* p_tcb)
5. 函数 OSTaskDelHook()
该函数为任务删除的钩子函数,会在被删除任务从所在任务链表中删除后调用。
void OSTaskDelHook(OS_TCB* p_tcb)
使用钩子函数需要用 App_OS_SetAllHooks() 初始化所有RTOS 钩子函数,并将os_cfg.h的OS_CFG_APP_HOOKS_EN配置为1。
6. 函数 OSTaskRegGet()
该函数用于获取指定任务本地存储寄存器中的值。
OS_REG OSTaskRegGet( OS_TCB* p_tcb,
OS_REG_ID id, //任务本地存储寄存器的索引号
OS_ERR* p_err)
7. 函数 OSTaskRegGetID()
该函数用于获取任务本地存储寄存器的索引号。
OS_REG_ID OSTaskRegGetID(OS_ERR* p_err)
8. 函数 OSTaskRegSet()
该函数用于设置任务本地存储寄存器的值。
void OSTaskRegSet( OS_TCB* p_tcb,
OS_REG_ID id,
OS_REG value,
OS_ERR* p_err)
9. 函数 OSTaskReturnHook()
该函数为任务意外返回钩子函数,会在任务意外返回后调用。
void OSTaskReturnHook(OS_TCB* p_tcb)
10. 函数 OSTaskStkChk()
该函数用于计算任务的任务栈剩余量。
void OSTaskStkChk(OS_TCB* p_tcb,
CPU_STK_SIZE* p_free,//指向接收任务栈剩余变量的指针
CPU_STK_SIZE* p_used,//指向接收任务使用量变量的指针
OS_ERR* p_err)
11. 函数 OSTaskStkInit()
该函数用于初始化任务栈,一般在任务创建的时候被调用。
CPU_STK* OSTaskStkInit(OS_TASK_PTR p_task,
void* p_arg,
CPU_STK* p_stk_base,
CPU_STK* p_stk_limit,
CPU_STK_SIZE stk_size,
OS_OPT opt)
12. 函数 OSTaskSwHook()
该函数为任务切换的钩子函数,在任务切换前保存上下文信息后调用。
void OSTaskSwHook(void)
任务切换之前,操作系统会将当前任务的 CPU 寄存器内容逐个保存到该任务的堆栈中。如通用寄存器(如 R0-R7)、栈指针寄存器(如 SP)、程序计数器(PC)等。这样做可以确保在任务切换回来时,能够恢复到之前的执行状态。
13. 函数 OSTaskTimeQuantaSet()
该函数用于修改任务的时间片长度。
void OSTaskTimeQuantaSet( OS_TCB* p_tcb,
OS_TICK time_quanta,
OS_ERR* p_err)
14. 函数 OSIdleTaskHook()
该函数为空闲任务的回调函数,在此函数中不能调用可能引发空闲任务挂起的 API 函数。
void OSIdleTaskHook(void)
15. 函数 OSStatTaskCPUUsageInit()
该函数用于初始化 CPU 使用率统计功能,在实际的开发中非常有用。初始化CPU使用率统计功能以后,可以直接通过TCB获取当前任务的CPU当前使用率和最大使用率。
void OSStatTaskCPUUsageInit(OS_ERR* p_err)
第八章 时间管理
8.1 系统时钟节拍
8.1.1 系统时钟节拍简介
OSTickCtr 就是用来记录自系统启动后,系统时钟节拍产生的个数。每当产生一个系统时钟节拍,OSTickCtr 都会被加 1。
OS_EXT OS_TICK OSTickCtr;
//展开
extern unsigned int OSTickCtr; //os.h
8.1.2 系统时钟节拍来源
RTOS 系统时钟节拍的来源是 SysTick。
在内核启动前,已经通过原本裸机的时钟配置函数对Systic进行配置。
/* 根据配置的节拍频率配置 SysTick */
cnts = (CPU_INT32U)(HAL_RCC_GetSysClockFreq() / OSCfg_TickRate_Hz);
OS_CPU_SysTickInit(cnts);
其实就是 系统时钟频率 / 滴答时钟频率,得到的值作为 SysTickInit() 函数的参数来作为 Systic 的重装载值。
注意,STM32 的 SysTic 可以配置成CPU同频或者CPU频率的八分之一,但是在OS中,Systic是强制和CPU同频的。也就是OS中初始化Systic的重装载值一般都是1。
8.1.3 系统时钟节拍处理
系统时钟节拍的处理是通过 Systic 中断完成的。
在移植 RTOS 的时候,已经修改了 Systic 的中断服务函数 Systic_Handler()。
在 Systic_Handler() 中,
首先通过全局变量 OS_STATE_OS_RUNNING 判断内核是否已经开始运行,
然后调用 TimeTick() 开始处理系统节拍事务,
最后调用 OSIntExit() 来挂起 PendSV,实现任务切换。
OSTimeTick() 处理系统节拍事务,
其实就是调用了 系统节拍钩子函数 TimeTickHook(),
调用了 SchedRoundRobin(&RdyList [ PriCur ]) 根据就绪态任务链表处理时间片。
调用 TickUpdate 更新 TickCtr。
时间片处理函数 :
SchedRoundRobin(OS_RDY_LIST *p_rdy_list)//时间片调度函数,传入RdyList[PriCur]
时间片在任务控制块里面是用时间原子计数器 TimeQuantaCtr 存储的,
SchedRoundRobin 处理时间片首先把 TimeQuantaCtr 减1更新,然后判断 TimeQuantaCtr 有没有剩余的时间片,有剩余时间片就不调度,没有的话,
把任务移到就绪态任务链表的末尾,并获取就绪态任务链表的表头作为当前任务。
8.2 任务延时相关函数
RTOS 的任务延时函数,本质上就是将任务添加到 Tick 任务链表中,并给一个挂起的超时时间。
TimeDly() 以系统时钟节拍为单位进行任务延时
TimeDlyHMSM() 以时、分、秒、毫秒为单位进行任务延时
TimeDlyResume() 恢复任务延时
1. OSTimeDly()
void OSTimeDly( OS_TICK dly, //延时节拍
OS_OPT opt, //延时选项
OS_ERR *p_err) //错误指针
/*
OS_OPT_TIME_DLY 任务延时的结束时刻为,表示任务延时 OSTickCtr=OSTickCtr+dly
OS_OPT_TIME_TIMEOUT 任务延时的结束时刻为,表示任务超时 OSTickCtr=OSTickCtr+dly
OS_OPT_TIME_MATCH 任务延时的结束时刻为 OSTickCtr=dly
OS_OPT_TIME_PERIODIC 任务延时的结束时刻为 OSTickCtr= OSTCBCurPtr->TickCtrPrev+dly
*/
2. 函数 OSTimeDlyHMSM()
延时,参数给时分秒,会转换成系统时钟
void OSTimeDlyHMSM( CPU_INT16U hours,
CPU_INT16U minutes,
CPU_INT16U seconds,
CPU_INT32U milli,
OS_OPT opt,
OS_ERR *p_err)
/*
OS_OPT_TIME_HMSM_STRICT 延时时间参数严格按照实际的时间格式
OS_OPT_TIME_HMSM_NON_STRICT 延时时间参数无需严格按照实际的时间格式
OS_OPT_TIME_DLY 任务延时的结束时刻为 当前TickCnt+参数
OS_OPT_TIME_TIMEOUT 任务延时的结束时刻为 当前TickCnt+参数
OS_OPT_TIME_PERIODIC 任务延时的结束时刻为 OSTickCtr= OSTCBCurPtr->TickCtrPrev(先前任务时间戳)+dly
OS_OPT_TIME_MATCH 任务延时的结束时刻为 OSTickCtr=dly
*/
3. 函数 OSTimeDlyResume()
该函数用于恢复被添加到 Tick 任务链表中挂起延时的任务。
void OSTimeDlyResume( OS_TCB *p_tcb,
OS_ERR *p_err)
8.3 其他系统时钟节拍相关函数
TimeGet() 获取当前系统时钟节拍计数器的值
TimeSet() 设置系统时钟节拍计数器的值
1. 函数 OSTimeGet()
该函数用于获取当前 系统时钟节拍计数器TickCtr 的值。
OS_TICK OSTimeGet(OS_ERR *p_err)
2. 函数 OSTimeSet()
该函数用于设置当前 系统时钟节拍计数器TickCtr 的值。
void OSTimeSet( OS_TICK ticks,
OS_ERR *p_err)
第九章 信号量
RTOS 按照信号量的功能可以分为二值信号量、计数型信号量、互斥信号量。
9.1 二值信号量
9.1.1 二值信号量简介
二值信号量只有两种情况,分为有资源和无资源。
二值信号量通常用于互斥访问和任务同步,与互斥信号量比较类似,但是二值信号量/计数型信号量可能有严重的优先级翻转问题。
优先级翻转问题:高优先级的任务由于获取不到二值信号量的资源,被迫等待资源的释放,这时优先级比它低的反而先执行。
信号量的组成可以理解成:信号量资源计数器Ctr + 任务等待挂起链表PendList。
二值信号量与计数型信号量的最大区别在于,二值信号量的资源计数器只能表示 0 和 1。
二值信号量和计数型信号量共用一个结构体os_sem。有两个最重要的成员变量,分别为 任务等待挂起链表PendList 和 信号量资源计数器Ctr。每个信号量通过结构体sem创建,都有一个资源计数器和等待挂起任务链表。semphare
9.1.2 µC/OS-III 二值信号量相关 API 函数
二值信号量与计数型信号量共用一个 sem 结构体。也共用一套API。
SemCreate() 创建一个信号量
SemDel() 删除一个信号量
SemPend() 尝试获取信号量
SemPendAbort() 终止挂起着等待信号量的任务
SemPost() 释放信号量资源
SemSet() 强制设置信号量的资源数
1. 函数 OSSemCreate()
该函数用于创建一个信号量。
void OSSemCreate( OS_SEM *p_sem, //指向信号量结构体的指针
CPU_CHAR *p_name, //指向信号量名ASCII码的指针
OS_SEM_CTR cnt, //信号量资源数初始值
OS_ERR *p_err)
2. 函数 OSSemDel()
该函数用于删除一个信号量。返回删除信号量时被中止挂起的任务数量。
OS_OBJ_QTY OSSemDel( OS_SEM *p_sem, //信号量结构体sem
OS_OPT opt, //删除操作选项
OS_ERR *p_err)
/*
没有任务等待该信号量资源时才删除
直接删除
*/
3. 函数 OSSemPend()
该函数用于挂起(获取)信号量。
OS_SEM_CTR OSSemPend(OS_SEM *p_sem,
OS_TICK timeout,
OS_OPT opt, //等待操作
CPU_TS *p_ts,
OS_ERR *p_err)
/*
等待时阻塞
等待时不阻塞直接返回
*/
4. 函数 OSSemPendAbort()
该函数用于终止挂起着的等待信号量的任务。
OS_OBJ_QTY OSSemPendAbort( OS_SEM *p_sem,
OS_OPT opt, //操作选项
OS_ERR *p_err)
/*
中止一个任务的等待操作
中止全部任务的等待操作操作
中止一个任务的等待操作,且终止后不进行任务调度
中止全部任务的等待操作,且终止后不进行任务调度
*/
5. 函数 OSSemPost()
该函数用于发布(释放)信号量。
OS_SEM_CTR OSSemPost( OS_SEM *p_sem,
OS_OPT opt, //发布信号量的操作
OS_ERR *p_err)
/*
发布一个任务等待的信号量
发布所有等待的信号量
发布一个等待信号量的任务,并且在发布后不立即进行任务调度,
而是允许其自然放弃CPU的执行权或发生其他调度时间
发布所有等待信号量的任务,并且在发布后不立即进行任务调度
*/
6. 函数 OSSemSet()
该函数用于设置信号量的资源数。
void OSSemSet( OS_SEM *p_sem,
OS_SEM_CTR cnt,
OS_ERR *p_err)
9.2 计数型信号量
计数型信号量和二值信号量用的同样的sem结构体,信号量计数器Ctr 和 挂起任务列表PendList原理都一样,Ctr的计数值不同。函数也是通用的。
9.3 优先级翻转
二值信号量和计数型信号量的优先级翻转问题是很常见的。优先级翻转会破坏任务执行的预期顺序,可能导致严重后果。使用互斥信号量解决。
9.4 互斥信号量
相对于二值信号量和计数型信号量,互斥信号量拥有优先级继承的机制使得互斥信号量能够在一定程度上解决优先级翻转的问题。
互斥信号量的优先级继承:
低任务持有互斥信号时,高任务获取不到互斥锁被挂起,低任务就将任务优先级提升到与高任务相同,这个过程就是优先级继承。
将低任务提升到高优先级,低优任务释放后马上就轮到高任务了,减少了中任务抢占对高任务的等待时间拖累。将优先级翻转问题的影响降低。
互斥信号量结构体os_mutex:
信号量持有计数器Ctr + 挂起任务列表PendList
持有计数器和资源计数器的区别在于,资源计数器是来一个获取信号量的就--,持有计数器是获取一次信号量就++。
9.5 互斥信号量相关 API 函数
MutexCreate() 创建一个互斥信号量
MutexDel() 删除一个互斥信号量
MutexPend() 尝试挂起(获取)互斥信号量
MutexPendAbort() 终止任务挂起等待互斥信号量
MutexPost() 发布发布(释放)互斥信号量
1. 函数 OSMutexCreate()
该函数用于创建一个互斥信号量。
void OSMutexCreate( OS_MUTEX* p_mutex,
CPU_CHAR* p_name, //互斥信号量名
OS_ERR* p_err)
2. 函数 OSMutexDel()
该函数用于删除一个互斥信号量。返回被终止挂起的任务数量。
OS_OBJ_QTY OSMutexDel(OS_MUTEX* p_mutex,
OS_OPT opt, //删除操作
OS_ERR* p_err)
/*
仅在没有挂起等待任务时才删除
无论有没有挂起任务强制删除
*/
3. 函数 OSMutexPend()
该函数用于获取互斥信号量。
void OSMutexPend( OS_MUTEX* p_mutex,
OS_TICK timeout, //最大等待获取时间
OS_OPT opt, //等待获取的操作 阻塞/非阻塞
CPU_TS* p_ts, //用来存放互斥信号量接收时的时间戳
OS_ERR* p_err)
4. 函数 OSMutexPendAbort()
该函数用于让任务不再等待互斥信号量。返回被终止等待的任务数量。
OS_OBJ_QTY OSMutexPendAbort( OS_MUTEX* p_mutex,
OS_OPT opt, //让任务放弃等待信号量的操作选项
OS_ERR* p_err)
/*
中止一个任务的等待
中止全部任务的等待
中止一个任务的等待且不再调度
中止全部任务的等待且不再调度
*/
5. 函数 OSMutexPost()
该函数用于释放互斥信号量。
void OSMutexPost( OS_MUTEX* p_mutex,
OS_OPT opt, //释放信号量的操作
OS_ERR* p_err);
/*
释放,调度
释放,不调度
*/
第十章 消息队列
10.1 消息队列简介
任务与其他任务、任务与中断之间的通讯一般可以通过全局变量或消息队列来完成。
全局变量要考虑共享资源的互斥问题,而且被修改后任务无法第一时间知道。
每个任务都可以有独自的内嵌消息队列。通过消息队列发送的消息,可以发送给多个任务。
消息的数据类型结构体msg:
struct os_msg{
OS_MSG* NextPtr; /* 指向下一条消息的指针 */
void* MsgPtr; /* 指向消息内容的指针 */
OS_MSG_SIZE MsgSize; /* 消息内容的大小,单位:字节 */
#if (OS_CFG_TS_EN > 0u)
CPU_TS MsgTS; /* 消息发送时的时间戳 */
#endif
};
消息 msg 结构体中包含了一个指向消息内容的指针 MsgPtr,类型为void,可以指向任何类型,也可以指向函数。
消息的发送方和接收方必须按照约定的方式去发送和接收msg,
该结构体的内容指针 void* MsgPtr 才能正确被解读。
消息是通过消息队列传输的,任务和中断都能够操作消息队列,但是中断只能往消息队列中发送消息,而不能从消息队列中接收消息。
消息队列中的消息是以 LIFO 的方式传输消息的,即以后进先出的方式传输消息。
LIFO 的传输机制在任务或中断需要向任务发送紧急的消息的情况下显得非常有用,可以使得紧急消息比非紧急消息更早地被任务接收。
可以指定任务等待消息的超时时间,就是消息队列中没有消息时任务的等待时间。
消息队列和信号量一样也使用了自己的 挂起任务链表 PendList。任务可以指定将消息发送到 PendSV 中优先级最高的任务或者所有任务。
10.2 消息队列相关 API 函数
QCreate() 创建一个消息队列
QDel() 删除一个消息队列
QFlush() 清空消息队列中的所有消息
QPend() 获取消息队列中的消息
QPendAbort() 终止任务挂起等待消息队列
QPost() 发送消息到消息队列
1. 函数 OSQCreate()
该函数用于创建一个消息队列。
void OSQCreate( OS_Q* p_q,
CPU_CHAR* p_name, //消息队列名
OS_MSG_QTY max_qty, //消息队列大小
OS_ERR* p_err)
2. 函数 OSQDel()
该函数用于删除一个消息队列。返回消息队列删除时,被中止挂起的任务数。
OS_OBJ_QTY OSQDel( OS_Q* p_q,
OS_OPT opt, //消息队列删除操作
OS_ERR* p_err)
/*
没有任务等待时才删除
强制删除
*/
3. 函数 OSQFlush()
该函数用于清空消息队列中的所有消息。
OS_MSG_QTY OSQFlush( OS_Q* p_q,
OS_ERR* p_err)
4. 函数 OSQPend()
该函数用于获取消息队列中的消息。
void *OSQPend( OS_Q* p_q,
OS_TICK timeout, //超时时间
OS_OPT opt, //获取消息选项 阻塞/非阻塞
OS_MSG_SIZE* p_msg_size, //接收消息大小
CPU_TS* p_ts, //存放时间戳
OS_ERR* p_err)
5. 函数 OSQPendAbort()
中止在等待消息的任务。
OS_OBJ_QTY OSQPendAbort( OS_Q* p_q,
OS_OPT opt, //中止在等待消息的任务选项
OS_ERR* p_err)
/*
中止一个
中止所有
中止一个,不调度
中止所有,不调度
*/
6. 函数 OSQPost()
该函数用于向消息队列发送消息。
void OSQPost( OS_Q* p_q,
void* p_void, //发布的消息
OS_MSG_SIZE msg_size, //发布的消息大小
OS_OPT opt, //发布消息选项
OS_ERR* p_err)
/*
将消息发布给所有等待消息队列的任务,发送消息保存在消息队列末尾
将消息发布给所有等待消息队列的任务,发送消息保存在消息队列最前
配合使用:
禁止/允许 在本函数内触发任务调度
*/
第十一章 任务内嵌信号量
11.1 任务内嵌信号量简介
任务内嵌信号量本质上就是一个信号量。但是 任务内嵌信号量 不需要sem或者mutex这么一个中间的内核对象。任务内嵌信号量 是分配在每个任务的 控制块结构体TCB 中的。
每一个任务都有独自的任务内嵌信号量。
任务内嵌信号量只能被该任务获取,但是可以由其他任务或者中断释放。
每个任务的内嵌信号量在任务创建的时候同时创建,并且发出的信号能够直接到达指定的任务。使用内嵌信号量的效率比使用内核对象的信号量高,实际的开发优先考虑任务内嵌信号量。
11.2 任务内嵌信号量相关 API
TaskSemPend() 获取任务内嵌信号量
TaskSemPendAbort() 中止等待中的任务
TaskSemPost() 释放指定任务的任务内嵌信号量TaskSemSet() 设置指定的任务内嵌信号量
1. 函数 OSTaskSemPend()
OS_SEM_CTR OSTaskSemPend( OS_TICK timeout, //超时
OS_OPT opt, //获取内嵌信号量选项 阻塞/非阻塞
CPU_TS* p_ts, //时间戳
OS_ERR* p_err)
2. 函数 OSTaskSemPendAbort()
CPU_BOOLEAN OSTaskSemPendAbort(OS_TCB* p_tcb,
OS_OPT opt, //中止等待内嵌信号量的任务选项
OS_ERR* p_err)
/*
中止一个
中止全部
允许调度
不允许调度
*/
3. 函数 OSTaskSemPost()
OS_SEM_CTR OSTaskSemPost( OS_TCB* p_tcb,
OS_OPT opt, //发布(释放)信号量选项
OS_ERR* p_err)
/*
释放,不允许得到锁的对象马上调度
释放,允许得到锁的对象马上调度
*/
4. 函数 OSTaskSemSet()
设置任务内嵌信号量的资源数。
OS_SEM_CTR OSTaskSemSet( OS_TCB* p_tcb,
OS_SEM_CTR cnt;
OS_ERR* p_err)
第十二章 任务内嵌消息队列
12.1 任务内嵌消息队列简介
任务内嵌消息队列本质上就是一个消息队列。任务内嵌消息队列不需要 消息队列msg 这么一个中间的内核对象,任务内嵌消息队列分配于任务的 任务控制块结构体TCB 中,每一个任务都有独自的任务内嵌消息队列,任务内嵌消息队列只能被该任务接收,可以由其他任务或中断发送。
12.2 任务内嵌消息队列相关 API 函数
TaskQFlush() 清空任务内嵌消息队列中的所有消息
TaskQPend() 获取任务内嵌消息队列中的消息TaskQPendAbort() 终止任务挂起等待任务内嵌消息队列
TaskQPost() 发送消息到任务内嵌消息队列
1. 函数 OSTaskQFlush()
该函数用于清空任务内嵌消息队列中的所有消息。返回被释放的消息数量。
OS_MSG_QTY OSTaskQFlush( OS_TCB* p_tcb,
OS_ERR* p_err)
2. 函数 OSTaskQPend()
该函数用于获取任务内嵌消息队列中的消息。返回指向消息的指针。
void *OSTaskQPend( OS_TICK timeout, //等待超时
OS_OPT opt, //接收消息选项 阻塞/非阻塞
OS_MSG_SIZE* p_msg_size, //接收消息的大小
CPU_TS* p_ts, //时间戳
OS_ERR* p_err)
3. 函数 OSTaskQPendAbort()
该函数用于中止任务等待消息的行为。
CPU_BOOLEAN OSTaskQPendAbort( OS_TCB* p_tcb,
OS_OPT opt,//中止任务等待操作选项
OS_ERR* p_err)
/*
中止,不调度
中止,允许调度
*/
4. 函数 OSTaskQPost()
该函数用于发送消息到任务内嵌消息队列。
void OSTaskQPost( OS_TCB* p_tcb,
void* p_void,
OS_MSG_SIZE msg_size,//消息大小
OS_OPT opt, //发布消息选项
OS_ERR* p_err)
/*
发布,允许调度
发布,不允许调度
*/
第十三章 RTOS 事件标志
13.1 事件标志简介
事件标志是一个用于指示事件是否发生的比特位,一个事件只有发生和未发生两种情况。
事件标志组由多个 事件标志 组成,因此任务就能够依靠 事件标志组 进行任务间的同步。通过事件标志,任务还能够通过多个事件之间的逻辑关系(与、或关系)进行任务间的同步。
13.2 事件标志组
事件标志组是多个事件的集合,使用结构体 flag_grp 定义。os.h
包含一个 32 位的成员变量 Flags。每一个比特能够用来存储一个事件标志。
13.3 事件标志相关 API 函数
FlagCreate() 创建一个事件标志组
FlagDel() 删除一个事件标志组
FlagPend() 等待事件标志组中的事件
FlagPendAbort() 中止任务等待事件标志组
FlagPendGetFlagRdy() 获取任务等待到的事件
FlagPost() 设置事件标志组中的事件
1. 函数 OSFlagCreate()
该函数用于创建一个事件标志组。
void OSFlagCreate( OS_FLAG_GRP* p_grp,
CPU_CHAR* p_name, //flag_grp名称
OS_FLAGS flags, //flag_grp初始值
OS_ERR* p_err)
2. 函数 OSFlagDel()
该函数用于删除一个事件标志组。返回被中止等待事件的任务数量。
OS_OBJ_QTY OSFlagDel( OS_FLAG_GRP* p_grp,
OS_OPT opt, //删除标志组操作
OS_ERR* p_err);
/*
不强删,直到没任务等事件
强删
*/
3. 函数 OSFlagPend()
该函数用于等待事件标志组中的事件。
OS_FLAGS OSFlagPend( OS_FLAG_GRP* p_grp,
OS_FLAGS flags,
OS_TICK timeout,//等待超时
OS_OPT opt, //等待事件选项 阻塞/非阻塞
CPU_TS* p_ts, //时间戳
OS_ERR* p_err)
4. 函数 OSFlagPendAbort()
该函数用于中止任务等待事件标志组。返回被中止挂起的任务数量。
OS_OBJ_QTY OSFlagPendAbort(OS_FLAG_GRP* p_grp,
OS_OPT opt, //中止任务等待事件标志组的操作
OS_ERR* p_err)
/*
不调度
调度
*/
5. 函数 OSFlagPendGetFlagsRdy()
该函数用于获取任务等待到的事件。返回任务获取到的事件标志。
OS_FLAGS OSFlagPendGetFlagsRdy(OS_ERR *p_err)
6. 函数 OSFlagPost()
该函数用于设置事件标志组中的事件。返回返回更新后的事件标志组。
OS_FLAGS OSFlagPost( OS_FLAG_GRP* p_grp,
OS_FLAGS flags, //设置标志
OS_OPT opt, //设置标志操作
OS_ERR* p_err)
/*
设置事件标志时,等待该事件的任务被立刻调度
设置事件标志时,等待该事件的任务不被立刻调度
清除事件标志时,等待该事件的任务被立刻调度
清除事件标志时,等待该事件的任务不被立刻调度
*/
第十四章 RTOS 软件定时器
14.1 软件定时器简介
14.1.1 软件定时器
软件定时器指的是由软件实现的定时器,不是指寄存器操作的硬件定时器。
RTOS 提供的软件定时器是一种向下计数的定时器,每经过一个系统时钟节拍,软件定时器的计数值减一,溢出时自动地去调用对应的超时回调函数。
软件定时器控制块结构体 tmr :
1. 成员变量 CallbackPtr
超时回调函数指针。
2. 成员变量 CallbackPtrArg
超时回调函数参数指针。
只要让 超时回调函数 传入指向 软件定时器控制块结构体 的指针。可以实现多个软件定时器结构体tmr调用同一个超时回调函数callback。
3. 成员变量 Opt
周期性选项变量。该成员变量在软件定时器被创建的时候被初始化,只有两种可能的值,分别为:ONE_SHOT //执行一次就停
和
PERIODIC //周期执行
4. 成员变量 Remain
计数器。当软件定时器开启时,每经过一个系统时钟节拍就将该成员变量减 1。
5. 成员变量 Dly
延时开启时间。开启软件定时器定时器后,延时特定系统时钟节拍数后才开始计数。
6. 成员变量 Period
定时周期。
软件定时器状态:
1. 未使用态
未创建或被删除,处于未使用态。
2. 停止态
被创建但未开启或被停止,处于停止态。
3. 运行态
被启动,处于运行态。
4. 完成态
当单次定时器定时超时后,处于完成态。
14.1.2 超时回调函数
软件定时器的超时回调函数完全由用户自定义,在 软件定时器的结构体tmr 的参数给个 函数指针 就行。
14.1.3 软件定时器任务
软件定时器任务负责定时器更新、计数器重装载、超时函数的调用。
因此不能在软件定时器的超时回调函数中使用可能导致任务被阻塞、被挂起的函数。
函数 OSInit() 中调用函数 OS_TmrInit() 进行了 RTOS 软件定时器功能的初始化,其中就创建了软件定时器任务。
软件定时器任务在处理定时器任务之前,会将自身挂起,等待软件定时器链表待中有软件定时器要超时的时候,再取消挂起状态来处理软件定时器。
14.1.4 软件定时器频率
要注意软件定时器的频率并不等于时钟频率。
OSTmrToTicksMult = OSCfg_TickRate_Hz / OSCfg_TmrTaskRate_Hz;
//tmr计数值时钟倍率 = 系统频率 / 软件定时器频率
软件定时器频率一般设置成和系统频率一样。
14.2 软件定时器相关 API 函数
TmrCreate() 创建一个软件定时器
TmrDel() 删除一个软件定时器
TmrRemainGet() 获取软件定时器的剩余超时时间
TmrSet() 设置软件定时器的延时时间、周期、超时回调函数及其参数等
TmrStart() 开启软件定时器定时
TmrStateGet() 获取软件定时器的状态。
TmrStop() 停止软件定时器定时
1. 函数 OSTmrCreate()
该函数用于创建一个软件定时器。
void OSTmrCreate( OS_TMR* p_tmr,
CPU_CHAR* p_name, //tmr名称
OS_TICK dly, //tmr延时
OS_TICK period, //tmr周期
OS_OPT opt, //tmr操作,单次/周期
OS_TMR_CALLBACK_PTR p_callback,//回调函数指针
void* p_callback_arg, //回调函数指针参数
OS_ERR* p_err)
2. 函数 OSTmrDel()
该函数用于删除一个软件定时器。返回删除是否成功。
CPU_BOOLEAN OSTmrDel( OS_TMR* p_tmr,
OS_ERR* p_err)
3. 函数 OSTmrRemainGet()
该函数用于获取软件定时器的剩余超时时间。
OS_TICK OSTmrRemainGet( OS_TMR* p_tmr,
OS_ERR* p_err)
4. 函数 OSTmrSet()
该函数用于设置软件定时器的延时时间dly、周期remain、超时回调函数及其参数等。
void OSTmrSet( OS_TMR* p_tmr,
OS_TICK dly, //定时器延时
OS_TICK period, //定时器周期
OS_TMR_CALLBACK_PTR p_callback,//回调函数
void* p_callback_arg, //回调函数参数
OS_ERR* p_err)
5. 函数 OSTmrStart()
该函数用于开启软件定时器定时。返回开启是否成功。
CPU_BOOLEAN OSTmrStart( OS_TMR* p_tmr,
OS_ERR* p_err)
6. 函数 OSTmrStateGet()
该函数用于获取软件定时器的状态。
OS_STATE OSTmrStateGet( OS_TMR* p_tmr,
OS_ERR* p_err)
7. 函数 OSTmrStop()
该函数用于停止软件定时器定时。返回停止是否成功。
CPU_BOOLEAN OSTmrStop( OS_TMR* p_tmr,
OS_OPT opt, //停止定时器操作
void* p_callback_arg,//回调参数
OS_ERR* p_err)
/*
停止定时器并从定时器链表移除(一次性定时器/周期性定时器都会停止)
停止一次性定时器
停止周期性定时器
将定时器从定时器链表移除,并调用回调函数
*/
第十五章 RTOS 时间戳
15.1 RTOS 时间戳简介
时间戳一般用来测量时长,比如中断被屏蔽的时长、代码的执行时长。
时间戳就是一个数,这个数从时间戳功能初始化以来,就在时间戳功能时基定时器的驱动下不断增加。
15.1.1 时间戳功能时基定时器
RTOS 的时间戳功能可使用任意位数的硬件或软件定时器模拟 32 位或 64 位定时器作为时基定时器。
时间戳的位数与时基定时器的位数相同,获取时间戳实际上就是获取时基定时器的计数值。
在 ARM Cortex-M 内核中有一个 DWT 外设(Data Watchpoint and Trace,数据观察点和跟踪),在 DWT 外设中有一个 32 位的寄存器 CYCCNT(周期计数寄存器),这是一个 32 位的向上计数器,用于记录内核时钟运行的个数,内核每过一个时钟,CYCCNT 寄存器的值就加 1,就算对于 STM32F1 系列 72MHz 的主频而言,其精度相对来说都是较高的了。
15.1.2 时间戳功能配置项
#define CPU_CFG_TS_32_EN DEF_ENABLED
#define CPU_CFG_TS_64_EN DEF_DISABLED
#define CPU_CFG_TS_TMR_SIZE CPU_WORD_SIZE_32
/*
TS_32_EN 使用 32 位的定时器作为时间戳功能的时基定时器
TS_64_EN 使用 64 位的定时器作为时间戳功能的时基定时器
TS_TMR_SIZE 定义时间戳的位数,不能小于时基定时器的位数
*/
使用 ARM Cortex-M 的 DWT 外设为时间戳功能提供时基,故开启 32位定时器作为时基定时器。
15.1.3 时间戳功能初始化
时间戳功能的初始化是在函数 CPU_Init()中完成的。
函数CPU_TS_Init()就是用于时间戳功能初始化。
函数 CPU_TS_TmrInit() 时间戳定时器(时基)初始化,因为时间戳功能的时基定时器需要由用户自定义配置,因此时间戳定时器初始化 TS_TmrInit() 需要根据实际的情况编写。
为了能够正常使用时间戳功能,还需要额外编写两个相关的函数,
分别为 函数CPU_TS_TmrRd() 和函数 CPU_TS32_to_uSec()。
TmrRd() 用于获取时间戳时基定时器的计数值(CYCCNT寄存器的值)。
TS32_to_uSec()用于将 32 位的时间戳数值转化为以微秒为单位的时间数值
15.2 时间戳相关 API 函数
TS_GET() 获取时间戳
TS32_to_uSec() 将时间戳转换为以微秒为单位的时间数值
1. 函数 OS_TS_GET()
该函数用于获取时间戳。
2. 函数 CPU_TS32_to_uSec()
该函数用于将时间戳转换为以微秒为单位的时间数值。
时间戳变量的单位 CPU_TS_TMR
第十六章 RTOS 内存管理
16.1 RTOS 内存管理简介
内存管理是指软件运行时对内存资源的分配和使用的一种计数,其最主要的目的就是为了能够高效地分配和释放的时候释放不再使用的内存空间。
C语言中有malloc 和 free ,但一般不建议在嵌入式系统中使用,因为malloc和free会产生大量的内存碎片。内存碎片地址不连续难以复用。
RTOS 将一块大内存作为一个内存区,一个内存区中有多个内存块,内存块大小相同,如下图所示:
在同内存区里面的内存块大小都相同,使用 RTOS 的内存管理方案申请和释放内存就只会在部分内存块内产生碎片,仍然保留了大量的可用区域。
用户可以根据实际的需求,创建多个不同的内存区,每个内存区中内存块的数量和大小都可以不同。
内存区所需的内存空间可以由编译器静态分配,也可以使用 malloc 函数动态分配,只需要保证分配给内存区的内存空间不被释放即可。
内存区被分为多个大小相同的内存块,与二维数组相似。
因此在使用静态方式为内存区分配内存时,可以定义一个静态二维数组,将这个二维数组作为内存区所需的内存空间,如下所示:
static uint8_t buffer[10][32];
二维数组作为内存区所需的内存空间,能够表示10个内存块。每块32个字节。也能表示10个内存区,每个内存区32个内存块,每块1字节。这仅仅是一种表达方式。
内存区结构体 os_mem:
void* AddrPtr; /* 指向内存区起始地址的指针 */
void* FreeListPtr; /* 指向内存控制块链表头的指针 */
OS_MEM_SIZE BlkSize; /* 单个内存块的大小 */
OS_MEM_QTY NbrMax; /* 内存区中内存块的总量 */
OS_MEM_QTY NbrFree; /* 内存区中空闲内存块的数量 */
16.2 内存管理相关 API 函数
MemCreate() 创建一个内存区
MemGet() 从内存区中获取一个内存块
MemPut() 释放内存块到内存区中
1. 函数 OSMemCreate()
该函数用于创建一个内存区。
void OSMemCreate( OS_MEM* p_mem, //内存区结构体
CPU_CHAR* p_name, //内存区名称
void* p_addr, //内存区区起始地址
OS_MEM_QTY n_blks, //内存块的数量
OS_MEM_SIZE blk_size,//内存块的大小
OS_ERR* p_err)
2. 函数 OSMemGet()
该函数用于从内存区中获取一个内存块。返回指向内存块的起始地址。
void *OSMemGet( OS_MEM* p_mem,
OS_ERR* p_err)
3. 函数 OSMemPut()
该函数用于释放内存块到内存区中。
void OSMemPut( OS_MEM* p_mem, //内存区指针
void* p_blk, //内存块指针
OS_ERR* p_err)
内存区结构体 mem 用法其实就是 定义个二维数组作为内存区,行作为内存块,元素大小*列数就是内存块大小