admin管理员组文章数量:1648321
文章目录
- AXI4总线协议
- 概述
- AXI4-Lite接口
- 创建代码实例
- 代码分析
- 模块参数
- 输入输出信号
- 其他信号
- 写地址通道
- 写数据通道
- 写响应通道
- 读地址通道
- 读数据通道
- 代码阅读
- 状态机的相关状态
- 一堆信号声明
- 一堆assign语句
- 开启信号
- 写地址通道
- 写数据通道
- 写响应通道
- 读地址通道
- 读数据通道
- 读写地址和数据生成
- 状态机
- 修改后用于自己的项目
本文首先对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):
- AXI4(有时候也叫AXI4-full):高性能存储映射接口
- AXI4-Lite:简化版的AXI4接口,用于较少数据量的存储映射通信
- AXI4-Steam:用于高速数据流传输,非存储映射接口
所谓存储映射,就是主设备访问从设备时需要给出访问的地址,即从设备是对应一段内存空间地址的,和上面说的IO统一编址差不多一个意思
AXI4-full接口支持突发传输,主要用于处理器访问存储器等需要指定地址的高速数据传输场景。AXI4-Lite接口为外设提供单个数据传输,主要用于访问一些低速外设中的寄存器。而AXI-Steam接口则向FIFO一样,数据传输时不需要地址,在主从设备之间直接连续读写数据,主要用于如视频、高速AD、PCIe、DMA接口等需要高速数据传输的场合。
AXI4-full接口支持突发传输,所谓突发传输,就是给一个地址,可以对从这个地址开始的若干个地址处的值都进行读写,至于这若干个地址分别是什么,需要根据写/读地址通道 Master 给出的控制信号来决定
AXI4-Lite接口为外设提供单个数据传输,所谓单个数据传输,就是给一个地址,就只能读写这一个地址处的值
AXI4-Lite接口
AXI4-Lite接口由五个独立的通道构成:
- 读地址
- 读数据
- 写地址
- 写数据
- 写响应
在一次读事务(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必须遵守以下的约定:
- VALID信号的拉高不能依赖于 READY 信号,也就是说源端不允许等目的端的 READY 信号拉高之后,才将 VALID 信号拉高。而且, 一旦 VALID 拉高,源端必须保持其处于拉高状态,直至成功握手(在时钟上升沿检测到 VALID 和 READY 均为高电平)后才能拉低 VALID
- 目的端可以等检测到 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
选择 Lite
,Interface Mode
选择 Master
,Data 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 WIDTH
和 DATA 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_VALUE | Master 将从 C_M_START_DATA_VALUE 开始生成要写的数据 |
C_M_TARGET_SLAVE_BASE_ADDR | Slave 的基地址 |
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_ff
,init_txn_ff2
和 init_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_ff
和 init_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
信号置位时,该实例会进行一次事务序列,一次事务序列包括
- 首先进行
C_M_TRANSACTIONS_NUM
次写事务 - 然后进行
C_M_TRANSACTIONS_NUM
次读事务,将刚刚写入的值都读出来 - 在进行读事务的同时,也会比对读出来的数据和预期数据是否相同
下面详细描述每个状态需要完成的工作:
-
IDLE:如果用户开启了一次事务序列,需要把信号ERROR和compare_done复位,然后状态变为INIT_WRITE;否则状态不变
-
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需要被复位
-
-
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需要被复位
-
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_data
和 config_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
版权声明:本文标题:AXI4协议之AXI4-Lite接口详解及实战 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/xitong/1729494394a1202766.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论