简介:这份资源是一套用 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::vector<uint8_t>;Proteus 那边实现一个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++ 仿真器对编译器要求不高,C++11 足够,但要注意 32/64 位和运行库。Windows 上常见的是 MSVC,需要装 Microsoft Visual C++ 2015-2022 Redistributable (x64),否则编译出的 exe 在别人机器上会报缺 DLL。Linux 上 g++ 7 以上即可。先确认工具链:
g++ --version # 确认支持 -std=c++11 cmake --version # 如果源码带 CMakeLists.txt如果源码没有构建脚本,就手动编译。典型结构是z80core.cpp、runcpm_bus.cpp、main.cpp三个文件:
g++ -std=c++11 -O2 -o runcpm_z80 \ z80core.cpp runcpm_bus.cpp main.cpp参数说明:-O2开优化,仿真器是热循环,不开优化跑 CP/M 会慢到没法用;-std=c++11是因为回调用了虚函数和nullptr。如果报undefined reference,多半是某个.cpp没加进编译列表。
3.2 跑通第一个 CP/M 程序
RunCPM 需要一个 CP/M 系统盘镜像,通常是.dsk或目录形式。启动流程是:仿真器复位,PC 置 0,从内存 0 开始执行 BIOS,BIOS 再跳转到 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_Read),Proteus 按仿真步长调用。关键参数是时钟频率,要和电路里晶振一致,否则串口波特率全错。常见做法是先在 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 &= 0xFFFF,inPort/outPort做port &= 0xFFFF(Z80 的 IO 是 16 位地址,但常用低 8 位)。加一层边界检查,越界时打印地址而不是静默崩溃。
5. 进阶技巧:用调试钩子和周期计数验证仿真精度
跑通只是起点,真正要拿这份源码做东西,得能验证它仿真得准不准。Z80 的指令周期和标志位是有明确标准的,可以用它反过来校验你的实现。
5.1 用标志位校验算术指令
Z80 的ADD、SUB、CP对标志位的影响很细,尤其是半进位 H 和溢出 P/V。写一组测试用例,把已知输入输出对起来:
// 测试 ADD A,B:A=0x7F, B=0x01,结果应为 0x80 regs.a = 0x7F; regs.b = 0x01; cpu.executeADD(regs.b); // 期望:a=0x80, f 的 S=1, Z=0, H=1, P/V=1(溢出), N=0, C=0 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 回归盘,三步都过才敢说改对了。这套流程帮我挡掉过好几次“看着能跑、实际时序全错”的翻车。希望帮到你。
本文还有配套的精品资源,点击获取