FPGA实战:AXI总线协议入门与AXI4-Lite从机设计
2026/9/18 5:47:56 网站建设 项目流程

在FPGA开发这条路上,流水灯是起点,状态机是拐点,而AXI总线协议大概就是那道分水岭。不少朋友从零开始学到Part.10,会觉得“我是不是已经入门了”——UART能收发,SPI能读写,数码管能动态显示,按键消抖、温控风扇这种小系统也都能跑起来。但一旦你想把系统做大,比如挂一个DDR控制器、接MIPI摄像头进来、让Zynq的ARM核去配置你的PL逻辑,或者在网上找一个开源IP核接进自己的工程,你马上就会撞到同一个问题:不同模块之间的接口到底怎么定义,谁来定这个规则?

AXI总线协议,就是Xilinx和ARM联合定义好的那套规则。它不是你选不选的问题,而是Xilinx生态里的IP核默认都长这样,你根本绕不开。这篇Part.11,我想从一个近似0基础的视角,用工程应用而不是教科书的方式,把AXI讲成你能上手用、能读懂波形、能自己写从机IP的东西。废话不多说,咱们直接开始。

1. 为什么到了Part.11才开始讲AXI

1.1 前面十篇你已经掌握了什么

从入门到现在,你手里应该已经有几个能用的模块了:串口收发、SPI读写、I2C访问EEPROM、数码管动态显示、按键消抖、PWM温控风扇,可能还有人做过信号发生器、交通灯、出租车计价器这类综合小项目。这些项目有一个共同点:所有逻辑都在单个FPGA内部完成,模块之间用自己定义的信号线连接

这种自建接口方式在小项目里完全没问题,你定义一组data_validdata_busydata_done,主控模块和功能模块之间一拍一握,逻辑清楚、波形好调。但它的缺点也很明显:没有统一标准。换一个IP核,接口对不上,就得写一堆胶水逻辑(glue logic)去转换;遇到跨时钟域,还得自己加两级触发器或者异步FIFO;想和ARM核通信,更是完全没有现成方案。

所以到了Part.11,当你的目标从“点亮一个灯”升级成“做一个通信系统、图像采集系统、SoC系统”时,继续自己定义接口已经不够用了。AXI的意义,是让你从“自己发明轮子”切换到“用行业标准轮子”。

1.2 AXI协议到底帮你解决了什么

AXI(Advanced eXtensible Interface)属于ARM AMBA协议家族,Xilinx从Spartan-6、Virtex-6时代开始就在自家FPGA里全面拥抱它。到了7系列和Zynq,几乎所有高性能IP核的接口都换成了AXI。你只需要记住一句话:在Xilinx的FPGA世界里,AXI就是IP核之间通信的“普通话”

它帮你解决三个实际痛点:

第一,标准化带来的即插即用。一个AXI接口的DMA IP,今天接在Zynq的PS侧能做数据搬运,明天接到MicroBlaze软核上也能用,后天放在纯PL工程里连接自定义逻辑依然没问题。只要接口符合AXI规范,大家都是“普通话”交流。

第二,并行通道提升吞吐。AXI把读地址、读数据、写地址、写数据、写响应拆成独立通道,读写可以同时进行,数据通路还能用突发(burst)方式连续传输。这对DDR读写、图像数据搬运这种高速场景是刚需。你之前写的SPI一次传一个字节,和AXI一次突发传16个字节甚至更多,完全是两种效率量级。

第三,生态红利。Xilinx提供了大量现成AXI IP:AXI UART、AXI GPIO、AXI DMA、AXI VDMA、AXI Ethernet、AXI PCIe、AXI MIPI CSI-2等等。你不会用AXI,这些IP就全是黑盒子,你只能看着别人用。而一旦掌握了AXI读写时序,这些IP在你的工程里就全变成乐高积木了。

2. 先把AXI协议的底子打好:五个通道和一套握手

2.1 五个通道各管什么事

AXI协议看起来复杂,其实拆开之后很规整。它一共五条独立通道,读和写完全分开:

写操作走三条通道:

  • 写地址通道(AW):主机告诉从机“我要往哪个地址写”。
  • 写数据通道(W):主机把要写的数据发出去,可以一拍传一个数据,也可以连续突发传多个数据。
  • 写响应通道(B):从机写完数据之后,回一个响应告诉主机“写好了”或者“出错了”。

读操作走两条通道:

  • 读地址通道(AR):主机告诉从机“我要从哪个地址读”。
  • 读数据通道(R):从机把读到的数据返回给主机,同时携带响应信号。

你注意看,读是主机发个地址,从机返回数据,天然就是一问一答;写则多了一条响应通道,原因是写数据没有“返回值”,主机会担心“你到底有没有把数据收好”,所以从机必须明确回答BVALID。这个设计听起来啰嗦,但在工程上非常重要——总线的可靠性就是从这一步一步握手里保证的。

2.2 VALID/READY握手,三种情况必须吃透

AXI每条通道的传输都靠一对信号完成握手:VALIDREADY

这是一个非常朴素的“你递东西、我接东西”模型:

  • 发送方拉高VALID,表示“我手里的数据/地址已经准备好了,你随时可以拿”。
  • 接收方拉高READY,表示“我已经准备好了,你现在给我就行”。
  • 当VALID和READY同时为高,这个时钟上升沿就完成一次数据传输。

时序上主要有三种情况,实际调波形的时候经常遇到,必须烂熟于心:

第一种,发送方先等接收方。VALID先拉高,READY还没拉高,此时双方都保持状态,直到某个时钟沿READY为高,两个信号同时有效,传输完成。这种最常见,从机没准备好,主机就得等。

第二种,接收方先等发送方。READY先拉高,主机的VALID还没到,那就等,直到VALID也拉高。这里有个坑:协议允许READY在VALID之前拉高,但从机设计时如果不小心把“先READY后等数据”和“数据来时READY刚好变低”搞混,就容易漏掉数据。

第三种,双方同时准备好。VALID和READY在同一拍都拉高,立刻完成。这种效率最高,但前提是发送方不能在见到READY后才开始“打包数据”,必须在VALID拉高时数据就已经在总线上稳定了。

注意:握手的核心规则是,一旦VALID拉高,发送方就不能撤销数据或地址,必须等到握手完成。如果你在调试时发现VALID闪了一下又变低,多半就是设计违规,数据丢了都查不出来。

2.3 突发传输和地址对齐,进阶绕不开的门槛

AXI真正厉害的地方是支持突发(Burst)传输。以写为例,主机在AW通道发一个起始地址,再通过AWLEN告诉从机这次一共传几拍数据(AWLEN + 1就是节拍数),然后W通道连续把数据拍出去。这样一次握手、连续传输,效率远高于逐地址写入。

突发传输有几点需要理解:

地址增量规则。数据宽度是32位(4字节)时,地址通常是每拍+4(按字节寻址);如果总线是64位,地址就是每拍+8。细心的朋友会问,如果一次突发跨越了4KB边界怎么办?协议规定,突发不能跨越4KB边界,这在连接DDR控制器、PCIe这种场景时尤其要小心,地址分配不对会直接卡死。

地址对齐问题。AXI严格要求地址必须对齐到突发长度,比如32位总线、突发4拍,起始地址必须是16字节对齐。经常有新手在这里翻车:想从地址0x1004开始连续读8个32位数据,结果一算地址跨了4KB还是没对齐,干脆改成0x1000对齐地址。实际工程里,内存分配器、DMA描述符都会帮你保证对齐,但你自己写从机IP时,必须处理“主机送来的地址不一定对齐”的情况,要自己判断地址落在哪个寄存器区间。

3. 三种AXI接口,工程上别选错

3.1 三兄弟的定位差异

Xilinx的IP核接口有三种AXI变体,名字很像,功能差很远:

  • AXI4(Full AXI):完整版,支持突发传输,适合高性能读写,比如DDR控制器、PCIe、DMA。性能和复杂度最高。
  • AXI4-Lite(轻量版):不支持突发,一次只读写一个数据,但接口简单、逻辑少、资源省。专门用来做寄存器配置和控制,比如ARM核通过AXI4-Lite去配置你的图像处理模块的参数。
  • AXI4-Stream(流式版):没有地址通道,只有数据流,主机不停地把数据“灌”给从机,适合连续数据流场景。摄像头数据、以太网数据、DMA搬运数据,用的基本都是Stream接口。

一句话总结工程选型:大批量数据搬运走AXI4,控制寄存器配置走AXI4-Lite,连续数据流走AXI4-Stream。

3.2 从机侧要处理的完整事务

如果你要自己写一个AXI从机,最常见的是AXI4-Lite从机,因为它的时序最简单。你一般只用到四个通道的交互:

一次写事务的完整流程是:主机先拉高AWVALID并把地址放到总线上,从机拉高AWREADY接收地址;接着主机在W通道把数据和写选通(WSTRB)放上来,从机拉高WREADY接收数据;从机把数据写入对应寄存器之后,在B通道回一个BRESP(OKAY表示成功),拉高BVALID;主机看到后拉高BREADY,写事务完成。

一次读事务就更直接:主机在AR通道发地址,从机接收后,把对应寄存器的数据放到R通道,同时拉高RVALID;主机看到有效数据后拉高RREADY,读完成。注意,读事务没有单独的“响应”数据,RRESP是和读数据一起带上来的。

从机侧最容易犯错的地方是“把写数据和写地址拆得太开”。AXI协议允许AW和W通道互相独立,主机可以先把地址发过来,等一会儿再发数据;也可以先发数据再补地址;甚至可以两个同时到。所以你的从机内部一定要有独立的“地址寄存”和“数据寄存”,谁先到就先把谁锁存住,等两边的握手都完成了,再统一提交这一次写操作。否则就会出现“数据到了但地址还没到,结果写错了地方”这种诡异问题。

3.3 什么时候用AXI4-Stream

很多做图像处理的同学会遇到AXI4-Stream。它的几个关键信号是:TDATA(数据)、TVALID(本拍数据有效)、TREADY(接收方准备好)、TLAST(这一帧/这一包的最后一次传输)、TKEEP(字节使能)。你把它理解成一个加强版的FIFO读写接口就对了:主机不断往里灌数据,从机不断往外取数据,TLAST告诉接收端“包结束了,赶紧处理边界”。

用AXI4-Stream最受益的场景是:图像Sensor采集的数据经过MIPI解包后,打包成AXI4-Stream流,直接喂给AXI VDMA,VDMA再把数据搬到DDR指定的帧缓存地址。整个过程不用ARM逐个像素搬运,效率极高。这也是为什么那些做FPGA图像处理、ISP去马赛克、MIPI采集的人,最终都要学会AXI-Stream——因为整个图像数据链路就是围绕Stream接口搭起来的。

4. 动手写一个AXI4-Lite从机IP

4.1 工程背景:做一个可配置LED控制器

理论讲再多,不动手等于零。我用一个非常经典的入门例子来讲:做一个AXI4-Lite从机,里面放四个32位寄存器,实现“ARM/主机通过AXI总线控制四路LED的开关和亮度”。这个例子麻雀虽小五脏俱全——涉及写地址通道、写数据通道、读数据通道、写响应通道的全部交互,而且验证起来很直观:上板看LED亮灭就对了。

寄存器映射这样定义:

地址偏移寄存器名功能
0x00CTRL_REGbit0~bit3对应四路LED使能,1为亮
0x04DUTY_REGbit0~bit7为PWM占空比,控制亮度
0x08STATUS_REGbit0为忙标志,当前是否有未完成的配置
0x0C预留实际工程里建议不要用,保持全0

寄存器控制LED,比直接拉IO多了一层“间接性”,但恰恰是这种间接性,让你以后接ARM、接DMA、做复杂SoC的时候不会手忙脚乱——控制面和数据面分离,是大型工程的基本素养。

4.2 核心代码思路与关键片段

完整代码我在开源平台放了一份,这里只贴最核心的写事务和读事务处理逻辑。先看模块接口,本质是把AXI信号剥开,内部转成几个寄存器读写脉冲:

module axil_led_slave #( parameter ADDR_WIDTH = 4 )( input wire aclk, input wire aresetn, // 写地址通道 input wire [ADDR_WIDTH-1:0] awaddr, input wire awvalid, output reg awready, // 写数据通道 input wire [31:0] wdata, input wire [3:0] wstrb, input wire wvalid, output reg wready, // 写响应通道 output reg [1:0] bresp, output reg bvalid, input wire bready, // 读地址通道 input wire [ADDR_WIDTH-1:0] araddr, input wire arvalid, output reg arready, // 读数据通道 output reg [31:0] rdata, output reg [1:0] rresp, output reg rvalid, input wire rready, // 用户逻辑输出 output reg [3:0] led_en, output reg [7:0] led_duty );

写事务我建议用一个小状态机来实现,这比用组合逻辑写一大堆if(awvalid && awready)要清晰得多,也方便加超时保护:

// 写事务:等待AW和W同时完成,然后产生写有效脉冲,回BRESP reg [1:0] wstate; localparam W_IDLE = 2'd0; localparam W_WAIT_DATA = 2'd1; localparam W_RESP = 2'd2; always @(posedge aclk) begin if (!aresetn) begin wstate <= W_IDLE; awready <= 1'b0; wready <= 1'b0; bvalid <= 1'b0; bresp <= 2'b00; end else begin case (wstate) W_IDLE: begin awready <= 1'b1; // 准备好接收地址 if (awvalid) begin awaddr_lat <= awaddr; // 锁存地址 awready <= 1'b0; wstate <= W_WAIT_DATA; end end W_WAIT_DATA: begin wready <= 1'b1; // 准备好接收数据 if (wvalid) begin wdata_lat <= wdata; wstrb_lat <= wstrb; wready <= 1'b0; wstate <= W_RESP; end end W_RESP: begin bresp <= 2'b00; // OKAY bvalid <= 1'b1; if (bready) begin bvalid <= 1'b0; wstate <= W_IDLE; end end default: wstate <= W_IDLE; endcase end end

注意几点:

AWREADY和WREADY分开拉高,是为了兼容“地址先到、数据后到”和“数据先到、地址后到”两种时序。实际AXI主机为了性能,经常把AW和W同时发出,我这个状态机也能接住:当AW先完成,W_WAIT_DATA里等WVALID;如果W先到,不用怕,W_IDLE只锁地址不锁数据,WVALID会在W_WAIT_DATA里被捕获。所以状态机没有丢失数据的风险。

BRESP必须等BREADY才能拉低BVALID。这是最常见的毛病——主机还没接收响应,从机就把BVALID撤了,主机接收到一个不完整的响应,轻则重试,重则总线卡死。

读事务更简单,做一个单拍返回即可:

// 读事务:AR握手完成后,下一拍返回数据 always @(posedge aclk) begin if (!aresetn) begin arready <= 1'b0; rvalid <= 1'b0; rdata <= 32'd0; end else begin arready <= 1'b1; if (arvalid && arready) begin arready <= 1'b0; rvalid <= 1'b1; case (araddr[3:2]) 2'd0: rdata <= ctrl_reg; 2'd1: rdata <= duty_reg; 2'd2: rdata <= status_reg; default: rdata <= 32'd0; endcase end else if (rvalid && rready) begin rvalid <= 1'b0; end end end

这里用araddr[3:2]做寄存器选择,是因为地址是字节寻址,每个寄存器占4字节,所以bit[1:0]永远为0,直接用高位译码最简单。实际工程里寄存器多了,建议用casez或者参数化的地址译码表来管理,别硬编码一大串case。

4.3 在Vivado里封装和验证

代码写好后,有两件事必须做:封装成IP,然后仿真验证。

Vivado里封装自定义IP有两条路。一条是“全手工”:Tools - Create and Package New IP,选“Create AXI4 Peripheral”,向导会生成一个完整可用的AXI4-Lite从机模板,你只需要把模板里的用户逻辑段替换成自己的寄存器读写代码。这对新手非常友好,推荐优先走这条路。另一条是“已有RTL封装”:如果你代码已经写好,选“Package your current project”,把顶层模块映射成AXI接口。这个灵活,但需要自己保证信号命名和时序正确。

我个人的建议是,第一次学AXI时,一定要走一遍向导,哪怕你最后不用它生成的代码,也要看一遍它生成的模板是什么样的。Vivado自动生成的AXI4-Lite从机模板里包含了完整的握手保护逻辑,比如“AW和W都完成后才写寄存器”“BRESP不会过早拉高”等,你对照着自己写的代码找差距,比闷头看协议文档高效得多。

验证环节,Xilinx的仿真环境里自带AXI Verification IP(AXI VIP),可以在Behavioral Simulation里当主机读写你的从机。新建一个仿真工程,例化AXI VIP,把它设成Master模式,然后让它对你的从机做几笔读写,看波形里的握手是否符合预期。重点看三件事:

  • 写事务:AW、W、B三个通道是否都完成握手,写入的寄存器值是否和你发的一致。
  • 读事务:AR握手后RVALID是否正常拉高,返回数据是否正确。
  • 边界情况:如果你连续发起两笔写事务,第二笔的AW地址会不会被当成第一笔的数据?读寄存器时地址换着花样的连续读,数据是否错位?

这类测试最好写成自校验的testbench,打印“Read back mismatch at offset 0x04”之类的信息,比自己肉眼看波形靠谱得多。

5. 真实工程中那些坑和排查方法

5.1 常见问题速查

现象可能原因排查方法
写寄存器后读回来全是0WSTRB写选通没做,寄存器写条件没判断WSTRB检查WSTRB,非全F时按字节写,至少bit0要为1
AXI事务永远不完成,仿真挂死从机的AWREADY或WREADY永远不拉高,或主机在等BRESP检查状态机是否卡在某个状态;确认BRESP/BVALID逻辑
读数据比预期晚一拍ARREADY握手和RVALID生成逻辑有时序差确认RVALID是“AR握手完成后的下一拍”拉高,而不是组合逻辑直接拉高
ARM写寄存器后软件卡死BRESP没返回或BVALID一直不拉低确认BVALID等BREADY后再撤;确认bresp设置成OKAY
多次读写后数据错乱从机地址锁存和状态机复用冲突确认每次事务完成后状态机回到IDLE,地址锁存没有被覆盖

5.2 跨时钟域和复位处理

AXI的主机和从机理论上可以跑在不同时钟域,但实际工程里,同一段总线的读写两侧最好都是同一个时钟。如果你一定要做跨时钟域,正规做法是在中间加一个AXI CDC IP,或者用异步FIFO把数据流隔开。自己做跨时钟域的AXI桥非常容易引入亚稳态,别省这个事。

复位也很讲究。AXI协议要求aresetn是低电平有效的异步复位,且复位释放必须同步。很多人在仿真里直接给aresetn = 1'b1结束复位,看起来没什么问题,但上板之后因为复位释放不同步,偶尔会出现第一次写事务丢失。稳妥做法是加一个复位同步器,用两级触发器把外部复位同步到ACLK上再释放。

另外,AXI总线上的信号在复位期间应该处于“无效但确定”的状态,比如VALID必须为0,READY可以是0也可以是1(但建议0),保证复位结束后第一个事务能正常启动。

5.3 调试AXI沿用的三板斧

调AXI接口和调一个小模块不同,一旦出问题,整条链路都在动,波形密密麻麻,看得人头皮发麻。我自己的习惯是三步走。

第一,分通道隔离。先在testbench里只测写事务,把AW、W、B三条通道的波形单独拉出来,把读通道的所有信号强制成空闲值。写通了再接读通道,别一起调,否则出错都不知道在哪个通道里。

第二,用打印代替看波形。仿真testbench里每完成一笔事务就打印一行,比如Write 0x00 <- 0x00000001 OKRead 0x04 -> 0x0000003C。波形只在打印结果对不上时才去翻,否则光看波形真能看瞎。

第三,加超时保护。testbench里设置一个计数器,比如10万个时钟周期内如果事务还没完成,就报错并$finish。这样即使你的从机状态机卡死了,仿真也不会永远挂在那,能快速定位是哪个事务卡住。

上板调试时,最快的定位手段是用Vivado的ILA(Integrated Logic Analyzer)去抓AXI接口信号。把S_AXI_AWVALID、S_AXI_WVALID、S_AXI_BVALID、S_AXI_ARVALID、S_AXI_RVALID这几个关键信号抽出来,触发条件设为“任意一个VALID拉高”,抓到波形后用光标核对握手是否符合预期。很多人在这一步才发现,自己的逻辑在仿真里一切正常,上板却因为时序收敛问题偶发失败——这时ILA就是你的救命稻草。

6. 从AXI4-Lite到AXI4/Stream的进阶路线

会写AXI4-Lite从机,只算摸到门框。真正往FPGA工程师方向走,下一步就是碰AXI4和AXI4-Stream。

如果你的目标岗位是图像处理、视频采集方向,优先学AXI4-Stream,因为整个视频链路都是Stream:摄像头IP输出Stream,VDMA输入输出Stream,图像处理算子输入输出Stream,甚至HDMI显示控制器也是Stream输入。你会写一个简单的axis_data_fifo之后,再看Xilinx那些图像处理的参考设计,思路会通很多。

如果你的目标是SoC方向、Zynq开发,那就把AXI4-Lite精细化,再学AXI4。Zynq的PS和PL之间,最常见的接口就是AXI GPIO、AXI UART、AXI DMA这些IP,全部是AXI接口。会写AXI-Lite寄存器从机,用AXI DMA搬数据,再看Linux下的UIO或FPGA Manager驱动,PS-PL之间的开发流程就贯通了。很多人面试时被问“AXI和AXI-Lite有什么区别”“写事务和读事务的过程分别是什么”,其实都是在考察你有没有真正写过、调过AXI接口,光背协议是真扛不住深挖的。

另外,有时间可以去看几个Xilinx官方和开源社区的项目。比如开源的RISC-V SoC,里面几乎全是AXI接口——指令总线、数据总线、外设总线都是AXI变体;比如PCIe相关参考设计,DMA引擎内部就是AXI4和AXI4-Stream的混合体;再比如一些MIPI CSI-2采集方案,Sensor接口转AXI4-Stream是标配。看这些项目不要求全懂,但你会逐渐形成一个感觉:AXI不是某个IP的事,是整个数字系统里的“血管系统”。把血管长什么样搞清楚,后面接什么都顺。

最后说个我自己的习惯:每当拿到一个新IP,不管它多大,我第一件事永远是打开它的产品手册,翻到AXI接口章节,把它的寄存器映射表从头到尾看一遍——哪些寄存器是配置用的,哪些是状态用的,哪些发起操作会自动清零,读和写分别有什么副作用。这套流程走下来,你对一个IP的理解会比看十篇教程都深刻。AXI学到后面你会发现,难点从来不是那几条握手信号,而是“把总线协议翻译成你真正想干的那件事”。这个翻译能力,只能靠一次一次写时序、调波形、查文档慢慢喂出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询