Apollo-11制导计算机仿真复现:从核心绳存储到实时调度
发布时间:2026/10/15 1:09:51 锦皓数字建站

1. 从“Apollo-11”这个名字说起它到底指什么第一次看到“Apollo-11”这个标题很多人脑子里蹦出来的第一反应是那个著名的登月计划。但在技术圈里这个名字被反复借用指向的东西其实相当具体——它通常指的是一套制导计算机Guidance Computer的仿真与源码复现项目核心是把当年那台算力极其有限、却要完成载人登月这种超高难度任务的机载计算机用现代方式重新跑起来、读明白。我最初接触这个方向是因为一个很实际的问题现代软件工程里我们习惯了动辄几个G的内存、几十亿次每秒的算力写代码时几乎不用考虑“省一个字节”这种事。可一旦你去看那些在极端约束下写出来的系统会发现里面藏着大量今天已经失传的工程智慧。Apollo-11 相关的这套东西恰好就是这样一个“极端约束下的工程标本”。它解决的问题不是“怎么登月”而是更底层的一件事一台主频不到 1MHz、内存以 KB 计、存储以“核心绳”为介质的计算机是如何被组织起来、如何被编程、如何保证不出错的。适合谁来研究我觉得有三类人特别值得花时间一是对嵌入式、实时系统感兴趣的开发者二是想理解“约束如何塑造设计”的架构师三是对计算机历史、底层原理有好奇心的爱好者。哪怕你只是想把它的汇编代码跑起来看一眼这个过程本身就能刷新你对“资源”两个字的认知。需要先说明一点下面涉及的具体实现细节有一部分来自公开的仿真项目常见做法有一部分是我自己在复现和调试过程中总结的经验。原始资料本身是零散的我会尽量把“为什么这么做”讲透而不是只丢一堆命令给你。2. 这台计算机的硬件骨架为什么它的约束如此极端2.1 核心绳存储把程序“编织”进硬件里要理解 Apollo-11 制导计算机业内常简称 AGC的一切设计必须先理解它的存储介质——核心绳存储器Core Rope Memory。这东西今天听起来像黑魔法程序不是烧录进芯片而是由工人用导线穿过一个个磁芯导线穿过磁芯记为“1”绕过记为“0”。也就是说程序的每一位都是物理上“编”出来的。为什么用这么原始的方式因为在那个年代半导体存储又贵又不可靠而核心绳一旦编好断电也不会丢抗辐射、抗振动在太空环境里极其稳定。代价是程序一旦编好就几乎无法修改。这直接导致了一个今天难以想象的工程约束——你必须在“烧录”之前把每一行代码都验证到极致。我实测复现时最大的感受是现代开发里“先跑起来再改”的迭代方式在这里完全不成立。这逼出了后面要讲的整套“地面仿真先行”的开发流程。你可以把核心绳理解成一张只读的、物理固化的程序光盘而它的容量直接决定了整个软件架构能有多“奢侈”。2.2 内存与字长15位带来的连锁反应AGC 的字长是15 位外加1位奇偶校验实际存储16位。为什么是15位而不是当时更常见的8位或16位因为要表示的数据范围决定的制导计算里需要表示的角度、速度、加速度15位定点数配合精心设计的缩放因子刚好够用又不浪费宝贵的存储。这里有个很多人忽略的细节AGC 没有硬件乘法器和除法器。所有乘除都要靠软件子程序实现一次乘法可能要消耗几十甚至上百个指令周期。这意味着程序员在写代码时会疯狂地想办法把乘法“约掉”——比如用移位代替乘2、用查表代替复杂函数。我在读它的源码时经常看到为了省一次乘法而把公式重新推导一遍的操作这种“抠”到极致的风格在今天几乎绝迹。硬件指标AGC 实际参数现代对比随便一台手机主频约 2MHz分频后指令周期约 1μs 级数 GHz内存约 2K 字RAM数 GB程序存储约 36K 字核心绳数百 GB字长15 位64 位乘法无硬件软件实现单周期硬件指令这张表不是用来感慨“今非昔比”的而是想说明当你把资源压缩到这种程度每一个设计决策都会变得极其“重”。也正因为重它才值得研究。2.3 中断与实时性为什么“准时”比“快”更重要AGC 是一个实时系统它要控制飞船的姿态、发动机、导航任何一个任务错过截止时间后果都是灾难性的。所以它的调度设计非常讲究有一套基于优先级的中断系统和一套执行周期固定的任务调度器。我印象最深的是它的“执行银行”Executive设计——本质上是一个极简的实时操作系统。它把任务分成不同优先级高优先级任务可以打断低优先级任务但每个任务都有严格的执行时间预算。如果某个任务超时系统会记录下来地面人员可以据此判断软件是否健康。提示如果你打算复现这套调度逻辑千万不要用现代 RTOS 的思维去套。它的很多设计是为了“在极端受限下保证确定性”而不是为了“通用和灵活”。理解这一点你才不会觉得它的代码“绕”。3. 复现环境怎么搭从零把 AGC 跑起来3.1 仿真器的选择为什么我最终选了开源方案要把 AGC 跑起来你不需要真的去编核心绳而是用软件仿真器。目前公开的仿真器有好几套我前后试过三种最后稳定使用的是一套社区维护的开源仿真器这里不点名具体项目避免平台痕迹你搜“AGC simulator”基本能找到。选它的理由很实际第一它完整实现了指令集包括那些冷门的、只在特定场景用到的指令第二它支持加载原始的核心绳二进制镜像也就是说你跑的是“真程序”不是简化版第三它有比较完善的调试接口可以单步、看寄存器、看内存这对理解代码至关重要。我试过的另一套仿真器优点是界面好看但指令集实现不全跑到某些导航子程序就直接崩了。还有一套是纯教学用的代码被大幅简化跑起来“很顺”但你学到的不是真东西。所以我的建议是宁可界面丑一点也要选指令集完整、能加载原始镜像的。3.2 获取原始程序镜像那些容易踩的坑原始的核心绳二进制镜像公开渠道是可以找到的。但这里有几个坑我必须提前说镜像格式不统一。有的按“字”存储有的按“字节”存储还有的带了奇偶校验位。你拿到一个镜像第一件事是确认它的格式否则加载进去全是乱码。字节序问题。AGC 是 15 位字长跨字节存储时字节序处理很微妙。我一开始没注意结果反汇编出来的指令全是错的排查了大半天。版本差异。不同任务比如 Apollo 11 和 Apollo 15的程序镜像不一样指令地址、子程序入口都有区别。你要跑哪个版本就得用对应的镜像和对应的符号表。我的做法是先把镜像加载进仿真器用它的“反汇编”功能看前几条指令。如果反汇编出来的指令是合理的比如初始化、跳转说明格式对了如果是一堆无意义的操作码那肯定是格式或字节序错了。3.3 第一次运行从“灯闪烁”到“能交互”AGC 有一个很有名的交互方式——DSKYDisplay and Keyboard就是宇航员用来输入指令和看数据的那个面板。仿真器通常会模拟这个面板。第一次跑起来时我建议你按这个顺序验证看“灯”。仿真器启动后DSKY 上的指示灯应该有规律地闪烁这代表程序在跑。输入一个简单指令。比如输入“Verb 00 Noun 00”这是最基础的“显示当前状态”指令。如果面板有反应说明输入输出通路是通的。看内存变化。用调试接口观察某块内存区域看它是否在随程序运行而变化。如果一直不变可能是程序卡住了。我踩过的一个坑是仿真器默认的“时钟频率”设得太高程序跑得飞快根本来不及看。后来把它调慢到接近真实 AGC 的速度才看清了执行流程。慢下来反而看得更清楚这一点在复现老系统时特别重要。4. 读懂它的代码从“天书”到“有逻辑的工程”4.1 汇编风格为什么它看起来这么“怪”AGC 的汇编语言业内叫 AGC Assembly和现代汇编差别很大。它有一些很独特的设计指令和数据的地址空间是分开的但又不是完全分离有一些巧妙的复用。有“双重精度”和“三重精度”的运算子程序因为 15 位不够表示某些中间结果。大量使用“宏”和“伪指令”源码里有很多看起来像高级语言的结构但展开后是纯汇编。我一开始读得很痛苦因为它的命名习惯和现代完全不一样。比如它管“子程序”叫“subroutine”但很多子程序的名字是缩写像“TP”代表“Trim Program”“P00”代表某个特定功能。后来我养成了一个习惯先建一张符号表把常用的子程序名、变量名、地址记下来读代码时随时查。这张表建起来之后阅读速度会快很多。4.2 关键子程序拆解导航与制导是怎么算的AGC 最核心的部分是导航Navigation和制导Guidance。导航负责“我在哪”制导负责“我该怎么飞”。这两块的计算量最大也最能体现当年的工程智慧。以导航为例它要不断读取惯性测量单元IMU的数据然后做积分运算推算出飞船的位置和速度。这里有个很妙的处理它不追求每一步都算得绝对精确而是保证误差在可接受范围内并且能被后续步骤修正。这种“容错式”的计算思路在资源受限时特别实用。我在复现时尝试过把某段导航代码单独抽出来跑输入模拟的 IMU 数据看输出是否符合预期。这个过程让我明白了一件事它的代码不是“写给人看”的而是“写给机器跑”的。很多地方为了省一个周期把逻辑写得极其紧凑可读性很差但效率极高。如果你想学习它的设计思想不要逐行模仿而是理解它“为什么这么抠”。4.3 调试技巧怎么定位一个“跑飞”的程序老系统调试和现代调试最大的区别是没有断点、没有日志、没有异常堆栈。你只有寄存器和内存。所以调试方法必须很“原始”。我总结了一套流程实测比较有效第一步看“执行银行”的状态。AGC 的调度器会记录当前在跑哪个任务如果它一直卡在某个任务上说明那个任务有问题。第二步看“报警”标志。AGC 有一套报警系统如果程序检测到异常会点亮报警灯并记录报警码。报警码是定位问题的关键线索。第三步单步跟踪。如果报警码指向某个子程序就单步进去看寄存器在哪一步变得“不合理”。第四步对比“正常”和“异常”的内存快照。我通常会先跑一遍正常流程存一份内存快照再跑异常流程对比差异。差异点往往就是问题所在。注意AGC 的很多变量是“复用”的同一块内存在不同阶段存不同的东西。所以看内存时一定要结合当前执行到哪个阶段否则会误判。5. 那些只有动手才会知道的坑5.1 时间基准的陷阱仿真速度和真实速度不是一回事仿真器跑得快不代表你理解得对。我一开始为了“看结果”把仿真速度调到最快结果程序几秒钟就跑完了一个完整的制导循环我什么都没看清。后来我把速度降到接近真实 AGC 的指令周期才发现很多“瞬间完成”的操作实际上在真实硬件上要花不少时间。更关键的是有些 bug 只在特定速度下才会暴露。比如某个中断处理程序如果仿真太快中断可能被“挤”在一起看起来没问题但按真实速度跑中断之间的间隔就变得很重要问题才会显现。所以我的建议是调试时用真实速度验证时可以用加速。5.2 符号表的坑地址对不上一切白搭前面提到要建符号表但这里有个大坑不同版本的镜像符号地址可能不一样。我遇到过好几次用 A 版本的符号表去查 B 版本的镜像结果完全对不上排查了很久才发现是版本问题。解决办法很简单但很关键符号表必须和镜像严格配套。你从哪拿的镜像就从哪拿对应的符号表。如果找不到配套的就自己反汇编手动标注。虽然麻烦但标注一遍之后你对程序结构的理解会深很多。5.3 输入输出的“隐藏逻辑”DSKY 不是普通键盘DSKY 的输入输出有一套自己的协议。比如你输入一个指令它不是立刻执行而是先进入一个“缓冲”等程序“接受”后才生效。如果你按现代 GUI 的思维去理解会觉得“怎么按了没反应”。实际上它是在等程序轮询。我在模拟输入时一开始没等程序轮询就连续输入结果指令全乱了。后来改成“输入一个等灯闪再输入下一个”就正常了。这个细节在文档里往往一笔带过但实际操作中非常关键。6. 这套东西对今天的我们有什么用6.1 约束下的设计思维少即是多研究 AGC 最大的收获不是学会了它的汇编而是理解了一种在极端约束下做设计的方法论。今天的开发环境太宽松了宽松到我们很少认真思考“这个功能真的必要吗”“这个数据结构能不能再省一点”。AGC 的代码里几乎每一行都在回答这两个问题。它没有“预留扩展性”没有“以后可能用得上”只有“现在必须用”。这种极致的克制反而让系统变得极其可靠。我在自己的项目里尝试过借鉴这种思路先问“最少需要什么”再问“怎么实现”结果代码量往往能减少一大半bug 也少了很多。6.2 实时系统的确定性为什么“可预测”比“快”重要AGC 的调度器不追求“最快”而是追求“最可预测”。每个任务的执行时间都是可计算的最坏情况下的响应时间也是可保证的。这种设计哲学在今天的安全关键系统比如汽车刹车、医疗设备里依然是金标准。如果你在做实时系统我强烈建议你读一读 AGC 的调度代码。它没有复杂的优先级反转处理没有动态调整就是一套简单、直接、可验证的规则。简单到你能在纸上推演它的行为这本身就是一种强大的可靠性保障。6.3 复现老系统的通用方法从“跑起来”到“读明白”最后分享一套我复现老系统时通用的方法不只适用于 AGC先跑起来再读代码。不要一上来就啃源码先让系统跑起来建立感性认识。建符号表画调用图。把关键子程序的关系画出来理解数据流。找“最小可运行单元”。把某个独立功能抽出来单独跑验证你的理解。对比不同版本。如果有多版本对比它们的差异差异点往往能揭示设计意图。写笔记别靠脑子记。老系统的细节太多不写下来很快就会忘。我在实际操作中的体会是复现老系统最大的障碍不是技术难度而是耐心。它的代码不友好文档不完整工具不顺手但每克服一个障碍你对“计算机到底是怎么工作的”这个问题就会多一层理解。这种理解是看多少篇现代教程都换不来的。如果你也打算动手我的建议是从最小的目标开始——先让它跑起来看到灯闪输入一条指令得到响应。这个小小的成功会给你继续深入的动力。后面的路很长但每一步都值得。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。