提起“Apollo-11”,大多数人第一反应是1969年那艘飞向月球的飞船。但今天这篇想聊的,是被我当作项目代号用了大半年的 Apollo-11——一套在浏览器里复刻登月计算机AGC的模拟工程。它不光是让老代码重新跑起来,还把当年那块布满按键、玻璃管数字的DSKY面板拆开揉碎,原样搬上了现代显示器的像素世界。我最初只是想搞懂“宇航员是怎么用键盘操作宇宙飞船的”,结果越挖越深,从指令集、中断时序一直研究到内存校验的翻转陷阱。这篇文章就是这段折腾过程的完整复盘:AGC到底怎么模拟、DSKY的Verb/Noun交互怎么落地、我踩了哪些莫名其妙的坑、以及这套东西还能用到哪里去。适合对复古计算、嵌入式仿真或航天科普感兴趣的朋友,哪怕你之前没碰过汇编,顺着步骤走也能点亮属于自己的那块DSKY。
1. 项目整体设计与思路拆解
1.1 Apollo-11 到底在复刻什么
如果只看名字,很容易以为,我们要复刻的是那个年代的整套飞船系统。但实际上,整个项目的核心只有一个对象:阿波罗制导计算机(Apollo Guidance Computer,简称AGC),以及它面前的交互面板DSKY。
AGC是一款非常特殊的机器。它的字长只有15位,内存加在一起不过几千字,主频也只有2 MHz左右。放到今天,连一颗最普通的单片机都比它快几百倍。但就是这么台“简陋”的计算机,承担了导航、制导、姿态控制以及登月下降等关键任务。这意味着,想让复刻项目尽量真实,就不能简单地把输出结果算出来,而是要逐条执行当年的机器码,连存储器的奇偶校验、中断系统的响应次序都得一并模拟出来。
复刻的思路不难理解:先找到一个能解释执行AGC指令的虚拟机,再把当年的飞行程序(比如登月舱上用的下降程序)加载进去,最后给模拟器配上一块虚拟的DSKY面板。这样,你在屏幕上看到的每一个数字变化、每一次按键触发灯亮,都对应着当年真实飞控流程里的一步操作。对我个人来说,这种“肉眼可见的历史现场”远比读十篇科普文章来得震撼。
整个项目在设计上,我没有选择去复刻硬件电路,而是把重心放在了“行为等价”上。也就是说,结果必须是那个年代AGC会算出的结果,按键时间和响应顺序也必须遵循原始交互逻辑。至于CPU内部是不是用晶体管堆出来的,倒是其次。
1.2 为什么坚持用软件仿真,而不是硬件复刻
动手之前,我专门评估过三种可行方案:
| 方案 | 成本 | 周期 | 保真度 | 踩坑风险 |
|---|---|---|---|---|
| 硬件复刻:找老芯片,自己焊电路板 | 高,老芯片难找且贵 | 长,焊接调试按月算 | 电路级一致 | 极高,供电和时序问题防不胜防 |
| 混合方案:现代MCU模拟AGC指令,搭配物理DSKY面板 | 中高 | 中 | 行为一致,手感真实 | 中 |
| 纯软件仿真:解释指令 + 虚拟DSKY | 低 | 几天就能跑起来 | 行为一致,适合代码级调试 | 低 |
我最后选了纯软件仿真,最主要的原因是:这个项目真正的价值不在“造一块能亮的灯板”,而在“让那套古老的软件系统能被现代人直接操作和理解”。软件仿真可以随时打断指令流、设置断点、观察寄存器状态,这是硬件方案很难做到的。
另一个决定因素是测试便利性。模拟器里加载程序只需要一个文件路径,不需要考虑供电、时钟、总线冲突。我甚至可以把时间拨到一个出错瞬间,反复验证某个跳转分支,这在真实环境下几乎不可能。
当然,纯软件方案有一个劣势:没有实体按键,体验不到“咔哒”的物理反馈。所以我后来用现代键盘合成为触感模拟,算是一种折中。
2. 核心细节解析与实操要点
2.1 一条指令是怎么被“喂”给老计算机的
AGC的指令字长15位,所以整个可寻址空间理论上是32768个单元。它没有把这么大的空间全用作内存,而是把存储区分成两块:只读的固定存储区(常被称为“绳索内存”或“Rope”)和可读写的临时存储区(又叫“可变内存”)。
固定存储区里放的是飞行程序的指令和常量表,特点是掉电不丢、只能读。可变存储区才承担实时运算、中间变量、导航参数缓存等功能。设计这么一套双区内存,很大程度是为了可靠性和省电——那个年代芯片工艺有限,尽可能简化逻辑、减少运动部件,比什么都重要。
在模拟器里,我按原始规格实现了地址映射关系。每次指令执行时,虚拟机根据操作数地址去查找它属于固定区还是可变区,再决定从哪块内存里取数据。听起来简单,但实际操作中坑不少。因为某些地址在原始设计里还映射了外设寄存器,比如光电扫描仪输入、DSKY按键状态,都是通过特定地址暴露给CPU的。第一次写模拟器时,我忘了处理这段映射,结果改写某个“普通内存地址”时,整个DSKY的灯光状态都跟着变,吓我一跳。
至于指令集本身,AGC的指令数量不多,大部分指令是针对导航运算设计的基础操作,比如加法、减法、比较、跳转、乘除等。它没有现代CPU那种复杂的流水线、分支预测和缓存机制。从体系结构角度看,这像是一台可以直接被人类“读透”的机器,反倒很适合做教学演示。
2.2 DSKY 的交互协议:Verb/Noun 体系
DSKY是AGC的“屏幕+键盘”,全称是Display and Keyboard。宇航员需要通过它和计算机对话,但那条对话通道窄得可怕——没有鼠标,没有图形界面,只有数字键和少数功能键。
整个交互体系的核心,是一套叫 Verb/Noun 的协议。每次操作,宇航员先输入一个两位数的“动词”(Verb),表示要做什么。再输入一个两位数的“名词”(Noun),表示要对什么数据做这件事。比如:
- Verb 06,意指“读一个数据”。
- Noun 34,可能代表“当前高度”或“速度”,具体由飞行阶段决定。
输入完成后,宇航员按执行键,DSKY面板上的数字管就显示结果。整个过程非常简洁,但信息密度极高。
我在虚拟DSKY里,把原始面板的元素都做进去了:一个三位数的程序号显示区,一个主体为五位数字的数值显示区,以及几个状态灯。键盘方面,除了0到9的数字键,还有用于切换Verb、Noun的专用键,以及清零键、报警确认键等。为了尽量接近真实交互,我还给按键加了大约几十毫秒的消抖延迟,避免模拟器在极短时间内重复触发。
这里需要特别提醒:千万别小看按键消抖这件事。原版DSKY的按键是靠电磁继电器完成触发的,按下后有明确的物理时序。软件模拟如果不做延时抑制,很可能会导致同一联机电信号被当成多次输入,从而让Verb或Noun数值跳变。
2.3 时间与中断:为什么要在模拟器里“对齐时间”
AGC虽然慢,但它是一台实时计算机。它必须每秒若干次地处理惯性测量单元的数据、调整姿态控制指令、更新显示。这一切离不开中断系统和定时器。
原始AGC里有一个专门的实时时钟,会周期性产生中断,让程序暂停当前任务,转而去处理更紧急的事情。比如在登月下降阶段,高度计的数据必须即时处理,晚几毫秒都可能导致姿态计算出现误差。软件模拟时,我也跟着实现了一套分级中断机制,并且允许用户配置时间倍率。
为什么要特别讲时间对齐?因为模拟器如果把时钟速度调得过快,程序逻辑可能直接乱掉。我的模拟器在默认状态下,按照真实指令执行速率来驱动整个虚拟AGC,这样才能保证每个中断间隔内恰好处理完该处理的指令数。
有一次为了测试,我把时钟倍率调成64倍,结果屏幕上数字跳得飞快,程序却直接卡死。查了很久才发现,中断请求排队长度超过了程序预期,导致主循环永远在处理中断,根本没有时间来更新状态。后来我加了一条规则:中断频率的上限必须模拟真实系统,即使倍率调高,也只能加快“等待时间”,不能缩短单条指令的处理时间。这样才能保持行为一致。
3. 实操过程与核心环节实现
3.1 从零搭一个可运行的环境
实操第一步,把模拟器跑起来。不同人写的AGC模拟器安装步骤会略有差异,但总体套路大致相同。以我这边为例,底层依赖是SDL库和ncurses库,分别用来绘制DSKY面板和处理终端控制。
sudo apt-get install build-essential libsdl2-dev libncurses5-dev git clone <你的AGC模拟器源码地址> cd agc-simulator ./configure --prefix="$HOME/agc" make -j4 make install安装完成后,先别急着加载飞行程序。我建议先跑一遍自检模式。有些模拟器带Built-in Self Test,可以在没有外部程序的情况下,验证CPU指令、内存读写、DSKY输入输出是否正常。如果你顶着黑屏就直接加载程序,一旦出错很难判断是虚拟机的问题还是程序本身的问题。
export PATH="$HOME/agc/bin:$PATH" agc -self-test如果屏幕上打印出自检通过,就可以加载你手头的AGC二进制程序。常见的格式有两种,一种是原始的穿孔纸带二进制镜像,另一种是纯十六进制文本。我用的是后者,好处是方便用脚本快速检查特定地址内容。
3.2 花20分钟点亮一块虚拟DSKY屏幕
很多人以为,显示DSKY需要复杂的图形代码。其实只要你已经实现了DSKY的寄存器模型,点亮屏幕就只是一层渲染的问题。
我用的模拟器里,DSKY被抽象成了一个独立模块,对外暴露几个状态字段,比如当前Verb值、当前Noun值、程序号、报警灯状态。渲染线程只需要每帧读取一次这些字段,然后按布局画到窗口上。
写一个最小示例的话,流程大概是下面这样:
import agc_sim dsky = agc_sim.DSky() dsky.set_program(64) # 进入下降阶段程序编号 dsky.set_verb(6) # 动词:读取数据 dsky.set_noun(34) # 名词:当前高度 dsky.execute() # 触发执行 while not dsky.idle(): dsky.tick() render(dsky.snapshot())当你看到屏幕上先显示“06 34”对应关系,随后数字管跳出一个高度值,这一步就算正式告成。那一刻,你算是摸到了半个世纪前宇航员每天面对的那扇“窗户”。
这里有个细节值得留意:DSKY显示数值不是一次性全亮。真实硬件每次只刷新部分数字管,刷新顺序严格受程序控制。如果你的模拟器没有按这个顺序刷新,某些依赖固定刷新顺序的飞行程序就可能判断错误。所以,即便最终显示结果是对的,最好还是把刷新顺序也按照原始规则实现。
3.3 跑通一次完整的登月下降流程
DSKY点亮只是开始,真正的重头戏是让飞行程序动起来。
我用的是一套公开的登月舱下降程序镜像,加载后,DSKY上会显示程序号P63、P64、P66等阶段。这段程序包含了从点火到最终着陆的完整控制逻辑,真实历史上,登月舱在下降时会按顺序逐一启用这些程序阶段。
实操时,我先按步骤输入启动命令,然后观察DSKY上的数字变化。程序运行约几秒后,代表高度的数值开始明显下降,同时面板上的“姿态稳定”相关灯被激活。日志输出大概长这样:
TIG 073:42:10 BURN TIME 004:20 ALTITUDE 15000 FT VELOCITY 2100 FPS DSKY VERB 06 NOUN 34 -> 15000当然,这只是在解释器里跑历史代码,不是真实任务重演。它真正打动我的是:当我看着那些数字一点点变化时,突然意识到,当年工程师就靠着这么有限的算力和这么简陋的面板,完成了一次次惊心动魄的轨道计算。
整个流程里,最需要耐心的是程序状态切换的时机。有些阶段必须等中断标志位达到特定值,宇航员才能输入下一个Verb。如果你按快了,程序会拒绝执行。我的建议是,第一次跑的时候把日志打开,看到某个输入未被响应,先别急着反复按键,要顺着日志看清楚程序到底在等什么。
4. 常见问题与排查技巧实录
4.1 程序卡死在“待机”状态
这是我最常遇到的问题。现象是DSKY面板正常亮起,数字也有显示,但飞行程序始终停在某个阶段,不推进。
排查思路非常简单:检查中断定时器有没有启动。许多飞行程序在早期流程里就会开启一个间隔计时器,如果模拟器没有正确模拟这个时钟源,程序永远收不到“时间已到”的中断,自然只能干等。
我建议在调试器里设置一个陷阱,专门捕捉定时器寄存器的写入,然后确认写入的值有没有被换算成正确的中断周期。我遇到过一种情况,模拟器把定时器的节拍长度算错了,结果每次中断间隔都被拉长了256倍,导致程序看起来“慢吞吞”,几乎不动。
4.2 按键输入偶发跳变
现象是输入“06 34”时,显示有时变成“07 34”或“06 35”,原因多半是模拟器里的按键扫描没有做去重。
真实DSKY的键盘通过扫描矩阵读取状态,按住一个键时,在电气上有一定时间内的抖动。模拟器如果直接把“按键按下的那一帧”当成一次有效输入,那么在按键保持期间,扫描逻辑可能会多次读到同一个键值。解决办法是在模拟键盘模块里加入10到30毫秒的延迟确认窗口,只有电平稳定后才提交给AGC程序。
4.3 固定内存区域报校验错误
AGC的固定内存采用了一种带校验的存储技术,原始设计里如果内存内容读出后奇偶校验不对,会直接触发报警。我在模拟器里也复现了这种校验机制,结果有段时间程序总在某个地址报错。
查原因时发现,是启动脚本加载镜像时,把最后一行的数据长度对错了,导致内存镜像里的每个字都偏移了一位,奇偶位自然全部对不上。这类问题最隐蔽,因为肉眼很难看出数据错位。建议写一个校验脚本,加载镜像时先对全文长度求模,确认它和预期字数一致,再开始执行。
| 问题现象 | 常见原因 | 快速排查方法 |
|---|---|---|
| 程序卡在待机状态 | 定时器中断未启动 | 查看中断使能寄存器 |
| 按键数字跳变 | 缺少消抖处理 | 加10~30ms确认窗口 |
| 固定内存校验错误 | 镜像字节序或长度不对 | 校验文件大小后再加载 |
| DSKY数值不刷新 | 刷新时序未按硬件顺序 | 检查刷新寄存器流程 |
| 程序拒绝输入 | 当前阶段未开启对应Verb | 看日志找等待条件 |
5. 影响范围与扩展应用
5.1 在课堂教学里为什么受欢迎
我最初写模拟器只是想自娱自乐,但后来把它带入过几次线下分享,效果出乎意料。在计算机体系结构课或嵌入式入门讲座里,AGC是一个天然的教学样本:指令集小、内存模型直观、中断系统简单却完整。学生可以在几分钟内看懂一条指令的生命周期,再亲眼看到它如何影响屏幕上的数字,比干讲理论有趣得多。
更有意思的是,通过DSKY上的Verb/Noun交互,学生能理解“人机接口的边界”到底意味着什么。没有鼠标、没有图形界面,一切操作都必须翻译成短代码组合。这种极简交互反而让操作者对自己正在做的事高度聚焦,尤其适合用来讨论现代界面设计中的信息过载问题。
5.2 从复刻工程中学到的三件事
整个项目做下来,有三个方面对我个人触动最深。
第一,指令集不复杂,复杂度在于外围协同时序。真正的难点不是一条加法指令怎么写,而是如何在中断到来时,让主程序和中断服务程序之间互不干扰地合作。
第二,校验机制是救命的底线。原始AGC的固定内存有奇偶校验,一旦数据损坏就报警,绝不带伤运行。我们的现代系统动不动就自动恢复,反而让人对数据损坏缺乏警惕。看到老机器“宁可停,也不出错”的固执,挺值得反思。
第三,界面的克制是一种美德。DSKY的按键布局几十年后看,仍然算得上高效。它不做任何多余的事,只是把必要的信息用最直接的方式呈现出来。现在很多仪表盘塞满了动画和渐变色彩,我并不觉得那一定更好。
5.3 后面还能怎么扩展
模拟器跑通之后,可扩展的方向其实不少。比如把它打包成一个Web应用,让零安装的用户在浏览器里操作虚拟DSKY,这对线上教学极其友好。再进一步,可以加入现代数据源,比如利用GPS位置和开源地形数据,实时计算当前坐标和模拟高度,替换掉原本的固定脚本,让DSKY变成一块“给现代飞行器用的复古仪表”。
我还有一个半成品计划,就是给模拟器加一个WebSocket接口,让手机当作遥控键盘使用。手机上显示数字键盘,电脑屏幕上渲染DSKY面板,操作起来像真的在按那个年代的按钮。等项目再成熟些,我打算把它接进一个更大的开源复古计算合集里,让想了解的人一次就能体验到多台古董计算机。
我在实际调试中的最大体会是:不要被“老系统”三个字吓退。当年工程师能靠纸带和穿孔卡片把逻辑想清楚,我们今天至少有调试器、版本控制和几十年的编程经验,没理由搞不定它的软件模拟。只要你愿意先把机器跑起来,剩下的问题都可以一个一个拆出来看,最后收获得比当初预期的多得多。
最后分享一个我个人习惯的小技巧:每次启动模拟器后,先不着急加载飞行程序,而是执行一次全内存自检并保存日志快照。这样一旦后续调试把环境搞乱了,你至少有一份“已知正常”的基线比对,排查起来会舒服很多。