AXI 总线(一)通道握手-AXI-Lite
文章目录
关于本系列教程
需要注意的是,AXI协议是ARM公司的知识产权,此分享仅为交流学习用途。本人也属于边学边总结,如有纰漏也请大家一同指出交流交流。
其中,由于官方文档实在写得太好,这里原理部分主要参考官方文档(甚至有一些是直接翻译)和赛灵思的官方文档。还有一些我觉得把AXI介绍得很不错的博客文章,可见于参考资料。当然也推荐大家去看官方文档,但是文档稍微有点繁杂,而且我相信大部分人学AXI总线是为了应用,详细点就是把自己写的模块接入到AXI总线中。所以本教程有以下特点:
- 会着重于总线中的正常读写过程和一些影响读写的关键信号和时序,而不会太注意一些AXI发展中的其他特性(也会介绍,但是更多可能会给出在文档的什么章节)。
- 注重代码Coding,在介绍协议的时候,会配合讲Xilinx给的示例IP,如本文中讲解了AXI-Lite协议的代码。在具体理解的时候,也会单独做一些实际的小项目和仿真。
废话有点多了,下面直接开始。
什么是AXI协议
本节主要参考自赛灵思官方博客:AXI Basics 1 - Introduction to AXI,可见于参考资料
AXI指的是( Advanced eXtensible Interface) 是ARM提出的AMBA(Advanced Micro_controller Bus Architecture)协议中的一部分。
其中又分三项:
- AXI4 (Full AXI4): 用于高性能内存映射需求。可以理解为满血版AXI
- AXI4-Lite:用于简单、低吞吐量的内存映射通信(例如,与控制寄存器和状态寄存器之间的通信)。可以理解为青春版AXI。
- AXI4-Stream:用于高速流数据(实际上,他并没有内存映射能力)而这往往又是摄像头等高速数据流的输入格式,所以后面会单独讲这个协议。
AXI读写通道
AXI协议中,有5条读写通道:
- 2条有关读的通道
- read address 读地址
- read data 读数据(与读响应合并)
- 3 条有关写的通道
- write address 写地址
- write data 写数据
- write response 写响应
之所以把他们称为“通道”,是因为在每一个通道处都会有一组独立的valid和ready信号,只有在时钟上升沿时,且这两个信号都为高的时候才会进行信息的传输。
那么,现在可以带着**为什么读响应和读数据能合并,写通道不能?和这valid和ready信号究竟要怎么握手?**这两个问题进行下面的介绍:
AXI读过程
在AXI中发起一次读需要在2个读通道上进行传输:
- 首先,从读地址(AR)通道,由主机告诉从机从什么地址开始读,读多少个数据(burst)
- 这个地址的数据在读数据通道上从从机传输到主机。
AXI写过程
在AXI中发起一次写需要在3个写通道上进行传输:
- 首先,从写地址(AW)通道,由主机告诉从机从什么地址开始写,写多少个数据(burst)
- 这个地址的数据在写数据通道上从主机传输到从机。
- 最后,传输结束时在写响应通道上的发送写响应,从从机发送到主机,以指示传输是否成功。
所以在这里我们就可以看到,在AXI读过程中,由于读数据的方向是从机到主机,所以主机一方面可以通过读的数量来看传输是否成功,另一方面,由方向上,读响应也可以随着数据发给主机。但是写通道的方向是主机到从机,所以必须多一条写响应通道。
单一通道的握手
如无特殊说明,valid指发送端,ready指接收端。
在通道中,需要valid和ready同时拉高才能传输信息,总体原则是谁没到就等谁。所以由valid和ready置高的先后顺序,有三种情况:
VALID 信号先到
此时,valid信号已经有效,所以信息线上的数据也是有效的了,但是接收端说我有点别的事要干,你先等会,即T2时刻才刚拉高,所以信息传输就发生在T2-T3,在T3时刻,发送端发现接收端已经把数据读走了便把valid拉低了。
但实际上,这个图只画了burst_length=1的时候,实际上如果burst_length不是1的话,这里两个标志线还会继续维持下去知道数据传输结束,这也是下面AXI-Lite代码中所没能讲到的。
READY信号先到
此时,接收端说我已经准备好了,你赶紧来。等发送端缓过气来,即T2时刻才刚拉高,所以信息传输就发生在T2-T3,发送端感知到接收端已经把数据读走了便把valid拉低了。
这里也需要注意,这个图也只画了burst_length=1的时候。
READY和VALID同时到达
同时到达就很开心了,等到下一个时钟上升沿 T2,传输就完成了。
原则性问题
这里还有一个最后的问题,**到底应该谁来发起一次读写?**毫无疑问,那应该是发送端。
所以在握手的过程中,发送端的valid信号不能依赖接收端的ready信号,不然有可能两边都不拉高,直接死锁
而且,一旦valid被拉高了,直到完成握手只能一直置位,不能左右横跳,即接收端不需要考虑发送方撤销传输的可能。
但是,ready信号的状态可以依赖于valid信号,协议没有规定 READY 信号默认状态,即未进行传输时的电平状态。
除了上面这些比较关键的之外,还有几个比较重要的小细节:
- 在写入中,写入响应必须始终紧跟它所属的最后一个写入传输
- 读取的数据必须始终跟在数据所关联的地址之后
- slave必须等待ARVALID和ARREADY都被断言,然后才拉高RVALID,以表明有效的数据可用
- 协议建议 AW/AR READY 信号(这里 AW/AR 指的是读写地址通道的 READY 信号)的默认电平为高电平。
AXI-Lite总线实现解析
下面,我们会解析xilinx官方的AXI-Lite实现,从代码的角度解析每一通道的握手过程。
生成一个AXI(-Lite)外设
在Vivado中,可以通过生成ip的方式来建立AXI协议的外设,具体操作如下:
-
Vivado中,点Tools-> Create and Package New IP:
-
简介面Next掉,然后选择创建AXI4 外设,Next:
-
进入外设细节面,填好信息,Next:
-
进入到添加接口界面,默认就是AXI-Lite了,可以什么都不改,直接下一步:
-
再最后一步中,直接选择Edit IP,进入修改界面:
-
如果以后再想修改的话,可以通过IP Catalog,找到创建的IP,右键选择Edit IP就可以了.
-
整体端口信号解析
进入编辑工程后,找到xxxx_S00_AXI.v找到接口实现模块,就是接下来的重点了。
首先,我们先梳理一下模块中的输入输出信号,为了更好的识别出各个通道,我这里已经帮大家用不同的颜色标注好了:
可见,在每一条通道中基本会有主要信息,辅助信息?,和两个握手信号。其中,辅助信息这个概念是我瞎掰的,主要为了后面好讲每一个通道的独特之处。下面,先从写过程开始介绍:
A(ddr)W(rite)通道
端口说明如下:
端口名 | I/O | 说明 |
---|---|---|
S_AXI_AWADDR | input | Write address (issued by master, accepted by Slave) |
S_AXI_AWPROT | input | Write channel Protection type. This signal indicates the privilege and security level of the transaction, and whether the transaction is a data access or an instruction access. |
S_AXI_AWVALID | input | Write address valid. This signal indicates that the master signaling valid write address and control information. |
S_AXI_AWREADY | output | Write address ready. This signal indicates that the slave is ready to accept an address and associated control signals. |
握手S_AXI_AWREADY,S_AXI_AWVALID
握手信号,在这里我们可以参考官方文档的握手流程(取了AXI 3的,对AW通道一致):
先扫一眼代码,注意en和ready的关系:
// I/O Connections assignments
assign S_AXI_AWREADY = axi_awready;
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_awready <= 1'b0; // 协议ready
aw_en <= 1'b1; // 模块ready
end
else
begin
if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
begin
// 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) // 写回应结束,模块内恢复ready
begin
axi_awready <= 1'b0;
aw_en <= 1'b1;
end
else
begin
axi_awready <= 1'b0;
end
end
end
可见代码基本符合流程图。可以看到en和ready的关系如下,后面会有大量类似的写法:
- en=1,axi_awready=0,可以通信/结束通信,等待valid
- en=0,axi_awready=1,建立通信,拉高ready
地址锁存 AW_addr
注意,这里做的是latch:
而且,注意对比判断逻辑块:
- ~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en
和上面的ready是同时判断的,所以在ready给出的同时,地址已经锁存好了.
// Implement axi_awaddr latching
// This process is used to latch the address when both
// S_AXI_AWVALID and S_AXI_WVALID are valid.
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
// Write Address latching
axi_awaddr <= S_AXI_AWADDR;
end
end
end
S_AXI_AWPROT
辅助信息?稍微检索可以得知,在xilinx中并没有实现这一项,翻看ARM官方文档有:
AXI provides access permissions signals that can be used to protect against illegal transactions:
- ARPROT[2:0] defines the access permissions for read accesses.
- AWPROT[2:0] defines the access permissions for write accesses.
The term AxPROT refers collectively to the ARPROT and AWPROT signals.
即这个信号是用来做权限管理的,具体定义如下:
由于这是入门篇,这里就先挖坑不讲了,有兴趣的小伙伴可以看看手册的A4.7节。这里还需要注意到,读地址通道的辅助信息也是这个噢。
W(rite)写通道
端口如下(为了篇幅不过长,这里就省了描述了):
- 主要信号S_AXI_WDATA
- 辅助信号S_AXI_WSTRB,做掩码
- 握手信号S_AXI_WVALID,S_AXI_WREADY
S_AXI_WVALID,S_AXI_WREADY握手信号
同样地,我们看官方文档的握手流程(取了AXI 3的,对W通道一致):
// I/O Connections assignments
assign S_AXI_WREADY = 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
// slave is ready to accept write data when
// there is a valid write address and write data
// on the write address and data bus. This design
// expects no outstanding transactions.
axi_wready <= 1'b1;
end
else
begin
axi_wready <= 1'b0;
end
end
end
可见,其中也运用了上面AW通道中类似ready和en的写法。
S_AXI_WDATA数据写入
这里可以先看简单版(无S_AXI_WSTRB),比较容易懂,完整代码在后面:
assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;
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:
slv_reg0 <= S_AXI_WDATA;
2'h1:
slv_reg1 <= S_AXI_WDATA;
2'h2:
slv_reg2 <= S_AXI_WDATA;
2'h3:
slv_reg3 <= S_AXI_WDATA;
default : begin
slv_reg0 <= slv_reg0;
slv_reg1 <= slv_reg1;
slv_reg2 <= slv_reg2;
slv_reg3 <= slv_reg3;
end
endcase
end
end
end
AXI地址解析
可以看到,这里基本就是在通道建立后,做一个选择器,那问题来了,为什么是这两位呢?:
axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]
首先,在AXI总线中,一个地址指一个字节
回顾代码,关于地址部分:
// Example-specific design signals
// local parameter for addressing 32 bit / 64 bit C_S_AXI_DATA_WIDTH
// ADDR_LSB is used for addressing 32/64 bit registers/memories
// ADDR_LSB = 2 for 32 bits (n downto 2)
// ADDR_LSB = 3 for 64 bits (n downto 3)
localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1;
localparam integer OPT_MEM_ADDR_BITS = 1;
所以针对ADDR_LSB,可以看到:
- ADDR_LSB = 0,每次+1,地址+1,跨1字节8位
- ADDR_LSB = 1,每次+1,地址+2,跨1字节16位
- ADDR_LSB = 2,每次+1,地址+4,跨1字节32位
- ADDR_LSB = 3,每次+1,地址+8,跨1字节64位
而对应OPT_MEM_ADDR_BITS,因为我们只有4个32位的寄存器,只要两条地址线,所以OPT_MEM_ADDR_BITS=1。
而写入过程完整代码如下(不用看,下面有S_AXI_WSTRB解析):
assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;
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
// 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
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
// Respective byte enables are asserted as per write strobes
// Slave register 2
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
// Respective byte enables are asserted as per write strobes
// Slave register 3
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
S_AXI_WSTRB
我们上面说到,在AXI总线中,一个地址指一个字节,但是如果并不是32位都是有效的怎么办呢?
如果我寄存器只有8位或者16位,但是AXI-Lite只能是32位或者64位,那咋办呢?
所以在写通道中就有一个类似 掩码的功能,用来指示什么数据是有效的,具体解析见下:
实际的代码逻辑其实已经在上面了,以slv_reg0为例:
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
那么我们代入C_S_AXI_DATA_WIDTH = 32,并且拆开for循环:
// slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
// byte_index = 0
if ( S_AXI_WSTRB[0] == 1 ) begin
// slv_reg0[(0 +: 8] <= S_AXI_WDATA[(0*8) +: 8];
slv_reg0[7:0] <= S_AXI_WDATA[7:0];
end
// byte_index = 1
if ( S_AXI_WSTRB[1] == 1 ) begin
// slv_reg0[(8 +: 8] <= S_AXI_WDATA[(1*8) +: 8];
slv_reg0[15:8] <= S_AXI_WDATA[15:8];
end
// byte_index = 2
if ( S_AXI_WSTRB[2] == 1 ) begin
// slv_reg0[(16 +: 8] <= S_AXI_WDATA[(2*8) +: 8];
slv_reg0[23:16] <= S_AXI_WDATA[23:16];
end
// byte_index = 3
if ( S_AXI_WSTRB[3] == 1 ) begin
// slv_reg0[(24 +: 8] <= S_AXI_WDATA[(3*8) +: 8];
slv_reg0[31:24] <= S_AXI_WDATA[31:24];
end
我觉得已经详细到不需要解释了,关注+:和-:的用法,可以查看本人博客一些Verilog的小东西(2)
写响应通道
端口信号:
Port name | Direction | Type |
---|---|---|
S_AXI_BRESP | output | wire [1 : 0] |
S_AXI_BVALID | output | wire |
S_AXI_BREADY | input | wire |
S_AXI_BVALID,S_AXI_BRESP握手信号及其响应
此时需要注意,AXI3和AXI4对于写响应通道的握手流程是不一致的,这里取最新的AXI4:
代码其实还是换汤不换药:
// I/O Connections assignments
assign S_AXI_BRESP = axi_bresp;
assign S_AXI_BVALID = axi_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
我们可以来看看,响应信号的具体含义:
这里由于我们简单介绍一下含义就算了,具体为什么会有这个机制可以看看手册A3.4.4和A7.2.这篇中我们主要介绍通道的握手通信,这里就不展开了。
那么接下来,就到读过程了
A(ddr)R(ead)读地址通道
握手S_AXI_ARREADY,地址写入S_AXI_ARADDR
读地址通道,到这里了,大家应该看到代码基本都是类似的,见于文档:
// I/O Connections assignments
assign S_AXI_ARREADY = axi_arready;
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
// indicates that the slave has acceped the valid read address
axi_arready <= 1'b1;
// Read address latching
axi_araddr <= S_AXI_ARADDR;
end
else
begin
axi_arready <= 1'b0;
end
end
end
辅助信息 S_AXI_ARPROT
见于S_AXI_AWPROT,一致的,这里也不需要给。
读R(ead)通道&&读响应通道
这里要注意到,读通道和读响应通道是合并了的,所以读通道的辅助信息理所当然就是响应信号啦。
握手及响应信号S_AXI_RVALID,S_AXI_RRESP
握手过程见官方文档:
// I/O Connections assignments
assign S_AXI_RRESP = axi_rresp;
assign S_AXI_RVALID = axi_rvalid;
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
// Valid read data is available at the read data bus
axi_rvalid <= 1'b1;
axi_rresp <= 2'b0; // 'OKAY' response
end
else if (axi_rvalid && S_AXI_RREADY)
begin
// Read data is accepted by the master
axi_rvalid <= 1'b0;
end
end
end
基本上都是一致的流程了,注意S_AXI_RRESP响应与写响应通道中的S_AXI_BRESP含义是一致的。
S_AXI_RDATA数据交互
这里也是基本的数据选择器,根据地址给数据就完事了。
// I/O Connections assignments
assign S_AXI_RDATA = axi_rdata;
// Implement memory mapped register select and read logic generation
// Slave register read enable is asserted when valid address is available
// and the slave is ready to accept the read address.
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
// Output register or memory read data
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_rdata <= 0;
end
else
begin
// When there is a valid read address (S_AXI_ARVALID) with
// acceptance of read address by the slave (axi_arready),
// output the read dada
if (slv_reg_rden)
begin
axi_rdata <= reg_data_out; // register read data
end
end
end
这里的地址解析和上文中的AXI地址解析也是一致的。
小结
主要介绍了一下几点:
- AXI大概是啥,通道大致有啥
- AXI各个通道中的握手过程
- AXI-Lite的接口代码分析
下一节中,我们会仿真,以及做一些结合ZYNQ-PS端的小应用,看看怎么仿和怎么用。后面介绍完完整AXI后再讲怎么接入。
参考资料
深入 AXI4 总线(O)专栏目录与资料集合–知乎ligibbs
也欢迎关注我的个人公众号,小何的芯像石头: