写在前面
在AXIS篇中,我们打包了2个AXI4-Stream接口的IP(一主一从)(带你快速入门AXI4总线--AXI4-Stream篇(2)----XILINX AXI4-Stream接口IP源码仿真分析),对着两个IP进行了仿真分析,同时也学习了一番XILINX提供的代码。在这篇文章中,我们照葫芦画瓢,也打包2个AXI4-Lite接口的IP(一主一从),来对其的仿真和原始代码学习一番。限于篇幅,将分2篇文章写完,本文写AXI4-Lite-slave接口。
1、调用IP
首先新建一个工程,然后点击Tools-----create and package new ip
点击Next
选择选项4,点击Next,各选项含义:
- 1---将当前工程打包为IP核
- 2----将当前工程的模块设计打包为IP核
- 3----将一个特定的文件夹目录打包为IP核
- 4----创建一个带AXI接口的IP核
填写IP信息(基本不修改,只改下名称方便后续管理),点击Next
选择Lite接口,接口类型选择从机slave,数据位宽32位,寄存器个数选择4个(这个寄存器主要用在SOC或者ZYNQ上,这里不需要过多了解),点击Next
精彩的来了,这里选择第3个,使用AXI4 VIP来验证IP,然后点击Next。(AXI4 VIP是XILINX的一个IP核,该IP核可以提供多种连接方式来对AXI接口进行验证,用起来很是贴心方便,我们后面会写相关文章,还请期待。)
这个时候就自动生成了如下界面,甚至还帮你打开了仿真界面。
我们先不急着仿真,先看看整个工程的结构再说。双击下图中的BD文件,
此时弹出结构框图如下,
整个工程由两部分组合:1、我们打包的IP,该IP的接口是AIX4-Lite-slave;2、AXI Verification IP,这是一个AXI的验证IP,提供多种验证方式,功能很强大,双击这个IP,看看它的内置定制信息:
可以看到,它可选选择接口模式来实现主机或从机或直通功能;可选协议类型,地址位宽,数据位宽等。我们这里不动它,直接cancel
接着点击按下图中操作,右击BD文件,选择generate output products来生成源码:
在此路径下生成了源码(第2个文件)文件,和顶层例化文件(第1个文件,这个不用管)
2、Slave接口的源码分析
打开上节生成的源码(注意:我删除了源码的注释,不然太长了。再优化了一下格式,主要是对齐。顺便再吐槽一下CSDN不能折叠代码):
代码较长,我将其分成NO.1-12共12个部分来进行讲解。只讲大体思路,其他内容请看代码注释。
NO.1:
`timescale 1 ns / 1 ps
//NO.1--------------------------------输入输出端口-------------------------------------------
module myip_axi_lite_slave_v1_0_S00_AXI #
(
parameter integer C_S_AXI_DATA_WIDTH = 32,
parameter integer C_S_AXI_ADDR_WIDTH = 4
)
(
//全局信号
input wire S_AXI_ACLK,
input wire S_AXI_ARESETN,
//写地址通道
input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_AWADDR,
input wire S_AXI_AWVALID,
output wire S_AXI_AWREADY,
//写数据通道
input wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_WDATA,
input wire [(C_S_AXI_DATA_WIDTH/8)-1 : 0] S_AXI_WSTRB,
input wire S_AXI_WVALID,
output wire S_AXI_WREADY,
//写响应通道
output wire [1 : 0] S_AXI_BRESP,
output wire S_AXI_BVALID,
input wire S_AXI_BREADY,
//读地址通道
input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_ARADDR,
input wire [2 : 0] S_AXI_ARPROT,
input wire S_AXI_ARVALID,
output wire S_AXI_ARREADY,
//读数据通道
output wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_RDATA,
output wire [1 : 0] S_AXI_RRESP,
output wire S_AXI_RVALID,
input wire S_AXI_RREADY
);
这部分主要是模块端口及参数例化。
参数例化:数据位宽32位;地址位宽4位
模块端口:AXI4-Lite协议的端口。不记得可以看这里:带你快速入门AXI4总线--AXI4-Lite篇(1)----AXI4-Lite总线
NO.2:
//NO.2--------------------------------寄存器定义------------------------------------------------------------
//AXI4-Lite接口相关
reg [C_S_AXI_ADDR_WIDTH-1 : 0] axi_awaddr;
reg axi_awready;
reg axi_wready;
reg [1 : 0] axi_bresp;
reg axi_bvalid;
reg [C_S_AXI_ADDR_WIDTH-1 : 0] axi_araddr;
reg axi_arready;
reg [C_S_AXI_DATA_WIDTH-1 : 0] axi_rdata;
reg [1 : 0] axi_rresp;
reg axi_rvalid;
//slave寄存器相关
localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1;
localparam integer OPT_MEM_ADDR_BITS = 1;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg0;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg1;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg2;
reg [C_S_AXI_DATA_WIDTH-1:0] slv_reg3;
wire slv_reg_rden;
wire slv_reg_wren;
reg [C_S_AXI_DATA_WIDTH-1:0] reg_data_out;
integer byte_index;
reg aw_en;
这部分主要是定义一些寄存器。
其中一些寄存器是从机需要输出给主机的信号,因为在always块中操作,所以需要定义成reg类型。
还有一些是对从机模块自身的slave寄存器操作的一些寄存器。
NO.3:
//NO.3--------------------------------端口赋值定义------------------------------------------------------------
//通过赋值方式避免直接操作输出端口
assign S_AXI_AWREADY = axi_awready;
assign S_AXI_WREADY = axi_wready;
assign S_AXI_BRESP = axi_bresp;
assign S_AXI_BVALID = axi_bvalid;
assign S_AXI_ARREADY = axi_arready;
assign S_AXI_RDATA = axi_rdata;
assign S_AXI_RRESP = axi_rresp;
assign S_AXI_RVALID = axi_rvalid;
这部分主要是将定义好的输出寄存器的值赋值给输出端口,避免直接操作输出端口。
NO.4:
//NO.4--------------------------------生成写地址准备信号axi_awready-------------------------------------------
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
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
这部分对两个信号赋值:
可以执行写事务标志信号aw_en(为高表示可以执行一次写事务);
写地址通道的从机准备信号axi_awready(为高表示从机可以被写入地址)
NO.5:
//NO.5----------------------------------锁存写地址信号axi_awaddr-------------------------------------------
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
axi_awaddr <= S_AXI_AWADDR; //下一个周期从机准备好接收数据,同时将地址寄存方便后面解析
end
end
end
这部分主要是将要写入的地址锁存,方便后面对从机的slave寄存器进行操作。
NO.6:
//NO.6----------------------------------生成写数据准备信号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
axi_wready <= 1'b1; //从机准备好写数据
end
else
begin
axi_wready <= 1'b0; //一般情况下从机处于非准备状态
end
end
end
这部分对写数据通道从机准备信号axi_wready赋值。
NO.7:
//NO.7----------------------------------将AXI总线上的数据写入从机中的寄存器-------------------------------------------
assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID; //当准备写入数据时拉高寄存器写使能,与AXI总线写数据对齐
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 //判断当前BYTE是否有效(即掩码功能是否生效)
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
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
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
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
这部分主要是根据要写入的地址,来讲总线上要写入从机的主句给写入到从机的slave寄存器。同时需要注意S_AXI_WSTRB这个写选通,当其为高时,代表总线上对应的BYTE是有效的,反之无效。
NO.8:
//NO.8----------------------------------生成写响应信号axi_bvalid、响应值axi_bresp-------------------------------------------
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
axi_bvalid <= 1'b1; //从机拉高响应有效信号,等待主机回复准备接收响应信号S_AXI_BREADY
axi_bresp <= 2'b0; //访问成功:'OKAY' response
end //不支持其他响应判断
else
begin
if (S_AXI_BREADY && axi_bvalid) //握手成功
begin
axi_bvalid <= 1'b0; //拉低axi_bvalid(仅需维持一个时钟周期)
end
end
end
end
这部分对两个信号赋值:
写响应通道的从机准备好响应信号axi_bvalid,其拉高表示,从机准备好完成一次写响应。
从机回复的写响应值axi_bresp,其值固定为0,代表响应成功(暂不支持其他值,如响应不成功等)
NO.9:
//NO.9----------------------------------寄存读取地址S_AXI_ARADDR,生成读数据准备信号-------------------------------------------
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
axi_arready <= 1'b1; //从机准备读地址
axi_araddr <= S_AXI_ARADDR; //将要读取的地址寄存
end
else
begin
axi_arready <= 1'b0; //其他情况默认从机没有准备接收读地址
end
end
end
这部分对两个信号赋值:
读地址通道的从机准备好信号axi_arready,其拉高表示,从机准备接收读取地址
将读地址通道的读地址S_AXI_ARADDR寄存给axi_araddr,方便后续根据读取地址找到对应的slave寄存器拿出要读到值
NO.10:
//NO.10----------------------------------生成读响应信号axi_rvalid、读响应值axi_rresp-------------------------------------------
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
axi_rvalid <= 1'b1; //从机读出的数据有效
axi_rresp <= 2'b0; // 'OKAY' response
end
else if (axi_rvalid && S_AXI_RREADY) //读数据通道握手完成
begin
axi_rvalid <= 1'b0; //axi_rvalid(仅需维持一个时钟周期)
end
end
end
这部分对两个信号赋值:
读数据通道的从机准备好信号axi_rvalid,其拉高表示,从机准备好完成一读操作。
从机回复的读响应值axi_rresp,其值固定为0,代表响应成功(暂不支持其他值,如响应不成功等)
NO.11:
//NO.11----------------------------------生成寄存器读使能信号、将寄存器的值取出-------------------------------------------
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; //将寄存器的值赋值给reg_data_out
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
这部分对两个信号赋值:
寄存器读取使能信号,该信号与读事务对齐,即进行一次读操作的时候将slave寄存器的值输出到总线上
读取数据中间变量reg_data_out,根据读取的地址将对应slave寄存器的值赋给reg_data_out
NO.12:
//NO.12----------------------------------生成总线上的读取数据axi_rdata-------------------------------------------
always @( posedge S_AXI_ACLK )
begin
if ( S_AXI_ARESETN == 1'b0 )
begin
axi_rdata <= 0;
end
else
begin
if (slv_reg_rden) //读取寄存器使能有效
begin
axi_rdata <= reg_data_out; //将寄存器的值输出到读数据通道,放到总线上,让主机读走
end
end
end
endmodule
这部分对信号赋值:
从机输出的被读取数据axi_rdata,将中间变量reg_data_out的值赋给axi_rdata。采用中间变量的方法可以是输出数据的时序对齐。
3、仿真波形
接下来使用Vivado自带的仿真器来进行仿真,观看仿真结果
3.1、AXI4-Lite总线的仿真波形
我们先把自动生成的仿真信号删除,添加如下的波形信号:
仿真结果如下:
可以看到仿真结果是用这个彩条+字符的形式表示的,非常清晰。这就是添加了AXI VIP IP的效果。
在AXI4-Lite总线上共发生了8个事务:先是连续的4个写事务,接着4个读事务。下面的五个通道分别示意了此时通道内执行的握手操作,将鼠标放在其中任意一处上,会出现如下信息(顺序1、地址0等):
在左键点击,会显示具体的事务流程如下:
从上图的箭头我们可以直到一次写事务的流程:写地址----写数据----写响应。再看看读事务的流程:
可以看到读事务的流程:读地址----读数据。
3.2、从机IP的slave接口仿真波形
看完了AXI4-Lite总线的仿真波形,我们再看下上面具体解析代码(可以理解为底层驱动)的仿真波形。按如下方法添加:
将信号按通道或用途做好分类,仿真结果如下:
信号较多,我们先解析写事务如下:
上图中,一共进行了4次写入操作,写入的地址分别为0、4、8、12,分别对应从机内4个Slave寄存器的地址。写入的数据分别为1-4。握手过程就不谈了。看一下数据是怎么被写到
Slave寄存器的,如下:
从上图可以看到:
slave寄存器写入使能信号slv_reg_wren是与总线上的写事务对齐的,这样就可以直接将总线要写入的数据写入slave寄存器;
根据写入的地址,将要写入的数据分别写入对应的slave寄存器,具体到上图,分别往4个寄存器写入数据1-4
再看下读事务的时序图:
完成握手后,依次从地址(0、4、8、12)中读出了数据1-4,与之前写入的一致(数据被存在从机的slave寄存器中)。接着看一下数据是从Slave寄存器中读出来的:
从上图可以看到:
slave寄存器读取使能信号slv_reg_rden是与总线上的读事务对齐的,这样就可以直接将slave寄存器的值读出赋值给总线,考虑到读取寄存器的值存在一个时钟周期的延迟,所以采用了临时变量reg_data_out[31:0]来打一拍,将时序对齐;
分别从4个slave寄存器中读取数据1-4。
4、其他
- 可以看到其实AXI4-Lite总线的使用还是相对比较简单的,只要设计好各个通道的握手时序,以及读写的时序关系就好了。下一篇文章我们再继续分析AXI4-Lite总线的Master接口的代码。
- 创作不易,希望各位大佬多多三连支持!一家之言,如有错误还请指正!
版本信息
文件:V1.0
编号:63
Vivado:Vivado 2019.2
Modelsim:无
Quartus II:无