资讯动态

将Mitra-15小型机移植到SIMH:复古计算模拟器的构建之路

发布时间:2026/8/30 10:00:12 来源:尧图企业网站定制
最近在整理复古计算项目时接触到一个很有意思的课题把法国 CII 公司 1970 年代的 Mitra-15 小型计算机移植到 SIMH 模拟器框架里。这个项目还处于 Work in Progress 阶段公开资料分散能直接跑通的完整实现很少所以把目前的调研、框架设计和踩坑点整理成文。本文适合对计算机历史、模拟器实现、嵌入式 CPU 仿真感兴趣的读者同时也给想了解 SIMH 如何扩展新机型的朋友提供一条可操作的路径。1. 背景与核心概念1.1 Mitra-15 是一台怎样的机器Mitra-15 是法国 CIICompagnie Internationale pour lInformatique在 1970 年代推出的一款 16 位小型计算机。CII 是当时法国政府推动的“计算计划”背景下成立的公司后来经过多次合并成为今天我们熟悉的 Groupe Bull 的一部分。Mitra-15 本身定位在工业控制、科学计算、教学实验和数据采集等场景在法国国内的大学、研究所和企业里都有一定装机量。从硬件架构上看Mitra-15 属于典型的小型机设计思路字长 16 位。内存按字编址容量在小型机里属于中等水平。有一组通用寄存器支持多种寻址方式。强调实时中断响应能力适合工控场景。外设接口丰富支持纸带阅读机、穿孔机、行式打印机、磁盘等经典外设。放在今天来看它的指令集并不复杂计算能力也很有限但它是理解 1970 年代欧洲计算机工业和技术路线的重要样本。想要研究法国早期软件生态、老式工控程序或者操作系统设计一个可以稳定运行的 Mitra-15 模拟器是非常有价值的工具。1.2 SIMH 是什么SIMH 是一个开源的、跨平台的计算机模拟器框架最初由 Bob Supnik 开发目的是模拟 DEC 早期的小型机比如 PDP-1、PDP-8、PDP-11 等。后来社区不断扩展支持了非常多经典机型覆盖范围从 IBM 大型机、Data General 小型机到各种早期微处理器系统。SIMH 与普通虚拟机不同。虚拟机通常依赖宿主机 CPU 的支持尽量让目标系统指令直接运行SIMH 则是“解释执行”它在 C 语言层面模拟 CPU 的取指、译码、执行步骤同时模拟内存、终端、磁盘、纸带等设备。模拟器里的操作系统并不知道自己在模拟器上运行它看到的仍然是一台完整的、可响应的真实计算机。对于复古计算爱好者来说SIMH 主要有几个优点跨平台Windows、Linux、macOS 都能编译运行。模拟器运行时带有调试控制台可以设置断点、单步执行、查看寄存器。设备框架统一新机型不需要从零搭建整个控制框架。老系统的磁盘镜像、磁带镜像可以通过 attach 方式挂载。1.3 为什么要做 Mitra-15 on SIMH做这个项目不是为了“跑分”也不是为了实用计算而是为了解决几个实际问题第一Mitra-15 的原始硬件已经很难找到并且维护成本极高很多设备已经报废。模拟器可以低成本地保存历史软件。第二Mitra-15 的指令集和硬件手册虽然能通过网络找到扫描件但纸上资料没法运行。有一个模拟器在手学者和学生可以直接加载测试程序验证手册中的每条指令。第三这是一个极好的模拟器学习项目。相比从零写一个 CPU 模拟器在 SIMH 框架里扩展新机型可以复用设备框架、调试器、命令行控制台把主要精力放在 CPU 指令集和机器结构上。第四这个项目目前还是 WIP并不意味着没有价值。WIP 项目同样可以公开代码、记录进展、让别人复现和接力。很多经典模拟器项目都是从一个“只能跑几条指令”的半成品开始的。2. 环境准备与版本说明2.1 软件环境因为是模拟器开发所以不太挑系统。只要能编译 C 语言项目就可以工作。下面是我建议的最小环境工具说明操作系统Linux / macOS / Windows 均可Git拉取 SIMH 源码和提交补丁C 编译器gcc、clang 或 MSVC构建工具make 或 CMake取决于 SIMH 分支文本编辑器任意推荐支持 C 语法高亮即可版本说明SIMH 的发展历史比较长不同分支的目录结构和内部 API 有一些差异。本文以常见的 SIMH v4 分支为参考来演示框架具体到你拉取的源码接口可能略有不同。遇到编译错误时优先对照仓库里已有的模拟器源码来调整不要死记某个固定版本的写法。2.2 新增机型的目录结构在 SIMH 代码库中扩展新机型通常有两种组织方式一种是在sim/目录下增加一组以机器名命名的源文件比如sim_m15.c、sim_m15.h另一种是在sim/下建立独立子目录比如sim/mitra15/里面放机器相关代码和测试程序。我倾向于使用独立子目录因为 Mitra-15 项目还处于早期阶段除了模拟器本身还需要准备测试汇编程序、原始手册扫描件、启动镜像等资料。单独目录不会污染主代码树也方便未来单独打包。规划出来的参考结构如下sim/ mitra15/ Makefile README.md sim_m15.c sim_m15.h tests/ add_test.m15 mem_test.m15 boot_test.m15 expected_results.txt docs/ mitra15_cpu_manual.pdf status.md核心文件就两个sim_m15.h和sim_m15.c。头文件放机器类型定义、寄存器结构体和设备表声明.c文件放 CPU 执行循环、内存访问函数、设备注册和机器初始化代码。3. SIMH 模拟一个 CPU 的核心原理3.1 模拟器基本组成不管目标机器是什么一个 CPU 模拟器至少要包含这几块内容内存用一段连续字节数组模拟物理内存。寄存器用变量或结构体保存 PC、累加器、状态位、通用寄存器。取指/译码/执行循环从 PC 指向的地址读取指令解析操作码和操作数模拟执行。地址映射把 CPU 发出来的地址翻译到内存数组或设备寄存器。中断与事件模拟时钟、外设中断、复位信号。调试接口让用户能读寄存器、写内存、设断点、单步执行。SIMH 本身已经提供了控制台、调试器、设备框架所以最核心的工作集中在 1-4 项。3.2 SCP 与机器回调SIMH 的交互控制台被称为 SCP它会读取用户输入的命令比如run、examine、deposit、break、attach再调用对应机器的底层函数。为了让 SCP 认识 Mitra-15需要在代码里提供机器描述结构、寄存器表、设备表。下面是一个常见的机器注册示意代码。这里只是为了说明结构不是完整可编译代码。/* 文件路径sim/mitra15/sim_m15.h */ #ifndef SIM_M15_H #define SIM_M15_H #include sim_defs.h /* Mitra-15 内存大小按 16 位字计算 */ #define M15_MEM_WORDS (64 * 1024) /* 状态寄存器位定义 */ #define M15_FLAG_CARRY 0x0001 #define M15_FLAG_ZERO 0x0002 #define M15_FLAG_NEG 0x0004 typedef struct { uint16_t pc; /* 程序计数器 */ uint16_t acc; /* 累加器 */ uint16_t flags; /* 条件码 */ uint16_t regs[8]; /* 通用寄存器组具体数量以手册为准 */ } M15_CPU; extern M15_CPU M15_cpu; #endif真实的 SIMH 机器注册还需要包含一些 SCP 要求的全局符号比如sim_name、sim_PC、sim_emax、sim_devices等这些符号在不同分支里不完全一致建议在新增源码时对照现有最小模拟器文件来写例如 SIMH 自带的sim_altair.c或sim_pdp8.c。3.3 执行循环的最小骨架CPU 模拟器的灵魂是执行循环。下面这个示例把核心步骤压缩成最小骨架用来展示思路/* 文件路径sim/mitra15/sim_m15.c核心片段 */ #define M15_OP_ADD 0x01 /* 示意值实际编码以 Mitra-15 手册为准 */ #define M15_OP_LOAD 0x02 #define M15_OP_STORE 0x03 #define M15_OP_HALT 0x00 static int32 sim_run(void) { for (;;) { /* 检查是否有外部事件需要处理 */ if (sim_interval) { /* 定时器、外设轮询 */ } /* 取指 */ uint16_t instr M15_MEM_READ(M15_cpu.pc); M15_cpu.pc; /* 译码与执行 */ switch (instr) { case M15_OP_ADD: /* 根据寻址模式取操作数并相加 */ break; case M15_OP_LOAD: /* 加载内存数据到累加器 */ break; case M15_OP_STORE: /* 把累加器写回内存 */ break; case M15_OP_HALT: return SCPE_OK; default: /* 非法指令进入异常处理 */ break; } /* 检查停止请求 */ if (stop_requested) { break; } } return SCPE_OK; }这个循环少了寻址模式、指令长度、中断响应等细节但结构已经足够表达“模拟器是怎么跑起来”的。实际项目中指令译码部分通常会写成一张指令表用操作码索引进函数指针数组这样比一长串 switch-case 更容易维护和对照手册。3.4 内存与设备模型内存访问不是简单地访问数组还要处理地址越界、I/O 映射、设备寄存器映射。一个健壮的内存读写接口长这样static uint16_t M15_MEM_READ(uint16_t addr) { if (addr M15_MEM_WORDS) { /* 越界访问记录日志并返回异常值 */ sim_debug(DBG_ERR, M15_dev, memory read out of range: %04x\n, addr); return 0xFFFF; } return M15_mem[addr]; } static void M15_MEM_WRITE(uint16_t addr, uint16_t value) { if (addr M15_MEM_WORDS) { sim_debug(DBG_ERR, M15_dev, memory write out of range: %04x\n, addr); return; } M15_mem[addr] value; }如果 Mitra-15 采用内存映射 I/O那么设备寄存器会占用一段特定的地址空间。内存读写函数里要对这些地址做特殊处理比如当地址落在控制台终端寄存器范围时调用终端设备的读写回调而不是访问内存数组。4. 在 SIMH 中添加 Mitra-15 的完整流程4.1 创建项目结构与初始文件先创建一个独立目录并建立最基础的源文件。这里假设你已经把 SIMH 源码克隆到本地。cd sim mkdir mitra15 cd mitra15 touch sim_m15.c sim_m15.h Makefile README.md初始文件可以先只放一个能编译的空壳目的是确认构建链路没问题。/* 文件路径sim/mitra15/sim_m15.c */ #include sim_m15.h int main(void) { return 0; }这份空壳当然不能运行但它可以验证 Makefile 和编译器环境是否正确。4.2 编写机器注册信息要让 SIMH 的 SCP 能识别这台机器需要提供机器名称、寄存器表、设备表和复位函数。下面是一个示意写法/* 文件路径sim/mitra15/sim_m15.c */ #include sim_m15.h M15_CPU M15_cpu; uint16_t M15_mem[M15_MEM_WORDS]; /* 寄存器表供 SCP 的 examine/deposit 命令使用 */ REG M15_regs[] { { PC, M15_cpu.pc, sizeof(M15_cpu.pc), 0, 0 }, { ACC, M15_cpu.acc, sizeof(M15_cpu.acc), 0, 0 }, { FLAGS,M15_cpu.flags, sizeof(M15_cpu.flags), 0, 0 }, { NULL } }; /* 设备表 */ DEVICE M15_dev[] { { M15, M15_dev_flags, M15_rd, M15_wr, M15_reset, NULL, NULL, NULL }, { NULL } };这个代码里M15_dev_flags、M15_rd、M15_wr、M15_reset还没有实现需要继续写。这里要特别说明不同 SIMH 分支中REG结构体和DEVICE结构体的字段顺序、类型名可能有差异请结合你使用的 SIMH 源码里的sim_defs.h调整。4.3 实现 CPU 核心CPU 核心包括复位、取指、执行、中断等函数。复位函数负责把 PC 设为固定启动地址、清空寄存器和内存状态static void M15_reset(void) { /* 根据 Mitra-15 手册复位后 PC 的初始地址通常是某个固定值 */ M15_cpu.pc 0x0000; M15_cpu.acc 0x0000; M15_cpu.flags 0; /* 清空内存是可选的取决于复位行为 */ memset(M15_mem, 0, sizeof(M15_mem)); sim_debug(DBG_EXP, M15_dev, Mitra-15 reset\n); }执行循环里的指令译码建议提前整理成指令表。先把指令操作码、助记符、操作数长度和执行函数列成一张表然后逐条实现。一次只实现几条指令跑通基本加减法和内存读写后再扩展其余指令。typedef void (*M15_OP_FUNC)(uint16_t instr); static void M15_OP_HALT_IMPL(uint16_t instr) { /* 设置停止标志 */ } static void M15_OP_ADD_IMPL(uint16_t instr) { /* 读取操作数并加到累加器 */ } static const M15_OP_FUNC M15_optab[256] { [0x00] M15_OP_HALT_IMPL, [0x01] M15_OP_ADD_IMPL, /* 其余指令继续补充 */ };这个设计的好处是以后想增加一条指令只需要写一个函数然后在表中登记不需要动执行循环的骨架代码。4.4 实现内存读写与 I/O 映射内存读写函数前面已经给出过基础版本。如果 Mitra-15 有控制台开关、拨码盘等简单设备可以把它们映射到固定地址。例如#define M15_IO_CONSOLE_SWITCH 0xFF00 #define M15_IO_CONSOLE_LAMP 0xFF01 static uint16_t M15_rd(int32 addr) { if (addr M15_IO_CONSOLE_SWITCH) { return console_switch_value; } if (addr M15_MEM_WORDS) { return M15_mem[addr]; } return 0xFFFF; } static void M15_wr(int32 addr, uint16_t value) { if (addr M15_IO_CONSOLE_LAMP) { console_lamp_value value; return; } if (addr M15_MEM_WORDS) { M15_mem[addr] value; } }设备寄存器不一定真实存在需要根据手册判断 Mitra-15 是否用了内存映射 I/O。如果它是独立 I/O 端口那就在指令执行中对 IN/OUT 类指令做特殊处理。4.5 接入 SIMH 控制台与调试器SIMH 的调试器让用户能单步执行和查看寄存器这也是模拟器项目开发阶段最重要的调试手段。要在调试器中看到寄存器需要把寄存器表正确注册给 SCP。当用户输入examine ACC或deposit PC 100时SCP 会通过寄存器表读写 CPU 内部状态。为了方便自己调试可以在执行循环里加一个单步调试分支if (sim_brk_summ SWMASK(S)) { /* 单步暂停 */ }具体宏名取决于 SIMH 版本现实中更常用的做法是直接用 SCP 自带调试功能启动模拟器后输入run 100表示从地址 0x100 开始运行输入step时单步执行输入examine 200时查看内存地址 0x200。这些命令在不同 SIMH 版本之间基本保持一致但细节以当前源码的sim_console.c为准。4.6 构建与运行最小可运行版本需要写一个 Makefile。以最常见的 gcc 为例CC gcc CFLAGS -g -O2 -I../.. sim_m15: sim_m15.o $(CC) -o $ $^ sim_m15.o: sim_m15.c sim_m15.h ../../sim_defs.h $(CC) $(CFLAGS) -c $ -o $ clean: rm -f sim_m15 sim_m15.o如果 SIMH 主树本身已经有一套构建系统更适合把新增机型嵌入它的 Makefile 中这样可以复用 SCP 基础设施的编译单元。具体做法是在主 Makefile 中新增一个 target把sim_m15.c加入源文件列表。编译完成后运行./sim_m15如果注册成功会进入 SIMH 风格的控制台Mitra-15 simulator sim此时可以尝试写入指令并执行。例如先把一条停机指令写入内存sim deposit 0 0000 sim run 0 sim如果返回提示符而不是卡死说明最基础的取指-执行-停机链路已经通了。4.7 通过测试程序验证进展有了最小框架后接下来的工作就是把手册中的指令逐条实现并测试。测试程序可以用手工编写的机器码也可以用汇编器生成。Mitra-15 是否已有可用的交叉汇编器需要确认如果暂时没有就先用手写字节码验证每条指令。一个简单的加法测试思路指令 0x01LOAD #5 - 把立即数 5 装入累加器 指令 0x02ADD #3 - 累加器加上 3 期望结果ACC 8在模拟器里把这几条指令 deposit 到内存然后运行再用examine ACC查看结果。每次验证通过后把测试用例保存到tests/目录并记录预期输出。这套回归测试在后续改动指令表时非常有用。5. Work in Progress 项目的现实问题5.1 为什么模拟器项目会长期停在 WIP在开源社区里复古计算模拟器经常是“永远在开发中”。这个状态不是拖延而是由客观困难决定的。首先老机器的硬件手册虽然能找到但并不是每一版硬件都有完整资料。Mitra-15 可能有多个型号和可选配置CPU 手册、外设手册、系统软件手册分散在不同机构整理成本很高。其次模拟器正确性依赖“参考实现”。如果没有一台还能开机的实体机器或者没有官方诊断程序就很难确认某条指令在特定边界条件下到底该产生什么效果。最后外设模拟的工作量往往比 CPU 核心还大。CPU 只有几条到几百条指令但纸带机、行式打印机、磁盘控制器都有各自复杂的时序。对于 Mitra-15 这种面向工控的机器中断系统和实时外设是难点也是 WIP 最常见的卡点。5.2 如何制定可执行的里程碑WIP 不代表没有计划。我建议把项目拆成下面几个里程碑里程碑目标验证标准M1CPU 核心指令集基础功能跑通自编的加减、存储、跳转测试M2内存映射与 I/O 地址能通过 I/O 地址读写外部设备寄存器M3控制台终端/指示灯模拟能输出字符到终端或通过指示灯观察状态M4中断与定时器周期性中断触发并能跳转到中断服务程序M5外设与系统软件加载原始操作系统或诊断程序并稳定运行每完成一个里程碑就在 README 里更新 status这样别人看到项目时能快速了解完成度。5.3 怎么判断“完成”对于一台老机器真正意义上的“完成”是指它能在模拟器上运行目标系统软件且行为与真实硬件一致。判断方法有三种指令覆盖率测试统计所有操作码和寻址模式组合是否都有用例覆盖。运行官方诊断程序如果找到 Mitra-15 的诊断程序这是最权威的验证。运行实际应用比如当年教科书的示例程序、工控程序或教学操作系统。在这些验证完成之前项目保持 WIP 状态是合理且诚实的做法。6. 常见问题与排查思路在模拟器开发过程中最常见的故障集中在内存访问、指令译码、设备注册和中断处理上。下面整理一个排查对照表问题现象常见原因解决思路模拟器启动后直接卡死PC 初始值错误或内存复位状态不对检查复位函数和内存初始值输入run后没有输出终端设备未注册或输出地址映射错误检查设备表和写回调指令执行结果与手册不符操作码编码、寻址模式解码错误用最小测试程序单步执行对照手册程序运行顺序错乱跳转指令目标地址计算错误重点检查相对寻址/绝对寻址的换算中断一直触发中断屏蔽位没有初始化先禁用中断再逐个功能打开examine看不到自定义寄存器寄存器表未注册或字段类型错误检查 REG 表是否挂入机器描述编译连接失败与当前 SIMH 版本 API 不一致对照sim_defs.h和现有机型代码调整排查时不要一上来就怀疑框架先确认最小链路内存写入是否正常取指是否从正确地址开始执行后 PC 是否更新。打印 trace 日志是效率最高的手段。可以在执行循环里记录每一步的寄存器状态PC0010 ACC0005 FLAGS0 INSTR0100 PC0011 ACC0008 FLAGS0 INSTR0102这种日志能直接看到错误发生在哪一步。7. 最佳实践与工程建议7.1 代码组织与命名规范模拟器代码要长期维护命名和结构必须清晰。给 Mitra-15 相关的类型、函数、变量统一加M15_前缀避免与 SIMH 自带代码冲突。寄存器表、设备表、指令表要分开定义指令表用数组索引而不是长 switch-case。每实现一条指令就在指令注释里标注手册页码或对应章节。例如/* 参考Mitra-15 CPU 手册第 3.2 节 ADD 指令 */这对后续维护者非常有帮助。7.2 历史资料与文档管理复古计算项目最怕的是资料丢失。建议把搜集到的手册扫描件、数据手册、程序列表统一放在docs/目录并在 README 里注明来源和版权状态。不要把扫描件直接放进 Git 仓库除非已经确认版权允许分发。更安全的做法是在 README 中给出外部链接模拟器只存放开源代码和测试用例。项目状态不要只靠口头说明。在 README 里写清楚当前已经实现的指令。还没实现的设备。最近的验证结果。下一步计划。这样即使项目停更几个月其他人接手也能快速进入状态。7.3 许可证与法律边界老机器的系统软件、诊断程序和手册往往有版权即使年代久远也未必进入公有领域。模拟器本身的代码可以开源但镜像文件、手册扫描件不要随意打包上传。使用这些资料前先确认版权方和许可协议。7.4 调试技巧模拟器开发过程中最有用的工具是“最小复现用例”。每次测试只测一个小功能比如一条 ADD 指令一个内存写入一个中断触发。不要等写了一大堆指令后再整体调试否则定位问题会非常痛苦。如果找不到参考实现可以用“对照表”方式验证把手册上每一个操作码、寻址模式、条件码变化列成表格然后逐条实现和测试。这个过程枯燥但可靠。8. 总结与后续路线这篇笔记从 Mitra-15 和 SIMH 的背景讲起梳理了模拟器项目的基础结构、代码框架和 WIP 阶段的推进方法。核心不是提供一份已经完成的 Mitra-15 模拟器源码而是给后来者一条可以执行的路线搭空壳、注册机器、实现内存读写、逐条验证指令、接入调试器、用测试用例闭环。接下来如果想继续推进这个项目优先做两件事一是把手头能找到的 Mitra-15 CPU 手册完整读一遍整理出指令集表二是维护一个最小测试程序集每实现一条指令就立刻回归。这两件事做完项目就能从“能取指”走到“能稳定运行简单程序”的阶段。如果你也在折腾类似的老机器模拟器项目可以从一台指令集更简单的机器入手先打通整个 SIMH 接入流程再回头啃 Mitra-15 的中断和外设。项目还在 Work in Progress 并不可怕只要能跑一条指令就有了继续前进的起点。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价