我们经常接到类似的问题:“我用 Verilog 写了 AXI4 Master,为什么 DDR 里读出来的数据全是乱的?”、“wlast 到底应该在什么时候拉高?”、“FIXED burst 还需要地址对齐吗?”——把这些问题往一起看,基本可以判定,大家不是不会写状态机,而是没把 AXI4 和 DDR 控制器的关系想明白。
这篇内容我打算直接用实际工程里总结出来的经验来讲:先弄清楚你和 DDR 控制器之间到底谁是“干活的人”,再动手写 AXI4 Master,然后按写通道、读通道逐个拆开,最后讲上板调试最容易翻车的几个点。完整的状态机代码会拆在对应章节里给出,注释写到可以直接照着改的程度。整篇更适合刚入门 FPGA、被 AXI4 和 DDR 折腾得睡不着的新手,也适合已经把读写跑通但偶尔出现诡异数据错位、想查根因的同学。
1. 先弄清楚你在跟谁打交道:DDR控制器与AXI4 Master的关系
1.1 你的Master不是直接连DDR引脚的
很多新手对“FPGA访问DDR”的第一印象是:我写个状态机,像操作 SRAM 一样拉高片选、给地址、读写数据,DDR 里的数据就能被搬走。这个想法错得比较离谱。
DDR 的工作机制和 SRAM 完全不同,它需要不停刷新、需要预充电、需要按 bank 和 row 切换、需要 data strobe 做双沿采样,还有 DLL 校准、片上终端等一堆精密时序。这些东西靠用户逻辑一条一条拉引脚是根本做不过来的。所以主流 FPGA 厂商的做法是给你提供一个 DDR 控制器 IP,比如 Xilinx 的 MIG(Memory Interface Generator)、Intel 的 EMIF,控制器内部把 DDR 的刷新、时序训练、bank 管理、读写调度全部接管。你写的逻辑只需要连接控制器向用户开放的接口,而不是 DDR 本身的引脚。
这里就引出了关键一点:你写的“AXI4 Master”本质上是 DDR 控制器的上位,它只负责按 AXI4 协议发出读写请求,至于请求什么时候真正落到 DDR 颗粒上、数据在 bank/row/column 之间怎么排布,你不需要关心。我见过有人纠结“我要不要自己把地址换算成 row 和 column”,完全不需要,MIG 会做地址映射,用户地址是连续线性地址就够了。
1.2 为什么很多新手宁可自己写Master,也不用现成IP
如果说控制器已经有现成 IP,那我是不是只要在 IP 配置界面里点一点、把里面的 FIFO 口读出来写进去就行?当然可以,但现实问题是,控制器的用户接口(比如 MIG 的 app 接口)虽然好用,它给你的只是简单的读使能、写使能、地址数据端口,调度能力非常有限。图像处理里常见的需求:一帧图像从边缘检测模块出来,要按 stride 跳到下一行继续写;或者前端产生数据的速度是突发的,后端 DDR 带宽经常被刷新占掉,这时如果只靠 app 接口的单个读请求、写请求,调度和反压处理全得自己在外围堆状态机,代码很容易散成一锅粥。
AXI4 Master 更像是一个统一的“交通规则”:它定义好了四个独立通道、握手协议、突发传输、响应机制,你的数据通路、缓存 FIFO、调度状态机都围绕 AXI4 的 VALID/READY 来设计。哪怕以后你不接 DDR 了,改成接 PCIe、接 AXI Crossbar、接 DMA,这套 Master 代码照样能复用。我自己习惯先写一套简洁的 AXI4 Master,之后再做图像缓存、数据采集、加速器搬运,都直接在这套基础上改,比每次重新怼 app 接口省心很多。
所以这里的结论是:如果你只需要临时从 DDR 读几十个数、写回几十个数,用 app 接口或者现成的 AXI4 VIP 就够了;但如果你想做出一个可以长期演进、能对接多种外设的数据通路,自己搞定 AXI4 Master 是值得的。
1.3 四个通道各管一段,记不住就错了
AXI4 的读和写是分开的,写操作涉及三个通道,读操作涉及两个通道,一共五个通道,其中写数据通道和写地址通道是解耦的。
- 写地址通道(AW):Master 告诉从机“我要往哪个地址写”,一次突发写只需要给一次地址。
- 写数据通道(W):Master 把要写的数据一个个发过去,最后一拍必须拉高 wlast。
- 写响应通道(B):数据写完后,从机通过 bvalid/bresp 告诉 Master“写成功了没”。AXI4 里的写操作必须有这一步响应,不是地址和数据发完就结束的。
- 读地址通道(AR):Master 告诉从机“我要从哪个地址读”。
- 读数据通道(R):从机把读回来的数据一拍一拍送回来,最后一拍用 rlast 标记。
每个通道都是独立的握手关系:valid 信号由发送方拉起,ready 信号由接收方拉起,只有当 valid 和 ready 同时为高时,这一拍数据才算真正传输成功。这个规则看起来简单,但实际调 board 的时候,大量问题都出在有人把 valid 打了一拍、结果和 ready 的握手时机对不上,或者手写 ready 逻辑导致握手死锁。
在往下写代码之前,建议你把上面五个通道的职责背下来,只要有一个通道的作用搞混,地址和数据对不上号的问题就会接二连三冒出来。
2. 动手写代码前,先避掉三个最贵的坑
2.1 地址对齐:FIXED Burst到底要不要对齐?
先回答那个经常被人挂在嘴边的疑问:AXI4 的 FIXED burst 要求地址对齐吗?要求,而且非常严格。
FIXED burst 的含义是:传输过程中地址保持不变,看起来像一个固定端口的重复读写,通常用于访问寄存器或者 FIFO。但“地址不变”不等于“地址可以随便给”。AXI4 协议规定,对于任何 burst 类型,AWADDR/ARADDR 的低位都必须与传输的数据位宽对齐。比如你用的是 32-bit 数据总线,那么地址的 [1:0] 必须为 0;如果数据总线是 64-bit,地址的 [2:0] 必须为 0。这个对齐要求不是从机“建议你这么做”,而是 Master 必须保证的协议要求,否则从机根本没法把地址映射到对应的字节通道上。
INCR burst(地址递增)更是如此。你发起一个 8 拍的突发读,每个 beat 是 32-bit,那么起始地址必须是 4 字节对齐,否则第一拍数据、第二拍数据在字节通道上的落位都是错的,读回来的数据整体错位,而且有时候还不会报错。很多人的现象就是:明明 DDR 里写的数据是 0x01 0x02 0x03 0x04,读出来却变成了 0x00 0x01 0x02 0x03,其实就是地址没对齐多偏了一个字节。
2.2 位宽、Beat、字节使能:先算明白再写代码
地址对齐规则和 awsize/arsize 直接相关,awsize 表示一个 beat 里的有效字节数,它的编码是 2 的幂次。常见对应关系如下:
| awsize/arsize 编码 | 单拍字节数 | 适用场景 |
|---|---|---|
| 3'b000 | 1 字节 | 稀疏数据、DDR 非对齐访问 |
| 3'b001 | 2 字节 | 16-bit 外设、图像灰度短数据 |
| 3'b010 | 4 字节 | 32-bit DDR 数据宽度,最常见 |
| 3'b011 | 8 字节 | 64-bit DDR 数据宽度,追求高带宽 |
awlen/arlen 表示“本次突发有多少拍减 1”。很多人第一次写 AXI4 都会在这被坑:如果我要发 8 个 32-bit 数据,awlen 应该写成 7,而不是 8。写成 8 就意味着突发 9 拍,控制器会等第 9 拍数据,等不到就卡死,或者把下一笔操作的第一个数据错误地当作当前突发的一部分。
地址递增的换算公式是:下一拍地址 = 当前地址 + (1 << awsize)。在写代码时别用乘法,用移位就行。例如 awsize=3'b010 代表 4 字节,地址递增就是 awaddr + 4,对应代码就是 awaddr + (32'd1 << awsize)。这个移位看起来基础,但状态机里忘掉这一步、导致所有 beat 写同一个地址,也是经常出现的低级错误。
2.3 突发跨4KB边界:一次最大突发撞墙的解法
AXI4 协议限制一次突发不能跨越 4KB 地址边界。原因是 AXI4 的地址保护机制按 4KB 页面管理,跨页会导致无法确定是否越权访问。DDR 控制器通常也直接沿用这个限制。
打个比方,你的数据总线是 32-bit,一次突发长度设为 255(这是 AXI4 允许的最大值),那么一次传输最多能搬 255×4=1020 字节,看起来完全没到 4KB,好像不会跨界。实际工程里真正容易出问题的是“起始地址不在整 4KB 边界上”的情况。比如基地址是 0x1000_0FF0,数据宽度 32-bit,突发长度 16,传输 64 字节,从 0x0FF0 一直写到 0x1030,正好跨越 0x1000 这个 4KB 边界。
解决办法有两个。第一个方案是在软件或者上层硬件里保证基地址 4KB 对齐,把需要传输的数据拆成多笔,每笔都不跨界;第二个方案是在 Master 内部加一个“边界检测”逻辑,每次发起突发前计算当前地址距离下一个 4KB 边界还剩多少字节,然后动态调整 awlen/arlen。第二个方案更通用,但状态机会复杂一些。新手阶段我建议先做第一个方案,把传输地址和长度在外部规划好,Master 只负责忠实地执行,能少踩很多坑。
3. 写通道状态机拆解:AW、W、B三段握手怎么配合
3.1 状态划分:一次写操作被拆成几步
很多入门代码喜欢把 AXI4 写操作做成一个大状态机:IDLE -> 写地址 -> 写数据 -> 等响应 -> 结束。这个思路直接可行,但要注意一个细节:AW 通道和 W 通道在 AXI4 协议里是独立的,理论上可以先发地址再发数据,也可以先发数据再发地址,甚至可以地址和数据同时发。不过对于初学者,最稳的做法是先发 AW,等 AW 握手成功之后再发 W。这么做牺牲一点点性能,但逻辑清晰,不容易出现对端还没准备好接收数据、你的 wvalid 已经拉了一个月的情况。
下面给出的代码是一个教学向的写状态机,只做单笔、顺序 INCR 突发写。我把计数器 wr_data_cnt 定义为“已经完成握手的数据个数”,而不是“当前正在发送的数据序号”。这个定义方式是关键,wlast 的时序就是靠它推出来的,很多新手卡在 wlast 上,根本原因是计数器语义没定清楚。
3.2 写状态机关键代码:计数器语义定清楚,wlast就顺了
// ========== AXI4 Master 写通道状态机(教学简化版) ========== localparam W_IDLE = 3'd0; localparam W_AW = 3'd1; localparam W_DATA = 3'd2; localparam W_RESP = 3'd3; localparam W_DONE = 3'd4; reg [2:0] wstate; reg [7:0] wr_data_cnt; // 已经完成握手的数据个数 always @(posedge axi_clk or negedge axi_rst_n) begin if (!axi_rst_n) begin wstate <= W_IDLE; awid <= 4'd0; awaddr <= 32'd0; awlen <= 8'd0; awsize <= 3'd0; awburst <= 2'b01; // INCR awvalid <= 1'b0; wvalid <= 1'b0; wlast <= 1'b0; wdata <= 32'd0; wstrb <= 4'b1111; wr_data_cnt<= 8'd0; end else begin case (wstate) W_IDLE: begin if (wr_start) begin awaddr <= wr_base_addr; awlen <= wr_burst_len - 1'b1; // 注意:len = 拍数 - 1 awsize <= 3'b010; // 32-bit awburst <= 2'b01; // INCR awvalid <= 1'b1; wstate <= W_AW; end end W_AW: begin // 地址握手成功,开始准备发数据 if (awvalid && awready) begin awvalid <= 1'b0; wvalid <= 1'b1; wdata <= wr_fifo_dout; wr_data_cnt<= 8'd0; wlast <= 1'b0; wstate <= W_DATA; end end W_DATA: begin if (wvalid && wready) begin if (wr_data_cnt == awlen) begin // 当前握手的这一拍就是最后一个数据 wvalid <= 1'b0; wlast <= 1'b0; wstate <= W_RESP; end else begin // 还有数据要发 wr_data_cnt <= wr_data_cnt + 1'b1; wdata <= wr_fifo_dout; // 如果下一次要发的是最后一拍,提前拉高 wlast if (wr_data_cnt == awlen - 1'b1) begin wlast <= 1'b1; end end end end W_RESP: begin // bvalid 可能比数据晚好几个周期才回来 if (bvalid && bready) begin if (bresp == 2'b00) begin wr_done <= 1'b1; wstate <= W_DONE; end else begin // 2'b01 是 EXOKAY,2'b10 是 SLVERR,2'b11 是 DECERR // 新手可以先只处理 2'b00,其他响应统一报错 wr_error <= 1'b1; wstate <= W_DONE; end end end W_DONE: begin wstate <= W_IDLE; wr_done <= 1'b0; end endcase end end这段代码里最容易引起疑问的就是wr_data_cnt == awlen - 1'b1这个条件。我们来推一遍:awlen 是“总拍数减 1”,假设配置 wr_burst_len=8,所以 awlen=7。刚进入 W_DATA 时 wr_data_cnt=0,此时发出的是第 1 个数据。握手成功后 wr_data_cnt 加 1,判断条件里 wr_data_cnt 变为 1。以此类推,当 wr_data_cnt==6 时,说明已经完成了 6 个数据的握手,当前正在发第 7 个数据,握手后 wr_data_cnt 变成 7,wdata 更新为第 8 个数据,同时wr_data_cnt==awlen-1满足,wlast 在下一个周期拉高。于是第 8 个数据发送时 wlast=1,而这一拍握手完成后满足wr_data_cnt==awlen,状态机立刻跳转到 W_RESP。整个流程正好 8 拍,wlast 准确落在最后一拍上。
如果你把 wr_data_cnt 定义为“当前正在发送的数据序号”,上面的条件都得重推,而且很容易出现差一拍的问题。这里我给的建议是:用“已发送数量”这个语义统一管理计数器,写完后用仿真波形核对一遍 wlast 的时机,比靠肉眼检查代码可靠得多。
3.3 写响应通道:数据发完了不算完,BVALID必须等到
很多第一次调 DDR 读写的人写完了 W 通道就往下一个状态跳,结果第二次写操作发起时数据全乱了。原因是 AXI4 写操作必须等待从机通过 B 通道返回响应,这个响应代表“数据真正被从机接收并处理”。对于 DDR 控制器,BVALID 通常会在 W 通道最后一拍握手成功后几个周期才出现,中间需要时间把数据写入内部缓冲、完成调度。
考虑实际性能,可以在 W_RESP 状态里给 bvalid 加一个超时计数,比如超过几十个周期还没有 bvalid 就拉 error。虽然 DDR 控制器正常不会这么慢,但加上超时保护能避免调试时状态机卡死在一个永远等不到响应的状态。另一个常见错误是 bready 一直为 1。在简单单笔写模型里这没问题,但如果以后做多笔写流水线,bready 必须根据接收缓冲区状态来控制,否则响应通道会把缓冲区塞爆。
3.4 写数据 FIFO 与反压:wdata 不要凭空产生
上面代码里 wdata 直接从wr_fifo_dout取,这也是我建议的工程做法:要写的数据先进 FIFO,Master 状态机只管按节拍从 FIFO 里读出来发出去。这么做的好处是,你的 Master 不需要关心上层数据是怎么产生的,FIFO 有数据就发,FIFO 空了就等。状态机里可以加一个判断:wr_fifo_empty 为高时不要进入 W_DATA,或者进入 W_DATA 后暂停拉高 wvalid,直到 FIFO 里有数据。我习惯是让 wvalid 受!wr_fifo_empty约束,不要让 wvalid 在没有数据的时候干拉高,否则从机的 wready 即使为高,握手成功拿到的也是一个无效数据,越写越错。
4. 读通道实现:AR发出去之后,数据从哪边回来
4.1 读请求:和写地址通道几乎一样
读通道的状态机相对写通道简单,因为不需要额外响应通道。整个流程是:IDLE 收到 rd_start 后,把地址、长度、大小、突发类型给到 AR 通道,arvalid 拉高;等 arready 握手成功后,就进入 R_DATA 状态,准备收数据。
// ========== AXI4 Master 读通道状态机(教学简化版) ========== localparam R_IDLE = 2'd0; localparam R_AR = 2'd1; localparam R_DATA = 2'd2; reg [1:0] rstate; reg [7:0] rd_data_cnt; always @(posedge axi_clk or negedge axi_rst_n) begin if (!axi_rst_n) begin rstate <= R_IDLE; arid <= 4'd0; araddr <= 32'd0; arlen <= 8'd0; arsize <= 3'b010; arburst <= 2'b01; arvalid <= 1'b0; rready <= 1'b0; rd_data_cnt <= 8'd0; end else begin case (rstate) R_IDLE: begin if (rd_start) begin araddr <= rd_base_addr; arlen <= rd_burst_len - 1'b1; arsize <= 3'b010; arburst <= 2'b01; arvalid <= 1'b1; rstate <= R_AR; end end R_AR: begin if (arvalid && arready) begin arvalid <= 1'b0; rready <= 1'b1; // 地址握手成功后立刻准备收数据 rstate <= R_DATA; end end R_DATA: begin if (rvalid && rready) begin rd_fifo_we <= 1'b1; rd_fifo_din <= rdata; if (rlast) begin rready <= 1'b0; rstate <= R_IDLE; rd_done <= 1'b1; end end end endcase end end这段代码的关键是 rlast 的处理:当检测到 rlast 拉高,说明这一拍是读突发的最后一拍,数据写入 FIFO 后整个读操作就结束了。rd_done 可以是一个单周期脉冲,用来通知上层“这一笔读完了,数据已经在 FIFO 里”。
4.2 读数据返回时序:rvalid和rready要配合好
DDR 控制器读操作的一大特点是延迟不确定。从 AR 握手成功到第一个 rvalid 拉高,中间可能隔了几拍,也可能隔了几十拍,取决于 DDR 刷新、bank 冲突、仲裁等待等。所以在 R_DATA 状态里,rready 必须持续有效,否则控制器已经准备好数据、你却不收,它就会停下来等你,整个读带宽立刻断崖式下降。
但“rready 一直为 1”也不是万金油,它有个前提:你要保证下游的读 FIFO 永远有空位。如果读 FIFO 满了,rready 还一直为 1,那 rdata 就会因为 FIFO 写不进去而丢失。实际工程中,我会把 R_DATA 状态的 rready 直接和!rd_fifo_full绑定,FIFO 快满了就拉低 rready,让控制器等一等。这个背压机制非常重要,尤其在图像处理里数据吞吐很大的时候,不处理背压很容易出现“读着读着丢一拍”的诡异现象。
4.3 多笔读请求:先别想着乱序返回
AXI4 支持多个读请求同时发出去,从机返回数据的顺序也可以乱序。但乱序处理对新手来说属于灾难级难度,你需要给每一个读请求分配 ID,然后根据返回数据的 ID 判断它是哪一笔操作的数据,还得在 FIFO 里按 ID 分类缓存。DDR 控制器一般默认不会主动乱序返回多个请求,除非你明确允许它乱序。所以我建议新手阶段把 Master 设计成“发一笔、等一笔、收一笔”的串行读模式,读性能虽然低一点,但逻辑清晰,出错也容易排查。等整个通路稳定了,再考虑增大并发度。
5. 把Master接到DDR控制器:跨时钟域与复位亚稳态
5.1 用MIG/EMIF生成AXI4接口时的配置选择
如果你用 Xilinx 的 MIG,在配置界面里会有一个“AXI4 Interface”相关的选项。我建议直接勾选 AXI4,而不是 AXI4-Lite。AXI4-Lite 是轻量级接口,不支持突发,一次只能读或写一个可寄存器长度的数据,拿来配置寄存器可以,拿来搬运图像数据就是灾难。勾选 AXI4 之后,MIG 会给你暴露一组完整的 AXI4 从机信号,你就把自己写的 Master 直接对应接上去。
Intel 的 EMIF 思路类似,现代版本提供的是 Avalon-MM 接口,如果你要用 AXI4 Master,通常需要经过 AXI 到 Avalon 的桥。不过多数 Intel 例程里也直接给出了 AXI4 接口选项,具体以你用的 Quartus 版本为准。
5.2 时钟选择:用户逻辑和控制器要跑同一个时钟域
MIG 给你提供两个关键时钟:一个是给你用户逻辑用的 ui_clk,一个是 DDR 物理层的高频时钟。你写的 AXI4 Master 必须跑在 ui_clk 上,不能自己随便用另一个 PLL 时钟。如果两个时钟来自同一个 MMCM/PLL 的同一路输出,相位关系是确定的,可以不算跨时钟域;只要不是同一个时钟网络,就必须做同步处理。
大多数 MIG 工程里,ui_clk 和 DDR 本身的高频时钟是异步的,所以从高频时钟域来的 DDR 读数据会经过控制器内部同步后才出现在 AXI4 总线上。我们自己设计时不要再去碰那些 DDR 物理层的信号,只认准 AXI4 接口这一套时钟即可。
5.3 复位同步释放:避免亚稳态最有效的技巧
AXI4 的复位信号虽然可以异步置位,但释放时必须同步到工作时钟。最简单可靠的做法是:外部异步复位进来,先打两拍同步到 ui_clk,再作为整个 Master 的复位。代码写法如下:
reg [1:0] rst_sync; always @(posedge axi_clk or negedge ext_rst_n) begin if (!ext_rst_n) begin rst_sync <= 2'b00; end else begin rst_sync <= {rst_sync[0], 1'b1}; end end assign axi_rst_n = rst_sync[1];这个同步器几乎每个 FPGA 工程都要用,但很多人图省事直接拿外部异步复位作为状态机复位,结果在掉电瞬间或上一个模块重启时,复位信号和时钟边沿的时序关系不确定,状态机偶发跳到非法状态。你如果发现自己的 DDR 读写“时而正常、时而不正常”,先检查一下复位释放是不是做了同步处理。
5.4 上电初始化:init_calib_done没拉高之前别碰DDR
MIG 上电后要做一段时间的校准,校准完成前它是不会响应你任何读写请求的。MIG 会输出一个 init_calib_done 信号,你要等这个信号拉高之后,Master 才允许发出第一个读或写请求。我做过一个项目,刚开始没加这个等待,上电后主机先发了一笔配置读,结果读回来的全是 0,程序初始化一直失败。后来在 Master 顶层加了一句:只有在 init_calib_done 为高且复位释放完成时才允许从 IDLE 出来,问题立刻消失。
这个等待不是可选项,是必选项。建议把这个信号接到你顶层控制逻辑里,作为整个 Master 的使能门控。
5.5 地址映射:别试图去猜bank和row
DDR 控制器的地址映射会把用户地址自动拆分成 row、col、bank 等内部地址,不要自己手动去算某个地址在哪个 bank、哪一行。你越权干预,反而容易引发额外的 bank 冲突和时序惩罚。你要做的只是保证地址对齐、不跨 4KB、突发长度合理。
如果发现 DDR 实际带宽上不去,优先考虑是不是突发长度太短。比如你每次只写 4 个 32-bit 数据,控制器要在刷新间隙里为一个短突发做一堆预充电和激活,效率自然高不了。图像处理里建议单次突发至少 16 拍以上,数据总线 32-bit 时一次 16 拍就有 64 字节传输,对 DDR 控制器友好得多。
6. 仿真与板级调试:怎么确认你的Master没写错
6.1 最快验证方式:写一个最小testbench
自己写的 AXI4 Master,上板之前一定要先仿真。我经常看到有人直接综合下载,然后用 ILA 抓信号,抓了半天下不了结论,就是因为在 PC 上验证比在板子上反复重新编译快太多了。最简单有效的测试是:例化自己的 Master,再接一个 AXI4 Slave 仿真模型,模型不需要功能多完整,只要能完成握手并返回响应即可。
ModelSim/Questa 里可以快速搭这样的仿真环境,Xilinx 的 Vivado 自带仿真器也能跑。写一个最小的 AXI4 Slave 模型,在接手 awvalid 后返回 awready,在收满 awlen+1 个 wdata 后返回 bvalid,对 R 通道就根据 arlen 循环返回 rdata 和 rlast。用这个模型做仿真,重点看三件事:写数据握手是否连续、wlast 是否落在最后一拍、读数据是否一个不漏地进了 FIFO。
6.2 ILA抓信号:三看三查
上板调试的时候,ILA 是必须的,但不要一上来就把所有信号都抓,那样波形密密麻麻反而看不出问题。我建议至少抓这些信号:
| 信号组 | 抓什么 | 主要看什么 |
|---|---|---|
| 顶层控制 | wr_start、rd_start、done、init_calib_done | 有没有在 init_calib_done 低的时候发起操作 |
| 写通道 | awvalid、awready、wvalid、wready、wlast、bvalid、bresp | AW 和 W 是否都握手成功,wlast 是否在最后一拍,bresp 是否 2'b00 |
| 读通道 | arvalid、arready、rvalid、rready、rlast、rdata | AR 是否握手,rvalid 和 rready 是否连续握手,rlast 是否在最后一拍 |
| FIFO 接口 | wr_fifo_empty、rd_fifo_full | 写数据有没有断流,读 FIFO 有没有满导致反压 |
实际调试中我发现一个现象:很多人上板后遇到数据不对,第一反应是疯狂加 ILA 观察窗口,其实更快的办法是先看 done 信号有没有正常拉高。如果整个交易一直没结束,多半是握手卡死了,比如 wready 没来而你还在等,或者 bvalid 一直没回来。如果 done 正常拉高但数据不对,再去查地址对齐和字节使能。
6.3 常见翻车信号特征对照
根据我自己和身边朋友踩过的坑,下面几个现象只要出现,基本可以直接定位到具体原因:
- 现象一:写入 DDR 的数据全对,读出来却每个 beat 错位 4 字节。大概率是读地址 araddr 没有按数据宽度对齐,或者 awsize/arsize 配置和实际总线宽度不一致。
- 现象二:写完一笔后第二笔写就卡死。先查 W_RESP 状态有没有等 bvalid,如果数据发完直接跳回 IDLE,上一笔响应没处理完,控制器内部出错的概率极高。
- 现象三:数据能写能读,但随机隔几笔就多一个字节或者少一个字节。八成是 wstrb 字节使能异常。比如你明明写 32-bit,wstrb 应该一直为 4'b1111,如果不小心置成了 4'b0011,那 DDR 里的高 2 字节根本没写进去。
- 现象四:uj_clk 都正常工作,但复位后的第一次读写永远失败。这个问题经常是复位同步没做,或者 init_calib_done 等待条件缺失。
- 现象五:ILA 里看到 awvalid 拉高了,awready 也拉高了,但 awlen 不对。记得 awlen 是拍数减 1,8 拍写 7,16 拍写 15,这个坑我见过至少三次,不能说烂。
我刚工作那阵子调 DDR 写 Master,最深的感受就是:AXI4 协议本身不复杂,难的是你对每一拍握手都保持敬畏。多写一个 valid、少拉一拍 ready,控制器和总线并不会报错,但数据就在你眼皮底下悄悄错位。所以如果你正在被 FPGA 的 DDR 读写折磨,不要急着怀疑人生,先回到协议本身,把时序图一拍拍对清楚,大部分问题都能在仿真阶段解决掉。
把上面的内容消化完,你手里其实已经有了一份能跑通读写的基本 Master 代码框架。接下来要做的事,就是把它接到自己的数据通路上,先用仿真确认行为正确,再上板实测。AXI4 这条路,走通一次之后,后面再做 DMA、图像缓存、异源数据采集,都会顺畅不少。