起因

自制工业控制系统:从UEFI到内核,为线切割机全自研专用系统的立项与技术选型》定了整体方向——用现代技术栈做一个不受第三方系统限制、又不用做得那么复杂的专用工业系统。调度模型是这个系统里最容易被想得过于复杂的一块:一提到"操作系统调度",很容易联想到 Linux CFS 那种复杂的多级反馈队列,或者实时操作系统教科书里一整套优先级继承、优先级天花板协议。这套东西对一台专用机床控制系统来说,大部分是不需要的负担。优先级继承和天花板协议是上世纪八十年代 RTOS 研究为治互斥锁上的优先级反转发明的药,1997 年火星探路者栽在这病上(低优先级的气象任务卡住高优先级的总线任务,整机被看门狗复位)之后才广为人知——可见药再好,病根是"任务共享锁"这个前提本身。工控行业自己还有一支更朴素的血统:前后台系统,主循环转着干杂活、中断干要紧事,DOS 时代 HF 那类线切割控制柜跑了几十年就是这个结构。这篇定下这台线切割机控制系统实际要用的调度模型:不是纯抢占式也不是纯协作式,是按任务的实时性要求分成两级,各自用最简单够用的机制——把那支老血统在现代硬件上重新讲一遍。

一、为什么不是非此即彼的选择

两条路各有一个绕不开的坑:

  • 纯协作式的风险:可靠性完全压在"每个任务都规矩地主动让出 CPU"这条纪律上。图形界面渲染、一次 exFAT 目录扫描,这类任务的执行时长本来就不容易精确控制——扫描碰上一张碎片化严重的 U 盘多出几百毫秒,这几百毫秒里插补停拍、放电还在走,工件表面就烧出一段。只要出一次这种事,“简单"省下的复杂度全得赔回去。
  • 纯抢占式的代价:要在几乎所有共享数据结构上做锁和优先级反转处理,才能保证高优先级任务不被低优先级任务无限拖延。一台专用机床不需要通用操作系统那种"任意优先级任务组合都要正确工作"的普适性保证,为这个买单不值。

x86 平台上有一条更简单的路:中断本身就是硬件级的抢占机制,不需要在软件调度器层面重新发明"抢占"这件事。把真正对时序敏感的逻辑直接挂在中断服务程序(ISR)里,让 CPU 硬件的中断优先级机制去保证确定性,剩下所有对时序不敏感的任务用简单的协作式主循环处理——这样两边都不需要处理最复杂的那部分场景。

二、两级结构

硬实时层(ISR-Driven Tier)

这一级不是"调度器管理的任务”,是直接挂在中断向量上的处理函数,完全绕开任何调度概念。挂在这一级的只有两类工作:

  • 放电脉冲控制:微秒级周期,控制高频放电的开关时序
  • 运动插补节拍:伺服位置更新,固定周期计算下一步该走到哪个坐标

触发源是 APIC 定时器的周期性中断。量级直接摆出来:放电几十 kHz,插补几 kHz(一拍 100µs~1ms)——向量号布局(0xF0 安全 / 0xE0 放电 / 0xD0 插补)在 APIC 篇,三个中断各自的专用栈(IST1/2/3,每栈 16KB,互相打断也不踩脚)在 CPU 自举篇。这一级的纪律要求很硬:每次 ISR 执行时间必须是编译期可预估的上界——给自己立条硬指标:插补 ISR 全路径(按缓存 miss 的最坏情况算)不超过周期的两成,放电 ISR 更紧。不做动态内存分配,不调用任何可能阻塞的逻辑,不等锁。ISR 执行时间一旦失控,“硬实时"三个字就是空话,所以这条纪律没有例外。

安全联锁(急停、限位、断丝检测)挂在比放电/插补更高优先级的中断源上(向量 0xF0,IST1 专用栈),信号经运动控制卡上报。有一条原则必须先说死:急停回路在硬件层面直接切断放电电源,软件中断是"知情并善后"的一方,不是安全的担保人——屏幕上的软急停按钮从来不算安全手段,安全链路上不能有软件的影子。

协作式主循环(Cooperative Tier)

图形界面渲染、鼠键输入处理、U盘/硬盘读写、G代码/3B代码解析,这些任务共用一个大循环,每个任务是一个显式的"步进函数”(step function),做完自己这一小片工作就主动返回。

这一级完全不承担硬实时保证的责任,允许有毫秒级的抖动——因为这里的任务本身对时序不敏感,协作式的简单性正好匹配它们真实的重要程度,不需要为它们付出抢占式调度器的复杂度成本。

为了防止某个 step 函数因为逻辑 bug 陷入死循环拖垮整个协作层,每个 step 进门打一个 TSC 时间戳、出门核对,超阈值(比如 10ms)记一笔 WARN 日志,连续超限就把这个模块挂上诊断界面——真出了事,日志里至少看得见是谁。这不是抢占,是纪律加一道保险丝。

三、两级之间怎么交换数据

两级之间不能共享可变状态用锁保护——锁在 ISR 里本来就是禁物:协作层持锁的时候被 ISR 打断,ISR 又去拿同一把锁,同一个核上就是死锁。改用无锁环形缓冲区做单向数据传递,单生产者单消费者(SPSC)模型,原子操作只需要最简的加载/存储:

  • 协作层 → 硬实时层:运动指令队列。协作层解析完 G代码/3B代码之后,把下一步该走到哪个坐标、该用什么放电参数写进队列尾部;ISR 每个周期从队列头部取一条执行,取不到就保持上一条状态或者进入预设的安全默认状态,不会因为队列空了就出错
  • 硬实时层 → 协作层:状态回传队列。ISR 把当前实际位置、放电电流采样、断丝检测标志写进去;协作层的图形界面、日志任务读出来显示,用户在屏幕上看到的加工状态就来自这条队列

SPSC 环形缓冲区的实现只要一个 2 的幂大小的数组加两个原子索引(头、尾),不需要动态分配,也不需要重量级同步原语。正确性全在两句话的顺序纪律上:生产者先写数据、再发布头指针;消费者先看到头指针、再读数据、最后推进尾指针——发布用 Release、观察用 Acquire。内存序一错(或者偷懒用普通读写),在优化器和缓存重排下迟早读到半新半旧的坐标,这种 bug 台架上是好的、上机台就出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub struct Spsc<T, const N: usize> {      // N 必须是 2 的幂
    buf: [UnsafeCell<T>; N],
    head: AtomicUsize,                    // 生产者写,消费者读
    tail: AtomicUsize,                    // 消费者写,生产者读
}

impl<T: Copy, const N: usize> Spsc<T, N> {
    fn push(&self, v: T) -> bool {
        let h = self.head.load(Relaxed);
        if h.wrapping_sub(self.tail.load(Acquire)) == N { return false; }  // 满
        unsafe { self.buf[h & (N - 1)].get().write(v); }   // ① 先写数据
        self.head.store(h.wrapping_add(1), Release);       // ② 再发布
        true
    }
    fn pop(&self) -> Option<T> {
        let t = self.tail.load(Relaxed);
        if t == self.head.load(Acquire) { return None; }   // 空
        let v = unsafe { self.buf[t & (N - 1)].get().read() };  // ③ 后读数据
        self.tail.store(t.wrapping_add(1), Release);
        Some(v)
    }
}

索引用 h & (N - 1) 掩码折叠,永远不做除法取模——N 是 2 的幂在这里不是优化项,是正确性前提。队列满了怎么办写在调用侧:状态回传这类"新值覆盖旧值无所谓"的,ISR 里直接丢弃加一个丢帧计数;运动指令这种丢不得的,靠协作层监控队列深度、提前降速。环本身永远不阻塞——它伺候的是 ISR,ISR 不等任何人。

四、模块划分

按这个设计,代码结构对应会是:

  • isr_safety.rs——安全联锁,最高优先级中断
  • isr_discharge.rs——放电脉冲控制
  • isr_motion.rs——运动插补节拍
  • spsc_queue.rs——无锁环形缓冲区,供上面三个 ISR 模块和协作层之间传递数据
  • coop_scheduler.rs——协作式主循环,负责按顺序驱动图形界面、输入处理、文件读写、指令解析这些 step 函数

前三个模块依赖定时器与中断框架——寄存器级配置见 APIC 定时器篇,向量表与 IST 专用栈见 CPU 自举篇coop_scheduler.rs 是协作层任务的骨架,图形界面和外设驱动都以 step 函数的形式挂进这个循环。

总结

这套调度模型不追求"通用操作系统级别"的完备性,只解决这台机床实际需要的问题:对时序真正敏感的两件事(放电脉冲、运动插补)直接交给硬件中断机制保证确定性,不进调度器;其余所有不敏感的任务放进一个简单的协作式循环,靠纪律加一道超时保险丝维持公平。两级之间用无锁 SPSC 队列单向传数据,锁和优先级反转这类复杂问题从结构上就不存在。这个设计比教科书里的实时调度理论简单得多,但对这台机床要解决的具体问题是够的——复杂度花在真正需要的地方,不为通用性买单。“确定性"三个字最终怎么用数据证明而不是嘴上说,实时性验证篇收这个尾。

参考链接