移植与原理
UCOS_strongerHuang的博客-CSDN博客
UCOS移植 - 不知道 - 博客园
一、ucos-ii使用
μC/OS-II学习--使用篇(一篇就足够了)_渭c-CSDN博客
8. 互斥量 — [野火]uCOS-III内核实现与应用开发实战指南——基于STM32 文档
二、一些问题
1.操作系统的各状态
状态描述状态转换的条件运行态(Running)任务正在 CPU 上执行- 发生 时间片到期(任务被 OS 切换) - 被 更高优先级任务抢占 - 任务 主动让出 CPU(OSTimeDly())就绪态(Ready)任务可以执行,但 CPU 被别的任务占用- CPU 空闲 或 当前任务释放 CPU - 任务 优先级最高(或抢占模式下,最高优先级任务运行)阻塞态(Blocked)任务在等待某个事件,如信号量、消息队列、超时等- 事件发生(信号量释放、消息队列有数据等) - 超时(如 OSTimeDly() 到期)挂起态(Suspended)任务被手动挂起,不会被 OS 调度- OS 解除挂起(如 OSTaskResume())
各状态之间的转换:
(1) 任务创建
OSTaskCreate() 或 OSTaskCreateExt() 创建任务,任务进入 就绪态。
(2) 任务运行
OS 调度器(OSStart())选择 优先级最高的就绪任务 运行,状态变为 运行态。
(3) 任务被阻塞
任务 等待信号量、消息队列、事件标志等,进入 阻塞态。
(4) 任务从阻塞态恢复
信号量释放、消息队列收到消息,任务回到 就绪态。
(5) 任务主动让出 CPU
OSTimeDly() 让出 CPU,任务从 运行态 → 阻塞态(等待时间到期后回到 就绪态)。
(6) 任务被挂起
OSTaskSuspend() 使任务进入 挂起态,直到 OSTaskResume() 重新唤醒。
2.OSTaskCreate与OSTaskCreateExt的关系
首先看一下两者的函数原型
INT8U OSTaskCreate(void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT8U prio);
task:任务函数指针。
p_arg:传递给任务的参数。
ptos:任务堆栈顶指针(任务栈从高地址向低地址增长,根据OS_STK_GROWTH宏)。
prio:任务优先级(0 最高)。
INT8U OSTaskCreateExt(void (*task)(void *p_arg),
void *p_arg,
OS_STK *ptos,
INT8U prio,
INT16U id,
OS_STK *pbos,
INT32U stk_size,
void *pext,
INT16U opt);
可以看出来比OSTaskCreate多出来5个参数
额外参数:
id:任务 ID(用户自定义,可用于任务管理。。。一般和优先级的数值一样就好)。
pbos:任务 栈底 指针(用于调试或检查任务栈溢出)。
stk_size:任务栈大小(以 OS_STK 为单位)。
pext:扩展参数(通常为 NULL,除非使用任务局部存储 TLS)。
opt:任务选项(如 OS_TASK_OPT_STK_CLR 自动清除任务栈)。
Ext其实就是基础版本的扩展,多了一些功能。
基础版本:
只能设置 任务栈顶,栈底由用户手动计算。
没有扩展功能,如 TLS(线程局部存储)、任务名称等。
EXT版本:(ucos-iii不再有)
可以指定任务栈底,更容易检测栈溢出。 可以扩展任务信息(如 TLS、任务 ID)。 可以提供多个选择opt(OS_TASK_OPT_STK_CLR)。
3.ucos中的三种临界区管理机制
在 Cortex 内核中有着大量的中断向量,当中断被设置并且发生的时候,系统就会从 Thread 模式切换至 Handler 模式;而 NVIC 则保证了中断可嵌套。
但是有的时候我们希望某些代码的执行过程中不要被中断,这些代码被称为临界段代码 Critical Section;那么,在 uCOS 中,系统又是如何做到的呢?
临界段代码是指那些需要连续运行,不可以被打断的代码;一般在STM32上我们有两种需要关注的临界段代码:
外设初始化相关代码
部分外设的初始化强依赖于时序,如果在这些初始化代码的执行过程中触发了中断可能会导致外设初始化失败或者不可预测的行为。
不可重入的函数
我们看一个例子就可以明白什么叫做不可重入函数了:
现在某一个低优先级的任务正在执行 swap 函数,并且已经执行完 temp = *x 这条指令了,假设这个时候 temp 存储在栈上并且被赋值为 1;
此时发生了中断,某一个高优先级的任务抢占了 CPU,并且该高优先级任务也调用了 swap 函数,将 temp 赋值为了 3;
当高优先级任务释放了 CPU 使用权,奇怪的事情发生了,原本应该被赋值为 1 的 temp 变量被刷写为了 3,这就导致变量 y 的值在该函数调用完成后出现了错误。
static int temp;
void swap(int* x, int* y){
temp = *x;
*x = *y;
*y = temp
}
而应对临界段代码,在 uCOS-II 中可以首先关闭中断,当临界段代码执行完毕后在重新开启中断;在实际使用中,我们只需要使用两个宏函数包裹需要的临界段代码即可:
OS_ENTER_CRITICAL();
/* Critical Section Code */
OS_EXIT_CRITICAL();
uCOS-II 中提供了三种方法保护临界段代码:
#if OS_CRITICAL_METHOD == 1
#define OS_ENTER_CRITICAL() __asm__("cli")
#define OS_EXIT_CRITICAL() __asm__("sti")
#endif
#if OS_CRITICAL_METHOD == 2
#define OS_ENTER_CRITICAL() __asm__("pushf \n\t cli")
#define OS_EXIT_CRITICAL() __asm__("popf")
#endif
#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() (cpu_sr = OSCPUSaveSR())
#define OS_EXIT_CRITICAL() (OSCPURestoreSR(cpu_sr))
#endif
第一种方法是使用一条指令关闭中断,在退出临界段代码时重新开启中断。OS_ENTER_CRITICAL()简单地关中断,OS_EXIT_CRITICAL()简单地开中断。这种方式虽然简单高效,但无法满足嵌套的情况。如果有两层临界区保护,在退出内层临界区时就会开中断,使外层的临界区也失去保护。虽然ucos的内核写的足够好,没有明显嵌套临界区的情况,但谁也无法保证一定没有,无法保证今后没有,无法保证在附加的驱动或什么位置没有,所以基本上第一种方法是没有人用的。
第二种方法是将 xPSR 寄存器的状态入栈之后在关闭中断,退出临界段代码时出栈即可。
但是这种方法有时会出现很严重的问题:因为 Cortex-M3 内核是允许从 SP 寄存器相对寻址的,并且大部分的编译器在函数内部都是这么做的,那么如果我们使用堆栈去保存 xPSR 寄存器状态时,编译器未必能够察觉到我们操作了 SP 指针,而在临界段的代码可能会因此出现严重的问题。
举个例子:
int a = 0xABCD1234;
OS_ENTER_CRITICAL();
func(a);
OS_EXIT_CRITICAL(); 假设在某个任务函数内的局部变量 a 保存在了栈上,我们进入临界段代码之后栈指针被推动了(为了保存 xPSR 的内容),但是编译器却并没有察觉到我们修改了 SP 指针,从而变量 a 仍然是按照推动之前的栈指针进行相对寻址,这会导致严重的问题。
第三个方法是我们最常用的方法,在这种方法我们使用一个变量保存当前处理器的中断使能状态,在退出临界段代码时恢复之前的状态;这样就避免了前面说到的两个问题。在 uCOS-II 中通过定义 OS_CRITICAL_METHOD 可以选中我们想要的保护方式;因为前两种方法会出现各种各样的问题,事实上我们在 Cortex-M3 中只使用方法三完成临界段保护。然而无论如何,关闭中断的函数也需要一定的时间去完成,当调用关闭中断的宏函数 OS_ENTER_CRITICAL 后,为了尽快的完成关闭中断的任务,方法三在 STM32 中 uCOS-II 使用了三条汇编指令:
CPU_SR_Save
MRS R0, PRIMASK
CPSID I
BX LR 在使用时,我们需要定义一个名为 cpu_sr 的变量,用于保存当前的中断使能状态;在保存状态之后使用 CPSID I 指令禁止中断,需要注意的是硬件失败仍然会被响应,这里禁止的仅仅是 ISR;最后跳转至我们需要执行的临界段代码即可。
另外一个需要注意的事情就是在临界段代码内 不能够 使用任何的中断资源,有朋友可能会觉得这是没有必要强调的,但事实是,我们仍然需要小心的对待。例如我们在临界段内使用了一个 delay 延迟函数,而很不巧,这个延迟的时基是由 SysTick 中断提供的,这就会导致我们的代码 hang 在 delay 函数内部。
function_a()
{
#if OS_CRITICAL_METHOD == 3
int cpu_sr;
#endif
int a = 1<<31;
OS_ENTER_CRITICAL();
function_b(a);
OS_EXIT_CRITICAL();
}
注:第三种方法有个问题(我也没太明白)
第三种方法对同一个函数体内的嵌套临界区无法支持,这在一些很长大的函数中使用时或许会造成一定困扰。
是指这样?
OS_CPU_SR cpr_sr;
OS_ENTER_CRITICAL(); // 1st entry
// 临界区代码
OS_ENTER_CRITICAL(); // 2nd entry (嵌套)
// 更深层次的临界区代码
OS_EXIT_CRITICAL(); // 退出第二次进入的临界区
OS_EXIT_CRITICAL(); // 退出第一次进入的临界区
OS_ENTER_CRITICAL() 每次调用时,都会用 cpu_sr 覆盖之前cpu_sr 变量的值。
OS_EXIT_CRITICAL() 仅能恢复最后一次 cpu_sr 的状态,嵌套进入的临界区状态会丢失。
这样在嵌套临界区退出时,中断可能会被错误地提前打开,造成 数据竞争 或 不安全行为。
可以多定义几个cpu_sr变量
void my_function(void) {
OS_CPU_SR cpu_sr1, cpu_sr2;
OS_ENTER_CRITICAL(); // 1st entry
cpu_sr1 = cpu_sr; // 备份第一次进入的中断状态
OS_ENTER_CRITICAL(); // 2nd entry
cpu_sr2 = cpu_sr; // 备份第二次进入的中断状态
// 临界区代码
cpu_sr = cpu_sr2; // 恢复第二次的状态
OS_EXIT_CRITICAL();
cpu_sr = cpu_sr1; // 恢复第一次的状态
OS_EXIT_CRITICAL();
}
也可以参考下一般的实时操作系统是如何实现关中断临界区的,就是以显式的方式用局部变量保存中断状态。
int int_lock()
{
int cpu_sr;
__asm__ __volatile__("pushfd \n\t pop %0\n\t cli":"=r"(cpu_sr));
return cpu_sr;
}
void int_unlock(int cpu_sr)
{
__asm__ __volatile__("push %0\n\t popfd"::"r"(cpu_sr));
}
function_a()
{
int a, cpu_sr;
a=1<<31;
cpu_sr = int_lock();
function_b(a);
int_unlock(cpu_sr);
}
int_lock()和int_unlock()的可以用汇编更高效地实现,也可以选择只恢复中断标志的状态。这种方法让我们显示地管理状态保存的情况,我觉得至少要比宏定义清楚多了。
uCOS 原理 - 临界段代码保护 | 初始化博客
ucos中的三种临界区管理机制_ucos三种临界区-CSDN博客
4.任务级切换与中断级切换
在 RTOS(如 UCOS、FreeRTOS) 里,任务切换有 任务级切换 和 中断级切换 两种方式。
任务级切换:
void OSCtxSw(void);
任务级切换是由 OS 任务调度器触发的任务切换,通常在任务代码里主动调用任务切换函数。
任务主动调用 OS 提供的 API(如 OSTimeDly(), vTaskDelay())。
任务等待信号量、消息队列等同步机制,导致任务进入等待状态,触发调度器运行。
上下文切换由 PendSV_Handler 完成。
ucos的任务是抢占式的吗,一个正在执行的低优先级任务,如果此时有一个高优先级任务就绪了会打断正在运行的低优先级任务吗?
uC/OS-II 是抢占式实时操作系统(Preemptive RTOS)。只要有一个 比当前运行任务优先级更高的任务进入就绪态,系统会立即中断当前低优先级任务,转而运行高优先级任务。
既然是抢占式,那么低优先级的任务会不会饿死?
会,如果系统中存在:
一个 高优先级任务不断处于就绪状态(比如频繁中断唤醒、没有延时);
还有几个中优先级任务轮流调度,频繁切换;
而低优先级任务从来没有机会“排到号”执行;
这就会造成 低优先级任务永远执行不到。
如何避免:
①高优先级任务必须等待事件,如:
while (1) {
// do something...
OSTimeDlyHMSM(0, 0, 0, 1); // 延时 1ms,释放 CPU
}
→ 这会让出 CPU,调度器有机会让低优先级任务运行。
②控制高优任务不要长时间霸占 CPU
③使用事件、信号量机制避免无效循环
让任务在没有数据时阻塞等待,不要占 CPU:
OSSemPend(my_sem, 0, &err); // 阻塞等待事件而不是死循环
中断级切换:
void OSIntCtxSw(void);
中断级切换是在中断服务程序(ISR)中触发任务切换,通常用于外部事件触发高优先级任务。
外部中断(EXTI)、定时器(TIM)、串口(USART) 触发中断。
在中断里 发信号量、消息队列,导致任务切换。
优先级高的任务可立即执行。中断返回时才立即切换任务。(在 中断退出前,检查是否需要切换到更高优先级的任务。当 中断结束后,有更高优先级的任务准备就绪,调度器需要切换任务.例如,ISR 中调用 OSIntExit(),如果有高优先级任务需要运行,就调用 OSIntCtxSw() 进行切换。)3
一般需要外部事件快速响应,用中断级切换。如果只是任务间调度,用任务级切换。
任务切换流程:
系统启动时,OSStartHighRdy() 让 CPU 直接执行最高优先级的任务。
任务运行时,如果 调度器决定切换任务,调用 OSCtxSw()。
中断发生时,如果有更高优先级任务就绪,OSIntCtxSw() 进行切换。
5.PendSV
PendSV(Pended System Call)是 Cortex-M 处理器内置的一个系统级中断,用于任务切换(上下文切换)。它的优先级最低,专门用于 OS 任务调度。
在 UCOS、FreeRTOS 这些 RTOS 中,PendSV 主要用于 任务切换。当需要切换任务时,系统不会立即执行任务切换,而是挂起(Pend)一个 PendSV 中断,然后在安全的时机切换任务。
作用如下:
任务切换(上下文切换):当需要切换任务时,RTOS 触发 PendSV,让 PendSV_Handler 负责保存当前任务上下文,再恢复下一个任务的上下文。
延迟执行任务切换:避免任务切换影响高优先级中断的执行。
减少任务切换的开销:因为 PendSV 的优先级最低,不会抢占其他重要的中断(如 SysTick)。
PendSV 任务切换流程
1. 触发 PendSV
当任务需要切换时,OS 调用 SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk; (NVIC里的Interrupt Control and State Register)触发 PendSV。
PendSV 不会立即执行,而是等到 CPU 处理完高优先级任务后,再执行。
2. PendSV_Handler 处理中断
PendSV_Handler 保存当前任务的上下文(寄存器、栈)。
选择新的任务(通过 RTOS 任务调度器)。
恢复新任务的上下文,执行新的任务。
3. 任务切换完成
PendSV_Handler 执行完毕后,CPU 开始执行新任务。
SysTick触发 RTOS 任务调度(定时器)PendSV执行任务切换(低优先级,防止影响其他中断)
6.ucos用TIM可以代替systick实现OS系统滴答
以下情况中可以用TIM代替systick实现ucOS系统时间滴答
SysTick 资源被占用:如果 SysTick 被用于其他用途,比如测量时间间隔,就不能再用作 OS 滴答时钟。
更灵活的时钟源:TIM 可以使用 不同的时钟源(APB 频率),分频更灵活,而 SysTick 只能用 核心时钟(HCLK 或 HCLK/8)。
定时精度需求:TIM 可以提供更高的定时精度和更丰富的功能,比如 输入捕获、PWM 等。
多定时器可用:STM32 具有多个 TIM,可以选择合适的一个,不会影响其他功能。
代码示例:
TIM初始化(这里以TIM2为示例)
void TIM2_Init(void)
{
// 使能 TIM2 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 配置 TIM2
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 1ms 触发一次
TIM_TimeBaseStructure.TIM_Prescaler = (SystemCoreClock / 1000000) - 1; // 1us 计数
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 使能 TIM2 更新中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
// 配置 NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能 TIM2
TIM_Cmd(TIM2, ENABLE);
}
TIM2中断处理
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 进入 OS 中断
OSIntEnter();
// 调用 UCOS 时间滴答函数
OSTimeTick();
// 退出 OS 中断
OSIntExit();
}
}
uC/OS 配置
在 os_cfg.h 里修改:
#define OS_TICKS_PER_SEC 1000 // 每秒 1000 次滴答(1ms)
注:在 os_cpu.h 里屏蔽 SysTick_Handler ,相关代码,并使用 TIM2_IRQHandler。
ucos有自己的systemtick作为时钟滴答,也可以用TIM定时器作为OS的时钟滴答。
总结一下这两者的优缺点:
对比项SysTick 方式TIM 中断方式属于谁Cortex-M 内核内部STM32 外设(TIMx)数量唯一(只能一个)多个(TIM2、3、14、15...)易用性✅ 简单直接支持❌ 需自己配置中断和时基精度高同样高冲突风险可能被其他库用掉(如 HAL、CMSIS)可选任意 TIM 避免冲突可扩展性❌ 无法多源✅ 可用于多个任务定时源工业项目使用较少✅ 更常用占用内核资源是(SysTick Handler 属于内核)否(外设中断)在多 RTOS 并存系统中使用❌ 不方便共用✅ 可灵活分配
再解释一下“Systick可能被其他库用掉(如 HAL、CMSIS),可选任意 TIM 避免冲突。”
有些库 会默认使用 SysTick 来实现延时/节拍功能
库 / 框架如何使用 SysTickCMSISSysTick_Handler 默认实现 delay_ms()、系统滴答STM32 HALHAL_Init() 初始化后,SysTick 会用于 HAL_Delay()、HAL_GetTick()ST 官方 BSP / LL / CubeMX默认用 SysTick 驱动主时钟某些中间件比如 USB、LCD 等也可能使用 SysTick 作为心跳
❗ 问题来了:
你用 HAL 提供的 HAL_Delay(),它背后会用 SysTick_Handler() 而你的 RTOS(比如 uCOS-II / FreeRTOS)也想用这个 SysTick_Handler() 来驱动任务调度 ➤ 就会产生 中断冲突或行为错乱!
而TIM 是 MCU 外设,多个通道可选 → 可避免冲突
STM32 芯片提供很多 硬件定时器 TIMx(TIM1~TIM17,型号不同数量不同):
它们不依赖于内核;
互不干扰,可配置为独立功能:
TIM2 用作系统节拍定时器
TIM3 做 PWM 输出
TIM14 做按键消抖
TIM16 做超时 watchdog
可以让 RTOS 使用 TIM2,而把 SysTick 交给 HAL/CMSIS 用,双方互不打架。
例子:
假设用 STM32CubeMX + HAL + FreeRTOS:
CubeMX 默认会让 FreeRTOS 使用 SysTick
如果你自己又调用了 HAL_Delay()(比如 OLED 驱动里用到)
会发现任务调度不正常,因为 SysTick_Handler() 被 HAL 和 FreeRTOS 同时改写了
👉 最终解决方案是:
让 FreeRTOS 使用 TIMx 作为节拍源
保留 SysTick 给 HAL
7.时间管理类函数
①OSTimeTick()
⏱️ 在每次“滴答时钟中断”发生时调用,驱动整个实时操作系统(RTOS)的节奏。
功能说明
⏳ 系统时钟节拍递增系统节拍计数器 OSTime(或 OS_TimeTickCtr),作为系统运行的时间基准。
⏰ 管理延时任务对调用了 OSTimeDly() / OSTimeDlyHMSM() 的任务的 延时计数器减一。
🔁 任务就绪管理当延时为 0,任务被重新放入就绪队列,等待调度。
🔄 引发调度若有高优先级任务就绪,可能触发一次任务切换。
代码的简略流程:
void OSTimeTick(void)
{
OS_TCB *ptcb;
#if OS_TIME_TICK_HOOK_EN > 0
OSTimeTickHook(); // 用户自定义的钩子函数
#endif
OSTime++; // 增加系统时钟节拍计数器
ptcb = OSTCBList; // 遍历任务控制块链表
while (ptcb != (OS_TCB *)0) {
if (ptcb->OSTCBDly != 0) {
ptcb->OSTCBDly--; // 延时减一
if (ptcb->OSTCBDly == 0) {
OSRdyGrp |= ptcb->OSRdyBit; // 置位就绪表
}
}
ptcb = ptcb->OSTCBNext; // 遍历下一个任务
}
}
一般在systick_handler函数中使用
void SysTick_Handler(void)
{
OSIntEnter(); // 进入中断(更新嵌套计数器)
OSTimeTick(); // 调用时钟节拍处理
OSIntExit(); // 退出中断,可能引发调度
}
OSTimeTick() 是 RTOS 心跳节拍的驱动器,负责时钟推进、任务延时计数、调度触发,是每个 RTOS 滴答中断的核心函数。
②.OSTimeGet
OS_TICK OSTimeGet(void);
用于 获取当前的系统时间节拍数,也就是 OSTimeTick() 累加的结果。
OSTimeGet() 返回的是操作系统从启动后经历的 “滴答时钟数”(tick 数),也就是系统运行的时间节拍计数。
系统每次进入 OSTimeTick()(一般由 SysTick 或定时器中断调用),就会:
OSTime++; // 系统时间加 1
而 OSTimeGet() 就是用来读取这个变量的值。
示例场景:
你可以用这个函数做:
⏲️ 时间差计算
🕒 延时任务处理
🔄 调试当前系统运行的时间
OS_TICK start, end;
start = OSTimeGet();
// do something
end = OSTimeGet();
if (end - start > 100) {
// 花了超过100个tick
}
OSTimeGet() 就像你问 RTOS:“你已经跑了多久?”——它返回的就是“系统心跳数”,是所有基于时间的调度、延时等操作的基础。
③.OSTimeSet()
用于手动设置当前系统节拍计数(tick的函数,也就是可以用它直接修改 OSTime 这个系统“心跳计数器”。
void OSTimeSet(OS_TICK ticks);
这个函数直接将系统的 OSTime 值修改为你指定的 ticks,等同于告诉操作系统:“当前的系统时间从现在开始按你说的走”。
可以用于时间同步、恢复运行状态、或者在调试阶段人为修改系统时间。
OSTimeSet(0); // 重置系统时间
//在掉电恢复或RTC同步后手动校准系统节拍
uint32_t rtc_seconds = ReadTimeFromRTC();
OSTimeSet(rtc_seconds * OS_TICKS_PER_SEC); // 同步系统tick计数器
🐖:
不建议频繁调用 OSTimeSet(),除非确实需要修改系统“时间戳”,否则可能会导致延时/调度行为不准确。
修改 OSTime 不会影响硬件的 SysTick 或 TIM,它只是影响了 RTOS 中任务调度所基于的“逻辑时间”。
④OS_TICKS_PER_SEC宏
这个宏表示 操作系统每秒钟产生的“时钟节拍数”(Tick 数),也就是系统节拍频率。也就是系统每秒中断次数(节拍数),比如设置为 1000 表示 1ms 一次中断。
其作用如下:
用于定义 每秒的节拍次数,也就是系统定时器(如 SysTick) 1 秒中断多少次;
所有与时间相关的 API(如 OSTimeDly()、OSTimeDlyHMSM())内部都会用它来计算延时时长;
控制系统调度的基本单位(每次节拍到达就进行一次调度检查);
#define OS_TICKS_PER_SEC 1000 // 表示 1 秒钟产生 1000 次 Tick(即每 1ms 一次)
#define OS_TICKS_PER_SEC 100 // 表示1秒钟产生100次Tick(即每10ms一次)
也可以设为 100、500、2000 等,只要和 SysTick 配置一致。
注意一个问题:Tick 越多,精度越高,但系统开销越大;Tick 越少,系统负担小,但精度降低。
SysTick 的重装值(reload value)是什么呢?与OS_TICKS_PER_SEC有何关系
SysTick 是一个 24 位的倒计时定时器,用于定时产生中断。在使用 SysTick_Config() 时,需要指定 重装值(Reload Value)
从哪个值开始倒计时,到 0 时触发中断,再重新加载该值开始下一次倒计时。
比如48MHZ,一个周期是1/48MHZ,也就是1/48微秒数值减1,那么
Reload Value = 48,000,000 / 1000 = 48,000
1/48 * 48000 = 1000us = 1ms 中断一次。
关系:Reload Value = SystemCoreClock / OS_TICKS_PER_SEC
再例如OS_TICKS_PER_SEC = 100,那么Reload Value = 480000,也就是10ms终端一次。
参数含义SystemCoreClock当前CPU主频(Hz)OS_TICKS_PER_SEC每秒系统节拍数(Tick 次数)Reload ValueSysTick 倒计时起始值关系Reload Value = SystemCoreClock / OS_TICKS_PER_SEC
Reload 值不能超过 0xFFFFFF(24位),否则 SysTick 配置会失败;
所以你不能让 OS_TICKS_PER_SEC 太小,或 SystemCoreClock 太大;
如果超了,就要用定时器 TIM 来代替。
⑤ OSTimeDly
OSTimeDly() 是 uC/OS-II 中用于让当前任务 延迟指定的时钟节拍(ticks) 的函数,也就是说任务会“睡眠”一段时间,然后由调度器唤醒继续运行。
void OSTimeDly(INT32U ticks);
参数就是延时的时钟节拍数(tick 数)。
一秒钟有多少 tick,由宏 OS_TICKS_PER_SEC 决定(例如 1000 代表每 1ms 产生一个 tick)。此时如果我想让任务延时1s,那么我就需要1000个tick,1ms*1000 = 1s。
// 延时 1 秒(假设 OS_TICKS_PER_SEC 为 1000)
OSTimeDly(1000);
这个任务会挂起 1000 个系统节拍(即 1 秒),期间不会占用 CPU
🐖
只能在任务中调用,不能在中断中使用。 如果你在中断中调用它,系统会异常或崩溃。
ticks = 0 时不会延时,会立即返回。 如果你希望任务释放 CPU 可以使用 OSTimeDly(1)。
在延时期间,任务被挂起,调度器会将 CPU 切换到其它就绪任务。
⑥OSTimeDlyHMSM
OSTimeDlyHMSM() 是 uC/OS-II 中用于让当前任务延时一段指定的时间的函数,时间单位可以精确到时、分、秒、毫秒,使用起来比 OSTimeDly() 更直观和灵活。这个函数会把你传入的时、分、秒、毫秒转换成对应的 tick 数,然后调用底层的 OSTimeDly() 实现延时。
INT8U OSTimeDlyHMSM(INT8U hours,
INT8U minutes,
INT8U seconds,
INT16U milli,
INT8U opt);
在 uC/OS-II 中,opt 参数(延时选项)通常为 0,不起作用。
返回值
OS_ERR_NONE:成功延时
OS_ERR_TIME_INVALID_MINUTES / OS_ERR_TIME_INVALID_HOURS 等:说明参数非法
OS_ERR_TIME_ZERO_DLY:全部时间为 0,不允许
代码示例:
// 延时 1 秒 200 毫秒
OSTimeDlyHMSM(0, 0, 1, 200, 0);
相当于
// 若 OS_TICKS_PER_SEC = 1000,那么就是延时 1200 ticks
OSTimeDly(1200);
⑦OSTimeDlyResume
OSTimeDlyResume() 是 uC/OS-II 提供的一个用于提前唤醒正在延时的任务的函数。简单来说,如果某个任务通过 OSTimeDly() 进入了延时状态,使用 OSTimeDlyResume() 可以让它立即从延时中恢复过来、进入就绪态。
INT8U OSTimeDlyResume(INT8U prio);
参数prio为要恢复的任务的优先级编号(0~63)。
返回值常量含义说明OS_ERR_NONE操作成功OS_ERR_PRIO_INVALID提供的优先级无效(越界)OS_ERR_TASK_NOT_DLY任务当前没有处于延时状态,不能被恢复OS_ERR_TASK_NOT_EXIST该优先级上没有任务
使用场景
某任务延时等待中,但外部事件提前满足(如中断、IO响应);
希望提前唤醒这个任务,恢复处理流程;
类似“打断睡觉”的效果。
🐖
被唤醒的任务在下一次调度时才会执行;
被唤醒前不能已经从延时状态变为就绪状态,否则会返回错误;
不支持唤醒非延时状态的任务;
只能用于唤醒通过 OSTimeDly() 或 OSTimeDlyHMSM() 进入延时状态的任务。
8.中断类函数
①OSIntEnter
这个函数是告诉内核“我正在进入一个中断服务程序(ISR)”,方便操作系统管理中断嵌套和中断中是否需要任务切换。
一般在所有的终端服务函数的开头都要调用这个函数。
void XXX_IRQHandler(void)
{
OSIntEnter(); // 👉 进入中断
...
// 处理中断内容
...
OSIntExit(); // 👉 离开中断(中断结束后检查是否需要任务切换)
}
具体作用:
·中断嵌套计数加一:
uC/OS 通过一个全局变量 OSIntNesting 来记录中断嵌套深度。
void OSIntEnter(void)
{
OSIntNesting++;
}
防止中断嵌套时错误调度:
系统只有在 OSIntNesting == 0 时才会允许进行任务切换; 否则正在处理中断,不该切任务。
配合 OSIntExit():
OSIntExit() 会减 OSIntNesting;
当递减到 0,表示当前已经退出了所有嵌套中断,此时可以调用任务调度器进行上下文切换。
如果漏掉了 OSIntEnter(),中断嵌套深度管理会错乱;
uC/OS 会错误判断此时不在中断里,导致 在中断中执行任务切换,造成系统错误或堆栈混乱!
这里有一个问题:OSIntNesting是临界区的资源吗?
不是,在 μC/OS-II 中,OSIntNesting 是一个用于记录中断嵌套层数的全局变量,但它不属于临界区资源,也不需要额外的保护机制(如关中断或信号量)。
什么是中断嵌套?
中断嵌套:当一个中断服务程序(ISR)正在执行时,如果更高优先级的中断发生,CPU 会暂停当前 ISR,转去执行更高优先级的 ISR,待其完成后才返回原 ISR。而OSIntNesting 的作用就是记录当前中断嵌套的深度(即有多少层中断未返回)。
同时,在中断服务函数(ISR)执行期间,不会立即进行任务切换。只有在 ISR 完成并调用 OSIntExit() 时,才可能触发任务切换。
每次进入 ISR 时,OSIntEnter() 会递增 OSIntNesting。
每次退出 ISR 时,OSIntExit() 会递减 OSIntNesting。
例如以下场景:
低优先级中断 A 触发:
OSIntNesting 从 0 → 1(OSIntEnter())。执行中断 A 的代码。 高优先级中断 B 打断中断 A:
CPU 暂停中断 A,转去执行中断 B。OSIntNesting 从 1 → 2(OSIntEnter())。执行中断 B 的代码。中断 B 完成后,OSIntNesting 从 2 → 1(OSIntExit())。 恢复中断 A:
CPU 继续执行中断 A 的剩余代码。中断 A 完成后,OSIntNesting 从 1 → 0(OSIntExit())。
高优先级中断 B 的修改(1→2→1)会先完成,然后低优先级中断 A 的修改(1→0)才会发生。不存在并发修改:因为高优先级中断 B 的执行是原子的(不会被更低优先级中断打断),所以 OSIntNesting 的修改(递增/递减)是严格串行的: 中断 A: 0 → 1
中断 B: 1 → 2 → 1
中断 A: 1 → 0
那么可能有个问题就是会不会在OSIntNesting++操作的时候有高优先级的中断到来,然后在OSIntNesting++操作没完成就进入了高优先级的ISR中?看下面
什么是临界资源?
共享性:被任务和中断共同访问(如任务间通信的队列)。非原子性操作:需多步操作完成(如链表插入)。抢占风险:可能被高优先级任务或中断打断导致数据不一致。
而OSIntNesting仅在ISR中会被修改,不会在任务中被修改(禁止手动修改),所以不具备共享性。OSIntNesting++操作在绝大多数微控制器(如 ARM Cortex-M、8051、PIC 等)中,对 8位变量(INT8U)的读写操作是单条机器指令,而 中断只能发生在指令之间,不会打断单条指令的执行。
单指令
INC [R1] ; 单指令完成读取-递增-写入(某些 CPU 支持)
多指令
LDRB R0, [R1] ; 读取变量到寄存器(指令1)
ADDS R0, #1 ; 寄存器递增(指令2)
STRB R0, [R1] ; 写回变量(指令3)
待实验
9、临界保护机制
①.OS_ENTER_CRITICAL/OS_EXIT_CRITICAL
本质:关/开中断(保护时间极短的代码段)。
保护 中断和任务都可能访问的共享变量(例如环形缓冲区索引)
非阻塞代码
不能主动引起任务切换的操作
临界区应尽可能短,如果太长那高优先级任务会被硬生生卡住一段时间,系统实时性变差。
这时可能就有一个疑问:如果在OS_ENTER_CRITICAL();保护期间,另一个高优先级任务到来,会执行高优先级任务吗?高优先级任务的保护部分会不会由于OS_ENTER_CRITICAL()而卡死?
如上所述本质上是:关闭中断(全局中断屏蔽),不禁止任务切换本身(但由于中断是触发任务切换的手段之一,因此也间接暂停了上下文切换)。而在 uC/OS-II 中:
任务调度一般发生在:中断处理程序中(如 OSIntExit()),或某些 API 调用中(如 OSSemPost())。而用 OS_ENTER_CRITICAL() 关闭中断,相当于:
“全世界静音,只让我一个人干完事”。其他任务想“吱个声”,都得等你 OS_EXIT_CRITICAL() 放行。
问题答案高优先级任务是否能立即执行?❌ 否,因为中断被关,调度器收不到唤醒信号高优先级任务的临界区会卡死吗?❌ 不会,但它会被延迟到 OS_EXIT_CRITICAL() 执行之后会不会丢中断?✅ 不会丢,中断仍然挂起,OS_EXIT_CRITICAL() 一执行,系统会立即响应
其实这个问题就跟linux的信号是一样的,当正在处理一个信号时,其他的信号屏蔽了,所以处理函数必须短小。
那也简单说一下任务调度的流程:
uC/OS-II 的任务切换是依赖“中断触发 + 调度器判断”来完成的,典型的就是 “系统时钟中断” 来唤醒任务、触发上下文切换。
举一个典型的 uC/OS-II 任务延时 + 系统节拍中断唤醒 + 抢占调度例子:
高优先级任务 A OSTimeDly(200),进入延时等待状态,任务 B 继续运行(优先级低),2 秒后任务 A 被唤醒,抢占任务 B。
①任务 A 执行
OSTimeDly(200); // 延时 2 秒(假设系统节拍为 10ms)
调用后任务 A 被挂起,状态变为 OS_STAT_DELAY
系统在 OSTCBCur->OSTCBDly = 200,表示还要 200 个节拍才能唤醒 A,每个任务都有一个。
②任务 B 继续运行
由于 A 被挂起,系统会自动调度 下一个就绪任务(任务 B)
B 进入运行态,占据 CPU
③系统时钟中断来临(每 10ms 触发一次)(可以自己更改)
SysTick_Handler() {
OSTimeTick(); // uC/OS-II 的节拍处理函数
}
每次中断,都会调用 OSTimeTick()
它会遍历所有延时任务,--OSTCBDly,直到为 0
④当任务 A 的延时倒数到 0:
OSTCBDly == 0 → 改变 A 的状态为就绪
此时 A 被加入就绪链表
因为 A 的优先级高于 B,调度器立即决定 需要切换到 A
⑤执行调度器 OSIntExit()
这个函数在中断尾部由 uC/OS-II 自动调用
OSIntExit() {
// 中断嵌套计数 -1
// 如果嵌套为 0,就执行 OS_Sched()
}
OS_Sched() 比较当前运行任务与最高优先级就绪任务
如果不同 → 发生任务切换(上下文切换)
概念uC/OS-II 机制调度器触发来自中断处理后 OSIntExit() 自动调用延时唤醒机制依靠系统节拍中断(SysTick)定时减计数任务切换时机在中断尾部或任务自己调用调度点时(如 OSSemPost)抢占原则优先级高的任务一旦就绪,立即抢占低优先级任务
②.信号量
OSSemCreate()
OSSemPend()
OSSemPost()
本质:计数型同步机制。
用途:
用于任务之间的同步(不适用于中断中 pend)
可用于实现资源访问控制、事件通知等
支持多个任务等待同一个事件
生产者消费者模型:
void Producer(void) {
Buffer_Add(data);
OSSemPost(sem);
}
void Consumer(void) {
OSSemPend(sem, 0, &err);
Buffer_Get();
}
不能在中断中 Pend()
Post() 可以在中断中使用,但要配合 OSIntEnter()/OSIntExit()
③.互斥锁
本质:特殊信号量,带优先级继承功能。
用途:
多任务之间访问共享资源(比如 LCD、串口)
适用于有优先级反转风险的系统
支持递归上锁(同一任务可以多次获取)
不能在中断中使用
带有优先级继承机制,能提升被阻塞任务的优先级以防止死锁
①优先级继承
“优先级继承(Priority Inheritance)” 是一种 避免“优先级反转”现象的机制,常用于实时操作系统(如 uC/OS-II)中,特别是在使用互斥锁(Mutex)保护共享资源时。
什么是优先级反转?
假设有以下三个任务:
任务名优先级(数值越小优先级越高)TaskH(高)1TaskM(中)5TaskL(低)10
执行场景:
低优先级的 TaskL 获取了一个互斥锁(比如访问串口);
此时高优先级的 TaskH 想要访问同一个资源,也去申请这个互斥锁,但锁已经被 TaskL 占了,TaskH 被挂起;
这时中优先级的 TaskM 正常执行,因为它不需要这个锁,它抢占了 TaskL 的CPU使用权;
结果:高优先级的 TaskH 被中优先级任务延迟了执行,这就是“优先级反转”。
优先级继承如何解决这个问题?
当低优先级的任务(如 TaskL)持有互斥锁时,有更高优先级的任务(如 TaskH)也要这个锁:
操作系统会临时提升 TaskL 的优先级,提升到 TaskH 的级别,这样:
TaskM(中优先级)将不再能抢占;
TaskL 迅速完成临界区工作并释放互斥锁;
TaskH 立即被唤醒执行;
TaskL 的优先级恢复原值。
TaskL(优先级10) —— 获取互斥锁
|
|——— TaskH(优先级1) 想获取锁 ——> 被挂起
|
|——> TaskL 的优先级被提升到1
|
|—— TaskL 运行完释放锁
|
|—— TaskH 立即运行
|
|—— TaskL 优先级恢复为10
信号量(OSSem) 没有优先级继承机制。
只有 互斥锁(Mutex) 支持优先级继承,适合用于保护临界资源。自动支持优先级继承
中断服务程序不应使用互斥锁。
为什么中断服务函数不应该使用互斥锁?
①中断服务函数应该尽可能短快,避免阻塞其他中断或任务;
②互斥锁面向任务,ISR 不是一个被调度的任务(不会进入就绪态/挂起态)
③互斥锁自动支持优先级继承,在 ISR 中,没有优先级的上下文可继承,ISR 也不会被抢占;所以当 ISR 使用互斥锁,系统无法处理优先级继承逻辑。
以上三者的总结
特性/方式OS_ENTER_CRITICAL()信号量(Semaphore)互斥锁(Mutex)作用范围任务和中断中都可用仅限任务上下文仅限任务上下文是否关中断✅ 是(通常关全局中断)❌ 否❌ 否是否支持中断中使用✅ 支持❌ 不支持❌ 不支持是否支持嵌套✅ 是(通过计数器)🚫 否✅ 是(带优先级继承)是否引发任务调度❌ 否✅ 是(例如 OSSemPend() 会阻塞)✅ 是(如锁不可用会阻塞)是否支持优先级继承❌ 否❌ 否✅ 是(防止优先级反转)使用复杂度简单中等中等略高
场景推荐方式中断访问的全局变量OS_ENTER_CRITICAL()中断中发信号给任务OSSemPost()(注意中断包围在 OSIntEnter/Exit())两个任务同步(如事件通知)信号量多任务访问共享资源,担心优先级反转互斥锁串口、SPI 等临界外设资源访问互斥锁简单变量读写(非结构体)保护OS_ENTER_CRITICAL()
④.OSSchedLock()
OSSchedLock() 是 禁止任务调度(不管谁抢占都不给),而互斥锁 是 有序协调多个任务访问共享资源(按规则来)。类似OS_ENTER_CRITICAL
与互斥锁的区别如下:
特性 / 区别OSSchedLock()互斥锁 (OSMutexPend, OSMutexPost)📌 功能本质关调度器,不允许任务切换任务之间有序排队访问资源👥 是否允许其他任务执行❌ 否,所有任务都暂停✅ 是,只是等待资源的任务会阻塞🔐 临界区保护粗暴地锁住整个系统调度精细地锁住某个资源(比如 LCD、串口)⚠ 是否可嵌套使用✅ 支持嵌套(内部有计数)✅ 支持(同一个任务可以重复获取)⏱ 是否可能阻塞❌ 不会(立即生效)✅ 会(任务等待资源可能阻塞)💥 是否适合中断中使用❌ 不可在中断中使用❌ 不可在中断中使用🔄 是否涉及任务切换❌ 不涉及✅ 涉及(Post 时可能唤醒高优先级任务)🧠 是否支持优先级继承❌ 不支持✅ 支持(防止优先级反转)🧩 推荐用途极短时间的全系统“暂停”处理**需要控制“谁在什么时候访问共享资源”**的场景