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

资讯详情

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

FPGA实战:AXI总线协议设计、时序关键点与调试技巧详解

FPGA实战:AXI总线协议设计、时序关键点与调试技巧详解

干FPGA时间久了你会发现,凡是跟高性能、多模块打交道的地方,绕不开AXI总线协议。无论是Zynq里ARM和PL的通信,还是自己搭一个多主多从的片上网络,AXI这套东西基本上就是事实标准。很多朋友一开始看协议文档会觉得头大,满屏的通道、握手、突发,好像很玄乎,但实际上把它拆成Master和Slave两边来看,再用状态机去落地,并没有想象中那么难。这篇文章我就从实战角度,把AXI总线协议的完整FPGA设计思路、时序关键点、以及调试中踩过的坑一次性讲透,适合刚接触AXI、想自己动手写Master或Slave模块的开发者参考。

1. 为什么我劝你用AXI总线,而不是自己造轮子

做FPGA设计的时候,很多人最开始习惯了自己定义接口,比如一个valid、data、ready,点对点传数据,简单粗暴。等到模块一多,A要跟B通信,C也要跟B通信,D还要跟A通信,这时候你就会发现,自己随便定的接口根本没有统一规则,每个模块都要单独写一套握手逻辑,光是适配对方时序就够折腾半天。AXI总线协议最大的价值,就是它把通信这件事给标准化了,大家按照同一套规则来,接口一接就能跑,省掉了大量重复劳动。

1.1 AXI总线到底是什么:三个通道与一次握手

AXI是ARM AMBA协议家族里的一员,主打高性能、高频率、低延迟。它跟传统总线最大的区别是采用了多通道机制:读地址通道、读数据通道、写地址通道、写数据通道、写响应通道。读和写相互独立,地址和数据相互分离,这意味着读写并行、流水线操作变得非常自然。一个事务的发起,本质上就是一次手握手的交互:发起方拉高VALID,接收方准备好后拉高READY,在两者同时为高的时钟上升沿,数据就算正式传输完成。

很多教材上来就是一堆信号表,容易把人劝退。我觉得理解AXI最笨也最有效的方法,是先把Master和Slave的角色分清。Master是发起请求的一方,比如CPU、DMA、我们自己的自定义逻辑;Slave是被动响应的一方,比如BRAM控制器、寄存器组、DDR控制器。两边签好协议,Master说我要读地址0x1000,Slave就把数据送回来,这个过程完全靠约定好的信号接口完成,不需要关心对方内部怎么实现。

1.2 这套设计的核心价值与应用场景

AXI总线协议的FPGA设计,常见应用场景非常广。首先是Zynq / MPSoC这类ARM+FPGA异构平台,ARM侧跑操作系统,FPGA侧做自定义外设,两者通过AXI接口互通;其次是多模块实时数据流,比如图像采集、预处理、后处理、显示输出,每条链路都是高带宽数据流,AXI的突发传输能把效率拉满;还有一些接口桥接场景,比如把SPI、I2C、UART这类低速接口封装成AXI Slave,统一给处理器访问,整个系统结构会清晰很多。

我见过不少项目,问题不是在算法逻辑上,而是死在接口上。明明DMA写的数据也没错,就是总感觉哪儿不对劲,最后查下来是时序违例或者握手条件没写对。所以说,AXI不仅仅是协议,更是一个系统工程,需要从Master端设计、Slave端实现、时序约束、仿真验证四个方面同时下手。这篇文章我会按这个顺序,把每个环节的关键点拆开讲。

2. AXI总线协议的关键机制,理解透才能动手写

写代码之前,先把协议的几个核心机制吃透。AXI协议说复杂也确实复杂,因为后面还有AXI-Stream、AXI-Lite、ACE缓存一致性、AXI-ATB追踪总线等一堆变种。但对于入门和实战,我们只需要把标准AXI4(或AXI4-Lite)的通道握手、突发、读写流程搞明白,就足够应对绝大多数场景了。

2.1 握手信号与VALID/READY通道机制

AXI每个通道都遵循VALID/READY握手规则。发送方拉VALID表示本周期数据/地址有效,接收方拉READY表示本周期可以接收。规范文档里给了三种握手场景:第一种是双方同时准备好,VALID和READY在同一周期都为高,立即传输;第二种是发送方先准备好,VALID先拉高,接收方过几个周期才拉READY;第三种是接收方先把READY拉高,发送方过几个周期才拉VALID。协议允许这两种情况交叉出现,但有一个硬性要求:一旦VALID拉高,在握手完成(也就是两者同时为高)之前,VALID不能拉低。换句话说,发送方一旦承诺这次数据有效,就必须等着接收方,绝不能把数据悄悄撤回去。

这个机制看着简单,实际写代码时最容易出问题。比如有的同学在状态机里为了省一个周期,看到READY为高就立刻改变DATA,但这时候VALID还没配对,那上一拍的数据就被悄无声息地覆盖了,接收方收到错误数据。正确做法是:VALID拉高后,数据必须保持稳定,直到握手成功为止。

2.2 突发传输:地址与数据的节奏控制

AXI4支持突发(Burst)传输,也就是一次地址请求后连续传多拍数据。突发长度由ARLEN/AWLEN决定,0表示传1拍,7表示传8拍,255表示传256拍;突发大小由ARSIZE/AWSIZE决定,0表示1字节,2表示4字节,3表示8字节。地址的增长规则和突发类型BURST也有关系,FIXED为固定地址,INCR为递增地址,WRAP为回卷地址。最常用的是INCR,比如读DDR里的连续数据块。

这里要特别提醒:突发长度和数据拍数必须对得上。比如你发了一个AWLEN=7、AWSIZE=2的写突发,按4字节×8拍计算,总数据量是32字节。在写数据通道上,必须严格送出8拍数据,WLAST在最后一拍拉高。如果中间数据通道还经常插入空闲,WLAST的位置必须跟着有效拍走,不能按绝对周期数算。很多第一次写Slave的人就在这里翻车:数了8个时钟周期就拉WLAST,结果中间有握手等待,真正传的数据没够8拍,对端就以为突发提前结束了。

2.3 读事务与写事务的完整流程

读事务相对简单,Master先在读地址通道发一个地址(ARVALID握手),Slave收到后在读数据通道返回数据。第一拍数据可能和地址握手相隔若干周期,因为Slave需要时间去访问内部存储。对于多拍突发,数据通道会连续返回多拍,最后一拍拉RLAST,Master在收到RVALID && RREADY且RLAST=1时,认定这次读事务结束。

写事务稍微复杂,因为多了一个写响应通道。Master先发写地址(AW通道),然后发写数据(W通道),Slave把数据真正写入后,在写响应通道回一个BRESP响应(OKAY表示成功,SLVERR表示失败)。这里需要注意,写响应返回的时机并不是在最后一拍写数据握手的同时,而是在Slave内部真正把数据落位之后。比如你写一个BRAM,需要一拍写使能,那响应就要在那一拍之后返回。如果写地址、写数据还没完全处理好就急着回BVALID,Master那边可能提前开始下一个事务,导致数据还没写入就被覆盖。

3. Master侧设计与时序拆解:从状态机到代码落地

接下来进入真正的代码设计环节。先讲Master侧,因为Master是整个总线事务的发起者,也是时序里最复杂、最容易写出死锁的一方。我以一个简单的寄存器和BRAM读写控制器为例,目标是在FPGA里用AXI Master去反复读写一组地址,用来验证链路完整性。

3.1 Master接口架构与内部信号规划

设计AXI Master时,首先要明确它要接什么样的Slave。如果只是访问寄存器,用AXI4-Lite就够,地址和数据都是单拍,没有突发;如果要搬大块数据,必须用完整的AXI4,地址通道、数据通道都支持突发。为了通用性,我习惯把两类接口都预留出来,内部统一做状态机控制。

模块的信号规划大致如下:全局的axi_aclk和axi_aresetn,读地址通道的axi_araddr、axi_arvalid、axi_arready、axi_arlen、axi_arsize,读数据通道的axi_rdata、axi_rvalid、axi_rready、axi_rlast、axi_rresp,写地址通道的axi_awaddr、axi_awvalid、axi_awready、axi_awlen、axi_awsize,写数据通道的axi_wdata、axi_wstrb、axi_wvalid、axi_wready、axi_wlast,写响应通道的axi_bresp、axi_bvalid、axi_bready。很多人抱怨信号太多记不住,其实你把它们按通道一块一块归好类,每个通道只有一个核心互动关系:谁发VALID、谁回READY、通道上跑什么内容,一下就看明白了。

3.2 状态机设计:IDLE、读写地址、读写数据、写响应

我常用的Master状态机分这么几段:IDLE空闲、READ_ADDR发读地址、READ_DATA收读数据、WRITE_ADDR发写地址、WRITE_DATA发写数据、WRITE_RESP收写响应。读写可以并行设计,但初学时建议先把读写分开,用同一个状态机串行处理,等跑通了再考虑读写独立并行。

在READ_ADDR状态下,ARVALID拉高,等待ARREADY;握手成功后,如果突发长度是1拍,直接跳到READ_DATA等数据。READ_DATA状态下,RREADY拉高,当RVALID为高时接收数据;如果RLAST为高,说明这是最后一拍,读完就回IDLE或者开始下一个事务。这里有个细节:RREADY应该拉多早?协议允许RREADY一直拉高,只要Master随时能接收数据。如果你的Master内部有FIFO或者寄存器能暂存数据,建议一直拉高RREADY,这样Slave返回数据时不用等待,效率最高。如果数据通路下游处理不过来,那就要用FIFO的almost_full信号去门控RREADY。

写事务的状态机需要多留一个心眼。在WRITE_ADDR成功握手之后,不一定要等到地址通道处理完才能发数据。AXI协议允许写地址和写数据通道独立,甚至可以先发数据再发地址。但为了代码清晰,我习惯先发地址,然后立即进入WRITE_DATA。数据通道要根据突发长度,逐拍发送,每一拍都在WVALID && WREADY握手有效时递减一个计数器,到最后一拍时拉高WLAST。等数据全部发完,再进WRITE_RESP等待BVALID。如果Slave因为没有正确写入而返回SLVERR,Master要有能力感知这个错误,至少拉一个错误标志,方便调试。

3.3 关键时序案例:连续写操作的背靠背处理

做DMA或数据搬移时,经常需要连续发起多个写突发,这时候如果每个突发之间都回IDLE再重来,会有几个周期的气泡,吞吐量上不去。AXI协议允许连续的突发之间不间隔:上一个事务的最后一拍写数据握手完成的同时,下一个事务的写地址可以先发出来,甚至写数据通道可以紧接着发下一段数据,只要不违反WLAST和WVALID的组合规则。

实际操作上,我用一个pending_burst计数器来管理每个通道上还有多少个未完成的事务。比如发起一个写突发后,不等BVALID回来,就继续发下一个写突发。因为写地址和写数据通道可以流水线化,只有写响应通道需要一个一个匹配。这样做的代价是:如果Slave处理能力有限,内部缓冲满了,AWREADY或WREADY会主动拉低,我的Master就会在对应状态停下等待,这是完全合规的。所以背靠背优化的核心不是把状态机写得多花哨,而是懂得什么时候该等、什么时候该冲。

3.4 Master侧代码示例:Verilog核心片段

下面给出一段简单的AXI4-Lite Master读地址发送部分,方便大家对照理解:

// AXI4-Lite Master read request reg arvalid_r; reg [31:0] araddr_r; wire arready_comb = axi_arready; always @(posedge axi_aclk or negedge axi_aresetn) begin if (!axi_aresetn) begin arvalid_r <= 1'b0; araddr_r <= 32'h0; end else begin if (state == S_READ_ADDR && start_read) begin arvalid_r <= 1'b1; araddr_r <= read_addr_q; end else if (arvalid_r && arready_comb) begin arvalid_r <= 1'b0; end end end assign axi_araddr = araddr_r; assign axi_arvalid = arvalid_r;

上面这段代码就是把arvalid拉高,同时锁存目标地址。当arready返回时,握手完成,撤销arvalid。这里面最关键的一点是:arvalid_r一旦为高,如果没有握手成功,下一周期必须保持,不能用其他状态机分支去把它清零。很多人喜欢用一个组合逻辑next_state直接赋值,结果把arvalid在握手前清掉了,这是很典型的错误。

读数据接收部分类似,重点是RREADY的赋值。如果是AXI4-Lite,没有RLAST,每笔请求只回一拍数据。只要Master内部有接收空间,rready可以直接拉高:

assign axi_rready = (state == S_READ_DATA);

握手时用axi_rvalid && axi_rready作为数据有效标志,采样即可。这里的精细点是:如果rready只在S_READ_DATA状态为高,那状态机切换到S_READ_DATA的周期和rvalid到达的周期可能有一个相位差,需要仔细核对仿真波形,确保没有漏采数据。

4. Slave侧设计与时序拆解:让Master的建议都能落地

说完Master,再看Slave。Slave是被动方,它不能主动发起事务,只能老老实实响应。但Slave设计的难点不是逻辑复杂度,而是要在各种突发、各种等待中精准完成响应。我经常打比方:Master是外卖小哥,Slave是餐厅后厨。外卖小哥可以随时来取餐,但后厨得保证菜都做好了、做得对,不能前一个订单还没做完就接下一个,更不能做完了不给小哥。

4.1 Slave接口架构与地址译码方案

设计AXI Slave时,接口信号和Master是镜像的,区别在于握手信号的方向:例如AWVALID是输入,AWREADY是输出;WVALID是输入,WREADY是输出;BVALID是输出,BREADY是输入。读数据通道上,RVALID是Slave输出,RREADY是Master输入。

地址译码是Slave的主要逻辑之一。最简单的方式是用高几位比较:如果地址落在某个区间,就选对应的内部寄存器或BRAM。对于寄存器组,每个寄存器对应一个偏移地址,读操作时用ARADDR的低位做索引;写操作时用AWADDR做索引。要注意的是,AXI地址是字节地址,而寄存器可能是32位宽,所以最低两位通常不能直接用来索引寄存器编号,右移两位才是寄存器序号。

4.2 读Slave时序:地址请求与数据返回

读Slave的响应流程:检测到ARVALID,并且地址译码已准备好,就可以拉高ARREADY完成握手。地址锁存后,如果是简单的寄存器读,下一拍就可以把数据放到RVALID和RDATA上。这里有个权衡:如果寄存器的读取组合延迟太大,就必须像流水线一样,在地址握手完成后再用一个周期去读,否则RDATA可能出现毛刺或延迟。

如果Slave内部是BRAM,BRAM本身有读延迟(通常1~3拍),需要根据延迟去规划RVLD的生成时机。举个例子,我在某个项目里用Xilinx BRAM做Slave存储,BRAM读地址在地址握手时写入,数据在2拍后出现在读数据线上,那么就需要一个延迟计数器,在第2拍拉高RVALID并输出RDATA。如果Master端RREADY没有拉高,RVALID要保持住,同时RDATA也要保持。这里特别容易踩坑:很多人会用一个组合逻辑把RVALID和某个内部read_data_valid信号直接赋值,但这个信号如果在握手等待时被清掉了,那Master就会一直等不到数据。

解决这个问题有一个通用技巧:用一个内部FIFO做读数据缓冲。把BRAM读出来的数据先写进FIFO,然后由Slave状态机按握手规则读出到AXI接口上。这样即使Master偶尔不准备好,数据也不会丢,而且逻辑清晰很多。

4.3 写Slave时序:地址、数据、响应的协作

写Slave的流程稍微绕一点。首先是AW和W两个通道可能独立到达,甚至W数据先于AW地址到达。AXI协议允许这种情况,所以Slave不能简单地先等待地址、再处理数据,而是要把地址和数据的到达独立记录下来。我常用的方案是把地址先锁存到寄存器,把写数据也先暂存下来,当两者都齐了,再执行实际写内部的寄存器或BRAM。

以单拍写为例:当AWVALID && AWREADY握手完成后,保存AWADDR;当WVALID && WREADY握手完成后,保存WDATA和WSTRB。然后用一个状态标志位write_start,当地址有效且数据有效时拉高一个周期,执行写操作。执行完之后,在写响应通道拉高BVALID,返回BRESP=OKAY。如果BREADY没有及时拉高,BVALID必须保持住,直到握手成功。

有一点值得注意:写响应返回之后,地址锁存和写标志要及时清掉,避免同一个地址被错误地写两次。另外,如果Master连续发两个突发,地址通道和数据通道的匹配关系要严格按发送顺序来。如果内部同时只能处理一笔写事务,那么在返回BVALID之前,应该把后续的AWVALID或WVALID用AWREADY=0或WREADY=0挡住,防止数据覆盖。

4.4 让Slave支持突发:计数器与数据缓冲

如果Slave要支持AXI4突发,复杂度会上升一个级别。读突发相对好做,关键是按ARLEN计数返回数据,每返回一拍,计数器加1,到最后一拍拉高RLAST。写突发的关键则是正确解析WLAST信号和WVALID配合,不能自己内部数周期。

我的做法是:在Slave内部维护一个write_burst_len寄存器,从AWLEN取得突发长度;再维护一个write_burst_cnt,每个写数据通道握手有效拍自增一次;当WVALID && WREADY且WLAST为高时,说明整个突发数据已经全部到达,此时如果地址也已经锁存,就可以执行批量写入。如果内部存储不支持一周期写多拍,那就需要把数据先缓存到内部FIFO,再逐拍写入。注意突发模式下,WSTRB是每一拍独立的,所以写逻辑要逐拍处理字节使能,不能整包一起写。

读突发也是一样,ARLEN锁存后,每发一拍数据,RLAST就在最后一拍为高。有些Slave为了提升突发效率,会做成一次读一个地址然后在内部线性递增地址。如果内部存储是双端口BRAM,可以做到连续读不中断,性能很好;如果是单端口寄存器组,请务必计算好读写冲突。

5. 仿真与上板调试:用逻辑分析仪和约束文件排查问题

写完代码不代表完事,仿真和调试才是AXI实战的重头戏。只要时序有一丁点不对,连跑几万个周期之后才会露馅。我调试AXI接口的经验是:先用仿真脚本把主流程跑通,再用约束文件把接口时序约束好,最后上板用ILA或逻辑分析仪抓波形。

5.1 激励搭建与关键波形观测点

测试AXI Master时,需要例化一个简单的Slave模型,通常用always块模拟响应:检测到ARVALID时,下一拍返回有效读数据;检测到AWVALID和WVALID后,返回写响应。测试AXI Slave时,则要例化一个Master模型,可以写成任务(task),一键发起读或写事务。仿真中要重点监控的信号包括:所有通道的VALID和READY握手事件、RLAST/WLAST的位置、BRESP的值,以及和内部状态机的对应关系。

我习惯在关键握手处打$display日志,例如:

if (arvalid && arready) $display("[%0t] AR handshake, addr=%h", $time, araddr);

这样跑完仿真后,直接看打印日志就能知道事务走到了哪一步。如果发现日志中某个READY一直没有拉高,基本可以锁定死锁卡在哪个通道上。

5.2 时序约束的添加与关键路径分析

AXI接口工作频率高,时序约束必须做对。在Vivado里,通常需要给axi_aclk创建时钟约束,然后针对跨时钟域或其他特殊路径做约束。如果Master和Slave都在同一个时钟域,只是普通同步逻辑,一般不需要额外管脚约束。但如果接口要接到IP核或者外部引脚,比如MIG的AXI接口,就要注意MIG本身会输出对应的约束,你只需要保证自己的逻辑符合这些约束的时序预期。

使用report_timing_summary检查时序是必备步骤。AXI接口常见的时序违例出现在地址译码逻辑、数据路径的宽位宽信号上。比如RDATA是256位或512位时,数据路径上组合逻辑容易变长。对策通常是增加流水线寄存器,把大的组合逻辑切开,代价是多一个周期的读延迟。AXI协议对延迟没有硬性要求,只要Master能等,多一个周期完全没关系。这也是AXI的灵活之处。

5.3 常见死锁、数据错位问题的排查实录

我在项目里遇到的一个典型问题:Master发起写突发后,给了AWADDR和AWLEN=3,但在写数据通道只发了两拍就拉高WLAST,然后等待BVALID。Slave那边严格按WLAST判断风暴结束,就认为这次突发只有2拍,写了2个数据就返回响应。结果Master认为自己写了4拍,后续数据全部错位。这个问题就是Master的WLAST生成逻辑写错了,没有按握手有效拍数计数,而是按绝对计数。排查的时候,在仿真波形里看WVALID && WREADY的有效次数和WLAST位置,一眼就能发现问题。

另一个常见问题时序握手死锁:Master在等待BVALID,Slave却在等待BREADY返回响应,两个信号互相等,系统卡死。根因通常是Slave提前把BVALID拉高,但Master状态机还没进入WRITE_RESP状态,所以BREADY为0;而Slave头脑简单,死等BREADY为1才撤销BVALID,两者磕上了。解决方式很简单:Slave返回BVALID后,如果BREADY为0,就必须持续保持BVALID,而Master必须在最后数据拍之后的周期无条件进入等待响应状态,不要把其他处理插在中间。双方规则都遵守,不可能死锁。

再有一个数据错位问题:Master在发写地址的同时,数据通道还在传上一笔突发,如果Slave只是简单地把地址和第一个数据配对,就会把上一笔的数据写到新地址里。这是经典的跨突发错位。正确做法是在Slave收到AW握手时,记录一个aw_pending标志,在收到WLAST之前,新的AW握手即使发生也不要覆盖写地址,而是先缓存到队列。等当前突发结束后,再处理下一个。这种问题在仿真中很容易被漏掉,除非你特意做连续背靠背突发的激励。

6. 从“能跑”到“跑得稳”的经验建议

最后说点个人体会。AXI总线协议的FPGA设计,很多人以为写完状态机、仿真通过就万事大吉,实际不是。把它接到真实系统里,往往还要考虑性能、资源、死锁恢复、错误处理这些问题。

我在实际项目中踩过几次坑之后,形成了一个习惯:每个AXI组件都加上一个超时计数器。具体做法是,当某个状态停留超过比如1024个周期还没有握手成功,就拉一个超时中断,要么复位状态机,要么打印日志。这个机制在调试时能救命,因为很多死锁一旦发生,单纯看波形未必能快速找到是哪个环节,有了超时定位会快很多。

性能方面,如果你追求高吞吐,推荐把Master和Slave的接口都用FIFO缓冲起来。地址通道用一个小的aw_fifo,数据通道用独立的w_fifo和r_fifo。这样即使上下游处理速度不一致,也不会因为一两个周期的等待拖慢整体带宽。代价是资源占用和延迟增加,但对大多数中高性能系统来说,这个牺牲非常划算。

另外,AXI协议文档很长,不用死记硬背。把常用的通道握手机制、突发规则、写响应流程记住,剩下遇到特殊情况再去翻ARM官方规范。我建议每个做FPGA的人手里都留一份AXI4规范PDF,遇到拿不准的信号语义直接查原文,不要凭感觉写代码。毕竟,时序这种东西,感觉是会骗人的,只有波形和规范不会。

返回列表