AXI4协议之AXI4-Lite接口详解及实战


本文首先对AXI4总线协议进行了一个简单的介绍,然后使用vivado提供的模板创建了一个AXI4-Lite Master的接口,并生成了一个具有Master和Slave的代码实例,阅读该示例代码,进行修改后用于自己的项目。

AXI4总线协议

详细信息可以参考官方手册 AXI Protocol

概述

AXI总线作为一种总线,可以挂载若干个主设备(master)和从设备(slave),AXI总线协议定义了主设备和从设备之间如何进行通信。

主设备可以向从设备发起读事务(Read Transaction)和写事务(Write Transaction),从设备只能被动接受主设备发起的请求并作出响应

一般来说,所有的设备共享总线的控制总线、地址总线和数据总线,因此同一时间只能有一对主从设备通过总线进行通信。由于可能会有多个主设备同时访问总线,因此必须要有个控制器选择哪个主设备优先进行通信,这便是总线仲裁;此外主设备可能会访问不同的从设备,此时控制器需要对主设备访问的地址进行译码,选择对应的从设备来和主设备进行通信。下面是一个示意图,图中的 Interconnect 就是我上面说的控制器。

请添加图片描述

在IO统一编址的系统中,每个从设备都会对应一段内存地址空间,比如说LCD控制器可能会对应地址0xff001000-0xff0010ff,这里0xff001000便是LCD控制器的基地址(Base Address);此外还有个地址偏移的概念,比如说地址0xff001004相对基地址的偏移就是0x4。主设备访问从设备需要使用实际地址,即基地址+地址偏移,而从设备一般只需要知道偏移地址即可。

LCD控制器(从设备)其实并不关心基地址是啥,它只要定义好每个偏移地址是用来干啥的就可以,比如说偏移地址0x4(该偏移地址对应的实际地址是基地址+偏移地址,即0xff001004)可能对应于存储LCD控制器型号的寄存器,LCD控制器需要自己定义好当主设备读写它对应地址空间的某个地址时,自己应该干啥,比如LCD控制器可以实现为,当主设备读取0xff001004(偏移地址0x4)时,它会把LCD的型号返回给主设备。

主设备和上图中的 Interconnect 则需要约定好从设备的基地址。主设备可能需要访问不同的从设备,自然需要知道从设备的基地址; Interconnect 需要将主设备访问的地址进行译码,从而选择对应的从设备来和主设备进行通信,因此也需要知道从设备的基地址。

AXI4总线协议支持以下三种类型的接口(Interface):

  1. AXI4(有时候也叫AXI4-full):高性能存储映射接口
  2. AXI4-Lite:简化版的AXI4接口,用于较少数据量的存储映射通信
  3. AXI4-Steam:用于高速数据流传输,非存储映射接口

所谓存储映射,就是主设备访问从设备时需要给出访问的地址,即从设备是对应一段内存空间地址的,和上面说的IO统一编址差不多一个意思

AXI4-full接口支持突发传输,主要用于处理器访问存储器等需要指定地址的高速数据传输场景。AXI4-Lite接口为外设提供单个数据传输,主要用于访问一些低速外设中的寄存器。而AXI-Steam接口则向FIFO一样,数据传输时不需要地址,在主从设备之间直接连续读写数据,主要用于如视频、高速AD、PCIe、DMA接口等需要高速数据传输的场合。

AXI4-full接口支持突发传输,所谓突发传输,就是给一个地址,可以对从这个地址开始的若干个地址处的值都进行读写,至于这若干个地址分别是什么,需要根据写/读地址通道 Master 给出的控制信号来决定

AXI4-Lite接口为外设提供单个数据传输,所谓单个数据传输,就是给一个地址,就只能读写这一个地址处的值

AXI4-Lite接口

AXI4-Lite接口由五个独立的通道构成:

  1. 读地址
  2. 读数据
  3. 写地址
  4. 写数据
  5. 写响应

在一次读事务(Read Transaction)的过程中,主机首先在读地址通道给出读地址和控制信号,然后从机由读数据通道返回读出的数据。下面是使用读地址和读数据通道实现一次读事务的示意图:

请添加图片描述

对于AXI4-Lite接口,一次读事务只能读取一个地址处的数据,但是对于AXI4-Full接口,一次读事务中,Master发出一个地址,而Slave可以从该地址开始连续读出多个地址处的数据并发送到读数据通道,这便是所谓的突发传输

在一次写事务(Write Transaction)的过程中,主机在写地址通道给出写地址和控制信号,然后在写数据通道发送要写的数据。从机在接收数据之后,在写响应通道给出响应信号。下面是一次写事务的示意图:

请添加图片描述

对于AXI4-Lite接口,一次写事务只能写入一个地址处的数据,但是对于AXI4-Full接口,一次写事务中,Master发出一个地址后,可以连续发出多个数据,Slave可以从该地址开始连续写多个地址处的数据,这便是所谓的突发传输

每个通道都包含了一组信号,尤其需要注意的是 VALID 和 READY 信号。VALID 信号由源端(source)产生,表示当前地址或者数据线上的信息是有效的;而 READY 信号由目的端(destination)产生,则表示已经准备好接收地址、数据以及控制信息。

举个例子,对于读数据通道,Slave需要将数据发送给Master,所以Slave是源端,也就是Slave负责产生 VALID 信号,Master负责产生READY信号

对于写数据通道,Master需要将数据发送给Slave,所以Master是源端,也就是Master负责产生 VALID 信号,Slave负责产生READY信号

在由通道传输信息的时候,需要通过 VALID 和 READY 信号进行握手,如下图所示,图片中的 INFORMATION 是指通过通道传输的某种信息,可以是地址、数据、控制信号等等

请添加图片描述

在 AXI 协议中,所有的输入信号都在是 ACLK 的上升沿采样,所有的输出信号必须在 ACLK 的上升沿之后才能改变。在 T1 之后,源端将 VALID 拉高,表明 INFORMATION 信号线上传输的是有效的地址、数据或者控制信息。目的端在 T2 之后将 READY 拉高,表明它已经准备好接收数据,此时源端必须保持 INFORMATION 数据稳定不变,直到 T3 时刻进行数据传输。

为了防止死锁,VALID信号和READY必须遵守以下的约定

  1. VALID信号的拉高不能依赖于 READY 信号,也就是说源端不允许等目的端的 READY 信号拉高之后,才将 VALID 信号拉高。而且, 一旦 VALID 拉高,源端必须保持其处于拉高状态,直至成功握手(在时钟上升沿检测到 VALID 和 READY 均为高电平)后才能拉低 VALID
  2. 目的端可以等检测到 VALID 信号拉高后,才将READY信号拉高;也可以不等

到这里,已经简单介绍了 AXI4-Lite 接口的读写过程,以及握手协议。

下面主要讲解AXI-Lite Master接口,在讲解时,是直接把一个主设备和一个从设备连接在一起,从而忽略总线的仲裁和地址译码,重点关注协议内容。

创建代码实例

下面介绍如何使用vivado提供的模板创建一个AXI4-Lite Master的接口,并生成一个具有Master和Slave的代码实例。

首先打开Vivado,创建一个新工程,进入后点击右上角 Tools->Create and Package New IP,会弹出一个窗口,点击 Next 后,窗口如下所示

请添加图片描述

勾选 Create a new AXI4 peripheral,点击 Next,窗口如下所示

请添加图片描述

这里可以设置IP的名称和存储路径,设置好后点击 Next,窗口如下所示

请添加图片描述

Interface Type 选择 LiteInterface Mode 选择 MasterData Width 这里不能修改(AXI4总线协议支持32位和64位的数据宽度,但是这里暂时不能修改,去哪修改后面再说明),Memory Size 只能在接口为 AXI4-full Slave 时才可以修改(回忆之前说的AXI4-full接口主要用于处理器访问存储器,Memory Size 的含义就清楚了),Number of Registers 只能在接口为 AXI4-Lite slave 时可以修改(回忆之前说的AXI4-Lite接口主要用于访问寄存器,Number of Registers 的含义就清楚了)。选择后点击 Next,窗口如下所示

请添加图片描述

勾选 Verify Peripheral IP using AXI4 VIP,用于生成一个具有Master和Slave的代码实例,点击 Finish,最终界面如下所示

请添加图片描述

可以看到vivado自动执行了模拟,点击图片右上角的 x 关掉模拟,界面如下所示

请添加图片描述

可以看到右边 Diagram 一栏给出了这个代码实例的连接图,左边这个是我们自己创建的 AXI4-Lite Master 接口,右边是vivado自己创建的用于验证Master功能的 AXI4-Lite Slave

如果我们要修改地址宽度和数据宽度,可以双击 Diagram 中的 slave_0,如下图所示,此时还不能修改,点击 ADDRESS WIDTHDATA WIDTH 前面的 AUTO,切换到 MANUAL 后即可修改,我这里保持默认。

请添加图片描述

点击 Address Editor,这里我们可以编辑 Slave 对应的地址空间,这里保持默认即可,如下图所示

请添加图片描述

另外值得一提的是,点击界面最左侧的 PROJECT MANAGER->IP catalog ,我们可以在 User Repository 下找到我们刚刚定义的 AXI4-Lite Master IP核。

接下来我们找到源代码,如下所示

请添加图片描述

我们主要关心的是 m_axi4_lite_0,也就是我们刚刚创建的IP核,右击 m_axi4_lite_v1_0_bfm_1_i,点击 Generate Output Products,会弹出以下窗口

请添加图片描述

点击 Generate,等待完成后,如下图所示,可以看到 m_axi4_lite_0 前面多了一个 >,点击 >(如果弹出窗口,点击 OK即可)展开IP核,等待完成后,最终结果如下所示

请添加图片描述

乍一看有很多层,其实都只是简单的套壳,我们接下来真正要分析的就是最下层的 m_axi4_lite_v1_0_M00_AXI_inst,双击后即可打开。具体代码的分析将在下一部分进行。

代码分析

接下来主要是分析官方给的Master示例,即 m_axi4_lite_v1_0_M00_AXI.v

首先来描述一下该示例代码干了啥:当用户给一个开启信号后,Master接口会首先进行4次写事务(Slave中定义了4个寄存器,一次写事务完成对一个寄存器的修改),然后再进行4次读事务(依次读取4个寄存器),在读的过程中会比对读出来的值和预期值(也就是写事务中写入的值)是否一样,最终输出比对的结果,然后等待用户下一次的开启信号。

模块参数

名称作用
C_M_START_DATA_VALUEMaster 将从 C_M_START_DATA_VALUE 开始生成要写的数据
C_M_TARGET_SLAVE_BASE_ADDRSlave 的基地址
C_M_AXI_ADDR_WIDTH读写地址的宽度,默认32位
C_M_AXI_DATA_WIDTH读写数据的宽度,默认32位
C_M_TRANSACTIONS_NUM执行读事务和写事务的次数,默认4次,因为Slave寄存器的数目是4,4次写/读事务依次对应一个寄存器

输入输出信号

其他信号

名称作用
input wire INIT_AXI_TXN也就是上面说的开启信号,用户需要将该信号拉高一个时钟周期来开启
output reg ERROR如果比对结果不一样,或者数据传输出现错误,ERROR会被置位
output wire TXN_DONE当读写事务以及结果比对都完成后,TXN_DONE会被置位
input wire M_AXI_ACLK时钟
input wire M_AXI_ARESETN复位信号,低电平复位

写地址通道

带有AW的信号,即Address Write

名称作用
output wire [C_M_AXI_ADDR_WIDTH-1 : 0] M_AXI_AWADDR写地址
output wire [2 : 0] M_AXI_AWPROT该信号表示写事务的权限和安全级别,以及写事务是数据访问还是指令访问。具体细节可以参考官方手册,示例代码中该信号的值恒为 3'b000
output wire M_AXI_AWVALID该信号拉高表明 Master 发出了有效的写地址和控制信号(控制信号就是指M_AXI_AWPROT)
input wire M_AXI_AWREADY该信号拉高表示slave已准备好接受地址和相关控制信号

写数据通道

带有W的信号

名称作用
output wire [C_M_AXI_DATA_WIDTH-1 : 0] M_AXI_WDATA写数据
output wire [C_M_AXI_DATA_WIDTH/8-1 : 0] M_AXI_WSTRB选通信号,用于表示写数据中哪些字节是合法的,也就是说哪些字节需要写入到slave中,示例代码中该信号的值恒为 4'b1111
output wire M_AXI_WVALID该信号拉高表示 Master 发出了有效的写数据和选通信号
input wire M_AXI_WREADY该信号拉高表明 Slave 已经准备好接受地址和选通信号

写响应通道

带有B的信号

名称作用
input wire [1 : 0] M_AXI_BRESP该信号表明写事务的状态
input wire M_AXI_BVALID这个信号拉高表明 Slave 发出了有效的 M_AXI_BRESP 信号
output wire M_AXI_BREADY该信号拉高表示 Master 已经准备好接受 M_AXI_BRESP 信号

读地址通道

带有AR的信号

名称作用
output wire [C_M_AXI_ADDR_WIDTH-1 : 0] M_AXI_ARADDR读地址
output wire [2 : 0] M_AXI_ARPROT保护类型。该信号表明事务的特权和安全级别,以及事务是数据访问还是指令访问。具体细节可以参考官方手册,示例代码中该信号的值恒为 3'b001
output wire M_AXI_ARVALID该信号拉高表明 Master 发出了有效的读地址和控制信号
input wire M_AXI_ARREADY该信号拉高表示 slave 已准备好接受地址和控制信号

读数据通道

带有R的信号

名称作用
input wire [C_M_AXI_DATA_WIDTH-1 : 0] M_AXI_RDATA读数据
input wire [1 : 0] M_AXI_RRESP读响应,表明读事务的状态
input wire M_AXI_RVALID读有效。表明slave正在发送读数据
output wire M_AXI_RREADY读准备。表明master可以接受读数据和响应信息

代码阅读

状态机的相关状态

首先,有一个状态机控制整个代码的行为,相关信号如下所示

	// function called clogb2 that returns an integer which has the
	// value of the ceiling of the log base 2
	function integer clogb2 (input integer bit_depth);
		begin
		for(clogb2=0; bit_depth>0; clogb2=clogb2+1)
			bit_depth = bit_depth >> 1;
		end
	endfunction

// TRANS_NUM_BITS is the width of the index counter for 
// number of write or read transaction.
	localparam integer TRANS_NUM_BITS = clogb2(C_M_TRANSACTIONS_NUM-1);

// Example State machine to initialize counter, initialize write transactions, 
// initialize read transactions and comparison of read data with the 
// written data words.
parameter [1:0] IDLE = 2'b00, // This state initiates AXI4Lite transaction 
		// after the state machine changes state to INIT_WRITE   
		// when there is 0 to 1 transition on INIT_AXI_TXN
	INIT_WRITE   = 2'b01, // This state initializes write transaction,
		// once writes are done, the state machine 
		// changes state to INIT_READ 
	INIT_READ = 2'b10, // This state initializes read transaction
		// once reads are done, the state machine 
		// changes state to INIT_COMPARE 
	INIT_COMPARE = 2'b11; // This state issues the status of comparison 
		// of the written data with the read data

reg [1:0] mst_exec_state;

从状态名称大概能知道每个状态的任务,mst_exec_state 就是当前的状态,具体状态机的状态是如何变化的,后面再解释

一堆信号声明

接下来是一连串的信号声明,有些信号从名字大概可以知道用途,后面碰到了再详细解释,这里先不管

// AXI4LITE signals
	//write address valid
	reg  	axi_awvalid;
	//write data valid
	reg  	axi_wvalid;
	//read address valid
	reg  	axi_arvalid;
	//read data acceptance
	reg  	axi_rready;
	//write response acceptance
	reg  	axi_bready;
	//write address
	reg [C_M_AXI_ADDR_WIDTH-1 : 0] 	axi_awaddr;
	//write data
	reg [C_M_AXI_DATA_WIDTH-1 : 0] 	axi_wdata;
	//read addresss
	reg [C_M_AXI_ADDR_WIDTH-1 : 0] 	axi_araddr;
	//Asserts when there is a write response error
	wire  	write_resp_error;
	//Asserts when there is a read response error
	wire  	read_resp_error;
	//A pulse to initiate a write transaction
	reg  	start_single_write;
	//A pulse to initiate a read transaction
	reg  	start_single_read;
	//Asserts when a single beat write transaction is issued and remains asserted till the completion of write trasaction.
	reg  	write_issued;
	//Asserts when a single beat read transaction is issued and remains asserted till the completion of read trasaction.
	reg  	read_issued;
	//flag that marks the completion of write trasactions. The number of write transaction is user selected by the parameter C_M_TRANSACTIONS_NUM.
	reg  	writes_done;
	//flag that marks the completion of read trasactions. The number of read transaction is user selected by the parameter C_M_TRANSACTIONS_NUM
	reg  	reads_done;
	//The error register is asserted when any of the write response error, read response error or the data mismatch flags are asserted.
	reg  	error_reg;
	//index counter to track the number of write transaction issued
	reg [TRANS_NUM_BITS : 0] 	write_index;
	//index counter to track the number of read transaction issued
	reg [TRANS_NUM_BITS : 0] 	read_index;
	//Expected read data used to compare with the read data.
	reg [C_M_AXI_DATA_WIDTH-1 : 0] 	expected_rdata;
	//Flag marks the completion of comparison of the read data with the expected read data
	reg  	compare_done;
	//This flag is asserted when there is a mismatch of the read data with the expected read data.
	reg  	read_mismatch;
	//Flag is asserted when the write index reaches the last write transction number
	reg  	last_write;
	//Flag is asserted when the read index reaches the last read transction number
	reg  	last_read;
	reg  	init_txn_ff;
	reg  	init_txn_ff2;
	reg  	init_txn_edge;
	wire  	init_txn_pulse;

一堆assign语句

接下来是一些 assign 语句

// I/O Connections assignments

	//Adding the offset address to the base addr of the slave
	// 接下来代码里使用的axi_awaddr都是寄存器的偏移地址
	assign M_AXI_AWADDR	= C_M_TARGET_SLAVE_BASE_ADDR + axi_awaddr;
	//AXI 4 write data
	assign M_AXI_WDATA	= axi_wdata;
	assign M_AXI_AWPROT	= 3'b000;
	assign M_AXI_AWVALID	= axi_awvalid;
	//Write Data(W)
	assign M_AXI_WVALID	= axi_wvalid;
	//Set all byte strobes in this example
	// 写数据的四个字节都选通
	assign M_AXI_WSTRB	= 4'b1111;
	//Write Response (B)
	assign M_AXI_BREADY	= axi_bready;
	//Read Address (AR)
	// 接下来代码里使用的axi_araddr都是寄存器的偏移地址
	assign M_AXI_ARADDR	= C_M_TARGET_SLAVE_BASE_ADDR + axi_araddr;
	assign M_AXI_ARVALID	= axi_arvalid;
	assign M_AXI_ARPROT	= 3'b001;
	//Read and Read Response (R)
	assign M_AXI_RREADY	= axi_rready;
	//Example design I/O
	// TXN_DONE并不是AXI协议里面的,这个信号就是用户自己定义的信号,用于指示啥时候读写事务都完成了
	assign TXN_DONE	= compare_done;

开启信号

另外,需要注意这几个变量:init_txn_ffinit_txn_ff2init_txn_pulse,接下来的代码给出了这几个变量的用法。

    assign init_txn_pulse = (!init_txn_ff2) && init_txn_ff;
	//Generate a pulse to initiate AXI transaction.
	always @(posedge M_AXI_ACLK)
	  begin
	    // Initiates AXI transaction delay    
	    if (M_AXI_ARESETN == 0 )
	      begin
	        init_txn_ff <= 1'b0;
	        init_txn_ff2 <= 1'b0;
	      end
	    else
	      begin
	        init_txn_ff <= INIT_AXI_TXN;
	        init_txn_ff2 <= init_txn_ff;
	      end          
	  end     

INIT_AXI_TXN 就是之前说的开启信号,这个开启信号置位的时间可能会持续多个周期,后面的很多代码会在时钟上升沿检查开启信号是否置位,如果置位那就把一堆寄存器都复位,也就是说如果直接用 INIT_AXI_TXN 作为开启信号,一旦这个信号置位持续多个周期,那么会导致接下来的多个周期里,该模块会反复置位;为了避免这个情况,示例代码使用了 init_txn_ffinit_txn_ff2 这两个变量,然后后面的代码实际使用的开启信号是 init_txn_pulse,阅读代码后可以发现,即使 INIT_AXI_TXN 置位持续多个周期,init_txn_pulse 也只是会置位一个周期。

写地址通道

接下来是写地址通道的代码。start_single_write 这个信号应该会保持高电平一个周期来启动一次写事务,该信号的生成在后面的代码中;write_index 用于表示已经发出了多少次写事务

//--------------------
	//Write Address Channel
	//--------------------
	// 这里也不包括要写的地址,地址在后面生成,实现了将控制逻辑分离
	// The purpose of the write address channel is to request the address and 
	// command information for the entire transaction.  It is a single beat
	// of information.
	// 在这个例子中,地址有效和数据有效是同时置位的,因为都是在上升沿检测start_single_write是否置位
	// Note for this example the axi_awvalid/axi_wvalid are asserted at the same
	// time, and then each is deasserted independent from each other.
	// This is a lower-performance, but simplier control scheme.
	// awvalid信号必需保持高电平,直到slave置位awready信号
	// AXI VALID signals must be held active until accepted by the partner.
	
	// 下面这段话是说,每次检查到slave返回的awready信号置位后,就立即将valid复位
	// 尽管可以不复位来发起多次请求,但是为了简便
	// A data transfer is accepted by the slave when a master has
	// VALID data and the slave acknoledges it is also READY. While the master
	// is allowed to generated multiple, back-to-back requests by not 
	// deasserting VALID, this design will add rest cycle for
	// simplicity.
	
	// 这段话的没有冲突应该是指slave不会出现上一个请求还在执行,下一个请求就又来了的情况
	// Since only one outstanding transaction is issued by the user design,
	// there will not be a collision between a new request and an accepted
	// request on the same clock cycle. 

	  always @(posedge M_AXI_ACLK)										      
	  begin                                                                        
	    //Only VALID signals must be deasserted during reset per AXI spec          
	    //Consider inverting then registering active-low reset for higher fmax     
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)                                                   
	      begin                                                                    
	        axi_awvalid <= 1'b0;                                                   
	      end                                                                      
	      //Signal a new address/data command is available by user logic           
	    else                                                                       
	      begin                                                                    
	        if (start_single_write)                                                
	          begin                                                                
	            axi_awvalid <= 1'b1;                                               
	          end                                                                  
	     //Address accepted by interconnect/slave (issue of M_AXI_AWREADY by slave)
	        else if (M_AXI_AWREADY && axi_awvalid)                                 
	          begin                                                                
	            axi_awvalid <= 1'b0;                                               
	          end                                                                  
	      end                                                                      
	  end                                                                          
	                                                                               
	                                                                               
	  // start_single_write triggers a new write                                   
	  // transaction. write_index is a counter to                                  
	  // keep track with number of write transaction                               
	  // issued/initiated                                                          
	  always @(posedge M_AXI_ACLK)                                                 
	  begin                                                                        
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)                                                   
	      begin                                                                    
	        write_index <= 0;                                                      
	      end                                                                      
	      // Signals a new write address/ write data is                            
	      // available by user logic                                               
	    else if (start_single_write)                                               
	      begin                                                                    
	        write_index <= write_index + 1;                                        
	      end                                                                      
	  end

从上面的代码可以看出,awvalid 置位后需要等检测到 awready 信号置位后,它才会复位,而且 awvalid 不会等 awready 置位才置位

写数据通道

	//--------------------
	//Write Data Channel
	//--------------------
	// 这里没有要传输的数据的代码,要传输的数据是在后面生成
	// 这个方法挺好的,尤其是我们进行模拟的时候,控制逻辑里面不包括要写入的数据,而是产生一个write_index的值
	// 后面要传输的数据可以根据这个write_index来生成
	//The write data channel is for transfering the actual data.
	//The data generation is speific to the example design, and 
	//so only the WVALID/WREADY handshake is shown here

	   always @(posedge M_AXI_ACLK)
	   begin 
	     if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)     
	       begin
	         axi_wvalid <= 1'b0;
	       end
	     //Signal a new address/data command is available by user logic
	     else if (start_single_write)
	       begin
	         axi_wvalid <= 1'b1;
	       end
	     //Data accepted by interconnect/slave (issue of M_AXI_WREADY by slave)
	     else if (M_AXI_WREADY && axi_wvalid)
	       begin
	        axi_wvalid <= 1'b0;
	       end
	   end

从上面的代码可以看出,wvalid 置位后需要等检测到 wready 信号置位后,它才会复位,而且 wvalid 不能等 wready 置位才置位

写响应通道

	//----------------------------
	//Write Response (B) Channel
	//----------------------------

	//The write response channel provides feedback that the write has committed
	//to memory. BREADY will occur after both the data and the write address
	//has arrived and been accepted by the slave, and can guarantee that no
	//other accesses launched afterwards will be able to be reordered before it.

	//The BRESP bit [1] is used indicate any errors from the interconnect or
	//slave for the entire write burst. This example will capture the error.
	//While not necessary per spec, it is advisable to reset READY signals in
	//case of differing reset latencies between master/slave.

	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
	      begin
	        axi_bready <= 1'b0;
	      end
	    // accept/acknowledge bresp with axi_bready by the master
	    // when M_AXI_BVALID is asserted by slave
	    else if (M_AXI_BVALID && ~axi_bready)
	      begin
	        axi_bready <= 1'b1;
	      end
	    // deassert after one clock cycle
	    else if (axi_bready)
	      begin
	        axi_bready <= 1'b0;
	      end
	    // retain the previous value
	    else
	      axi_bready <= axi_bready;                                        
	  end
	                                                                       
	//Flag write errors                                                    
	assign write_resp_error = (axi_bready & M_AXI_BVALID & M_AXI_BRESP[1]);

在上面的代码中,bready 会等 bvalid 置位后才会置位(当然 bready 也可以不等),然后维持一个周期的高电平后就会拉低

读地址通道

	//----------------------------
	//Read Address Channel
	//----------------------------

	//start_single_read triggers a new read transaction. read_index is a counter to
	//keep track with number of read transaction issued/initiated

	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
	      begin
	        read_index <= 0;
	      end
	    // Signals a new read address is
	    // available by user logic
	    else if (start_single_read)
	      begin
	        read_index <= read_index + 1;
	      end
	  end

	  // A new axi_arvalid is asserted when there is a valid read address
	  // available by the master. start_single_read triggers a new read
	  // transaction
	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
	      begin
	        axi_arvalid <= 1'b0;
	      end
	    //Signal a new read address command is available by user logic
	    else if (start_single_read)
	      begin
	        axi_arvalid <= 1'b1;
	      end
	    //RAddress accepted by interconnect/slave (issue of M_AXI_ARREADY by slave)
	    else if (M_AXI_ARREADY && axi_arvalid)
	      begin
	        axi_arvalid <= 1'b0;
	      end
	    // retain the previous value
	  end

从上面的代码可以看出,arvalid 置位后需要等检测到 arready 信号置位后,它才会复位,而且 arvalid 不会等 arready 置位才置位

读数据通道

	//--------------------------------
	//Read Data (and Response) Channel
	//--------------------------------

	//The Read Data channel returns the results of the read request 
	//The master will accept the read data by asserting axi_rready
	//when there is a valid read data available.
	//While not necessary per spec, it is advisable to reset READY signals in
	//case of differing reset latencies between master/slave.

	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
	      begin
	        axi_rready <= 1'b0;
	      end
	    // accept/acknowledge rdata/rresp with axi_rready by the master
	    // when M_AXI_RVALID is asserted by slave
	    else if (M_AXI_RVALID && ~axi_rready)
	      begin
	        axi_rready <= 1'b1;
	      end
	    // deassert after one clock cycle
	    else if (axi_rready)
	      begin
	        axi_rready <= 1'b0;
	      end
	    // retain the previous value
	  end
	                                                                        
	//Flag read errors                                                     
	assign read_resp_error = (axi_rready & M_AXI_RVALID & M_AXI_RRESP[1]);

在上面的代码中,rready 会等 rvalid 置位后才会置位(当然 rready 也可以不等),然后维持一个周期的高电平后就会拉低

读写地址和数据生成

这块理解起来比较简单,不再赘述,如果后续我们要修改示例代码来用于自己的设计,这块也是需要重点关注的

	//Address/Data Stimulus

	//Address/data pairs for this example. The read and write values should
	//match.
	//Modify these as desired for different address patterns.

	  //Write Addresses
	  always @(posedge M_AXI_ACLK)
	      
	        if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)
	          begin
	            axi_awaddr <= 0;
	          end
	          // Signals a new write address/ write data is         
	          // available by user logic
	        else if (M_AXI_AWREADY && axi_awvalid)
	          begin
	          	// 这是给下一次传输生成的新数据,而不是当前这个传输
	            axi_awaddr <= axi_awaddr + 32'h00000004;
	          end
	      end

	  // Write data generation
	  always @(posedge M_AXI_ACLK)
	      begin
	        if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 )
	          begin
	            axi_wdata <= C_M_START_DATA_VALUE;
	          end
	        // Signals a new write address/ write data is
	        // available by user logic
	        else if (M_AXI_WREADY && axi_wvalid)
	          begin
	          	// 这是给下一次传输生成的新数据,而不是当前这个传输
	            axi_wdata <= C_M_START_DATA_VALUE + write_index;
	          end
	        end

	  //Read Addresses
	  always @(posedge M_AXI_ACLK)
	      begin
	        if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)
	          begin
	            axi_araddr <= 0;
	          end
	          // Signals a new write address/ write data is
	          // available by user logic
	        else if (M_AXI_ARREADY && axi_arvalid)
	          begin
	          	
	            axi_araddr <= axi_araddr + 32'h00000004;
	          end
	      end

	  always @(posedge M_AXI_ACLK)
	      begin
	        if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)
	          begin
	            expected_rdata <= C_M_START_DATA_VALUE;
	          end
	          // Signals a new write address/ write data is
	          // available by user logic
	        else if (M_AXI_RVALID && axi_rready)
	          begin
	            expected_rdata <= C_M_START_DATA_VALUE + read_index;
	          end
	      end

状态机

	  //implement master command interface state machine
	  always @ ( posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 1'b0)
	      begin
	      // reset condition
	      // All the signals are assigned default values under reset condition
	        mst_exec_state  <= IDLE;
	        start_single_write <= 1'b0;
	        write_issued  <= 1'b0;
	        start_single_read  <= 1'b0;
	        read_issued   <= 1'b0;
	        compare_done  <= 1'b0;
	        ERROR <= 1'b0;
	      end
	    else
	      begin
	       // state transition
	        case (mst_exec_state)

	          IDLE:
	          // This state is responsible to initiate
	          // AXI transaction when init_txn_pulse is asserted
	            if ( init_txn_pulse == 1'b1 )
	              begin
	                mst_exec_state  <= INIT_WRITE;
	                ERROR <= 1'b0;
	                compare_done <= 1'b0;
	              end
	            else
	              begin
	                mst_exec_state  <= IDLE;
	              end

	          INIT_WRITE:
	            // This state is responsible to issue start_single_write pulse to
	            // initiate a write transaction. Write transactions will be
	            // issued until last_write signal is asserted.
	            // write controller
	            if (writes_done)
	              begin
	                mst_exec_state <= INIT_READ;//
	              end
	            else
	              begin
	                mst_exec_state  <= INIT_WRITE;

	                  if (~axi_awvalid && ~axi_wvalid && ~M_AXI_BVALID && ~last_write && ~start_single_write && ~write_issued)
	                    begin
	                      start_single_write <= 1'b1;
	                      write_issued  <= 1'b1;
	                    end
	                  else if (axi_bready)
	                    begin
	                      write_issued  <= 1'b0;
	                    end
	                  else
	                    begin
	                      start_single_write <= 1'b0; //Negate to generate a pulse
	                    end
	              end

	          INIT_READ:
	            // This state is responsible to issue start_single_read pulse to
	            // initiate a read transaction. Read transactions will be
	            // issued until last_read signal is asserted.
	             // read controller
	             if (reads_done)
	               begin
	                 mst_exec_state <= INIT_COMPARE;
	               end
	             else
	               begin
	                 mst_exec_state  <= INIT_READ;

	                 if (~axi_arvalid && ~M_AXI_RVALID && ~last_read && ~start_single_read && ~read_issued)
	                   begin
	                     start_single_read <= 1'b1;
	                     read_issued  <= 1'b1;
	                   end
	                 else if (axi_rready)
	                   begin
	                     read_issued  <= 1'b0;
	                   end
	                 else
	                   begin
	                     start_single_read <= 1'b0; //Negate to generate a pulse
	                   end
	               end

	           INIT_COMPARE:
	             begin
	                 // This state is responsible to issue the state of comparison
	                 // of written data with the read data. If no error flags are set,
	                 // compare_done signal will be asseted to indicate success.
	                 ERROR <= error_reg;
	                 mst_exec_state <= IDLE;
	                 compare_done <= 1'b1;
	             end
	           default :
	             begin
	               mst_exec_state  <= IDLE;
	             end
	        endcase
	    end
	  end //MASTER_EXECUTION_PROC

下图描述了状态机状态改变的过程

请添加图片描述

首先需要解释一下上图中的一次事务序列是啥。当用户将 init_txn_pulse 信号置位时,该实例会进行一次事务序列,一次事务序列包括

  1. 首先进行 C_M_TRANSACTIONS_NUM 次写事务
  2. 然后进行 C_M_TRANSACTIONS_NUM 次读事务,将刚刚写入的值都读出来
  3. 在进行读事务的同时,也会比对读出来的数据和预期数据是否相同

下面详细描述每个状态需要完成的工作:

  1. IDLE:如果用户开启了一次事务序列,需要把信号ERROR和compare_done复位,然后状态变为INIT_WRITE;否则状态不变

  2. INIT_WRITE:如果C_M_TRANSACTIONS_NUM次写事务都已经完成,状态变为INIT_READ;否则状态不变,并且如果下列条件满足,就进行一次写事务,即将start_single_write和write_issued置位

    • 当前写地址通道是空闲的,即 (~axi_awvalid)

    • 当前写数据通道是空闲的,即 (~axi_wvalid)

    • 当前写响应通道是空闲的,即 (~M_AXI_BVALID)

    • 还有写事务需要去进行,即 (~last_write)

    • ~start_single_write

    • ~write_issued

    last_write的命名略有点歧义,实际上当实例即将去执行最后一次写事务的时候,该信号的值仍然为0,从而使得最后一次写事务可以成功执行,当最后一次写事务的写地址已经完成后(此时该写事务并没有进行完全,还有写数据和写响应),该信号才会被置位。

    start_single_write用于开启一次写事务,该信号只维持一个周期的高电平,下一个时钟上升沿又会复位

    write_issued用于表示当前写事务是否仍在进行,所以当写响应完成后(即axi_bready的值为1)时,write_issued需要被复位

  3. INIT_READ:如果所有的读事务已经完成,状态变为INIT_COMPARE;否则状态不变,并且如果下列条件满足,就进行一次读事务,即将start_single_read和read_issued置位

    • 当前读地址通道是空闲的,即 (~axi_arvalid)
    • 当前读数据通道是空闲的,即 (~M_AXI_RVALID)
    • 还有读事务需要去进行,即 (~last_read)
    • ~start_single_read
    • ~read_issued

    last_read的命名略有点歧义,实际上当实例即将去执行最后一次读事务的时候,该信号的值仍然为0,从而使得最后一次读事务可以成功执行,当最后一次读事务的读地址已经完成后(此时该读事务并没有进行完全,还有读数据),该信号会被置位。

    start_single_read用于开启一次读事务,该信号只维持一个周期的高电平,下一个时钟上升沿又会复位

    read_issued用于表示当前读事务是否仍在进行,所以当写响应完成后(即axi_rready的值为1)时,read_issued需要被复位

  4. INIT_COMPARE:将ERROR更新为error_reg(这个寄存器在其他地方被更新),置位compare_done,状态变为IDLE

下面是writes_done和last_write信号的生成

	  //Terminal write count

	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
	      last_write <= 1'b0;

	    //The last write should be associated with a write address ready response
	    else if ((write_index == C_M_TRANSACTIONS_NUM) && M_AXI_AWREADY)
	      last_write <= 1'b1;
	    else
	      last_write <= last_write;
	  end

	  //Check for last write completion.

	  //This logic is to qualify the last write count with the final write
	  //response. This demonstrates how to confirm that a write has been
	  //committed.

	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
	      writes_done <= 1'b0;

	      //The writes_done should be associated with a bready response                 
	    else if (last_write && M_AXI_BVALID && axi_bready)
	      writes_done <= 1'b1;
	    else
	      writes_done <= writes_done;
	  end

下面是reads_done和last_read信号的生成

//Terminal Read Count

	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
	      last_read <= 1'b0;

	    //The last read should be associated with a read address ready response         
	    else if ((read_index == C_M_TRANSACTIONS_NUM) && (M_AXI_ARREADY) )
	      last_read <= 1'b1;
	    else
	      last_read <= last_read;
	  end

	/*
	 Check for last read completion.

	 This logic is to qualify the last read count with the final read
	 response/data.
	 */
	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
	      reads_done <= 1'b0;

	    //The reads_done should be associated with a read ready response
	    else if (last_read && M_AXI_RVALID && axi_rready)
	      reads_done <= 1'b1;
	    else
	      reads_done <= reads_done;
	    end

下面是进行数据的比较,如果存在数据不匹配,或者读事务或写事务的响应有问题,那么error_reg会被置位

	//Data Comparison
	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)
	    read_mismatch <= 1'b0;

	    //The read data when available (on axi_rready) is compared with the expected data
	    else if ((M_AXI_RVALID && axi_rready) && (M_AXI_RDATA != expected_rdata))
	      read_mismatch <= 1'b1;
	    else
	      read_mismatch <= read_mismatch;
	  end

	// Register and hold any data mismatches, or read/write interface errors
	  always @(posedge M_AXI_ACLK)
	  begin
	    if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)
	      error_reg <= 1'b0;

	    //Capture any error types
	    else if (read_mismatch || write_resp_error || read_resp_error)
	      error_reg <= 1'b1;
	    else
	      error_reg <= error_reg;
	  end

修改后用于自己的项目

在我自己的项目中,需要通过 AXI4-Lite Master 接口去配置模块里的寄存器,所以我简单的将上面的代码修改了一下。

首先我添加了两个存储器 config_dataconfig_addr,前者用于存储我需要写入的数据,后者用于存储写入数据的地址,然后我使用了 readmemh 来初始化,如下所示

	reg [C_M_AXI_DATA_WIDTH-1:0]  config_data[C_M_TRANSACTIONS_NUM:0];
	reg [C_M_AXI_ADDR_WIDTH-1:0]  config_addr[C_M_TRANSACTIONS_NUM:0];
    initial begin
        $readmemh("YOUR_DATAFILE_PATH/config_data.txt", config_data);
        $readmemh("YOUR_ADDRFILE_PATH/config_addr.txt", config_addr);
    end

C_M_TRANSACTIONS_NUM 的值需要修改为配置寄存器的总次数

然后就是需要修改地址和数据生成的代码,如下所示

	  //Write Addresses
	  always @(posedge M_AXI_ACLK)                                  
	      begin                                                     
	        if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)                                
	          begin                                                 
	            axi_awaddr <= config_addr[0];                                    
	          end                                                   
	          // Signals a new write address/ write data is         
	          // available by user logic                            
	        else if (M_AXI_AWREADY && axi_awvalid)                  
	          begin                                                 
	            axi_awaddr <= config_addr[write_index];                     
	          end                                                   
	      end                                                       
	                                                                
	  // Write data generation                               
	  always @(posedge M_AXI_ACLK)                                  
	      begin                                                     
	        if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 )                                
	          begin                                                 
	            axi_wdata <= config_data[0];                  
	          end                                                   
	        // Signals a new write address/ write data is           
	        // available by user logic                              
	        else if (M_AXI_WREADY && axi_wvalid)                    
	          begin                                                 
	            axi_wdata <= config_data[write_index];    
	          end                                                   
	        end          	                                       
	                                                                
	  //Read Addresses                                  
	  always @(posedge M_AXI_ACLK)                                  
	      begin                                                     
	        if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)                                
	          begin                                                 
	            axi_araddr <= config_addr[0];                                    
	          end                                                   
	          // Signals a new write address/ write data is         
	          // available by user logic                            
	        else if (M_AXI_ARREADY && axi_arvalid)                  
	          begin                                                 
	            axi_araddr <= config_addr[read_index];            
	          end                                                   
	      end                                                       
	                                                                
	                                                                                                                            
	  reg [TRANS_NUM_BITS : 0] 	comp_index;    
	  // 这里需要新定义一个comp_index,不能使用read_index,否则会出问题                                                                                                      
	  always @(posedge M_AXI_ACLK)                                  
	      begin                                                     
	        if (M_AXI_ARESETN == 0  || init_txn_pulse == 1'b1)                                
	          begin                                                 
	            expected_rdata <= config_data[0];   
	            comp_index <= 0;          
	          end                                                   
	          // Signals a new write address/ write data is         
	          // available by user logic                            
	        else if (M_AXI_RVALID && axi_rready)                    
	          begin                                                 
	            expected_rdata <= config_data[comp_index + 1];
	            comp_index <= comp_index + 1;
	          end                                                   
	      end 

接下来我进行了模拟,结果发现中间的读事务发生了死锁,波形图如下所示

请添加图片描述

可以看到 rvalid 和 rready 信号一直为0,在阅读代码后找到了问题所在:Slave 在等 rready 信号置位后才会置位 rvalid(这是错误的),然后 Master 在等 rvalid 置位后才会置位 rready,互相等就发生了死锁。由于 Slave 的代码之前可以正常运行,秉持着 既然代码能运行,那就不要随便改 的原则(doge),我将官方的代码修改为 rready 不需要等 rvalid,如下所示

// 更改 axi_rready 的生成逻辑,因为 slave 的 rvalid 信号会等 rready 信号
// else if (M_AXI_RVALID && ~axi_rready)      
else if (~axi_rready)                        
  begin                                                             
    axi_rready <= 1'b1;                                             
  end

然后我再次运行模拟后又出现了问题,可能会发生不断读的情况,查看波形图后发现 start_single_read 在拉高时会持续高电平2个周期,这会导致 read_index 信号的值出现问题,从而使得 last_read 信号的值不会拉高,故 Master 会不断进行读事务。最终成功将问题定位在下面的代码

if (~axi_arvalid && ~M_AXI_RVALID && ~last_read && ~start_single_read && ~read_issued)
   begin
     start_single_read <= 1'b1;
     read_issued  <= 1'b1;
   end
else if (axi_rready)
   begin
     read_issued  <= 1'b0;
   end
else
   begin
     start_single_read <= 1'b0; //Negate to generate a pulse
   end

这个问题的根源在 rready,因为 rready 信号会不断翻转,start_single_write 在变为 1 之后的那个时钟上升沿,上面这个 if 会检测到 rready 的值为 1,因此会执行代码 read_issued <= 1'b0 而不是本应该执行的 start_single_read <= 1'b0,这会使得 start_single_read 会持续高电平2个周期。由于 read_issued 在这个示例代码中并没有什么用,为了简便地解决这个问题,我直接将 read_issued 删除了,修改后的代码如下所示

if (~axi_arvalid && ~M_AXI_RVALID && ~last_read && ~start_single_read)
   begin
     start_single_read <= 1'b1;
   end
else
   begin
     start_single_read <= 1'b0; //Negate to generate a pulse
   end

在进行仿真确保 Master 的功能都正确后,考虑到我只是需要通过 Master 接口去配置模块里的寄存器,因此我修改了 Master 状态机的代码,即完成所有的写事务后,不再进行读事务,而是直接回到初始状态,修改的代码如下所示

INIT_WRITE:                                                               
	// This state is responsible to issue start_single_write pulse to       
	// initiate a write transaction. Write transactions will be             
	// issued until last_write signal is asserted.                          
	// write controller                                                     
	if (writes_done)                                                        
		begin       
		// 不需要写后再读了                                                          
		// mst_exec_state <= INIT_READ;
		mst_exec_state <= IDLE;  
		// 下面这个语句原本是在INIT_COMPARE时做的
		// 但是由于不需要读,就把这个移到这里来了
		compare_done <= 1'b1;                                   
		end
  • 33
    点赞
  • 121
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值