AXI4协议(一)AXI4_lite 上
先附上AXI协议 v2-0 版
AXI(advanced extensible interface)总线是AMBA总线家族中的一员,是由AHB发展而来,用于在SOC中的各个ip之间互联。AXI适用于高带宽,低延迟的应用,尤其是DDR4这样的高速路存储外设。
在XILINX的所有自家ip中,几乎都支持AXI接口标准。所以AXI在FPGA设计中特别常用,尤其是ZYNQ这种需要和ARM核交互的设备。
AXI是并行主线,与串行总线不同,他的接口很多很复杂,总共分为五个通道,信号数量多,不利于集成与仿真调试,需要借助工具进行集成和仿真调试。而且协议相比串行总线要复杂的很多,需要一些学习成本和开发经验才能正确的应用它。
AXI的功能特点有如下几点:
- 单独的地址/控制和数据通道(五个传输通道)
- 支持使用字节选通进行未对齐的数据传输(KEEP与STRB信号)
- 突发的传输仅发出起始地址(FIXED、INCR、WRAP)
- 单独的读写数据通道以实现低成本直接内存访问(全双工)
- 能够发出多个未完成的地址(outstanding模式)
- 无序传输模式(乱序模式)
- 轻松添加寄存器级以提供定时关闭(配置灵活)
有些功能点看不明白没关系,我们将分成几篇文章,挨个介绍并实现这些功能。
接下来我们从最简单的AXI_lite入手,一步步逐渐完善AXI的各种复杂的功能,包括AXI_stream,AXI_full(FIXED、INCR、WRAP),AXI_interconnect,outstanding模式以及乱序模式。
首先介绍AXI的基本概念,AXI协议支持单主单从通信,也支持多主多从通信,不过主从身份不可互换,始终是主机发起读命令或写命令。AXI作为一种并行总线,它的传输通道有五个,而且每个通道互相独立,分为写地址/命令通道,写数据通道,写回复通道,读地址/命令通道与读数据通道。
写数据 读数据为什么AXI的写通道要比读通道多一个响应通道呢?
要弄清楚这个问题首先要理解AXI的握手机制,AXI之所以可以在完全独立的读写通道中高速且安全地传输数据,靠的就是其内部的握手机制,开始时需要握手以确定双方是否准备好,结束后仍需要握手来确认是否成功传输数据。
开始阶段的握手:
VALID先到 READY先到AXI的握手信号分为VALID和READY,其中VALID信号是传输段发送的,READY是接收端发送的。只有在VALID和READY同时有效的时候,信息才会被传递。VALID有效代表发送以准备好,READY有效代表接收已准备好,当其中一方有效,另一方无效时,有效的一方信号需要保持,等待另一方信号的有效。
之前说过AXI中的主从身份不可换,由于写通道的地址与数据都是由主机传输给从机,所以主机无法判断发送过去的数据是否可以被从机顺利接收,或是发起的写命令是否合法,因此需要一个写响应信号来让从机可以高速主机写地址是否合法,写数据是否被成功写入。
所以写响应通道要发送BVALID和BRESP。其中BVALID要与主机发过来的BREADY进行握手代表写数据结束。BRESP为两个bit的信号,作为写数据是否成功的回应。
BRESP | 含义 | |
---|---|---|
00 | OK | 普通模式数据传输成功 |
01 | EXOK | 独占模式数据传输成功 |
10 | SLVERR | 从机收到命令/地址,但是不支持这个命令/地址,返回错误 |
11 | DECERR | 互联模式下,解码后的地址没有对应的从机。(interconnect模式下) |
讲一下什么叫独占模式,独占模式指的是独占从机的某一地址空间,向其中写入数据并读出数据,如果这个独占地址在写入和读出过程中被其他主机访问,或者从机不支持独占模式,则报错。
AXI主要有三种模式,AXI_lite,AXI_stream和AXI_full。其中stream面向流的传输,不涉及到内存地址,适合摄像头这种无地址数据流。AXI_full则是完整的AXI协议,支持三种地址模式(FIXED、INCR、WRAP)。这些会在后续的文章中详细讲解与实现。
AXI_lite是轻量级的AXI协议,它每次传输的数据和地址的突发长度只有1,也就是burst=1。常用与较少数据量的存储映射通信,比如配置寄存器。
下面把AXI_lite的所有信号罗列出来:
写地址 | AW_ADDR | ADDR_WIDTH-1 :0 | |
---|---|---|---|
AW_VALID | |||
AW_READY | |||
AW_PORT | 1 : 0 | 写通道保护信号 | |
写数据 | W_DATA | DATA_WIDTH-1 : 0 | |
W_STRB | (DATA_WIDTH/8)-1 : 0 | 写字节有效位控制 | |
W_VALID | |||
W_READY | |||
写回应 | B_RESP | 1:0 | |
B_VALID | |||
B_READY | |||
读地址 | AR_ADDR | ADDR_WIDTH-1 : 0 | |
AR_VALID | |||
AR_READY | |||
AR_PORT | 1:0 | 读通道保护信号 | |
读数据 | R_DATA | ||
R_RESP | 1:0 | ||
R_VALID | |||
R_READY |
介绍一下AW_PORT和AR_PORT,是写/读通道保护信号,[0]表示正常或特权,[1]表示安全或非安全,[2]表示指令或数据。这个信号需要用户在使用中根据需要自行配置,我们在本次实现的AXI_lite中不考虑这个信号。
W_STRB信号是写字节有效位控制,在高速通信协议中,都会有这个信号来代表传输的字节是否有效,PCIE、GTX等协议中都有,1代表有效,0代表无效。如果数据为32位,则STRB = 32/8 = 4位。
内部握手信号的控制
在普通模式下,内部的各种握手信号的产生并不是随便产生的,为了保证安全,AXI协议规定了各种信号的依赖关系:
写模式 读模式注意!!!图中的单箭头,头部的信号可以取决与尾部信号来产生;双箭头,在判断尾部信号的时候必须先判断头部信号。
举例:在判断单箭头头部AWREADY信号时,根据单箭头尾部AWVALID和WVALID信号来判断
把WVALID和AWVALID作为AWREADY有效的条件在判断双箭头尾部WREADY信号时,先判断双箭头头部信号BVALID
把BVALID控制的清零放在高顺位在AXI4协议中的依赖关系要更加严格,在写模式中又添加了一些依赖关系。
slave端代码实现
端口和内部信号声明
module AXI_slave #
(
parameter integer ADDR_WIDTH = 4,
parameter integer DATA_WIDTH = 32
)
(
//全局变量
input CLK,
input RESETN,
// 写地址
input [ADDR_WIDTH-1 : 0] AW_ADDR,
input AW_VALID,
output reg AW_READY,
// 写数据
input [DATA_WIDTH-1 : 0] W_DATA,
input [(DATA_WIDTH/8)-1 : 0] W_STRB,
input W_VALID,
output reg W_READY,
// 写响应.
output reg [1 : 0] B_RESP,
output reg B_VALID,
input B_READY,
// 写地址
input [ADDR_WIDTH-1 : 0] AR_ADDR,
input AR_VALID,
output reg AR_READY,
// 写数据
output reg [DATA_WIDTH-1 : 0] R_DATA,
output reg [1 : 0] R_RESP,
output reg R_VALID,
input R_READY
);
integer i;
reg [ADDR_WIDTH-1 : 0] waddr;
reg [ADDR_WIDTH-1 : 0] raddr;
reg [DATA_WIDTH-1 : 0] W_DATA_reg0;//内部寄存器
reg [DATA_WIDTH-1 : 0] W_DATA_reg1;//内部寄存器
reg [DATA_WIDTH-1 : 0] W_DATA_reg2;//内部寄存器
reg [DATA_WIDTH-1 : 0] W_DATA_reg3;//内部寄存器
reg [DATA_WIDTH-1 : 0] R_DATA_reg;
localparam LSB = DATA_WIDTH/32 + 1;//用于配合字节有效位,32为=2,64为=3
我们选择32位数据来做演示。
我们在slave配置四个内部寄存器,由于在各种存储系统以及地址系统中,地址的最小位都是按byte来设定的。所以32位数据有4个byte,及addr的后两位用来选择4个byte,前两位用于选择内部哪个寄存器,因此addr设置为4位。
//写地址通道
//=====================================================================
//写地址AW_READY的产生
always@(posedge CLK)
if(~RESETN)
AW_READY <= 1'b0;
else if(B_VALID && B_READY)//master 中AWVALID的清零信号应该也是它,同步清零
AW_READY <= 1'b0;
else if(~AW_READY && AW_VALID && W_VALID)
AW_READY <= 1'b1;
else
AW_READY <= 1'b0;
//写地址缓存
always@(posedge CLK)
if(~RESETN)
waddr <= 'd0;
else if(~AW_READY && AW_VALID && W_VALID)//AWREADY拉高的同一拍内的地址被存下来
waddr <= AW_ADDR;
//写数据通道
//======================================================================
always@(posedge CLK)
if(~RESETN)
W_READY <= 1'b0;
else if(B_VALID && B_READY)//master 中WVALID的清零信号应该也是它,同步清零
W_READY <= 1'b0;
else if(~W_READY && AW_VALID && W_VALID) //地址和数据要同步,所以都有效时才能ready
W_READY <= 1'b1;
else
W_READY <= 1'b0;
always@(posedge CLK)
if(~RESETN) begin
W_DATA_reg0 <= 'd0;
W_DATA_reg1 <= 'd0;
W_DATA_reg2 <= 'd0;
W_DATA_reg3 <= 'd0; end
else if(AW_VALID && W_VALID && AW_READY && W_READY) begin
case(waddr[ADDR_WIDTH - 1 : LSB])
2'b00:begin
for(i=0;i<DATA_WIDTH/8;i=i+1) begin
if(W_STRB[i]) //字节有效位
W_DATA_reg0[i*8 +: 8] <= W_DATA[i*8 +: 8]; end
end
2'b01:begin
for(i=0;i<DATA_WIDTH/8;i=i+1) begin
if(W_STRB[i]) //字节有效位
W_DATA_reg1[i*8 +: 8] <= W_DATA[i*8 +: 8]; end
end
2'b10:begin
for(i=0;i<DATA_WIDTH/8;i=i+1) begin
if(W_STRB[i]) //字节有效位
W_DATA_reg2[i*8 +: 8] <= W_DATA[i*8 +: 8]; end
end
2'b11:begin
for(i=0;i<DATA_WIDTH/8;i=i+1) begin
if(W_STRB[i]) //字节有效位
W_DATA_reg3[i*8 +: 8] <= W_DATA[i*8 +: 8]; end
end
default:begin
W_DATA_reg0 <= W_DATA_reg0;
W_DATA_reg1 <= W_DATA_reg1;
W_DATA_reg2 <= W_DATA_reg2;
W_DATA_reg3 <= W_DATA_reg3; end
endcase
end
AXI协议规定可以使用异步复位,但是复位信号要在时钟上升沿来临前释放。所以我们这里直接使用同步复位。
在写数据处要注意, +:8 符号指的是从当前位向高数8位,-:8 为从当前位向低数8位。
例:我们要表示[ 7:0 ],可以用 [ 0 +: 8 ] 或者 [ 7 -: 8 ] 。
我们这里把32位数据分成4个byte挨个写入,是为了配合字节有效位W_STRB,有效则写入,无效则不写入。
上述的case看起来比较清晰,也可以合并为
assign j = waddr[ADDR_WIDTH - 1 : LSB]
for(i=0;i<DATA_WIDTH/8;i=i+1) begin
if(W_STRB[i]) //字节有效位
data_regj[i*8 +: 8] <= W_DATA[i*8 +: 8];
end
其他
//写响应通道
//======================================================================
always@(posedge CLK)
if(~RESETN) begin
B_VALID <= 1'b0;
B_RESP <= 2'b00; end//00:OK ;01:EXOK;10:SLVERR;11:DECERR
else begin
if(~B_VALID && AW_VALID && W_VALID && AW_READY && W_READY)begin
B_VALID <= 1'b1;
B_RESP <= 2'b00; end
else if(B_READY && B_VALID) //地址和数据要同步,所以都有效时才能ready
B_VALID <= 1'b0;
end
//读地址通道
//======================================================================
always@(posedge CLK)
if(~RESETN)
AR_READY <= 1'b0;
else if(~AR_READY && AR_VALID && R_VALID)
AR_READY <= 1'b1;
else
AR_READY <= 1'b0;
always@(posedge CLK)
if(~RESETN)
raddr <= 'd0;
else if(~AR_READY && AR_VALID)
raddr <= AR_ADDR;
//读数据通道
//======================================================================
always@(posedge CLK)
if(~RESETN) begin
R_VALID <= 1'b0;
R_RESP <= 2'b00; end
else if(R_READY && R_VALID)begin
R_VALID <= 1'b0;
R_RESP <= 2'b00; end
else if(~R_VALID && AR_VALID) begin
R_VALID <= 1'b1;
R_RESP <= 2'b00; end
always@(*)
if(~R_READY && R_VALID && AR_VALID) begin //addr到了的时钟的数据立刻读取
case(raddr[ADDR_WIDTH - 1 : LSB])
2'b00:R_DATA_reg = W_DATA_reg0;
2'b01:R_DATA_reg = W_DATA_reg1;
2'b10:R_DATA_reg = W_DATA_reg2;
2'b11:R_DATA_reg = W_DATA_reg3;
default R_DATA_reg = 'd0;
endcase
end
//简化写法
//assign j = raddr[ADDR_WIDTH - 1 : LSB]
//R_DATA_reg = W_DATA_regj
always@(posedge CLK)
if(~RESETN)
R_DATA <= 'd0;
else if(~R_READY && R_VALID && AR_VALID)
R_DATA <= R_DATA_reg;
else
R_DATA <= 'd0;
endmodule
注意在读数据时,当R_VALID和R_READY握手时数据同步出现,因此要用组合逻辑提前一拍读取地址。
在我们的设计中错误信号R_RESP始终为00,及代表数据一直有效。在AXI标准协议中规定R_RESP会在以下几种情况下报错:
- FIFO/CACHE溢出或不足情况
- 尝试了不支持的传输大小
- 试图对只读位置进行写访问
- 从设备中的条件超时
- 试图访问不存在的寄存器地址
- 试图访问禁用或断电的功能
至此,AXI_lite_slave就设计完成了,下一篇文章我们继续设计AXIAXI_lite_master。