软件版本:VIVADO2017.4 操作系统:WIN10 64bit 硬件平台:适用米联客 ZYNQ系列开发板 米联客(MSXBO)论坛:www.osrc.cn答疑解惑专栏开通,欢迎大家给我提问!! 15.1 概述ZYNQ拥有ARM+FPGA这个神奇的架构,ARM和FPGA通过AXI4总线进行通信。本章对AXI总线源码进行分析,首先对总线和接口以及协议进行区别,其次通过分析AXI4-Lite,AXI4-Stream,AXI4总线的从机代码,进一步认识AXI协议,那么在后面学习AXI接口的IP时就不会有恐惧的心理。 这里需要特别说明一下AXI总线和AXI接口的关系。在ZYNQ中,支持AXI4-Lite,AXI4和AXI4-Stream三种总线协议,需要注意的是PS与PL之间的接口(AXI-GP接口,AXI-HP接口以及AXI-ACP接口)只支持AXI-Lite和AXI协议这两种总线协议。也就是说PL这边的AXI-Stream的接口是不能直接与PS对接的,需要经过AXI4或者AXI4-Lite的转换。比如后面将用到的VDMA IP ,它就实现了在PL内部AXI4到AXI-Stream的转换,VDMA利用的接口就是AXI-HP接口。 15.2 AXI总线与ZYNQ的关系AXI(Advanced eXtensible Interface)本是由ARM公司提出的一种总线协议,Xilinx从6系列的FPGA开始对AXI总线提供支持,此时AXI已经发展到了AXI4这个版本,所以当你用到Xilinx软件的时候看到的都是“AIX4”的IP,如Vivado打包一个AXI IP的时候,看到的都是Create a new AXI4 peripheral。 到了ZYNQ就更不必说了,AXI总线更是应用广泛,双击查看ZYNQ的IP核的内部配置,随处可见AXI的身影。 15.3 AXI总线和AXI接口以及AXI协议总线、接口和协议,这三个词经常被联系在一起,但是三者有区别。 总线是一组传输通道,是各种逻辑器件构成的传输数据的通道,一般由数据线、地址线、控制线等构成。 接口是一种连接标准,又被称为物理接口。 协议就是传输数据的规则。 15.3.1 AXI总线概述在ZYNQ中有支持三种AXI总线,拥有三种AXI接口,当然用的都是AXI协议。其中三种AXI总线分别为: AXI4:(For high-performance memory-mapped requirements.)主要面向高性能地址映射通信的需求,是面向地址映射的接口,允许最大256轮的数据突发传输; AXI4-Lite:(For simple, low-throughput memory-mapped communication )是一个轻量级的地址映射单次传输接口,占用很少的逻辑单元。 AXI4-Stream:(For high-speed streaming data.)面向高速流数据传输;去掉了地址项,允许无限制的数据突发传输规模。 AXI4总线和AXI4-Lite总线具有相同的组成部分: (1)读地址通道,包含ARVALID, ARADDR, ARREADY信号; (2)读数据通道,包含RVALID, RDATA, RREADY, RRESP信号; (3)写地址通道,包含AWVALID,AWADDR, AWREADY信号; (4)写数据通道,包含WVALID, WDATA,WSTRB, WREADY信号; (5)写应答通道,包含BVALID, BRESP, BREADY信号; (6)系统通道,包含:ACLK,ARESETN信号。 AXI4总线和AXI4-Lite总线的信号也有他的命名特点: 读地址信号都是以AR开头(A:address;R:read) 写地址信号都是以AW开头(A:address;W:write) 读数据信号都是以R开头(R:read) 写数据信号都是以W开头(W:write) 应答型号都是以B开头(B:back(answer back)) 了解到总线的组成部分以及命名特点,那么在后续的实验中您将逐渐看到他们的身影。每个信号的作用暂停不表,放在后面一一介绍。 AXI4-Stream总线的组成有: (1)ACLK信号:总线时钟,上升沿有效; (2)ARESETN信号:总线复位,低电平有效 (3)TREADY信号:从机告诉主机做好传输准备; (4)TDATA信号:数据,可选宽度32,64,128,256bit (5)TSTRB信号:每一bit对应TDATA的一个有效字节,宽度为TDATA/8 (6)TLAST信号:主机告诉从机该次传输为突发传输的结尾; (7)TVALID信号:主机告诉从机数据本次传输有效; (8)TUSER信号 :用户定义信号,宽度为128bit。 对于AXI4-Stream总线命名而言,除了总线时钟和总线复位,其他的信号线都是以T字母开头,后面跟上一个有意义的单词,看清这一点后,能帮助读者记忆每个信号线的意义。如TVALID = T+单词Valid(有效),那么读者就应该立刻反应该信号的作用。每个信号的具体作用,在后面分析源码时再做分析 |
// Ports of Axi Slave Bus Interface S00_AXI input wire s00_axi_aclk, input wire s00_axi_aresetn, input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_awaddr, input wire [2 : 0] s00_axi_awprot, input wire s00_axi_awvalid, output wire s00_axi_awready, input wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_wdata, input wire [(C_S00_AXI_DATA_WIDTH/8)-1 : 0] s00_axi_wstrb, input wire s00_axi_wvalid, output wire s00_axi_wready, output wire [1 : 0] s00_axi_bresp, output wire s00_axi_bvalid, input wire s00_axi_bready, input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_araddr, input wire [2 : 0] s00_axi_arprot, input wire s00_axi_arvalid, output wire s00_axi_arready, output wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_rdata, output wire [1 : 0] s00_axi_rresp, output wire s00_axi_rvalid, input wire s00_axi_rready |
没错笔者曾在《AXI总线概述》这节中提到了他们,这次通过源码分析再次隆重介绍它们。


Vivado为我们生成的AXI-Lite的操作源码,是一个模板,我们只需要读懂它,然后稍加修改,就可以在实际工程使用。
我们先来看一段源码中与WDATA相关的代码:
always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin slv_reg0 <= 0; slv_reg1 <= 0; slv_reg2 <= 0; slv_reg3 <= 0; end else begin if (slv_reg_wren) begin case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin // Respective byte enables are asserted as per write strobes // Slave register 0 slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end 2'h1: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin // Respective byte enables are asserted as per write strobes // Slave register 1 slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end 2'h2: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin // Respective byte enables are asserted as per write strobes // Slave register 2 slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end 2'h3: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin // Respective byte enables are asserted as per write strobes // Slave register 3 slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end default : begin slv_reg0 <= slv_reg0; slv_reg1 <= slv_reg1; slv_reg2 <= slv_reg2; slv_reg3 <= slv_reg3; end endcase end end end |
这段程序的作用:
当PS向AXI4-Lite总线写数据时,将从总线上传输过来的数据存储到寄存器slv_reg。而slv_reg寄存器有0~3共4个。至于赋值给哪一个由
axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]决定,根据宏定义其实就是由axi_awaddr[3:2] (写地址中不仅包含地址,而且包含了控制位,这里的[3:2]就是控制位)决定赋值给哪个slv_reg。
PS调用写函数时,如果不做地址偏移的话,axi_awaddr[3:2]的值默认是为0的,举个例子,如果我们自定义的IP的地址被映射为0x43C00000,那么我们Xil_Out32(0x43C00000,Value)写的就是slv_reg0的值。如果地址偏移4位,如Xil_Out32(0x43C00000 + 4,Value) 写的就是slv_reg1的值,依次类推。
本例分析时只关注slv_reg0(其他几个寄存器结构相同,分析方法相同):
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end |
其中,C_S_AXI_DATA_WIDTH的宏定义的值为32,也就是数据位宽;S_AXI_WSTRB是写选通信号,S_AXI_WDATA就是写数据信号。
存在于for循环中的最关键的一句:
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
当byte_index = 0的时候这句话就等价于:
slv_reg0[7:0] <= S_AXI_WDATA[7:0];
当byte_index = 1的时候这句话就等价于:
slv_reg0[15:8] <= S_AXI_WDATA[15:8];
当byte_index = 2的时候这句话就等价于:
slv_reg0[23:16] <= S_AXI_WDATA[23:16];
当byte_index = 3的时候这句话就等价于:
slv_reg0[31:24] <= S_AXI_WDATA[31:24];
也就是说,只有当写选通信号为1时,它所对应S_AXI_WDATA的字节才会被读取。
读懂了这段话之后,我们就知道了,如果我们想得到PS写到总线上的数据,我们只需要读取slv_reg0的值即可。
那么如果,我们想写数据到总线让PS读取该数据,我们该怎么做呢?我们继续来看有关RADTA读数据代码:
// Output register or memory read data always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_rdata <= 0; end else begin // When there is a valid read address (S_AXI_ARVALID) with // acceptance of read address by the slave (axi_arready), // output the read dada if (slv_reg_rden) begin axi_rdata <= reg_data_out; // register read data end end end |
观察可知,当PS读取数据时,程序会把reg_data_out复制给axi_rdata(RADTA读数据)。我们继续追踪reg_data_out:
always @(*) begin // Address decoding for reading registers case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0 : reg_data_out <= slv_reg0; 2'h1 : reg_data_out <= slv_reg1; 2'h2 : reg_data_out <= slv_reg2; 2'h3 : reg_data_out <= slv_reg3; default : reg_data_out <= 0; endcase end |
和前面分析的一样此时通过判断axi_awaddr[3:2]的值来判断将那个值给reg_data_out上,同样当PS调用读取函数时,这里axi_awaddr[3:2]默认是0,所以我们只需要把slv_reg0替换成我们自己数据,就可以让PS通过总线读到我们提供的数据。
这里可能有的读者会问了,slv_reg0不是总线写过来的数据吗?因为笔者说过这个程序是Vivado为我们提供的例子,它这么做无非是想验证写出去的值和读进入的值相等。但是他怎么写确实会对初看代码的人造成困扰。
最后笔者提出一个问题,为什么写通道要比读通道多了一列应答通道,这是为什么呢?
首先,你要知道这个应答信号是干什么用的?

写应答,主要是回复主机你这个写过程是没有问题的,那读为什么不需要这个过程呢?

这时因为主机在读取数据时,从机可以直接通过读数据通道给主机反馈信息,因此就没有必要再来开辟一个单独的应答通道了。
小结:
如果我们想读AXI4_Lite总线上的数据时,只需关注slv_reg的数据,我们可自行添加一段代码,如:
reg [11:0]rlcd_rgb; always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin rlcd_rgb <= 12'd0; end else begin rlcd_rgb <= slv_reg0[11:0]; end end assign lcd_rgb = rlcd_rgb; |
如果我们想对AXI4_Lite信号写数据时,我们只需修改对reg_data_out的赋值,如:
//写总线测试修改!!!!!!!!! wire[31:0]wlcd_xy;// = {10'd0,lcd_xy}; assign wlcd_xy = {10'd0,lcd_xy}; assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid; always @(*) begin // Address decoding for reading registers case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) 2'h0 : reg_data_out <= wlcd_xy;//slv_reg0; 2'h1 : reg_data_out <= slv_reg1; 2'h2 : reg_data_out <= slv_reg2; 2'h3 : reg_data_out <= slv_reg3; default : reg_data_out <= 0; endcase end |
最后强调下如果我们自定义的IP的地址被映射为0x43C00000,那么我们Xil_Out32(0x43C00000,Value)写的就是slv_reg0的值。如果地址偏移4位,如Xil_Out32(0x43C00000 + 4,Value) 写的就是slv_reg1的值,依次类推。
目前这里只有4个寄存器,那是因为之前选择的是4个,其实我们可以定义的更多:

在ps的头文件里可以看到我们自定义的IP的地址是有个范围的
#define XPAR_MYIPFREQUENCY_0_S00_AXI_BASEADDR 0x43C00000
#define XPAR_MYIPFREQUENCY_0_S00_AXI_HIGHADDR 0x43C0FFFF
理论上只要基地址 + 偏移量不要超过HIGHADDR即可。
分析完源码后,我们不做修改,直接对IP进行封装。
(IP生成及修改的详细步骤请参考《自定义IP User_GPIO实验》,这里只做原理性讲解)
Step1:新建一个工程,添加之前自定义的IP。
Step2:添加Debug 信号,并自动连线。连接好后的原理图如下所示。

Step3:点击Generate the Output Products,自动生成顶层文件。
Setp4:综合映射,生成Bit文件。导出硬件,加载SDK。
Step5:创建一个新的空工程。
Step6:将提供例程中SDK工程的main.c源文件复制,并粘贴到新建SDK工程。
Step7:右击工程,选择Debug as ->Debug configurations。
Step8:选中system Debugger,双击创建一个系统调试,点击Apply,点击Debug。
Step9:回到VIVADO界面,单击Open Hardware Manager àOpen Target->Auto Connect

Step10:加载完成后的界面

Step7:Trigger Setup,点击“+”,选择AXI_WVALID,双击添加。设置Radix为B,触发条件Value为1。

Step8:设置触发位置为512

Step9:单击运行按钮,启动触发。

Step10:进入等待触发状态

Step11:单击SDK中的运行按钮后,VIVADO 中 HW_ILA2 窗口采集到波形输出,可以看到AXI总线的工作时序。
SDK中mian.c程序功能是向AXI4总线写入1~4,再从AXI4总线读数据,从上面对未修改直接封装的IP分析,可以,读出的数据应等于写入的数据。
从波形图可以看出,写入的数据是1、2、3、4,对应基地址的偏移地址是0、4、8、12。

Step12:在SDK中打开串口,可以看到串口在打印从AXI4总线读出的数据,分别是1、2、3、4。写入数据与读出数据一致。这样我们完成了对AXI4-lite总线接口的IP的读写测试。

网站内容版权所有归 米联客品牌所有,如果网站内容有侵权行为请联系客服热线0519-80699907,本站点会第一时间核对排查并处理 GMT+8, 2025-12-7 07:37