FPGA中集成DW8051软核:选型、例化与调试实战指南
简介本资源是一套基于Verilog实现的dw8051 8051兼容软核完整FPGA工程专为数字电路与嵌入式系统开发者设计适用于FPGA初学者进阶学习及MCU软核移植实践。资源已在Atlys开发板上成功验证包含可综合的dw8051顶层模块、预生成ROM/RAM存储器、4组并行I/O端口逻辑以及配套的HEX转IN格式工具和Uedit文本编辑器便于用户快速烧录和调试自定义程序。压缩包共1172个文件总计27.85MB涵盖Verilog源码.v、约束文件.ucf、仿真脚本.tcl、日志与报告.log/.xrpt、位流配置.bit及大量ISE/XST编译中间文件.ngc/.ncd/.xise等结构完整、开箱即用。已有321人下载学习特别适合需要理解8051架构在FPGA中映射原理、掌握软核集成流程与存储器初始化方法的硬件开发人员。 最近在整理一份老项目的FPGA工程随手打开那个压缩包里面躺着一份DW8051的Verilog源码和一堆仿真脚本。这让我想起最初在FPGA上摆弄这个8位软核的日子——当时光是把程序跑起来就折腾了整整三个晚上一度怀疑是自己不会写Verilog。如果你正准备在FPGA里跑一个8051软核或者拿到一份带DW8051的工程不知道怎么下手这篇就当是趟过河的人给你画的水深图。DW8051这个名字在FPGA圈子里不陌生它是Synopsys DesignWare库里的一款8位MCU内核兼容经典8051指令集但比老掉牙的Intel 8051快不少典型配置下能跑到几十兆甚至上百兆的主频。配合FPGA里的片上SRAM、外设总线它非常适合做协议解析、状态控制、命令预处理这类“不复杂但灵活”的活儿。一个很典型的场景是FPGA负责高速信号处理DW8051作为软核承担系统管理、参数配置和通信协议两者各干各的既省了一颗外部MCU又不用在FPGA里写一堆难以维护的状态机。整篇我会按照实际动手的顺序来写先聊为什么在FPGA里选8051类软核再讲DW8051的例化和总线接入然后是仿真与板级调试接着拓展一套参数化的I2C/分频/UART外设最后把我在这个工程里踩过的坑集中交代一遍。内容基于我的实战经验也会对比说明常见的开源替代方案适合正在做FPGA软核集成、或者准备从零开始移植8051内核的同学参考。1. 为什么偏偏是DW8051FPGA里塞8位MCU的工程账1.1 三个8051软核候选DW8051、MC8051与OpenCores实现决定在FPGA里用软核之前我其实犹豫过一阵子。手头能选的8051软核大致有三类第一类是Synopsys DesignWare里的DW8051商业IP代码严谨、文档齐全很多公司有DesignWare授权可以直接用第二类是OpenCores上开源的MC8051VHDL写的结构清晰适合学习和二次开发第三类是各种个人维护的Verilog 8051核质量参差不齐。三个方案我实际都碰过写个对比给你看方案语言授权时序质量上手难度DW8051VerilogDesignWare授权高经过大量验证中端口多但结构规整MC8051OpenCoresVHDL开源中需自己调时序低代码量小个人8051核Verilog各不相同不稳定需逐条指令验证高坑多如果你所在单位或学校有DesignWare授权直接用DW8051是最省心的因为它的总线时序、存储接口和指令周期都经过了商业验证你只需要把它当做一个黑盒配置好参数。如果没授权MC8051是很好的替补它的架构和DW8051相似也是Harvard结构、SFR访问方式一致调试思路可以平移。1.2 授权、工具链和生态选型前先算三笔账很多人一上来就问“哪个8051核性能最好”但在FPGA工程里性能往往不是第一约束。我吃过亏之后总结出三笔账授权账、工具链账、生态账。授权账最要命。DW8051不是免费午餐它是DesignWare家族的成员使用前必须确认授权许可范围。如果你在公司项目里用先去找EDA工具管理员查清楚license是否包含DesignWare IP如果是个人学习可以申请Synopsys的评估许可或者干脆用OpenCores的MC8051。千万别从网上下个来路不明的源码就往公司工程里塞这属于给自己埋雷。工具链账指的是综合和仿真工具对IP的支持。DW8051的源码风格严格遵循RTL编码规范Vivado、Quartus、Design Compiler都能综合但你要注意DesignWare库里的某些工艺相关单元可能只面向ASIC放进FPGA工程时得把仿真库路径配好。我第一次在Vivado里综合DW8051时报了好几个“cannot find module”错误原因就是没把dw8051的依赖文件全部添加进工程。生态账更实在你选的8051核周围有没有配套的仿真脚本、启动代码、外设实例DW8051虽然商业但网上能找到不少高校论文和参考设计MC8051则因为开源论坛里的讨论很多遇到问题容易搜到答案。如果做一个长期维护的项目选生态好的方案能省非常多时间。2. DW8051顶层例化与总线接入从引脚图到Verilog连线2.1 认识DW8051的端口分组总线、控制、扩展拿到DW8051的源码后别急着往工程里拖先花半小时把顶层端口过一遍。DW8051的端口大致分三组系统组、存储器接口组、特殊功能组。系统组包括clock、reset_n、int0_n到int3_n这些基础信号时钟和复位命名很直白没有绕弯。存储器接口组是重点它严格按8051的Harvard结构分成了程序存储器总线PROM接口即ROM接口和片外数据存储器总线XRAM接口。PROM接口输出程序地址、读使能读回指令XRAM接口则承担数据读写。这两个接口都有各自的地址、数据、读写控制和等待请求信号。特殊功能组里最需要注意的是调试接口DW8051支持through-the-scan或者JTAG类调试口具体看配置。如果只是“跑起来”可以不接调试口但一定要留出测试探针否则后面定位问题全靠逻辑分析仪去抓内部信号很不方便。2.2 用parameter定制内核配置项逐个说清楚DW8051支持通过Verilog parameter调节内核配置这点非常友好。我常用的参数如下参数名可选值作用我的推荐INST_RAM_EN0/1是否使能内部指令RAM1省外部ROM逻辑RAM_MODE0/1内部RAM大小模式1256字节XRAM_SIZE越大越宽片外数据空间地址位宽视外部SRAM而定PROM_SIZE越大越宽程序空间地址位宽与代码大小匹配VARIABLE_STRETCH0/1是否支持可变等待周期1接慢速外设有用举个例子如果你的应用代码不超过8KB可以把PROM_SIZE设为13位8K地址内部RAM用256字节XRAM只留16位地址。这样综合出来的面积最小、频率最高。参数不只是写在例化那儿了事还要注意和实际挂载的存储体匹配。DW8051的例化文件里通常有一堆defparam或者顶层赋参数的地方Vivado里如果例化时用#(...)传参记得每个参数都核对一遍别偷懒。我见过一个同事把XRAM_SIZE设成64KB但实际SRAM只有16KB结果程序一访问高地址就回读全零排查了两天才发现是参数越界。2.3 将8051的哈佛总线挂到片内双口SRAM与AHB/APB桥DW8051的总线时序并不复杂但它假设外部有一个“能在一个周期或者经过等待后返回数据”的存储器。FPGA里最自然的做法是把PROM接口接到片内块RAMBlock RAM上把XRAM接口接到另一个块RAM上。我在实际工程设计里常用双口RAM方案一端接DW8051另一端接FPGA内部逻辑或者一个下载桥。这样有一个非常大的好处——调试初期你可以通过JTAG或者串口把一个“装载器”写进程序RAM再从装载器启动用户代码避免反复烧写比特流。下面是连接PROM接口的简化示意注意DW8051的prom_addr、prom_data、prom_wr_n等信号要跟BRAM的原语对应好:// DW8051 PROM interface - Block RAM wire [PROM_WIDTH-1:0] prom_addr; wire [31:0] prom_wdata; wire [31:0] prom_rdata; wire prom_wr_n; wire prom_wr_error; blk_mem_gen_0 prom_ram ( .clka(clk_50m), .wea(!prom_wr_n), .addra(prom_addr), .dina(prom_wdata), .douta(prom_rdata) );BRAM的读延迟一般是1个周期而DW8051在PROM接口上默认期望在一个时钟周期内完成取指这时候需要把DW8051的prom_wait_n信号拉低或者配置等待周期让它知道外部存储慢一拍。这个细节千万别忽略我就是因为没配置等待信号看到程序在取指阶段乱跳白查了半天的RTL逻辑。如果要外接更多设备比如UART、GPIO、I2C控制器建议建一个AHB或APB桥把DW8051的XRAM总线变成通用总线。但如果你只是做一个小设计直接在顶层写一个地址译码器就够用高位地址区分外设每个外设对应一个地址段读回来一个寄存器值。这种“土办法”胜在简单直观代码也好维护。3. 让它跑起来仿真、程序装载与板级调试三板斧3.1 写一个靠谱的testbench复位、时钟、握手时序DW8051的仿真其实不复杂但必须把复位时序和程序装载时序写对。我在testbench里通常会做这么几件事产生时钟和复位、模拟一个片外RAM初始化过程、运行足够长的周期后把数据通过仿真波形导出来。首先是复位。DW8051的复位信号是低电平有效我一般让它至少保持100个时钟周期再释放。为什么这么长因为内核里有些状态寄存器需要多个周期才能稳定初始化而且仿真模型里PLL或者BRAM初始化也需要时间。然后是PROM接口。在仿真里我用一个二维数组变量模拟程序存储器用initial块把hex文件里的机器码逐字节读进数组然后利用always (posedge clk)块响应DW8051发出的取指请求。这里最关键的是要把ROM读取延迟和DW8051prom_wait_n的时序对齐。下图是一个简单的仿真存储模型reg [7:0] prom_mem [0:8191]; wire prom_access !prom_wr_n; // 读请求 always (posedge clk) begin if (!prom_rst_n) prom_rdata 32h0; else if (!prom_wr_n) prom_rdata {prom_mem[prom_addr3], prom_mem[prom_addr2], prom_mem[prom_addr1], prom_mem[prom_addr]}; end仿真中我还习惯加上一个“看门狗”如果100微秒内程序没有执行到我们预设的断点就$display提示超时避免出现无休止的死循环仿真。实测下来这个法子很管用。3.2 烧进FPGA后的第一个坎程序是怎样装进XRAM的仿真全部通过之后烧到板子上你会遇到FPGA世界特有的问题程序区是空的。DW8051没有内部flash程序必须由外部烧进去。我在自己项目中做过三种装载方式按推荐程度排序BRAM初始化文件方式在生成BRAM IP时加载一个.coe或.mem文件比特流烧进去程序就已经在了。这种方式最简单适合demo和固定功能。串口装载器方式在FPGA里写一个小模块上电后先把串口接收到的程序通过XRAM接口写入外部RAM写完拉高一个标志再复位DW8051开始执行。这种方式适合调试不用反复重新综合工程。JTAG/Debug Access方式通过DW8051自带的调试口写入最灵活但需要额外的调试桥接逻辑工作量略大。我通常用的组合是第一次烧录用BRAM初始化文件验证基本功能之后改用串口装载器。串口装载器的核心就是一个状态机帧格式简单定成地址两字节 数据两字节 校验一字节FPGA端每收一个16字节包写到XRAM一个地址收完整个image以后产生一个上升沿给DW8051复位释放引脚。这种方式有一个非常容易被坑的点DW8051的XRAM接口是同步接口而串口异步采样的数据如果直接写进去会出现亚稳态。所以装载器里一定要用双寄存器同步串口的接收信号再打一拍写XRAM。3.3 板上调试别靠瞎猜抓总线、查时序、看波形程序能跑了不代表程序是对的。我第一次把LED点灯程序跑起来时兴奋了没几秒就发现串口打印是乱码。当时第一反应是波特率配错了折腾了很久才发现根因在时钟分频。这里分享一个调试方法在FPGA工程里把DW8051的prom_addr、prom_wr_n、xram_addr等信号引到ILA集成逻辑分析仪上触发条件设置成地址等于某个关键值比如C程序里某个函数的起始地址。然后观察这个地址出现前后总线数据是否符合预期。这套操作看起来简单但比在代码里插$display直观得多尤其是跑在硬件上时。另外我强烈建议在调试初期把DW8051的工作主频降到很低比如1MHz或2MHz然后把串口波特率调到9600或者更低。这样即使时序有一些亚稳态问题也能“低速度掩盖部分问题”先把逻辑正确性验证了再提频。4. 用Verilog给8051搭外设I2C、UART与参数化设计4.1 状态机实现I2C主机挂一颗EEPROM接下来聊聊外设扩展。8051老内核虽然计算能力一般但做外设控制绝对合适。我在这套工程里写过一个I2C主机控制器用它来读写一颗EEPROM用来存校准参数。I2C主机的核心就是一个状态机从IDLE开始依次进入START、发送地址、发送寄存器地址、发送数据、STOP等状态。常用的代码如下localparam S_IDLE 3d0; localparam S_START 3d1; localparam S_ADDR 3d2; localparam S_REG 3d3; localparam S_DATA 3d4; localparam S_STOP 3d5; always (posedge clk) begin if (!rst_n) state S_IDLE; else case (state) S_IDLE: if (start_i) state S_START; S_START: state S_ADDR; S_ADDR: if (bit_cnt 3d7) state S_REG; ... endcase end实际工程里I2C时序的坑在于SCL时钟的高低电平宽度必须满足器件手册要求。DW8051的主频如果是50MHz直接用50MHz分频得到400kHz的SCL一定要算清楚高电平时间和低电平时间。EEPROM的WP引脚、上拉电阻也别忘了否则挂在总线上设备地址对不上读出来全是0xFF。另外如果你的I2C总线上有多个器件务必给每个器件分配不同的7位地址并且不要照抄寄存器手册里的8位读/写地址。我第一次写驱动时把器件地址的左移一位和读/写位搞混了结果主机一直收不到ACK。4.2 参数化的分频器与计数器一种写法适配多时钟FPGA里最常用的模块不是复杂的算法而是分频器和计数器。DW8051跑50MHz但串口需要115200波特率I2C需要400kHzPWM需要1kHz这些都是对同一个时钟分频得来的。我推荐用Verilog的parameter把分频系数做成可配置的比如下面这个通用分频器module clk_div #( parameter DIV_N 50000000 )( input wire clk, input wire rst_n, output reg clk_out ); localparam CNT_WIDTH $clog2(DIV_N); reg [CNT_WIDTH-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_out 0; end else if (cnt DIV_N/2 - 1) begin cnt 0; clk_out ~clk_out; end else begin cnt cnt 1; end end endmodule这个模块在例化时给不同的DIV_N就能得到不同频率。注意一点对于“精确到每个周期”的串口波特率DIV_N选定时要注意误差控制在2%以内。例如50MHz产生115200波特率分频系数是434.027取434时误差只有0.006%完全够用。4.3 把外设模块封装成AXI-Lite让8051像访问XRAM一样访问外设当外设变多之后单纯的地址译码器会变得混乱。我是后来才痛下决心把常用的外设模块统一封装成AXI-Lite从机接口然后用一个AXI-Lite桥接到DW8051的XRAM总线上。好处是显而易见的外设寄存器地址被统一到4KB的地址空间每个外设占一段读写操作看起来就像访问一组连续地址。举个例子I2C控制器的状态寄存器映射到0x4000_0000数据寄存器映射到0x4000_0004。DW8051里只要用外部数据存储器的访问指令就能操作这些寄存器。这个封装过程有一点要注意AXI-Lite的握手和DW8051的XRAM等待周期不是一一对应的。DW8051发出读请求后如果AXI从机准备数据需要若干周期就得把xram_wait_n信号拉低让DW8051进入等待状态直到AXI从机返回有效数据。这个过程我在第一次接的时候忽略掉了结果每次读外设寄存器返回的都是上一次的值数据全错位。5. 实测踩坑记录时钟、复位、差分引脚与内存映射5.1 PLL配置里的相位和抖动直接决定串口波特率准不准FPGA的全局时钟网络虽然质量不错但DW8051内核对外部时钟的要求比我预想的要高。我用Vivado的PLL IP给DW8051生成125MHz时钟时一开始没有设置phase导致内核时钟沿和外部RAM时钟沿之间存在不可控的相位偏移。程序跑是能跑但串口偶尔会多收或少收一个字节。排查过程比较痛苦最后是在ILA里同时抓了内核时钟和RAM读写信号发现数据建立时间总是差了一点点。解决方法是把PLL输出时钟的phase往前提了90度左右同时把RAM接口的输入寄存器打一拍。这样时序裕量一下子上来了串口再没出现过偶发错字。如果你用的不是PLL而是直接把开发板晶振时钟引给DW8051那就要格外小心时钟树的延迟。最好加一个BUFG让时钟在全局网络上走一圈。5.2 异步复位异步释放概率性启动失败的元凶这个坑几乎每个FPGA工程师都会遇到。DW8051的复位信号如果直接从外部按键或芯片引脚接入不同模块的复位释放时间可能相差几个纳秒导致内核里有些寄存器已经进入工作状态有些还在复位出现小概率的启动失败。解决方法是采用异步复位同步释放电路。所谓同步释放就是先把异步复位信号在时钟域里打两拍再输出给内核复位。代码大概是reg rstn_r1, rstn_r2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rstn_r1 1b0; rstn_r2 1b0; end else begin rstn_r1 1b1; rstn_r2 rstn_r1; end end assign cpu_reset_n rstn_r2;这样复位释放只发生在时钟上升沿不会出现在任意时刻。我加了这行代码后反复上下电几十次都稳定启动之前10次里大概有1次会直接卡死。5.3 DXP/DXN不是摆设LVDS差分引脚接错导致的白屏有一次我在扩展板上接了一个LVDS接口的LCD屏FPGA端的差分引脚名字叫DXP和DXN。我图省事直接把它们当普通IO口例化结果屏白屏怎么调时序都没用。后来才发现LVDS差分对在Verilog里不能随便用assign驱动得通过原语或者IO电平标准来定义。在Vivado里正确的做法是在XDC约束文件里把引脚电平配置成LVDS并在代码中把差分对声明为(* IOSTANDARD LVDS *)。如果核里要求输出单端信号但外部是差分还需要一个OBUFDS原语转一下。原理是FPGA IO Bank里的差分缓冲器接收差分信号否则DXP和DXN根本不会被识别为一对数据自然全乱。这个教训后来让我养成一个习惯任何涉及物理引脚的功能先看板卡的原理图确认电平类型再写代码。5.4 内存映射错位当程序指针跳到诡异地址最后一个坑是内存映射。DW8051的程序空间和数据空间虽然分开但如果你把外部程序存储器的大小配得比实际代码大程序指针不会自动约束在合法范围内它可能跳转到外部不存在的地址读回来的指令是全零然后执行0x00被视为NOP或者进入未知状态。我的C代码里用了一个绝对跳转编译器把目标地址生成了一个24位的常量但DW8051的程序地址引脚只接了低16位高8位悬空后随机接地或接电源结果跳转到的地址就“飘”了有时能跑有时跑飞。查到最后我重新检查了编译器输出的map文件确认所有函数和数据的地址都在低16位内同时把PROM地址位宽参数调成与BRAM实际大小一致问题才解决。如果你自己写启动代码建议在链接脚本里明确指定程序段、数据段和栈的位置比如把栈放在XRAM的末尾。这样即使程序跑飞最大概率是访问到栈空间而不是跳到空白地址。我个人在实际项目里的体会是DW8051这类8位软核虽然在性能上被ARM Cortex-M系列甩开几条街但在FPGA里做系统管理、协议控制这些任务时它的轻量、可控、易调试特点还是很吃香的。如果你现在正对着DW8051的例化文件无从下手先别急着改代码把存储映射、时钟复位、外设分频这三个基础点按顺序打通后面就会顺畅得多。最后再提醒一句拿到工程压缩包先看版本记录DW8051在不同DesignWare版本里的顶层端口名可能略有差异代码不是灵丹妙药时序才是。本文还有配套的精品资源点击获取