2026/10/29 16:45:58

C++ Z80仿真器源码解析:从RunCPM到Proteus的移植与避坑指南

C++ Z80仿真器源码解析:从RunCPM到Proteus的移植与避坑指南 简介这份资源是一套用 C 编写的 Z80 处理器仿真器源码面向希望深入理解经典 8 位微处理器、操作系统仿真与电子设计自动化的开发者与学习者。它可分别配合 RunCPM 与 Labcenter Proteus 使用前者让现代计算机运行 CP/M 系统及经典工具后者将仿真器接入虚拟 RAM、ROM、I/O 设备用于嵌入式设计的验证与调试。压缩包共 7 个文件约 9KB包含 cpp 主源码、vcxproj 与 sln 工程文件、md 说明文档以及 filters、gitignore、gitattributes 等辅助配置结构精简便于直接导入 Visual Studio 阅读与编译。目前已有 103 人学习下载。通过研读源码读者可掌握 Z80 指令解码、寄存器与内存模型、中断异常处理等核心实现思路并了解如何将仿真器集成到两个不同平台是学习处理器仿真与 C 工程实践的实用参考。1. 从一份 Z80 仿真器源码说起它到底能跑通什么如果你手头有一份用 C 写的 Z80 仿真器源码目标平台是 RunCPM 和 Labcenter Proteus那它解决的其实是一件很具体的事让一台 8 位老机器的 CPU 行为在现代 PC 上被完整复现出来并且能和 Proteus 里的外围电路对接。RunCPM 本身是一个把 CP/M 2.2 跑在宿主系统上的模拟环境它需要一个能执行 Z80 指令的内核而 Proteus 是电路级仿真工具它需要的是一个能嵌进仿真电路、按总线时序响应外设的 CPU 模型。这两件事对仿真器的要求并不一样一份源码要同时服务它们说明它的结构是分层的——指令执行核心和总线接口是分开的。这份资源适合三类人一是想学 CPU 仿真怎么写的人Z80 指令集规整、文档齐全是入门仿真器的好样本二是要在 Proteus 里搭 Z80 系统、需要自己改 CPU 模型的人三是跑 RunCPM 时想换掉默认内核、自己控制指令实现的人。它不解决“一键跑起来”的问题它给的是可编译、可改、可嵌的源码剩下的接线和配置得自己动手。2. 拆开这份 C 源码Z80 核心、总线接口与 RunCPM 的对接点拿到源码先别急着编译先看清楚它内部是怎么分层的。Z80 仿真器写得好不好关键不在指令条数而在“指令执行”和“内存/IO 访问”有没有解耦。解耦了同一份核心才能既跑 RunCPM 又嵌 Proteus没解耦改一处就崩一片。2.1 指令执行核心与寄存器模型Z80 的寄存器组是它和 8080 最大的区别主寄存器组 A/F/B/C/D/E/H/L加上备用组 A/F/B/C/D/E/H/L还有 IX、IY、SP、PC、I、R。一份合格的 C 实现通常会把它们放进一个结构体而不是散成一堆全局变量。常见做法是struct Z80Regs { uint8_t a, f, b, c, d, e, h, l; // 主寄存器组 uint8_t a2, f2, b2, c2, d2, e2, h2, l2; // 备用组EX AF,AF 用 uint16_t ix, iy, sp, pc; // 变址与栈指针 uint8_t i, r; // 中断向量与刷新寄存器 bool iff1, iff2; // 中断允许标志 };这里每个字段的含义直接对应 Z80 手册里的寄存器名f是标志寄存器进位、零、符号、奇偶、半进位都压在这一个字节里。备用组单独存是因为EXX和EX AF,AF是整组交换不是逐字节搬。把寄存器集中放的好处是复位、保存现场、调试打印都只操作一个结构体RunCPM 需要保存上下文时直接拷这个结构就行。指令执行一般是一个step()函数取指、译码、执行、更新 R 寄存器、算周期数。周期数不能省Proteus 里外设的时序是按 T 状态算的少算一个周期串口或定时器就可能对不上。2.2 内存与 IO 访问的回调设计这是整份源码能不能同时服务两个平台的分水岭。RunCPM 跑在宿主系统上内存是宿主分配的一块数组Proteus 里内存可能是仿真电路中的 RAM 芯片读写要走引脚。所以核心不能直接mem[addr]得走回调class Z80Bus { public: virtual uint8_t readMem(uint16_t addr) 0; virtual void writeMem(uint16_t addr, uint8_t val) 0; virtual uint8_t inPort(uint16_t port) 0; virtual void outPort(uint16_t port, uint8_t val) 0; };核心只持有Z80Bus*不关心背后是数组还是电路。RunCPM 那边实现一个FlatMemoryBus内部就是std::vectoruint8_tProteus 那边实现一个ProteusBus把读写转成对仿真引脚的调用。这样同一份Z80Core编译两次接不同总线即可。提示回调是虚函数每条指令都要调好几次性能敏感的场景可以改成模板参数或函数指针但先跑通再优化。2.3 RunCPM 侧的对接把 CP/M 的 BDOS 调用接进来RunCPM 的机制是Z80 程序执行到CALL 0005h时进入 CP/M 的 BDOS 入口由宿主系统接管。仿真器要做的是在step()执行到 PC 等于 5 时不真的跳转而是把 C 寄存器里的功能号交给 RunCPM 的处理函数。常见写法是在总线或核心外层加一个钩子if (regs.pc 0x0005) { uint8_t func regs.c; runCPM_BDOS(func, regs); // 由 RunCPM 实现具体功能 return; // 不执行内存里的指令 }参数说明func是 BDOS 功能号比如 2 是输出字符、9 是输出字符串、1 是读字符。regs传引用因为 BDOS 调用会改 A、HL 等寄存器作为返回值。这个钩子必须放在取指之前判断否则会先读到内存里的占位指令。2.4 Proteus 侧的对接总线时序与引脚映射Proteus 的 CPU 模型不是靠 C 直接跑而是通过 DLL 或 ActiveX 接口和仿真内核通信。你要做的是把readMem/writeMem/inPort/outPort映射到 Proteus 的引脚读写 API 上同时把 MREQ、IORQ、RD、WR 这些控制信号按 T 状态拉高拉低。这里最容易翻车的是时序Proteus 期望你在每个 T 状态更新引脚而不是一条指令结束后一次性更新。所以step()内部要能按周期回调而不是只返回总周期数。3. 编译与运行从源码到 RunCPM 可执行文件源码能看懂是一回事能编译出可运行的东西是另一回事。这一章按实际操作顺序走从环境准备到跑起第一个 CP/M 程序。3.1 编译环境与依赖确认C 仿真器对编译器要求不高C11 足够但要注意 32/64 位和运行库。Windows 上常见的是 MSVC需要装 Microsoft Visual C 2015-2022 Redistributable (x64)否则编译出的 exe 在别人机器上会报缺 DLL。Linux 上 g 7 以上即可。先确认工具链g --version # 确认支持 -stdc11 cmake --version # 如果源码带 CMakeLists.txt如果源码没有构建脚本就手动编译。典型结构是z80core.cpp、runcpm_bus.cpp、main.cpp三个文件g -stdc11 -O2 -o runcpm_z80 \ z80core.cpp runcpm_bus.cpp main.cpp参数说明-O2开优化仿真器是热循环不开优化跑 CP/M 会慢到没法用-stdc11是因为回调用了虚函数和nullptr。如果报undefined reference多半是某个.cpp没加进编译列表。3.2 跑通第一个 CP/M 程序RunCPM 需要一个 CP/M 系统盘镜像通常是.dsk或目录形式。启动流程是仿真器复位PC 置 0从内存 0 开始执行 BIOSBIOS 再跳转到 CCP。你要做的是把镜像加载进FlatMemoryBus的数组里然后循环调step()FlatMemoryBus bus; bus.loadImage(cpm22.bin, 0x0000); // 系统加载到 0 地址 Z80Core cpu(bus); cpu.reset(); while (running) { cpu.step(); if (cpu.regs.pc 0x0005) { runCPM_BDOS(cpu.regs.c, cpu.regs); } }逻辑说明loadImage把二进制镜像铺到内存起始处reset把 PC 清零、寄存器置初值主循环每次执行一条指令遇到 BDOS 入口就交给宿主。跑起来后应该能看到 CP/M 的A提示符。看不到先查镜像加载地址对不对再查 BDOS 钩子有没有生效。3.3 在 Proteus 里挂上 CPU 模型Proteus 侧不是跑 exe而是把编译出的 DLL 注册成 CPU 模型。步骤是在 Proteus 里选 Z80 器件属性里指定你的 DLL 路径DLL 导出规定的函数如Z80_Reset、Z80_Step、Z80_ReadProteus 按仿真步长调用。关键参数是时钟频率要和电路里晶振一致否则串口波特率全错。常见做法是先在 Proteus 里放一个最小系统Z80 RAM ROM 一个 LED 接 IO 口写个点灯程序验证总线读写通了再上复杂外设。注意Proteus 的 DLL 接口对调用约定有要求Windows 下通常是__stdcall写错会导致仿真直接崩且没有明确报错。4. 避坑与排查Z80 仿真器最容易翻车的五个地方这一章全是血泪经验每条都按现象、原因、解决写。Z80 仿真看着简单真跑起来问题都藏在细节里。4.1 现象CP/M 提示符出不来程序跑飞原因BDOS 钩子判断时机不对或者镜像加载地址错了。Z80 复位后 PC 是 0但有些 CP/M 镜像的 BIOS 入口在 0xF200 之类的高地址加载到 0 就全错。解决先确认镜像的加载地址用调试器打印前几条指令的 PC 和操作码对照 Z80 手册看是不是合法指令。如果 PC 很快跳到一片 0x00基本就是镜像没加载对。4.2 现象Proteus 里 CPU 不动引脚一直是低原因DLL 没被正确调用或者时钟没接。Proteus 的 CPU 模型需要外部时钟信号驱动如果电路里没放时钟源或者时钟频率设成 0仿真内核不会调Z80_Step。解决检查电路里有没有时钟元件属性里的频率是不是和 DLL 里假设的一致。另一个常见原因是 DLL 位数和 Proteus 不匹配32 位 Proteus 加载不了 64 位 DLL。4.3 现象指令周期数对不上定时器偏慢或偏快原因step()返回的周期数没算全。Z80 有些指令是条件分支走和不走周期数不同比如JR跳转是 12 周期不跳是 7 周期。如果统一按一个值返回累积误差会让定时器漂移。解决每条指令按手册的分支情况分别返回周期数条件成立与否用实际标志位判断。这个坑在 Proteus 里尤其明显因为外设是按周期计时的。4.4 现象中断进不去或者进去就出不来原因IFF1/IFF2 的处理不对或者中断模式IM 0/1/2没实现全。Z80 有三种中断模式IM 2 需要从 I 寄存器和外设提供的向量拼出中断向量表地址。只实现 IM 1 的话很多系统跑不起来。解决把三种模式都实现重点检查EI指令的延迟效应——EI之后的下一条指令执行完才真正允许中断这个细节漏了会导致中断时序错乱。4.5 现象编译过了运行时报内存越界或崩溃原因内存读写回调没有做地址掩码。Z80 是 16 位地址但有些实现里数组开小了或者 IO 端口地址没做 0xFF。Proteus 侧如果引脚映射越界会直接让仿真崩。解决所有readMem/writeMem入口先做addr 0xFFFFinPort/outPort做port 0xFFFFZ80 的 IO 是 16 位地址但常用低 8 位。加一层边界检查越界时打印地址而不是静默崩溃。5. 进阶技巧用调试钩子和周期计数验证仿真精度跑通只是起点真正要拿这份源码做东西得能验证它仿真得准不准。Z80 的指令周期和标志位是有明确标准的可以用它反过来校验你的实现。5.1 用标志位校验算术指令Z80 的ADD、SUB、CP对标志位的影响很细尤其是半进位 H 和溢出 P/V。写一组测试用例把已知输入输出对起来// 测试 ADD A,BA0x7F, B0x01结果应为 0x80 regs.a 0x7F; regs.b 0x01; cpu.executeADD(regs.b); // 期望a0x80, f 的 S1, Z0, H1, P/V1(溢出), N0, C0 assert(regs.a 0x80); assert((regs.f 0x80) ! 0); // S 符号位 assert((regs.f 0x04) ! 0); // P/V 溢出参数说明f的位定义是 S(7) Z(6) F5(5) H(4) F3(3) P/V(2) N(1) C(0)。0x7F 0x01从正数加到负数有符号溢出所以 P/V 置位低四位0xF 0x1产生半进位H 置位。这类用例覆盖几十条算术指令就能把标志位逻辑钉死。5.2 用周期计数对齐 Proteus 时序在 Proteus 里挂一个示波器或逻辑分析仪测MREQ和RD的时序和你的step()返回的周期数对比。常见做法是跑一段已知周期的循环比如LD B,0xFF加DJNZ自减总周期数是可算的看 Proteus 里跑完这段用了多少仿真时间反推每个 T 状态对不对得上。对不上就回去查那条指令的周期表。5.3 用 RunCPM 跑真实 CP/M 软件做回归最狠的验证是拿真实软件跑。CP/M 上有 Turbo Pascal、WordStar、Zork 这些经典程序它们对 BDOS 调用和内存布局的依赖很真实。跑通一个说明仿真器基本可用跑通一批说明兼容性够。我一般会准备一个测试盘里面放几个不同年代的程序每次改完核心都跑一遍哪个崩了就知道改坏了什么。提示回归测试盘要固定版本别每次换镜像否则出问题分不清是仿真器改了还是镜像变了。从那以后我每次改 Z80 核心都强制先跑标志位用例再跑 Proteus 最小系统最后上 CP/M 回归盘三步都过才敢说改对了。这套流程帮我挡掉过好几次“看着能跑、实际时序全错”的翻车。希望帮到你。本文还有配套的精品资源点击获取