干这行久了很多东西尝个鲜就放下了但“Apollo-11”这个项目我前后磨了三版从纯软件模拟一路做到带实体按键的桌面小装置越做越上瘾。你可能觉得奇怪半个多世纪前的登月制导计算机和今天的开发环境完全不是一个物种折腾它到底图什么我的答案就一句那是人类用最有限的硬件条件硬生生解决了一个超高复杂度控制问题的巅峰样本。哪怕放到现在它身上可挖的东西依然比很多现代工程多得多。这个项目不是单纯做个怀旧皮肤而是把阿波罗登月任务的核心计算系统——包括那个只有2K内存的制导计算机常被简称为AGC和它的交互终端DSKY——用现代手段重新实现一遍。你双击可执行文件屏幕上会亮起那个标志性的数字键盘面板你在键盘上敲动词和名词代码就能切换登月程序、读取高度和速度你甚至可以完整跑一遍从进入月球轨道到最终着陆的下降流程看着燃料消耗和高度变化实时刷出来。适合谁对航天历史感兴趣的人、想理解“资源受限下如何设计系统”的嵌入式开发者、以及单纯想在桌面上拥有一台能玩的复古设备的人。不管你是哪一类这篇文章都能给你一套能落地的参考路径。1. 项目整体设计与思路拆解1.1 为什么选“登月计算机”作为复现目标先说一个背景阿波罗登月任务里真正让飞船安全落到月球表面的不是地面控制中心而是飞船上那台“小得可怜”的计算机。它要同时处理惯性测量数据、控制发动机推力方向、调整姿态、跟宇航员交互还得在内存几乎用尽的情况下保持系统不死机。这种“戴着镣铐跳舞”的设计方式放到今天是完全反直觉的——我们习惯了买大内存、堆算力但它证明了只要算法和架构足够精妙极低配置也能完成极高难度的任务。对我个人来说复现它的价值有三层理解实时系统的本质AGC不是跑Linux再装个什么框架它是单线程、轮询、直接操作寄存器的裸机程序。每一行指令都精确对应硬件行为没有任何多余的抽象。复现过程中你会被逼着去思考“这条指令到底被哪段电路执行”“这周期怎么对齐”。学习一种“语法式”人机交互设计DSKY没有屏幕菜单只有数字键盘和少量指示灯。宇航员通过输入“动词-名词”代码来指挥计算机。这种设计在信息密度极低的情况下依然能覆盖上百种操作场景现代嵌入式设备的人机交互设计中依然值得借鉴。把零散知识串成体系复现这个项目你至少要碰到内存映射、bank切换、汇编指令集、数值积分、姿态控制、开关电源信号处理这些领域。它们不是割裂的知识而是被一个真实任务串成的有机整体。换句话说这不只是写个模拟器更像一次系统性的“计算机考古”。你会重新理解计算机是怎么一步步走到今天的。1.2 关键路线取舍硬件还原还是软件模拟做这类复现项目第一个要拍板的问题就是走硬件路线还是软件路线。硬件路线的极致是用FPGA去还原AGC的原始逻辑电路或者用现代微控制器加实体按键、数码管做一个“桌面上能亮的DSKY”。优点当然是有实物感敲按键的反馈、指示灯闪烁的仪式感是屏幕模拟给不了的。但缺点也很明显调试成本高、周期长而且如果你只是想研究算法和人机交互硬件部分很容易喧宾夺主。软件路线则更轻量你可以用浏览器、Python、甚至一个MCU的裸机工程把核心逻辑跑起来。重点是“逻辑等价”而不是“电路等价”。用这种方式你可以专注分析AGC的内存布局、指令执行流程、登月下降算法而不用先被示波器和逻辑分析仪拖住。我自己最终采用的是“软件内核为主硬件面板为辅”的分层方案内核层用C语言模拟AGC的寄存器、内存池、指令执行循环保证指令级别兼容。交互层编写DSKY键盘事件处理逻辑把按键映射为动词-名词指令。显示层提供一个简易的图形界面模拟数码管显示和告警灯状态。可选外设层通过串口连接一个实体DSKY面板按键和指示灯是真实可用的。这个分层最大的好处是你可以先不管硬件把内核和算法跑通验证逻辑正确后再接实体面板。万一实体面板卡住了也不会导致整个项目瘫痪。很多模拟器项目失败就是因为在第一步就选了最重的方案结果连基础逻辑都没跑通就失去了动力。1.3 项目的总体架构设计以下是软件内核版的架构说明整个系统我分成了四个模块彼此之间用清晰的接口切分CPU模拟模块维护AGC的寄存器组如累加器、程序计数器、溢出寄存器等、随机存储器可擦写内存、只读存储核心内存的镜像逐条执行指令。输入输出模块处理DSKY的按键输入和灯阵状态。AGC和DSKY之间靠一组数据通路交互你需要按它真实的“输出通道”格式去模拟。轨迹计算模块通过惯性测量数据模拟数据可以是预设曲线或用户手动设置计算位置、速度、姿态并驱动着陆雷达的高度测量。任务调度模块模拟真实任务中“程序”的切换比如从轨道下降程序切到着陆减速程序。这里有一个容易被忽略的设计关键AGC的内存不是一块平坦的RAM而是分bank存储区的。代码从某个bank切到另一个bank需要额外的指令而且某些地址会因为bank切换而产生不同的含义。很多第一次做模拟器的人会在这一步被坑。所以模块设计时我特意把整个存储空间抽象成一个“带bank上下文”的数据结构而不是简单的数组。这一步代价不大但避免了后期无穷无尽的地址错乱问题。架构层面还有个小建议把内核和界面逻辑做彻底解耦。不要因为想快速看到效果就把按键处理和指令执行搅在一个函数里。真实情况是界面代码会频繁改动内核则保持稳定。如果你耦合在一起每改一次UI都要重新验证一遍核心逻辑效率会非常低。2. 核心细节解析与实操要点2.1 再看一次AGC的基本硬件结构想真正复现而不是“画个皮”你必须理解AGC的几个关键数字字长15位二进制另有1位符号位所以很多资料写“15位”或“16位”要看清。这意味着它的数值范围极窄整数大约是正负16000多内存地址空间也是按这个位数设计的。可擦写内存RAM只有2048字。这就是它存储变量的一切空间。具体又分成多个bank每个bank 256字程序区和数据区在不同条件下复用。只读内存ROM大约36K字。指令序列和常量都存在这里通过“核心内存”工艺做成本质上靠物理走线固化飞行途中不可修改。循环周期一条基本的加减法指令需要约12微秒更准确地说它有一个脉冲定时系统每个操作码都有固定的执行周期。在模拟器里你需要把“周期”抽象出来否则后面做实时性统计时会很被动。用现代计算机的视角看2K内存连一张普通图片都放不下但这台机器承载了登月全程的制导运算。它靠的不是大存储而是“精心设计的数据结构”。比如位置和速度变量不是用通用的浮点格式而是用一组定标小数类似定点数表示数值范围、精度都提前想好了。复现时我强烈建议你用定标小数而不是直接上浮点否则你根本无法量化它的精度损失也就看不懂它程序里那些补偿计算的由来。这一点在实际开发中特别容易踩雷现代程序员习惯了double会觉得“用浮点是天经地义”。但在模拟AGC时必须把数据类型当成“硬件的一部分”来模拟因为原始程序的每一步都是在跟定标误差较劲。跳过这一步你看到的就是一条条莫名其妙乘以常数的指令整个逻辑链会断掉。2.2 DSKY交互一场基于“动词-名词”的对话DSKY的全称翻译过来是“显示键盘”它没有鼠标、没有命令行只有一个数字键盘0-9、正号、负号、清除等三个程序状态按钮比如“功能键”之类一些显示数码管和告警灯它的和人交互的核心句式是“动词 名词”。举个例子动词代码比如“指定数据显示”不同版本编号略有差异名词用来指向对象比如“高度”或“速度”。宇航员先输入两位数字的动词代码再输入两位数字的名词代码然后按下执行键计算机就明白“把某个数据显示出来”或者“执行某项操作”。这种交互设计放在今天的视角依然很先进它用极少的硬件实现了语义分层——动词表示“做什么”名词表示“对什么做”通过组合可以覆盖大量场景。你甚至不需要理解底层寄存器就能设计出几十种指令组合。在复现时我建议你把DSKY的输入解析模块单独拆出来做一个有限状态机状态一等待动词输入最多两位数字状态二等待名词输入最多两位数字状态三确认执行或清除这个状态机不难写但它会引导你注意一个实际问题“DSKY的操作是异步的而AGC是同步执行的”。也就是说宇航员可能在计算机正忙时输入指令而计算机只有在一个程序循环的特定时间点才会去扫描输入通道。如果你的模拟器只是“来一个事件按一下处理”就无法还原真实任务中的排队和丢弃机制。2.3 最值钱的部分登月下降段的导航与控制思路如果说键盘交互是皮肉那登月下降程序就是骨架。真实的登月后期分了好几个阶段从月球轨道高度约100公里左右的近月点开始先用发动机减速进入下降轨道然后是“着陆减速段”逐步降低高度和水平速度最后是“最终逼近段”由宇航员透过舷窗观察月面选择着陆点配合计算机给出的引导信息手动或半自动操纵。这个过程里面有三个关键的算法问题状态估算航天器位置和速度不能直接测出来只能通过惯性测量单元IMU积分得到再通过地面测控或交会雷达修正。模拟时你可以用一组常微分方程来抽象描述飞船运动外加一些测量噪声然后让“机载计算机”用简单滤波真实AGC里也有延迟锁定环之类的方式做雷达测量处理去估算状态。这一步建议先用模拟数据把积分算对再加噪声。制导律设计发动机推力方向和大小需要实时调整让飞船按预定的高度-速度曲线落下去。上世纪那个时代还没有太多现代控制理论实际的制导逻辑非常依赖预先计算好的“标称轨迹”和偏差修正表。复现时你可以做一个简化的闭环按预设轨迹插值得到期望高度和速度再根据实际值和期望值的偏差输出一个修正加速度指令。燃料预算登月舱的燃料极其有限每个阶段的发动机开机时间都被严格规划。在模拟器里我会把燃料消耗量和推力积分挂钩一旦燃料耗尽就触发“引擎停火”状态你会发现飞船直接变成自由落体——这一步能极大增强项目的“紧张感”。实操建议不要一开始就试图完整复刻整套登月流程。先做一个简化版本——飞船从100公里高度开始下降发动机持续推动手动控制推力大小目标是让高度平滑降到0且速度也为0。把这条闭环跑通再逐步加引力场变化、姿态限制、雷达切换这些细节。3. 实操过程与核心环节实现3.1 环境准备与工具链选型我最终的软件核心里面用了C语言作为主力原因有两点一是C的指针和内存模型能很自然地映射AGC的地址空间二是后续想移植到单片机上驱动实体面板C几乎不需要改动。界面部分我用了简单的图形窗口框架只用到了最基础的文字渲染和绘图功能。如果你更熟悉Web技术也可以用浏览器作为界面内核通过编译成WebAssembly或干脆用JavaScript重写。重要的是内核和界面分离语言不是硬性门槛。工具链建议如下编译器本机C编译器GCC或Clang均可构建工具一个简单的Makefile或CMake脚本调试工具GDB或等价调试器版本管理随便用什么但建议早期就为“算法调整”单独开分支这里有必要再强调一次不要因为界面简单就跳过“开发时的观察工具”。你的模拟器需要能随时打印如下信息当前指令地址、当前bank、寄存器值、最近执行的几条指令。否则遇到bug时你只能像盲人摸象一样猜。我在第一版就是因为没有日志系统排查一个地址错乱问题花了整整两个晚上。3.2 用代码还原DSKY输入输出链路下面我用一个极简但结构完整的例子来说明DSKY输入处理的核心逻辑。这不是完整AGC模拟但展示了状态机思路。typedef enum { VERB_PENDING, NOUN_PENDING, EXEC_PENDING, IDLE } dsky_state_t; typedef struct { int verb; int noun; dsky_state_t state; } dsky_t; void dsky_init(dsky_t *dsky) { dsky-verb 0; dsky-noun 0; dsky-state VERB_PENDING; } int dsky_digit(dsky_t *dsky, int digit) { // 返回1表示产生了完整指令 switch (dsky-state) { case VERB_PENDING: dsky-verb dsky-verb * 10 digit; if (dsky-verb 10) { dsky-state NOUN_PENDING; } return 0; case NOUN_PENDING: dsky-noun dsky-noun * 10 digit; if (dsky-noun 10) { dsky-state EXEC_PENDING; } return 0; case EXEC_PENDING: // 此时更多输入可视为无效或重新开始 return 0; default: return 0; } }这个状态机最大的优点是“防呆”宇航员按错了键可以通过清除键重新开始不会有难以恢复的死锁。同时它精确地还原了“代码必须输满两位才进入下一状态”的约束。实际AGC的真实输入处理还要更复杂它要同时处理“程序中断”和“操作错误告警”但状态机主骨架不会变。对应地显示侧需要把“当前值”刷新到一组数码管上。我建议用一组“分段模拟”对象而非直接贴图片这样如果你以后接实体数码管只要替换渲染函数即可。3.3 实现登月下降段的轨迹计算登月下降的物理模型可以简化成二维或三维空间内受到月球重力吸引、同时受到发动机推力作用的问题。为了专注核心算法我从最简单的“一维垂直起降”模型开始逐步扩展到含水平速度的二维下降。一维模型下的动力学方程h(t) 表示高度向下为正v(t) 表示速度向下为正重力加速度 g 恒定简化发动机推力提供向上的加速度 a_engine它受燃料质量影响最朴素的计算循环如下# 简化的一维登月下降模拟Python示例 # 设定参数 g 1.62 # 月球重力加速度 m/s^2 max_thrust 30.0 # 最大推力加速度 m/s^2 fuel 1000.0 # 燃料剩余单位 fuel_rate 10.0 # 燃料消耗率跟推力成正比 h 20000.0 # 初始高度 米 v -50.0 # 初始速度负代表下降速度 m/s dt 0.1 while h 0: # 你想给多大推力这里简化为手动输入 throttle 0.3 # 0~1之间的油门指令 # 推力加速度不能超过最大推力 a_engine max_thrust * throttle # 更新加速度向上为正所以这里用减法 # 注意在向下为正的坐标系里重力加速度g是正的推力使v增速变缓 v_next v (g - a_engine) * dt h_next h v_next * dt # 计算燃料消耗并且推力随着燃料减少而下降 fuel - fuel_rate * throttle * dt if fuel 0: max_thrust 0 # 引擎没燃料了 v v_next h h_next print(f着陆时速度: {v:.2f} m/s, 剩余燃料: {fuel:.2f})这个例子虽然简单但它复现了核心逻辑推力越大减速越快但燃料消耗也越快如何在落地瞬间让速度归零本质上是“最优控制”的雏形。加二维化之后你需要维护两组状态量水平位置和速度、垂直高度和速度。发动机可以朝不同方向喷控制器必须同时修正水平速度为了不对准着陆点偏离太远和垂直下降率为了不砸到地面。我建议在实现时把控制器分解为两个独立的PID通道一个管高度变化率一个管水平漂移。你会发现当两个通道同时工作时耦合是个麻烦这在真实任务里也正是最考验制导逻辑的地方。3.4 把系统跑起来与验证方法把内核和DSKY拼起来后验证方法很重要。不能只看界面亮了就认为“项目成功了”应该按测试用例逐条验证基础指令测试加载一段预先写好的小程序按单步执行比对寄存器和内存值。内存越界测试故意访问非法bank看系统是否给出告警或保持静默这点和AGC的真实行为对照。场景测试从某个初始位置启动下降程序看高度曲线和速度曲线是否平滑是否出现“振荡”或“燃料突然耗尽”。交互测试模拟按键操作看看状态机能否正确处理错误输入比如动词只输入一位就按执行。我当时还做了一件事记录每一次运行的数据画成高度-速度曲线图。你会看到如果控制器增益调得太高曲线会在目标线附近反复穿越象征飞船“在空中抖动”如果增益太低则偏离目标线最后要么撞地要么燃料耗完才勉强减速。这种可视化方式比看一串数字直观太多能快速帮你调参。4. 常见问题与排查技巧实录4.1 内存bank切换导致的地址错位这是我在复现时踩的第一个大坑。AGC的程序存储区是分bank的每条跳转指令除了给出目标地址还可能隐含“下一条指令去哪个bank”。如果你的模拟器没有维护bank上下文指令很容易从错误的存储区取到垃圾数据表现是“程序飞了”界面随机亮灯。排查技巧给指令执行循环加“当前bank号”和“当前地址”的日志输出。一旦出现执行地址跳变到异常区域立刻回看之前执行的跳转指令基本都能发现是“隐含bank切换没生效”。解决方式就是严格按照硬件手册去实现那几条特殊的跳转指令而不是自己发明简化版。4.2 DSKY输入乱码和按键抖动实体按键存在抖动即一次物理按键会在电信号上产生多次脉冲软件不做处理就会一次输入多个数字。真实的DSKY电路有消除抖动的设计但你在自己的板子上必须额外处理。最简单的软件消抖方法是“延迟确认”检测到按下事件后等若干毫秒再读一次如果电平依然有效才接受。不要小看这几十毫秒在实时系统里它必须被纳入输入等待流程否则你会出现“想输12结果变成1122”的喜剧效果。4.3 模拟精度和速度的权衡如果你完全按真实AGC的周期来执行每一条指令在今天的电脑上依然能跑得很流畅但如果你在模拟器里又加入了实时轨迹计算、UI刷新就可能出现“内核很快、显示跟不上”的撕裂感。我用了一个全局时钟抽象内核每执行完“一条模拟指令”就累积模拟微秒数UI刷新时读取这个时钟到了某个阈值才重绘一次。这样显示逻辑和CPU执行节奏就脱钩了无论机器多快多慢显示频率基本稳定。4.4 常见问题速查表问题现象可能原因解决方向程序执行跳飞bank切换未正确实现检查跳转指令里隐含bank上下文是否更新输入两位数字后无反应状态机卡在等待名词状态检查当前状态和清除键逻辑确认“动词已满两位”标志是否提前触发高度曲线振荡减小不了控制器增益过高降低比例增益增加阻尼项观察稳定余量燃料消耗速度异常推力与消耗率换算不一致统一推力加速度和燃料消耗量的量纲界面刷新闪烁重绘时机不协调引入全局模拟时钟定时合并刷新实体面板按键偶发失灵按键抖动或电平不稳加入软件消抖和错误重读机制必要时加滤波电路这张表同样适用于你未来做任何“复古计算系统模拟”类项目。很多坑不是AGC特有的而是“模拟旧硬件”这件事本身的共性。5. 一些个人经验复现Apollo-11这类系统最让我受益的其实不是“我写出了多少行代码”而是被迫重新审视了很多现代开发中习以为常的“默认条件”。内存大了我们就不太关心变量布局CPU快了我们就不太在乎循环效率浮点精确了我们就不去想定标和误差。但AGC这个项目每一个节省都是被逼出来的每一步都是精算过的。如果你也想尝试我的建议是先定一个“能亮起来”的版本目标比如屏幕上出现DSKY面板敲三个键调用出一个程序显示一个飞速下降的高度数字。别看这个目标小它已经能帮你绕过最复杂的地基把整个闭环建立起来。一旦这个循环通了后面加什么功能都是水到渠成。另外一个小技巧留着折腾日志。不是记录“今天做了什么”而是当你踩完一个坑后用几行话把这个坑的起因、排查过程、最终修法写下来。这个动作对“将来维护项目”的作用远比你想象的大。我就是在一次版本迭代时因为没写日志改坏了一个本不该动的初始化函数然后对着正常代码改了半天才意识到问题。这个项目后续还能往哪个方向拓展你可以试着把轨迹算法换成更通用的形式比如加入随机风场干扰、测距仪噪声看看系统是否还能稳定着陆。甚至可以在模拟的DSKY上实现“手把手教学模式”——把每一步操作的含义解释弹出来变成一个面向大众的交互科普作品。祝你也早日造出自己桌面上那台“微型登月飞控”。