十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA控制DM9000A实现以太网通信的Verilog设计详解

FPGA控制DM9000A实现以太网通信的Verilog设计详解 简介面向FPGA开发者和嵌入式硬件工程师这套Verilog实现的DM9000A以太网收发方案覆盖寄存器读写、初始化流程、PHY配置、时序控制与仿真验证等关键环节。资源包共71个文件约2.53MB以.v源码模块为主含Quartus工程文件、仿真波形、综合报告及DM9000A官方手册另附接口全图和应用笔记便于对照理解引脚连接和状态机。已有624人学习下载。包内提供Dm9000a_Init、Dm9000a_IOWR、phy_write等可复用模块及完整工程配置可直接参考板级调试也可借鉴DMA传输、寄存器读写时序和中断处理思路用于构建更完整的网络通信系统。对想深入MAC与PHY交互细节的开发者这是一份实用的参考资源。1. 项目概述与设计思路1.1 DM9000A 这颗芯片到底是什么做 FPGA 网络通信绕不开以太网控制器这个话题。DM9000A 是 Davicom 出品的一颗 10/100M 自适应以太网芯片内部集成了 MAC 和 PHY外部走 16 位或 8 位并行总线接口直接通过寄存器读写就能完成收发包操作。这颗芯片在国产 FPGA 开发板、嵌入式系统里用得非常多原因很简单接口简单、驱动成熟、资料多不像 RTL8211 那种纯 PHY 需要自己处理 MAC 层逻辑也不像 W5500 那样内部带协议栈导致灵活度受限。用 FPGA 控制 DM9000A核心工作就是通过 Verilog 在逻辑层实现一套总线读写控制器把 DM9000A 当成一个挂在 FPGA 外部总线上的从设备来操作。这个项目的典型应用场景是FPGA 作为数据采集或信号处理前端需要把处理结果通过网络发送给上位机或者上位机下发控制参数FPGA 接收后解释执行。CSMA/CD、MAC 帧封装、CRC 校验这些工作 DM9000A 的硬件已经做了FPGA 侧只需要关心数据怎么搬进去、怎么搬出来。这里要注意一个理解上的坑DM9000A 不是 PHY 芯片它是 MACPHY 二合一所以 FPGA 侧的接口是内存映射式寄存器接口不是 MII/GMII 接口。1.2 方案选型背后的考量我在做这个项目之前对比过几种方案用 RTL8211自己写 MAC、用 DM9000A、用 W5500甚至考虑过直接用 FPGA 内嵌的千兆 MAC 硬核比如 Xilinx 的 TEMAC。最后选了 DM9000A主要看重三点接口友好16 位总线读写时序和 SRAM 差不多用状态机就能搞定不需要 DDR 对齐之类的高级技巧。PHY 寄存器可配内部 PHY 通过寄存器访问可以读取链路状态、协商速率方便调试。资料丰富Datasheet 写得很清楚网上例程也很多遇到问题容易找到参考。当然 DM9000A 也有痛点芯片比较老只能跑到 100Mbps突发吞吐能力有限内部 SRAM 只有 16KB 的接收缓冲和 2KB 的发送缓冲。但对于绝大多数教学演示和中低速数据采集场景它完全够用。1.3 整体设计目标这个项目的目标是实现一个可复用的 DM9000A 控制器 IP 核包含初始化、发送、接收三大功能模块用状态机实现总线时序对外提供简单的 FIFO 接口方便上层逻辑直接对接。设计上我给它定了三个硬性指标初始化过程完全自动化上电后 FPGA 自动完成 DM9000A 的复位和寄存器配置无需外部 CPU 干预。发送通道支持最多 1518 字节的标准以太网帧接收通道支持广播帧、单播帧和组播帧过滤。整个控制器模块化设计数据通路和控制通路分离后续移植到其他 FPGA 平台时只改顶层引脚约束即可。2. 硬件连接与接口时序分析2.1 引脚连接的关键细节DM9000A 的对外接口主要包括16 位数据总线 SD[15:0]、地址线 SA[4:2]用于选择内部寄存器页、CMD 引脚区分地址周期和数据周期、读写信号 IOR/IOW、片选 CS、复位信号 PWRST# 和中断输出 INT。在一个典型项目中FPGA 侧的连接方案是DM9000A 引脚FPGA 引脚方向说明SD[15:0]inout双向数据总线方向由读写信号控制SA[4:2]output寄存器地址通常接到 GPIO 的 3 根线CMDoutput高电平为数据周期低电平为地址周期IORoutput读选通低有效IOWoutput写选通低有效CSoutput片选低有效PWRST#output硬件复位低有效复位后拉高INTinput中断请求高有效可配置为电平或脉冲这里有一个非常值得注意的细节SA[4:2] 只有 3 根线这决定了 DM9000A 的寄存器地址空间被映射到 8 个字节缝里所以驱动代码里访问寄存器时地址要左移 2 位再交给地址线。很多初学者第一次写代码时直接用寄存器地址驱动 SA结果读写永远失败就是这个原因。硬件连接时还需要在数据总线上加上拉电阻避免总线悬浮导致芯片误判。2.2 读写时序到底是怎样的DM9000A 的读写时序本质上就是一个异步 SRAM 时序核心操作分两步先写地址到地址周期再读写数据到数据周期。CMD 引脚在整个过程中扮演了“当前周期是地址还是数据”的角色。写寄存器时序分成两个阶段。第一阶段是地址周期拉低 CMD把目标寄存器地址放到 SA[4:2] 和 SD[7:0] 上然后拉低 IOW 并保持至少 20ns最后拉高 IOW。第二阶段是数据周期拉高 CMD把要写入的数据放到 SD[15:0] 上然后拉低 IOW 保持至少 20ns最后拉高 IOW。两次 IOW 脉冲完成一次寄存器写操作。读寄存器时序略有不同第一阶段同样拉低 CMD 写地址第二阶段拉高 CMD 后拉低 IOR等待 DM9000A 将数据驱动到总线上芯片手册要求至少等待 80ns然后 FPGA 采数据最后拉高 IOR。这里的关键是建立时间和保持时间。DM9000A 的 datasheet 里有一张 AC 特性表对 IOW/IOR 的最小脉宽、数据建立时间等都给出了明确参数。写 Verilog 时序控制时我习惯用系统时钟周期数来计比如用 50MHz 时钟一个周期是 20ns那么 IOW 低电平时间至少要保持 2 个时钟周期才保险。太短的脉冲宽度会导致寄存器读写偶发失败表现为“读出来的值有时候对有时候不对”。3. DM9000A 的寄存器体系与初始化流程3.1 必须掌握的寄存器速查表DM9000A 内部寄存器分为几个页通过 0x00~0x1F 这个范围的地址索引访问。项目中我实际用到的关键寄存器如下地址名称读写功能描述0x00NCRR/W网络控制寄存器软复位、内部 PHY 上电控制0x01NSRR网络状态寄存器发送/接收完成标志0x02TXCRR/W发送控制寄存器使能发送、CRC 自动生成等0x05RXCRR/W接收控制寄存器使能接收、广播帧过滤等0x06TSRR发送状态寄存器报告发送结果0x07RSRR接收状态寄存器报告接收结果0x08TCRR/W发送控制命令写 1 触发一次发送0x0DISRR/W中断状态寄存器写 1 清除对应中断0x0FIMRR/W中断屏蔽寄存器置 1 允许对应中断0x10MRCMDXR接收内存读地址命令口0x12MRCMDR接收内存数据读取口0xF8MWCMDW发送内存数据写入口0xFCTXPLLW发送长度低字节0xFDTXPLHW发送长度高字节0xFEISRR通用状态寄存器发送/接收中断标志0x02RSRR接收状态寄存器注意与 0x07 关系注意 0x0D 和 0xFE 在某些资料里都写 ISR实际上 0x0D 是中断状态寄存器0xFE 里面同时包含了发送完成标志和接收完成标志两者用途不同。我以前一个同事在这里栽过跟头把 0xFE 当 0x0D 来清中断结果中断一直进不去。3.2 初始化状态机的完整流程上电后 DM9000A 默认处于休眠状态内部 PHY 没有上电必须按固定顺序初始化才能正常工作。我把初始化流程设计成了一个小状态机按顺序执行以下步骤第一步硬件复位。把 PWRST# 拉低至少 10ms然后拉高再等待 20ms 让芯片时钟稳定。这里要注意 DM9000A 芯片的 25MHz 时钟源如果是外部晶振提供的上电稳定时间一般在几毫秒到十几毫秒不等保险起见复位拉低时间要够长。第二步软件复位。写寄存器 0x00NCR的 bit0 为 1触发芯片内部复位然后等待至少 20ms再读 0x00 确认复位完成最后清掉复位位。第三步PHY 上电。DM9000A 内部 PHY 由 NCR 的 bit1 控制默认是关断状态。写 NCR 0x03打开内部 PHY 电源TX 和 RX 功能使能。第四步PHY 寄存器配置。通过 PHY 访问端口0x0C 写 PHY 地址、0x0D 写寄存器号、0x0E 写数据配置自动协商使能。具体操作是对 PHY 的寄存器 0控制寄存器写入 0x1200使能自动协商并设置 100M 全双工优先。第五步写 MAC 地址。将 6 字节 MAC 地址写入 0x10~0x15 寄存器。MAC 地址可以自己编一个不冲突的比如 00:11:22:33:44:55也可以用开发板上自带的 EEPROM 里的地址。建议在测试阶段固定写一个地址后期再改成可配置。第六步配置接收控制。写 RXCR 寄存器为 0x39使能接收功能允许广播帧和单播帧通过丢弃错误帧。这里 0x39 是推荐的典型配置值。第七步使能发送。写 TXCR 寄存器开启发送功能并通过 TCR 寄存器清零发送控制标识。第八步清除中断标志并设置中断屏蔽。写 ISR 清除所有中断写 IMR 使能发送和接收中断。整体初始化完成后读 NSR 寄存器确认链路状态和 PHY 工作状态。如果链路不通NSR 的 bit2LNKST位会为 0。用逻辑分析仪或者直接用 ILA 抓内部信号就能快速定位问题是出在初始化流程还是出在外面网线或对端设备上。4. 核心代码实现发送与接收的数据通路4.1 初始化状态机的 Verilog 编写思路状态机是 FPGA 控制逻辑的骨架。我习惯把 DM9000A 控制器分成三个子模块总线读写模块负责单次寄存器读写时序、上层状态机模块负责初始化、发送、接收流程、FIFO 数据缓冲模块负责数据缓存和跨时钟域处理。系统时钟用 50MHz复位用异步复位同步释放。初始化状态机的核心逻辑我用一个 16 位的状态寄存器来表示每个状态对应一步写寄存器操作。比如复位状态RST_ST里拉低 PWRST# 并计时WAIT_ST里延时等待SW_RST_ST里执行软件复位写操作依次类推。为了代码简洁我写了一个通用的写寄存器任务传入地址和数据内部控制 IOW、CMD 和数据总线的时序状态机只需要关心什么时候调用哪个任务。这种写法在实际工程中非常好用因为代码可读性强修改初始化序列也很直观。有一个实际的编码问题需要注意任务task里如果包含时序控制在综合时会被翻译成组合电路加寄存器的结构多个任务同时调用会冲突。我的处理是保证每个任务内部都是完整的一段时序逻辑且同一时刻只有一个任务被调用。4.2 寄存器读写时序的状态机参考下面给出一个单次读操作的 Verilog 参考代码采用三段式状态机localparam IDLE 3d0; localparam ADDR 3d1; localparam READ 3d2; localparam HOLD 3d3; localparam DONE 3d4; reg [2:0] state, next_state; reg [7:0] addr_reg; reg [15:0] data_out_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end always (*) begin case (state) IDLE: if (read_req) next_state ADDR; else next_state IDLE; ADDR: next_state READ; READ: next_state HOLD; HOLD: if (read_done) next_state DONE; else next_state HOLD; DONE: next_state IDLE; default: next_state IDLE; endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) begin CMD 1b0; IOR 1b1; IOW 1b1; CS 1b1; addr_reg 8d0; data_out_reg 16d0; read_done 1b0; end else begin read_done 1b0; case (state) ADDR: begin CS 1b0; CMD 1b0; {SA[4:2]} addr_reg[4:2]; // 注意这里地址左移 SD_DATA_OUT addr_reg; // 低 8 位放地址 SD_DIR 1b0; // 数据总线方向为输出 IOW 1b0; end READ: begin IOW 1b1; CMD 1b1; IOR 1b0; // 拉低读信号 SD_DIR 1b1; // 数据总线方向切为输入 end HOLD: begin data_out_reg SD_DATA_IN; // 采样数据 IOR 1b1; read_done 1b1; end DONE: begin CS 1b1; CMD 1b0; end endcase end end这段代码有几个点需要特别解释数据总线是三态总线SD_DIR 信号控制 FPGA 侧的输出使能读的时候必须切换为输入方向否则数据总线冲突FPGA 可能把 DM9000A 的数据拉死。SA 地址线的取值不是直接等于寄存器地址而是取了寄存器地址的中间三位因为 SA[4:2] 只有三根线而一个地址周期里地址信息还会通过 8 根数据线的一部分传输二者组合才能确定寄存器页。写操作和读操作类似只是把 IOR 换成 IOW数据方向始终保持输出。4.3 发送一帧数据的完整流程DM9000A 的发送流程在驱动层面分四步检查上一次发送是否完成、写发送长度、往发送缓冲区写数据、触发发送命令。我把它实现为一个发送状态机每收到上层模块的发送请求自动执行这四步。第一步是写发送控制寄存器往 0xFC 和 0xFD 分别写入帧长度低字节和高字节。帧长度是指从目的 MAC 地址开始到数据字段结束的字节数不包含前导码和 CRC。第二步是通过 0xF8 端口连续写入整个帧数据每写一个 16 位字需要一次地址周期加数据周期操作。第三步是写 0x02TXCR 的命令位触发发送。第四步是轮询 0xFE 寄存器等待 bit0TX1END置 1说明发送完成然后读 0x06TSR确认发送结果正确。这里有一个性能相关的问题如果每写一个 16 位字都走一次地址周期开销很大。DM9000A 的发送缓冲区写端口有一个特性连续写数据时地址不需要重复写只需要在第一个字写入前写一次地址之后保持 CMD 为高连续拉低 IOW 写数据即可。利用这个特性可以显著提高发送效率。实现时需要在状态机里加一个“连续写模式”的路径当发送状态机进入数据写循环后不再插入地址周期。4.4 接收一帧数据的完整流程接收流程比发送稍微繁琐一点因为数据是从 DM9000A 内部 SRAM 里读出来的必须先检查状态再决定是否读取整帧。完整流程如下第一步轮询 0xFE 寄存器的 bit1PRX判断是否收到新包。第二步读 0x10 寄存器第一次读到的值只有最高两位有效指示内存状态低字节的 bit0 表示是否有新包。如果 bit0 为 1进入下一步。第三步再读一次 0x10得到一个状态字节这个字节就是帧头状态。第四步读 0x12MRCMD连续读出两个 16 位字第一个字的低字节是接收状态RSR第二个字是接收帧长度以字节为单位。注意这里读出的长度是整个帧的长度包括 4 字节 CRC实际有效数据要减去 4。第五步根据帧长度连续读 0x12 端口每次读 16 位把数据写入 FIFO。第六步读完后写 0x02RXCR 的命令位释放接收缓冲区否则 DM9000A 内部内存指针不会更新下一包数据会被覆盖。接收状态机有一个比较容易出错的地方读 0x12 端口时读到的第一个 16 位数据中高字节是状态、低字节是长度的高字节第二个 16 位数据的低字节才是长度的低字节。很多参考代码把长度解析错了导致后续数据读取数量不对帧尾丢数据或者多读数据。我调试时遇到过一次现象收到的数据每帧最后多了 4 个字节打印出来看就是 CRC原因就是没把 CRC 长度减掉。以下是一段接收主状态机的核心代码框架define RX_IDLE 4d0 define RX_CHECK 4d1 define RX_STATUS 4d2 define RX_LEN 4d3 define RX_DATA 4d4 define RX_RELEASE 4d5 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_state RX_IDLE; frame_len 16d0; rx_count 16d0; end else begin case (rx_state) RX_IDLE: begin if (rx_ready) rx_state RX_CHECK; end RX_CHECK: begin // 读 0x10判断 bit0 if (rx_valid) begin if (rx_data[0]) rx_state RX_STATUS; else rx_state RX_IDLE; end end RX_STATUS: begin // 读 0x10获取状态字节 rx_state RX_LEN; end RX_LEN: begin // 读 0x12 两次解析长度 if (len_done) begin frame_len len_value; rx_count 16d0; rx_state RX_DATA; end end RX_DATA: begin if (rx_count (frame_len - 4)) begin rx_count rx_count 16d1; // 每拍写入 FIFO end else begin rx_state RX_RELEASE; end end RX_RELEASE: begin // 写 0x02 释放接收缓冲区 rx_state RX_IDLE; end default: rx_state RX_IDLE; endcase end end5. 仿真验证与板上调试心得5.1 如何用 ModelSim 搭一个简单的验证环境FPGA 开发中仿真和上板调试的时间比例大概在 6:4仿真做得好上板会省很多事。DM9000A 的仿真验证我分了两层第一层是单模块测试分别验证初始化状态机、发送状态机和接收状态机的时序是否符合预期第二层是系统集成测试把三个模块连起来模拟完整的收发流程。由于 ModelSim 没有 DM9000A 的行为模型需要自己写一个简化的仿真模型。这个模型的核心逻辑是保存一个寄存器数组响应读写总线的请求模拟一个发送完成延时当检测到触发发送的命令位后延时一段时间设置发送完成标志模拟一个接收缓存区预定好一帧数据当读指针触发时依次返回帧数据。这个模型不用完全仿真 DM9000A 的内部行为只要能验证 FPGA 侧的逻辑时序就够了。我实际写这个模型花了不到一小时但对后续调试帮助巨大。仿真时有一个容易忽略的细节Verilog 仿真中#延时语句是不可综合的习惯写 RTL 的工程师在写测试文件时会自然地把时钟翻转和输入变化写得比较理想化导致仿真通过但上板不工作。我建议仿真时就按真实时序来约束比如 IOW 低电平保持时间严格按系统时钟周期来算这样仿真结果才真实可信。5.2 上板调试三板斧第一板抓总线波形。用 FPGA 内部的逻辑分析仪Vivado 里叫 ILAQuartus 里叫 SignalTap抓取 SD、CMD、IOR/IOW、CS 这几根关键信号验证读写时序是否正确。如果发现 IOW 低电平时间太短就把状态机里 HOLD 状态延长一拍。第二板验证寄存器读写。初始化完成后读几个关键寄存器的值比如 NCR、NSR、RXCR看看是否写进去了。特别是读 NSR 的链路状态位判断网线是否连接正常。如果读链路状态一直为 0大概率是物理层配置出了问题拉网线或者换个交换机会有意外收获。第三板用 PC 端工具验证收发。写一个简单的循环回环测试逻辑把收到的数据原样发回去。PC 端用网卡调试工具比如 Packet Sender 或者 Wireshark 配合自定义发包工具发一个广播包FPGA 收到后原路返回PC 端能收到说明收发链路都是通的。如果发送方向不工作PC 端会一直收不到包此时先用逻辑分析仪抓发送状态机的内部信号重点看发送完成后 TSR 寄存器的值是不是 0表示发送成功如果有错误位根据报错内容逐项排查。6. 常见问题与避坑指南6.1 问题速查表现象可能原因排查思路链路指示灯不亮PHY 没上电或 PHY 配置未完成检查 NCR 寄存器 bit1 是否为 1网线是否为交叉线寄存器读写全返回 0xFF数据总线方向冲突或引脚约束错误检查 SD_DIR 方向控制确认引脚约束里 inout 口是否正确初始化卡在软复位复位延时不够加大延时软复位后至少等 20ms发送完成后对端收不到帧长度写错或 CRC 未自动生成打印 TSR 寄存器值确认发送长度是否正确接收数据最后多了 4 字节没减 CRC 长度帧长度减 4 再读数据收发概率性失败时序裕量不足或数据总线电平不稳定加长 HOLD 状态时间数据总线上拉电阻检查6.2 三个印象深刻的坑第一个坑是初始化时序不够。我第一次写初始化状态机时软复位后只等了 5ms 就开始读寄存器结果芯片没准备好读出来的全是 0xFF初始化流程判断异常直接跳出了。后来把软复位后的等待时间拉到 30ms问题消失。DM9000A 的内部状态转换并不像数据手册时序图上画的那么简单芯片上电和复位完成后还有一个内部校准和 PHY 启动的过程等得久一点不会有坏处。第二个坑是 FIFO 读写跨时钟域的握手问题。接收数据写入 FIFO 用的写时钟是系统时钟上层读 FIFO 的时钟如果不同就会引入跨时钟域问题。项目里我先用了同步 FIFO后来上层改成了异步逻辑就出现偶发丢包。解决办法是把数据缓冲统一用一个异步 FIFO并且读写指针的格雷码转换要做对不能贪图方便直接用二进制计数器。第三个坑和 PCB 布线有关。有一次上板后通信速率稍微一高比如持续满包发送就会偶发丢帧逻辑分析仪抓的时候又看不出来问题。后来量了 DM9000A 的电源引脚纹波偏大确定是 PCB 布线时数据总线和时钟线靠得太近串扰导致的数据采样错误。这种问题在逻辑层面无解只能改板。一般做 FPGA 项目时对 16 位数据总线要等长走线时钟信号和管理信号要做包地处理DM9000A 的 25MHz 晶振要靠近芯片引脚放置。7. 后续可以怎么扩展我做完这个控制器之后又往上面加了一些东西一个 ARP 回应模块让 FPGA 可以被上位机用 ping 命令直接探测到一个简单的 UDP 封装模块把采集的数据打成 UDP 包发送出去。这样整个系统的调试体验好了很多不再需要依赖二层的收发测试工具直接用网络调试助手就能和 FPGA 通信。如果后续想跑更高性能可以换用支持千兆的 RTL8211 配合 FPGA 内部 MAC 硬核但那一套方案的复杂度要比 DM9000A 高一个量级需要处理 GMII/RGMII 接口时序、DDR 双沿采样、MAC 状态机等一系列问题。我的建议是如果不是对网络带宽有硬性要求DM9000A 这套方案足够把 FPGA 网络通信的逻辑和流程吃透把这套控制器写好再去碰千兆方案的时候会轻松很多。对我个人来说这个项目最大的收获不是“能收发数据包”这个结果而是把 Verilog 状态机设计、总线时序分析和跨时钟域处理这组基本功扎实地练了一遍这些经验在后续做别的接口控制器时都是通用的。本文还有配套的精品资源点击获取
返回列表