为什么要做 MYOS
很多嵌入式项目并不需要一个“功能最全”的 RTOS,而是需要一个“可完全解释”的 RTOS。MYOS 的价值在于把任务切换、调度时机和中断分发压缩成几段短函数,让开发者能直接从 SP、PSW、EA 和定时器寄存器看懂系统状态,而不是隔着厚重抽象层排查问题。
MYOS 是一个面向 8051 与 STM32 小型嵌入式系统的轻量级实时操作系统实验项目。 这个项目主要关注任务调度、中断驱动、任务栈切换与最小化内核实现, 目标不是做功能最全的 RTOS,而是构建一个结构透明、执行路径清晰、便于理解和修改的小型内核。
最近更新:2026-04-29 · 当前状态:持续实验中
在资源受限 MCU 上实现最小化任务调度能力, 验证多任务切换、中断统一入口和独立任务栈管理。
为什么要自己做一个能看得懂、改得动、控得住的 RTOS。
很多嵌入式项目并不需要一个“功能最全”的 RTOS,而是需要一个“可完全解释”的 RTOS。MYOS 的价值在于把任务切换、调度时机和中断分发压缩成几段短函数,让开发者能直接从 SP、PSW、EA 和定时器寄存器看懂系统状态,而不是隔着厚重抽象层排查问题。
通用 RTOS 在功能、移植层和生态上更强,但也会带来更大的心智负担。对于 8051 这类极小内存目标,通用方案的可配置性未必等于可控性。MYOS 选择的是“少做一点,但每一步都清楚”,尤其适合教学、实验平台和对切换路径高度敏感的裸机场景。
从源码能看出,MYOS 不追求复杂优先级继承、对象系统或完整内核服务,而是围绕三个目标展开:一是极简,确保任务状态和上下文恢复路径足够短;二是可控,让调度真正由中断节拍驱动;三是裸机友好,便于直接嵌入已有寄存器级工程。
8051 是理解 RTOS 本质的好平台,因为资源紧、寄存器少、SP 单一,任何调度动作都必须精打细算。STM32 则覆盖了更常见的 Cortex-M 裸机项目,适合作为从“能跑”走向“可维护任务系统”的过渡。MYOS 的思想可以跨平台迁移,但在这两类 MCU 上最能体现它的价值。
这个项目最初用于小型控制系统实验,目标不是实现完整 RTOS 功能集合, 而是在资源有限平台上验证任务调度、中断驱动和任务栈切换机制。
当前重点仍然是内核结构整理与实验验证,因此部分模块仍在持续调整中。
把源码翻译成系统语言,而不是把函数名再读一遍。
从调用关系看,MYOS 的关键路径非常明确:hrtos_main 先把调度入口挂到 HRTOS 框架,再把业务初始化任务注册进去;之后 CPU 进入常驻循环,真正推动 scheduler 前进的是 Timer0 中断;Timer0 一到,my_os 就读取当前中断号、保存当前任务的栈位置、调用 os_select 找到下一项可运行任务,最后通过 os_task_release 恢复该任务的 SP 并以 RETI 方式返回。也就是说,MYOS 的多任务并不是在主循环里轮询函数,而是在中断出口处直接把 CPU 交给另一个任务。
hrtos_main 完成系统挂载,明确谁是系统调度入口,谁是业务初始化入口。my_os 统一接住中断,Timer0 走调度分支,其他中断走分发表。每个函数都从功能、流程、设计原因和系统作用四个层面解释。
os_task_service(u8 id)功能说明:更新当前运行任务编号,把“谁正在执行”写入全局状态。
执行流程:接收任务 ID,直接写入 OS_YUNXING_BIANHAO,不做栈恢复,也不做中断返回。
为什么这样设计:把“逻辑上的任务归属”与“物理上的上下文恢复”分开,能让状态同步保持极轻量,也便于某些场景只切换标记、不立即切换现场。
对系统的作用:它像任务调度里的记账器,让内核任何时候都知道当前 CPU 理论上属于哪个任务。
os_task_release(u8 id)功能说明:恢复目标任务执行权,是实际发生上下文切换的关键函数。
执行流程:先写入当前任务编号,再把 SP 切到 OS_JINCHENG_SP[id],随后清零 PSW、打开 EA,最后执行 RETI。
为什么这样设计:在 8051 里,RETI 会从当前栈顶取回返回地址。只要先把 SP 指向目标任务保存过的栈,CPU 就会从“那个任务上次被打断的位置”继续跑,不需要再写一个复杂跳转器。
对系统的作用:这是 MYOS 把调度决策真正落地成任务恢复的桥梁,也是实时性最核心的一步。
os_task_id()功能说明:读取当前正在运行的任务编号。
执行流程:直接返回 OS_YUNXING_BIANHAO。
为什么这样设计:调度器在保存现场前必须知道“当前是谁”,这个函数把读取动作做成最短路径,避免在中断内引入额外成本。
对系统的作用:它是调度链的定位点,确保被保存栈快照的对象与实际运行任务一致。
os_select(u8 id)功能说明:基于轮询的任务选择算法(循环扫描 + 就绪位判断),负责决定下一次切给谁运行。
执行流程:先从当前任务下一个编号开始向后扫描到 OS_JINCHENG_MAX - 1;如果发现 OS_PEOCESS_OK[i] & 1 为真,就把该任务记为候选;若后半段没命中,再从任务 2 扫回当前任务,形成一轮完整环扫。
为什么这样设计:这是一种非常适合小型 RTOS 的 round-robin scheduler。它不依赖复杂队列,也不维护昂贵的数据结构,只要任务就绪位存在,就能用 O(n) 的固定思路完成挑选。
对系统的作用:它决定了 MYOS 的公平性与简洁度。代码里从任务 2 开始回绕,也说明系统故意把部分编号留给内核或初始化任务,普通任务只在业务区间内轮询。
os_task_protect(u8 id)功能说明:保存当前任务的栈位置,完成最小粒度的上下文保护。
执行流程:把当前中断阶段记录下来的 os_sp 写入 OS_JINCHENG_SP[id]。
为什么这样设计:8051 只有一个硬件栈指针,想做多任务就必须把“每个任务上次停在哪里”单独保存下来。MYOS 没有做超重的上下文块,而是抓住最关键的 SP 快照,这正是轻量级内核的取舍。
对系统的作用:如果没有这一步,切换后任务就无法从原来的调用栈继续执行,整个 RTOS 的任务隔离能力也就不存在了。
os_inter_sp()功能说明:导出当前中断上下文关联的栈指针值。
执行流程:直接返回 os_sp。
为什么这样设计:把内部 SP 快照开放为只读接口,可以让其他内核辅助模块、调试逻辑或监控工具复用同一份现场信息,而不必直接碰内部变量。
对系统的作用:它本身不参与调度决策,但为诊断、扩展和一致性访问提供了基础能力。
volatile void my_os()功能说明:中断驱动调度核心(Timer0 触发调度),同时也是 MYOS 的统一中断入口。
执行流程:先调用 os_inter() 读取中断来源;如果是 1,就把它当成 Timer0 节拍,清理定时器状态后获取当前任务 ID、保存当前任务 SP、调用 os_select 挑出下一任务,再调用 os_task_release 完成切换;如果不是 1,则把中断号映射到 my_inter[],再交给 os_operate 执行对应处理函数。
为什么这样设计:把 scheduler 放在定时中断里,意味着任务切换时机是可预测的;把其他硬件中断也收拢到同一个入口,则让系统入口更统一,避免业务中断各自散落、难以管理。
对系统的作用:它相当于 MYOS 的心跳与总闸门。没有 my_os,任务不会轮转,中断也不会被按自定义方式分发。
hrtos_main()功能说明:系统启动入口,负责把 MYOS 嵌入到底层 HRTOS 运行框架中。
执行流程:先关闭全局中断 EA,再用 os_task(...) 注册 my_os 和 my_main 两个关键入口,之后重新打开中断,最后进入常驻 while(1)。
为什么这样设计:启动时先关中断,是为了避免系统数据结构尚未建立好就被硬件事件打断;先挂调度入口,再挂业务初始化入口,则说明 MYOS 希望先把“内核骨架”放稳,再让应用层开始跑。
对系统的作用:它把“一个裸机工程”变成“一个能被调度的系统”,是所有任务与中断策略的起点。
以下代码按职责重组,目的是辅助理解机制,而不是把原始源码整块堆上来。
这一组函数不负责“挑任务”,而负责标识当前任务、保存 SP 快照,以及在真正切换时恢复目标任务现场。
void os_task_service(u8 id)
{
OS_YUNXING_BIANHAO = id;
}
char os_task_id(void)
{
return OS_YUNXING_BIANHAO;
}
void os_task_protect(u8 id)
{
OS_JINCHENG_SP[id] = os_sp;
}
U8 os_inter_sp(void)
{
return os_sp;
}
void os_task_release(u8 id)
{
OS_YUNXING_BIANHAO = id;
SP = OS_JINCHENG_SP[id];
PSW = 0;
EA = 1;
__asm RETI
}
os_select 的重点不是优先级计算,而是用两段循环完成一次环形扫描,谁的就绪位有效,就把 CPU 让给谁。
U8 os_select(u8 current)
{
u8 i;
for (i = current + 1; i < OS_JINCHENG_MAX; i++)
{
if (OS_PEOCESS_OK[i] & 1)
{
OS_HRTOS = i;
goto selected;
}
}
for (i = 2; i <= current; i++)
{
if (OS_PEOCESS_OK[i] & 1)
{
OS_HRTOS = i;
goto selected;
}
}
selected:
return OS_HRTOS;
}
Timer0 到来时走调度分支,其他硬件中断则从 my_inter[] 分发表找到对应处理函数,这就是 MYOS 的统一入口设计。
u16 my_inter[10];
volatile void my_os(void)
{
U8 irq = os_inter();
if (irq == 1)
{
TF0 = 0;
TH0 = 0;
TL0 = 0;
irq = os_task_id();
os_task_protect(irq);
irq = os_select(irq);
os_task_release(irq);
}
else
{
if (irq)
{
irq--;
}
os_operate(my_inter[irq]);
}
}
这段逻辑说明 MYOS 并不是孤立运行,而是作为一个自定义调度层嵌到 HRTOS 框架上,再由 my_main 接手用户初始化。
void hrtos_main(void)
{
EA = 0;
os_task(my_os, 15, 9, 0);
os_task(my_main, 1, 1, 2);
EA = 1;
while (1)
{
}
}
用文字把一次完整调度闭环走通,读完就能理解这个 RTOS 是怎样工作的。
hrtos_main 先关中断,避免系统表项未建立完成时出现抢占。
my_os 作为自己的统一内核入口挂进去。
my_main 作为初始化任务进入系统,其他业务任务随后在统一的任务编号空间里被管理。
my_os 根据中断号进入调度逻辑;如果是其他中断,则走中断分发表。
os_task_protect 保存当前任务 SP,再由 os_select 做轮询式选择,最后调用 os_task_release 把 SP 指到目标任务。
RETI 从新任务的栈中恢复返回地址,CPU 自然回到该任务上次被打断的位置,调度闭环完成。
当前已完成或正在验证的实验方向。
通过多个任务分别控制 LED 闪烁、按键扫描和计时逻辑, 验证 MYOS 的基本调度能力。
记录不同任务切换前后的 SP 保存与恢复状态, 验证任务上下文恢复逻辑是否稳定。
使用 Timer0 作为系统节拍源, 验证中断驱动调度在固定 Tick 下的任务轮转行为。
正在评估将当前调度结构迁移到 Cortex-M 平台, 研究 PendSV 或 SysTick 替代方案。