提起“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面板操作起来像真的在按那个年代的按钮。等项目再成熟些我打算把它接进一个更大的开源复古计算合集里让想了解的人一次就能体验到多台古董计算机。我在实际调试中的最大体会是不要被“老系统”三个字吓退。当年工程师能靠纸带和穿孔卡片把逻辑想清楚我们今天至少有调试器、版本控制和几十年的编程经验没理由搞不定它的软件模拟。只要你愿意先把机器跑起来剩下的问题都可以一个一个拆出来看最后收获得比当初预期的多得多。最后分享一个我个人习惯的小技巧每次启动模拟器后先不着急加载飞行程序而是执行一次全内存自检并保存日志快照。这样一旦后续调试把环境搞乱了你至少有一份“已知正常”的基线比对排查起来会舒服很多。