带你快速入门AXI4总线--AXI4-Lite篇(2):XILINX AXI4-Lite接口IP源码仿真分析(Slave接口)

本文转载自:孤独的单刀的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

1.jpg

点击Next 

2.png

  选择选项4,点击Next,各选项含义:

  • 1---将当前工程打包为IP核

  • 2----将当前工程的模块设计打包为IP核

  • 3----将一个特定的文件夹目录打包为IP核

  • 4----创建一个带AXI接口的IP核

3.png

填写IP信息(基本不修改,只改下名称方便后续管理),点击Next

4.jpg

 选择Lite接口,接口类型选择从机slave,数据位宽32位,寄存器个数选择4个(这个寄存器主要用在SOC或者ZYNQ上,这里不需要过多了解),点击Next

5.jpg

   精彩的来了,这里选择第3个,使用AXI4 VIP来验证IP,然后点击Next。(AXI4 VIP是XILINX的一个IP核,该IP核可以提供多种连接方式来对AXI接口进行验证,用起来很是贴心方便,我们后面会写相关文章,还请期待。) 

6.jpg

这个时候就自动生成了如下界面,甚至还帮你打开了仿真界面。

7.jpg

 我们先不急着仿真,先看看整个工程的结构再说。双击下图中的BD文件,

8.jpg

    此时弹出结构框图如下,

9.jpg

    整个工程由两部分组合:1、我们打包的IP,该IP的接口是AIX4-Lite-slave;2、AXI Verification IP,这是一个AXI的验证IP,提供多种验证方式,功能很强大,双击这个IP,看看它的内置定制信息:

        可以看到,它可选选择接口模式来实现主机或从机或直通功能;可选协议类型,地址位宽,数据位宽等。我们这里不动它,直接cancel

10.jpg

  接着点击按下图中操作,右击BD文件,选择generate output products来生成源码:

11.png

在此路径下生成了源码(第2个文件)文件,和顶层例化文件(第1个文件,这个不用管)

12.png

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总线的仿真波形

我们先把自动生成的仿真信号删除,添加如下的波形信号:

13.png

 仿真结果如下:

14.bmp

可以看到仿真结果是用这个彩条+字符的形式表示的,非常清晰。这就是添加了AXI VIP IP的效果。

        在AXI4-Lite总线上共发生了8个事务:先是连续的4个写事务,接着4个读事务。下面的五个通道分别示意了此时通道内执行的握手操作,将鼠标放在其中任意一处上,会出现如下信息(顺序1、地址0等):

15.png

 在左键点击,会显示具体的事务流程如下:

16.png

 从上图的箭头我们可以直到一次写事务的流程:写地址----写数据----写响应。再看看读事务的流程:

17.png

 可以看到读事务的流程:读地址----读数据。

3.2、从机IP的slave接口仿真波形

        看完了AXI4-Lite总线的仿真波形,我们再看下上面具体解析代码(可以理解为底层驱动)的仿真波形。按如下方法添加:

18.png

将信号按通道或用途做好分类,仿真结果如下:

19.png

信号较多,我们先解析写事务如下:

20.png

上图中,一共进行了4次写入操作,写入的地址分别为0、4、8、12,分别对应从机内4个Slave寄存器的地址。写入的数据分别为1-4。握手过程就不谈了。看一下数据是怎么被写到

 Slave寄存器的,如下:

21.png

  从上图可以看到:

 slave寄存器写入使能信号slv_reg_wren是与总线上的写事务对齐的,这样就可以直接将总线要写入的数据写入slave寄存器;

 根据写入的地址,将要写入的数据分别写入对应的slave寄存器,具体到上图,分别往4个寄存器写入数据1-4

再看下读事务的时序图:

22.png

    完成握手后,依次从地址(0、4、8、12)中读出了数据1-4,与之前写入的一致(数据被存在从机的slave寄存器中)。接着看一下数据是从Slave寄存器中读出来的:

23.png从上图可以看到:

slave寄存器读取使能信号slv_reg_rden是与总线上的读事务对齐的,这样就可以直接将slave寄存器的值读出赋值给总线,考虑到读取寄存器的值存在一个时钟周期的延迟,所以采用了临时变量reg_data_out[31:0]来打一拍,将时序对齐;

分别从4个slave寄存器中读取数据1-4。

4、其他

可以看到其实AXI4-Lite总线的使用还是相对比较简单的,只要设计好各个通道的握手时序,以及读写的时序关系就好了。下一篇文章我们再继续分析AXI4-Lite总线的Master接口的代码。

最新文章

最新文章