尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

用Python实现Voyager 1 FDS模拟器:探索18位CPU指令集与工程实践

用Python实现Voyager 1 FDS模拟器:探索18位CPU指令集与工程实践 每一台飞行了近半个世纪的航天器背后都有一群工程师用最原始的方式守护着它。当 Voyager 1 在距离地球超过 240 亿公里的深空传回异常数据时地面上的人无法去“维修”只能用一台模拟器去复现那台在太空中已经运行了 46 年的计算机然后逐行排查它的“大脑”到底哪里出了问题。这就是 Voyager 1 FDS Computer Emulator 真正的价值所在。本文将围绕 Voyager 1 的飞行数据子系统FDS计算机模拟器展开内容分为三部分首先介绍 FDS 是什么、为什么需要模拟器然后分析 FDS 的硬件架构和模拟器核心原理最后用一个可运行的 Python 示例带你手写一个简化版 FDS 模拟器。无论你是对航天工程感兴趣的开发者还是嵌入式、复古计算领域的爱好者都能从中获得一套完整的思路。1. 背景与核心概念1.1 什么是 Voyager 1 FDSFDS 是 Flight Data Subsystem 的缩写中文通常叫“飞行数据子系统”。它是旅行者 1 号探测器上负责数据采集、格式化和编码的核心计算机。可以这样理解探测器上的各种科学仪器——磁强计、宇宙射线探测器、等离子体探测器等——采集到的都是原始电压信号或计数数据。这些数据必须经过 FDS 的采集、量化和编码才能变成一个个数据帧经由无线电信标发回地球。换句话说FDS 是探测器上的“数据处理中枢”。从硬件上讲FDS 并不是我们熟悉的 x86 或 ARM 处理器它是一台基于 TTL 逻辑芯片构建的定制计算机。它没有操作系统没有内存管理单元甚至没有我们习以为常的字节概念。它的字长是 18 位内存规模只有几千个“字”。在这样一个资源极度受限的平台上工程师们用汇编语言编写了全部飞行软件并且这套软件在发射前写好后基本没有机会做大规模改动——因为硬件在太空中软件也只能通过无线电波一点一点地上传补丁。更关键的是FDS 从 1977 年发射至今已经在太空中连续运行了超过 46 年。2023 年 11 月它开始向地面传回完全无法理解的重复数据——修复团队不得不一边分析有限的遥测信息一边在地面搭建 FDS 的模拟器用来验证每一个可能的修复方案。1.2 为什么需要 FDS 模拟器直接操作真实硬件是不可能的因为 Voyager 1 远在太阳系边缘。工程师们唯一能做的就是通过深空网络DSN向探测器发送指令然后等待数十小时才能收到响应。这种场景下模拟器成为唯一的“试验场”为了复现故障工程师需要在地面搭建一台与 FDS 硬件行为一致的模拟器为了测试修复代码他们必须在模拟器上验证内存写入、指令执行、数据帧格式是否正常为了理解 46 年前编写的汇编代码模拟器可以提供逐指令跟踪、内存转储、断点调试等现代工具。所以Voyager 1 FDS Computer Emulator 并不是一个玩具项目而是一套严谨的硬件模拟工程。它需要模拟 CPU 指令集、内存映射、I/O 寄存器、时序行为甚至要模拟硬件故障的状态。1.3 模拟器与仿真器的区别在进入技术细节之前有必要区分两个容易混淆的概念模拟器Emulator侧重于“行为等价”。模拟器接受相同的输入产生相同的输出但内部实现可以完全不同。它通常运行在宿主机器上逐条解释目标机器的指令。仿真器Simulator侧重于“过程等价”或“数学等价”。仿真器会对系统的物理过程、数学模型进行近似计算。比如模拟飞行器空气动力学的飞行仿真器。在 FDS 场景中两者都会用到。行为级的指令模拟器用于验证软件逻辑如果涉及硬件时序、信号电平就可能需要更底层的仿真模型。本文讨论的是前者——通过指令级模拟来还原 FDS 的软件行为。2. 环境准备与版本说明在开始编写 FDS 模拟器之前先明确开发环境和工具链。2.1 开发语言选择本文示例使用Python 3.8。选择 Python 的原因很简单语法简洁适合表达寄存器和内存操作自带argparse、logging等库方便编写调试工具便于快速迭代和可视化。如果你更熟悉 C/C也可以用 C 实现同样逻辑性能会更好但代码量会明显增加。对于学习 FDS 模拟器思想Python 是最合适的。2.2 必需工具工具用途说明Python 3.8运行模拟器代码本文示例基于 3.83.10 亦可文本编辑器或 IDE编写代码VSCode、PyCharm 均可Git版本管理建议为模拟器项目创建仓库十六进制编辑器可选查看内存转储可用xxd或 VS Code Hex 插件版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.3 项目结构规划推荐的项目目录如下voyager_fds_emulator/ ├── emulator/ │ ├── __init__.py │ ├── cpu.py # CPU 状态与指令执行 │ ├── memory.py # 内存模型 │ ├── instruction.py # 指令集定义 │ └── fds.py # 模拟器主循环 ├── programs/ │ ├── hello.asm # 示例汇编程序 │ └── loader.py # 加载与转换脚本 ├── tests/ │ └── test_cpu.py # 单元测试 ├── main.py # 模拟器入口 └── README.md这个结构把 CPU、内存、指令集分离便于后续扩展。模拟器是工程软件结构清晰比“能跑”更重要。3. FDS 硬件架构拆解在编写模拟器之前必须清楚 FDS 的硬件架构。虽然我们无法拿到全部官方图纸但公开资料足够我们建立一个合理的模型。3.1 CPU 核心定制 TTL 处理器FDS 的 CPU 并不使用现成的商用处理器芯片而是用 TTL 逻辑芯片搭出来的。这种设计在 20 世纪 70 年代的航天器中很常见因为商用微处理器在当时还不够可靠也没有经过航天级验证。TTL 处理器最重要的特征是固定指令集和固定字长。FDS 的每个“字”是 18 位。这个 18 位字长很特殊它比 16 位多出 2 位可以用来做奇偶校验、符号位扩展或额外的状态信息。从模拟器的角度我们不关心 TTL 门电路的具体实现只关心程序员视角的寄存器与内存。3.2 寄存器组基于公开资料和逆向工程的共识FDS 至少包含以下几类寄存器程序计数器PC指向当前指令所在的地址累加器AC存放算术逻辑结果变址寄存器B有时叫 X用于变址寻址指令寄存器IR存放当前正在执行的指令状态寄存器SR记录进位、零、溢出等标志位。需要强调的是不同资料对寄存器名称的记载有差异。模拟器设计时我们应该把寄存器抽象成一组变量而不是在硬件细节上较真。关键是要保证指令行为正确。3.3 内存组织FDS 的内存以“字”为单位。每个字 18 位所以内存可寻址空间不是 2^8 256 字节而是 2^? 个字。实际 FDS 的存储容量非常有限以千字K words为单位。模拟器中内存最简单的表示方式是一个整数列表memory [0] * MEMORY_SIZE每个元素是一个 18 位无符号整数。为了模拟真实行为我们需要在写入时做掩码操作确保值不超过 18 位范围。3.4 指令集特征FDS 的指令集是定制设计的。虽然难以确认全部操作码但可以确定它包含以下几类指令加载/存储类将内存值读入寄存器或将寄存器写入内存算术类加法、减法、逻辑与、逻辑或、移位等跳转类无条件跳转、条件跳转I/O 类读取遥测输入、写入下行数据缓冲区控制类停机、中断处理、状态切换。由于 18 位字可以被拆成 3 个 6 位字段一种常见的指令编码方式是字段 16 位操作码字段 26 位寄存器或寻址模式字段 36 位地址或操作数这种编码方式在模拟器中非常容易实现。3.5 寻址模式FDS 支持的寻址模式至少包括直接寻址指令中的地址就是操作数所在的内存地址间接寻址指令中的地址指向一个内存单元该单元存放实际地址变址寻址用变址寄存器加上指令中的偏移量得到最终地址。模拟器实现时需要用一个函数resolve_address(raw_addr, mode, index_reg)来处理这些模式。4. 模拟器核心原理4.1 指令周期模型所有 CPU 模拟器的核心都是“取指—译码—执行”循环。对于 FDS 模拟器这个循环可以表示为取指Fetch从PC指向的内存地址读取一个字的指令译码Decode从 18 位字中拆出操作码、寄存器字段、操作数字段执行Execute根据操作码执行相应操作并更新寄存器更新 PC除非是跳转指令否则 PC 自增 1。在 Python 中这个循环可以写成while not halted: ir memory.read(pc) opcode, reg, operand decode(ir) pc execute(opcode, reg, operand, pc) cycles 14.2 寄存器与内存模拟用一个类来管理 CPU 状态是模拟器最自然的实现方式class FDS_CPU: def __init__(self, memory_size4096): self.memory [0] * memory_size self.pc 0 self.ac 0 self.b 0 self.ir 0 self.sr 0 self.halted False为了让代码更接近硬件可以使用属性来定义 18 位字MASK_18 (1 18) - 1 def set_ac(self, value): self.ac value MASK_18这样可以保证模拟器不会出现 18 位溢出更接近真实硬件行为。4.3 指令译码器指令译码器是整个模拟器的“翻译官”。它把 18 位指令拆分成操作码和操作数def decode(instruction): opcode (instruction 12) 0x3F # 高 6 位 reg_field (instruction 6) 0x3F # 中 6 位 operand instruction 0x3F # 低 6 位 return opcode, reg_field, operand这个拆分方式并不一定与真实 FDS 完全一致但作为模拟器学习项目它是清晰且可扩展的。真实实现中需要参考具体图纸或逆向工程报告对字段宽度进行调整。4.4 寻址处理寻址是模拟器中最容易出错的部分。我们用一个函数来统一处理def resolve_address(self, operand, mode): if mode ADDR_DIRECT: return operand elif mode ADDR_INDIRECT: return self.memory[operand] elif mode ADDR_INDEXED: return (operand self.b) MASK_18 else: raise ValueError(fUnknown addressing mode: {mode})这里把寻址模式放在reg_field的低几位中。具体编码规则可以根据项目需求定制。4.5 I/O 与外设模拟真实 FDS 通过 I/O 接口读取科学仪器数据、写入遥测帧。模拟器中可以通过外部回调函数模拟def io_read(self, port): # port 0: 模拟遥测输入 # port 1: 读取地球指令 ... def io_write(self, port, value): # port 1: 输出下行数据帧 ...在调试时我们可以让 I/O 函数打印信息或者把输出写入文件作为验证模拟器正确性的依据。5. 完整实战编写一个最小 FDS 模拟器现在进入动手环节。我们实现一个最小但可运行的 FDS 模拟器支持加载、存储、加法、跳转、停机等基本指令并提供调试输出。5.1 创建项目结构首先创建项目目录和包文件mkdir voyager_fds_emulator cd voyager_fds_emulator mkdir emulator programs tests touch emulator/__init__.py5.2 定义指令集与常量在emulator/instruction.py中定义指令操作码# 文件路径emulator/instruction.py FDS 简化指令集定义 # 操作码 HLT 0x00 # 停机 LDA 0x01 # 加载到累加器 STA 0x02 # 存储累加器到内存 ADD 0x03 # 加法 SUB 0x04 # 减法 JMP 0x05 # 无条件跳转 BZ 0x06 # 累加器为零时跳转 OUT 0x07 # 输出字符/调试 # 寻址模式 ADDR_DIRECT 0 ADDR_INDIRECT 1 ADDR_INDEXED 2 # 18 位掩码 MASK_18 (1 18) - 1 # 内存大小 MEMORY_SIZE 4096 def make_instruction(opcode, mode, operand): 将操作码、寻址模式、操作数打包成一个 18 位指令字 return ((opcode 0x3F) 12) | ((mode 0x3F) 6) | (operand 0x3F)5.3 实现内存模块emulator/memory.py负责管理内存读写# 文件路径emulator/memory.py 内存模型以 18 位字为单位的线性内存 from .instruction import MASK_18 class Memory: def __init__(self, size4096): self.size size self.cells [0] * size def read(self, address): if not (0 address self.size): raise IndexError(fMemory read out of range: {address}) return self.cells[address] def write(self, address, value): if not (0 address self.size): raise IndexError(fMemory write out of range: {address}) self.cells[address] value MASK_18 def load_program(self, program, start0): 将指令列表写入内存返回结束地址 for i, instr in enumerate(program): self.write(start i, instr) return start len(program) def dump(self, start0, endNone): 内存转储便于调试 if end is None: end self.size for addr in range(start, end): print(f{addr:04o}: {self.cells[addr]:06o})这里使用八进制打印地址和值是因为 18 位字最自然的切分方式是 3 个 6 位段而八进制每一位正好表示 3 位二进制非常契合 FDS 这种 6 位字段的指令格式。5.4 实现 CPU 与主循环emulator/cpu.py是模拟器的心脏# 文件路径emulator/cpu.py FDS CPU 模拟器核心 from .instruction import ( HLT, LDA, STA, ADD, SUB, JMP, BZ, OUT, ADDR_DIRECT, ADDR_INDIRECT, ADDR_INDEXED, MASK_18 ) class FDS_CPU: def __init__(self, memory): self.memory memory self.pc 0 self.ac 0 self.b 0 # 变址寄存器 self.ir 0 # 指令寄存器 self.sr 0 # 状态寄存器 self.halted False self.cycles 0 def resolve_address(self, operand, mode): if mode ADDR_DIRECT: return operand elif mode ADDR_INDIRECT: return self.memory.read(operand) elif mode ADDR_INDEXED: return (operand self.b) MASK_18 else: raise ValueError(fUnknown addressing mode: {mode}) def decode(self, instruction): opcode (instruction 12) 0x3F mode (instruction 6) 0x3F operand instruction 0x3F return opcode, mode, operand def step(self): if self.halted: return # 取指 self.ir self.memory.read(self.pc) self.pc (self.pc 1) MASK_18 self.cycles 1 # 译码 opcode, mode, operand self.decode(self.ir) # 执行 if opcode HLT: self.halted True elif opcode LDA: addr self.resolve_address(operand, mode) self.ac self.memory.read(addr) elif opcode STA: addr self.resolve_address(operand, mode) self.memory.write(addr, self.ac) elif opcode ADD: addr self.resolve_address(operand, mode) result self.ac self.memory.read(addr) # 18 位溢出处理 self.ac result MASK_18 if result MASK_18: self.sr | 0x01 # 进位置 1 elif opcode SUB: addr self.resolve_address(operand, mode) result self.ac - self.memory.read(addr) self.ac result MASK_18 elif opcode JMP: addr self.resolve_address(operand, mode) self.pc addr MASK_18 elif opcode BZ: if self.ac 0: addr self.resolve_address(operand, mode) self.pc addr MASK_18 elif opcode OUT: # 将累加器作为 ASCII 字符输出 # 这里用 chr 可能产生不可见字符所以也打印数值 print(fOUT: ac{self.ac} ({chr(self.ac) if 32 self.ac 127 else ?})) else: raise ValueError(fUnknown opcode: {opcode:#04x}) def run(self, max_cycles10000): while not self.halted and self.cycles max_cycles: self.step() if self.cycles max_cycles: print(警告达到最大循环次数模拟器停止可能存在死循环)这里有几个细节值得注意模拟器使用 MASK_18处理 18 位溢出模拟硬件截断行为进位标志存放在sr最低位加入max_cycles限制防止死循环导致程序卡死OUT 指令是调试用的简化实现真实 FDS 的 I/O 要比这复杂得多。5.5 组装模拟器入口emulator/fds.py把 CPU 和内存组装起来# 文件路径emulator/fds.py Voyager 1 FDS 模拟器组装 from .memory import Memory from .cpu import FDS_CPU from .instruction import MEMORY_SIZE class FDS_Emulator: def __init__(self, memory_sizeMEMORY_SIZE): self.memory Memory(memory_size) self.cpu FDS_CPU(self.memory) def load_program(self, instructions, start0): return self.memory.load_program(instructions, start) def run(self, max_cycles10000): self.cpu.run(max_cycles) def dump_registers(self): print( FDS 寄存器状态 ) print(fPC {self.cpu.pc:04o}) print(fAC {self.cpu.ac:06o}) print(fB {self.cpu.b:06o}) print(fIR {self.cpu.ir:06o}) print(fSR {self.cpu.sr:04o}) print(fCycles {self.cpu.cycles}) print(fHalted {self.cpu.halted})5.6 编写测试程序现在写一个简单的汇编程序然后手工汇编成 18 位指令。为了便于阅读我们直接构造内存中的指令值。假设需求是将常数5加载到累加器将累加器存储到地址0100将地址0100的内容加载回累加器加上常数3将结果输出停机。在main.py中构造程序# 文件路径main.py FDS 模拟器入口 from emulator.fds import FDS_Emulator from emulator.instruction import ( make_instruction, LDA, STA, ADD, OUT, HLT, ADDR_DIRECT ) def build_test_program(): 手工汇编测试程序 1. LDA #5 把立即数 5 加载到累加器 2. STA 0100 把累加器写入内存 0100 3. LDA 0100 从内存 0100 读回累加器 4. ADD #3 累加器加立即数 3 5. OUT 输出累加器 6. HLT 停机 program [] program.append(make_instruction(LDA, ADDR_DIRECT, 5)) # 地址 0000 program.append(make_instruction(STA, ADDR_DIRECT, 0o100)) # 地址 0001 program.append(make_instruction(LDA, ADDR_DIRECT, 0o100)) # 地址 0002 program.append(make_instruction(ADD, ADDR_DIRECT, 3)) # 地址 0003 program.append(make_instruction(OUT, ADDR_DIRECT, 0)) # 地址 0004 program.append(make_instruction(HLT, ADDR_DIRECT, 0)) # 地址 0005 return program def main(): emulator FDS_Emulator(memory_size0o1000) # 4096 个字 program build_test_program() emulator.load_program(program, start0) print( 运行前内存 ) emulator.memory.dump(0, 10) emulator.run(max_cycles100) print(\n 运行后状态 ) emulator.dump_registers() print(\n 运行后内存 ) emulator.memory.dump(0, 10) if __name__ __main__: main()5.7 运行与验证在终端中运行python main.py预期输出类似 运行前内存 0000: 010005 0001: 020100 0002: 010100 0003: 030003 0004: 070000 0005: 000000 0006: 000000 0007: 000000 0010: 000000 0011: 000000 OUT: ac8 (8) 运行后状态 PC 0006 AC 000010 B 000000 IR 000000 SR 000000 Cycles 6 Halted True 运行后内存 0000: 010005 0001: 020100 0002: 010100 0003: 030003 0004: 070000 0005: 000000 0006: 000000 0007: 000000 0010: 000000 0011: 000000这里AC输出为八进制000010即十进制的 8。因为5 3 8结果正确。5.8 扩展加入更多指令为了让模拟器更接近真实 FDS我们还可以加入以下指令逻辑与AND逻辑或OR左右移位SHL/SHR变址加载LDX实现方式与 ADD 类似只需要在cpu.py的step()中增加分支即可。每增加一条指令都要同步更新instruction.py中的操作码表。6. 常见问题与排查思路编写 FDS 模拟器时最常见的坑集中在指令编码、寻址模式和溢出处理上。下面整理一张排查表问题现象常见原因解决思路程序运行结果与预期不符操作码或寻址模式编码错误手工汇编一条指令逐步比对编码结果模拟器死循环跳转指令的目标地址错误加入max_cycles限制打印每次跳转的 PC 值内存越界异常程序地址计算错误检查resolve_address中的返回值是否在合法范围OUT 输出乱码OUT 指令行为与真实硬件不一致明确定义 OUT 是输出数值还是 ASCII 码必要时输出十六进制运算结果超过 18 位没有做掩码处理所有寄存器更新后执行value MASK_18加载程序后执行报错程序起始地址与 PC 不一致统一入口地址pc start或默认从 0 开始进位标志不更新SUB/ADD 后没有写sr在算术指令后显式更新sr的进位位如果你遇到类似报错可以按下面顺序排查先打印程序计数器PC和指令寄存器IR手工拆解IR的 18 位确认操作码、模式、操作数是否符合预期打印resolve_address的返回值确认访问的内存地址是否符合设计在每条指令执行后打印寄存器状态与手工推演结果比较。7. 最佳实践与工程建议7.1 用遥测数据验证模拟器模拟器正确性的最终检验标准是能否复现真实 FDS 的遥测输出。如果你能拿到 Voyager 1 历史下行数据帧的格式说明可以将模拟器生成的数据帧与真实数据对比逐字节核对。在没有真实数据的情况下至少要保证用独立编写的解释器交叉验证指令结果。7.2 指令集表驱动设计随着指令增多if-else分支会越来越难维护。更合理的做法是用数据表驱动指令执行# 指令处理函数注册表示例 instruction_table { LDA: lambda cpu, mode, operand: cpu.ins_lda(mode, operand), STA: lambda cpu, mode, operand: cpu.ins_sta(mode, operand), ADD: lambda cpu, mode, operand: cpu.ins_add(mode, operand), ... }这种设计新增指令时只需注册一个函数也让模拟器结构更接近真实微码设计。7.3 日志与跟踪模拟器调试的利器是“指令跟踪日志”。建议为每条指令记录以下信息执行时刻cycle 计数PC 值IR 值操作码名称操作数和解析后的地址执行后的累加器和标志位日志级别分为TRACE、DEBUG、INFO生产调试时只开TRACE平时运行开INFO。7.4 单元测试覆盖模拟器是基础设施必须用单元测试守护。推荐对以下场景测试每条指令的基本执行寻址模式的边界情况间接寻址、变址溢出算术溢出与标志位更新程序加载的边界地址。# tests/test_cpu.py 示例 import unittest from emulator.fds import FDS_Emulator from emulator.instruction import make_instruction, LDA, HLT, SUB class TestCPU(unittest.TestCase): def test_lda_immediate(self): emu FDS_Emulator() emu.load_program([ make_instruction(LDA, 0, 5), make_instruction(HLT, 0, 0) ]) emu.run() self.assertEqual(emu.cpu.ac, 5) def test_sub_overflow(self): emu FDS_Emulator() emu.load_program([ make_instruction(LDA, 0, 1), make_instruction(SUB, 0, 2), make_instruction(HLT, 0, 0) ]) emu.run() self.assertEqual(emu.cpu.ac, (1 - 2) 0x3FFFF) if __name__ __main__: unittest.main()7.5 最小权限与安全虽然 FDS 模拟器不直接涉及网络安全但涉及真实深空任务时你仍然要注意模拟器代码应与真实飞行软件严格分离上传或测试任何修复补丁前必须先在模拟器上完整跑一遍回归测试模拟器的 I/O 行为应可配置不能让调试输出影响正常数据帧。8. 总结与下一步本文从 Voyager 1 FDS 的背景出发解释了为什么需要为这台 46 年前的航天计算机编写模拟器然后拆解了 FDS 的硬件架构与指令集特征最后用 Python 实现了一个最小可运行的 18 位字模拟器。完成这个项目后你已经掌握了计算机模拟器的核心方法论寄存器建模、内存管理、指令周期的实现、寻址模式的处理以及如何用调试工具验证正确性。这套方法论不仅适用于 FDS也适用于 CHIP-8、Game Boy、Z80 等任何复古计算机模拟器项目。如果继续深入下面这些方向值得探索将模拟器移植到 C/Rust提升执行性能为模拟器实现反汇编器直接阅读 18 位机器码对应的汇编指令增加图形化调试界面例如基于 Web 的调试前端尝试为模拟器编写更复杂的 FDS 飞行程序例如数据帧格式化逻辑。太空探测器离我们很远但计算机模拟的工程方法离我们很近。希望这篇文章能帮你打开一扇通往复古计算和航天软件工程的门。如果模拟器运行过程中有新的发现欢迎在评论区分享你的思考和踩坑记录。
返回列表