资讯动态

FPGA控制DM9000A以太网收发的Verilog实现与状态机设计

发布时间:2026/10/15 15:26:54 来源:尧图企业网站定制
简介面向FPGA与嵌入式开发者的DM9000A以太网收发完整工程基于Verilog实现MAC层控制逻辑涵盖芯片初始化、寄存器读写、数据收发状态机、DMA与中断处理等关键环节适合学习以太网协议栈、硬件接口设计或进行毕业设计/竞赛开发参考。资源包共71个文件约2.53MB以Verilog源码.v为核心辅以Quartus工程文件.qpf/.qsf、仿真波形.vwf、编译报告.rpt、初始化代码及DM9000A数据手册、接口实物图等目录模块清晰便于按功能对照学习。已有627人在CSDN学习下载。压缩包内不仅包含初始化、读/写时序、PHY寄存器配置等可复用模块还提供完整的Quartus II工程配置与下载文件.sof/.pof可直接用于仿真验证或上板调试。借助这一资源可快速掌握FPGA控制DM9000A进行以太网帧收发的时序设计思路理解CRC校验、DMA传输与中断响应等实现细节有效缩短自研以太网通信模块的开发周期。1. 直接用 FPGA 控制 DM9000A 做以太网收发先解决一个“接口是否合拍”的问题“FPGA 控制 DM9000A 进行以太网数据收发的 Verilog 实现”这个标题很多人看到的第一反应是“不就是挂个网卡芯片吗”但真的做过就会明白核心工作不在 DM9000A而在 FPGA 内部那一套寄存器读写控制器和数据搬运通路。DM9000A 是一颗 10/100M 自适应以太网控制器对外走类 ISA 并行总线内部有 MAC、PHY 和收发 FIFO。FPGA 要做的是给它提供正确的复位、寄存器读写时序然后像 CPU 一样去配置它的控制寄存器、写 MAC 地址、把要发送的帧搬进它的 TX FIFO再从 RX FIFO 里取回收到的帧。这个方案在低成本仪器、测试板卡和教学实验里很常见尤其适合“不要 CPU只想用纯逻辑把网络收发跑通”的场景。动手之前先搞清楚一件事DM9000A 和 FPGA 之间的“总线”比一般外设更挑时序后面所有代码都要围绕这套时序展开。2. DM9000A 并行接口与寄存器先看懂 CMD 和数据总线再动状态机DM9000A 给人最容易漏掉的一点它和 FPGA 之间不是 SPI/I2C而是一个带地址锁存风格的并行总线。虽然它不像传统 SRAM 接口那样有一整根地址总线但多了个 CMD 引脚。CMD 为低时数据线上传的是内部寄存器索引CMD 为高时数据线上才是真正的寄存器数据。也就是说FPGA 至少要维护“地址”和“数据”两个窗口这个窗口切换就是后面状态机的主线。2.1 一组需要严格对齐的控制引脚CS、IOR、IOW、CMD常见接法里DM9000A 的 SD[15:0] 直接连到 FPGA 的通用 IOCMD 引脚通常由 FPGA 内部某个地址位映射也可以直接用一根 IO 控制。CS#、IOR#、IOW# 都是低有效RST 是高有效复位。INT 引脚是中断输出如果 FPGA 逻辑里不想做中断可以把它接成普通输入靠轮询状态寄存器也能跑通但 INT 一定要处理成“不复位、不悬空”否则容易出现上电后收发状态不同步。接线时最容易出问题的是数据总线方向。DM9000A 的数据线是双向的FPGA 端必须用 inout 引脚并且在 Verilog 里同时控制输出使能和一个寄存器缓冲不要直接 assign dm9_data reg_wdata否则读数据时会和芯片输出打架。一般会把 inout 拆成三段写数据时使能输出读数据时把使能拉高把输入采样到 reg_rdata总线空闲时使能拉高让数据线处于高阻。这个习惯要养成因为后文所有状态机代码都依赖这个双向口处理。还有一个容易看漏的点DM9000A 支持 8 位和 16 位数据模式。16 位模式下一次读写一个 word寄存器索引和数据的低字节都在 SD[7:0] 上SD[15:8] 在某些访问里可能是 0 或高字节。如果你把数据总线接到 FPGA 的不同管脚上就会遇到“索引写对了但寄存器数据高低字节错位”的问题。我一般会在顶层约束文件里把 SD[15:0] 连续排布并且先写一个 ID 读取测试确认 16 位模式下的字节序再继续做收发否则后面查起来很痛苦。2.2 16 位模式下的读写时序一个总线周期里发生了什么一次完整的寄存器写访问可以拆成两步。第一步把 CMD 拉低把寄存器索引放到数据总线上然后给一个 IOW# 低脉冲DM9000A 在 IOW# 上升沿锁存索引。第二步把 CMD 拉高把要写的寄存器值放到数据总线上再给一个 IOW# 低脉冲这次锁存的是数据。读访问类似第一步锁存索引第二步把 CMD 拉高后拉低 IOR#FPGA 在 IOR# 低电平期间采样数据总线。下面这段用 task 描述时序意图方便理解窗口切换task write_dm9_reg; input [7:0] reg_idx; input [15:0] reg_val; begin dm9_cs_n 1b0; dm9_cmd 1b0; // 切换到索引窗口 dm9_data {8h00, reg_idx}; // 16位模式索引放在低字节 dm9_iow_n 1b0; // 产生索引写脉冲 #100; // 保持时间非可综合写法 dm9_cmd 1b1; // 切换到数据窗口 dm9_data reg_val; #100; dm9_iow_n 1b1; // 数据写脉冲结束寄存器更新 dm9_cs_n 1b1; end endtask这段代码不能直接综合因为 #100 是仿真延时但能帮你理解时序。真正可综合的逻辑要用状态机加周期计数器把 CS/CMD/IOW/IOR 全部用时钟沿驱动。读时序也是同样思路第一步锁存索引第二步把 CMD 拉高后拉低 IOR#然后在下一个时钟沿采样 dm9_data。时序参数要按数据手册来不同 DM9000A 批次对最小脉冲宽度要求略有差异。我的习惯是 50MHz 系统时钟下一个总线读或写周期给足 4 个时钟周期其中地址窗口占 1 拍数据窗口占 2 拍释放占 1 拍。如果系统时钟到 100MHz可能需要增加一个周期计数器保证 IOW/IOR 低电平宽度不低于 10ns。这个余量比寄存器配置本身更容易帮你避开“时好时坏”的随机性问题。2.3 寄存器地图和内存端口那些你迟早要查的地址DM9000A 的寄存器地址不多但收发通路上有几个地址必须记牢。表格里列出来的是最常用的一组偏移名称读写属性作用0x00NCR读写网络控制寄存器bit0 是软复位0x01NSR只读网络状态寄存器收发就绪标志0x02TXCR/TXPL读写发送控制也兼 TX 长度低字节0x03TXSR/TXPH读写发送状态也兼 TX 长度高字节0x04RCR读写接收控制寄存器bit0 是 RXEN0x05RSR只读接收状态寄存器0x08-0x0DPAR0-PAR5读写本机 MAC 地址0x2AVID只读芯片厂商 ID0x2BPID只读芯片产品 ID0x40MWCMD写连续写模式把数据写入 TX FIFO0xF0MRCMDX读内存读扩展命令判断 RX 是否就绪0xF1MRCMD读连续读模式从 RX FIFO 读数据这里最容易混淆的是 0x02/0x03。它们既是 TX 控制/状态寄存器又承担发送长度低字节和高字节的写入功能。实际驱动里的常见做法是发送前先连续写这两个偏移地址来设置长度随后再写 0x02 并置 TXREQ 位来触发发送。不同参考代码可能会用不同常量名但偏移地址基本一致。PHY 寄存器不直接暴露在地址空间里而是通过 0x18 到 0x1B 这一组 EPCR/EPAR/EPDR 间接访问。FPGA 里如果只想把链路层打通PHY 配置可以交给芯片自动协商甚至不显式去读 PHY 状态。但如果你想做链路检测就得会用这组寄存器先写 EPAR 指定 PHY 寄存器地址再写 EPCR 触发读或写再从 EPDR 取数据。初始化阶段我一般不碰 PHY除非出现“复位后插着网线也不亮灯”的情况。3. 用状态机把寄存器读写收进时钟域可综合的实现骨架寄存器读写一旦落到可综合 Verilog就不能用前面那种 task 加延时的方法了。常见方案是写一个总线状态机把所有控制信号在时钟沿处打一拍再通过周期计数控制脉冲宽度。这个状态机要同时服务初始化、发送配置和接收状态读取所以它的接口应该尽量通用一个寄存器地址、一个写数据、一个读写请求、一个忙标志和读回数据。收发模块只是这个接口的使用者。3.1 寄存器读写状态机从 IDLE 到 DONE 的四种转移核心状态可以只分四五个IDLE、SET_CMD、DO_ACCESS、DONE。IDLE 等待请求SET_CMD 把索引写到数据总线上同时给一次索引写脉冲DO_ACCESS 切到数据窗口根据请求拉低 IOW 或 IORDONE 释放总线和片选信号。下面是一个简化但可综合的状态机片段localparam IDLE 3d0; localparam SET_CMD 3d1; localparam WR_DATA 3d2; localparam RD_DATA 3d3; localparam DONE_ST 3d4; reg [2:0] bus_state; reg [1:0] hold_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin bus_state IDLE; dm9_cs_n 1b1; dm9_cmd 1b0; dm9_iow_n 1b1; dm9_ior_n 1b1; reg_busy 1b0; dm9_ob_en 1b0; end else begin case (bus_state) IDLE: begin if (wr_req || rd_req) begin reg_busy 1b1; dm9_cs_n 1b0; dm9_cmd 1b0; dm9_ob_en 1b1; dm9_data {8h00, reg_idx[7:0]}; dm9_iow_n 1b0; bus_state SET_CMD; end end SET_CMD: begin dm9_iow_n 1b1; // 索引锁存结束 dm9_cmd 1b1; // 切到数据窗口 if (wr_req) begin dm9_data reg_wdata; dm9_iow_n 1b0; // 产生数据写脉冲 end else begin dm9_ior_n 1b0; // 产生读脉冲 end hold_cnt 2d0; bus_state (wr_req) ? WR_DATA : RD_DATA; end WR_DATA: begin hold_cnt hold_cnt 1b1; if (hold_cnt 2d1) begin dm9_iow_n 1b1; bus_state DONE_ST; end end RD_DATA: begin hold_cnt hold_cnt 1b1; if (hold_cnt 2d1) begin reg_rdata dm9_data; // 在 IOR 有效期间采样 dm9_ior_n 1b1; bus_state DONE_ST; end end DONE_ST: begin dm9_cs_n 1b1; dm9_ob_en 1b0; reg_busy 1b0; bus_state IDLE; end default: bus_state IDLE; endcase end end这个状态机里最值得注意两点。第一dm9_data 是三态总线必须有 dm9_ob_en 控制输出使能否则读时序会和芯片输出冲突。第二读数据采样点要放在 IOR# 仍然有效的那个时钟周期而不是等 IOR# 已经拉高之后再采样。你可以把 hold_cnt 扩展到 8bit 来调节脉冲宽度比如 100MHz 下加一个reg [3:0] wait_cnt让 IOW/IOR 低电平保持 3 到 4 拍这个余量能明显减少偶发读写错误。另外dm9_data在写索引和写数据两个窗口共用同一根总线所以状态机里务必先切 CMD再赋值数据。如果顺序反了DM9000A 会看到“数据窗口里写的是索引”的时序寄存器内容就乱了。这个坑在仿真里不一定会暴露但上板十次有七八次会翻车。3.2 初始化序列软复位、ID 校验、MAC 和 RCR初始化序列建议按固定顺序做先软复位再读芯片 ID然后写 MAC 地址最后开接收。复位和等待时间必须给足否则后续访问会读到随机值。下面这段代码展示了顺序task init_dm9000a; begin // 1. 软复位NCR[0] 置 1然后清零 write_dm9_reg(8h00, 16h0001); delay_us(20); write_dm9_reg(8h00, 16h0000); delay_ms(2); // 2. 读芯片 ID确认总线时序正确 read_dm9_reg(8h2A, chip_vid); read_dm9_reg(8h2B, chip_pid); if (chip_vid ! 16h0A46 || chip_pid ! 16h9000) begin init_error 1b1; // 说明复位或时序有问题 end // 3. 写 MAC 地址到 PAR0-PAR5 // 每写一个字低字节对应一个 PAR 寄存器 write_dm9_reg(8h08, {mac[47:40], mac[39:32]}); write_dm9_reg(8h0A, {mac[31:24], mac[23:16]}); write_dm9_reg(8h0C, {mac[15:8], mac[7:0]}); // 4. 使能接收RCR[0]RXEN // 如果还要接收广播这个配置已经够用 write_dm9_reg(8h04, 16h0001); end endtask这段伪代码里 write_dm9_reg/read_dm9_reg 都对应前面的总线状态机接口。注意 MAC 寄存器的写法PAR0-PAR5 是 6 个字节寄存器但在 16 位模式下按字访问时偏移地址是 0x08、0x0A、0x0C对应 PAR0/PAR1、PAR2/PAR3、PAR4/PAR5。高低字节顺序要和你希望 MAC 在以太网帧里的排列顺序一致否则对端看到的源 MAC 会倒序。RCR 寄存器不建议一上来就加太多过滤位。很多文档会写RCR 0x21之类那是因为它开了广播和组播过滤。最省事的做法是只写0x01让芯片接收单播、广播和标准组播先把数据通路跑通。等收发稳定后再根据需求关掉不需要的帧。3.3 时钟频率和总线宽度的参数选择这个状态机适合的参数一个是系统时钟频率一个是总线周期保持拍数。我一般会在模块顶部全部定义成 parametermodule dm9000_if #( parameter CLK_FREQ 50_000_000, parameter IO_HOLD_CYCLE 3 )( ... );CLK_FREQ 用于初始化时的延时计算IO_HOLD_CYCLE 用于控制 IOW/IOR 低电平宽度。50MHz 下 IO_HOLD_CYCLE 取 2 到 3 拍就够100MHz 下如果 IO 引脚走线较长建议 5 拍以上。对于数据总线宽度默认按 16 位模式写因为一次可搬运两字节发送和接收效率更高。如果板子硬件只连了 SD[7:0]则需要把所有 16 位访问改成连续两个 8 位访问状态机里每个写数据阶段会多一倍步骤初始化代码也要跟着改。不要试图用一个参数同时支持两种模式那会让代码里的字节序判断到处都是最终把自己绕晕。4. 从 TX FIFO 到 RX FIFO数据收发状态机和帧长处理寄存器控制器跑通以后剩下的工作就是两条数据通路一条把 FPGA 内部要发送的帧搬到 DM9000A 的 TX FIFO另一条把 DM9000A 收到的帧搬到 FPGA 内部缓存。很多人在这一步犯的错是把发送和接收都用“一个大 while 循环”的思路写结果时序一复杂总线上读一个字节要等好几天。正确做法依然是状态机并且让数据总线和内部缓存之间只用一拍完成读写。4.1 发送通路先搬数据再写长度最后置 TXREQ发送一个以太网帧的常见顺序是先把整帧数据通过 MWCMD 命令连续写到 TX FIFO然后写入帧长度到 TXPL/TXPH最后写 TXCR 的 TXREQ 位触发发送。帧数据从哪里来在 FPGA 里一般是一块双口 RAM 或 FIFO发送状态机按节拍读出来。下面是一个发送状态机的关键片段localparam TX_IDLE 4d0; localparam TX_MWCMD 4d1; localparam TX_DATA 4d2; localparam TX_LENL 4d3; localparam TX_LENH 4d4; localparam TX_TRIG 4d5; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_state TX_IDLE; end else begin case (tx_state) TX_IDLE: begin if (tx_valid tx_ready) begin dm9_cs_n 1b0; dm9_cmd 1b0; dm9_data 16h0040; // MWCMD 索引 dm9_iow_n 1b0; tx_cnt 16d0; tx_state TX_MWCMD; end end TX_MWCMD: begin dm9_iow_n 1b1; // 索引锁存完成 dm9_cmd 1b1; dm9_data tx_ram_dout; // 第一拍帧数据 tx_state TX_DATA; end TX_DATA: begin dm9_iow_n 1b0; // 写一帧数据到 TX FIFO if (tx_cnt tx_len - 1) begin tx_state TX_LENL; end else begin tx_cnt tx_cnt 1b1; // 这里下一拍从 RAM 读新数据 dm9_data tx_ram_dout; end end TX_LENL: begin dm9_iow_n 1b1; dm9_cmd 1b0; dm9_data {8h00, tx_len[7:0]}; dm9_iow_n 1b0; tx_state TX_LENH; end TX_LENH: begin dm9_iow_n 1b1; dm9_data {8h00, tx_len[15:8]}; dm9_iow_n 1b0; tx_state TX_TRIG; end TX_TRIG: begin dm9_iow_n 1b1; dm9_cmd 1b0; dm9_data 16h0002; // 写 TXCR 地址 dm9_iow_n 1b0; tx_state TX_TRIG2; end // TX_TRIG2 里置 TXREQ 并释放总线 default: tx_state TX_IDLE; endcase end end发送状态机最重要的参数是 tx_len。这个长度指的是 TX FIFO 里实际写入的帧数据字节数不包含 DM9000A 自动补的 CRC。如果你在 FPGA 里已经手动算了 CRC 并写进帧尾那就要把 CRC 也计入长度同时在发送控制寄存器里关掉自动 CRC 填充。绝大多数情况建议让 DM9000A 自己补 CRC所以 tx_len 以太网帧头 IP/UDP 头 有效载荷最短到 60 字节不足 60 字节要补 0否则对端网卡可能直接丢帧。还有一个容易被忽略的点TX FIFO 写入阶段结束后要等 TX 状态寄存器里的发送完成标志才能发起下一帧。有些设计图省事在 TX_TRIG 之后立刻把 tx_ready 拉高导致连续发送时第一帧还没发完第二帧数据已经把 TX FIFO 覆盖了。正确的握手应该是tx_ready 在 TX 完成标志有效后再拉高至少留出一个帧间隙。4.2 接收通路检查 RXRDY、读状态、连续搬移接收流程比发送多一个判断要先知道 DM9000A 的 RX FIFO 里有没有完整的一帧。常见做法是读 NSR 寄存器看 RXRDY 位然后用 MRCMDX 命令读一个特殊字来确认。确认就绪后切到 MRCMD 连续读数据最后根据 RX 状态寄存器判断这帧是否有效。接收状态机片段如下localparam RX_IDLE 4d0; localparam RX_CMD 4d1; localparam RX_STATUS 4d2; localparam RX_DATA 4d3; localparam RX_END 4d4; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_state RX_IDLE; end else begin case (rx_state) RX_IDLE: begin // 轮询 NSR读到 RXRDY 后再进入读命令 dm9_cs_n 1b0; dm9_cmd 1b0; dm9_data 16h0001; // NSR 地址 dm9_ior_n 1b0; rx_state RX_CMD; end RX_CMD: begin dm9_ior_n 1b1; // nsr_data 是刚读到的 NSR if (nsr_data[0]) begin dm9_cmd 1b0; dm9_data 16h00F0; // MRCMDX 地址 dm9_ior_n 1b0; rx_state RX_STATUS; end else begin rx_state RX_IDLE; // 没有帧继续轮询 end end RX_STATUS: begin dm9_ior_n 1b1; dm9_cmd 1b1; // 这里读到的第一个 16 位数据高字节约定为 0x01 // 低字节是 RX 状态和长度的一部分 rx_state RX_DATA; end RX_DATA: begin dm9_ior_n 1b0; // 不断读 MRCMD直到帧长度达到预期值 if (rx_cnt rx_len) begin rx_state RX_END; end else begin rx_fifo_data dm9_data; rx_fifo_wr 1b1; rx_cnt rx_cnt 1b1; end end RX_END: begin dm9_ior_n 1b1; dm9_cs_n 1b1; rx_fifo_wr 1b0; rx_state RX_IDLE; end default: rx_state RX_IDLE; endcase end end接收流程里最容易出错的是 MRCMDX 和 MRCMD 的语义差异。MRCMDX 不是取走数据而是从 RX FIFO 头部“瞄一眼”判断是否有一帧完整的帧等着。看到标志后需要用 MRCMD 真正消费数据。如果读完一个坏帧不把它清掉后续所有帧都会堵在 FIFO 里。所以 RSR 的读取和错误判断要放在 RX_STATUS 之后发现 CRC 错误或帧长度非法时也要把剩余数据读完或者通过关闭 RX 再开启来触发芯片内部清空。4.3 用一块双口 RAM 做帧缓存避免一路写一路卡纯收发状态机可以直接接 FIFO但做完整以太网收发时我建议用双口 RAM 而不是简单 FIFO。因为发送前需要知道整帧长度接收后需要让上层逻辑能随机访问协议头。一个典型参数是位宽 16bit深度 1024能存一帧最大 1518 字节的以太网帧外加 4 字节帧状态。使用双口 RAM 的好处是收发状态机读端口和上层用户逻辑写端口完全独立不会出现写一半被读走的情况。如果是在 FPGA 里用 FIFO要关注 almost_full 信号。发送时DM9000A 内部 TX FIFO 一旦满了MWCMD 连续写可能会丢数据。所以发送状态机里最好在每写一拍后看 DM9000A 的 TX 状态但不建议每个字节都判断那样会拖慢总线。折中办法是在 TX_DATA 状态里每写 8 拍检查一次 TX 完成或 FIFO 满标志满则暂停当前帧等标志消失再继续。这样既不会丢帧也不会让总线吞吐掉得太快。接收侧的帧长度一般是 DM9000A 在状态字里给出的不是由 FPGA 自己猜。所以 RX_DATA 状态里读到一个合法长度后要用一个寄存器把剩余字节数锁存下来依次递减。如果收到广播风暴或错误帧长度可能超过你的缓存深度常见做法是设置一个最大长度上限超过 1518 字节直接放弃该帧并继续读完直到 FIFO 清空。这样能防止恶意大帧把 FPGA 内部 RAM 打爆。5. 联调避坑DM9000A 最容易翻车的 5 个现场含排查顺序寄存器控制器和数据通路都写完后真正耗时间的是上板联调。下面这 5 个现场我基本都踩过按排查优先级从高到低写出来。5.1 复位即玄学ID 读不出来现象初始化脚本读 0x2A/0x2B 时读到 0xFFFF 或随机值偶尔复位后第一次能读对第二次就读错。原因DM9000A 的软复位要求 NCR 置位后保持足够时间释放后还要等芯片从 PHY 上电状态稳定下来。很多参考代码只写了 20us 等待实际上不同板子差异很大。另一个常见原因是 RST 引脚在上电瞬间处于不定态导致芯片一直没退出复位。解决把等待时间加大到“保守线”。我一般用 FPGA 时钟计数器做延时软复位置位保持 20us复位释放后等 2ms 以上再访问寄存器。如果读 ID 仍不对先检查 RST 引脚是否接了可靠下拉再看数据总线高低字节是否接反最后才怀疑芯片本身。5.2 总线时序和采样点寄存器时好时坏现象写 MAC 地址后用读回的方式校验发现偶发第一位错。连续读写寄存器时每隔几十次就返回一个 0x00。原因状态机把 dm9_data 的赋值和 dm9_iow_n 拉低放在了同一个时钟沿导致数据还没稳定 IOW 就已经有效。或者读操作在 IOR 上升沿之后才采样数据已经从总线上消失。解决把数据变化和 IOW/IOR 有效沿错开一拍。写操作先赋数据下一拍拉低 IOW读操作在 IOR 有效期间用寄存器打一拍采样不要在同一个 always 块里既拉低 IOR 又采样数据。如果你用的是下板调试强烈建议把总线的 IO 时序约束加上告诉综合工具 CS/CMD/IOW/IOR 之间是外部延迟关系否则布局布线后各个信号到达时间可能偏差很大。5.3 发送完成位一直不置位现象TX 触发以后寄存器里发送完成标志始终不变但用示波器看 IOW/CS 时序又没什么问题。原因帧长度寄存器没有写对。DM9000A 触发发送时如果 TXPL/TXPH 是 0芯片认为空帧不会真正发起发送。有些设计把帧长写成了“数据长度加 4”但 TX FIFO 里没写 CRC导致芯片等一个永不出现的 CRC。解决发送前先确认 TX FIFO 里写入的字节数和 TXPL/TXPH 完全一致不要多算 CRC。触发位 TXREQ 要写在最后一次长度写入之后建议用独立一拍写 TXCR 地址并置 bit0不要和长度写混在一起。完成标志以 NSR 或 TX 状态寄存器为准不要靠猜延时。5.4 对端提示帧长度不对或 CRC 错误现象PC 端抓包能看到帧但 Wireshark 提示 “frame check sequence incorrect”或者长度字段比实际多了 4 字节。原因MAC 层同时有“自动填充 CRC”和“手动送 CRC”两种模式。如果你在 FPGA 里发送的帧尾已经带 4 字节 CRC而 DM9000A 又在后面自动补了 4 字节就会造成 CRC 错。如果你把 tx_len 设成了包含 CRC 的长度但芯片自动补 CRC则长度字段多算 4。解决二选一。要么 FPGA 只负责发到“伪头部 数据”把 tx_len 设为不含 CRC 的字节数让 DM9000A 自动补要么 FPGA 自己算好 CRC 并写入帧尾此时要先把 DM9000A 的自动 CRC 填充关掉tx_len 包含 CRC。最简单的方案是前者初始化时不要开任何“CRC 手动”模式发送长度按 len 6 6 2 payload 计算即可。5.5 接收被错误帧卡死现象RX 状态寄存器一直显示有帧但每次读出来的数据都一样或者一轮接收后整个 FIFO 再也不出新数据。原因读到一个 CRC 错误或长度错误的帧后没有消费完这一帧的全部数据DM9000A 的 RX 状态机停留在“帧未读完”的状态。另一个常见原因是 MRCMD 模式读的数据比实际帧多或少一个周期导致下一个帧头被错位。解决进入 RX_DATA 后严格按状态字给出的长度读完不提前退出。每次读完一帧后判断 RSR 里的错误位如果出错要把这一帧完整吞掉再处理下一帧。更稳的做法是读到错误帧后写 RCR 关闭 RXEN再重新打开让芯片内部清一次 FIFO代价是会丢接下来几帧但系统至少不会死锁。6. 进阶验证做一个最小回环测通 ARP再谈吞吐收发通路稳定后不要急着去调大流量吞吐先做一个最小回环让 FPGA 在初始化完成后自动发出一个 ARP 请求帧等 PC 端用 ping 来触发 ARP 响应FPGA 再把收到的响应帧原样发回去。这个实验能同时验证四件事MD9 的 TX FIFO 写入、发送触发、接收就绪判断、RX 数据搬移。PC 端只要用抓包软件看到 FPGA 发出的 ARP 请求并且能看到 FPGA 回应的重复帧基本说明链路层已经通了。然后你再把 FPGA 接收到的一个 UDP 帧内容存进 RAM再原封不动发回去用网卡工具持续打流观察有无丢包。验证过程中记录几个关键指标发送一帧的端到端延迟、连续发送时的帧间隙、接收时的最大连续帧数。常见误区的“100M 以太网 FPGA 里 100Mbps”实际上 DM9000A 的并行总线在 16 位模式和 50MHz 时钟下理论搬运能力远超 100Mbps瓶颈反而在 DM9000A 内部 FIFO 和 MAC/PHY 的帧处理上。实测能做到 80Mbps 以上就已经是合理水平再往上就要检查发送状态机是否在每帧之间浪费了过长等待时间。你可以给发送通路加一个简单的手握计数器从 tx_valid 拉高到 tx_ready 重新拉高之间记录周期数连续发包时这个数字的稳定性比瞬时速率更能反映状态机优劣。如果你希望这个方案能长期用在项目中建议再补三件事把复位后的初始化超时做成可配置参数方便换不同批次的 DM9000A把总线状态机的读数据采样点做成一个 localparam便于针对不同 PCB 走线长度调整最后在顶层文件里显式约束 IOR/IOW/CS 的对外延迟标记否则换一块 FPGA 型号就容易从“能跑”变成“偶发跑不通”。我做这个项目时最大的教训是DM9000A 的寄存器配置并不难难的是每一次时序微调都要看数据手册而不是靠猜。后来我给自己定了一个规矩每次联调前先在纸上画清楚索引窗口和数据窗口的波形再动代码。这个习惯帮我省掉了很多改一次综合一次的死循环。这个方案和这些参数边界希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑