本文转载自:孤独的单刀的CSDN博客
写在前面
在AXIS篇中,我们打包了2个AXI4-Stream接口的IP(一主一从)(带你快速入门AXI4总线--AXI4-Stream篇(2)----XILINX AXI4-Stream接口IP源码仿真分析),对着两个IP进行了仿真分析,同时也学习了一番XILINX提供的代码。在这篇文章中,我们照葫芦画瓢,也打包2个AXI4-Lite接口的IP(一主一从),来对其的仿真和原始代码学习一番。限于篇幅,将分2篇文章写完,本文写AXI4-Lite-slave接口。
1、调用IP
首先新建一个工程,然后点击Tools-----create and package new ip
点击Next
选择选项4,点击Next,各选项含义:
1---将当前工程打包为IP核
2----将当前工程的模块设计打包为IP核
3----将一个特定的文件夹目录打包为IP核
4----创建一个带AXI接口的IP核
填写IP信息(基本不修改,只改下名称方便后续管理),点击Next
选择Lite接口,接口类型选择从机slave,数据位宽32位,寄存器个数选择4个(这个寄存器主要用在SOC或者ZYNQ上,这里不需要过多了解),点击Next
精彩的来了,这里选择第3个,使用AXI4 VIP来验证IP,然后点击Next。(AXI4 VIP是XILINX的一个IP核,该IP核可以提供多种连接方式来对AXI接口进行验证,用起来很是贴心方便,我们后面会写相关文章,还请期待。)
这个时候就自动生成了如下界面,甚至还帮你打开了仿真界面。
我们先不急着仿真,先看看整个工程的结构再说。双击下图中的BD文件,
此时弹出结构框图如下,
整个工程由两部分组合:1、我们打包的IP,该IP的接口是AIX4-Lite-slave;2、AXI Verification IP,这是一个AXI的验证IP,提供多种验证方式,功能很强大,双击这个IP,看看它的内置定制信息:
可以看到,它可选选择接口模式来实现主机或从机或直通功能;可选协议类型,地址位宽,数据位宽等。我们这里不动它,直接cancel
接着点击按下图中操作,右击BD文件,选择generate output products来生成源码:
在此路径下生成了源码(第2个文件)文件,和顶层例化文件(第1个文件,这个不用管)
2、Slave接口的源码分析
打开上节生成的源码(注意:我删除了源码的注释,不然太长了。再优化了一下格式,主要是对齐。顺便再吐槽一下CSDN不能折叠代码):
代码较长,我将其分成NO.1-12共12个部分来进行讲解。只讲大体思路,其他内容请看代码注释。
NO.1:
`timescale 1 ns / 1 ps //NO.1--------------------------------输入输出端口------------------------------------------- module myip_axi_lite_slave_v1_0_S00_AXI # ( parameter integer C_S_AXI_DATA_WIDTH = 32, parameter integer C_S_AXI_ADDR_WIDTH = 4 ) ( //全局信号 input wire S_AXI_ACLK, input wire S_AXI_ARESETN, //写地址通道 input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_AWADDR, input wire S_AXI_AWVALID, output wire S_AXI_AWREADY, //写数据通道 input wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_WDATA, input wire [(C_S_AXI_DATA_WIDTH/8)-1 : 0] S_AXI_WSTRB, input wire S_AXI_WVALID, output wire S_AXI_WREADY, //写响应通道 output wire [1 : 0] S_AXI_BRESP, output wire S_AXI_BVALID, input wire S_AXI_BREADY, //读地址通道 input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_ARADDR, input wire [2 : 0] S_AXI_ARPROT, input wire S_AXI_ARVALID, output wire S_AXI_ARREADY, //读数据通道 output wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_RDATA, output wire [1 : 0] S_AXI_RRESP, output wire S_AXI_RVALID, input wire S_AXI_RREADY );
这部分主要是模块端口及参数例化。
参数例化:数据位宽32位;地址位宽4位
模块端口:AXI4-Lite协议的端口。不记得可以看这里:带你快速入门AXI4总线--AXI4-Lite篇(1)----AXI4-Lite总线
NO.2:
//NO.2--------------------------------寄存器定义------------------------------------------------------------ //AXI4-Lite接口相关 reg [C_S_AXI_ADDR_WIDTH-1 : 0] axi_awaddr; reg axi_awready; reg axi_wready; reg [1 : 0] axi_bresp; reg axi_bvalid; reg [C_S_AXI_ADDR_WIDTH-1 : 0] axi_araddr; reg axi_arready; reg [C_S_AXI_DATA_WIDTH-1 : 0] axi_rdata; reg [1 : 0] axi_rresp; reg axi_rvalid; //slave寄存器相关 localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1; localparam integer OPT_MEM_ADDR_BITS = 1; reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0; reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1; reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2; reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3; wire slv_reg_rden; wire slv_reg_wren; reg [C_S_AXI_DATA_WIDTH-1:0] reg_data_out; integer byte_index; reg aw_en;
这部分主要是定义一些寄存器。
其中一些寄存器是从机需要输出给主机的信号,因为在always块中操作,所以需要定义成reg类型。
还有一些是对从机模块自身的slave寄存器操作的一些寄存器。
NO.3:
//NO.3--------------------------------端口赋值定义------------------------------------------------------------ //通过赋值方式避免直接操作输出端口 assign S_AXI_AWREADY = axi_awready; assign S_AXI_WREADY = axi_wready; assign S_AXI_BRESP = axi_bresp; assign S_AXI_BVALID = axi_bvalid; assign S_AXI_ARREADY = axi_arready; assign S_AXI_RDATA = axi_rdata; assign S_AXI_RRESP = axi_rresp; assign S_AXI_RVALID = axi_rvalid;
这部分主要是将定义好的输出寄存器的值赋值给输出端口,避免直接操作输出端口。
NO.4:
//NO.4--------------------------------生成写地址准备信号axi_awready------------------------------------------- always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_awready <= 1'b0; //从机没有准备好接收写地址 aw_en <= 1'b1; //可以执行写事务 end else begin if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en) //主机准备好了写地址和写数据、系统可以执行写事务、且从机没有准备好 begin axi_awready <= 1'b1; //从机准备好接收写地址 aw_en <= 1'b0; //正在执行写事务 end else if (S_AXI_BREADY && axi_bvalid) //一旦写事务被响应,代表一次写操作结束 begin aw_en <= 1'b1; //响应信号结束可以执行写事务 axi_awready <= 1'b0; //从机没有准备好接收写地址 end else begin axi_awready <= 1'b0; //一般情况下从机处于非准备状态 end end end
这部分对两个信号赋值:
可以执行写事务标志信号aw_en(为高表示可以执行一次写事务);
写地址通道的从机准备信号axi_awready(为高表示从机可以被写入地址)
NO.5:
//NO.5----------------------------------锁存写地址信号axi_awaddr------------------------------------------- always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_awaddr <= 0; end else begin //主机准备好了写地址和写数据、系统可以执行写事务、且从机没有准备好 if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en) begin axi_awaddr <= S_AXI_AWADDR; //下一个周期从机准备好接收数据,同时将地址寄存方便后面解析 end end end
这部分主要是将要写入的地址锁存,方便后面对从机的slave寄存器进行操作。
NO.6:
//NO.6----------------------------------生成写数据准备信号axi_wready------------------------------------------- always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_wready <= 1'b0; end else begin //没有准备好写数据、写数据有效、写地址有效、可以执行写事务 if (~axi_wready && S_AXI_WVALID && S_AXI_AWVALID && aw_en ) begin axi_wready <= 1'b1; //从机准备好写数据 end else begin axi_wready <= 1'b0; //一般情况下从机处于非准备状态 end end end
这部分对写数据通道从机准备信号axi_wready赋值。
NO.7:
//NO.7----------------------------------将AXI总线上的数据写入从机中的寄存器------------------------------------------- assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID; //当准备写入数据时拉高寄存器写使能,与AXI总线写数据对齐 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 //判断当前BYTE是否有效(即掩码功能是否生效) 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 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 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 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
这部分主要是根据要写入的地址,来讲总线上要写入从机的主句给写入到从机的slave寄存器。同时需要注意S_AXI_WSTRB这个写选通,当其为高时,代表总线上对应的BYTE是有效的,反之无效。
NO.8:
//NO.8----------------------------------生成写响应信号axi_bvalid、响应值axi_bresp------------------------------------------- always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_bvalid <= 0; axi_bresp <= 2'b0; end else begin //在写入数据,且从机没有准备回应响应有效信号 if (axi_awready && S_AXI_AWVALID && ~axi_bvalid && axi_wready && S_AXI_WVALID) begin axi_bvalid <= 1'b1; //从机拉高响应有效信号,等待主机回复准备接收响应信号S_AXI_BREADY axi_bresp <= 2'b0; //访问成功:'OKAY' response end //不支持其他响应判断 else begin if (S_AXI_BREADY && axi_bvalid) //握手成功 begin axi_bvalid <= 1'b0; //拉低axi_bvalid(仅需维持一个时钟周期) end end end end
这部分对两个信号赋值:
写响应通道的从机准备好响应信号axi_bvalid,其拉高表示,从机准备好完成一次写响应。
从机回复的写响应值axi_bresp,其值固定为0,代表响应成功(暂不支持其他值,如响应不成功等)
NO.9:
//NO.9----------------------------------寄存读取地址S_AXI_ARADDR,生成读数据准备信号------------------------------------------- always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_arready <= 1'b0; axi_araddr <= 32'b0; end else begin if (~axi_arready && S_AXI_ARVALID) //主机准备好发送读地址,从机没准备接收 begin axi_arready <= 1'b1; //从机准备读地址 axi_araddr <= S_AXI_ARADDR; //将要读取的地址寄存 end else begin axi_arready <= 1'b0; //其他情况默认从机没有准备接收读地址 end end end
这部分对两个信号赋值:
读地址通道的从机准备好信号axi_arready,其拉高表示,从机准备接收读取地址
将读地址通道的读地址S_AXI_ARADDR寄存给axi_araddr,方便后续根据读取地址找到对应的slave寄存器拿出要读到值
NO.10:
//NO.10----------------------------------生成读响应信号axi_rvalid、读响应值axi_rresp------------------------------------------- always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_rvalid <= 0; axi_rresp <= 0; end else begin if (axi_arready && S_AXI_ARVALID && ~axi_rvalid) //读地址通道握手成功,从机读出的数据无效 begin axi_rvalid <= 1'b1; //从机读出的数据有效 axi_rresp <= 2'b0; // 'OKAY' response end else if (axi_rvalid && S_AXI_RREADY) //读数据通道握手完成 begin axi_rvalid <= 1'b0; //axi_rvalid(仅需维持一个时钟周期) end end end
这部分对两个信号赋值:
读数据通道的从机准备好信号axi_rvalid,其拉高表示,从机准备好完成一读操作。
从机回复的读响应值axi_rresp,其值固定为0,代表响应成功(暂不支持其他值,如响应不成功等)
NO.11:
//NO.11----------------------------------生成寄存器读使能信号、将寄存器的值取出------------------------------------------- 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 <= slv_reg0; //将寄存器的值赋值给reg_data_out 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
这部分对两个信号赋值:
寄存器读取使能信号,该信号与读事务对齐,即进行一次读操作的时候将slave寄存器的值输出到总线上
读取数据中间变量reg_data_out,根据读取的地址将对应slave寄存器的值赋给reg_data_out
NO.12:
//NO.12----------------------------------生成总线上的读取数据axi_rdata------------------------------------------- always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin axi_rdata <= 0; end else begin if (slv_reg_rden) //读取寄存器使能有效 begin axi_rdata <= reg_data_out; //将寄存器的值输出到读数据通道,放到总线上,让主机读走 end end end endmodule
这部分对信号赋值:
从机输出的被读取数据axi_rdata,将中间变量reg_data_out的值赋给axi_rdata。采用中间变量的方法可以是输出数据的时序对齐。
3、仿真波形
接下来使用Vivado自带的仿真器来进行仿真,观看仿真结果
3.1、AXI4-Lite总线的仿真波形
我们先把自动生成的仿真信号删除,添加如下的波形信号:
仿真结果如下:
可以看到仿真结果是用这个彩条+字符的形式表示的,非常清晰。这就是添加了AXI VIP IP的效果。
在AXI4-Lite总线上共发生了8个事务:先是连续的4个写事务,接着4个读事务。下面的五个通道分别示意了此时通道内执行的握手操作,将鼠标放在其中任意一处上,会出现如下信息(顺序1、地址0等):
在左键点击,会显示具体的事务流程如下:
从上图的箭头我们可以直到一次写事务的流程:写地址----写数据----写响应。再看看读事务的流程:
可以看到读事务的流程:读地址----读数据。
看完了AXI4-Lite总线的仿真波形,我们再看下上面具体解析代码(可以理解为底层驱动)的仿真波形。按如下方法添加:
将信号按通道或用途做好分类,仿真结果如下:
信号较多,我们先解析写事务如下:
上图中,一共进行了4次写入操作,写入的地址分别为0、4、8、12,分别对应从机内4个Slave寄存器的地址。写入的数据分别为1-4。握手过程就不谈了。看一下数据是怎么被写到
Slave寄存器的,如下:
从上图可以看到:
slave寄存器写入使能信号slv_reg_wren是与总线上的写事务对齐的,这样就可以直接将总线要写入的数据写入slave寄存器;
根据写入的地址,将要写入的数据分别写入对应的slave寄存器,具体到上图,分别往4个寄存器写入数据1-4
再看下读事务的时序图:
完成握手后,依次从地址(0、4、8、12)中读出了数据1-4,与之前写入的一致(数据被存在从机的slave寄存器中)。接着看一下数据是从Slave寄存器中读出来的:
从上图可以看到:
slave寄存器读取使能信号slv_reg_rden是与总线上的读事务对齐的,这样就可以直接将slave寄存器的值读出赋值给总线,考虑到读取寄存器的值存在一个时钟周期的延迟,所以采用了临时变量reg_data_out[31:0]来打一拍,将时序对齐;
分别从4个slave寄存器中读取数据1-4。
4、其他
可以看到其实AXI4-Lite总线的使用还是相对比较简单的,只要设计好各个通道的握手时序,以及读写的时序关系就好了。下一篇文章我们再继续分析AXI4-Lite总线的Master接口的代码。