[FPGA8]基于AXI协议的自定义IP核

基于AXI协议的自定义IP核

  • [FPGA5]打包IP核_PWM发生器,我们已经学习创建打包了一个PWM波生成器的IP核。在ZYNQ系统中设计IP核,最常用的就是使用AXI 总线将 PS 同 PL 部分的 IP 核连接起来。本次将尝试编写打包一个基于AXI总线协议的PWM波发生器IP核。

Vivado 工程建立

使用向导创建AXI模板

  1. 点击Tools -- Create and Package IP...,点击Next
  2. 选择Create a new AXI4 peripheral,创建一个新的AXI4设备
  3. 填写名称与描述,并选择自定义IP的存储位置,这里将Name设为axi_pwm,Display name设为axi_pwm_v1.0,Description设为AXI_PWM_Out
  4. 设置需要使用的接口类型,有Full、Lite、Stream三种模式可选择,由于PWM波发生器仅需要配置频率,占空比等相关的配置寄存器,故选用AXI-Lite协议。寄存器数量根据实际需要,设定为4,位宽按照协议规定,可以在 32 位和 64 位之间进行选择,这里选用32位。
  5. 点击Finish完成 IP 的创建,这时能够在IP Catalog界面中看到刚刚创建的IP核,此时IP核只有读写寄存器的功能,故我们需编写代码,实现所需功能。

修改IP核模板

  1. 选择新生成的IP核,右键Edit in IP Packager,可填入工程名与工程位置
  2. 点击Add Sources,添加 PWM 功能的核心代码
  3. Add or Create Design Sources界面,点击+号创建或添加文件至工程,并勾选Copy Sources into IP Directory选项。新建活添加的文件主要用于编写实现目标功能的代码,即PWM波的发生。
  4. 在自动生成的AXI模板程序中例化我们添加的pwm功能文件,并添加额外所需的输入输出接口,并使用寄存器 slv_reg0-4,对PWM的参数进行具体的控制操作。具体修改添加操作见后文。
  5. 若添加了额外的输入输出接口,则还需在顶层元件例化中添加对应端口

重新打包IP核

  1. 双击Sources -- Design Sources -- IP-XACT -- component.xml文件,打开IP核打包配置界面
  2. File Groups选项中点击Merge changers from File Groups Wizard
  3. Customization Parameters选项中点击Merge changes form Customization Parameters Wizard
  4. 当然,如果在此处想要添加配置端口,预定义常量参数,参数类型等,均可根据实际IP核的功能需求进行修改,具体配置方法详见[FPGA5]打包IP核_PWM发生器
  5. 最后,点击Re-Package IP,完成 IP核的修改工作。

AXI代码模板分析

AXI定义与申明

  1. 该段代码定义了使用的参数,是我们在生成模板的时候预先设置的参数,数据位宽32位,地址位4位。由于位宽为4*8byte,每一个寄存器为4Bit,4个寄存器为16Bit,故地址线为2^4=16。
	module myip_v1_0 #
	(
		// Users to add parameters here

		// User parameters ends
		// Do not modify the parameters beyond this line


		// Parameters of Axi Slave Bus Interface S00_AXI
		parameter integer C_S00_AXI_DATA_WIDTH	= 32,
		parameter integer C_S00_AXI_ADDR_WIDTH	= 4
	)
  1. 该段代码的注释是让用户添加所需要的端口,在该项目中,需要添加一个PWM波的输出接口,故声明一个位宽为1的pwn_out输出接口。
	// Users to add ports here
    output wire pwn_out,
	// User ports ends
	// Do not modify the ports beyond this line
  1. 接下来是对于AXI系统输入输出接口的定义,具体的接口定义与功能,可参考代码注解或者查看[FPGA7]AXI通信协议
  2. 这段代码是对于用户会使用到的逻辑信号与寄存器的申明,包括使用向导时定义的四个寄存器slv_reg0-4,以及寄存器可读使能,可写使能,输出寄存器等
	//-- Signals for user logic register space example
	//------------------------------------------------
	//-- Number of Slave Registers 4
	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;

AXI握手逻辑

  • 以写地址握手逻辑为例,进行握手代码的分析,其余握手操作均同理
  • 上述代码块通过控制 awready 信号完成了一次地址通道传输。逻辑检测 awvalid,wvalid信号的电平,当主机在写地址以及写数据通道上就绪时,从机置高 awready 信号,完成一次地址传输。与上awready 是因为传输只需要在一次时钟上升沿 valid,ready 信号同时置高即可,在下一个周期,从机需要负责将 awready 信号置低,主机一般也会将 awvalid 信号置低,不过这和从机就不相干了。
  • 在置高 awready 信号的条件中与上了 aw_en 变量,这是为了使从机有控制地址通道传输的能力。在整个写传输周期中,即地址-数据-写回复信号传输持续期间,如果主机又发起了一次新的写传输请求,置高 awvalid,wvalid信号,那么此时 aw_en 信号为低,从机将不会响应直至从机完成写回复,结束本次传输后,才回去响应下一次传输。
  • 在从机置起 awready 信号完成地址通道传输的同时,从机也会从写地址上锁存当前的写地址。锁存的写地址会用于其后一个周期时刻来判断待写入的寄存器。
always @( posedge S_AXI_ACLK )//当时钟上升沿
	begin
	  if ( S_AXI_ARESETN == 1'b0 )//判断是否复位
	    begin
	      axi_awready <= 1'b0; //如果复位则置0
	      aw_en <= 1'b1;
	    end 
	  else
	    begin    
	      if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
	        begin//判断是否收到AWVALID信号,WVALID信号和写地址使能信号
	          // slave is ready to accept write address when 
	          // there is a valid write address and write data
	          // on the write address and data bus. This design 
	          // expects no outstanding transactions. 
	          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     

AXI写入

  • 以写数据为例,分析写入的核心代码
  • 首先判断读取写数据的使能条件。写地址传输的后一个时刻为写数据传输,写地址的完成的信号即作为写数据的使能信号。
  • 当写使能有效后,逻辑首先根据写地址判断主机所要操作的寄存器。这里对写地址进行切片:axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]
  • 在完成寄存器的选择后,将总线上已经就绪的写数据写入寄存器。如果不使用 STRB 信号对某些字节进行屏蔽,那么这样就完事了: slv_reg0 <= S_AXI_WDATA;
  • STRB 信号的作用是屏蔽一些字节的写入,比如某个寄存器只有 8 bit,或者 16 bit,但寄存器的写入必须以 32 bit 为单位。完整写入 32 bit 势必会影响临近的其他寄存器,此时可以使用 STRB 信号,指定要写入的字节位置为 1,屏蔽字节位置写 0 。代码中使用 for 循环,以字节为单位,判断 STRB 信号为 1,则进行写入,否则保持原寄存器值不变。
	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
            //对于地址位数进行判断,并分Bit进行for循环写入数据
	        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  
	          default : begin //如果没有写入,则对数据进行保持
	                      slv_reg0 <= slv_reg0;
	                      slv_reg1 <= slv_reg1;
	                    end
	        endcase
	      end
	  end
	end    

AXI写响应

  • 完成一次写操作的最后一步,是由从机在写回复通道上对此次传输进行响应。
  • 在完成传输,即写数据/地址通道完成握手后的下一周期,从机置起 bvalid 信号,并在 bresp 信号上给出 'OK' 信号。在响应通道完成握手后,置低 bvalid 完成响应操作。
	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
	          // indicates a valid write response is available
	          axi_bvalid <= 1'b1;
	          axi_bresp  <= 2'b0; // 'OKAY' response 
	        end                   // work error responses in future
	      else
	        begin
	          if (S_AXI_BREADY && axi_bvalid) 
	            //check if bready is asserted while bvalid is high) 
	            //(there is a possibility that bready is always asserted high)   
	            begin
	              axi_bvalid <= 1'b0; 
	            end  
	        end
	    end
	end   

AXI读出

	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;
	        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

用户功能代码

  • 在用户功能代码部分,关于pwm波发生的相关代码,可直接参照[FPGA5]打包IP核_PWM发生器中关于PWM波的发生代码部分,只需要将该部分代码在AXI代码模板中进行例化,并在顶层文件中添加额外的输入输出接口即可,具体添加代码如下:
	// Add user logic here
	axi_pwm
	#(
		.PWM_BIT_WIDTH(C_S_AXI_DATA_WIDTH)//预定义参数,位宽
	)
	(
		.clk(S_AXI_ACLK),  //输入时钟
		.rst_n(S_AXI_ARESETN),  //复位信号
		.period(slv_reg0),//周期=period*时钟频率
		.void_value(slv_reg1), //占空比=void_value/period
		.pwm_out(pwm_out) //输出
	);
	// User logic ends