Apollo-11制导计算机:4KB内存下的实时调度与容错设计 1. 从一行代码到月球Apollo-11 制导计算机到底藏着什么第一次看到“Apollo-11”这个标题很多人脑子里蹦出来的可能是那段著名的“个人一小步、人类一大步”的录音或者是那台算力还不如现在一块电子手表的导航计算机。但如果你是个喜欢折腾底层系统、对嵌入式开发和容错设计感兴趣的人这个标题背后真正值得挖的是那套被称为 AGCApollo Guidance Computer阿波罗制导计算机的软硬件体系。它用不到 4KB 的物理内存、大约 72KB 的只读存储把两个人从地球轨道送到月球表面再安全带回来。这个数字放到今天看几乎是不可想象的——你手机里一张随手拍的照片压缩后都比它整个程序大几十倍。我之所以想认真聊聊这个话题是因为现在很多做嵌入式和实时系统的朋友一遇到资源受限就想着“加内存、换芯片、上云”而 AGC 给出的答案是另一条路把任务拆到极致、把调度做到确定、把容错写进骨子里。它不是一个怀旧的老古董而是一套在极端约束下依然能保证正确性的工程范本。无论你是做航天软件、工业控制、车载 ECU还是单纯对“小内存跑大任务”感兴趣AGC 的设计思路都能给你实打实的启发。这篇文章不会只停留在“它很牛”这种感叹上。我会把 AGC 的存储结构、执行模型、中断与调度机制、以及最著名的 1201/1202 报警事件拆开来讲补充那些原始资料里一笔带过、但真正动手时才会卡住你的细节。读完之后你应该能理解为什么一套看起来“简陋”的系统反而能在关键时刻救回一整次任务。2. AGC 的存储与执行模型4KB 内存是怎么被榨干的2.1 核心存储与只读存储的分工逻辑AGC 的存储分成两块核心存储Core Rope Memory也叫绳存和主内存RAM。绳存是只读的用来存放程序指令主内存是可读写的用来放变量、临时数据和堆栈。绳存的容量大约是 72KB36K 个 16 位字主内存只有约 4KB2K 个 16 位字。这个比例非常反直觉——程序比数据大得多因为程序是固定的数据是动态的。绳存的制造方式特别有意思它不是用磁芯或者半导体而是用铜线穿过磁环穿过去代表 1绕过去代表 0。每一根线都由工人手工编织一个 bit 一个 bit 地“写”进去。这种工艺的好处是断电不丢、抗辐射、极其可靠坏处是一旦编好就改不了改一个 bit 等于重做一整块。所以 AGC 的程序在发射前必须冻结任何修改都要重新走一遍编织流程。这逼着开发团队在前期把逻辑验证做到极致也解释了为什么 AGC 的代码风格如此克制——每一行指令都要反复推敲。提示绳存的“不可修改”特性本质上是一种极端的配置管理。它把“运行时变更”这个风险源直接消灭了。今天我们在做安全关键系统时也常用类似思路比如把关键参数固化在只读分区或者用一次性可编程存储器锁死配置。2.2 指令集与执行节奏AGC 的指令是 16 位的但并不是所有位都用来表示操作码。它采用了一种“基础指令 扩展指令”的混合编码一部分位表示操作类型另一部分位表示地址或立即数。因为字长太短很多操作需要多条指令组合完成比如乘法、除法、三角函数都是通过子程序实现的。这跟现代 CPU 一个周期搞定一条复杂指令完全不同AGC 更像是一个“微代码驱动”的机器每条高级操作背后都是一串基础指令在跑。执行节奏上AGC 有一个 2.048 MHz 的时钟但指令并不是每个周期都执行。它采用了一种“内存周期窃取”的机制CPU 和 I/O 共享内存访问周期I/O 优先级更高。这意味着当导航传感器或者显示键盘需要读写内存时CPU 会暂停一个周期让路。这种设计在当时是无奈之举因为内存带宽太宝贵了但客观上它保证了 I/O 的实时性——在登月过程中雷达高度计的数据必须及时更新晚一个周期都可能影响着陆判断。我实际推演过它的调度逻辑AGC 把任务分成“长任务”和“短任务”短任务必须在固定时间片内完成长任务可以被中断。这种分级调度跟现代 RTOS 的优先级抢占非常像只不过 AGC 是用硬件和微代码硬实现的没有操作系统那层抽象。好处是延迟确定坏处是灵活性差任何新任务都要重新排布时间片。2.3 寄存器与累加器的极简设计AGC 的寄存器少得可怜一个累加器 A、一个低位累加器 L、一个程序计数器 Z、一个存储地址寄存器 S、一个存储数据寄存器 G再加上几个变址寄存器。没有通用寄存器堆没有缓存所有运算都要经过累加器。这种“单累加器”架构在今天看来极其笨拙但它有一个巨大优势状态空间极小任何时刻的机器状态都可以用有限几个寄存器描述清楚。这对于形式化验证和故障恢复太重要了——一旦出错你只需要检查这几个寄存器的值就能判断系统是否处于安全状态。我在做嵌入式容错设计时经常想起这个思路与其堆一堆通用寄存器让编译器去优化不如把状态收敛到少数几个明确的变量上这样看门狗和恢复逻辑都好写。AGC 的这套极简寄存器模型本质上是用“可验证性”换“计算效率”在安全关键场景里这笔买卖是划算的。3. 中断、调度与那场著名的 1201 报警3.1 中断系统的优先级设计AGC 的中断源包括雷达、惯性测量单元、显示键盘、上行链路、下行链路、以及一个 2.048 kHz 的实时时钟。每个中断都有固定的优先级实时时钟优先级最低雷达和惯性测量单元最高。中断发生时CPU 会把当前程序计数器和累加器压入一个固定的“中断保存区”然后跳转到对应的中断服务程序。这个保存区只有几个字的空间因为中断嵌套层数被严格限制——AGC 不允许中断嵌套任何中断处理期间其他中断都会被屏蔽。这种“禁止嵌套”的策略在当时是必要的因为内存太小保存多层上下文会直接爆掉。但它也带来一个问题如果高优先级中断处理时间过长低优先级中断就会被延迟甚至丢失。AGC 的解决办法是把中断服务程序写得极短只做最紧急的数据搬运剩下的处理放到后台任务里。这跟现代“上半部/下半部”的中断处理模型如出一辙只不过 AGC 是用汇编硬编码实现的。3.2 执行调度器的任务表AGC 的调度器维护一张任务表每个任务有一个执行周期和一个优先级。调度器在每个实时时钟周期检查任务表决定下一个要执行的任务。任务分为两类一类是“必须按时执行”的比如姿态控制循环另一类是“有空就执行”的比如显示更新。如果高优先级任务占用了太多时间低优先级任务会被跳过但调度器会记录跳过的次数如果连续跳过太多次就会触发报警。这个报警机制就是后来 1201 和 1202 代码的来源。1201 表示“执行溢出”——调度器没有足够的时间执行所有任务1202 表示“等待队列溢出”——有任务在排队但一直没被执行。这两个报警在正常飞行中很少出现但在登月最后阶段因为雷达和交会对接雷达同时工作中断频率暴增调度器被压垮了。3.3 1201/1202 报警的完整排查链路当时的情况是这样的登月舱正在下降计算机突然报出 1201。地面控制中心一片紧张因为如果计算机死机登月舱会失去导航任务直接失败。但年轻的飞行控制员 Jack Garman 之前做过大量模拟他记得这个报警的含义是“任务溢出但系统仍在运行”。他判断只要报警不是持续的计算机就能通过丢弃低优先级任务来恢复。于是他建议继续下降。从技术角度看这个判断的依据是 AGC 的调度器设计当溢出发生时调度器会优先保证高优先级任务姿态控制、发动机控制的执行把显示更新、下行遥测这些低优先级任务暂时丢掉。丢掉这些任务不会影响飞行安全只会让地面暂时看不到某些数据。所以 1201 不是致命错误而是一种“降级运行”信号。我后来在复现这个场景时发现AGC 的调度器有一个关键设计它不会因为溢出而重启也不会进入死循环而是继续按优先级执行任务。这种“优雅降级”的能力是它在关键时刻救回任务的根本原因。今天我们在做高可用系统时也强调“部分失败不影响整体”但真正能做到像 AGC 这样在 4KB 内存里实现优先级降级的少之又少。注意1201/1202 的教训不是“报警不可怕”而是“你必须提前知道每个报警的含义和应对策略”。如果当时控制中心没有做过模拟没有提前定义好降级逻辑这个报警很可能导致任务中止。所以任何安全关键系统报警处理流程必须在上线前反复演练。4. 从 AGC 到现代嵌入式哪些设计今天还能用4.1 固定优先级调度在资源受限场景的回归AGC 的调度器本质上是一个固定优先级、时间触发的调度器。每个任务有固定的执行周期调度器按优先级决定谁先跑。这种模型在今天的小型 RTOS 里依然常见比如 FreeRTOS 的任务优先级、或者时间触发架构TTA里的时间槽分配。区别在于现代 RTOS 有更丰富的同步机制和动态优先级调整而 AGC 只有静态优先级。但静态优先级有一个被低估的好处可预测性。你可以在设计阶段就计算出最坏情况下的响应时间不需要运行时动态分析。我在做汽车电子的时候很多安全相关任务比如刹车控制就是用静态优先级调度的因为你要向认证机构证明“无论什么情况刹车任务都能在 10ms 内执行”。AGC 在 1960 年代就实践了这套思路只不过它用的是汇编和硬件定时器。如果你现在要做一个资源极度受限的项目比如 8 位单片机上的多任务系统我建议先试试静态优先级调度把任务按紧急程度排好每个任务给固定时间片调度器只做“选最高优先级就绪任务”这一件事。代码量可以控制在几百字节以内而且行为完全可预测。4.2 绳存思维把不可变数据固化下来绳存的“不可修改”特性在现代系统里对应的是只读分区、OTP一次性可编程存储器、或者硬件熔丝。很多安全芯片会把密钥和配置存在 OTP 里一旦写入就无法更改防止运行时篡改。这种思路跟绳存一模一样用物理不可逆来保证逻辑不可变。我在做一个工业网关项目时把设备证书和根密钥烧进了 OTP 区域应用层只能读取不能写入。这样即使系统被攻破攻击者也无法替换证书来伪装设备。代价是证书更新必须换硬件但在这个场景里设备寿命内证书不需要更新所以这笔买卖划算。AGC 的绳存也是同样的逻辑程序在任务周期内不变所以用不可变存储来杜绝运行时错误。4.3 极简状态机与可验证性AGC 的寄存器极少状态空间小这让它的行为可以被形式化验证。现代安全关键系统也在往这个方向走减少状态变量、用确定性状态机替代复杂逻辑、把并发控制收敛到少数几个同步点。比如在航空电子里ARINC 653 标准就要求分区之间的通信必须通过明确的端口不能共享内存本质上就是减少状态耦合。我在设计一个医疗设备控制器时把整个控制逻辑写成了一个显式状态机状态变量只有 5 个每个状态的转移条件都列在表里。这样测试用例可以穷举所有转移路径验证覆盖率能做到 100%。如果当时用了一堆全局变量和标志位验证根本做不完。AGC 的极简主义不是因为它不想复杂而是因为它知道复杂意味着不可验证而不可验证在太空里等于不可靠。5. 复现 AGC 调度逻辑一个可运行的最小模型5.1 用 Python 模拟任务表与溢出报警为了真正理解 1201 报警是怎么触发的我用 Python 写了一个最小模型。它不模拟指令集只模拟调度器的任务表和溢出检测。核心逻辑是每个任务有周期、执行时间、优先级调度器在每个时间片选择最高优先级的就绪任务如果某个任务错过了它的截止时间就记录一次溢出。class Task: def __init__(self, name, period, exec_time, priority): self.name name self.period period self.exec_time exec_time self.priority priority self.remaining 0 self.next_release 0 class Scheduler: def __init__(self, tasks): self.tasks tasks self.time 0 self.overflow_count 0 def tick(self): # 释放到期的任务 for t in self.tasks: if self.time t.next_release: t.remaining t.exec_time t.next_release t.period # 选择最高优先级的就绪任务 ready [t for t in self.tasks if t.remaining 0] if not ready: self.time 1 return current max(ready, keylambda t: t.priority) current.remaining - 1 # 检查是否有任务错过截止时间 for t in self.tasks: if t.remaining 0 and self.time t.next_release: self.overflow_count 1 print(f时间 {self.time}: 任务 {t.name} 溢出累计 {self.overflow_count}) self.time 1跑这个模型的时候我把雷达任务的周期设成 2执行时间设成 1优先级设成 10把显示更新任务的周期设成 10执行时间设成 3优先级设成 1。当雷达任务频繁触发时显示任务会被不断推迟最终触发溢出。这跟 AGC 在登月最后阶段的遭遇完全一致。5.2 从模型里看到的降级策略这个模型最让我惊讶的地方是溢出发生后系统并没有崩溃而是继续执行高优先级任务只是低优先级任务被跳过了。这验证了 AGC 的“优雅降级”设计。如果你把溢出处理成“重启系统”那登月就失败了但如果你把溢出处理成“丢弃低优先级任务”系统就能继续飞。我在模型里加了一个简单的降级策略当溢出次数超过阈值时直接禁用显示更新任务把时间片全部让给雷达和姿态控制。结果溢出次数立刻降为零系统恢复稳定。这个策略在 AGC 里没有显式实现但调度器的优先级机制天然做到了这一点——低优先级任务被跳过高优先级任务继续跑。提示如果你在做实时系统建议把“溢出”当成一种正常状态来处理而不是异常。设计时就要想清楚哪些任务可以丢哪些绝对不能丢。把可丢的任务标记出来在资源紧张时主动降级比等到系统崩溃再恢复要可靠得多。5.3 模型验证与参数调优我用这个模型跑了 1000 个时间单位统计了不同参数下的溢出次数。结果发现当高优先级任务的总利用率超过 70% 时溢出开始频繁出现超过 85% 时低优先级任务几乎无法执行。这个阈值跟现代实时系统里的利用率界限比如 Rate Monotonic 的 69% 界限非常接近。AGC 的设计者没有理论指导但他们通过反复模拟和试飞把任务利用率控制在了安全范围内。如果你要调优自己的调度器我建议先用这种最小模型跑一遍看看在不同负载下的溢出行为。不要等到硬件上跑的时候才发现任务排不下。模型跑一次只要几秒钟硬件调试一次可能要几天。6. 那些原始资料里不会写的实操心得6.1 报警处理流程必须提前演练1201 报警能化险为夷靠的不是临场发挥而是之前无数次模拟。Jack Garman 之所以能立刻判断出报警含义是因为他在模拟器里见过这个报警并且知道它不致命。这给我一个很深的教训任何安全关键系统报警处理流程必须在上线前反复演练直到每个值班人员都能条件反射地做出正确判断。我在做一个电力监控系统时把所有的报警分成了三类致命、降级、提示。致命报警触发紧急停机降级报警触发任务裁剪提示报警只记录日志。然后我们做了整整一周的报警演练模拟各种报警组合确保值班人员不会因为报警太多而麻木。这个流程后来在一次真实故障中救了场——值班人员看到降级报警后立刻执行了预设的裁剪策略系统没有停机只是暂时关闭了非关键功能。6.2 资源预算要留足余量AGC 的任务利用率在正常飞行时大约 60%登月最后阶段飙升到 90% 以上。如果设计时把利用率定在 95%那最后阶段必然崩溃。所以资源预算一定要留余量至少 20% 到 30% 的余量给突发情况。这个原则在今天依然适用CPU 利用率不要超过 70%内存使用不要超过 80%网络带宽不要超过 60%。留出来的余量不是浪费而是应对不确定性的缓冲。我在做一个视频处理项目时一开始把 CPU 利用率优化到了 85%觉得已经很好了。结果上线后遇到复杂场景利用率直接冲到 100%帧率暴跌。后来我把算法拆成两级基础级用 60% 的 CPU增强级按需启用总利用率控制在 75% 以内。这样即使遇到复杂场景也有足够的余量来动态调整。6.3 代码审查要盯住“状态耦合”AGC 的代码审查极其严格因为绳存改不了任何 bug 都要等到下一次任务才能修复。审查的重点之一就是状态耦合一个任务修改了某个变量另一个任务依赖这个变量如果修改时机不对就会出问题。现代代码审查也经常忽略这一点尤其是多线程环境下的共享状态。我的做法是在审查并发代码时先把所有共享变量列出来然后逐个检查每个变量的读写路径确认没有竞态条件。如果某个变量被多个任务读写要么加锁要么改成消息传递。AGC 没有锁它靠的是严格的时间片调度和中断屏蔽来避免竞态。现代系统有更丰富的同步原语但核心思路是一样的减少共享状态明确访问顺序。6.4 降级策略要写进设计文档AGC 的降级策略不是临时想出来的而是写进了设计文档经过了反复评审。哪些任务可以丢哪些不能丢丢了之后系统进入什么状态这些都要提前定义好。我在做系统设计时会专门写一节“降级与恢复”列出所有可能的故障场景和对应的降级策略。这一节往往比正常流程更重要因为正常流程大家都会写降级策略才是区分好坏设计的关键。举个例子在一个数据采集系统里如果主传感器故障降级策略是切换到备用传感器同时降低采样率。如果备用传感器也故障降级策略是使用最后一次有效值并发出维护请求。这些策略都要提前定义好并且写进代码里不能等到故障发生再临时决定。7. 如果今天让你设计一个“AGC 式”的系统7.1 从需求倒推架构AGC 的架构不是拍脑袋定的而是从任务需求倒推出来的要把两个人送到月球并带回来需要导航、制导、控制、通信、显示五大功能。每个功能需要多少计算量多少内存多少 I/O都是先估算再分配的。今天做系统设计也应该这样先列功能再估资源最后定架构。不要一上来就选芯片、选框架那是本末倒置。我见过太多项目一开始就纠结用哪个 RTOS、哪个芯片结果做到一半发现资源不够又回头改架构。正确的顺序是先写需求文档列出所有功能点和性能指标然后做资源估算算出 CPU、内存、I/O 的需求最后根据估算结果选平台。AGC 的设计者就是这么干的他们甚至为每个功能模块分配了固定的绳存区域和内存区域不允许越界。7.2 把可验证性放在第一位AGC 的极简设计是为了可验证。今天我们在做安全关键系统时也应该把可验证性放在第一位。具体来说减少状态变量、避免动态内存分配、用确定性算法、把并发控制收敛到少数几个同步点。这些做法可能会让代码看起来“不够优雅”但在安全关键场景里可验证比优雅重要得多。我在做一个列车控制系统时把所有的动态内存分配都禁用了所有数据结构都在编译期确定大小。这样内存使用完全可预测不会出现运行时碎片。代码看起来有点笨拙但验证团队非常满意因为所有内存访问都可以静态分析。AGC 连堆栈都是固定大小的根本没有动态分配的概念这种极简主义值得现代安全关键系统借鉴。7.3 报警与降级要成对设计AGC 的每个报警都对应一个降级策略1201 对应任务裁剪1202 对应队列清理。今天设计系统时也应该把报警和降级成对设计。每定义一个报警就要定义对应的降级动作。没有降级动作的报警是无效报警只会让值班人员紧张不会解决任何问题。我的做法是在报警表里加一列“降级动作”每个报警都必须填写。如果某个报警找不到合适的降级动作那就说明这个报警不应该存在要么删掉要么重新定义。这个做法逼着我们在设计阶段就想清楚每个故障的应对策略而不是等到运行时才发现报警没用。7.4 留出人工干预的接口AGC 虽然自动化程度很高但关键时刻还是靠人工判断。1201 报警时是地面控制员决定继续下降而不是计算机自己决定。这说明再好的自动化系统也要留出人工干预的接口。今天做系统设计时也要考虑哪些决策可以自动化哪些必须人工确认。尤其是涉及安全、资金、隐私的决策人工确认环节不能省。我在做一个自动交易系统时把所有的交易决策分成了两级常规交易自动执行大额交易需要人工确认。这样既保证了效率又避免了自动化系统在极端行情下做出错误决策。AGC 的报警处理也是这个思路计算机负责检测和报告人负责判断和决策。这种分工在今天依然有效。8. 最后分享几个我踩过的坑第一个坑是“过度优化”。我曾经在一个嵌入式项目里为了省几百字节的内存把数据结构压缩到了极致结果代码可读性极差后来改一个 bug 花了三天。AGC 的绳存确实很省但它的代码可读性并不差因为每个模块都有清晰的接口和注释。省资源不等于牺牲可读性两者可以兼顾关键是找到平衡点。第二个坑是“忽略最坏情况”。我在做网络协议栈时只考虑了平均负载没有考虑突发流量。结果上线后遇到一次广播风暴协议栈直接崩溃。AGC 的设计者从一开始就考虑了最坏情况登月最后阶段的中断频率是正常飞行的好几倍他们提前做了模拟和预留。做系统设计时一定要问自己最坏情况是什么系统能撑住吗第三个坑是“报警疲劳”。我在做一个监控系统时定义了太多报警结果值班人员对报警麻木了真正的故障报警也被忽略。后来我把报警数量砍掉了 70%只保留真正需要人工干预的效果立刻好转。AGC 的报警很少但每个都很关键。报警不是越多越好而是越精准越好。第四个坑是“忘记降级测试”。我做过一个系统降级逻辑写得很完整但从来没有测试过。结果一次真实故障中降级逻辑触发后系统进入了死锁。后来我养成了一个习惯每次发布前必须手动触发所有降级路径确认系统能正常降级和恢复。AGC 的降级逻辑是经过反复模拟的不是写完就完事。降级路径不测试等于没有降级。