H MYOS Kernel Lab
个人项目 · 轻量级实时操作系统

MYOS / HRTOS 项目实战与内核实现

MYOS 是一个面向 8051 与 STM32 小型嵌入式系统的轻量级实时操作系统实验项目。 这个项目主要关注任务调度、中断驱动、任务栈切换与最小化内核实现, 目标不是做功能最全的 RTOS,而是构建一个结构透明、执行路径清晰、便于理解和修改的小型内核。

最近更新:2026-04-29 · 当前状态:持续实验中

项目目标

在资源受限 MCU 上实现最小化任务调度能力, 验证多任务切换、中断统一入口和独立任务栈管理。

目标平台 8051 / STM32
调度方式 Timer0 + 轮询调度
任务切换 SP 快照恢复
项目定位 学习 / 实验 / 裸机实践

当前内容

  • 任务调度器实现分析
  • 中断系统与分发机制
  • 任务栈切换实验
  • 源码结构整理与模块拆解
Design Philosophy

系统设计理念

为什么要自己做一个能看得懂、改得动、控得住的 RTOS。

为什么要做 MYOS

很多嵌入式项目并不需要一个“功能最全”的 RTOS,而是需要一个“可完全解释”的 RTOS。MYOS 的价值在于把任务切换、调度时机和中断分发压缩成几段短函数,让开发者能直接从 SP、PSW、EA 和定时器寄存器看懂系统状态,而不是隔着厚重抽象层排查问题。

为什么不用 FreeRTOS / 现成 RTOS

通用 RTOS 在功能、移植层和生态上更强,但也会带来更大的心智负担。对于 8051 这类极小内存目标,通用方案的可配置性未必等于可控性。MYOS 选择的是“少做一点,但每一步都清楚”,尤其适合教学、实验平台和对切换路径高度敏感的裸机场景。

设计目标:极简、可控、裸机

从源码能看出,MYOS 不追求复杂优先级继承、对象系统或完整内核服务,而是围绕三个目标展开:一是极简,确保任务状态和上下文恢复路径足够短;二是可控,让调度真正由中断节拍驱动;三是裸机友好,便于直接嵌入已有寄存器级工程。

为什么面向 8051 / STM32

8051 是理解 RTOS 本质的好平台,因为资源紧、寄存器少、SP 单一,任何调度动作都必须精打细算。STM32 则覆盖了更常见的 Cortex-M 裸机项目,适合作为从“能跑”走向“可维护任务系统”的过渡。MYOS 的思想可以跨平台迁移,但在这两类 MCU 上最能体现它的价值。

项目背景

这个项目最初用于小型控制系统实验,目标不是实现完整 RTOS 功能集合, 而是在资源有限平台上验证任务调度、中断驱动和任务栈切换机制。

当前重点仍然是内核结构整理与实验验证,因此部分模块仍在持续调整中。

Kernel Mechanics

MYOS 实现机制

把源码翻译成系统语言,而不是把函数名再读一遍。

从调用关系看,MYOS 的关键路径非常明确:hrtos_main 先把调度入口挂到 HRTOS 框架,再把业务初始化任务注册进去;之后 CPU 进入常驻循环,真正推动 scheduler 前进的是 Timer0 中断;Timer0 一到,my_os 就读取当前中断号、保存当前任务的栈位置、调用 os_select 找到下一项可运行任务,最后通过 os_task_release 恢复该任务的 SP 并以 RETI 方式返回。也就是说,MYOS 的多任务并不是在主循环里轮询函数,而是在中断出口处直接把 CPU 交给另一个任务。

1启动阶段由 hrtos_main 完成系统挂载,明确谁是系统调度入口,谁是业务初始化入口。
2运行阶段由 my_os 统一接住中断,Timer0 走调度分支,其他中断走分发表。
3任务切换阶段先保存当前任务栈,再执行基于轮询的任务选择算法,最后恢复目标任务 SP 并从中断返回。
核心判断: 这个设计最精妙的地方不是“选中谁”,而是“把切换放在中断返回前”。这样做省掉了额外跳板,既减少了调度开销,也让实时性更稳定。
Function Breakdown

核心函数拆解

每个函数都从功能、流程、设计原因和系统作用四个层面解释。

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_osmy_main 两个关键入口,之后重新打开中断,最后进入常驻 while(1)

为什么这样设计:启动时先关中断,是为了避免系统数据结构尚未建立好就被硬件事件打断;先挂调度入口,再挂业务初始化入口,则说明 MYOS 希望先把“内核骨架”放稳,再让应用层开始跑。

对系统的作用:它把“一个裸机工程”变成“一个能被调度的系统”,是所有任务与中断策略的起点。

Code Modules

代码展示区

以下代码按职责重组,目的是辅助理解机制,而不是把原始源码整块堆上来。

模块一:任务状态与上下文接口

这一组函数不负责“挑任务”,而负责标识当前任务、保存 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
}

模块二:轮询式 scheduler

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)
    {
    }
}
Execution Flow

系统运行流程图

用文字把一次完整调度闭环走通,读完就能理解这个 RTOS 是怎样工作的。

启动 → 中断初始化 → 任务注册 → 调度循环 → 中断触发 → 任务切换 → 恢复现场
1. 启动 hrtos_main 先关中断,避免系统表项未建立完成时出现抢占。
2. 中断初始化 底层 HRTOS 已提供中断接入能力,MYOS 把 my_os 作为自己的统一内核入口挂进去。
3. 任务注册 my_main 作为初始化任务进入系统,其他业务任务随后在统一的任务编号空间里被管理。
4. 调度循环 主循环本身几乎不做业务,系统真正的节奏由 Timer0 定时中断驱动,这也是 RTOS 与普通 super loop 的分水岭。
5. 中断触发 Timer0 到来后,my_os 根据中断号进入调度逻辑;如果是其他中断,则走中断分发表。
6. 任务切换 系统先用 os_task_protect 保存当前任务 SP,再由 os_select 做轮询式选择,最后调用 os_task_release 把 SP 指到目标任务。
7. 恢复现场 RETI 从新任务的栈中恢复返回地址,CPU 自然回到该任务上次被打断的位置,调度闭环完成。
Project Experiments

实验项目

当前已完成或正在验证的实验方向。

多任务 LED 控制实验

通过多个任务分别控制 LED 闪烁、按键扫描和计时逻辑, 验证 MYOS 的基本调度能力。

任务栈切换验证

记录不同任务切换前后的 SP 保存与恢复状态, 验证任务上下文恢复逻辑是否稳定。

Timer0 调度测试

使用 Timer0 作为系统节拍源, 验证中断驱动调度在固定 Tick 下的任务轮转行为。

STM32 移植预研

正在评估将当前调度结构迁移到 Cortex-M 平台, 研究 PendSV 或 SysTick 替代方案。