我最近在开源库里翻到一个很特别的项目,名字就叫作Apollo-11。第一眼看到它时,我以为哪个团队重新起了个响亮的名字,结果细看才发现,它是人类首次登月任务里那台制导计算机(简称 AGC)的软件镜像、仿真器以及大量说明文档的整理总和。说得直白一点,你完全可以在自己的电脑上,把五十年前那台飞向月球的飞行导航程序跑起来,通过一个模拟的 DSKY 键盘显示面板观察它的状态,甚至手动切换飞行阶段。对嵌入式开发者、航天迷和复古计算爱好者来说,这几乎是一个没有任何门槛的“活体档案”。这篇文章我会讲清楚它到底是什么、背后的硬件是怎么设计的、代码是如何调度任务的,再把我实际运行仿真环境时踩过的坑和一些心得一并写出来。
1. 项目到底还原了什么
1.1 一份可运行的登月软件快照
这个标题下的核心内容,我从一开始就理解为一套“完整快照”而不是“模拟演示”。它保存了阿波罗制导计算机的原始机器指令、汇编源码、反汇编注释以及配套仿真工具链,全部被打包成一个可在现代系统上加载运行的集合。尤其重要的是,它保留了两个关键软件包:一类是系统常驻的执行管理模块,另一类是登月各个阶段的应用程序,比如自动着陆阶段会用到的 P63、P64 等程序。
我特意强调“快照”两个字,是因为它并不是后人按照手册重新写出来的“精神复刻”,而是从历史资料中重新转录出的机器码与源码。转录之后再配以反汇编与注释,让它能被现代汇编器加载、可被仿真器执行。这样的好处很明显:我们看到的是一条不会被过度润色的历史代码线。每一条指令在这个仓库里长什么样,当年在月球轨道上运行时就是什么样。
1.2 它适合谁,解决什么问题
我用它做了不少实验,也推荐给身边几类朋友,反馈都很不一样。
对于刚入门嵌入式实时系统的开发者来说,这是一个极好的裸机实时任务调度教材。它没有使用现成的 RTOS,而是在有限的指令集上自己完成优先级调度、中断响应、看门狗式异常监控,现代嵌入式工程里常见的套路几乎都能在源码里找到雏形。对航天历史感兴趣但并不写代码的人,可以从这套镜像里体会到“资源限制如何倒逼代码设计”这一课。AGC 的内存小得惊人,程序必须极度讲究,这和今天我们写代码时的“大手大脚”形成强烈反差。
我本人更看重第三类场景:做飞行模拟或复古计算项目的人。很多仿制阿波罗控制面板的开源硬件,在交互逻辑上做不到完全还原,就是因为缺少这套真实软件资源。现在有了它,我们可以知道某一次按键后,机器内部到底执行了怎样的 Verb/Noun 请求,而不只是照着照片做一个会亮的灯。
1.3 这个项目的边界和局限
需要指出的是,标题虽是 Apollo-11,但仓库核心聚焦的是制导计算机本体和显示键盘,也就是 AGC 与 DSKY 这一套“人机交互闭环”。它不包含推进系统仿真,不模拟发动机动力学与轨道力学。你在运行 P63 自动着陆程序时看到的是计算机内部状态变化,而不是画面里火箭在减速下降。如果想得到完整的三维视景,还需要另外挂接图形引擎和动力学模型。
我最初误以为它是一个“模拟整个人类登月”项目,后来才把它定位清楚。这个定位反而更纯粹,因为我们可以把注意力全部放在计算机本身:“这套软件是如何在 2K 内存限定下,完成实时飞行控制和导航计算的?”
2. 五十年前的硬件底子,比想象中更硬核
2.1 内存数量:4KB 里的 4096 种可能
看这套代码前,先看硬件指标,否则你会很难理解程序为什么写得如此“拧巴”。AGC 的数据存储器(擦除内存)大约有 2048 个 16 位字,也就是约 4KB 容量的 RAM;固定存储器大约有 36000 个 16 位字的只读存储区,负责保存主要飞行程序。这里一个词是 15 位数据位加 1 位校验位。换算成今天熟悉的单位,它连一张微信头像大小都装不下,却要承载登月任务的导航、制导和显示控制。
固定存储器有个非常有趣的名字,叫“核心绳存储器”。它的制作方式很像传统工艺:手工把导线穿过一个一个磁环,导线穿过就表示 1,不穿过就表示 0,然后把一堆磁环像编绳子一样固化起来。程序里每一位都是“穿线师傅”手工完成的,无法修改。如果发现代码写错,理论上的补救方式接近于重新“穿一段绳子”。这种物理实现方式让“只读”这个现代概念变得极其可信,也无比沉重。
2.2 速度:一秒八万条指令够用吗
再看频率,AGC 的时钟只有 1.024MHz,每条指令需要 11.7 微秒到 23.4 微秒不等,整体算力约为每秒 85000 条指令。这个数字放在今天,连一颗普通微控制器都不如,但在六十年代它就是能够完成闭环制导计算的可靠工具。
判断它够不够用,不能用今天的算力思维,而要看实时任务的“死线”。控制飞行器姿态和推力的算法,以几十毫秒为操作节拍;每秒 85000 条指令听起来不多,却足以以 20 毫秒左右的周期更新一次完整的导航滤波与姿态计算。换句话说,这台计算机不是靠蛮力取胜,而是靠精密的任务调度把每一个时钟周期都安排得明明白白。就像一台手摇计算器,只要每个齿轮都咬合到位,同样可以把人送到目的地。
2.3 没有操作系统,怎么管理任务
在 AGC 里,我们找不到现代意义上的操作系统。它没有虚拟内存、没有文件系统、没有动态内存分配、也没有特权级保护。它能用的只有两级任务调度结构:执行器(Executive)和等待列表(Wait List)。
执行器负责从可运行任务中挑出最高优先级的任务去执行;等待列表则用来登记那些需要在精确时刻触发的短小任务,比如每两秒更新一次显示数据、定时读取加速度计数据等。这种“极简实时内核”完全用手写汇编实现,却做到了在现代系统上都不容易做到的精确时序控制。宇航员通过 DSKY 面板上的数字键盘,以“动词/名词”组合的方式发起请求——动词代表要执行的动作,比如“计算”“显示”“更新”,名词代表作用的对象,比如“加速度”“速度差”“雷达高度”。命令组合很小,但学习成本低,适合在狭小座舱内操作。
3. 软件如何安排一次登月
3.1 从 P01 到 P66:飞行阶段的“程序号”
在城市生活里,我们启动 App 靠图标点击;在 AGC 上,飞行员通过键入一个三位数字来切换应用,这些数字被称作“程序编号”。P01 是待机,P02 是姿态粗对准,P40/P41 负责中途动力飞行,P63 是自动着陆减速,P64/P66 则完成最终下降与悬停判断。每个编号背后都绑定了一组特定的计算任务和显示模式。
实际操作中,宇航员会在合适的时间窗内输入类似 P63 这样的指令,AGC 便开始接管着陆。它会比较雷达高度、垂直速度和加速度数据,并通过数字自动驾驶仪生成推力指令。值得注意的是,这一过程并不是一个孤立程序跑到底,而是多个任务在中断与等待列表之间切换,像是是一条流动的生产线。
3.2 解释器模式:一种面向硬件的“字节码”
A GC 的机器指令是定长的,适合做简单的加减、寄存器和内存操作,但飞行导航经常需要处理浮点、矢量、三角函数。如果每一个函数都展开成底层汇编,代码体积会迅速击穿 36K 字的只读存储上限。
我跟着源码里的说明才看明白,阿波罗软件使用了一套类似现代“解释器”的机制:飞行程序由若干伪指令组成,伪指令实际运行时由一段解释代码转换成多条机器指令。开发者以一种比汇编更接近数学表达的高级伪语言编写,再由解释器在飞行过程中逐步“翻译”执行。这其实就是现代语言虚拟机的一种原始雏形。我们今天看到 Java 字节码、Lua 字节码,追根溯源可以在这里找到相似的工程思路。
3.3 源码里藏着一代工程师的脾气
翻这套代码最大的乐趣,是会在注释里碰到一些很“不安分”的文字。比如在动力飞行阶段,老工程师直接留下了一句像是点火口号的注释。这类注释让冷冰冰的编号瞬间活了过来,你能感觉到写出这些代码的人不是机械的“打字员”,而是一群清楚自己在参与历史事件的普通人。
我看这些注释时常常会停下来想:在那台内存小得可怜、每一条指令都要反复抠字眼的机器上,他们依旧留出了表达情绪的空间。这对今天那些“注释里只写时间和作者”的工程项目来说,算是一种挺有趣的提醒。
4. 本地复现:从源码到一台会“闪灯”的 DSKY
4.1 准备工具链
要在现代电脑上跑这套软件,我建议先准备一个偏向命令行环境的系统。无论是 Linux 还是 macOS,关键是要安装编译工具(如 gcc、make)以及若干开发依赖库。这里有一个很容易被忽略的点:开源社区给的构建说明通常假设你本机已有较新的构建链,如果你用的是比较精简的发行版,可能还需要手动装上一些图形库依赖。
我第一次尝试时,因为缺了一个比较冷门的开发库,编译过程在最后链接阶段才报错。报错信息并不会说清楚“你缺的是哪个依赖包”,只会甩出一行找不到文件的提示。后来我总结出一个省时间的方法:先按文档列出的依赖清单全装一遍,再开始编译,而不是等报错再一个个找。
4.2 编译与启动
整个编译过程并不漫长,核心工具是几个独立的仿真程序:一个负责 AGC 本体执行,另一个负责 DSKY 显示界面。编译完成后,需要把从仓库里下载好的“登月版本软件镜像”放到仿真器指定的目录中。镜像文件相当于一套预先穿好线的“核心绳存储器”内容,仿真器会把它读取为只读存储。
启动时要注意电源开关的模拟顺序。它不像现代设备,打开电源后直接进入系统;这台上世纪的电脑有一套启动流程和校验逻辑。如果直接加载镜像却忘了按正确的上电步骤,面板可能一直停在初始状态,看起来像“死机”,其实是还没完成初始化。
4.3 通过模拟键盘发出指令
仿真界面里的 DSKY 面板保留了当年的实体按键布局:数字键盘、动词键、名词键以及功能键。操作逻辑很简单:先输入 Verb 编号,再输入 Noun 编号,程序便会执行相应动作。
我在实践中的建议是,一开始不要急着输入复杂的 P63 指令。先做几个最简单的显示操作,比如请求核心内存里的某些状态值,确认屏幕能正确刷新,再逐步切换到飞行程序。这样如果后面出现异常,你至少能判断问题出在操作流程还是仿真环境本身。
4.4 观察程序进程与警报输出
仿真器的终端会打印飞行日志,包括任务调度情况、警报发生时间点和中断触发原因。这是调试的“金矿”。我在第一次进入 P63 模拟时,终端会打印出一长串与雷达数据采样相关的记录。能亲眼看到五十年前“软件报警”在这些记录里真实重现,那种感觉和看历史资料完全不一样。
5. 从 1202 警报里看实时系统的降级设计
5.1 警报到底是出错,还是自我保护
谈到阿波罗制导计算机,一个绕不开的经典话题就是登月过程中多次出现的 1201、1202 警报。很多人第一次听说时会以为机器快要宕机了,实际上这是系统的一种自我保护行为。
简单来说,1202 警报表示“实时任务超时控制在可接受范围,但值得提醒”。当系统检测到有任务被延迟、等待列表溢出或某些低优先级任务无法及时完成时,它会主动向宇航员报告。它没有直接让系统崩溃,而是把现场状态隔离下来,转移到一个紧急监视流程中,让宇航员和地面控制中心来判断风险。
5.2 为什么警报之后仍能继续任务
从代码逻辑上看,这种警报触发后会保存关键路径上的数据,暂停一部分非紧要任务,同时维持导航与控制计算继续运行。也就是说,它更像是一个“老练的主厨发现配菜切不过来了,但灶台上的主菜仍在正常烹饪”。警报提醒归提醒,核心锅铲没有停下。
这种工程哲学跟我们现代嵌入式开发里的“故障降级”非常相似。我们在设计高可靠性产品时,常说要保证故障安全;AGC 给的回答是复杂一点:故障时不追求全系统安全停机,而是保证当前最关键的控制动作不中断,只在必要时提醒人类接管。对做飞行器控制、医疗设备或其他高实时性应用的开发者来说,这套思想非常值得反复咀嚼。
5.3 把警报设计带回到现代代码
我在这套源码里学到的一个实用方法是:不要把“错误处理”设计成“要么成功、要么失败”。在 AGC 这类场景里,系统必须区分“灾难性失败”和“可延后问题”。用现代的术语说,它就是在设计里预留了背压信号。当系统过载时,它给操作员提供一个可读的提示,而不是直接丢掉所有数据。
我们在自己的项目里同样可以借鉴:给任务队列设置一个“拥挤度阈值”,超过阈值后主动丢尽一些低优先级更新,而不是死死维护一个越来越慢的队列。这比等系统崩溃后再来定位问题要优雅得多。
6. 常见问题与排查实录
6.1 编译时遇到“找不到库”怎么办
这类报错几乎人人都会遇到。常见的原因是构建脚本默认链接某些库,但本机没有安装开发版本。我碰到过缺readline和ncurses的情况,装好之后问题立刻解决。另一个更隐蔽的问题是与 32 位兼容库相关,如果构建工具链默认生成 64 位程序,而某个依赖库是 32 位,链接阶段也会报错。解决办法是确认仿真器版本是否支持 64 位构建,必要时安装 multilib 支持。
6.2 启动后图形界面黑屏
黑屏多半不是程序崩溃,而是图形显示界面依赖的窗口系统没有正确初始化。有的仿真器可以选择字符模式或图形模式。我先在图形模式下运行,遇到过面板窗口不出来但进程还在跑的奇怪状态,后来切到字符模式才顺利看到输出。如果你的目标是研究软件逻辑,字符模式反而更省心,日志也更集中。
6.3 镜像文件损坏导致非法指令
有时候你加载完全一样的数据,却看到一堆“非法指令”或“未知操作码”的提示。先别怀疑 CPU 模拟器写错了,绝大多数情况是镜像文件被传输或解压损坏。源码包里一般会给出校验信息,方法很朴实:重新下载文件,用工具校验比对。文件一但正确,那些离谱报错就会马上消失。
6.4 怎么把参考资料变成实际生产力
我强烈建议把 AGC 的说明书、指令集手册,以及程序编号注释说明这三份资料放在手边。仿真器只能让你看到现象,要真正明白“为什么这一步要输入 Verb 37”这类问题,必须回到原始文档里去理解交互协议。源码注释其实给了很好的地图,但地图和实景对照时仍然需要官方说明来兜底。
我个人的一点体会
我在本地把整套环境跑通之后,最大的感受不是“五十年前好厉害”,而是一种很现实的技术教训:限制越硬,设计反而越清醒。在内存只有 4KB 的环境里,没有空间给含糊的逻辑;每一个任务能不能被调度、中断会不会丢失,都是可以直接数出来的。这种确定性的美,在现代庞大的抽象层级里反而变成稀缺品。
如果你也想试一下,我最后分享三条最实用的心得。第一,不要跳过电源启动顺序,先确认面板稳定再输入指令,否则你会误判为故障。第二,尽量使用日志模式运行仿真器,它记录的内容比你在界面里看到的多得多,很多“怪异现象”只要对照日志就会立刻清晰。第三,看不懂某段程序逻辑时,先去看它对应的飞行阶段说明,再回来读汇编,顺序反了你会一直在指令流里打转。
这套代码不是什么现代工程的标准答案,但它一定是一堂极好的“如何在极限约束下做出可靠系统”的历史课。真金白银的教训,都藏在那一行行没有被时间磨平的指令里。