☰
Apollo-11制导计算机源码解析:从AGC汇编到模拟器实战
2026/10/16 4:36:59 网站建设 项目流程

1. 项目缘起与整体设计思路

1.1 为什么一个“老古董”代码库值得反复折腾

第一次听说 Apollo-11 这个项目,很多人会以为又是哪个航天科普的 PPT 或者纪录片合集。实际上,它是一个把上世纪六十年代阿波罗计划中制导计算机(AGC)的飞行软件源码,完整搬到现代开发环境里跑起来的开源工程。原始代码是用一种叫 AGC Assembly 的汇编语言写的,当年存储这些代码的介质是一种叫“核心绳存储器”的东西——你可以把它想象成一张手工编织的、不可擦写的 ROM,每一根线穿过或绕过磁环就代表一个 0 或 1。这种工艺放到今天,连博物馆都未必能完整展示,但 Apollo-11 这个项目做的事情,就是让这些代码在现代电脑上重新“活”过来。

这个项目能做什么?简单说,它提供了一个完整的模拟器,可以加载阿波罗 11 号任务中指令舱和登月舱两套独立的飞行软件,让你在自己的机器上运行、调试、甚至单步跟踪当年宇航员在关键时刻看到的那些指令。它解决的核心问题是:原始代码虽然公开了,但普通人根本没法运行——没有对应的硬件,没有汇编器,没有链接器,连代码的注释都是几十年前的风格。Apollo-11 项目把这些坑全部填平了,提供了一套可以在 Linux、macOS 甚至 Windows 的 WSL 环境下编译运行的完整工具链。

适合谁来参考?我觉得有三类人最值得花时间研究。第一类是对计算机体系结构、嵌入式系统感兴趣的学生和工程师,因为 AGC 是一台真正的实时嵌入式计算机,它的设计哲学和今天的单片机、RTOS 有太多可以对照的地方。第二类是对软件考古、历史代码修复有兴趣的开发者,这个项目里充满了“在没有现代工具的情况下如何写出可靠代码”的智慧。第三类就是纯粹的好奇者,想看看人类第一次登月时,那台只有 72KB 内存的计算机到底是怎么工作的。

1.2 整体架构:从源码到可运行模拟器的三层结构

Apollo-11 项目的整体设计可以拆成三层来看,每一层都有明确的职责,层与层之间的接口也定义得很清楚。这种分层思路本身就值得学习,因为很多现代项目反而把边界搞得很乱。

第一层是源码层。项目仓库里包含了从原始扫描件转录出来的汇编代码,分为两个主要目录:一个对应指令舱的 Comanche 055 软件,另一个对应登月舱的 Luminary 099 软件。这些代码不是简单的文本复制,而是经过了逐行校对,修正了扫描件中模糊不清的字符,并且保留了原始的注释和页码标记。源码文件的后缀通常是.agc,里面全是类似TCF、CCS、INDEX这样的助记符,第一次看会觉得很懵,但看多了会发现它的指令集其实非常精简。

第二层是工具链层。这一层负责把汇编代码变成模拟器能执行的二进制格式。项目里包含了一个用 Python 写的汇编器,叫yaYUL(Yet Another YUL assembler,YUL 是当年 AGC 汇编语言的一种叫法)。这个汇编器会解析.agc文件,处理符号表、宏定义、条件汇编,最后生成一个.bin文件。同时还有一个链接器,负责把多个目标文件合并成最终的内存映像。工具链里还有一堆辅助脚本,比如用来校验校验和的、用来生成符号表的、用来把二进制转成十六进制格式的。这些工具大部分是用 Python 2 写的,所以如果你在 Python 3 环境下跑,需要做一些兼容性处理,这是第一个坑,后面会详细说。

第三层是模拟器层。项目提供了一个用 C 语言写的 AGC 模拟器,核心文件是agc.c和agc.h。这个模拟器实现了 AGC 的指令集、寄存器组、内存模型、中断系统,甚至包括那个著名的“1201 和 1202 报警”——当年登月过程中因为雷达数据过载触发的执行溢出报警。模拟器还支持外设模拟,比如 DSKY(显示键盘单元)、IMU(惯性测量单元)、雷达、发动机指令接口等。你可以通过命令行参数选择加载哪套软件、以什么速度运行、是否开启调试输出。

这三层之间的数据流是这样的:源码层经过工具链层编译链接,生成一个二进制内存映像;模拟器层加载这个映像,然后按照 AGC 的时钟周期逐条执行指令。如果你想修改代码,只需要改.agc文件,重新跑一遍工具链,再启动模拟器就行。整个流程和现代嵌入式开发非常像,只不过工具链是几十年前风格的。

1.3 方案选型背后的考量:为什么不用现成的模拟器

你可能会问,既然已经有其他 AGC 模拟器了,为什么还要折腾这个项目?我实际用下来,觉得 Apollo-11 项目有几个不可替代的优势。

首先是代码的完整性。这个项目里的源码是从原始扫描件逐行转录的,而且经过了多轮校对。有些其他模拟器只包含部分代码,或者用的是反汇编出来的版本,注释和符号名都丢失了。Apollo-11 项目保留了原始的符号名,比如P63、P64、R02这些,你在调试的时候能直接对应到任务手册里的描述,这对理解代码逻辑帮助极大。

其次是工具链的自包含性。很多模拟器要求你自己去搞汇编器,或者依赖某个特定版本的商业工具。Apollo-11 项目把汇编器、链接器、模拟器全部打包在一起,你只需要一个 Python 环境和 C 编译器就能跑起来。这种“开箱即用”的设计对于想快速上手的人来说非常友好。

第三是社区维护的活跃度。这个项目在代码托管平台上有专门的仓库,虽然核心代码已经稳定,但社区还在不断提交修正和改进。比如有人发现了原始扫描件中的一处笔误,提交了修正;有人为 Python 3 做了兼容性补丁;还有人增加了对更多外设的模拟支持。这种持续的维护意味着你遇到问题时,大概率能找到解决方案或者已经修复的版本。

当然,这个方案也不是没有缺点。最大的问题是性能。用 Python 写的汇编器在处理大型源码文件时速度较慢,完整编译一次可能需要几十秒到几分钟,取决于你的机器性能。另外,模拟器本身是单线程的,如果你把时钟频率设得太高,可能会占用大量 CPU 资源。不过对于学习和调试来说,这些都不是大问题,毕竟你不需要用它来跑实时任务。

2. 核心细节解析与实操要点

2.1 源码结构:两套软件、四个模块、一个核心

Apollo-11 项目的源码结构乍看很乱,但理清之后会发现它的组织方式非常清晰。整个代码库分为两大块:指令舱软件和登月舱软件。每一块又分为四个主要模块:执行程序、中断处理、任务调度、外设驱动。

执行程序是核心中的核心,它负责解释和执行来自 DSKY 的指令,管理导航、制导、控制等主要功能。在指令舱软件里,这个模块叫Comanche;在登月舱软件里,叫Luminary。这两个名字都是当年项目的内部代号,和功能没有直接关系,但社区里一直沿用下来。

中断处理模块负责响应各种硬件中断,比如雷达数据就绪、键盘输入、定时器到期等。AGC 的中断系统设计得很有意思,它没有现代处理器的中断向量表,而是用一组固定的入口地址,每个入口对应一个中断类型。中断处理程序必须非常短小,因为 AGC 的内存和寄存器资源极其有限。

任务调度模块是 AGC 的“操作系统”,它用一个叫Executive的程序来管理多个任务的执行。每个任务有一个优先级,调度器会根据优先级和当前状态决定下一个执行哪个任务。这种设计在当年是非常先进的,可以看作现代 RTOS 的雏形。

外设驱动模块负责和 DSKY、IMU、雷达等设备通信。这些驱动代码里充满了对硬件寄存器的直接操作,读起来很像今天的单片机裸机编程。比如 DSKY 的显示刷新,就是通过向特定的内存地址写入数据来实现的。

注意:源码文件里的注释是理解代码的关键。很多注释是当年程序员写的,风格非常简洁,甚至有些缩写需要查资料才能明白。建议配合任务手册一起看,手册里会解释每个程序的输入输出和预期行为。

2.2 工具链的安装与配置:Python 2 到 Python 3 的迁移

工具链的安装是第一个容易卡住的地方。项目原始代码是基于 Python 2 的,而现代系统默认都是 Python 3。如果你直接运行python yaYUL.py,大概率会报语法错误。我试过几种解决方案,最后发现最稳妥的是用 Python 3 的兼容模式,同时手动修改几个关键文件。

首先,你需要确认系统里有 Python 3 和 GCC。在 Ubuntu 或 Debian 上,可以这样检查:

python3 --version gcc --version

如果版本太低,建议先升级。Python 3.6 以上、GCC 7 以上基本没问题。

接下来,克隆项目仓库。假设你已经有了仓库地址,直接git clone到本地。进入目录后,你会看到yaYUL文件夹和Simulator文件夹。yaYUL里是汇编器和链接器,Simulator里是模拟器。

Python 2 到 Python 3 的主要改动点有几个:print语句要加括号,dict.has_key()要改成in,xrange要改成range,字符串和字节串的区分要处理。项目社区已经有人提交了 Python 3 的补丁,但可能不在主分支里。你可以先尝试直接运行,如果报错,根据错误信息逐个修改。我踩过的坑是:yaYUL.py里有一处用了string.uppercase,这个在 Python 3 里已经移除了,需要改成string.ascii_uppercase。还有一处用了map()返回列表,在 Python 3 里返回的是迭代器,需要包一层list()。

修改完 Python 文件后,还需要编译模拟器。进入Simulator目录,直接make就行。如果报错说找不到readline库,需要安装libreadline-dev。编译成功后,会生成一个叫yaAGC的可执行文件。

提示:如果你不想手动改 Python 代码,可以试试用2to3工具自动转换。命令是2to3 -w yaYUL/,它会自动修改目录下的所有 Python 文件。但自动转换不一定完美,转换后还是要手动检查一遍。

2.3 编译与链接:从汇编源码到二进制映像

编译过程分为两步:先用yaYUL把.agc文件汇编成.bin,再用链接器把多个.bin合并成一个完整的内存映像。这个过程和现代编译工具链很像,但参数和文件命名规则需要熟悉一下。

以指令舱软件为例,源码文件在Comanche055目录下。你需要先设置一些环境变量,告诉汇编器去哪里找包含文件。通常的做法是写一个简单的 shell 脚本,把常用命令封装起来。我自己的脚本是这样的:

#!/bin/bash export AGC_ROOT=$(pwd) python3 yaYUL/yaYUL.py --input Comanche055/MAIN.agc --output Comanche055.bin

MAIN.agc是主文件,它会通过#include指令引入其他文件。汇编器会自动处理这些包含关系。如果一切顺利,你会得到一个Comanche055.bin文件,大小大约是 36KB 左右。这个大小对应 AGC 的内存容量:指令舱软件占用大约 36K 个 15 位字,登月舱软件稍微小一点。

链接步骤通常不需要手动做,因为yaYUL已经生成了完整的内存映像。但如果你要修改代码,可能需要重新链接。链接器叫yaLINK,用法和yaYUL类似,但输入是多个.bin文件,输出是一个合并后的.bin。

注意:汇编过程中如果出现“符号未定义”的错误,通常是因为某个包含文件没有被正确引入。检查MAIN.agc里的#include列表,确保所有依赖文件都在。另外,有些符号是硬件定义的,比如寄存器地址,这些在Hardware.agc里定义,必须包含。

2.4 模拟器启动与基本操作:让 DSKY 亮起来

编译成功后,就可以启动模拟器了。最基本的命令是:

./yaAGC --core=Comanche055.bin

这会启动模拟器,加载指令舱软件,然后等待你输入命令。模拟器默认会打开一个命令行界面,你可以输入help查看所有可用命令。最常用的命令是run(开始执行)、step(单步执行)、break(设置断点)、examine(查看内存)、deposit(修改内存)。

如果你想让 DSKY 显示出来,需要额外启动一个 DSKY 模拟器。项目里提供了一个叫yaDSKY的程序,它用图形界面模拟了 DSKY 的显示和键盘。启动方式是:

./yaDSKY --cfg=Comanche055.cfg

这个程序会读取配置文件,建立和yaAGC的通信。配置文件里定义了 DSKY 的端口号和通信协议。默认情况下,yaAGC监听 19697 端口,yaDSKY连接这个端口。如果端口被占用,可以在配置文件里修改。

启动后,你会看到一个类似老式计算器的界面,上面有两行数字显示(分别是 PROG、VERB、NOUN 和 R1、R2、R3),下面有一排按键。你可以按VERB和NOUN键输入指令,比如VERB 06 NOUN 62是显示当前时间。如果模拟器运行正常,你会看到数字开始跳动,这说明 AGC 软件已经在执行了。

提示:第一次启动时,DSKY 可能显示全零或者乱码,这通常是正常的,因为软件需要一段时间初始化。等几秒钟,如果数字开始变化,就说明一切正常。如果一直没反应,检查yaAGC的输出,看看有没有报错信息。

3. 实操过程与核心环节实现

3.1 完整编译流程:从零到可运行模拟器

我把自己从零开始编译 Apollo-11 的完整过程记录了一遍,包括遇到的错误和解决方法。整个过程大约需要 30 分钟到 1 小时,取决于你对命令行的熟悉程度。

第一步,准备环境。我用的是一台 Ubuntu 20.04 的虚拟机,配置是 2 核 CPU、4GB 内存。这个配置足够跑模拟器了。先安装必要的依赖:

sudo apt update sudo apt install git build-essential python3 python3-pip libreadline-dev

第二步,获取源码。假设你已经有了仓库地址,直接克隆:

git clone <repository-url> apollo-11 cd apollo-11

第三步,修复 Python 3 兼容性。我写了一个小脚本,自动替换常见的 Python 2 语法:

find yaYUL -name "*.py" -exec sed -i 's/print \(.*\)/print(\1)/g' {} \; find yaYUL -name "*.py" -exec sed -i 's/xrange/range/g' {} \; find yaYUL -name "*.py" -exec sed -i 's/string.uppercase/string.ascii_uppercase/g' {} \;

这个脚本只是处理了最常见的几个问题,实际运行时可能还会遇到其他错误,需要根据报错信息手动修改。

第四步,编译模拟器。进入Simulator目录,运行make:

cd Simulator make

如果一切顺利,会生成yaAGC和yaDSKY两个可执行文件。如果报错,检查错误信息,通常是缺少某个头文件或库。我遇到过一次readline.h找不到的问题,安装libreadline-dev后解决。

第五步,编译 AGC 软件。回到项目根目录,运行汇编器:

python3 yaYUL/yaYUL.py --input Comanche055/MAIN.agc --output Comanche055.bin

这一步可能会花几十秒,因为汇编器要处理大量文件。如果报错,仔细看错误信息,通常是某个符号未定义或者语法错误。我遇到过一个错误是Invalid opcode: TCF,后来发现是因为某个包含文件没有被正确引入,导致汇编器不认识TCF指令。

第六步,启动模拟器。先启动yaAGC:

./Simulator/yaAGC --core=Comanche055.bin

然后在另一个终端启动yaDSKY:

./Simulator/yaDSKY --cfg=Comanche055.cfg

如果 DSKY 窗口出现并且数字开始变化,恭喜你,成功了。

3.2 调试实战:跟踪一次 DSKY 输入的处理过程

光看模拟器跑起来还不够,真正有意思的是跟踪代码的执行过程。我选了一个简单的场景:在 DSKY 上输入VERB 06 NOUN 62,看看 AGC 是怎么处理的。

首先,在yaAGC的命令行里设置一个断点,地址是P00或者P06,这两个是 DSKY 输入处理程序的入口。你可以用break P06命令。然后按run开始执行。

在 DSKY 上依次按下VERB、0、6、NOUN、6、2、ENTR。每按一个键,yaAGC就会在断点处停下来。你可以用examine命令查看寄存器的值,比如examine A查看累加器,examine L查看链接寄存器。

我跟踪下来的流程是这样的:按键事件首先被中断处理程序捕获,然后放入一个输入队列。主程序在空闲时从队列里取出按键,根据当前状态决定是更新显示还是执行命令。VERB 06 NOUN 62是一个显示当前时间的命令,所以程序会读取内部时钟,把小时和分钟转换成十进制,然后写到 DSKY 的显示寄存器。

这个过程涉及好几个子程序,比如KEYRUPT(键盘中断)、DISPLAY(显示刷新)、TIME2DEC(时间转十进制)。每个子程序都很短,但逻辑非常紧凑。比如TIME2DEC里用了查表法来做除法,因为 AGC 没有硬件除法指令,所有除法都要用移位和减法来实现。

注意:调试时不要一次执行太多指令,否则很容易跟丢。建议用step命令单步执行,每执行几步就用examine查看关键寄存器和内存地址。另外,AGC 的指令周期是 11.7 微秒,模拟器默认会尽量接近这个速度,但你可以用--speed参数调整。

3.3 参数计算:AGC 的时钟与指令周期

AGC 的时钟频率是 2.048 MHz,这个频率来自一个晶体振荡器,然后经过分频得到各种时序信号。指令周期不是固定的,取决于指令类型。大部分指令需要 1 到 2 个内存周期,每个内存周期是 11.7 微秒。所以一条典型指令的执行时间大约是 11.7 到 23.4 微秒。

这个速度放到今天来看慢得不可思议,但在当年,这是能造出来的最可靠的计算机之一。你可以算一下:如果一条指令平均 20 微秒,那么一秒钟能执行 5 万条指令。阿波罗 11 号的软件大约有 7 万条指令,也就是说,完整跑一遍所有代码只需要 1.4 秒左右。当然,实际运行时大部分时间都在等待外设输入,CPU 并不是一直在满负荷工作。

模拟器里有一个--speed参数,可以控制模拟速度。默认值是 1.0,表示尽量接近真实速度。如果你把它设成 0,模拟器会全速运行,不等待时钟。设成 2.0 就是两倍速。我一般用 0 来快速验证功能,用 1.0 来观察时序相关的行为。

提示:如果你想精确测量某段代码的执行时间,可以在模拟器里打开周期计数功能。命令是--cycle-count,它会在每次停止时输出已经执行的周期数。你可以用这个功能来验证你对指令周期的计算。

3.4 外设模拟:DSKY、IMU 和雷达的交互

Apollo-11 项目模拟的外设不止 DSKY 一个。IMU(惯性测量单元)和雷达的模拟也很有价值,尤其是对理解导航和制导逻辑的人来说。

IMU 模拟器叫yaIMU,它模拟了陀螺仪和加速度计的输出。你可以通过配置文件设置初始姿态和速度,然后模拟器会根据 AGC 发出的控制指令更新状态。这个模拟器对于理解P52(IMU 校准)和P63(登月制动)等程序非常有帮助。

雷达模拟器叫yaRADAR,它模拟了交会雷达和着陆雷达的数据输出。阿波罗 11 号登月过程中著名的 1201 和 1202 报警,就是因为雷达数据过载导致执行程序溢出。你可以在模拟器里重现这个场景:启动雷达模拟器,设置高数据速率,然后观察 AGC 的反应。

这些外设模拟器都通过 TCP 端口和yaAGC通信,端口号在配置文件里定义。你可以同时启动多个模拟器,让它们协同工作。比如同时启动 DSKY、IMU 和雷达,就能模拟一个完整的任务场景。

注意:外设模拟器的配置文件格式比较古老,用的是类似 INI 的格式。如果你要修改端口号或者初始状态,直接编辑.cfg文件就行。但要注意,yaAGC和模拟器之间的端口号必须一致,否则连不上。

4. 常见问题与排查技巧实录

4.1 编译阶段的高频错误与解决方案

编译阶段最容易出问题的地方是 Python 兼容性和符号解析。我整理了一个常见错误速查表,覆盖了大部分我遇到过的情况。

错误信息可能原因解决方法
SyntaxError: invalid syntaxPython 2 语法在 Python 3 下不兼容用2to3转换或手动修改
NameError: name 'xrange' is not definedPython 3 移除了xrange替换为range
AttributeError: module 'string' has no attribute 'uppercase'Python 3 移除了string.uppercase替换为string.ascii_uppercase
Undefined symbol: TCF包含文件缺失或路径错误检查MAIN.agc的#include列表
make: *** No rule to make target 'yaAGC'缺少 Makefile 或依赖检查Simulator目录下是否有 Makefile
fatal error: readline/readline.h: No such file缺少 readline 开发库安装libreadline-dev

除了这些,还有一个比较隐蔽的问题:汇编器在处理某些宏定义时会陷入死循环。我遇到过一次,原因是某个宏在展开时递归调用了自己。解决方法是找到那个宏,手动加上终止条件。这种情况很少见,但一旦遇到,很难排查,因为汇编器不会报错,只是卡住不动。

提示:如果你在编译时遇到莫名其妙的错误,先试试用项目社区提供的最新补丁。很多时候,问题已经被别人遇到并修复了,你只需要同步一下代码。

4.2 模拟器运行时的典型故障

模拟器跑起来之后,也可能出现各种奇怪的现象。我总结了几种最常见的情况。

第一种是 DSKY 显示乱码或者不刷新。这通常是因为yaDSKY和yaAGC之间的通信出了问题。检查两个程序的配置文件,确保端口号一致。另外,防火墙可能会阻止本地连接,可以临时关闭防火墙试试。

第二种是模拟器突然停止响应。这可能是遇到了死循环或者非法指令。你可以在yaAGC的命令行里按Ctrl+C中断,然后用examine查看当前指令地址,看看卡在哪里。常见的原因是某个条件判断写错了,导致程序一直在循环。

第三种是时间显示不准确。AGC 的内部时钟是基于中断的,如果模拟器的时钟频率设置不对,时间就会走快或走慢。检查--speed参数,确保它和你的预期一致。另外,有些版本的模拟器在时钟中断处理上有 bug,需要打补丁。

第四种是外设模拟器连不上。这通常是端口冲突或者配置文件路径错误。用netstat命令检查端口是否被占用,然后修改配置文件里的端口号。

注意:模拟器在运行时会输出大量调试信息,如果你不需要,可以用--quiet参数关闭。但排查问题时,这些信息非常有用,建议先打开看看有没有异常。

4.3 独家避坑技巧:从踩坑到填坑

我在折腾 Apollo-11 的过程中,积累了一些文档里不会写的经验,这里分享几个最有用的。

第一个技巧是用符号表来辅助调试。汇编器在编译时会生成一个符号表文件,里面记录了每个符号对应的内存地址。你可以把这个文件加载到模拟器里,这样在examine的时候就可以直接用符号名,而不是记地址。命令是--symbols=Comanche055.sym。这个功能极大提高了调试效率,尤其是当你需要查看某个特定变量的值时。

第二个技巧是用脚本自动化重复操作。模拟器的命令行支持脚本文件,你可以把一系列命令写在一个文本文件里,然后用--script参数加载。比如你想每次启动都自动设置几个断点、加载符号表、运行到某个地址,就可以写一个脚本。我自己的启动脚本大概有十几行,省去了每次手动输入命令的麻烦。

第三个技巧是对比不同版本的代码。Apollo-11 项目包含了多个版本的软件,比如指令舱有 Comanche 055 和 Comanche 056,登月舱有 Luminary 099 和 Luminary 116。这些版本之间的差异往往很小,但反映了当年程序员在任务前的最后修改。你可以用diff命令对比两个版本的源码,看看改了哪些地方,然后思考为什么改。这种对比学习的效果非常好。

第四个技巧是关注社区的问题追踪。项目仓库的 issue 区里有很多有价值的讨论,比如某个符号的定义有歧义、某个指令的行为和文档不一致等。这些讨论往往能帮你避开一些隐藏的坑。我遇到过一个问题是某个内存地址的初始值不对,导致程序启动后行为异常,后来在 issue 区找到了答案:原始扫描件里那个地址的值印错了,社区已经提交了修正。

提示:如果你发现了新的问题或者有改进建议,不妨提交一个 issue 或者 pull request。这个项目的维护者很友好,而且你的贡献可能会帮助到其他人。

4.4 性能优化:让模拟器跑得更流畅

虽然 Apollo-11 模拟器对硬件要求不高,但在一些老机器上或者运行复杂场景时,还是可能卡顿。我试过几种优化方法,效果比较明显。

第一种是关闭不必要的调试输出。模拟器默认会输出很多信息,比如每条指令的执行、每次内存访问等。这些输出会拖慢速度。用--quiet参数可以关闭大部分输出,只保留错误信息。

第二种是调整模拟速度。如果你不需要精确的时序,可以把--speed设成 0,让模拟器全速运行。这样 CPU 占用会高一些,但响应更快。

第三种是使用更快的 Python 解释器。汇编器是用 Python 写的,如果你用 PyPy 来运行,速度会比 CPython 快很多。我试过用 PyPy 跑yaYUL,编译时间从 40 秒缩短到了 10 秒左右。

第四种是把模拟器编译成优化版本。默认的make可能没有开启最高优化级别。你可以修改 Makefile,把-O2改成-O3,或者加上-march=native让编译器针对你的 CPU 优化。这样模拟器的执行速度会有明显提升。

注意:优化编译可能会引入一些微妙的 bug,尤其是涉及到浮点运算或者时序的地方。如果你发现模拟器行为异常,先换回默认编译选项试试。

5. 从 Apollo-11 项目中学到的工程智慧

5.1 资源受限下的编程哲学

AGC 只有 72KB 的内存,其中还包括了操作系统和外设驱动。留给应用程序的空间非常有限。在这种条件下,当年的程序员发展出了一套独特的编程哲学,我觉得对今天的嵌入式开发仍然有借鉴意义。

首先是代码复用。AGC 软件里有大量的小型子程序,每个只做一件事,比如加法、减法、移位、查表。这些子程序被反复调用,而不是把功能内联到主程序里。这种设计节省了内存,但也增加了调用开销。程序员需要在两者之间权衡。

其次是数据驱动。很多逻辑不是用条件判断实现的,而是用查表法。比如把角度转换成三角函数值,不是实时计算,而是查一个预先算好的表。这种方法的精度取决于表的大小,但速度非常快。

第三是中断驱动。AGC 的大部分时间都在等待中断,而不是轮询外设。中断处理程序尽量短小,只做最紧急的事情,剩下的工作交给主程序。这种设计提高了响应速度,也降低了功耗。

这些哲学放到今天,在物联网设备、可穿戴设备、汽车电子等领域仍然适用。资源永远是不够的,如何在限制下写出可靠的代码,是每个嵌入式工程师都要面对的问题。

5.2 代码的可读性与可维护性

Apollo-11 的代码是用汇编写的,但可读性并不差。当年的程序员有一套命名约定和注释规范,让代码即使过了几十年也能看懂。

比如,所有变量名都是大写的,用下划线分隔单词。子程序名通常以功能开头,比如DISPLAY、KEYRUPT、TIME2DEC。注释里会说明子程序的输入输出、使用的寄存器、可能修改的内存地址。这些注释虽然简短,但信息量很大。

另外,代码里有很多“防御性编程”的痕迹。比如在除法之前先检查除数是否为零,在访问数组之前先检查索引是否越界。这些检查在资源紧张的情况下可能会被省略,但当年的程序员还是坚持做了,因为一次错误就可能导致任务失败。

这种对可读性和可维护性的重视,值得今天的开发者学习。尤其是在团队协作中,代码不仅是给机器执行的,更是给人看的。

5.3 测试与验证的方法

Apollo-11 项目里包含了一些测试脚本和验证工具,虽然不如现代测试框架那么完善,但思路很清晰。

比如,有一个脚本用来校验汇编后的二进制文件的校验和。AGC 的内存里有一个校验和区域,软件启动时会计算所有代码的校验和,和预存的值比较。如果不一致,就说明代码被篡改了或者内存出了问题。这个机制在当年非常重要,因为核心绳存储器是不可擦写的,一旦出错只能更换。

还有一个脚本用来模拟各种输入场景,比如键盘输入、雷达数据、IMU 数据,然后检查程序的输出是否符合预期。这种“场景测试”的方法在今天仍然很常用,尤其是在航天、医疗等对可靠性要求极高的领域。

我自己的做法是,在修改代码之后,先跑一遍基本的启动测试,确认 DSKY 能正常显示,然后再跑几个关键场景,比如 P63 和 P64,看看有没有异常。这种分层测试的方法可以快速定位问题。

5.4 社区协作与知识传承

Apollo-11 项目之所以能持续维护,离不开社区的协作。很多人贡献了自己的时间和专业知识,修复 bug、改进工具、编写文档。这种协作模式对于历史代码的修复和传承非常重要。

我注意到,社区里有一些人是当年参与过阿波罗计划的工程师,他们在 issue 区里分享了很多第一手的经验。比如有人解释了为什么某个子程序要那样写,因为当年的硬件有某个限制。这些信息在官方文档里是找不到的,只有亲历者才知道。

另外,社区还整理了一份详细的 Wiki,包含了编译指南、调试技巧、常见问题等。这份 Wiki 是新手入门的最佳资料,比看源码效率高得多。

如果你对历史代码感兴趣,我建议你加入这个社区,哪怕只是提一个问题或者修正一个错别字,都是对知识传承的贡献。

6. 后续扩展与个人体会

6.1 可以继续深挖的方向

Apollo-11 项目本身已经比较完整了,但还有很多可以扩展的方向。比如,你可以尝试把 AGC 软件移植到现代的微控制器上,用真实的硬件来运行。这需要写一个 AGC 指令集的解释器,或者用 FPGA 来实现 AGC 的硬件逻辑。这种“复古硬件”项目在创客社区里很受欢迎。

另一个方向是可视化调试。目前的模拟器主要是命令行界面,如果你能做一个图形化的调试器,把内存、寄存器、DSKY 显示、外设状态都可视化,会大大降低学习门槛。已经有一些社区项目在做类似的事情,但还有很大的改进空间。

还有一个方向是任务重现。你可以用模拟器完整重现阿波罗 11 号的登月过程,从发射到着陆,每一步都按照历史记录来操作。这需要深入理解任务手册和飞行计划,但完成后会非常有成就感。

6.2 我个人的一些体会

折腾 Apollo-11 项目的这段时间,我最大的收获不是学会了怎么运行一个老软件,而是对“软件工程”这四个字有了更深的理解。当年的程序员在资源极其有限、工具极其简陋的条件下,写出了能把人送上月球的代码。他们的代码里没有花哨的设计模式,没有复杂的框架,但每一行都经过深思熟虑,每一个字节都用在刀刃上。

这种对代码的敬畏之心,在今天的开发环境里反而少见了。我们有了更多的内存、更快的 CPU、更强大的工具,但代码的质量并没有成比例提升。有时候我会想,如果今天的项目也像 AGC 那样限制内存和速度,我们还能不能写出可靠的软件?

另一个体会是,历史代码是一座宝库。很多人觉得老代码没有学习价值,但我觉得恰恰相反。老代码里蕴含的工程智慧,是经过实践检验的,比很多新潮的概念更可靠。只要你愿意花时间去读、去调试、去理解,一定能学到东西。

最后再分享一个小技巧:如果你在调试时遇到一个很难的问题,不妨暂时放下代码,去读一读当年的任务手册或者宇航员的回忆录。很多时候,理解了当时的场景和约束,问题就迎刃而解了。技术不是孤立的,它总是嵌入在具体的历史和人文背景中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询