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等然后通过一个叫DSKYDisplay 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用户可以用WSL2Mac用户直接用Homebrew装依赖也行。整个流程分为四步第一步获取源码。项目托管在开源社区直接克隆仓库即可。仓库里包含三个主要目录yaAGCCPU仿真器、yaDSKYDSKY界面、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 --coretest.bin --port19697这时候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分钟。这里记录几个关键节点T0分钟输入V37 N63AGC进入P63。DSKY显示PROG 63ALT高度约50,000英尺VEL速度约5,500英尺/秒。T3分钟高度降到约30,000英尺速度降到约3,000英尺/秒。AGC自动切换到P64接近阶段。这时候DSKY会显示PROG 64并且ALT和VEL的刷新频率明显加快。T7分钟高度约10,000英尺速度约500英尺/秒。AGC进入P65着陆阶段。这时候你需要手动输入V06 N63来监控姿态角确保登月舱没有倾斜。T10分钟高度约3,000英尺速度约100英尺/秒。AGC进入P66手动控制阶段。这时候你可以用V77指令来微调下降速率。我试过用V77把下降速率从**-10英尺/秒调到-5英尺/秒**结果着陆时间延长了约1分钟但着陆更平稳。T12分钟高度降到0DSKY显示PROG 66ALT为0VEL为0。仿真器弹出“Lunar Contact”提示着陆成功。整个过程中我最大的感受是AGC的自动化程度远超我的预期。在P63到P65阶段几乎不需要人工干预AGC会根据预设的制导律自动调整发动机推力和姿态。只有在P66阶段才需要宇航员手动介入。这种设计哲学——把复杂计算交给机器把最终决策权留给人——在今天的人工智能时代依然有很强的参考价值。4. 常见问题与排查技巧实录4.1 仿真器启动失败怎么办这是新手最常遇到的问题。我整理了一个排查速查表现象可能原因解决方法yaAGC启动后立即退出缺少.bin文件或路径错误检查--core参数指向的文件是否存在端口被占用19697端口已被其他程序使用用--port参数换一个端口比如19698DSKY连接不上防火墙阻止了本地回环连接临时关闭防火墙或添加规则允许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 N64AGC会拒绝执行DSKY上会显示OP ERR操作错误。这时候你需要按RSET清除错误然后等条件满足再试。4.4 仿真速度太慢或太快yaAGC默认以实时速度运行即模拟AGC的2.048 MHz时钟。但在现代电脑上这个速度其实很慢——你会觉得DSKY的刷新像蜗牛爬。如果你只想快速验证逻辑可以用--speed参数加速比如--speed10表示10倍速。我试过100倍速结果发现中断处理会出错因为仿真器来不及响应。所以加速不要超过10倍否则仿真结果不可信。反过来如果你觉得仿真太快想仔细看每一步可以用--speed0.1减速。这个在调试汇编程序时特别有用你能清楚地看到每条指令执行后寄存器的变化。4.5 独家避坑技巧最后分享几个我从实践中总结的独家技巧备份你的.bin文件。每次修改汇编源码后重新汇编生成的.bin文件会覆盖旧的。我习惯把每个版本的.bin文件按日期命名比如Luminary099_20240101.bin这样出问题可以快速回滚。用script命令记录终端输出。yaAGC的终端日志很有价值但默认不保存。在启动yaAGC之前先运行script agc_log.txt这样所有输出都会保存到文件里方便事后分析。DSKY按键有“长按”和“短按”的区别。短按是正常输入长按超过2秒会触发重复输入。我在输入长数字时不小心长按了结果同一个数字被输入了两次导致程序号错误。后来我养成了每次按键不超过1秒的习惯。定期检查AGC的“时钟”。AGC内部有一个时间计数器每10毫秒递增一次。如果仿真跑久了发现时间计数器不走了说明仿真器可能进入了死锁状态。这时候只能重启yaAGC没有别的办法。这个项目我断断续续玩了三年每次重新捡起来都能发现新的细节。最近我在尝试把yaAGC的输出接入一个三维可视化工具想看看能不能把整个着陆过程用动画呈现出来。目前还在折腾坐标变换的部分等跑通了再另开一篇分享。如果你也在玩这个项目欢迎交流——尤其是如果你找到了更高效的调试方法一定要告诉我。