Apollo-11制导计算机:36K内存如何实现登月级可靠性

📅 发布时间:2026/10/16 5:30:14
Apollo-11制导计算机:36K内存如何实现登月级可靠性
1. 从一行代码到月球Apollo-11 制导计算机到底藏着什么第一次看到“Apollo-11”这个标题很多人脑子里蹦出来的可能是那段著名的“个人一小步、人类一大步”的录音或者是那台算力还不如现在一块电子表、却把三个人送上月球的制导计算机。我最初接触这个项目也是因为一个很实际的问题为什么半个多世纪前的嵌入式系统在没有现代操作系统、没有高级语言、内存以KB计的条件下能做到那么高的可靠性这个问题背后其实是一整套关于实时系统设计、冗余容错、任务调度和极限资源管理的工程哲学。Apollo-11 这个标题核心指向的是阿波罗计划中登月任务所依赖的那套机载制导与导航系统尤其是Apollo Guidance ComputerAGC。它不是一个普通的“老古董计算机”故事而是一个关于如何在极端约束下做工程决策的经典案例。适合谁来读如果你是对嵌入式开发、实时操作系统、航天软件工程、可靠性设计感兴趣的开发者或者你正在做一个资源极度受限、但对稳定性要求极高的项目那这套东西里的思路放到今天依然有很强的参考价值。我写这篇东西不是要复述历史课本而是想把这套系统里那些“为什么这么设计”的逻辑拆开再结合今天我们能实际动手复现的部分给出一套可操作的路径。你不需要真的去造火箭但你可以用模拟器跑一遍 AGC 的代码看看那个年代的人是怎么用 36K 字的内存写出一个能导航到月球的程序的。这背后的工程判断比代码本身更值钱。2. AGC 的硬件底子36K 内存怎么撑起一次登月2.1 核心参数与设计取舍AGC 的硬件规格放到今天看简直“寒酸”到不可思议。但正是这种寒酸逼出了很多精妙的设计。先看几个关键数字项目规格今天对应概念字长15 位数据 1 位奇偶校验约等于 16 位机内存36K 字约 72KB比一张 1MB 图片还小只读存储核心绳存储器Core Rope类似固化 ROM但可现场编织时钟约 2.048 MHz比现代单片机慢几百倍指令数约 30 条基本指令极简指令集这里最值得说的是核心绳存储器。它不是普通 ROM而是由工人手工把导线穿过磁芯来“编织”程序——穿过去代表 1绕过去代表 0。这种存储方式的好处是一旦编好程序就物理固化不怕辐射翻转也不怕断电丢失。坏处是改一个 bit可能要重新编一整段绳子。所以那个年代的软件发布是真的“烧录”而且烧录一次成本极高。提示今天我们用 Flash 或 EEPROM 时总觉得“写错了再擦除就行”。但 AGC 的绳存储逼着工程师在写代码前必须把逻辑验证到极致。这种“不可逆”的约束反而提升了软件质量。2.2 为什么不用更“先进”的方案当时也有磁芯内存RAM但磁芯内存怕辐射、怕震动、功耗也高。在登月舱那种环境下可靠性优先级远高于性能。核心绳存储虽然写入麻烦但读取极其稳定而且不怕电磁干扰。另一个原因是重量和功耗绳存储不需要持续供电就能保持数据这对电池容量有限的航天器来说至关重要。我实际在模拟器里跑 AGC 代码时发现它的指令周期非常固定几乎没有流水线带来的不确定性。这意味着最坏执行时间是可预测的——这对实时控制系统来说比平均性能重要得多。今天很多实时操作系统RTOS追求的就是这种“确定性”而 AGC 在硬件层面就做到了。2.3 中断与优先级比现代 RTOS 还果断AGC 有一套非常硬核的中断系统。它把任务分成不同优先级高优先级中断可以打断低优先级任务而且中断响应时间极短。更关键的是它有一个**“重启保护”机制**如果系统检测到异常会自动重启并恢复到最近的安全状态同时保留关键数据。这个机制在 Apollo 11 登月最后阶段救了一命。当时雷达数据异常计算机不断发出 1201 和 1202 警报意思是“执行溢出任务队列满了”。如果换成普通系统可能直接死机。但 AGC 的调度器果断丢弃了低优先级任务保住了最关键的导航和着陆程序。这背后的设计思想是在资源不足时不是拼命加资源而是明确什么可以丢什么绝对不能丢。3. 实时任务调度AGC 怎么决定先算哪个3.1 执行周期与任务表AGC 的软件里有一个核心概念叫“执行周期”Executive Cycle。它把任务分成两类长任务和短任务。短任务每个周期都跑长任务分时复用。调度器维护一张任务表每个任务有固定的时间片和优先级。我翻过一些公开的 AGC 源码注释发现它的调度逻辑非常直接每个周期约 11.7 毫秒调度器扫描任务表按优先级从高到低执行。如果某个任务超时会被挂起等下一个周期继续。这种设计没有复杂的抢占式调度算法但胜在行为可预测。注意今天很多 RTOS 用优先级抢占 时间片轮转但 AGC 的选择是“固定周期 优先级队列”。前者灵活后者确定。在航天场景下确定性比灵活性重要得多。3.2 1201/1202 警报的真相很多人以为 1201 警报是“计算机坏了”其实不是。它的完整含义是“执行溢出——没有足够的核心组Core Set来运行当前任务。”简单说就是任务太多内存不够用了。当时登月舱的 rendezvous 雷达被意外打开产生了大量数据占用了本不该占用的计算资源。AGC 的调度器发现任务队列满了于是触发警报同时自动丢弃低优先级任务。地面控制中心的一位年轻工程师这里不点名判断出这是“任务过载”而非“硬件故障”建议继续登月。这个决策背后是对 AGC 调度逻辑的深刻理解。我在模拟器里复现这个场景时故意让雷达数据涌入观察 AGC 的反应。结果确实如文档所述它会先丢低优先级任务然后如果还不够就重启。但重启后能恢复到安全状态不会丢失导航解算的关键数据。这种“优雅降级”的能力是很多现代系统都该学的。3.3 优先级反转与资源竞争AGC 里也有资源竞争问题。比如导航程序和显示程序都要访问某些共享内存。它的解决办法是关键数据只在特定任务中修改其他任务只能读。这其实是一种“单写多读”的简化模型避免了复杂的锁机制。今天我们用互斥锁、信号量来解决竞争但锁本身会带来优先级反转和死锁风险。AGC 的做法更粗暴但更可靠能不用锁就不用锁用任务划分来隔离资源。这在资源极度受限的系统中反而是一种智慧。4. 从绳存储到模拟器怎么在本地跑起 AGC 代码4.1 可用的模拟器与工具链如果你想亲手跑一下 AGC 的代码不需要真的去编绳子。现在有多个开源模拟器可以复现 AGC 的行为。我试过其中两个一个偏重指令级模拟一个偏重系统级仿真。下面是我实际用过的工具链组合yaAGC指令级模拟器能跑原始 AGC 二进制代码。Virtual AGC包含汇编器、模拟器和调试工具适合研究源码。AGC 汇编语言类似现代汇编但语法更原始需要先理解它的指令格式。安装过程不算复杂但有几个坑。比如原始 AGC 代码的二进制格式和现代工具链不兼容需要先转换。另外模拟器的时钟频率需要手动设置否则跑起来会太快或太慢。4.2 编译与运行一个最小示例我建议从最简单的“显示程序”开始。AGC 有一个 DSKYDisplay and Keyboard接口相当于它的“屏幕和键盘”。你可以写一段汇编让它在 DSKY 上显示一个数字然后观察模拟器的输出。# 以 Virtual AGC 为例大致流程如下 # 1. 获取源码和工具链 # 2. 用 yaYUL 汇编器编译 .agc 文件 yaYUL my_program.agc -o my_program.bin # 3. 启动模拟器并加载二进制 yaAGC --coremy_program.bin --dsy运行后你会在模拟器的 DSKY 窗口看到输出。如果没看到检查两个地方一是汇编时是否指定了正确的起始地址二是模拟器的时钟是否设置成了 AGC 的实际频率约 2.048 MHz。提示AGC 的汇编器对格式要求很严标签、操作码、操作数之间必须用特定分隔符。我一开始用空格分隔结果汇编报错。后来查文档才知道它要求用制表符或特定列对齐。4.3 调试与观察任务调度模拟器通常带一个调试模式可以单步执行、查看寄存器和内存。我建议你重点观察执行周期的切换。在调试器里你可以看到调度器如何在不同任务间跳转以及当任务超时后如何处理。一个实用的技巧是在模拟器里人为制造“任务过载”比如让某个任务死循环然后观察 AGC 是否触发 1201 警报。这能帮你直观理解它的容错机制。我试过几次发现它确实会在任务队列满时丢弃低优先级任务而且丢弃的顺序和优先级表完全一致。5. 可靠性设计的底层逻辑为什么它敢载人5.1 冗余与投票机制Apollo 任务里不止一台 AGC。登月舱和指令舱各有一台而且地面还有备份。但更关键的是AGC 内部有奇偶校验和重启保护。奇偶校验能检测单比特错误重启保护能在检测到异常时恢复到安全状态。这种设计思路是不追求永不犯错而是追求犯错后能快速恢复。今天很多高可用系统也是这个逻辑——不是避免故障而是让故障不影响整体。5.2 软件工程中的“防御性编程”AGC 的代码里充满了防御性检查。比如每个子程序入口都会检查参数是否在合理范围内如果不在就跳转到错误处理。这种风格在今天看来可能有点“啰嗦”但在资源受限、无法动态调试的环境下这是唯一的办法。我读过一段 AGC 的导航代码里面有一个“星历表插值”子程序。它在计算前会检查输入角度是否在 0 到 360 度之间如果不是就强制取模。这种检查在今天的代码里可能被优化掉但在当时它是防止系统崩溃的关键。5.3 从 AGC 到现代嵌入式哪些思路可以借鉴如果你正在做嵌入式或实时系统AGC 的这几个思路可以直接用固定周期调度比动态优先级更可预测适合硬实时场景。优雅降级提前定义好哪些任务可以丢哪些不能丢。无锁设计用任务划分代替锁减少竞争和死锁。重启保护定期保存关键状态异常时能快速恢复。我在一个工业控制项目里试过“固定周期 优先级队列”的调度方式发现它的最坏执行时间比用 FreeRTOS 的动态调度更稳定。当然灵活性会差一些但如果你的场景对确定性要求高这值得考虑。6. 模拟器实操中的坑与技巧6.1 时钟频率与指令周期不匹配我第一次跑模拟器时发现程序执行得飞快DSKY 显示一闪而过。后来才知道模拟器默认时钟是 1 GHz而 AGC 实际是 2 MHz。调整时钟后行为就正常了。这个坑很典型模拟器为了速度默认用现代 CPU 频率但被模拟的系统有自己的时序。6.2 汇编器的列对齐问题AGC 汇编器要求操作码和操作数在特定列。如果你用普通文本编辑器很容易因为空格和制表符混用导致对齐错误。我的建议是用支持列模式编辑的编辑器或者直接用官方提供的模板文件。6.3 内存地址的“字”与“字节”混淆AGC 是 15 位字长不是 8 位字节。所以当你看到“36K 内存”时它指的是 36K 个 15 位字不是 36KB。在模拟器里分配内存时如果按字节算会差很多。我一开始就搞混了导致程序加载地址错误。注意AGC 的地址空间是分页的每页 1024 字。跨页跳转需要特殊处理。写汇编时尽量把相关代码放在同一页减少跨页开销。6.4 调试信息的解读模拟器的调试输出里有很多缩写和状态码。比如“1201”是执行溢出“1202”是任务队列满。这些代码在原始文档里有解释但模拟器可能只显示数字。建议提前把常见警报码整理成一张表方便对照。7. 如果今天要复刻一个“迷你 AGC”我会怎么做7.1 硬件选型从现代单片机出发如果让我用今天的硬件复刻一个 AGC 级别的系统我不会去仿制绳存储而是用带 ECC 的 Flash 铁电存储器。铁电存储器FRAM读写速度快、功耗低、抗辐射很适合做关键数据存储。MCU 可以选带锁步核的型号比如某些车规级芯片它们本身就支持冗余计算。7.2 软件架构固定周期 任务表软件上我会用时间触发架构Time-Triggered Architecture。每个任务有固定的执行槽调度器就是一个循环表。这样最坏执行时间完全可预测。任务间通信只用共享内存 状态标志不用队列和锁。7.3 容错策略检查点 重启我会在每个周期结束时保存关键状态到 FRAM。如果检测到异常比如看门狗超时就重启并从最近检查点恢复。同时定义好任务优先级低优先级任务在过载时直接丢弃不尝试补救。7.4 验证方法模拟过载与故障注入验证时我会用故障注入工具模拟内存位翻转、任务超时、中断风暴等场景观察系统是否能优雅降级。这比单纯跑功能测试更能暴露问题。8. 从 AGC 学到的工程判断我在实际项目中用过 AGC 的一些思路后最大的体会是资源越少设计越要清晰。现代开发环境太富裕了内存随便用CPU 随便跑反而容易写出不可预测的系统。AGC 的每一行代码、每一个任务、每一次调度都是经过反复权衡的。另一个体会是可靠性不是靠堆冗余堆出来的而是靠清晰的降级策略。AGC 的重启保护之所以有效是因为它明确知道哪些数据必须保留哪些可以丢。今天很多系统做高可用只是简单加备份但没有定义“降级后哪些功能必须活着”结果一出问题就全崩。最后如果你对 AGC 感兴趣我建议不要只读历史而是真的去跑一下模拟器。当你看到那段用绳存储编出来的代码在屏幕上跳动时你会对“工程”这两个字有不一样的理解。我后来在做一个资源受限的物联网项目时就借鉴了 AGC 的固定周期调度和任务丢弃策略效果比预想的稳。有些东西越老越值钱。