1. 项目缘起与整体设计思路
第一次看到“Apollo-11”这个标题,很多人脑子里蹦出来的可能是那个著名的登月计划。但在我们这行——准确说,是在嵌入式系统仿真、历史计算平台复刻、以及制导计算机(AGC)研究这个小圈子里,“Apollo-11”指的是一份被完整开源出来的阿波罗11号制导计算机(Apollo Guidance Computer,简称AGC)源代码,以及围绕它衍生出来的一整套仿真、汇编、测试工具链。这份代码最早由一位名叫Ron Burkey的开发者带着团队从麻省理工仪器实验室当年打印的纸质清单里一行行转录出来,后来被放到了开源社区,成了无数嵌入式爱好者、计算机历史研究者、甚至航天爱好者的“朝圣级”项目。
我接触这个项目大概是在三年前,当时正在做一个关于实时操作系统任务调度的内部技术分享,想找一个足够极端、足够有说服力的案例来说明“在资源极度受限的环境下,调度算法能有多精妙”。翻了一圈资料,最后落在了AGC上。这台计算机的主频只有2.048 MHz,内存分为36K字的只读存储(Core Rope Memory)和2K字的可读写存储(Core Memory),字长15位(实际使用16位,其中1位奇偶校验)。用今天的眼光看,这配置连一个电子表都不如,但它却完成了载人登月过程中最关键的制导、导航与控制任务。这种反差感,正是“Apollo-11”项目最吸引人的地方。
这个项目能做什么?简单说,它让你可以在现代电脑上完整地模拟AGC的硬件行为,加载当年阿波罗11号任务实际使用的软件版本(比如Comanche 055、Luminary 099等),然后通过一个叫DSKY(Display and Keyboard)的虚拟界面,像当年的宇航员一样输入指令、读取状态。你甚至可以把它接入一个叫yaAGC的仿真器,配合yaDSKY图形界面,跑一遍完整的登月流程模拟。对于搞嵌入式开发的人来说,这是一个绝佳的汇编语言学习平台——AGC用的是一种自定义的Block II指令集,只有几十条指令,但每一行代码都经过极致优化,读起来像在读一首精炼的诗。
适合谁来参考?我认为有三类人值得花时间折腾一下:第一类是嵌入式软件工程师,尤其是做航天、工业控制、汽车电子方向的,AGC的中断处理机制和任务优先级调度至今仍有借鉴意义;第二类是计算机体系结构爱好者,想亲眼看看在没有流水线、没有缓存、没有虚拟内存的年代,程序是怎么跑起来的;第三类是历史技术考古爱好者,对阿波罗计划的技术细节有执念,想亲手“摸一摸”当年的代码。当然,如果你只是单纯想找个酷炫的复古项目玩玩,它也完全够格——毕竟,能在地面模拟器里“驾驶”登月舱降落,这件事本身就足够浪漫。
整体设计上,这个项目遵循了一个非常清晰的分层仿真架构。最底层是CPU仿真层,负责逐条解释执行AGC的机器指令,精确到每个时钟周期;中间是外设仿真层,包括DSKY、IMU(惯性测量单元)、雷达、发动机指令接口等;最上层是任务脚本层,允许你通过脚本或手动输入来驱动整个仿真流程。这种分层的好处是,你可以只关心其中一层——比如你只想研究指令集,那就只跑CPU仿真;如果你想复现整个登月过程,那就把三层都拉起来。我个人的经验是,先从CPU层入手,把几条基本指令跑通,再逐步往上加外设,这样不容易被复杂的交互逻辑劝退。
2. 核心细节解析与实操要点
2.1 AGC的硬件架构到底特殊在哪
要理解这个项目,得先搞清楚AGC的硬件设计逻辑。它最反直觉的一点是:内存和存储是分开的,而且只读存储是用“绳存储”(Core Rope Memory)做的。所谓绳存储,就是由工人手工将导线穿过磁芯,穿线的方式决定了每一位是0还是1。这种存储方式的好处是非易失性极强——断电几十年数据也不会丢,而且抗辐射能力远超后来的半导体存储。坏处是一旦穿好就无法修改,所以当年MIT的工程师们必须在穿线之前把程序调试到近乎完美。我读到过一份资料,说阿波罗11号的Luminary 099版本软件在发射前几周还在修改,工人们不得不连夜重新穿线,那种压力可想而知。
AGC的字长是15位数据加1位奇偶校验,总共16位。为什么是15位?因为要表示角度、速度、加速度这些物理量,15位补码能覆盖的范围是-16384到+16383,配合定点小数运算,精度刚好够用。指令格式分为两种:基本指令和扩展指令。基本指令占15位,其中3位是操作码,12位是地址;扩展指令则用两个连续的字来表示,能访问更大的地址空间。这种设计在当年是权衡了指令密度和寻址范围之后的结果。我实际用仿真器跑的时候发现,AGC的指令执行时间不是固定的——有的指令只要1个内存周期(约11.7微秒),有的要2个甚至更多。这意味着写汇编的时候,指令排布的顺序会直接影响执行效率,这种“手工流水线”的思路在今天看来依然很有意思。
另一个关键点是中断系统。AGC有10个硬件中断向量,优先级从高到低排列。最高优先级的是电源故障中断,其次是任务超时中断,然后是DSKY按键中断等等。这种设计确保了最关键的任务(比如保持姿态稳定)永远不会被低优先级任务打断。我在自己的仿真测试中试过,如果同时触发多个中断,AGC会严格按照优先级顺序处理,而且中断响应时间是可以预测的——这对实时系统来说至关重要。当年阿波罗13号出事故时,地面控制中心能快速上传新的制导程序,靠的就是这套中断机制允许在飞行中动态修改任务优先级。
2.2 汇编语言层面的几个“坑”
AGC的汇编器叫yaYUL,是项目里自带的一个工具。它的语法和现代汇编器差别很大,第一次用很容易踩坑。我整理了几个最常见的:
- 地址是八进制表示的。AGC的地址空间按八进制组织,比如
4000表示的是八进制4000,换算成十进制是2048。如果你习惯用十六进制,这里会非常别扭。我的建议是在注释里同时写上十进制值,方便对照。 - 指令和伪指令混在一起。yaYUL里既有真正的机器指令(如
TC、CCS、INDEX),也有伪指令(如EQU、SET、BANK)。伪指令不生成机器码,只影响汇编过程。新手容易把EQU当成赋值语句,其实它是符号定义,类似于C语言的#define。 - Bank切换是手动管理的。AGC的只读存储分成36个Bank,每个Bank有1024个字。当程序需要跨Bank调用时,必须手动设置Bank寄存器。如果忘了切换,程序会跳到错误的地址,仿真器直接报“Bank Switch Error”。我踩过这个坑,调试了整整一个下午才发现是漏了一行
BANK指令。
提示:在写AGC汇编时,建议每写十几行就汇编一次,用yaYUL的列表文件检查生成的机器码。不要等写了几百行再一起汇编,否则错误定位会非常痛苦。
2.3 DSKY交互的细节与实操
DSKY是宇航员和AGC之间的唯一界面,它由一个数字显示屏和按键面板组成。显示屏有三行:第一行是程序号(Program),第二行是动词号(Verb),第三行是名词号(Noun)。宇航员通过输入“动词+名词”的组合来执行操作,比如V06 N62表示“显示当前姿态角”。这种交互方式在今天看来极其原始,但在当年是为了减少按键次数、降低误操作概率而精心设计的。
在仿真器里操作DSKY,你需要先启动yaDSKY程序,它会弹出一个图形界面,上面有和真实DSKY一模一样的按键。我建议先用鼠标点击熟悉按键布局,然后再尝试用键盘快捷键——yaDSKY支持把数字键和功能键映射到键盘上,熟练之后输入速度会快很多。有一个细节值得注意:DSKY的按键有防抖逻辑,如果你按得太快,仿真器可能会忽略第二次按键。我在模拟登月舱降落时,因为连续输入太快,导致程序号没有正确切换,结果仿真直接进入了错误状态。后来我养成了每次按键后看一眼显示屏确认的习惯,这个问题就再也没出现过。
另外,DSKY的指示灯也很重要。比如PROG灯亮表示程序正在运行,KEY REL灯亮表示AGC请求释放键盘。如果你在仿真中看到KEY REL灯闪烁,说明AGC正在等待你输入数据,这时候如果不及时响应,程序可能会超时。我实测下来,超时时间大约是10秒,超过之后AGC会自动执行默认操作,这在真实任务中是极其危险的。
3. 实操过程与核心环节实现
3.1 环境搭建:从零到跑通第一个程序
我是在一台Ubuntu 22.04的机器上搭建环境的,Windows用户可以用WSL2,Mac用户直接用Homebrew装依赖也行。整个流程分为四步:
第一步,获取源码。项目托管在开源社区,直接克隆仓库即可。仓库里包含三个主要目录:yaAGC(CPU仿真器)、yaDSKY(DSKY界面)、yaYUL(汇编器)。我建议先只编译yaAGC和yaYUL,把DSKY放一放,因为DSKY依赖图形库,编译起来稍微麻烦一点。
第二步,安装依赖。主要是gcc、make、flex、bison,以及libncurses和libX11。在Ubuntu上一条命令就能搞定:
sudo apt-get install build-essential flex bison libncurses-dev libx11-dev第三步,编译。进入yaAGC目录,执行make。如果一切顺利,你会得到一个叫yaAGC的可执行文件。然后进入yaYUL目录,同样make,得到yaYUL。这两个工具是后续所有操作的基础。
第四步,汇编并运行第一个程序。项目里自带了一些示例汇编文件,比如Validation目录下的测试程序。我选了一个最简单的,用yaYUL汇编:
./yaYUL -o test.bin test.agc然后启动yaAGC,加载生成的test.bin:
./yaAGC --core=test.bin --port=19697这时候yaAGC会监听一个TCP端口,等待DSKY连接。如果你还没编译DSKY,可以用telnet手动发送指令来测试。我试过用telnet localhost 19697连上去,然后输入R(表示Reset),再输入G(表示Go),就能看到AGC开始执行程序。虽然看不到DSKY界面,但yaAGC的终端会打印出执行日志,包括每条指令的地址和操作码。这个日志是调试的利器,我经常用它来确认程序是否按预期跳转。
3.2 加载阿波罗11号实际任务软件
跑通测试程序之后,就可以上“正菜”了——加载阿波罗11号任务实际使用的Luminary 099软件。这个软件版本是登月舱使用的,包含了**P63(制动阶段)、P64(接近阶段)、P65(着陆阶段)、P66(手动控制)**等核心程序。源码仓库里通常有一个Luminary099目录,里面是完整的汇编源文件。
汇编Luminary 099需要一点耐心,因为文件很大,yaYUL跑完大概要十几秒。汇编完成后会生成一个.bin文件,大小约36K字,正好填满AGC的只读存储。加载到yaAGC之后,你需要通过DSKY输入一系列指令来启动任务。我记录了一个最小启动序列:
- 按
RSET键复位。 - 输入
V36 N00,按ENTR。这是“刷新程序”指令,会清空所有寄存器。 - 输入
V37 N00,按ENTR。这是“改变程序”指令,把程序号设为00。 - 输入
V06 N62,按ENTR。这是“显示姿态角”指令,如果DSKY显示出一组角度值,说明IMU仿真正常。 - 输入
V37 N63,按ENTR。这是“改变程序”到63,即制动阶段。
完成这五步之后,AGC就进入了P63制动阶段。这时候如果你有yaIMU(惯性测量单元仿真器)在跑,DSKY会开始显示高度和速度的变化。我实测下来,从P63开始到着陆,整个仿真过程大约需要10到15分钟,取决于你设置的初始轨道参数。
3.3 参数计算:如何设置初始轨道
初始轨道参数决定了整个着陆过程的难度。在真实任务中,这些参数是由地面跟踪站计算好之后上传的。在仿真里,你可以手动设置。我一般用以下公式来估算初始高度和速度:
- 初始高度:约50,000英尺(约15.24公里),这是P63开始时的典型高度。
- 初始水平速度:约5,500英尺/秒(约1,676米/秒),这是绕月轨道的速度。
- 初始垂直速度:约**-100英尺/秒**(约-30米/秒),表示正在下降。
这些参数在yaAGC的启动配置里可以设置。我试过把初始高度调高到60,000英尺,结果发现燃料消耗明显增加,最后在P66阶段差点没油了。所以如果你想完整跑完着陆,不要随意调高初始高度,除非你也相应增加燃料预算。
另一个关键参数是着陆点坐标。在仿真里,你可以通过V06 N64来查看当前着陆点。我建议先用默认坐标跑一遍,熟悉流程之后再尝试修改。修改坐标需要用到V48指令,输入新的经纬度,这个操作在真实任务中是由地面控制中心完成的,宇航员只需要确认。
3.4 实操现场记录:一次完整的着陆仿真
我完整跑过一次从P63到P66的着陆仿真,整个过程大约12分钟。这里记录几个关键节点:
- T+0分钟:输入
V37 N63,AGC进入P63。DSKY显示PROG 63,ALT(高度)约50,000英尺,VEL(速度)约5,500英尺/秒。 - T+3分钟:高度降到约30,000英尺,速度降到约3,000英尺/秒。AGC自动切换到P64接近阶段。这时候DSKY会显示
PROG 64,并且ALT和VEL的刷新频率明显加快。 - T+7分钟:高度约10,000英尺,速度约500英尺/秒。AGC进入P65着陆阶段。这时候你需要手动输入
V06 N63来监控姿态角,确保登月舱没有倾斜。 - T+10分钟:高度约3,000英尺,速度约100英尺/秒。AGC进入P66手动控制阶段。这时候你可以用
V77指令来微调下降速率。我试过用V77把下降速率从**-10英尺/秒调到-5英尺/秒**,结果着陆时间延长了约1分钟,但着陆更平稳。 - T+12分钟:高度降到0,DSKY显示
PROG 66,ALT为0,VEL为0。仿真器弹出“Lunar Contact”提示,着陆成功。
整个过程中,我最大的感受是AGC的自动化程度远超我的预期。在P63到P65阶段,几乎不需要人工干预,AGC会根据预设的制导律自动调整发动机推力和姿态。只有在P66阶段,才需要宇航员手动介入。这种设计哲学——把复杂计算交给机器,把最终决策权留给人——在今天的人工智能时代依然有很强的参考价值。
4. 常见问题与排查技巧实录
4.1 仿真器启动失败怎么办
这是新手最常遇到的问题。我整理了一个排查速查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| yaAGC启动后立即退出 | 缺少.bin文件或路径错误 | 检查--core参数指向的文件是否存在 |
| 端口被占用 | 19697端口已被其他程序使用 | 用--port参数换一个端口,比如19698 |
| DSKY连接不上 | 防火墙阻止了本地回环连接 | 临时关闭防火墙,或添加规则允许19697端口 |
| 汇编时报“Bank Overflow” | 程序太大,超出了单个Bank的容量 | 检查是否有多余的代码,或手动调整Bank分配 |
| 仿真过程中突然卡死 | 程序进入了死循环 | 用yaAGC的--debug模式查看最后执行的指令地址 |
我踩过最坑的一次是端口被占用。当时我同时开了两个yaAGC实例,第二个死活连不上DSKY。后来用netstat -tulpn | grep 19697一查,发现第一个实例还在后台跑着。杀掉之后一切正常。所以每次启动仿真前,先确认没有残留进程,这个习惯能省很多时间。
4.2 DSKY显示乱码或闪烁
DSKY显示乱码通常是因为字符编码不匹配。yaDSKY默认使用七段数码管字体,如果你的系统缺少这个字体,显示就会变成方块。解决办法是安装项目自带的字体文件,通常在yaDSKY/fonts目录下,复制到~/.fonts然后运行fc-cache -fv即可。
闪烁问题则多半是刷新率设置不当。yaDSKY有一个--refresh参数,默认是60Hz。如果你的显示器刷新率不是60Hz,可能会看到闪烁。我试过把刷新率调到30Hz,闪烁就消失了。另外,如果你在虚拟机里跑yaDSKY,开启3D加速也能明显改善显示效果。
4.3 程序号不切换或指令无响应
这个问题通常有两个原因:一是按键输入没有被AGC接收,二是AGC处于等待状态。先检查DSKY上的KEY REL灯是否亮着——如果亮着,说明AGC正在等待你输入数据,这时候你需要输入完整的“动词+名词”组合,然后按ENTR。如果KEY REL灯没亮,但按键无响应,那可能是yaAGC和yaDSKY之间的连接断了。我遇到过几次,原因是yaDSKY在后台被系统挂起(比如笔记本合盖休眠)。重新启动yaDSKY,然后按RSET键复位AGC,通常就能恢复。
还有一个隐蔽的坑:AGC的程序号切换需要满足特定条件。比如从P63切换到P64,必须等到高度降到30,000英尺以下才会自动触发。如果你在高度还很高的时候手动输入V37 N64,AGC会拒绝执行,DSKY上会显示OP ERR(操作错误)。这时候你需要按RSET清除错误,然后等条件满足再试。
4.4 仿真速度太慢或太快
yaAGC默认以实时速度运行,即模拟AGC的2.048 MHz时钟。但在现代电脑上,这个速度其实很慢——你会觉得DSKY的刷新像蜗牛爬。如果你只想快速验证逻辑,可以用--speed参数加速,比如--speed=10表示10倍速。我试过100倍速,结果发现中断处理会出错,因为仿真器来不及响应。所以加速不要超过10倍,否则仿真结果不可信。
反过来,如果你觉得仿真太快,想仔细看每一步,可以用--speed=0.1减速。这个在调试汇编程序时特别有用,你能清楚地看到每条指令执行后寄存器的变化。
4.5 独家避坑技巧
最后分享几个我从实践中总结的独家技巧:
- 备份你的
.bin文件。每次修改汇编源码后重新汇编,生成的.bin文件会覆盖旧的。我习惯把每个版本的.bin文件按日期命名,比如Luminary099_20240101.bin,这样出问题可以快速回滚。 - 用
script命令记录终端输出。yaAGC的终端日志很有价值,但默认不保存。在启动yaAGC之前先运行script agc_log.txt,这样所有输出都会保存到文件里,方便事后分析。 - DSKY按键有“长按”和“短按”的区别。短按是正常输入,长按(超过2秒)会触发重复输入。我在输入长数字时不小心长按了,结果同一个数字被输入了两次,导致程序号错误。后来我养成了每次按键不超过1秒的习惯。
- 定期检查AGC的“时钟”。AGC内部有一个时间计数器,每10毫秒递增一次。如果仿真跑久了发现时间计数器不走了,说明仿真器可能进入了死锁状态。这时候只能重启yaAGC,没有别的办法。
这个项目我断断续续玩了三年,每次重新捡起来都能发现新的细节。最近我在尝试把yaAGC的输出接入一个三维可视化工具,想看看能不能把整个着陆过程用动画呈现出来。目前还在折腾坐标变换的部分,等跑通了再另开一篇分享。如果你也在玩这个项目,欢迎交流——尤其是如果你找到了更高效的调试方法,一定要告诉我。