之前在Xilinx的FPGA上做设计,只是知道AXI接口,但是没有详细地了解过这个协议,现在需要用AXI总线做整个系统的设计了,所以还是有必要详细了解一下这个协议。

参考资料

IHI0022H: AMBA AXI and ACE Protocol Specification
IHI0051A: AMBA 4 AXI4-Stream Protocol Specification
  上面这两篇都是ARM官方的文档,都可以在ARM的官网找到,这两个文档对AXI4、AXI4-Lite和AXI4-Stream做了详细的介绍。除了这两个文档,再就是Xilinx的几个IP的文档了,有AXI Interconnect、SmartConnect、AXI4-Stream Interconnect,这三个只需要大致看一下就行。
  网上也有很多介绍AXI协议的帖子,也都写得非常好,而且也比较详细。本文的内容会比较少,因为都只是我在阅读过程中做的一些笔记,如果真心想了解这些内容,强烈建议要用心阅读上面两篇Specification!

AXI 协议

  • 特点
    • 地址、控制和数据分开控制
    • 支持非对齐的数据传输
    • 支持突发传输
    • 读写通道分开
    • 支持乱序传输
  • AXI 结构
    • 有AR、R、AW、W、B 五个独立通道,分别对应读地址、读数据、写地址、写数据、写响应。
    • 地址通道带有控制信息
    • B是响应通道,用来返回写响应
    • R通道不仅有读的数据也有响应信息
    • 总线互连的时候,公用地址和数据总线;或者公用地址线,多条数据总线;再或者有多条地址和数据总线。
  • 全局信号
    • 时钟,上升沿采样;复位低电平有效
  • 时钟与复位
    • 时钟 上升沿有效,接口之间的信号没有组合逻辑通路
    • 复位可以是异步复位但一定要同步释放
    • 复位的时候 manager端的AW、AR、W通道的VALID信号都要是low;subordinate端的R、B通道的VALID信号都要是low
    • VALID只有在复位撤销后的第一个时钟上升沿才能变为有效
    • 同一通道的VALID信号不能依赖READY信号
    • VALID有效之后只有出现READY才能撤销
    • destination可以等待VALID出现再产生READY
    • READY出现之后,可以在VALID出现之前撤销
  • 通道信号
    • 建议AWREADY和ARREADY默认为高电平,这样可以比较快
    • 推荐inactive的byte lanes的WDATA RDATA是低电平
  • 通道之间的关系
    • 写响应要在写数据的LAST信号有效之后产生
    • 读数据要在读地址送给subordinate之后送出数据
    • 协议里面之规定了这两组关系,也就是说可能会有比如写数据比写地址先到或者后到的情况,这些都是允许的。
  • 事务结构
    • AXI协议是基于突发(burst)传输的,突发传输由subordinate计算地址,因为manager只会发一个起始地址,后面字节的地址得靠subordinate来计算。
    • 不能超过4kB地址边界
    • AxBurst定义了突发的类型,突发不能终止
    • 有三类突发,INCR,FIXED,WRAP。INCR是地址自增的,FIXED是地址固定的,WRAP是会地址回滚的,它跟INCR类似,但就是传输过程中如果地址到了上边界,它就会让地址再回到下边界。
    • AxLEN表示突发的长度,就是一次burst有几次VALID和READY的握手;AxSize表示一次传输的字节个数,就是一次VALID和READY的握手传输多少个字节。
    • WRAP的突发长度不能超过16,INCR虽然可以超过,但实际上是转换成多个16长度的突发
    • AxSize用3bit表示,最多128字节
  • Regular Transaction 常规的事务
    • Regular_Transactions_Only 这个属性如果是TRUE,那么就只支持常规事务
    • AxLen 1,2,4,8,16
    • if AxLen > 1 , AxSize = data bus width
    • AxBurst = INCR or WRAP,没有FIXED
    • 地址是对齐的
  • 数据读写的结构
    • 实现混合的大小端数据的读写和非对齐传输(因为有字节选通,所以数据是按照字节为单位传的,没有大小端的问题,可以做到大小端混合的数据传输)
    • byte strobe 在VALID信号为低的时候保持低电平或者维持之前的信号不变
    • narrow transfers,就是用strobe选通。INCR和WRAP模式,不同传输次数之间的字节位置不同;FIXED模式下,字节位置固定。
    • 非对齐传输的两种方式:
      • 直接给非对齐的地址,从这个非对齐地址开始到对齐的边界算作一次传输
      • 用更低的对齐的地址,然后用strobe选通
    • 一次burst对应一次响应,有四种响应OKAY、EXOKAY、SLVERR、DECERR
  • AxCACHE
  • subordinate设备分为peripheral和memory
  • peripheral的信号可以简化,因为它实现的功能少
  • modifiable:
    • 一个事务可以拆分成多个事务
    • 多个事务可以合并成一个事务
    • 一个读事务可以读比需要的更多的数据
    • 一个写事务可以访问比需要的更多的地址空间,通过strobe选通
    • 在另外生成的事务中,AxADDR AxSIZE AxLEN AxBUREST可以被改写
    • AxLOCK和AxPROT不能被改写
  • 有了ID就可以有多个Outstanding的事务
  • RID和ARID对应
  • subordinate要对RID排序,这个排序的深度在设计subordinate的时候给定,manager没办法获得这个深度
  • interconnect会在每个master的ID前面加上master的ID,所以不用担心不同master的ID重复
  • exclusive访问:不支持其他master的访问

AXI4-Lite

  • 所有事务的AxLEN都是1
  • size都是数据位宽32/64
  • 不支持exclusive访问
  • non-modifiable
  • 虽然支持多个Outstanding的事务,但是subordinate可以合理地利用握手信号来限制这个

AXI4-Stream

  • 三种byte类型
    • Data byte:数据
    • Position byte: placeholder,不包含数据
    • Null byte:不包含任何数据和位置信息
  • 术语定义:
    • Transfer:一次VALID和READY的握手
    • Packet:多个Transfer
    • Frame:多个Packet
  • 四种数据流
    • Byte stream:data byte和null byte间隔
    • 只有data byte的对齐传输
    • 连续的data byte,在起始和结尾处补上position byte构成对齐传输
    • Sparse stream:data byte和position byte间隔
  • 信号
    • TSTRB和TKEEP:TSTRB区分data和position byte,TKEEP无效的是null byte
    • 时钟和复位同AXI4
    • 推荐tuser的bit数是字节个数的整数倍
  • 和AXI4的不同
    • AXI4不允许interleaving
    • AXI4-Stream没有最大的burst长度
    • AXI4-Stream的数据位宽任意
    • AXI4-Stream包含TID和TDEST指示源和目的信息
    • AXI4-Stream对TUSER的定义更具有操作性
    • AXI4-Stream多了TKEEP信号

自定义AXI4-Lite外设

  Vivado的Tool标签下有一个选项就是“Create and Package New IPs”,点进去之后可以选择创建一个新的AXI4外设,然后给这个IP起个名字,可以先放到IP库里,然后在Block Design中调用它,再右键“Edit in IP Packager”。

  点击“Edit in IP Packager”之后,可以看到这个IP的顶层文件和一个它实例化的AXI4-Lite接口的模块。我们可以在这个模块中加入自己的设计,这里面一开始生成的代码大部分都不需要去改动,而且注释也都写得很清楚,每个always块都有注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Implement axi_awready generation
// axi_awready is asserted for one S_AXI_ACLK clock cycle when both
// S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_awready is
// de-asserted when reset is low.
always @( posedge S_AXI_ACLK ) begin
if ( S_AXI_ARESETN == 1'b0 ) begin
axi_awready <= 1'b0;
aw_en <= 1'b1;
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) begin
aw_en <= 1'b1;
axi_awready <= 1'b0;
end
else begin
axi_awready <= 1'b0;
end
end
end

  第一个always块是对awready和aw_en进行控制,注释也写了它“excepts no outstanding transactions”,也就是它只能逐个事务进行处理,只有在响应完成之后才能接收新的写数据。wready和awready是一样的,都会受到aw_en的控制,只有aw_en为高电平的时候,这两个ready才有可能置一;然后aw_en又只会在响应完成之后置高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Implement memory mapped register select and write logic generation
// The write data is accepted and written to memory mapped registers when
// axi_awready, S_AXI_AWVALID, axi_wready and S_AXI_WVALID are asserted. Write strobes are used to
// select byte enables of slave registers while writing.
// These registers are cleared when reset (active low) is applied.
// Slave register write enable is asserted when valid address and data are available
// and the slave is ready to accept the write address and write data.

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
else if(slv_reg0[C_EN_BIT]) begin
slv_reg0[C_EN_BIT] <= 1'b0;
end
end
end

  我给这个AXI4-Lite接口的外设分配了4个寄存器,上面这段代码是在AW和W通道都握手的情况下通过W通道往寄存器写数据,case语句选择寄存器,再根据WSTRB选择对应的字节。
  概括一下剩下的代码实现的功能。写响应只实现了响应OKAY,没有其他类型的响应。在ARVALID有效之后给出ARREADY,ARREADY只响应一个周期,同时锁存住读地址。在发现AR通道握手之后给出一个周期的RVALID信号。slv_reg_rden是AR通道握手,这个信号有效比RVALID提前一个周期,用这个信号作为使能,打一拍之后把数据送到RDATA上,正好RDATA和RVALID一起有效。
  用户逻辑设计的时候,可能只需要改上面的写寄存器的部分。根据寄存器中的值设计相应的逻辑对上面这部分代码没有影响。用户也可以增加自己的接口和参数定义,修改完之后记得改顶层的调用。