ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Apollo-11制导计算机:36K代码如何实现载人登月

Apollo-11制导计算机:36K代码如何实现载人登月

1. 从一行代码到月球:Apollo-11 制导计算机到底有多“抠”

第一次看到 Apollo-11 的制导计算机(AGC)参数时,我的反应是:这玩意儿真的把人送上过月球?主频 0.043 MHz,内存 2048 字(一个字 15 位),只读存储器 36K 字,全部代码用汇编手写,没有操作系统,没有高级语言,没有浮点运算单元。放在今天,随便一个电子表都比它算力强。但就是这套系统,在 1969 年完成了载人登月最关键的导航、制导与控制任务。

很多人对 Apollo-11 的理解停留在“人类第一次登月”这个历史事件上,但如果你是从工程或者软件的角度去翻它的源码,会发现这其实是一个极其硬核的实时嵌入式系统案例。它的核心价值不在于“古老”,而在于它展示了一套在极端资源约束下如何做架构取舍、如何设计容错机制、如何让软件在硬件随时可能出问题的情况下依然把宇航员活着带回来的完整思路。

这篇文章适合几类人看:一是对嵌入式开发和实时系统感兴趣的人,AGC 是一个教科书级别的资源受限系统设计样本;二是对软件工程历史有兴趣的人,Apollo-11 的源码在多年前已经公开,里面有很多值得玩味的设计决策;三是做高可靠性系统的人,AGC 的冗余设计和优先级调度机制,放到今天的航天、医疗、工业控制领域依然有参考价值。

我写这篇的出发点很简单:网上讲 Apollo-11 的文章大多停留在“哇好厉害”的层面,很少有人把它的软件架构、执行逻辑、故障处理机制拆开来讲清楚。我想从工程视角,把 AGC 这套系统里最值得学习的几个设计点讲透,包括它为什么这么设计、当时面临什么约束、这些方案在今天看来是否还有意义。

2. AGC 的硬件底子:为什么 36K 代码能管一艘飞船

2.1 内存和存储的真实配置

先把这个系统的硬件参数摆清楚,不然后面讲软件设计会没有参照。

组件规格说明
主频约 0.043 MHz时钟周期约 23 微秒
内存(RAM)2048 字15 位/字,可读写,用于工作变量
只读存储(ROM)约 36K 字15 位/字,存储程序代码
字长15 位不是 8 位也不是 16 位,比较特殊
指令类型约 30 条极简指令集
浮点运算无硬件支持全部用软件模拟

2048 个字的内存是什么概念?你今天写一个稍微复杂点的函数,局部变量加临时对象可能就超过这个数了。但 AGC 要用这 2048 个字管理飞船的姿态、导航、制导、生命支持监控、发动机控制等所有实时任务。这意味着每一个变量的分配都要精打细算,不能有半点浪费。

36K 字的 ROM 用来存程序。你可以理解为整个飞船的“操作系统加应用软件”总共只有这么多空间。对比一下,今天一个简单的手机 App 动辄几十兆,是 AGC 全部代码量的上千倍。

2.2 15 位字长的设计逻辑

为什么用 15 位而不是 8 位或 16 位?这背后有很实际的工程考量。AGC 需要处理的角度、速度、加速度等物理量,用 15 位可以表示的范围和精度刚好够用。如果用 8 位,精度不够,导航计算误差会累积到不可接受的程度;如果用 16 位,硬件成本会增加,而在 1960 年代,每一个逻辑门都是真金白银和重量。

15 位可以表示 0 到 32767 的整数,或者用补码表示 -16384 到 16383。对于大多数导航计算来说,这个范围配合定点运算的缩放策略,可以满足精度要求。AGC 的软件里大量使用“缩放因子”的概念,就是把物理量乘以一个常数后存入整数变量,计算完再除回来。这套做法在今天看来很原始,但在没有浮点单元的硬件上,这是唯一可行的方案。

2.3 只读存储器的物理实现

AGC 的 ROM 不是今天意义上的芯片,而是所谓的“核心 rope 存储器”。每一根线穿过或绕过磁芯代表 0 或 1,程序在出厂前就已经“编织”好了。这意味着程序一旦写好,物理上就不可更改。这种存储方式的可靠性极高,不怕断电,不怕辐射,但代价是没有任何灵活性。

这个细节值得注意:AGC 的软件在发射前经过了无数次地面测试和仿真,因为一旦上天,你不可能像今天推送更新一样去修 bug。所有的逻辑分支、所有的异常处理,必须在编织之前就考虑周全。这种“一次写死”的约束,反而倒逼出了极其严谨的软件工程实践。

3. 执行框架:没有操作系统的系统怎么跑起来

3.1 固定周期调度:一切任务都挂在时间轴上

AGC 没有我们今天理解的操作系统,没有进程、没有线程、没有动态内存分配。它的执行模型非常简单粗暴:一个固定周期的调度器,按照预设的时间表依次执行各个任务。

具体来说,AGC 的调度器以大约 11.7 毫秒为一个基本周期(这个数字来自制导周期的需求)。在每个周期内,调度器按照优先级和固定顺序执行一系列“作业”(job)。每个作业有固定的执行时间预算,如果某个作业超时,调度器会记录异常并继续执行下一个。

这种设计的好处是确定性极强。你可以在设计阶段就计算出最坏情况下每个周期的总执行时间,确保不会超过硬件能力。坏处是灵活性差,任何新增功能都必须重新做时间预算分析,确认不会打破原有的调度平衡。

3.2 优先级设计:为什么导航比显示重要

AGC 的任务优先级不是随便定的,而是按照“如果这个任务失败,宇航员还能不能活着回来”这个标准来排序的。

最高优先级的是姿态控制和制导计算。飞船在太空中随时可能翻滚,如果姿态失控,发动机点火方向就是错的,后果不堪设想。所以姿态控制任务必须保证每个周期都能执行,不能被任何其他任务阻塞。

其次是导航更新。导航计算需要处理雷达数据、惯性测量单元数据,计算出飞船当前的位置和速度。这个任务的实时性要求也很高,但可以容忍偶尔的延迟,因为导航误差可以在后续周期中修正。

最低优先级的是显示和键盘输入处理。宇航员通过 DSKY(显示键盘单元)输入指令、查看数据,这个交互过程慢一点没关系,宇航员可以等。如果系统负载过高,显示刷新可以被推迟甚至跳过。

这种优先级划分的逻辑,在今天的安全关键系统里依然是金科玉律:先保证核心控制回路,再保证状态估计,最后才是人机交互。

3.3 中断处理:什么时候可以打断当前任务

AGC 支持有限的中断。最重要的中断来源是硬件时钟和某些关键传感器。中断处理程序必须极短,通常只是设置一个标志位,把实际处理推迟到下一个调度周期。

这里有一个很值得学习的经验:AGC 的中断处理程序不允许调用任何可能阻塞的函数,也不允许做复杂的计算。所有中断要做的事情就是“记录事件发生了”,然后尽快返回。这种“上半部极简、下半部处理”的模式,和今天 Linux 内核的中断处理设计思路完全一致。

我在实际做嵌入式开发时也遵循这个原则:中断服务程序里只做最必要的事情,比如读取硬件寄存器、清除中断标志、设置一个 volatile 变量。复杂的逻辑全部放到主循环里根据标志位来处理。这个习惯能避免很多难以调试的时序问题。

4. 冗余与容错:当硬件随时可能罢工

4.1 双机热备还是冷备

Apollo 飞船实际上有两套 AGC:指令舱一套,登月舱一套。在登月过程中,登月舱的 AGC 负责主要的制导和导航,指令舱的 AGC 处于监控状态。如果登月舱的 AGC 出现严重故障,理论上可以切换到指令舱的系统,但实际操作中这种切换非常复杂,因为两套系统的传感器和执行机构不完全相同。

更常见的做法是同一套 AGC 内部的冗余机制。AGC 的软件设计了大量自检和交叉验证逻辑。比如导航计算会同时用两种不同的算法计算位置,如果两个结果差异超过阈值,系统会报警并尝试判断哪个算法出了问题。

4.2 重启机制:1202 报警背后的设计哲学

Apollo 11 登月过程中最著名的事件之一就是 1202 报警。这个报警代码的含义是“执行溢出”——调度器发现某个任务没有在规定时间内完成,导致后续任务排队。

当时的情况是:登月舱的会合雷达被意外打开,产生了大量数据,占用了 AGC 的处理时间。调度器检测到任务超时后,触发了重启机制。重启不是简单地重新启动系统,而是执行一套精心设计的恢复流程:清除低优先级任务的队列,保留关键导航数据,重新初始化调度器,然后继续执行。

这个机制的核心思想是:当系统过载时,主动丢弃不重要的任务,保证核心功能存活。1202 报警实际上是一个“优雅降级”的信号,而不是致命错误。宇航员和地面控制人员经过训练,知道这个报警意味着什么,所以没有惊慌。

这个案例给我的启发很大。很多系统在设计时只考虑正常流程,一旦遇到资源不足就崩溃或者死锁。AGC 的做法是:承认资源永远可能不够,提前设计好“不够的时候先扔什么”的规则。这种思路在今天的高并发服务、实时控制系统里同样适用。

4.3 看门狗与故障恢复

AGC 有一个硬件看门狗定时器。如果软件在预定时间内没有“喂狗”,看门狗会触发系统复位。复位后,软件会从特定的入口点重新开始执行,并尝试恢复关键状态。

这里有一个细节:AGC 的复位不是冷启动,而是“热重启”。一些关键变量存储在非易失性存储器或者有备用电源保护的内存区域,复位后可以读取这些变量,恢复到复位前的状态。这种设计让系统在遇到瞬时故障时能够快速恢复,而不是从头开始。

我在做工业控制项目时也用过类似的策略:关键状态定期写入带电池备份的 SRAM,看门狗复位后先读取这些状态,判断是继续执行还是重新初始化。这个做法能显著提高系统的可用性。

5. 人机交互:DSKY 的设计智慧

5.1 为什么只有数字键盘和简单显示屏

DSKY 是宇航员和 AGC 交互的唯一界面。它有一个数字键盘、几个功能键、以及几行七段数码管显示屏。没有图形界面,没有触摸屏,没有鼠标。

这种极简设计不是技术限制,而是可靠性考量。在太空环境中,每一个额外的组件都是潜在的故障点。图形显示屏需要更复杂的驱动电路,触摸屏在失重和振动环境下可靠性存疑。数字键盘加数码管的结构简单、坚固、易于测试。

从软件角度看,DSKY 的交互协议也非常简单:宇航员输入一个“动词”和一个“名词”,比如“动词 06 名词 62”表示“显示当前速度”。AGC 解析这个指令,执行相应操作,把结果以数字形式显示出来。整个交互过程是确定性的、可预测的。

5.2 输入验证与防错

AGC 对 DSKY 输入做了严格的验证。如果宇航员输入了一个非法的动词或名词组合,系统会显示错误提示,拒绝执行。这种验证逻辑在软件中实现,确保不会因为误操作导致危险动作。

更关键的是,某些关键操作需要二次确认。比如发动机点火指令,需要宇航员输入特定的确认码后才会执行。这种“双人规则”在软件层面的体现,就是关键命令需要两个独立的输入步骤。

我在设计任何涉及安全关键操作的系统时,都会借鉴这个思路:危险操作必须有确认步骤,确认步骤不能和初始操作在同一个界面完成,最好有不同的输入通道。这样能最大程度避免误触。

5.3 显示刷新的优先级处理

前面提到,显示任务是低优先级的。这意味着当系统负载高时,DSKY 的显示可能不会立即更新。宇航员按下按键后,可能需要等几百毫秒才能看到结果。

这个延迟在正常操作中是可以接受的,因为宇航员的操作节奏本来就不快。但在紧急情况下,这个延迟可能会让人焦虑。AGC 的设计是:即使显示更新被推迟,按键输入本身不会被丢弃,而是被缓存起来,等系统有空闲时再处理。

这个细节说明了一个重要的设计原则:输入和输出可以有不同的实时性要求。输入不能丢,但输出可以等。很多交互系统在设计时把输入输出绑定在一起处理,导致高负载时输入响应变慢。分开处理能提高系统的响应性。

6. 从 Apollo-11 源码里能学到什么

6.1 代码组织:模块化在 36K 里怎么做

Apollo-11 的源码后来被公开,可以在网上找到。翻看这些代码,最让我惊讶的是它的模块化程度。尽管只有 36K 字的空间,代码依然按照功能划分成清晰的模块:姿态控制、导航、制导、显示驱动、键盘解析、故障处理等。

每个模块有明确的接口定义,模块之间的调用关系被严格控制。这种组织方式让代码可读、可测试、可维护。即使在今天,很多嵌入式项目的代码组织还不如 AGC 清晰。

我个人的经验是:资源越紧张,越需要好的代码结构。因为资源紧张意味着你不能靠“加一层抽象”来解决问题,每一行代码都要用在刀刃上。这时候清晰的模块划分能帮你快速定位问题、评估修改影响。

6.2 注释密度:为什么每行代码都有解释

AGC 源码的注释密度极高,几乎每一行汇编指令都有对应的解释。这些注释不仅说明代码在做什么,还说明为什么这样做、相关的物理量是什么、单位是什么、缩放因子是多少。

这种注释风格在今天的商业代码里很少见,但在安全关键系统里应该是标配。因为这类系统的代码生命周期很长,维护者可能不是原始作者,没有详细的注释根本无法理解和修改。

我在写任何涉及硬件操作或者复杂算法的代码时,都会强制自己写清楚:这个变量的物理意义是什么、取值范围是多少、为什么选这个缩放因子、如果这个值异常可能是什么原因。这些注释在调试时能救命。

6.3 测试与验证:地面仿真怎么做

AGC 的软件在投入使用前经过了大量的地面仿真测试。这些测试包括正常流程测试、边界条件测试、故障注入测试等。测试用例被精心设计,覆盖了各种可能的异常场景。

一个值得学习的做法是:AGC 的测试不仅验证软件逻辑是否正确,还验证软件在硬件故障时的行为。比如模拟某个传感器输出异常值,观察软件是否能检测到并采取正确的容错措施。

这种“故障注入测试”在今天的安全关键系统开发中是标准实践,但在 1960 年代能这样做,说明当时的工程团队对可靠性有极高的要求。

7. 把 AGC 的思路用到今天的项目里

7.1 资源受限场景下的取舍策略

AGC 最核心的经验是:在资源受限时,必须明确什么是最重要的,然后围绕这个核心做设计。AGC 的核心是“把宇航员安全送到月球并带回来”,所有设计决策都围绕这个目标。

在今天做嵌入式或者实时系统时,这个思路依然适用。先问自己:这个系统最核心的功能是什么?如果资源不够,哪些功能可以降级或丢弃?把这些问题的答案写下来,作为架构设计的依据。

我见过很多项目在资源规划时“什么都想要”,结果每个功能都做得半吊子,关键时刻哪个都靠不住。AGC 的做法是反过来的:先保证核心功能绝对可靠,其他功能能砍就砍。

7.2 优先级调度的通用模式

AGC 的固定周期调度加优先级队列的模式,可以直接搬到很多实时系统里。具体做法是:

  1. 确定系统的最小调度周期,这个周期由最紧急的任务决定。
  2. 把所有任务按照紧急程度排序,分配优先级。
  3. 为每个任务估算最坏执行时间,确保一个周期内所有任务的总时间不超过周期长度。
  4. 设计过载处理逻辑:当任务超时时,先丢弃低优先级任务,保证高优先级任务完成。

这套方法在工业控制、机器人、汽车电子等领域都很实用。关键是要在项目早期就做时间预算分析,不要等到系统跑起来才发现时间不够。

7.3 容错设计的检查清单

从 AGC 的容错机制里,我总结了一个检查清单,每次做可靠性要求高的项目时都会过一遍:

  • 系统有没有看门狗?看门狗复位后能否恢复关键状态?
  • 关键数据有没有冗余存储?冗余数据的一致性怎么保证?
  • 有没有过载检测机制?过载时先丢弃什么?
  • 关键操作有没有二次确认?确认逻辑是否独立于初始操作?
  • 有没有自检逻辑?自检覆盖了哪些故障模式?
  • 故障发生后,系统是继续运行、降级运行还是安全停机?

这个清单不一定适用于所有项目,但至少能帮你系统地思考容错问题,而不是等到出事了才临时补救。

8. 一个被忽略的细节:AGC 的软件是“编织”出来的

最后分享一个我觉得很有意思的细节。AGC 的程序不是烧录到芯片里的,而是用“核心 rope 存储器”物理编织出来的。每一根导线穿过磁芯代表 1,绕过磁芯代表 0。这意味着程序在物理上是一张网,修改程序等于重新编织这张网。

这个细节让我想到一个问题:当软件变得“不可修改”时,开发者的心态会有什么不同?在 AGC 的时代,程序员知道自己的代码一旦编织进去就改不了了,所以他们在写每一行代码时都极其谨慎。今天的开发者习惯了随时热更新、随时回滚,反而容易写出质量不高的代码。

我不是说我们应该回到不可修改的时代,但 AGC 的这种“一次写对”的心态值得学习。在提交代码之前问自己:如果这段代码永远不能修改,我还敢这样写吗?如果答案是否定的,那就再改改。

这个思路在我做关键模块开发时特别有用。把每次提交都当成“最后一次修改”,代码质量会有明显提升。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进