如果一台计算机坏了,常规思路是插上调试器、翻日志、重启、换内存。但面对 Voyager 1 FDS Computer Emulator 这类项目时,情况完全不同:你要模拟的是旅行者 1 号飞船上的飞行数据子系统(FDS),它远在两百多亿公里以外,信号单程延迟超过二十个小时,飞船上也没有第二块可替换的 FDS 硬件。人类最后一次亲手接触这套计算机,还是发射前在地面测试的时候。
也就是说,FDS 一旦出现异常,地球上的人能做的只有一件事:在另一台机器上尽可能忠实地复现它的行为,先在模拟环境里把所有可能操作都试一遍,再把最有把握的指令序列发到深空。
我的核心判断是:这类模拟器真正的价值,并不是把一台几万年前的古董航天计算机“跑起来”带来的情怀满足,而是给一个无法物理访问的深空系统,补上了可调试、可回归、可验证的影子环境。没有这套影子环境,远程修复就像蒙着眼睛修一台不能断电的服务器。
1. 为什么今天还有人要模拟一台飞了四十多年的航天计算机
1.1 FDS 是旅行者号的“声音”和“前台”
要理解 FDS 模拟器的意义,得先知道 FDS 在整艘飞船上负责什么。
旅行者 1 号不是一台计算机,而是一整套系统的组合。飞船上的各个子系统负责姿态控制、推进、能源、科学观测,而 FDS 承担的是飞行数据管理:它把科学仪器和工程传感器产生的数据收集起来,按一定格式组织成遥测帧,再交给通信系统发回地球。同时,地面发送的上行指令也会通过飞船的其他部分,最终作用到各个子系统上。
你可以把 FDS 理解成飞船的“前台接待”和“新闻发言人”。它不一定决定飞船往哪飞、拍什么照片,但地球上的人能不能看懂飞船的状态,完全取决于 FDS 是否正常工作。如果 FDS 出了问题,飞船本身可能还活着,能源和推进也都正常,但地面收到的信号会变成无意义的比特流。
这正是旅行者 1 号这类任务最棘手的地方。一台设备坏了,你不能打开机箱去换芯片,也不能像维护地面服务器一样远程重装系统。所有修复动作,都只能通过无线电指令完成。
1.2 模拟器解决的是“看不到、摸不着、不敢试”的问题
真实系统不可访问,这正是 FDS 模拟器存在的理由。
和普通软件调试不同,向深空飞船发指令有三个约束:
- 时间成本极高。一次指令往返需要几十个小时,你不可能快速试错。
- 指令风险不可逆。一条错误的指令,可能让飞船进入更糟糕的状态,甚至让本就微弱的通信彻底中断。
- 状态信息不完整。地面收到的遥测是经过编码和压缩的,你看到的飞船状态,天然带有延迟和不确定性。
在这种情况下,模拟器成了唯一允许你“随便折腾”的沙盒。你可以先在模拟器里加载一段疑似有问题的内存镜像,观察程序会跳到哪里;也可以先把一条修复指令在模拟器里执行一遍,看它会不会破坏关键数据;甚至可以故意注入错误,验证飞船的看门狗和错误处理程序能不能把系统拉回来。
所以,模拟器不是怀旧玩具,而是远程修复的安全网。
一个值得注意的事实是,这类模拟项目通常不会从一开始就追求 100% 的硬件级精度。更常见的做法是先模拟核心指令和内存行为,再用历史遥测数据逐步校准,让模拟器从“能跑”变成“可信”。
2. 要模拟 FDS,得先把一台航天计算机拆成四层
FDS 不是我们熟悉的 x86 或 ARM 机器,它是一台专门为深空任务设计的定制计算机。因此,模拟它时不能直接套用现成的 CPU 模拟器,而是要先把它拆成几个层次,分别建模。
2.1 指令系统层
第一层是指令系统。
你需要知道 FDS 的指令集是什么样,比如它有哪些寄存器、程序计数器怎么处理、内存是字节寻址还是字寻址、有没有条件跳转、有没有中断指令。
这些信息通常来自历史文档。但现实是,半个世纪前的工程资料未必完整,不同版本之间也可能有差异。所以我在看到一个复古计算机模拟项目时,第一反应不是去看模拟器代码,而是先找资料清单:指令集手册、内存映射、遥测帧格式、地面软件参考实现,这些缺一不可。
在没有完整资料的情况下,可以先用一个最小解释器把程序跑起来。下面是一个非常简化的结构,只用来展示模拟器的入口,不代表真实 FDS 指令集:
# 示例结构:极简解释器的入口 # 这不是真实 Voyager FDS 指令集,只展示模拟器骨架 class FdsMachine: def __init__(self, mem_size): self.mem = [0] * mem_size self.pc = 0 # 程序计数器 self.regs = [0] * 8 # 通用寄存器 self.running = True def fetch(self): opcode = self.mem[self.pc] self.pc += 1 return opcode def execute(self, opcode): # 按 opcode 分发到具体执行逻辑 # 例如 load, store, add, branch 等 pass def run(self): while self.running: opcode = self.fetch() self.execute(opcode)这样的骨架虽然简陋,但已经具备一个模拟器最核心的部分:取指、执行、更新状态。后续要做的,是往这个骨架里填入真实指令语义。
2.2 处理器状态与存储层
第二层是处理器状态和存储。
模拟器不能只关心“指令能不能执行”,还要精确表示寄存器、内存、堆栈、程序计数器,以及它们之间的边界。尤其要注意的是,老式航天计算机往往采用自定义字长和寻址方式,不能想当然地按 8 位字节处理。
下面是一个示例建模项,不代表真实 FDS 硬件细节:
| 建模对象 | 作用 | 模拟器中的实现 |
|---|---|---|
| 寄存器组 | 保存计算中间结果 | 一个列表或对象数组 |
| 程序计数器 | 指向下一条指令地址 | 整数类型,需要支持回绕 |
| 主内存 | 存放指令和数据 | 根据字长选择数组类型 |
| 堆栈区 | 保存子程序调用现场 | 地址范围和栈指针寄存器 |
| 状态标志位 | 记录进位、零、溢出等条件 | 布尔值集合或位掩码 |
| I/O 映射 | 访问外部设备和遥测接口 | 内存映射或专用指令 |
很多模拟器项目最后跑不起来,问题不是出在指令实现上,而是出在内存边界和字长上。例如某个地址范围是只读的,模拟器却允许写入;某个寄存器只有低 12 位有效,模拟器却按完整 16 位去计算。这类细节一旦错了,后面所有验证都会失真。
2.3 外设与遥测链路层
第三层是外设和遥测链路。
FDS 不是孤立运行的,它要接收传感器数据、响应时钟中断、把遥测帧交给调制器。模拟器需要为这些外部输入建立接口。
这一层可以模块化处理:
- 输入模块:模拟科学数据和工程数据源。
- 遥测帧格式器:按固定格式组装字节。
- 输出缓冲区:把帧写入一个 FIFO 缓冲区,模拟器可以读取。
- 中断控制器:在特定事件到来时触发中断。
这里的难点是,外设的时序和指令执行是并行的。CPU 执行指令是一回事,遥测数据到达是另一回事。模拟器如果只按单线程循环执行,可能永远无法复现真实的竞态问题。
2.4 任务环境与故障层
第四层,也是区分“玩具模拟器”和“任务支持模拟器”的一层:任务环境与故障注入。
真实飞船上,软件不是在一个干净环境里运行的。辐射可能造成内存位翻转,电源波动可能引发复位,指令上传时可能正好撞上看门狗超时。这些异常在普通模拟器里通常被忽略,但对于验证远程修复策略来说,恰恰是最需要模拟的。
所以,一个成熟的 FDS 模拟器往往包含故障注入框架。它允许你在指定地址翻转一个 bit、在某条指令后暂停、模拟电源跌落触发复位,然后观察系统能不能恢复。
3. 从零搭建一个最小可用的 FDS 模拟器
如果你也想尝试做一个 FDS 模拟器,我的建议是:先不要追求硬件级还原,也不要一上来就研究复杂的遥测格式。把目标设定为“能跑通一个最小流程”,先把骨架搭出来,再逐步逼近真实系统。
3.1 先做最小指令集解释器
第一步,实现一个最小指令集解释器。
这一步的目标不是完整,而是“能跑”。你先定义一套简单的指令格式,包括数据加载、存储、算术运算、跳转等少数几条指令,然后把它们做成可执行的 Python 类或 C 程序。
关键要保证三个能力:
- 每执行一条指令后,寄存器、内存、程序计数器状态可观察。
- 可以单步执行,方便调试。
- 可以在任意地址设置断点,遇到断点后停止。
有了这些,你才算拥有了一个“显微镜”。后续不管是对齐 memory map,还是分析一段奇怪的历史程序,都要靠这架显微镜。
3.2 加上可观测性
很多模拟器项目死在“黑盒”状态:程序跑起来了,但是你看不到内部变化,出了问题也没法定位。
所以第二步是把模拟器的观测能力建好。常见做法包括:
# 示例命令结构 trace regs dump mem 0x0000 0x0100 break 0x1234 step 20这些命令的核心逻辑,本质上就是读取模拟器内部状态并打印出来。别小看这一步,没有可观测性的模拟器,后期调试成本会非常高。
3.3 对齐内存和外设接口
第三步,把内存和外设映射补齐。
此时你需要一份尽可能准确的内存布局材料。下面仍然是一个示例,不代表真实 FDS 内存图:
| 地址范围 | 模拟对象 | 说明 |
|---|---|---|
| 0x0000 — 0x3FFF | RAM | 程序和主要数据 |
| 0x4000 — 0x4FFF | I/O 寄存器 | 遥测输入输出接口 |
| 0x5000 — 0x5FFF | 遥测帧缓冲区 | 待发送数据 |
| 0x6000 — 0x6FFF | 只读程序区 | 固定代码和常量 |
在模拟器里,你需要保证内存访问在这个映射规则下工作。越界访问、只读区域写入,都应该产生某种告警,这样你才能尽早发现程序异常。
给外设建接口时,我建议使用“寄存器读写回调”的模式。处理器往某个 I/O 地址写数据,会触发一个回调函数,由这个函数完成外设行为。这样指令执行和外部事件是解耦的,后续也更容易加入中断和时序。
3.4 用历史数据和故障场景做回归
第四步,建立回归测试集。
这一步才是让模拟器变得可信的关键。单次跑通,只能说明流程没有断;真正有参考价值的是,模拟器在面对已知输入时,能产生和历史上一致或逻辑一致的输出。
一个可行的流程是:
- 收集地球和飞船之间实际传输的遥测帧样本。
- 把这些样本拆成“输入数据 + 期望输出”的测试用例。
- 在模拟器上运行同一段程序,比对输出。
- 每次修改模拟器后,跑一遍全部回归用例,防止旧行为被破坏。
如果手头没有真实遥测帧,也可以先从简单的人工测试开始:构造一段已知程序,在模拟器上执行,检查关键寄存器和内存变化是否符合预期。至少保证模拟器自身逻辑可靠。
3.5 一个可以复用的五步搭建框架
综合起来,从零搭建 FDS 模拟器可以按这个顺序推进:
- 最小指令集解释器:先让指令能被执行。
- 调试观测层:加单步、断点、寄存器转储和内存快照。
- 内存和外设映射:让寻址和 I/O 行为更像真实机器。
- 遥测回归集:用历史数据或人工测试锁住行为。
- 故障注入与指令序列回放:让模拟器能演练修复操作。
这个顺序不是随意排列的。每一步都建立在前一步的验证基础上,哪怕中间发现资料不足,也可以退回到上一步继续调试。
4. 最容易被低估的是“真实一致性”
许多模拟器都能跑,但跑出来的结果未必可信。真正考验项目水平的,不是 CPU 模拟得对不对,而是模拟器能不能在关键边界条件上保持一致。
4.1 时序和中断问题
指令级模拟器通常只关心“每条指令执行后状态如何”,但真实系统是并行运行的:中断随时可能到达,外设随时可能更新数据,看门狗计时器一直在走。
如果模拟器忽略了中断优先级、时钟频率和看门狗超时,那么在模拟器里看起来正常的修复指令序列,真实飞船上可能会因为一次中断抢占而完全不同。
所以在模拟器里,至少要为这些事件建模:
- 时钟中断:多久触发一次,中断服务程序做什么。
- 看门狗:多长时间没喂狗会触发复位,复位后程序从哪个地址启动。
- I/O 事件:遥测数据何时就绪,外设状态何时变化。
时序建模不一定要做到逐周期精确,但至少要能覆盖“关键事件可能发生”的情况。
4.2 可信度验证的四个层次
我一般把模拟器可信度验证分成四个层次:
| 验证层次 | 含义 | 验证方法 |
|---|---|---|
| 指令级 | 每条指令执行结果是否正确 | 用已知指令序列做单元测试 |
| 程序级 | 一段完整程序能否跑出预期流程 | 对比寄存器、内存、分支路径 |
| 遥测级 | 模拟器输出帧格式是否和真实遥测一致 | 用历史遥测帧做比对 |
| 操作级 | 模拟器能否复现一次修复操作的前后状态变化 | 回放上行指令序列并检查状态转移 |
越往后,验证成本越高,但对任务支持的参考价值也越大。如果只是学习,验证到指令级和程序级基本足够;如果真的想用模拟器帮助分析故障,至少要达到遥测级。
4.3 常见问题排查链路
模拟器运行出问题时,不要东一榔头西一棒子地乱试。按下面这条链路排查,通常更快:
- 看现象:程序卡死、无输出、内存异常、寄存器跳变到错误值。
- 看取指:程序计数器是否落在合法入口地址,还是跳到了未初始化区域。
- 看内存:镜像是否完整,加载地址和程序期望的地址是否一致。
- 看外设:程序有没有初始化 I/O 寄存器,外设是否处于正确状态。
- 看中断:中断是否被正确触发,中断服务程序是否被执行。
- 看输出:遥测帧的字节序、位序、同步字是否和预期一致。
举个例子,如果模拟器启动后没有任何输出,不要先怀疑遥测格式器写错了。先检查程序计数器是否停在某个死循环里,再检查它是不是压根没执行到输出那段代码。很多看似复杂的模拟器问题,最后都出在“程序入口不对”这种最初级的地方。
4.4 资料的残缺和模拟器的边界
做 FDS 模拟器还有一个避不开的困难:资料不完整。半个世纪前的硬件手册可能丢失,某些寄存器的行为只能靠行为反推,不同文档之间甚至存在矛盾。
遇到这种情况,我有两个判断:
- 第一,模拟器永远只是“对真实系统的一种近似”,不是真实系统本身。
- 第二,模拟器通过了测试,不等于真实飞船会按同样方式运行。它的价值更像是一个高置信度的决策辅助工具,而不是绝对可靠的预言机。
所以,如果你的模拟器能在大多数场景下给出合理行为,同时能清楚标注哪些参数来自可靠文档、哪些来自推测,它就已经很有价值了。
5. 从深空模拟器到现代工程:一套可迁移的远程修复方法论
FDS 模拟器看似只和航天历史有关,但它背后代表的工程方法,在现代软件开发中其实随处可见。
5.1 先影子验证,再下发指令
远程修复深空探测器的流程,本质上和现代软件发布流程很像:
- 在一套影子环境里加载新指令序列。
- 观察影子系统的状态变化。
- 确认没有安全风险后,把指令序列翻译成真实上行命令。
- 发送到真实系统,再通过遥测确认结果。
这和灰度发布、预发布环境、金丝雀发布背后的思路是一致的:你不能拿生产环境直接做实验,你要先在足够接近生产环境的地方验证,再决定是否真正影响线上系统。
FDS 模拟器的独特之处在于,它连“生产环境”都无法物理访问,所以影子验证几乎是唯一的选择。这也让它的工程方法论变得极端清晰:模拟不是目的,安全地改变真实系统状态才是目的。
5.2 把故障注入变成日常测试能力
另一个容易忽略的点是,故障注入应该成为模拟器的常规测试能力,而不是偶尔用一次的手工操作。
在模拟器里建立故障注入框架后,你可以编写类似下面的逻辑:
# 故障注入示例:在指定内存地址翻转一个 bit def inject_bitflip(machine, addr, bit): machine.mem[addr] ^= (1 << bit) # 示例:翻程序计数器附近的一个字节,观察系统是否会被看门狗拉回 inject_bitflip(machine, 0x0000, 3) machine.run() check_system_state(machine)这样的能力放在真实飞船上是不可能随意做的,但在模拟器里却是完全可行的。它能帮助你回答一个关键问题:这个系统在最坏情况下,能不能自愈。
如果你是在维护某个现代服务,也可以借鉴这个思路。主动在预发布环境中注入超时、异常、网络分区,再观察系统如何恢复,能让你的系统比“只做正常路径”可靠得多。
5.3 对嵌入式和复古计算开发者的启发
对于做嵌入式、模拟器、数字孪生或者复古计算的开发者来说,FDS 模拟器是一个极好的长期练习项目。
它需要你同时具备三层能力:
- 计算机体系结构:理解指令集、地址映射、中断、外设。
- 系统工程:在资料不全的情况下做合理性假设和交叉验证。
- 软件工程:把模拟器做成可测试、可回归、可观测的工具,而不是一次性脚本。
这三个能力在普通项目里不一定有机会同时练习。而 FDS 模拟器天然要求你同时面对这三者,这也是它迷人的地方。
5.4 适合谁做、不适合谁做
一个很现实的问题是:这类项目适合所有人吗?我的答案是否定的。
| 适合做 | 不适合做 |
|---|---|
| 对计算机体系结构有扎实基础的人 | 想快速获得可视化成果的人 |
| 愿意花时间考据历史文档的人 | 讨厌整理资料、反复比对数据的人 |
| 接受“模拟器是近似工具”的人 | 期望模拟结果 100% 等于真实硬件的人 |
| 能长期维护测试集和回归用例的人 | 只对“跑通一个 demo”感兴趣的人 |
如果你只是想了解旅行者 1 号的历史,看纪录片可能更高效。但如果你真正想做的是理解一台定制计算机如何工作、如何从残缺资料中反推出行为模型,那么 FDS 模拟器会是很适合你的方向。
我的建议是,不要一开始就想着复现整个飞行任务。第一周的目标可以只是:一条指令能够被正确取出、执行、改变寄存器。先让最小的机器转起来,再问它当年在深空里到底经历了什么。