ARTICLE DETAIL

资讯详情

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

手写AXI4主设备接口:从握手到状态机,用Verilog实现高效读写

手写AXI4主设备接口:从握手到状态机,用Verilog实现高效读写 所以当你的面试官问起“你用过AXI4吗”的时候他真正想确认的并不是你会不会调用Xilinx的AXI DMA IP核而是当你面对一片没有现成IP的空FPGA需要把自定义外设挂到ARM处理器总线上时你能不能自己把这套协议的主机端或者从机端用Verilog写出来。这篇文章就是干这个的基于Verilog实现AXI4协议接口的读写功能从握手信号的本质逻辑讲起到写通道和读通道的状态机具体怎么设计再到仿真时容易被忽略的边界问题最后聊聊性能上你能优化的空间到底在哪。我默认你想实现的是一个AXI4主设备Master接口如果你需要的是从设备Slave同样能从通道拆分和状态机的对应关系里找到参照。1. AXI4不是“复杂的协议”而是“五个并行的FIFO”先把心态放平。很多人一打开ARM的AMBA AXI4协议规范看到一两百页的PDF直接劝退。实际上AXI4真正需要你来处理的就是5个通道之间的握手关系而每个通道的本质就是一个带valid/ready握手的FIFO接口。你不需要把整本规范背下来但下面这个思维模型必须建立。1.1 为什么选择手写AXI4主设备接口可能你会问Vivado里拖一个AXI Master IP核不就行了为什么要手写答案分场景。如果你做的是纯PS端通过M_AXI_GP访问PL端的寄存器那确实用Xilinx自带IP最省事。但一旦涉及到性能敏感的数据搬运、自定义DMA控制器、或者需要精确控制总线时序的协议转换逻辑通用IP核反而成了黑盒出了问题你连波形都看不懂。我在实际项目中就吃过这个亏用AXI DMA搬数据带宽死活上不去后来抓了很长时间的波形才发现是地址递增模式配置错了导致每次都重新发送地址而不是burst传输。如果当初懂AXI4的burst地址规则这个问题一眼就能定位。手写接口的另一个价值在于当你的系统需要同时支持多个主设备、做总线仲裁或者插入流水线寄存器时只有理解协议底层才能正确设计。1.2 握手信号与valid/ready依赖关系5个通道分别是写地址通道AW、写数据通道W、写响应通道B、读地址通道AR、读数据通道R。每个通道都有独立的valid和ready信号规则只有三条valid拉高后直到握手完成valid和ready同时为高之前不能拉低。如果主设备没有准备好可以先不拉高valid但一旦拉高就不能因为等待而取消。这是AXI协议里最基础也最容易犯错的一条valid不能依赖ready。也就是说你设计状态机时不能写“if(ready) valid 1”这样的逻辑必须是“状态满足条件时直接拉高validready什么时候给是对方的事”。你可以等valid先拉高、再去等ready也可以等ready为高后再拉高valid。两条路径都符合协议只是时序组合逻辑深度不一样。这三条规则决定了你设计接口的整个基调。写通道的数据和地址可以独立握手这意味着你可以先把地址发出去再慢慢给数据也可以反过来。这个灵活性对性能的影响我会在第3章专门说。这里有个常见的认知误区很多人以为AXI4是“工作在多个时钟域的总线”。其实AXI4的5个通道必须都在同一个时钟域协议允许不同通道之间无固定的时序关系但主从设备必须同源时钟。如果你确实需要跨时钟域正确做法是在主设备内部或者从设备内部用异步FIFO隔离总线本身必须是同步的。2. 写通道实现把控制、数据、响应拆开处理写操作涉及三个通道AW写地址、W写数据、B写响应。通道之间互不依赖除了一个特殊情况写响应只会在写数据和写地址都完成之后才会产生这是从设备的逻辑保证在主设备端你只需要接收响应即可。2.1 先设计写状态机还是先处理数据流我的经验是先画数据流再画状态机。对于写主设备来说数据流是这样的应用层给出写请求地址长度数据。主设备在AW通道发起地址握手同时在W通道发起数据握手。从设备接收完成后通过B通道返回写响应。主设备收到B响应确认本次写入完成。基于这个数据流写状态机的核心状态可以设计为IDLE空闲等待应用层写请求。AW_TRANS发送写地址握手完成后进入W_TRANS。W_TRANS发送写数据。如果使用了FIFO缓冲可以每拍发送一个数据直到beat计数为零。B_WAIT等待写响应。收到BVALID且BREADY拉高后完成。这里有一个很重要的设计决策地址发送和数据发送是否要并行。最简单的设计是串行先发送地址等地址握手完成后再发数据。这样状态机很好写但性能差。如果你做的是纯寄存器配置接口比如AXI-Lite风格的低速写操作完全没有问题但数据量大的场景就撑不住了——地址交换那一个cycle完全被浪费。我的建议是直接在AW和W通道上地址数据并行发起// 写请求到来时同时拉高AWVALID和WVALID assign awvalid write_req state_reg IDLE; assign wvalid write_req state_reg IDLE;注意上面的代码没有让AWVALID依赖AWREADY也没有让WVALID依赖WREADY这正好符合AXI协议中valid不能依赖ready的规则。只要应用层的write_req足够长握手成功与否由总线决定状态机只在后续转换时才判断握手是否完成。2.2 多拍写入与wlast的生成逻辑当一次写操作需要多个数据拍beat时写数据通道就需要在最后一拍拉高WLAST信号。WLAST没有独立的通道它只是W通道上的一根信号线用来告诉从设备“这次burst的最后一个数据到了”。确认WLAST的生成逻辑并不复杂关键在于确定burst长度。AXI4的突发长度由AWLEN[7:0]指定数值是实际的拍数减1。比如一次写入16个数据AWLEN15。如果每拍传一个数据那么WLAST应该在数据计数器等于AWLEN的时候拉高if (wvalid wready) begin if (w_cnt awlen) begin wlast 1b1; w_cnt 0; end else begin w_cnt w_cnt 1b1; wlast 1b0; end end正确时序下WLAST必须是和数据一起传递的也就是你发出最后一个有效数据的那一拍WLAST必须同时为高而且一旦拉高必须在有的那拍完成。这里容易踩的坑是有些同学会把WLAST当作一个“拍后标志”即传输完后再单独发一拍WLAST。这是错误的会被从设备当成一个无效的额外数据拍。另外建议在设计时把AWLEN、AWSIZE等信号做成可配置寄存器而不是写死在模块里。我做过一个版本就是图省事写死了长度后来换对接模块时不得不改RTL重新综合非常被动。2.3 数据位宽与窄传输的处理AXI4的窄传输narrow transfer是指总线数据宽度大于单次访问的字节数。比如32位总线上做一次8位写操作AWSIZE2b000数据只出现在低8位或指定的strb位上。窄传输在实际项目中大量出现——寄存器配置往往就是32位总线上写8位、16位控制字。对应的WSTRB信号用来标注哪几个字节有效4字节总线上WSTRB[3:0]每一位对应一个字节的写使能。我刚接触AXI4时在这里被坑过一次做窄传输时总是把整个数据放到低字节然后WSTRB按地址变化。调试了很久才意识到AXI4协议里面WSTRB是根据地址的起始偏移和数据宽度共同决定的不能简单地在低字节置1。例如在32位总线上地址偏移为1、数据宽度为8位时数据实际应该放在byte lane 1WSTRB应该是4b0010。写这块逻辑时最稳妥的办法是建立一个字节通路byte lane映射表根据地址和访问宽度翻译WSTRB而不是靠猜。这也是AXI4性能高于APB/AHB的一个代价——它对主设备更“挑剔”了。3. 读通道实现地址一次发出数据分批回来相比写通道的多路并行读通道其实更简单但不少人在理解上绕了弯子。读操作只有两个通道AR读地址和R读数据。地址通道的一次握手只能对应R通道上的一串数据返回所以读状态机的核心是“等待数据的过程”。3.1 AR通道的发起与地址计算读取的第一步就是发地址。对于单次访问ARVALID拉高、ARREADY为高时地址被从设备接收。ARM规范里AR通道的握手不需要等待数据返回也就是说读请求发出去之后状态机可以从容地等待RVALID。地址递增的逻辑和写一致AXI4的burst是“递增式为主”地址增量等于单次数据位宽对应的字节数而不是固定4字节。这是新手最容易算错的地方。比如你想连续读8个16位数据数据总线宽度是32位合理的做法是AWLEN7AWSIZE2b00116位每个beat的地址增量是2字节。而不是AWLEN3AWSIZE2b01032位把两个数据拼到一个拍里。两者的区别在于总线效率前者是8个beat的递增burst地址有规律从设备可以预测并预取后者每拍固定32位如果应用层只需要低16位那么高16位数据其实是被浪费的带宽。我建议的读地址状态机设计localparam IDLE 2d0; localparam AR_SEND 2d1; localparam R_WAIT 2d2; always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else begin case (state) IDLE: if (read_req) state AR_SEND; AR_SEND: if (arvalid arready) state R_WAIT; R_WAIT: if (rvalid rready rlast) state IDLE; endcase end end读地址通道只工作一次然后状态机就进入长时间等待R数据的阶段。不要试图在这个阶段继续发AR除非你想设计的是多个outstanding的乱序传输那需要独立的ID管理我放到第5章聊。3.2 读数据缓存与对齐问题R通道上返回的数据不一定按你希望的方式对齐。读操作没有WSTRB因为读出来的数据总是完整的字节通路宽度。但你作为主设备需要从返回的数据中提取你真正需要的字段。比如你需要从从设备读回一个64位的状态字而数据总线是32位的那么R通道会返回两拍第一拍对应对齐地址的低32位第二拍是高32位。读状态机需要把这两拍拼起来。这里的拼接逻辑要注意字节序。ARM体系下AXI4总线默认小端模式所以第一拍数据对应低地址应该拼到结果的低字节位。if (rvalid rready) begin if (r_cnt 0) data_low rdata; else data_high rdata; end更复杂的实现里接收到的数据会先存进FIFO由后级逻辑按需消费。这种情况下你不需要关心R通道的拍序只需要把FIFO深度设置成不小于最大burst长度防止溢出。这里有一个经验值FIFO深度至少是突发长度的2倍。因为如果后级消费速率不稳定写满一次就可能导致R通道ready拉低而从设备那边可能已经准备好连续返回数据此时如果不能在一个很短的时间内恢复ready就会拖慢整个总线的读效率。4. 仿真验证与踩坑你看到的波形不一定对写完RTL只算完成了一半另一半是仿真验证。AXI4协议接口的仿真调试很多问题不是功能错误而是协议违例——功能看起来对但协议时序不合法。这类问题最难查因为一旦接入真实系统表现出的症状往往是偶发死机、随机数据错误而不是你眼前这个波形直接报错。4.1 自己写testbench还是用SystemVerilog断言如果你只是验证主设备接口最简单的方案是用Verilog写一个扮演从设备角色的bus functional modelBFM。BFM的关键不是验证逻辑而是以协议允许的时序来响应主设备的请求。我习惯在testbench里加断言assertion这样比肉眼盯波形高效得多。比如验证valid和ready的依赖关系// 断言一旦valid拉高就不能仅为等待ready而拉低 property p_valid_no_unwait; (posedge clk) valid | valid throughout !ready[-1]; endproperty如果没有SystemVerilog环境也可以退而求其次用始终块做采样检查。我遇到过这样一个经典问题写通道里我用了组合逻辑把WVALID连接到状态机上结果WVALID随WREADY变化产生了一个毛刺导致仿真里从设备收到了错误的写使能。这类问题靠看功能波形是看不出来的只有加断言检查采样窗口才能发现。4.2 仿真中遇到的几个经典问题按我自己的经验手写AXI4接口仿真最容易出现三片雷区。雷区一握手信号和状态机之间的时序竞争。很多人在状态机输出用组合逻辑直接驱动VALID又用同一个状态机状态去采样READY导致VALID和READY的建立时间计算混乱。在实际时序约束分析时整合逻辑很可能不满足时序要求。一个简单做法是把状态寄存器改成“预判下一拍状态”的方式让VALID的生成和状态机的下一拍状态对齐避免组合逻辑环。雷区二WLAST和WVALID的时序对齐。前面提到过WLAST要随最后一拍数据一起发送。但如果WVALID和WLAST的生成逻辑在不同的分支里仿真环境下有可能因为阻塞赋值的顺序问题导致两者错拍。写RTL时建议在同一个always块中对WVALID、WLAST、WCNT统一赋值保证同拍更新。雷区三读通道RVALID与RLAST不伴随。从设备返回最后一拍数据时RVALID和RLAST必须同时有效这本来是协议规定的。但如果你在状态机里判断的是‘接收完成“之后再去看RLAST就会丢掉最后一拍数据。我建议在R通道接收逻辑里把“接收数据”和“判断最后一拍”放在同一个条件里处理而不是分成两个状态if (rvalid rready) begin fifo_wr_en 1b1; if (rlast) begin fifo_wr_en 1b1; // 这里带上last标记一起写入FIFO end end这个做法在数据通路后级需要“整包处理”时尤其有用。如果最后再额外判断一次rlast就得多一个状态也多一层判断延迟。4.3 地址边界与burst跨越问题地址对齐和边界跨越问题是仿真覆盖里容易漏掉的点。AXI4协议不允许一个burst跨越4KB边界历史原因与页表管理相关。如果主设备发出跨越4KB的burst请求从设备可以拒绝或者截断这在实际SoC中意味着总线错误。设计主设备时有两种处理方式。第一种是在应用层保证每次burst的长度设置不超过当前地址到4KB边界的剩余空间。第二种是在接口层自动拆分如果应用层Access一次传入的地址长度跨越了4KB接口自动把它拆成两个burst。第二种方式更通用代价是控制逻辑复杂一些。我自己倾向第一种方式至少在接口层要暴露一个“burst长度限制”的信号由上层来保证合法性。否则你会陷入到底拆几个burst、burst间要不要插入空闲拍这类细节工程量会明显上涨。仿真的边界测试不建议只测满长度burst。我实际遇到过从地址偏移为0xFFC开始读16字节的情况如果按固定burst发出直接越界。这个Case如果你测试向量没覆盖到合到SoC里跑系统很容易随机死机还极难复现。5. 实测极限性能瓶颈与优化空间接口已经能跑通了波形也符合预期了再往前一步是性能问题。AXI4在协议层面可以支持很高的并发但真正决定带宽的是你怎么使用它。下面几项是我在实际项目中反复调优的经验。5.1 关键路径与时序收敛时序收敛是接口设计绕不开的话题。AXI4总线上最可能成为关键路径的通常是握手信号和地址计算逻辑。比如产生ARADDR的加法器如果是一个宽度的累加器从地址寄存器的输出到下一个地址的组合逻辑会很长尤其当数据总线宽度到128位甚至256位时。我比较推荐的加法器处理方式是“先减后加”转为“预加”在状态机处于IDLE状态时就把下一次burst的地址预计算好状态机进入传输状态后直接用预计算结果而不是等当前beat结束才做加法。这个优化看起来简单但对时序的帮助很大尤其当总线频率到200MHz以上综合工具对组合逻辑深度的要求会非常苛刻。5.2 提升吞吐率outstanding传输与ID管理AXI4协议允许主设备同时发出多个写事务多个AW或多个读事务多个AR而不必等第一个完成。这就是outstanding的能力。它能让总线的读写请求“排着队”被从设备处理避免因为从设备处理慢导致的总线空闲。但outstanding是一把双刃剑。多事务同时进行后返回数据的顺序可能不再和请求一致。AXI4没有强制要求必须保序这就得靠事务ID来区分响应属于哪个请求。如果你在做一个简单的FIFO搬运DMA我建议先从outstanding1开始把功能跑通再加深度。做高并发时每多一级outstanding你得维护一个请求队列记录每个挂起事务的地址和长度收到响应时再根据ID做匹配。这个逻辑如果一开始没有设计数据结构后期重构成本会非常高。5.3 与AXI4-Lite和AXI-Stream的对比很多项目实际是AXI4、AXI4-Lite、AXI-Stream三者混合使用的。我的设计原则是AXI4-Lite只用于寄存器配置的读写不支持burst每次读写一个数据适合低带宽的控制面。不要拿它跑数据。AXI4用于通用内存读写支持突发传输适合CPU访问内存、DMA搬运这种场景。AXI-Stream严格来说没有地址线只有数据流适合连续的数据处理比如视频流、FIFO输入输出。做一个完整SoC时我的习惯是把AXI4用作系统主总线外设按功能挂到AXI-Lite或AXI-Stream上再通过AXI Interconnect做转换。这个结构比全部用AXI4更清晰也更容易做时序收敛。5.4 优化边界条件零长度传输与异常处理AXI4协议不允许零长度传输。AWLEN为零时代表传输长度是1拍至少传一个数据。所以接口设计里一定要对应用层的写请求做保护如果请求长度为零直接在本层丢弃不向总线发起任何事务。很多系统的稳定性问题就出现在这种“看起来没人会发”的异常请求上。另一个需要处理的异常是“未就绪状态下应用层撤销请求”。如果应用层给出的wr_req只在IDLE状态有效那你得想清楚如果IDLE时wr_req到来下一拍进入AW_TRANS状态而wr_req紧接着拉低此时接口应该继续完成本次传输而不是回到IDLE。正确做法是把wr_req锁存到一个内部寄存器直到传输完成再清除。5.5 一个精简的主设备写通道参考框架把上面的讨论收拢到一起一个精简但完整的写通道主设备框架大致是这样的应用层给出写请求、地址、数据、长度接口将请求锁存进写控制寄存器AW通道和W通道并行发起握手数据通路通过内部计数器和WSTRB进行字节选择收到B响应后产生写完成标志。这个框架里最关键的并不是某个信号的赋值而是状态划分和请求锁存机制。我的个人经验是接口越往上层状态可以越粗越往下层越要把“产生握手”和“等待握手”分开这样代码的可读性会高出很多。6. 排错经验用签核清单替代肉眼找bug写完了代码跑到客户系统里崩了这时候最能体现功底。以下是我这些年总结的一套AXI4主设备接口“签核清单”每一条都是我实际踩过坑后沉淀下来的。检查所有通道的valid信号是否在IDLE状态时都拉低。如果上电时AWVALID是高会直接被从设备当成一次非法请求。检查是否在握手的同一拍改变地址。地址必须在握手之前稳定不能边握手边改地址。检查窄传输时的WSTRB是否与地址偏移一致。这一点对寄存器配置的可靠性影响很大。检查突发传输是否跨越4KB。用脚本把所有访问范围的地址算一遍确认不会越界。检查读数据通路FIFO是否有溢出保护。如果RVALID来时FIFO已经满了必须保持RREADY拉低。如果RREADY不能及时拉高要考虑是否是数据消费速率不足。检查复位释放后总线是否处于已知状态。尤其是ARREADY/AWREADY这种从设备给出来的输入信号在复位无效后一个周期内不要采样。检查跨时钟域路径。即使AXI4协议本身是单时钟但你的应用层逻辑很可能在另一个时钟域。建议所有跨时钟域信号都过异步FIFO不要直接用两级同步器去同步多bit总线。这套清单我每次做AXI4接口都会过一遍。相比对着仿真波形一条条翻清单式的自检更高效不会漏查关键点。接口验证和功能验证不一样功能错了容易看出来协议违例往往在上板后才会暴露那时候排查成本就很高了。写在最后接口设计最值钱的其实是边界思维手写一套AXI4主设备接口看起来是把协议规范翻译成RTL实际上锻炼的是“边界思维”。协议规定了合法行为的最小集合你的实现则是在这个集合里找到一个功能和时序的平衡点。我见过很多同行能默写AXI4通道信号但一到互联多个外设、处理outstanding乱序返回时就暴露出对协议理解的浅层化——这不是背规范背出来的是真刀真枪调波形调出来的。做这套接口最让我印象深刻的一次经历是调一个读通道数据错位的问题。波形看起来RVALID、RREADY握手完全正常数据也是连续的但拼出来的结果就是错4字节。查了两三天最后发现是地址递增增量写错了我把32位总线的每个beat的增量写成了4而实际上访问32位数据时增量确实是4但那次访问的是16位数据AXI协议里burst地址递增按传输数据的实际字节数跳不是按总线宽度跳。虽然代码注释里写了“32位总线”但协议里的规则是看AWSIZE不是看总线物理宽度。这个坎过了之后我再也没有在高位宽总线上犯过类似错误。在此也提醒所有正准备动手的同学不要只看你想实现的功能本身多想一想总线上的其他主设备、从设备会怎么响应你的行为AXI4互联的网络里任何一个协议违例都会被放大成系统级故障。把接口做扎实了你的DMA、你的SoC、你的整个系统才能站得住。
返回列表