公司动态
基于Cyclone IV FPGA的Z80单板计算机设计与实现
1. 项目概述在FPGA上复活经典CPU如果你和我一样对老式计算机的架构和运行原理着迷同时又对现代可编程逻辑器件FPGA的灵活性充满兴趣那么“在Cyclone IV FPGA上实现Zilog Z80单板计算机SBC”这个项目绝对是一个能让你玩上几个月甚至几年的“硬核玩具”。这不仅仅是一个简单的仿真而是将一块经典的8位CPU——Z80连同其周边的内存、ROM、串口等关键外设全部用硬件描述语言Verilog或VHDL“雕刻”在一块FPGA芯片里最终形成一个可以独立运行、能执行真实机器码的完整计算机系统。Z80 CPU是上世纪70年代末到90年代初的传奇它驱动了诸如Sinclair ZX Spectrum、MSX、以及无数工业控制设备和早期游戏机。而Cyclone IV FPGA则是Altera现Intel PSG家族中一款性价比极高、资源适中且非常普及的入门级FPGA芯片。将这两者结合其核心价值在于你无需去寻找一块可能已经停产、价格高昂且脆弱的古董Z80芯片就能在一个完全可控、可调试的现代硬件平台上深入理解并亲手构建一个完整的微计算机系统。从CPU指令执行时序、总线仲裁、到内存映射、外设中断处理每一个细节都完全由你定义和实现。这对于学习计算机体系结构、数字逻辑设计、以及嵌入式系统软硬件协同开发是一次无与伦比的实践。这个项目适合所有层级的硬件爱好者对于初学者它是一个从理论到实践的绝佳桥梁对于有经验的工程师它则是一个验证自定义外设或研究历史架构的沙盒。最终你将得到一块能运行CP/M操作系统、玩复古游戏或者控制真实物理世界的“万能”Z80计算机核心。2. 核心设计思路与架构选型2.1 为什么选择Z80与Cyclone IV FPGA的组合选择Z80作为核心CPU其吸引力在于它的简洁性与历史地位。与现代动辄数十级流水线、复杂缓存架构的处理器相比Z80的架构相对简单直接一个累加器、几个通用寄存器、一个简单的ALU和清晰的总线接口。这种简单性使得用HDL硬件描述语言描述其行为变得可行。你可以在网上找到多个经过验证的开源Z80核心如T80核心这为项目提供了一个可靠的起点让你可以专注于系统集成而非从零设计CPU。而选择Altera Cyclone IV EP4CE系列FPGA则主要基于资源、生态与成本的考量。一块典型的EP4CE6或EP4CE10芯片其逻辑单元LE数量在6K到10K之间这对于实现一个Z80核心约1200-1800 LE、一些片上内存Block RAM以及若干外设控制器如UART、定时器、GPIO来说绰绰有余。其内置的PLL可以方便地生成系统所需的各种时钟而丰富的I/O引脚则便于连接外部SDRAM、Flash、VGA或PS/2键盘等设备。更重要的是Cyclone IV拥有庞大且成熟的用户社区Quartus II开发工具现在已整合到Quartus Prime Lite中的授权免费相关的教程、参考设计和调试经验非常丰富能极大降低学习曲线和排错成本。2.2 整体系统架构设计一个最基本的Z80 SBC系统架构可以抽象为以下几个核心部分它们通过Z80的系统总线地址总线、数据总线、控制总线连接在一起Z80 CPU Core项目的“大脑”。它从总线读取指令和数据执行运算并控制总线访问。时钟与复位电路为整个系统提供同步时钟例如3.6864MHz便于产生标准的串口波特率和可靠的上电复位、手动复位信号。地址解码器这是系统的“交通警察”。Z80发出的16位地址线需要被解码以决定当前访问的是哪个设备ROM、RAM还是外设。例如我们可以定义地址0x0000-0x7FFF为ROM空间0x8000-0xFFFF为RAM空间而将0xFF00-0xFFFF的某些特定地址映射给外设。存储器系统ROM (Read-Only Memory)通常用FPGA内部的Block RAM实现用于存放系统的监控程序Monitor、BASIC解释器或者CP/M的BIOS。上电后CPU从0x0000地址开始执行ROM中的代码。RAM (Random-Access Memory)同样使用Block RAM实现作为系统的运行内存。如果所需RAM较大如512KB则需要使用FPGA外部连接的SRAM或SDRAM芯片并设计相应的控制器。输入/输出 (I/O) 设备UART (串口)这是最重要的调试和交互接口。通过一个UART IP核可以将Z80的并行数据转换为串行数据通过FPGA的某个引脚连接到USB转串口芯片如CH340、FT232从而实现与PC终端软件的通信。定时器/计数器可以用于产生精确的时间间隔实现软件延时或实时时钟功能。通用I/O (GPIO)将一组FPGA引脚直接映射到Z80的I/O地址空间用于控制LED、读取开关状态或连接其他简单外设。注意在FPGA内部ROM和RAM通常都用Block RAM实现但它们的初始化方式不同。ROM的初始内容需要在Quartus的.mifMemory Initialization File文件中定义并在综合时被编译进比特流文件。而RAM的内容是易失的上电后为随机值需要由程序初始化。2.3 总线交互与同步设计要点Z80总线是典型的异步总线这意味着读写时序需要外设或我们的解码器/存储器根据/MREQ、/IORQ、/RD、/WR等控制信号来响应。在FPGA中实现时一个关键决策是采用同步设计还是保持异步接口我强烈推荐将Z80核心与外部总线交互的部分进行同步化封装。具体做法是在Z80核心外部包裹一个“总线接口单元”。这个单元在系统时钟的上升沿采样Z80发出的异步控制信号和地址数据并产生同步的使能、读写信号给内部的Block RAM和外设控制器。同时它也需要在适当的时钟周期后将读取的数据同步地送回到Z80的数据总线上。这样做的好处是整个FPGA内部的设计都是同步的便于时序分析、约束和避免亚稳态问题极大地提高了系统的稳定性和可移植性。例如一个简化的同步读操作流程可以是在时钟上升沿检测到/MREQ和/RD同时为低锁存当前地址Addr_reg。根据Addr_reg和地址解码结果生成对应存储器或外设的read_enable信号。存储器或外设在下一个时钟周期输出数据Data_out。总线接口单元将Data_out锁存并在Z80核心要求的建立时间前驱动到Z80的数据输入引脚上。3. 关键模块的详细实现与调试3.1 Z80 CPU核心的集成与配置开源社区有几个优秀的Z80核心实现例如T80、T80s或TV80。以T80为例将其集成到你的项目中通常只需实例化一个模块并连接好时钟、复位、总线接口以及可选的WAIT、INT、NMI等信号。// 示例T80核心实例化 t80s #( .Mode(0), // 0 Z80, 1 Fast Z80, 2 8080, 3 GB .T2Write(1), // 1 在T2周期写经典Z80时序 .IOWait(1) // 1 插入等待周期于I/O操作 ) cpu ( .RESET_n(~reset), // 低电平复位 .CLK(clk), // 主时钟 .CEN(1‘b1), // 时钟使能通常接1 .WAIT_n(wait_n), // 等待请求低电平有效 .INT_n(int_n), // 可屏蔽中断 .NMI_n(nmi_n), // 非屏蔽中断 .BUSRQ_n(1’b1), // 总线请求未使用可接1 .M1_n(m1_n), // 机器周期1可用于中断响应判断 .MREQ_n(mreq_n), // 存储器请求 .IORQ_n(iorq_n), // I/O请求 .RD_n(rd_n), // 读信号 .WR_n(wr_n), // 写信号 .RFSH_n(ref_n), // 刷新信号连接DRAM控制器时有用 .HALT_n(halt_n), // 暂停状态 .BUSAK_n(busak_n),// 总线响应 .A(addr_bus), // 16位地址总线 .DI(data_in), // 8位数据输入CPU读取 .DO(data_out) // 8位数据输出CPU写入 );关键配置与调试心得时钟使能CEN如果你的系统时钟频率远高于Z80的设计频率如50MHz你需要通过一个分频器产生一个较低频率的cen信号如3.6864MHz连接到CEN引脚而不是直接将高速时钟接给CLK。这能保证CPU以正确的速度执行指令。等待状态WAIT_n如果你的存储器或外设响应速度慢无法在一个时钟周期内完成操作你需要拉低WAIT_n信号通知CPU插入等待周期。在纯Block RAM系统中通常不需要。复位处理确保复位信号RESET_n有足够的持续时间例如保持低电平至少4-5个系统时钟周期让CPU内部状态机完全复位。可以使用一个简单的计数器来产生可靠的上电复位脉冲。3.2 存储器子系统的设计与映射存储器设计是SBC的骨架。我们假设设计一个64KB的地址空间。地址解码逻辑使用简单的比较器或case语句实现。// 简单的地址解码示例 always (*) begin rom_cs (addr_bus[15] 1‘b0); // 假设A150为ROM (0x0000-0x7FFF) ram_cs (addr_bus[15] 1’b1); // 假设A151为RAM (0x8000-0xFFFF) uart_cs (iorq_n 1‘b0) (addr_bus[7:0] 8’h00); // I/O地址0x00为UART endROM实现使用Quartus的ROM: 1-PORTIP核。关键步骤是创建.mif文件。你可以用汇编器如zasm将你的监控程序汇编成二进制.bin然后使用工具或自己写脚本转换成.mif格式。在IP核配置中指定初始化和数据宽度8位。RAM实现使用RAM: 1-PORT或RAM: 2-PORTIP核。双端口RAM有时很有用例如一个端口给Z80另一个端口给未来可能添加的VGA显示控制器直接读取显存。注意配置读写操作是同步还是异步为了时序一致性建议全部使用同步读写。一个常见的坑Block RAM有输出寄存器。当你使能rom_cs和rd_n后数据并不会立即出现在输出端口而是会在下一个时钟沿出现。如果你的总线接口单元设计为组合逻辑直接输出就会读到错误数据。务必查阅IP核的时序图确保你的控制逻辑满足其建立/保持时间要求通常需要将读操作设计为流水线式延迟一个周期取数。3.3 串口UART通信的实现UART是系统的“嘴巴”和“耳朵”。实现一个UART控制器包括波特率发生器、发送器和接收器。波特率发生器使用一个计数器从系统时钟如50MHz分频产生16倍于目标波特率的时钟信号例如对于115200波特率需要50MHz / (115200 * 16) ≈ 27的计数值。这个16倍时钟用于对接收线进行过采样提高抗干扰能力。发送器TX一个状态机在写使能信号到来时将CPU写入的数据位加上起始位、停止位按波特率时钟逐位移出到TX引脚。接收器RX一个状态机使用16倍波特率时钟检测起始位下降沿然后在数据位的中间点采样RX引脚将串行数据组装成并行字节并设置“接收数据就绪”标志。寄存器映射通常映射两个I/O地址数据寄存器0x00写入时启动发送读取时获取接收到的字节。状态/控制寄存器0x01包含“发送缓冲区空”、“接收数据就绪”等状态位以及可能的中断使能位。调试UART的黄金法则先让TX工作。写一个最简单的汇编测试程序循环向UART数据寄存器写入字符‘A’0x41的ASCII码。用逻辑分析仪或示波器直接测量FPGA的TX引脚你应该能看到标准的串行波形低电平起始位8位数据位高电平停止位。确认硬件波形正确后再连接USB转串口模块到PC用终端软件如Putty、Tera Term查看。如果PC端收到乱码99%是波特率不匹配请仔细检查你的波特率分频计算和系统时钟频率。4. 系统集成、上电与初级测试4.1 “Hello World”监控程序的编写与烧录当硬件框架搭建好后我们需要一段“灵魂”代码——监控程序Monitor。它通常用Z80汇编语言编写其最基本的功能就是通过串口与用户交互。一个最简单的监控程序流程如下初始化栈指针SP指向RAM的顶端。初始化UART设置波特率如果需要。向串口发送一个欢迎字符串例如“Z80 SBC Ready ”。进入主循环从串口读取一个字符然后回显发送同一个字符。将这段汇编代码用汇编器如zasm或vasm编译成二进制文件.bin。然后你需要将这个二进制文件转换成Quartus ROM IP核能识别的.mif或.hex格式。这里可以使用Python脚本或一些现成的小工具。最后在Quartus中重新编译整个工程生成新的.sofSRAM Object File文件。使用USB Blaster或其他JTAG下载器将.sof文件烧录到Cyclone IV FPGA中。上电后打开PC上的串口终端设置正确的波特率如115200、8N1你应该能看到“Z80 SBC Ready ”的提示。键入字符终端会回显这证明从CPU、ROM、RAM到UART的整个数据通路是通的。4.2 内存测试与诊断程序开发回显测试通过后下一步是验证RAM的可靠性。编写一个内存测试程序其思路是向一段RAM区域例如0x8000开始写入一个已知的模式如0xAA、0x55或递增数列。再从该区域读取数据与写入的值逐一比较。如果全部匹配通过串口输出“RAM Test PASSED”否则输出失败地址和数值。这个测试能暴露出地址解码错误、RAM读写时序问题。如果测试失败首先用逻辑分析仪抓取Z80总线信号。观察在写入和读取时地址线、数据线、/MREQ、/WR、/RD的时序是否符合Z80的规范。特别检查WAIT_n信号是否被误触发。4.3 中断系统的实现与测试Z80强大的功能之一是其可嵌套的中断系统模式0、1、2。实现中断可以极大地提高系统效率例如串口接收数据时无需轮询。步骤包括中断源使能UART的“接收数据就绪”中断。当UART收到一个字节时产生一个高脉冲作为中断请求信号。中断控制器简单版由于中断源少可以直接将UART中断请求连接到Z80的INT_n引脚。在软件中通过EI指令开中断。中断服务程序ISR在ROM中固定位置如模式1的中断向量地址0x0038编写ISR。它的任务是保存现场将AFBC等寄存器压栈从UART数据寄存器读取字节处理例如放入缓冲区然后恢复现场并用RETI指令返回。测试编写主程序开中断后进入一个空循环。通过串口发送数据如果系统能正确进入ISR并处理例如将收到的字节回显并加一个特殊标记则中断系统工作正常。实操心得中断调试比较棘手。一个有效的办法是在ISR的开始通过一个特定的GPIO引脚输出一个短脉冲用示波器观察。当你发送串口数据时如果能看到这个脉冲证明CPU确实响应了中断并跳转到了ISR。然后再逐步调试ISR内部的逻辑。5. 高级功能拓展与性能优化5.1 连接外部存储器与内存管理片上Block RAM资源有限EP4CE6只有276Kbits。要运行CP/M这类“大型”系统需要外部RAM。常见的方案是连接一片512KB或1MB的静态RAMSRAM如IS61LV51216。这需要设计一个外部存储器控制器。该控制器的核心是将Z80的异步总线时序转换为符合SRAM芯片读写的同步时序。它需要处理地址线扩展Z80只有16位地址线64KB需要通过分页Bank Switching机制访问更大内存。例如使用一个输出端口I/O写来选择一个“页”将Z80的64KB窗口映射到外部RAM的不同256KB区域。时序生成根据SRAM的数据手册产生满足tRC读周期时间、tAA地址访问时间等参数的/CE片选、/OE输出使能、/WE写使能信号。等待状态插入外部SRAM的速度可能跟不上全速运行的Z80此时控制器需要拉低Z80的WAIT_n信号插入等待周期。5.2 添加视频输出VGA/HDMI与键盘输入要让SBC更像一台真正的电脑视频输出和键盘输入必不可少。VGA显示实现一个VGA控制器产生标准的行同步HSync和场同步VSync信号并根据像素坐标从显存中读取颜色值输出。显存可以是一块双端口RAM一个端口由VGA控制器以像素时钟读取另一个端口由Z80 CPU写入。Z80可以通过I/O指令或内存映射方式将显存映射到CPU地址空间来绘制图形或显示字符。字符显示通常需要一个字符发生器ROM将ASCII码转换成点阵图案。PS/2键盘实现一个PS/2协议解码器。PS/2键盘发送串行数据控制器需要准确采样数据线在时钟下降沿读取数据位组装成扫描码并通过中断或状态位通知CPU读取。性能权衡添加VGA控制器会消耗大量的逻辑资源和内存带宽。在Cyclone IV上实现一个640x48060Hz的8色显示可能需要一个至少几KB的显存和一个运行在25MHz左右的像素时钟域。你需要使用PLL生成这个像素时钟并注意跨时钟域CDC问题当CPU写入显存时系统时钟域VGA读取像素时钟域需要用到异步FIFO或双端口RAM的真双端口特性来安全传递数据。5.3 运行经典操作系统CP/M 2.2这是项目的“毕业设计”。CP/M是Z80时代的标志性操作系统。要让其运行起来你需要准备CP/M BIOSCP/M由BIOS基本输入输出系统、BDOS基本磁盘操作系统和CCP控制台命令处理器组成。BIOS是硬件相关的你需要根据你的SBC硬件串口作为控制台、可能的内存盘“虚拟磁盘”来修改或重写BIOS的源码。这包括实现CONST检查控制台状态、CONIN控制台输入、CONOUT控制台输出、SELDSK选择磁盘、SETTRK设置磁道、SETSEC设置扇区、READ/WRITE读写扇区等函数。构建磁盘映像将修改编译后的BIOS与标准的BDOS、CCP链接并创建一个虚拟的磁盘映像文件通常是一个.img文件里面包含CP/M系统文件和你的测试程序。加载与引导你需要编写一段位于ROM开头的引导加载程序Bootloader。它的任务是通过某种方式例如从SD卡、或者就简单地从UART接收将磁盘映像文件的内容加载到RAM中指定的位置通常是0x0100开始的TPA区然后跳转到BIOS的冷启动入口点。运行如果一切顺利系统启动后你会看到CP/M的命令提示符“A”。你可以尝试运行内置命令如DIR或者将一些经典的CP/M软件如ZORK文字冒险游戏拷贝到磁盘映像中运行。这个过程会挑战你对Z80汇编、硬件接口和操作系统引导流程的全面理解。成功在你自己设计的FPGA SBC上看到“A”提示符那种成就感是无与伦比的。6. 调试技巧、常见问题与避坑指南6.1 调试工具箱从仿真到逻辑分析仪仿真Simulation是第一步在烧录FPGA前务必用ModelSim或Quartus自带的仿真工具进行彻底的RTL级仿真。编写测试平台Testbench模拟时钟、复位并模拟CPU的读写操作。观察总线信号、内部状态机是否按预期变化。这是定位设计逻辑错误最高效的方法。嵌入式逻辑分析仪SignalTap II这是Altera FPGA独有的强大工具。它允许你在FPGA运行时实时捕获内部信号的波形。当你发现系统行为异常如程序跑飞、数据错误时可以设置触发条件例如当某个地址被写入特定值时抓取相关信号其效果等同于一个价格高昂的数字逻辑分析仪直接探入芯片内部。务必合理设置采样深度和触发条件避免错过关键信号。“LED大法”与“UART打印法”最朴素但有效。将关键状态信号如CPU的HALT_n、M1_n或某个状态机的状态寄存器分配到FPGA的闲置引脚外接LED。通过LED的亮灭模式可以判断程序是否在运行、卡在何处。在代码中插入串口打印语句输出特定字符可以追踪程序的执行流。6.2 常见问题速查表现象可能原因排查思路上电后无任何串口输出1. CPU未正确复位。2. 时钟未正确供给CPU。3. ROM未正确初始化或地址映射错误。4. UART波特率设置错误或硬件连接问题。1. 用示波器检查复位信号波形和持续时间。2. 用示波器/逻辑分析仪检查CPU的CLK/CEN引脚是否有时钟活动。3. 用SignalTap查看CPU的地址总线看它是否从0x0000开始取指以及ROM的数据输出是否正常。4. 先用逻辑分析仪抓取FPGA的TX引脚原始波形核对波特率。串口输出乱码1. 波特率不匹配最常见。2. 数据位、停止位设置错误。3. 信号电平问题如FPGA是3.3V LVCMOS而USB转串口模块期望5V。1. 双重检查波特率分频计算和系统主频。2. 确认终端软件和UART IP核的帧格式8N1一致。3. 检查硬件连接必要时使用电平转换芯片。程序运行一段时间后死机1. 时序违例亚稳态传播。2. 栈指针SP设置错误导致栈溢出覆盖了程序代码。3. 中断处理不当未正确保存/恢复现场。4. 电源噪声或去耦不足。1. 在Quartus中运行时序分析TimeQuest解决所有红色警告建立/保持时间违例。2. 检查汇编程序中SP的初始化值是否在有效的RAM高端地址。3. 仔细检查中断服务程序确保所有用到的寄存器都被压栈/出栈。4. 检查PCB电源走线在FPGA的每个电源引脚附近放置足够的去耦电容0.1uF。读写特定内存地址时数据错误1. 地址解码逻辑有误导致多个设备同时响应。2. 存储器IP核的读写时序与Z80总线接口不匹配。3. 总线冲突多个驱动源同时驱动数据线。1. 用SignalTap同时观察地址线、各片选信号rom_csram_cs确保同一时刻只有一个为低。2. 仔细比对Z80读周期时序图和Block RAM IP核的读时序图在接口模块中插入正确的等待周期或锁存数据。3. 确保当CPU不输出数据时WR_n为高且RD_n为高时其数据输出端应为高阻态。6.3 资源优化与设计建议节省Block RAMZ80系统启动后ROM中的监控程序可能只需要在初始化时用到。可以考虑实现一个“ROM禁用”机制通过一个I/O端口写操作将ROM的片选关闭并将其地址空间重新映射给RAM。这样整个64KB地址空间都可以作为RAM使用方便运行大型程序。使用参数化设计将系统时钟频率、波特率、内存大小等关键参数用parameter或define定义在顶层模块或专门的配置文件中。这样移植到不同型号的Cyclone IV芯片或改变系统频率时只需修改少数几个参数无需翻遍所有代码。版本控制与注释使用Git管理你的项目代码。为每个模块编写清晰的注释特别是状态机的状态定义和复杂的时序逻辑。几个月后当你回头添加新功能时你会感谢当初写了注释的自己。在Cyclone IV FPGA上构建Z80 SBC的旅程是一次从软件思维到硬件思维从高层应用到底层硬件的深刻穿越。每一个问题的解决都让你对“计算机如何运作”这个根本问题多一分理解。当最终你的系统稳定运行并开始执行你亲手编写的程序时那种对系统的完全掌控感和创造带来的愉悦是使用现成开发板无法比拟的。这个项目没有绝对的终点你可以不断为其添加硬盘控制器、声卡、网卡甚至连接真实的复古硬件它就是一个属于你的、不断进化的数字世界实验室。