1. 从代号到工程:Apollo-11 项目的整体设计思路
第一次看到“Apollo-11”这个代号,很多人脑子里蹦出来的可能是那段著名的登月历史。但在我们这行——尤其是做嵌入式系统、航天软件、高可靠性计算的人眼里,Apollo-11 更像是一个工程哲学的代名词:在极端资源约束下,用极其有限的算力完成一次不容有失的任务。我最早接触这个项目,是因为一个做工业控制的朋友吐槽:“现在的微控制器动不动就几百兆主频、几兆内存,写代码的人反而越来越不讲究了。”他扔给我一份 Apollo-11 制导计算机(AGC)的指令集手册,说:“你看看人家怎么用 2K 内存把两个人送上月球的。”
从那以后,我断断续续花了小半年时间,把 AGC 的架构、指令集、任务调度逻辑、甚至那套著名的“1201/1202 报警”代码都翻了一遍。这篇文章不是历史考据,而是想从一个现代嵌入式开发者的视角,把 Apollo-11 项目里那些至今仍然值得抄作业的设计思路、实操细节和踩坑经验,完整地拆解出来。如果你正在做资源受限的实时系统、高可靠性状态机、或者单纯对“极限编程”感兴趣,这篇内容应该能给你不少启发。
Apollo-11 的核心计算单元是 AGC,全称 Apollo Guidance Computer。它有两个物理模块:一个在指令舱(Command Module),一个在登月舱(Lunar Module)。两者硬件完全一样,软件根据任务阶段切换。这台计算机的主频大约是 2.048 MHz,字长 15 位(加上奇偶校验位凑成 16 位),内存分为核心存储(Core Rope Memory,只读,存放程序)和核心内存(Core Memory,可读写,存放数据)。只读存储的容量是 36K 字,可读写内存只有 2K 字。你没看错,2K 字。一个现代人写个“Hello World”的日志缓冲区可能都不止这个数。
那它要完成什么任务?简单列一下:惯性导航计算、雷达交会对接、推力矢量控制、姿态控制、发动机点火时序、甚至还包括那套著名的“用户界面”——DSKY(Display and Keyboard)。DSKY 只有数字键盘和几位七段数码管,宇航员通过输入“动词+名词”的两位数字组合来执行指令。比如“Verb 06 Noun 62”就是显示当前速度。这套交互逻辑放到今天看简直原始得可爱,但在当时,它必须保证宇航员在高压环境下零误操作。
为什么我要先讲这些?因为理解 Apollo-11 的设计,必须从它的约束条件出发。现代开发中我们习惯的“先跑起来再优化”“内存不够加一条”“算力不足换芯片”,在那个年代统统不成立。你只有 2K 内存,程序烧死在绳子里,改一行代码意味着重新编织核心存储——那是个物理过程,需要几周时间。所以整个项目的设计思路是极度前置的规划、极度保守的冗余、极度清晰的模块划分。这三点,恰恰是今天很多敏捷开发团队最容易忽略的。
1.1 为什么选择“解释器+任务表”的架构
AGC 的软件架构有一个非常关键的设计:它没有采用当时流行的“直接写机器码”或者“简单汇编”的方式,而是实现了一个解释器(Interpreter)。这个解释器运行一套更高级的伪指令,每条伪指令对应一组底层机器操作。程序的主体逻辑用伪指令编写,解释器负责翻译执行。
这个选择在当时是有争议的。反对者认为解释器会浪费宝贵的算力——每执行一条伪指令,都要额外跑一段解释代码。但项目组最终拍板用了这个方案,理由有三条,我把它翻译成现代语言:
第一,代码密度。伪指令的编码效率比原生机器码高得多。原生指令可能一条就要占一个 15 位字,而伪指令可以把多个操作打包进一个字。在 36K 字的只读存储里,这意味着能塞进更多功能。实测下来,解释器方案让有效代码密度提升了大约 2 到 3 倍。
第二,可维护性。任务过程中如果需要修改某个计算逻辑,用伪指令写的代码更容易理解和修改。虽然修改仍然需要重新编织核心存储,但至少程序员不用在二进制层面调试。
第三,任务调度统一。AGC 有一个“执行银行”(Executive Bank)的概念,本质上是一个优先级抢占式调度器。所有任务被分配到不同的优先级队列,调度器根据当前时间片和任务优先级决定执行哪个。解释器让任务切换的上下文保存变得更规整——每个任务的状态可以用统一的数据结构描述。
我后来在一个工业控制器项目里借鉴了这个思路:用一套轻量级的脚本引擎来编排任务,底层用 C 写驱动和中断。结果发现,虽然脚本引擎本身有开销,但整个系统的可测试性和可修改性大幅提升。尤其是现场调试时,改一个任务顺序不需要重新编译固件,直接改脚本就行。当然,前提是你的脚本引擎足够稳定,不能成为新的故障点。
1.2 冗余设计:不是“备份”,而是“异构校验”
Apollo-11 的冗余设计经常被误解为“双机热备”。实际上,AGC 的冗余策略更接近异构校验。指令舱和登月舱各有一台 AGC,但它们并不是简单的主从关系。在任务的不同阶段,两台计算机的角色不同,而且它们之间会进行数据交换和交叉验证。
更关键的是,AGC 内部还有一套自检程序。这套程序会定期检查内存奇偶校验、指令执行结果、传感器数据合理性。如果发现异常,会触发报警并尝试恢复。最著名的例子就是着陆过程中的 1201 和 1202 报警。这两个报警代码的含义是“执行溢出”——调度器发现某个任务占用了超过预期的时间片,导致后续任务排队。
当时宇航员和地面控制中心面临一个选择:继续着陆还是中止。最终决定继续,因为报警的原因是 rendezvous radar 的数据被错误地送进了调度队列,占用了大量 CPU 时间。地面控制中心判断,只要忽略这个雷达数据,AGC 仍然有足够算力完成着陆。这个决策背后,是对系统负载的精确建模——他们知道每个任务的执行时间,知道最坏情况下的余量是多少。
我在做高可靠性系统时,经常拿这个案例提醒团队:冗余不是简单加一台机器,而是要对系统的每一个关键路径做负载分析和故障模式分析。你知道你的系统在 100% 负载下还能保证关键任务执行吗?你知道哪个任务最坏情况下会占用多少时间吗?如果不知道,那冗余就是摆设。
2. 核心细节解析:从指令集到 DSKY 交互
2.1 AGC 指令集:15 位字长的精打细算
AGC 的指令集是围绕 15 位字长设计的。每个字 15 位,加上 1 位奇偶校验,总共 16 位。指令格式分为几种:基本指令、扩展指令、伪指令。基本指令占一个机器字,包含操作码和地址。扩展指令通过操作码切换,可以访问更多地址空间。
这里有一个非常有意思的设计:地址空间的分页机制。36K 字的只读存储被分成多个“存储体”(Bank),每个体 1K 字。程序访问不同体需要切换体寄存器。这个设计在现代微控制器里也很常见,比如某些 8 位单片机用分页访问超过 64K 的存储空间。但 AGC 的分页切换是显式的,程序员必须手动管理体寄存器。这意味着写代码时要非常小心,否则很容易出现“跨体调用”导致的错误。
我实测过用现代汇编模拟 AGC 的体切换逻辑,发现最麻烦的不是切换本身,而是中断处理程序中的体保护。如果中断发生时正在切换体,中断服务程序必须保存当前体寄存器,执行完再恢复。否则返回主程序时就会跑到错误的存储体里。这个坑我在一个 8051 项目里踩过——当时用 bank switching 扩展程序空间,结果中断里忘了保存 bank 寄存器,系统随机跑飞。后来加了保护才稳定。
AGC 的算术指令也很有特点。它没有硬件乘法器和除法器,所有乘除都是通过移位和加法实现的。比如乘法,用“移位-相加”的循环算法。一个 15 位乘法大约需要几十条指令,耗时几百微秒。在 2 MHz 主频下,这个速度其实不算慢,但如果你要算复杂的轨道力学,就必须精打细算。
注意:AGC 的加法指令有“溢出”标志,但没有自动饱和处理。如果加法结果超出 15 位范围,会触发溢出报警。在导航计算中,溢出意味着数据不可信,必须立即处理。
2.2 核心存储:绳子里的程序
Apollo-11 的程序不是存在磁盘或 ROM 里,而是编织在核心存储(Core Rope Memory)里。核心存储的原理是用细导线穿过一个个小磁环,导线穿过磁环表示“1”,绕过磁环表示“0”。整个存储体是一个物理编织物,由工人手工或半自动机器编织。
这个过程有多复杂?一个 36K 字的存储体,大约有 36K × 16 个磁环,每个磁环要穿线。编织完成后还要测试、封装。如果发现一个位错了,理论上可以修复,但实际操作中往往需要重新编织整个模块。所以程序在编织前必须经过极其严格的验证。
我为什么对这个细节感兴趣?因为它解释了为什么 AGC 的软件测试流程如此严苛。现代开发中我们习惯“先发布再修 bug”,但在核心存储时代,一个 bug 的修复成本是几周时间和大量人力。这种约束倒逼出了形式化验证、代码审查、仿真测试等一系列方法。今天我们在做安全关键系统(如医疗设备、汽车刹车控制)时,仍然需要类似的严谨度,只是工具更先进了。
2.3 DSKY:极简交互背后的设计哲学
DSKY 是宇航员与 AGC 交互的唯一界面。它有一个数字键盘(0-9)、几个功能键(Verb、Noun、Enter、Clear 等),以及几行七段数码管。宇航员通过输入“Verb + Noun”的组合来执行操作。比如:
- Verb 06 Noun 62:显示当前速度
- Verb 37 Noun 01:启动着陆程序
- Verb 05 Noun 09:显示报警代码
这套交互逻辑的核心是两位数字编码。每个 Verb 和 Noun 都是两位数字,总共 100 种 Verb 和 100 种 Noun。实际使用的组合大约几十种。为什么不用更直观的菜单?因为菜单需要屏幕和更多按键,而 DSKY 的硬件限制决定了它只能显示数字。
但极简不等于简单。DSKY 的设计考虑了误操作防护。比如,某些关键操作需要连续输入多个 Verb/Noun 组合才能触发。Clear 键可以随时取消当前输入。如果输入了无效组合,DSKY 会显示“OP ERR”(操作错误)。这些细节在今天看很基础,但在当时是经过大量人因工程测试的。
我在设计工业 HMI 时借鉴过 DSKY 的思路:用编码代替菜单,用确认步骤代替单次点击。比如在一个注塑机控制器上,我们把所有操作编码成“功能码+参数码”,操作员需要输入功能码、参数码、再按确认。虽然学习曲线比触摸屏陡,但误操作率大幅下降。尤其是戴手套操作时,数字键盘比触摸屏可靠得多。
3. 实操过程:从任务规划到着陆执行
3.1 任务阶段划分与程序切换
Apollo-11 的任务分为多个阶段:发射、地球轨道、地月转移、月球轨道、着陆、月面活动、上升、返回。每个阶段 AGC 运行不同的程序段。程序切换通过 DSKY 输入或地面指令触发。
以着陆阶段为例,关键程序是 P63(制动阶段)、P64(接近阶段)、P65(着陆阶段)、P66(手动控制)。P63 启动后,AGC 控制下降发动机点火,根据雷达高度和速度计算推力。P64 切换到自动引导,P65 在最后阶段微调,P66 允许宇航员手动接管。
这个流程的难点在于阶段切换的时机判断。如果切换太早,导航数据不准确;切换太晚,可能错过最佳制动窗口。AGC 通过状态机管理阶段切换,每个状态有明确的进入条件和退出条件。条件通常基于高度、速度、剩余燃料等参数。
我后来在一个无人机着陆项目里复现了类似的状态机。我们用高度和下降速率作为切换条件,但发现传感器噪声会导致状态频繁跳变。解决办法是加入滞回区间——比如高度低于 100 米且下降速率大于 2 米/秒才切换到下一阶段,同时要求条件持续 0.5 秒以上。这个技巧在 AGC 里也有体现,只是当时没有“滞回”这个术语。
3.2 1201/1202 报警的现场处理
着陆过程中最惊险的一幕是 1201 和 1202 报警。报警触发后,DSKY 显示代码,宇航员需要快速判断是否继续。地面控制中心通过遥测数据看到报警,同时监控 AGC 的负载情况。
事后分析表明,报警原因是 rendezvous radar 的开关被错误地置于“自动”位置,导致雷达数据持续送入 AGC。这些数据本应在交会对接阶段使用,但在着陆阶段是多余的。AGC 的调度器发现任务队列溢出,于是触发报警并自动重启调度器,丢弃低优先级任务。
这个处理逻辑非常值得学习:当系统过载时,不是崩溃,而是降级运行。AGC 的调度器会优先保证关键任务(如推力控制、姿态控制),丢弃非关键任务(如雷达数据更新)。这种“优雅降级”的能力,在现代高可靠性系统中仍然是核心设计目标。
我在一个视频监控服务器项目里遇到过类似问题:当同时接入的摄像头数量超过设计容量时,服务器会卡死。后来我们借鉴 AGC 的思路,给任务分优先级,过载时自动降低非关键摄像头的帧率或分辨率,保证关键区域不丢帧。效果立竿见影。
3.3 着陆后的数据记录与回放
Apollo-11 的 AGC 有一个数据记录系统,会把关键参数写入核心内存的特定区域。这些数据在任务结束后被读取和分析。记录的内容包括:导航状态、发动机参数、报警历史、宇航员输入序列。
这个记录系统的设计很巧妙:它使用循环缓冲区,当缓冲区满时覆盖最旧的数据。这样即使任务时间很长,也能保留最近一段时间的详细数据。缓冲区的大小是有限的,所以记录哪些参数、记录频率是多少,都需要提前规划。
我在做嵌入式日志系统时,经常用这个思路。比如一个电池管理系统,每 100 毫秒记录一次电压、电流、温度,循环缓冲区保留最近 24 小时的数据。如果发生异常,可以回溯分析。关键是记录频率和缓冲区大小的平衡——频率太高,缓冲区很快被覆盖;频率太低,可能错过关键瞬态。
4. 常见问题与排查技巧实录
4.1 调度器溢出:如何判断是哪个任务惹的祸
AGC 的 1201/1202 报警本质上是调度器溢出。在现代系统中,类似的问题表现为 CPU 占用率 100%、任务超时、看门狗复位。排查思路可以借鉴 AGC 的方法:
第一步,确认溢出是否可复现。如果只在特定条件下出现,记录触发条件。AGC 的报警代码会附带一些上下文信息,比如当前执行的任务编号。
第二步,检查任务执行时间。AGC 的每个任务都有预期的执行时间,如果某个任务实际执行时间远超预期,可能是数据异常导致循环次数增加。比如导航计算中,如果雷达数据跳变,可能导致迭代次数暴增。
第三步,检查任务队列。是否有任务被重复触发?是否有任务没有正确退出?AGC 的调度器有任务状态表,可以查看每个任务的状态。
我整理了一个排查速查表,适用于大多数实时系统:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 周期性超时 | 某个任务执行时间过长 | 用示波器或 GPIO 翻转测量任务耗时 |
| 随机超时 | 中断嵌套过深或中断处理时间过长 | 检查中断优先级和中断服务程序 |
| 启动后立即超时 | 初始化任务未正确退出 | 检查任务状态机的初始状态 |
| 特定操作后超时 | 该操作触发了大量计算或数据更新 | 在该操作前后加时间戳 |
4.2 内存不足:2K 字怎么省着用
AGC 的可读写内存只有 2K 字,程序必须精打细算。常见的省内存技巧包括:
- 变量复用:不同阶段使用同一块内存,但通过状态标志区分。比如导航计算和着陆计算可以共用一些临时变量。
- 位域压缩:多个布尔标志打包进一个 15 位字。AGC 的很多状态字都是位域。
- 查表代替计算:三角函数、对数等复杂计算用查表实现。AGC 的只读存储里存了大量数学表。
- 定点数代替浮点数:AGC 使用定点数运算,虽然精度有限,但节省内存和算力。
我在一个 8 位单片机项目里,RAM 只有 256 字节。当时用了一套类似的技巧:把状态标志打包成位域,用查表法算正弦值,变量按生命周期复用。最后功能全部实现,还剩下 30 多字节余量。
提示:变量复用虽然省内存,但会增加调试难度。建议在复用变量时加注释,说明每个阶段该变量的含义。
4.3 DSKY 输入错误:如何防止误操作
DSKY 的输入错误在任务中可能造成严重后果。AGC 的防护措施包括:
- Verb/Noun 合法性检查:输入无效组合时显示“OP ERR”。
- 关键操作确认:某些操作需要连续输入两个 Verb/Noun 组合。
- Clear 键随时可用:任何输入过程中按 Clear 都会取消当前操作。
- 输入超时:如果一段时间没有按 Enter,当前输入自动取消。
这些措施在今天看很基础,但很多现代 HMI 反而忽略了。我见过一些触摸屏界面,点错一个按钮就直接执行危险操作,没有确认步骤。尤其是在工业环境,操作员可能戴手套或注意力不集中,误操作概率很高。多一步确认,少一次事故,这个原则永远不会过时。
4.4 核心存储编织错误:如何测试和修复
核心存储的编织错误是硬件层面的问题。测试方法通常是逐位读取并校验。AGC 有一套自检程序,上电后会遍历所有存储字,检查奇偶校验。如果发现错误,会记录错误地址并报警。
修复方法取决于错误类型。如果是单个磁环穿线错误,理论上可以手动修复,但实际操作中往往需要更换整个存储模块。这也是为什么程序在编织前必须经过仿真测试——用软件模拟 AGC 的执行,验证所有逻辑路径。
现代开发中,我们虽然不用核心存储,但烧录固件前的仿真测试同样重要。尤其是安全关键系统,建议在硬件上跑之前,先用仿真器或模拟器验证关键流程。我见过太多团队直接烧录然后现场调试,结果一个简单的逻辑错误导致设备损坏。
5. 从 Apollo-11 到现代嵌入式:可复用的经验清单
5.1 优先级调度:关键任务永远有算力
AGC 的调度器是优先级抢占式的。高优先级任务可以打断低优先级任务。这个设计保证了推力控制、姿态控制等关键任务在任何情况下都能得到执行。
现代实时操作系统(RTOS)都支持优先级调度,但很多开发者没有正确使用。常见错误包括:所有任务优先级相同、高优先级任务阻塞等待低优先级任务、中断优先级配置错误。我的经验是:关键任务优先级最高,且其执行时间必须可预测。如果关键任务里有不确定的循环或等待,那优先级调度就失去了意义。
5.2 优雅降级:过载时丢卒保车
AGC 在过载时会丢弃低优先级任务,保证关键任务执行。这个策略在现代系统中同样适用。比如一个网关设备,当连接数超过处理能力时,可以优先保证已建立连接的转发,丢弃新连接请求。或者一个数据采集系统,过载时降低采样率,保证数据不丢失。
实现优雅降级的关键是定义降级等级。哪些功能可以降级?降级后对用户体验的影响是什么?这些需要在设计阶段就明确。我通常会把功能分为“关键”“重要”“次要”三级,过载时按级别依次降级。
5.3 数据记录:循环缓冲区是标配
AGC 的循环缓冲区记录方案简单有效。现代嵌入式系统中,循环缓冲区几乎是标配。但要注意几个细节:
- 缓冲区大小:根据记录频率和需要回溯的时间计算。比如 100Hz 记录,需要回溯 1 小时,那就是 360000 个样本。如果每个样本 4 字节,需要 1.44MB。对于小内存系统,可能需要降低频率或缩短回溯时间。
- 写入原子性:如果记录过程中发生中断,要保证缓冲区指针不被破坏。通常用双指针或原子操作。
- 读取接口:提供方便的读取接口,比如按时间范围读取、按条件过滤。
5.4 人因工程:编码交互的现代应用
DSKY 的编码交互虽然原始,但在特定场景下仍然有效。比如:
- 工业遥控器:用数字编码代替菜单,操作更快。
- 医疗设备:用确认步骤防止误操作。
- 车载系统:用物理按键代替触摸屏,减少分心。
我在一个农业无人机遥控器项目里,把常用操作编码成两位数字,操作员可以盲操作。相比触摸屏,效率提升明显,而且不怕泥水。
5.5 测试策略:仿真先行,硬件在后
AGC 的程序在编织前经过大量仿真测试。现代开发中,仿真测试同样重要。尤其是安全关键系统,建议:
- 单元测试:每个模块独立测试。
- 集成测试:模块组合测试。
- 硬件在环测试:用仿真器模拟传感器和执行器,测试完整系统。
- 故障注入测试:模拟传感器故障、通信中断、电源波动等。
我见过一个团队跳过仿真测试,直接在现场调试。结果一个简单的状态机错误导致设备在特定条件下死机。后来他们补做了仿真测试,发现并修复了多个类似问题。仿真测试的时间投入,远小于现场调试的成本。
6. 我个人在实际操作中的体会
Apollo-11 项目最让我震撼的,不是技术本身,而是在极端约束下做工程决策的思维方式。2K 内存、2MHz 主频、没有浮点运算、程序烧死在绳子里——这些约束在今天看来近乎荒谬,但项目组没有抱怨约束,而是在约束下找到了最优解。
我后来在做嵌入式开发时,经常问自己一个问题:如果我的内存只有现在的十分之一,我会怎么写代码?这个问题逼着我重新审视每一个变量、每一个函数、每一个数据结构。很多时候,我发现代码里有很多“习惯性浪费”——不必要的全局变量、过大的缓冲区、冗余的状态标志。精简之后,系统反而更稳定、更快。
另一个体会是冗余和降级的设计要提前做。AGC 的调度器溢出处理不是临时加的,而是设计之初就考虑到的。现代开发中,我们往往先实现功能,再考虑异常处理。但异常处理如果后加,往往需要改动架构。所以我的建议是:在架构设计阶段就把异常处理、降级策略、数据记录考虑进去。哪怕一开始只实现最简单的版本,也比后期硬塞要好。
最后分享一个小技巧:用状态机管理复杂流程。AGC 的整个任务流程就是一系列状态机。状态机的优点是逻辑清晰、易于测试、便于添加异常处理。我在做任何有多个阶段的系统时,都会先画状态图,定义每个状态的进入条件、退出条件、执行动作。这个习惯帮我避免了很多“流程混乱”的问题。
Apollo-11 的代码和文档现在都可以在网上找到,包括那套著名的“AGC 汇编语言”。如果你对底层开发感兴趣,我强烈建议花时间读一读。不是为了复古,而是为了理解在资源受限时,什么才是真正重要的。这种理解,会让你在现代开发中做出更清醒的决策。