![e74e7b2d6b820da9ab97445debce01a6.png](https://i-blog.csdnimg.cn/blog_migrate/c4c0e8087c666e6ec9626eb7135921ca.jpeg)
本系列我想深入探寻 AXI4 总线。不过事情总是这样,不能我说想深入就深入。当前我对 AXI总线的理解尚谈不上深入。但我希望通过一系列文章,让读者能和我一起深入探寻 AXI4。
在本系列先前的文章中,我们首先通过协议 specification 了解协议的接口与机制,并通过操作一个 AXI 接口的 RAM IP 进行了一番实战。
从协议机制上来说,协议的制定者权衡协议的使用场景,主从机接口,性能等各方面的需求与限制以及实现的难度制定了这些规则。从笔者个人的经验来说,对于一个协议:
把手册打印出来 -> 翻烂 -> 没人比我更懂这个协议了(误)
翻烂可不是了解一个协议最有效的手段,最重要的还是针对协议的实践。通过操作 AXI 接口的 IP 可以熟悉协议的接口。而通过实现一个协议的接口模块,可以更深入地了解协议。本文将借助 Xilinx 提供的 AXI 接口代码,实现一个 AXI-lite 接口模块,了解协议接口机制的实现。
Vivado 提供的接口代码
在 Vivado 中使用 IP 编辑器创建一个 AXI 接口的 IP,就可以获取到 Xilinx 在创建模板中提供的接口代码,这里简单地纪录下创建过程。
首先在 Tools 中选择 创建打包新 IP 菜单,选择建立一个 AXI4 外设。
![fb1b08b4b82033c50b44501292badb3d.png](https://i-blog.csdnimg.cn/blog_migrate/0e658b35adf1fab4fe28e09c782e4865.jpeg)
在接口界面,添加 AXI 接口。这里的接口可选作为主机或者从机的 AXI-Full,AXI-Lite 或者 AXI-Stream 协议,本文选择 32 位 AXI-Lite 从机作为例子。位宽按照协议规定,可以在 32 位和 64 位之间进行选择。
![22a05577c0a85d5ec63865673feb7d1b.png](https://i-blog.csdnimg.cn/blog_migrate/f9e5cd9fde096606197feb908208ca28.jpeg)
在创建完 IP 核后.....没错,就是什么都不会发生。如果要编辑 IP 核或者像本文要做的那样查看 IP 的 RTL 代码,那么在 IP catalog 选项中找到刚刚新建的 IP 核,一般在 User Repository 选项下可以找到。右键-在 IP Packager 中编辑。
![71025d29b47c0e69648462a1f2fb3cfe.png](https://i-blog.csdnimg.cn/blog_migrate/217461b8ed0f5ec018aa5ea825733dd1.jpeg)
使用 IP Packager 编辑会打开一个新的基于 创建的 IP 核的 Vivado 工程,在这个工程中可以修改 IP 的信息,参数,接口以及 RTL 代码。在 source 中有一个 IP 核的顶层模块以及其下的 AXI-Lite 接口模块,这就是我们要学习的接口实现 demo 了。
![708cd460628c616503c1d805f7ec550b.png](https://i-blog.csdnimg.cn/blog_migrate/7e7ffbc0b091e4df85bf57dc8cea72fc.jpeg)
AXI-Lite 接口模块信号列表
Xilinx 提供的代码是非常好的学习材料,编码规范并提供了详细的注释。首先可以学习的是参数化的设计,在 IP 核代码中,很多的参数经常需要变化,比如在 GUI 界面中修改数据宽度等等,这就需要完善的参数化设计。
在声明端口之前可以使用#( )声明模块中的参数变量,这里起到两个作用,一是声明了参数a,b,二是赋予它们的默认值。注意一个以上参数的声明方式,不同参数之间用 "," 分隔,这和声明端口的语法是一致的。但每个参数之前的 parameter 是不可以共用的。
module my_module
#(
parameter a = 1,
parameter b = 2
)
(
端口声明
)
在实例化模块时,可以指定传入模块参数的值,如下方实例化 my_module ,模块命名为 m ,在 #( ) 中向模块的参数传入数值,模块中的参数 a,b 得到新的数值 10,20 。如果没有传值操作,那么模块中参数 a,b 使用默认值 1,2。(这里注意实例名 m 的位置)
//实例化 my_module
my_module
#(
.a(10),
.b(20)
)
m//实例名
(
端口连接
)
在 AXI-Lite 模块中定义了两个参数,数据总线的宽度以及地址总线的宽度。在后续的端口宽度定义中使用了这些参数。关于地址总线宽度为什么这里会是 4 将在后文中具体讨论。
![3b2baa0954e28d9dded5082ad0e88f1c.png](https://i-blog.csdnimg.cn/blog_migrate/6771ea57cc58860c4deeee8a69024da0.jpeg)
AXI-Lite 相比完整的 AXI 协议,侧重于配置与状态信号传输,不支持突发传输,并在裁剪了用于内存访问的控制信号后,大幅减少了端口数量,便于集成。端口列表如下,在代码中对每个端口的作用也有注释说明。
![a1493298ebeb704283ad87ccf3291cf0.png](https://i-blog.csdnimg.cn/blog_migrate/3d02bd2c9629e4e6e060d4b21ed8d7a0.jpeg)
CSR 寄存器
所以系统中集成 AXI-Lite 接口模块的作用一般是什么?
在一个 SoC 系统中,处理器需要配置大量的外设配置寄存器,并从外设的状态寄存器中读取外设的状态,进行初始化完成的判断等操作。AXI-Lite 总线就在 Configure & Status Report 阶段作为主机和外设之间的桥梁。
AXI 接口模块会将来自主机,一般是处理器的 AXI 数据锁存到寄存器中,将数值转为为逻辑电平信号输出给外设模块。将来自外设的电平信号也锁存到寄存器中,在主机发出读取请求时,将状态寄存器的数值以 AXI 读数据的形式发给主机。 AXI 接口模块在系统中起到配置寄存器的配置输入和状态寄存器的状态上报这两方面的作用。
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
自然而然,寄存器就是接口模块中的重要组成部分。在本模块中,设定有 4 个寄存器,每个寄存器位宽 32 位。寄存器的数量和位宽决定了地址总线的宽度。在计算机系统中,地址以字节为单位,4 个寄存器共有 16 个字节,所以地址总线的宽度为:
Waddr = log2(16) = 4 bit
总线操作中片选读写寄存器是以寄存器而不是字节为单位,所以在通过地址判断所要读写的寄存器时,只需要判断地址的高 2 bit,片选出 4 个寄存器。在选择了寄存器之后,也可以借助 WSTRB 字段控制对寄存器中的指定字节进行写操作,这部分会在后文解析协议实现时详细讨论。(WSTRB 信号只是指示从机屏蔽指定字节,实际上这部分数据也是传输的,协议规定传输必须以 32/64 位进行)
配置与状态寄存器可以分为只读,只写,可读可写三类。本文先讨论比较简单的只读,只写情况,即将整个32 bit 宽的寄存器从读/写功能上划分为两类,所有 bit 对于主机而言,全部为只读或者全部为只写。
如何实现只写/只读寄存器,假设寄存器 0 是一个只写寄存器,寄存器 1 是一个只读寄存器:
首先在接口模块上定义输入输出端口,进行寄存器和电平信号的转换。在输出端口上输出内部存储器的内容,或者将外部输入的逻辑电平赋予内部寄存器。
总线接口实现逻辑会在主线产生读写请求时,接口模块会将只写寄存器用总线数据更新或者将只读寄存器的数据更新到总线上,实现寄存器信息的更新。
output [31:0] reg0_out;
input [31:0] reg1_in;
assign reg0_out = slv_reg0;
always @(posedge clk)
slv_reg1 <= reg1_in;
接口时序实现
AXI-Lite 协议同完整的 AXI 协议一样有 5 个独立的通道,但由于不支持长度超过 1 的突发传输。Lite 协议中地址控制信息通道和数据通道严格满足一对一的关系。比如写地址通道写入地址以及控制信息后,写数据通道只在下一有效时钟沿进行一次数据传输。地址信号传输完成信号作为下一时刻写数据的使能信号。下图表示了地址和数据流向,代码也反映了这一时序关系。
![20ce09ee6ec1e76d6ad902d691dc632f.png](https://i-blog.csdnimg.cn/blog_migrate/0798bf70a1e3a06cf690ff2c5f74cfe0.jpeg)
AXI-Lite 协议上并没有特别定义传输的信号时序,时序与 AXI-Full 协议 Burst 长度为 1 的情况相同。
我们首先关注地址通道的逻辑,读写地址通道的逻辑类似,这里以稍复杂些的写地址通道为例。
![d9421458a5a76e4371aa809754e1640d.png](https://i-blog.csdnimg.cn/blog_migrate/1c95441682dee4de57141334317998d4.jpeg)
上述代码块通过控制 awready 信号完成了一次地址通道传输。逻辑检测 awvalid,wvalid信号的电平,当主机在写地址以及写数据通道上就绪时,从机置高 awready 信号,完成一次地址传输。与上 ~awready 是因为传输只需要在一次时钟上升沿 valid,ready 信号同时置高即可,在下一个周期,从机需要负责将 awready 信号置低,主机一般也会将 awvalid 信号置低,不过这和从机就不相干了。
在置高 awready 信号的条件中与上了 aw_en 变量,这是为了使从机有控制地址通道传输的能力。在整个写传输周期中,即地址-数据-写回复信号传输持续期间,如果主机又发起了一次新的写传输请求,置高 awvalid,wvalid信号,那么此时 aw_en 信号为低,从机将不会响应直至从机完成写回复,结束本次传输后,才回去响应下一次传输。
![e4f193960b959db35aa93ff9c5ef5b1c.png](https://i-blog.csdnimg.cn/blog_migrate/d08abe01691cac31aa15ed532531f2bf.jpeg)
根据上述协议中守则,从机可以等待 AWVALID 或者 WVALID,也可以等两者都就绪后,再置起 AWREADY,那么本实现中等待的就是两者都置起。也就是说虽然数据在地址后一周期传输,但是在地址传输时,数据已经就绪。
在从机置起 awready 信号完成地址通道传输的同时,从机也会从写地址上锁存当前的写地址。锁存的写地址会用于其后一个周期时刻来判断待写入的寄存器。
![5f02ed659c4b5cd476795916614db823.png](https://i-blog.csdnimg.cn/blog_migrate/3fef83d7d80b2997b858020fc9f27833.jpeg)
有趣的是锁存地址的过程块可以合并到上述产生 awready 的过程块中去,两者的使能条件相同。但从 Verilog 的哲学来说,合则节省代码行数,分则使逻辑功能划分更清楚。反正代码已经很多行了,如果分开写能够更好体验逻辑功能,那就分开写吧。
接下来是写数据通道的时序产生,同写地址通道类似,从机的写数据通道中的 wready 信号也根据主机的 wvalid 信号产生。当 wvalid 为高,则置高 wready 一个周期。因为本实现中写地址通道和写数据通道是严格一对一的关系,所以 wready 的信号的产生和 awready 信号在同一个周期中进行,判断 wvalid 与 awvalid 同时为高 。如果实现的是支持突发传输的 AXI-Full 协议,那么写数据将和写地址就失去了这种一一对应的关系。(一次地址传输对应多次数据传输)
![5924d8fe19a21bbc6b6abe48057dbce0.png](https://i-blog.csdnimg.cn/blog_migrate/1a0e17e48a6eccbf3fccd884bed6479e.jpeg)
本实现中最为重量级的一个部分是写数据的接收。首先判断读取写数据的使能条件。如前文所说:写地址传输的后一个时刻为写数据传输,写地址的完成的信号即作为写数据的使能信号。
![3c0f5bfc6e1ebdcd30c0a2baa1230db3.png](https://i-blog.csdnimg.cn/blog_migrate/9d0264d40d491613cb12a1da5610d036.jpeg)
当写使能有效后,逻辑首先根据写地址判断主机所要操作的寄存器。这里对写地址进行切片:
axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB]
![42f24b959bf66d2a6aa391e5c3d3d493.png](https://i-blog.csdnimg.cn/blog_migrate/ef593cdbead9374c96be3175ea1815d8.jpeg)
ADDR_LSB 与寄存器地址宽度相关:32 位时为 2 ,64 位时为 3 。选择寄存器时不需要考虑地址的 [ADDR_LSB-1 : 0] 部分。
寄存器宽度为 32 位时,每个寄存器 4 个字节,选择寄存器时,不需要判断地址低两位,因为这是单个寄存器内部的字节地址。
OPT_MEM_ADDR_BITS 与寄存器的数量有关,2^(OPT_MEM_ADDR_BITS+1) = 寄存器数量。So:
地址线的位宽 = ADDR_LSB+OPT_MEM_ADDR_BITS + 1
以 4 个 32 位寄存器为例,地址线的宽度 = log2(4*4) = 4 ,即 2 + 1 + 1、
#注:在根据地址选择寄存器时,当然也可以对整个地址宽度进行判断,但这样会造成资源的浪费。
在完成寄存器的选择后,将总线上已经就绪的写数据写入寄存器。如果不使用 STRB 信号对某些字节进行屏蔽,那么这样就完事了: slv_reg0 <= S_AXI_WDATA;
![227e15a14a7c05846c4a59f603f158a0.png](https://i-blog.csdnimg.cn/blog_migrate/d8f495a02fb15a9da5e263e38253849f.jpeg)
STRB 信号的作用是屏蔽一些字节的写入,比如某个寄存器只有 8 bit,或者 16 bit,但寄存器的写入必须以 32 bit 为单位。完整写入 32 bit 势必会影响临近的其他寄存器,此时可以使用 STRB 信号,指定要写入的字节位置为 1,屏蔽字节位置写 0 。
代码中使用 for 循环,以字节为单位,判断 STRB 信号为 1,则进行写入,否则保持原寄存器值不变。
完成一次写操作的最后一步,是由从机在写回复通道上对此次传输进行评价...啊不 响应。
![4d2071b05cbda65b75e1721356190400.png](https://i-blog.csdnimg.cn/blog_migrate/57e14f1c7208938b1d8126cfde61013f.jpeg)
在完成传输,即写数据/地址通道完成握手后的下一周期,从机置起 bvalid 信号,并在 bresp 信号上给出 'OK' 信号。在响应通道完成握手后,置低 bvalid 完成响应操作。
这里我们的从机要么不给,要就回复 'OK',那么就不会有错误的情况发生么?
我的理解是在这个模块中,和主机在芯片内部通信,就隔几个寄存器,几个 um 的距离,一般不会出现传输错误的情况。再者说,出现了错误,我们从机也不知道啊。(无辜.jpg)
笔者个人唯二遇到 AXI 回复错误的情况分别发生在 interconnect 和 DataMover IP 上。一是在 DataMover 上向 IP 写了错误的 CMD,IP 回复 Internal error;二是 interconnect 和从机的连接有问题,interconnect 无法转发请求,回复 decode error;这些错误回复在开发阶段对 Debug 是很有帮助的,但生产环境中,主机一般不会对这些错误回复进行处理。
主机可能是这样一种佛系思想:
小错误不影响功能,错了也就错了,不管
Fatal 错误,啊,我 dump 了,所以也不管
好了,后续的文章可能会对 response 做进一步分析。
读通道逻辑和写通道类似,本文就不再做进一步展开,读者可以自行阅读代码。这里有一个建议,即使笔者在完成了这篇文章的写作后,再通过仿真也解决了一些疑惑。所以这里建议读者还是要亲自仿真一下,获得第一手感性认识。
在设计中集成接口模块
在开发完成了一个寄存器总线接口模块后(假装都是我们自己写的),我们有两种方式将其集成到顶层设计中。
- 以 IP 的形式,打开我们先前提到的 IP Packager,在 IP 的工程中集成用户其余的模块。完成集成仿真验证后就完成了整个功能 IP 的开发。这种方式适合将成熟的代码与接口进行集成。这种方式,修改代码后需要更新 IP,会有些繁琐。
- 另一种适合在开发的过程中集成接口的方式是复制 IP 生成的接口模块代码到自己的工程中,例化为一个模块使用。这样比较适合开发中的,经常需要修改的顶层设计。
结语
本文我们通过学习 Xilinx 在 IP 接口中提供的 AXI-Lite 接口实现的模板,有 AXI 的实现有了更深入的了解,其实从 AXI-Lite 的角度来说,很简单哈,这一部分也是 AXI 协议的基础部分。在后续的文章中,我们将对 AXI 协议的实现做更深入的了解。