1. 从代号到工程现场:Apollo-11 到底在做什么
第一次看到“Apollo-11”这个代号,很多人脑子里蹦出来的可能是那个著名的载人航天任务。但在我们这行——尤其是做嵌入式、仿真系统、历史计算设备复刻的圈子里,Apollo-11 指的是另一件事:一套围绕阿波罗制导计算机(AGC)展开的完整软件与硬件复现工程。它的核心是一份叫Comanche055的汇编源码,加上一套能在现代机器上跑起来的AGC 模拟器,再配合DSKY 显示键盘单元的交互逻辑,最终让你能在自己的电脑上,用当年宇航员的方式,输入动词和名词,把一艘虚拟的登月舱从月球轨道上“飞”下去。
说白了,Apollo-11 这个项目解决的是一个很具体的问题:如何让五十多年前的制导计算机代码,在今天继续运行、继续被理解、继续被验证。它适合谁呢?如果你是对嵌入式汇编、实时系统、历史计算、仿真验证感兴趣的人,或者你单纯想搞明白“当年那台只有 72KB 内存的机器是怎么把人送上月球的”,那这个项目就是为你准备的。它不要求你懂航天工程,但要求你有耐心去读汇编、去理解中断优先级、去接受一个没有浮点运算单元的 CPU 是怎么做导航计算的。
我最初接触这个项目,是因为一个做飞行仿真的小项目需要参考早期数字计算机的调度逻辑。当时试过直接读扫描版的手册,效率极低,后来发现 Apollo-11 这个复现工程已经把源码、模拟器、测试用例整理得相当完整,才算是找到了入口。这篇文章就把我从环境搭建到实际跑通登月流程的完整过程拆开来讲,顺带把中间踩过的坑和那些“文档里不会写”的细节一并交代清楚。
2. 核心架构拆解:AGC、DSKY 与模拟器三层结构
2.1 AGC 为什么是整套系统的灵魂
AGC 全称 Apollo Guidance Computer,是当年麻省某实验室为阿波罗计划研制的数字计算机。它的核心参数放到今天看简直“寒酸”:16 位字长、约 72KB 的磁芯存储器、主频 2.048 MHz、没有浮点运算单元、没有乘法指令。但就是这台机器,承担了制导、导航、控制三大任务,而且是在硬实时约束下完成的。
Apollo-11 项目复现的重点,就是这台机器的指令集架构和执行时序。AGC 的指令集非常精简,核心就是那几十条基本指令:加载、存储、加法、转移、以及一堆“扩展指令”。没有乘法,所有乘法都靠移位加来实现;没有除法,除法靠移位减。这意味着你在读源码的时候,会看到大量XCH、TS、AD、CCS这类操作,它们组合起来完成一个看似简单的数学运算。
我个人的理解是,AGC 的设计哲学就是“用时间换空间,用软件换硬件”。硬件能省则省,把复杂度全部压到软件层。这也解释了为什么 AGC 的源码里会有那么多看起来“绕”的写法——不是程序员不想写直白,而是硬件资源逼着你必须精打细算。
2.2 DSKY 不只是输入输出,它是人机接口的原始形态
DSKY 全称 Display and Keyboard,是宇航员和 AGC 交互的唯一界面。它上面有两排指示灯、几行数字显示、一个键盘,键盘上只有数字 0-9 和几个功能键:VERB、NOUN、ENTER、CLR、PRO、KEY REL等。
Apollo-11 项目里的 DSKY 模拟器,完整复现了这套交互逻辑。你输入一个动词(Verb)和一个名词(Noun),比如V06N63,意思就是“显示当前姿态角”。AGC 收到这个指令后,会把对应的数据送到 DSKY 的寄存器里,然后显示出来。
这里有个很容易被忽略的细节:DSKY 的按键不是即时响应的。AGC 有一个叫KEYRUPT的中断,按键会触发这个中断,然后由中断服务程序去读取键码。如果你在模拟器里按得太快,或者同时按多个键,行为可能和真实硬件不一致。我在早期测试的时候就遇到过“按键丢失”的问题,后来才发现是模拟器的中断调度周期设置得太粗,调整之后才恢复正常。
2.3 模拟器层:让 1969 年的代码跑在 2024 年的 CPU 上
Apollo-11 项目提供的模拟器,本质上是一个指令级仿真器。它逐条读取 AGC 的机器码,模拟每条指令的执行效果,同时维护一个虚拟的存储器空间和寄存器组。模拟器还需要处理定时器中断和IO 中断,因为 AGC 的很多行为是时间驱动的。
模拟器的核心循环大致是这样的:
while (running) { // 检查是否有中断待处理 if (interrupt_pending) { handle_interrupt(); } // 取指 instruction = memory[pc]; // 译码并执行 execute(instruction); // 更新计时器 update_timer(); }看起来简单,但难点在于时序精度。AGC 的指令执行时间是固定的,比如一条AD指令需要 1 个机器周期,而MP扩展指令可能需要几十个周期。模拟器必须准确模拟这些周期数,否则 DSKY 的刷新率、中断的响应时间都会出问题。我在调试的时候就发现,如果模拟器的周期计数不准,登月舱的下降速度会明显偏离预期,因为制导算法依赖精确的时间步长。
3. 环境搭建与源码获取:从零开始跑通 Apollo-11
3.1 工具链选择:为什么我最终选了 C 语言版本
Apollo-11 项目在社区里有多个语言的移植版本,有 C 的、有 Python 的、还有 JavaScript 的。我一开始图省事,用的是 Python 版本,结果跑起来发现性能瓶颈很明显。AGC 的模拟需要每秒执行几十万条指令,Python 的解释器开销太大,导致 DSKY 刷新卡顿,中断响应也不及时。
后来换到 C 版本,编译之后直接跑,流畅度完全不一样。C 版本的优势在于贴近硬件、执行效率高、内存占用小,而且方便和底层调试工具配合。如果你只是想快速体验一下,Python 版本也能用;但如果你想认真研究 AGC 的行为,或者做二次开发,我建议直接上 C 版本。
编译 C 版本需要的基础环境:
- GCC 或 Clang 编译器
- Make 构建工具
- 一个终端模拟器(用于显示 DSKY)
- 可选:SDL 库(用于图形化 DSKY 显示)
在 Linux 或 macOS 上,这些工具基本都自带或者一条包管理命令就能搞定。Windows 用户建议用 WSL,因为原生 Windows 的终端对 ANSI 转义序列支持不太一致,DSKY 的显示可能会乱。
3.2 源码目录结构:先搞清楚每个文件夹在干什么
拿到源码之后,别急着编译。先花十分钟把目录结构看一遍,能省掉后面很多困惑。典型的 Apollo-11 源码目录是这样的:
Apollo-11/ ├── Comanche055/ # 指令舱源码 ├── Luminary099/ # 登月舱源码 ├── yaAGC/ # AGC 模拟器 ├── yaDSKY2/ # DSKY 模拟器 ├── yaYUL/ # 汇编器 └── Validation/ # 测试用例和验证脚本Comanche055和Luminary099是两个核心源码目录,分别对应指令舱和登月舱的软件。yaAGC是模拟器主体,yaDSKY2是 DSKY 的图形界面,yaYUL是汇编器——对,你没看错,要跑源码,得先把它汇编成机器码。
这里有个坑:yaYUL这个汇编器本身也是用 C 写的,编译它的时候可能会遇到一些老式 C 代码的兼容性问题。我在 GCC 10 上编译的时候就报了几个警告,虽然不影响生成可执行文件,但如果你用的是更严格的编译器,可能需要手动加一些-Wno-参数。
3.3 编译与链接:一步步把模拟器跑起来
编译流程大致分三步:
编译 yaYUL 汇编器
cd yaYUL make这一步会生成
yaYUL可执行文件。汇编 AGC 源码
./yaYUL Comanche055.agc > Comanche055.bin这一步把汇编源码转换成二进制机器码。注意,
Comanche055.agc是主文件,但它会引用很多其他文件,所以实际命令可能需要指定包含路径。编译 yaAGC 模拟器
cd ../yaAGC make生成
yaAGC可执行文件。启动模拟器并加载二进制
./yaAGC --core=Comanche055.bin
如果一切顺利,你会看到模拟器开始输出一些调试信息,然后等待 DSKY 连接。这时候再启动yaDSKY2,它就会通过 socket 和yaAGC通信,显示出 DSKY 的界面。
注意:
yaAGC和yaDSKY2之间的通信默认走本地回环地址的某个端口。如果你在容器里跑,或者有防火墙规则,可能会连不上。我建议先用netstat确认端口监听状态,再排查连接问题。
4. 实操全流程:从 DSKY 输入到登月舱下降
4.1 启动流程与初始状态检查
模拟器跑起来之后,DSKY 上会显示一些初始状态。这时候你需要先确认几件事:
- PROG 指示灯是否亮起,表示程序正在运行
- VERB和NOUN显示是否正常
- RESTART指示灯是否熄灭(如果亮着,说明模拟器可能没正确加载程序)
我遇到过一种情况:模拟器启动了,但 DSKY 上什么都不显示。排查了半天,发现是yaDSKY2的配置文件里指定的端口和yaAGC实际监听的端口不一致。改过来之后立刻正常。所以如果你也遇到类似问题,先检查端口配置。
4.2 输入动词名词:和 AGC 对话的正确姿势
AGC 的交互逻辑是“动词 + 名词”的组合。动词表示“做什么”,名词表示“对什么做”。比如:
V06N63:显示当前姿态角V37N00:启动 P37 程序(登月舱下降)V05N09:显示报警码
输入方式是按VERB键,然后输入两位数字,再按NOUN键,输入两位数字,最后按ENTER。DSKY 上会分两行显示,上面是动词,下面是名词。
这里有个细节:AGC 对输入顺序有严格要求。如果你先按了NOUN再按VERB,它会认为你输入的是另一个指令。我一开始不熟悉的时候,经常按错顺序,结果 AGC 直接报了个OPR ERR(操作错误)。后来养成习惯,每次输入前先默念“先动词后名词”,就很少出错了。
4.3 加载 P37 程序:登月舱下降的关键步骤
P37 是登月舱下降程序的核心。要启动它,需要输入V37N00,然后按ENTER。AGC 会开始执行一系列初始化操作,包括:
- 检查登月舱的当前状态
- 加载下降制导参数
- 启动雷达高度计模拟
- 进入制导循环
这个过程在真实任务中需要几分钟,在模拟器里取决于你的 CPU 性能。我在一台老笔记本上跑的时候,大概等了十几秒才看到 DSKY 上的状态变化。如果你觉得太慢,可以调高模拟器的执行速度,但要注意速度调得太高可能会导致中断丢失,反而让程序行为异常。
4.4 监控下降过程:关键参数与报警处理
P37 启动之后,DSKY 会定期刷新一些关键参数。你需要关注的主要有:
| 参数 | 含义 | 正常范围 |
|---|---|---|
| 高度 | 登月舱距月面的高度 | 从约 15km 递减到 0 |
| 下降速率 | 垂直速度 | 初始约 -30m/s,最终接近 0 |
| 水平速度 | 横向速度 | 应逐渐减小到接近 0 |
| 剩余燃料 | 推进剂余量 | 应足够完成下降 |
如果某个参数超出范围,AGC 会触发报警。常见的报警码有1201(执行溢出)、1202(程序报警)、1203(键盘报警)。其中1202是最著名的——当年阿波罗 11 号实际任务中就触发过这个报警,差点导致任务中止。
在模拟器里触发1202通常是因为中断负载过高。AGC 的调度器在检测到中断处理不过来的时候,会主动丢弃一些低优先级任务,并触发报警。如果你在模拟器里看到1202,可以尝试降低模拟速度,或者检查是否有其他进程占用了太多 CPU。
5. 常见问题与排查技巧实录
5.1 模拟器启动失败:从日志里找线索
模拟器启动失败是最常见的问题,表现可能是闪退、卡死、或者输出一堆看不懂的错误信息。我的排查顺序是这样的:
- 看日志:
yaAGC默认会输出一些调试信息,如果启动失败,通常会打印错误原因。比如“无法加载 core 文件”或者“端口已被占用”。 - 检查文件路径:
--core参数指定的二进制文件路径是否正确?文件是否存在?权限是否足够? - 检查端口占用:
yaAGC默认监听某个端口,如果这个端口已经被其他程序占用,启动会失败。用lsof -i :端口号可以查看占用情况。 - 检查依赖库:某些版本的
yaDSKY2依赖 SDL 库,如果系统里没装,启动时会报错。
我印象最深的一次是模拟器启动后立刻退出,日志里只写了一句“segmentation fault”。后来用gdb跟了一下,发现是yaYUL生成的二进制文件格式不对——原因是汇编的时候漏了一个包含文件,导致某些符号没有解析。重新汇编之后问题就解决了。
5.2 DSKY 显示异常:字符乱码与刷新问题
DSKY 显示异常通常有两种表现:字符乱码和刷新不同步。
字符乱码多半是终端编码问题。DSKY 模拟器输出的是 ANSI 转义序列,如果你的终端不支持某些序列,或者编码设置不对,就会显示成乱码。解决办法是确保终端使用 UTF-8 编码,并且支持 ANSI 颜色和光标控制。
刷新不同步则可能是模拟器和 DSKY 之间的通信延迟导致的。yaDSKY2默认会定期向yaAGC请求状态更新,如果这个间隔设置得太长,显示就会滞后。可以在配置文件里调小这个间隔,但注意不要调得太小,否则会增加通信开销。
5.3 程序行为异常:中断优先级与调度问题
AGC 的中断系统是优先级驱动的。高优先级中断可以打断低优先级中断的处理。如果模拟器对中断优先级的模拟不准确,程序行为就会异常。
我遇到过一次:登月舱下降过程中,高度数据突然跳变。排查后发现是模拟器在处理某个 IO 中断时,没有正确保存和恢复寄存器状态,导致制导程序读到了错误的数据。这个问题在模拟器的后续版本里被修复了,但如果你用的是老版本,可能需要手动打补丁。
提示:如果你在做二次开发,建议先跑一遍
Validation目录下的测试用例。这些用例覆盖了大部分核心功能,能帮你快速定位问题。
5.4 性能调优:让模拟器跑得更流畅
模拟器的性能主要受两个因素影响:指令执行速度和中断处理效率。
指令执行速度取决于你的 CPU 和编译优化选项。用-O2或-O3编译yaAGC,性能会有明显提升。如果还是不够快,可以尝试关闭一些调试输出,减少 IO 开销。
中断处理效率则和模拟器的调度策略有关。某些版本的yaAGC使用轮询方式检查中断,效率较低。如果你有精力,可以改成事件驱动的方式,中断到来时直接触发处理函数,而不是等下一个轮询周期。
6. 从复现到理解:这个项目真正有价值的地方
跑通 Apollo-11 的模拟器,看到 DSKY 上显示出高度和速度数据,只是第一步。真正有意思的是读源码。AGC 的汇编代码里藏着很多当年工程师的巧思,比如:
- 用自修改代码来节省存储空间
- 用固定点运算替代浮点运算
- 用优先级调度保证关键任务不被阻塞
这些技术在今天看来可能有些过时,但背后的设计思想——在极端资源约束下做实时控制——依然适用于很多嵌入式场景。我自己在做无人机飞控的时候,就借鉴了 AGC 的中断调度思路,把关键控制循环放在最高优先级,非关键任务放在低优先级,效果很好。
另外,Apollo-11 项目还提供了一个很好的验证框架。你可以修改源码,重新汇编,然后跑测试用例,看看改动是否影响了程序行为。这种“改-编-测”的闭环,对于学习实时系统的人来说,是非常宝贵的实践机会。
最后分享一个小技巧:如果你觉得直接读汇编太吃力,可以先从yaYUL生成的列表文件入手。列表文件里会把每条汇编指令和对应的机器码并列显示,对照着看,理解起来会快很多。我在最初读源码的时候,就是靠列表文件一点点啃下来的。