旅行者 1 号上的 FDS 计算机模拟器,听起来像是一小撮航天爱好者的自嗨,但真正上手以后你会发现,它比跑一个大模型或者调一个视频渲染管线更能逼你理解“计算机到底是怎么工作的”。FDS 的全称是 Flight Data System,中文通常叫飞行数据系统,它是旅行者 1 号上负责汇总科学仪器和工程遥测数据、再把数据组织成适合下传格式的专用计算机。这篇文章写给三类人:对模拟器实现感兴趣的人、想搞清楚航天器老软件怎么跑的人、以及单纯想找一个既能动手又不吃显卡的硬核项目的人。我会先讲 FDS 的角色,再讲模拟器怎么跑起来、怎么验证行为对不对,最后把最容易踩的坑和实用扩展一起说清楚。
1. 先搞清楚 FDS 是什么,才知道模拟器要模拟什么
1.1 FDS 在旅行者 1 号里的角色
旅行者 1 号不只是“一个探测器”,它身上有好几台计算机,分别负责不同的工作:有负责指令处理和执行控制的,有负责姿态控制的,还有负责数据处理和遥测下传的。FDS 就是数据处理这一环的核心。探测器上的科学仪器会把数据交到 FDS,FDS 按照地面设计好的格式,把这些数据打包成遥测帧,再交给通信系统发回地球。换句话说,FDS 就是这艘飞船的“数据打包中枢”。
这一点直接决定了模拟器的边界。你在本地模拟 FDS,不是在模拟整艘飞船,而是在模拟一个输入输出非常固定的专用处理器:它有特定的指令集,有特定的内存布局,有特定的外部设备接口。知道了这个定位,你就不会一上来就指望它去渲染 3D 画面或者跑 Linux。它要干的事情比这些窄得多,但也严谨得多。
1.2 它跟普通计算机的差异很大
FDS 不是我们熟悉的 x86、ARM 这种通用 CPU。公开资料显示,它是一台用定制逻辑电路搭出来的计算机,字长、寄存器和寻址方式都与现代 CPU 差别很大。存储器用的是当年航天器上常见的非易失性存储方案,优点是掉电不丢数据,缺点是容量非常小,和现在动辄几个 GB 的内存完全不是一个量级。指令集也很精简,没有缓存,没有分支预测,没有多级流水线,执行模型比现代 CPU 直白得多。
这个特点对模拟器开发者其实是个好消息。硬件越简单,模拟器的状态越可控,寄存器、内存、程序计数器、中断标志这些要素很容易在一开始就全部列清楚。难的反而是“模拟得准不准”——你是不是真的理解了每条指令对标志位的影响,是不是把外部设备触发中断的时序也模拟进去了。这也是后面验证环节的核心。
1.3 为什么现在还有人写这种模拟器
原因有三类。第一类是为了研究历史任务。旅行者 1 号已经飞了几十年,很多地面硬件早就退役了,只有把 FDS 的行为用软件模拟出来,才能继续分析它传回来的数据、回放当年出现过的异常。第二类是为了教育。用一台没有任何系统软件、不能安装任何框架的处理器来学习汇编、理解计算机组成原理,反而比在复杂平台上更容易讲清楚。第三类纯粹是兴趣,复古计算和航天历史交叉的项目本来就不多,FDS 模拟器属于那种“能讲出很多故事”的代码仓库。
我个人的建议是:如果只是想体验一下,先跑通最小例子就行;如果想长期维护,就要把指令集表、内存映射和遥测帧格式整理成文档,否则过两周你自己都看不懂写过的逻辑。
2. 把 FDS 模拟器跑起来,需要准备哪些条件
2.1 硬件门槛比想象中低
很多人一听“模拟器”就以为需要大内存、好显卡,其实完全不是。FDS 是一台老式定制计算机,主频非常低,内存也很小。对现代电脑来说,模拟它连 CPU 的零头都用不到。实测下来,普通办公笔记本、几年前的 i3 或者 i5,只要能正常跑编译工具链,就不会有性能瓶颈。唯一要注意的是不要在模拟器外面再套一层杂七杂八的运行时,减少干扰。
这里的判断标准很简单:如果编译和启动都很快,说明机器性能足够了。真正消耗时间的是调试过程,是反复比对指令行为、内存变化和输出帧格式,而不是 CPU 算力。所以不要用“性能不够”当借口拖着不开始。
2.2 软件环境需要编译器、终端和基础调试工具
按照常见模拟器项目的惯例,这类工具大多用 C 或者 C++ 实现,因为要在命令行里模拟寄存器和内存,用这些语言最直接,也最容易跨平台。Linux、macOS 和 Windows 上的 WSL 环境都可以编译运行。需要准备的东西基本就是一套 C/C++ 编译环境、make 或者 cmake 这类构建工具,再加一个能查看输出日志的终端。
这里要特别说清楚一个容易混淆的点:网络上经常出现的 “install android emulator hypervisor driver” 和 FDS 模拟器没有任何关系。它不是 Android 模拟器,也不需要安装 Hypervisor 驱动,更不需要开启 VT-x、AMD-V 这类硬件虚拟化支持。同样,它也不是 Locale Emulator 那种改系统区域设置的本地化工具,更不是 Moxa 那种工业串口终端模拟器。FDS 模拟器的本质是逐指令解释执行,不依赖硬件虚拟化,所以不用担心嵌套虚拟化、BIOS 设置这些事。
在开始之前,建议先确认三件事:第一,git 能不能正常拉取代码;第二,编译器版本是不是项目要求的最低版本;第三,终端里能不能干净地运行 make。这三件事都通过了,再进入下一步。不要一上来就关防火墙、改系统配置,那种折腾通常和本项目无关。
2.3 输入文件:程序镜像、测试程序和格式说明
一个 FDS 模拟器要真的跑起来,通常需要三类输入文件:
| 输入文件 | 作用 | 常见格式 |
|---|---|---|
| 程序镜像 | 模拟 FDS 存储介质里的软件内容 | 二进制文件、十六进制文本 |
| 测试程序 | 验证 CPU 指令执行是否正确 | 汇编源码或预编译镜像 |
| 格式说明 | 用于验证输出数据字段和校验 | 文档、结构化描述文件 |
这里有个容易误会的点:模拟器仓库里不一定自带完整的 FDS 飞行软件镜像。受历史资料完整度影响,很多项目会提供测试程序,让你先验证 CPU 行为,再逐步加载真实工作负载。如果仓库里没有给出镜像文件,不要急着到处找,先用它自带的测试用例跑通,这样才能确认模拟器本身没有大问题。
注意:下载外部镜像文件时,只从项目作者指明的来源获取,不要随便点击来路不明的打包文件。这既是安全问题,也是数据可信度问题。
3. 最小运行流程:从编译到输出第一帧数据
3.1 编译和启动
假设项目已经克隆到本地,第一件事永远是读 README。不同项目的构建方式不一样,有的用 make,有的用 cmake,有的干脆提供一个编译脚本。以最常见的构建方式为例,流程大概是先执行构建命令生成可执行文件,再运行可执行文件并指定一个程序镜像,最后在终端或输出文件里查看结果。
不要跳过“先编译再运行”这个顺序。直接去看源码当然也可以,但先从可运行状态开始,能让你后续调试时有稳定的基线。我一般会先跑一遍项目自带的测试命令,确认所有测试通过,再手动加载测试镜像。这一步看起来简单,但能省掉后面很多“到底是我改坏了还是它本来就这样”的疑惑。
3.2 单条程序验证
跑单条程序是验证模拟器最有效的方式。可以选择一段非常简单的指令序列,比如把某个寄存器写入一个已知值,再把它存储到内存地址 A,最后把内存地址 A 的内容通过输出接口打印出来。运行结束后,看输出值和预期是否一致。
这里的关键是“预期结果”要能提前算出来。汇编级代码的每条指令影响哪些寄存器、哪些标志位,是可以在纸面上推的。你先把预期写在纸上,再去看模拟器执行结果。如果一致,说明指令主路径没问题;如果不一致,就把范围缩小到具体指令和具体标志位,逐条排查。
3.3 观察输出并判断是否成功
FDS 的最终输出是遥测帧。不同实现里,输出方式可能是控制台打印十六进制数据,也可能是生成一个输出文件。判断成功标志通常有三条:帧头是否符合格式、数据长度是否和预期一致、帧校验是否通过。
不要只看“程序没崩、有输出”就认为成功了。一个模拟器能跑完一段代码,和它能把 FDS 的真实行为模拟准确,中间隔着一大截。就像一个人能背诵代码和能正确执行代码是两回事。我把这个过程叫“先用简单输入建立信任,再用复杂输入验证边界”。
4. 怎么验证模拟器行为是对的
4.1 寄存器、内存和程序计数器的三件套检查
现代程序调试依赖断点和日志,老式计算机模拟器反而更朴素。最常用的验证手段就是盯着三样东西看:寄存器、内存、程序计数器。每执行完一条指令,程序计数器是否跳到正确地址,寄存器是否更新成预期值,内存是否按照寻址方式写入了正确内容。这三项都对了,这条指令的执行就基本正确。
实际调试中,建议给模拟器加一个单步模式,或者至少能打印每条指令执行前后的 CPU 状态。没有这个能力,遇到复杂指令组合时会非常痛苦。你可以先不追求“一步到位”,只要能一条一条指令地走,再配合状态打印,已经能覆盖大部分问题。
4.2 校验遥测帧比看日志更靠谱
日志只能告诉你程序流程走到了哪里,遥测帧校验才能告诉你数据处理逻辑对不对。FDS 模拟器跑完以后,输出数据会按照遥测帧格式组织。你需要检查帧同步码、副帧标识、数据字段长度、校验和这四类信息,它们任何一项不对,都意味着模拟器或者输入程序有问题。
这个环节也最容易暴露出“假成功”。有的模拟器为了演示效果,会直接透传一段固定数据,看起来有输出,实际没有执行任何 FDS 逻辑。所以拿到输出以后,先从格式上做验证,再尝试修改输入数据,看看输出是否会跟着变化。能变化,才有可能是真的在执行。
4.3 指令级差异对比
如果项目提供了参考实现,或者你能找到关于 FDS 指令集的公开资料,指令级对比是最好的验证方式。做法是:把同一段测试程序分别跑在参考实现和你自己的理解上,对比每一步的寄存器差异。第一次对比很可能有差异,这很正常,关键是要能解释每一个差异的来源。
这里要多说一句:不要迷信“官方模拟器”这种说法。航天项目内部使用过的软件,受限于历史资料和公开程度,很多细节需要靠逆向工程和交叉验证。你能拿到的最可靠证据,是当时留存的格式说明、遥测记录和项目文档,而不是某个博客或者论坛帖子。凡是没法用文档解释的结论,都先存疑。
5. 老式航天计算机模拟最容易踩的坑
5.1 时序和中断是最阴的坑
现代 CPU 模拟器往往忽略精确时钟周期,但对 FDS 这类和外部设备强耦合的机器,中断时序不能随便忽略。磁带上记录的数据、无线电信道的时序、仪器数据到达的窗口,都会通过中断或者外部信号影响程序执行顺序。如果模拟器把中断触发时间写得不准确,程序可能偶尔跑对,偶尔跑错,而且错误很难复现。
遇到“有时对有时错”的问题,第一反应不要怀疑编译器或者数学库,先看中断系统。把外部事件触发记录打印出来,对比时间顺序和中断响应状态。很多看似随机的失败,其实是中断处理函数在错误的时间修改了共享内存。
5.2 字长、字节序和位操作
老式定制计算机的字长不一定按 8 的倍数来设计,字节序和位序也可能和 x86 的习惯不同。模拟器最常见的一类错误,就是实现者用现代 CPU 的假设去处理老机器的数据。比如把 16 位读成两个 8 位,然后没有注意高低字节的排列顺序,结果帧结构解析出来全是乱的。
我的建议是:在模拟器内部始终用一个明确的“机器字”类型,并且把所有输入输出数据的字节序转换收敛到一个单独的模块。不要在业务逻辑里到处写移位和掩码操作,那样后续排查起来会非常痛苦。先定好内部表示,再处理外部数据,这个顺序不能反过来。
5.3 文档缺口导致验证困难
老航天器的资料不可能像现在的 SDK 一样完整。指令集可能有,但某些时序细节、特殊寄存器的副作用、外部设备的确切行为,可能没有公开文档。这时候你能做的是:把不确定的地方明确标注出来,用已知的数据点去反向验证,而不是靠猜补全。
如果项目仓库里已经有 TODO、FIXME 或者 issue 列表,先看一遍。很多前人踩过的坑都写在里面。不要重复发明轮子,也不要觉得“既然别人遇到了问题,我大概也会遇到,干脆绕过”。直接了解问题边界,能帮你省几天时间。
5.4 常见现象与排查顺序
| 现象 | 优先排查方向 | 说明 |
|---|---|---|
| 程序直接崩溃 | 镜像文件格式、入口地址 | 很多启动崩溃不是指令错误,而是镜像没加载对 |
| 输出全为 0 | 输入数据、字节序 | 数据没有被正确读取到内存,或者位序反了 |
| 输出偶尔对偶尔错 | 中断时序、共享内存 | 先打印中断触发记录,再看程序计数器跳转 |
| 校验始终不对 | 帧格式、校验算法 | 字段长度和离线文档逐项核对 |
| 速度突然变慢 | 日志输出、循环未退出 | 检查是否陷入了死循环,而不是性能问题 |
不要一上来就开并发、开批量。有些开发者在拿到模拟器以后,第一反应是“我要批量跑所有测试用例,顺便开多线程”。这个方向不能说错,但顺序错了。在第一阶段,你应该让所有任务串行执行,并且保留完整日志。批量化和并发的收益很后期才能体现,而它引入的时序问题、资源共享问题会立刻掩盖模拟器本身的错误。
对比一下就知道:串行模式下一个用例失败,你能直接看到它的寄存器状态和输出日志;并发模式下二十个用例一起跑,某个失败可能被其他输出淹没。先把单条链路调到相对稳定,再考虑吞吐。这个原则不只在 FDS 模拟器上适用,处理任何批量任务都一样。
6. 模拟器跑通之后,还能往哪个方向延伸
6.1 用于航天历史研究和科普
模拟器最大的价值之一是让普通人亲眼看到 FDS 的执行过程。你可以把一次完整的遥测数据打包流程拆成一步一步的指令展示,配合寄存器状态变化,让读者理解航天器上的计算机到底在做多么精确的事情。这类内容在科普文章、线下工作坊和课程设计里都很受欢迎。
而且这类项目非常适合做可视化。因为硬件状态有限,你可以直接把寄存器、内存和程序计数器画在同一个页面上,每一步高亮变化的字段。比起现代 CPU 复杂到无法完整可视化的状态,FDS 这种简单机器能把“程序在执行”这件事讲得非常直观。
6.2 接入现代遥测解码链路
如果模拟器的接口做得足够干净,它还可以作为遥测解码链路的前端。具体做法是把模拟器的输出从终端打印改成文件输出,或者提供简单的管道接口,再交给现代的数据分析工具做进一步处理。这样你就能用现代编程语言分析历史格式,而不必把所有解析逻辑都塞进模拟器内部。
这个方向要注意接口稳定性。先定义好输出格式,比如每条遥测帧占几行、字段怎么分隔、校验结果怎么标注,再写下游解析脚本。如果输出格式随时变,下游脚本就会跟着崩,最后你会花大量时间在“对齐格式”而不是“分析数据”上。
6.3 反哺模拟器工程实践
最后也是我比较推荐的方向,就是把这个项目当成一个完整的软件工程练习。文档怎么整理、测试怎么设计、指令集怎么分层、状态怎么打印、怎么让每次运行结果可复现,这些问题在一台简单的机器上练习,成本远比在大项目里低。等你把 FDS 模拟器的调试流程走完一遍,再回头去看其他模拟器,很多概念都会变得很清晰。
我自己在调试这类老计算机模拟器时的习惯是:先确定一个最小可验证目标,比如“让加法指令正确更新标志位”;然后写一个只覆盖这个目标的测试;测试通过以后,再扩展下一个目标。整个过程不要贪多,一次只验证一件小事,最终把所有小事叠加起来,才敢说这个模拟器基本可信。
我自己的经验是,像 FDS 这种老计算机模拟项目,真正花时间的不是“让代码跑起来”,而是“让行为可验证”。你越早把指令集表、寄存器定义、内存映射和输出帧格式整理清楚,后面的调试就越轻松。如果只是学习,跑通用例就够了;如果要做长期研究,就要把输入镜像的来源、版本和已知问题全部记录在案。先把单任务跑稳,再考虑扩展,这条路径对 FDS 模拟器,对大部分模拟器项目,都适用。