AXI协议(二)-AXI-Lite主端解析及仿真
文章目录
在这一篇中,你将 可能学会:
- AXI-Lite主端的示例代码理解
- 如何自定义自己的AXI-Lite外设/接口(部分)
- 如何仿真验证AXI总线(小试牛刀罢了)
生成Master Axi-Lite示例IP
这个跟之前是一致的,唯一不同是接口模式选为主端
Master IP代码解析
打开*_master_v1_0_M00_AXI.v,可以看见有三部分:
- Master模式下的AXI协议握手信号控制
- User Logic下有一个状态机
- User Logic下受状态机控制的其他信号
所以很显然,由于我们已经对AXI协议已经有一定的了解,只要从状态机出发,再去掌握他怎么控制一些关键信号的就完事了🌹
状态机控制解析
对应代码L493,首先用插件画出他的状态转移图:
那差不多也就是初始化,然后循环写读了,在这状态机中,他还同时控制了以下信号:
写过程的:
-
start_single_write
-
write_issued
读过程的:
-
start_single_read
-
read_issued
检验:
-
compare_done
-
ERROR
下面我们一个个状态来讲
初始状态init_txn_pulse
从IDLE状态开始,先有初始化信号init_txn_pulse:
assign init_txn_pulse = (!init_txn_ff2) && init_txn_ff;
//Generate a pulse to initiate AXI transaction.
always @(posedge M_AXI_ACLK)
begin
// Initiates AXI transaction delay
if (M_AXI_ARESETN == 0 )
begin
init_txn_ff <= 1'b0;
init_txn_ff2 <= 1'b0;
end
else
begin
init_txn_ff <= INIT_AXI_TXN;
init_txn_ff2 <= init_txn_ff;
end
end
很简单的一个下降沿检测电路,也就是说INIT_AXI_TXN有一个下降沿的时候发起一次写读。
写状态writes_done
这里我们先不管底层是怎么实现的,就看看这些标志位是怎么改变的,先看writes_done:
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
writes_done <= 1'b0;
//The writes_done should be associated with a bready response
else if (last_write && M_AXI_BVALID && axi_bready)
writes_done <= 1'b1;
else
writes_done <= writes_done;
end
可以看到,控制writes_done的信号线有AXI写响应通道和last_write,那再看看last_write又是什么:
parameter integer C_M_TRANSACTIONS_NUM = 4
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
last_write <= 1'b0;
// The last write should be associated with
// a write address ready response
else if ((write_index == C_M_TRANSACTIONS_NUM) && M_AXI_AWREADY)
last_write <= 1'b1;
else
last_write <= last_write;
end
也就是说,last_write就是记录现在是不是在传最后一个字节了,writes_done还需要等到写响应通道有结果才会断言。注意这里的write_index,还没讲到
此时回到状态机中,看关于写过程的两个关键信号start_single_write和write_issued。
INIT_WRITE:
······
if (~axi_awvalid && ~axi_wvalid && // AW和W通道ok
~M_AXI_BVALID && ~last_write && // 没到最后一个字节
~start_single_write && ~write_issued)
begin
start_single_write <= 1'b1;
write_issued <= 1'b1;
end
else if (axi_bready)
begin
write_issued <= 1'b0;
end
else
begin
start_single_write <= 1'b0; //Negate to generate a pulse
end
可以看到start_single_write就像是一个脉冲信号(pulse),write_issued就像是一个使能位(en),都比较简单,具体怎么控制可以快进到下面。
读状态reads_done
这状态其实跟写状态思路上高度相似,先看reads_done:
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
reads_done <= 1'b0;
//The reads_done should be associated with a read ready response
else if (last_read && M_AXI_RVALID && axi_rready)
reads_done <= 1'b1;
else
reads_done <= reads_done;
end
基本跟writes_done一个东西,再看last_read:
parameter integer C_M_TRANSACTIONS_NUM = 4
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
last_read <= 1'b0;
//The last read should be associated with a read address ready response
else if ((read_index == C_M_TRANSACTIONS_NUM) && (M_AXI_ARREADY) )
last_read <= 1'b1;
else
last_read <= last_read;
end
这个也是一致的,所以大致可以猜到,在状态机控制中:
- start_single_read
- read_issued
这两个的控制也是一致的:
if (~axi_arvalid && ~M_AXI_RVALID &&
~last_read &&
~start_single_read && ~read_issued)
begin
start_single_read <= 1'b1;
read_issued <= 1'b1;
end
else if (axi_rready)
begin
read_issued <= 1'b0;
end
else
begin
start_single_read <= 1'b0; //Negate to generate a pulse
end
这里应该有细心的朋友留意到了,由于AXI中读通道和读响应是合并了的,所以这里的判断条件稍微改了一下。
比较状态compare_done
这里是直达IDLE的,这里直接看状态机:
INIT_COMPARE:
begin
// This state is responsible to issue the state of comparison
// of written data with the read data. If no error flags are set,
// compare_done signal will be asseted to indicate success.
ERROR <= error_reg;
mst_exec_state <= IDLE;
compare_done <= 1'b1;
end
这里先看compare_done是干啥的:
// Asserts when AXI transactions is complete
output wire TXN_DONE,
······
assign TXN_DONE = compare_done;
简单,再看error_reg:
// Register and hold any data mismatches, or read/write interface errors
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
error_reg <= 1'b0;
//Capture any error types
else if (read_mismatch || write_resp_error || read_resp_error)
error_reg <= 1'b1;
else
error_reg <= error_reg;
end
可以看到,有三种检测的错误类型:
- read_mismatch 即读回来的数和写出去的数不一致:
//Data Comparison
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
read_mismatch <= 1'b0;
// The read data when available (on axi_rready)
// is compared with the expected data
else if ((M_AXI_RVALID && axi_rready) &&
(M_AXI_RDATA != expected_rdata))
read_mismatch <= 1'b1;
else
read_mismatch <= read_mismatch;
end
其中,expected_rdata是在读端单独做判断:
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
begin
expected_rdata <= C_M_START_DATA_VALUE;
end
// Signals a new write address/ write data is
// available by user logic
else if (M_AXI_RVALID && axi_rready)
begin
expected_rdata <= C_M_START_DATA_VALUE + read_index;
end
end
这里可以看到,当时发送端的数据也是采用C_M_START_DATA_VALUE + index的,不过这个后面再讲。
- write_resp_error写响应错误
//Flag write errors
assign write_resp_error = (axi_bready & M_AXI_BVALID & M_AXI_BRESP[1]);
为什么这个是错误呢?回看上一篇中:
可见,在M_AXI_BRESP中,高位为1就是错误。
- read_resp_error读响应错误
//Flag write errors
assign read_resp_error = (axi_rready & M_AXI_RVALID & M_AXI_RRESP[1]);
同上,一模一样。
状态机小结
回看整个状态机的,我们可以看到,User logic主要交互的信号有:
- INIT_AXI_TXN 发起写读过程
- TXN_DONE 传输完成标志
- ERROR 错误信息
而与AXI交互的重要信号有:
- start_single_write(发起一次写的pulse)
- start_single_read(发起一次读的pulse)
接下来我们就根绝这些信号,再分析一次AXI-Lite信号
AXI-Lite Master协议解析
同样有5个通道:地址写(AW),写(W),写响应(WRB),地址读(AR),读®,其实在上一篇中已经讲过一次了,这里希望启发到大家的是,真正要用的时候,要怎么控制。
由于读写过程中,过程高度相似,这里只介绍地址写(AW),写(W),写响应(WRB)这三个通道
这里放出上篇中的端口表,这里需要留意,端口方向都是反的!!,因为上篇中讲的是从机
还需要注意的是,我们这里的是主机,握手时主要控制的是valid信号
写地址通道
注意,文章的代码顺序和它本身顺序是不一致的,这里主要按照上面的分通道来写,会比较简洁明了一些。
assign M_AXI_AWPROT = 3'b000;
always @(posedge M_AXI_ACLK)
begin
//Only VALID signals must be deasserted during reset per AXI spec
//Consider inverting then registering active-low reset for higher fmax
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
begin
axi_awvalid <= 1'b0;
end
//Signal a new address/data command is available by user logic
else
begin
if (start_single_write)
begin
axi_awvalid <= 1'b1;
end
//Address accepted by interconnect/slave
else if (M_AXI_AWREADY && axi_awvalid)
begin
axi_awvalid <= 1'b0;
end
end
end
//Write Addresses
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
begin
axi_awaddr <= 0;
end
// Signals a new write address/ write data is
// available by user logic
else if (M_AXI_AWREADY && axi_awvalid)
begin
axi_awaddr <= axi_awaddr + 32'h00000004;
end
end
这里可以看到,主要就是start_single_write来的时候,拉起valid,在收到awready的时候握手完成拉低。
这里axi_awaddr也是指明在写入时时顺序写的,这里也是,在AW握手成功后才会将新地址加上,此时axi_awvalid已经被拉低了。地址为什么是加4,可以看看上一篇教程。
从代码中也可以看到,xilinx官方所要求的 valid信号不可以根据ready来拉高,这一个原则在后面也是有的
写通道
这里跟上面是一致的,主要是数据的准备那一块还有一点逻辑没讲
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
begin
axi_wvalid <= 1'b0;
end
//Signal a new address/data command is available by user logic
else if (start_single_write)
begin
axi_wvalid <= 1'b1;
end
//Data accepted by interconnect/slave
else if (M_AXI_WREADY && axi_wvalid)
begin
axi_wvalid <= 1'b0;
end
end
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
begin
write_index <= 0;
end
// Signals a new write address/ write data is
// available by user logic
else if (start_single_write)
begin
write_index <= write_index + 1;
end
end
// Write data generation
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1 )
begin
axi_wdata <= C_M_START_DATA_VALUE;
end
// Signals a new write address/ write data is
// available by user logic
else if (M_AXI_WREADY && axi_wvalid)
begin
axi_wdata <= C_M_START_DATA_VALUE + write_index;
end
end
这里也是回应到上面所说的,在这个例子中,数据生成用的是C_M_START_DATA_VALUE+ index的方法,write_index的生成方法也是看start_single_write。相应地,在读中,就会有read_index,用的start_single_read。这里不再赘述
写响应通道
这里的握手信号和写其实是反过来的,因为你是接收从机的写结束信号嘛:
always @(posedge M_AXI_ACLK)
begin
if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1)
begin
axi_bready <= 1'b0;
end
// accept/acknowledge bresp with axi_bready by the master
// when M_AXI_BVALID is asserted by slave
else if (M_AXI_BVALID && ~axi_bready)
begin
axi_bready <= 1'b1;
end
// deassert after one clock cycle
else if (axi_bready)
begin
axi_bready <= 1'b0;
end
// retain the previous value
else
axi_bready <= axi_bready;
end
握手还是那一套,简单。
AXI协议实现总结
通过看代码,其实只要修改start_single_read和start_single_write这两个信号,握手部分就不用管了,最后还要考虑地址和数据怎么对应起来就完事了。
仿真
完整工程建立
所以这里的仿真其实十分简单,我们直接把主机和从机接起来完事了:
这里我们拉一主一从,从就拿我们上一节中生成的ip就完了:
然后点 Run Connection Automation(直接连也可以,但是我懒了…):
此时会发现,我们控制的几个引脚没引出,这里的,右键那几个端口,Create Port(Ctrl +K):
完成之后,整体就长这样:
然后打包成wrapper:
我这里之前已经有了,也是一样的操作
然后打开Windows->Address Editor:
看到从机的地址之后,修改主机的起始地址:
仿真流程
在AXI总线的仿真中,有好几种方法,这里也介绍一下:
- Xilinx官方出的VIP,是一个专用来仿真AXI的IP,他有一个示例工程,是用SystemVerilog搞的,笔者还不会。。。(这就去学
- 自己根据协议写TB
那这里由于我们主端和从端的外设都有了,我们就直接接上得了。
- 建立一个仿真文件_tb.v
- 用插件生成一个tb,并亿点点修改
module top_tb();
// design_1_wrapper Parameters
parameter PERIOD = 10;
// design_1_wrapper Inputs
reg clk_100MHz = 0 ;
reg m00_axi_init_axi_txn = 1 ;
reg reset_rtl_0 = 0 ;
reg reset_rtl_0_0 = 0 ;
// design_1_wrapper Outputs
initial
begin
forever #(PERIOD/2) clk_100MHz=~clk_100MHz;
end
initial
begin
#(PERIOD*2) reset_rtl_0_0 = 1;
#(PERIOD*10) reset_rtl_0_0 = 0;
#(PERIOD*10) reset_rtl_0_0 = 1;
end
design_1_wrapper u_design_1_wrapper (
.clk_100MHz ( clk_100MHz ),
.m00_axi_init_axi_txn ( m00_axi_init_axi_txn ),
.reset_rtl_0 ( reset_rtl_0 ),
.reset_rtl_0_0 ( reset_rtl_0_0 )
);
initial
begin
#(PERIOD*500) m00_axi_init_axi_txn = 1;
#(PERIOD*10) m00_axi_init_axi_txn = 0;
#(PERIOD*2) m00_axi_init_axi_txn = 1;
#(PERIOD*1000)
$stop;
end
endmodule
在vivado中用自带仿真工具,点行为级仿真,然后直接拉AXI总线下来观察:
从端同理,然后把主端的几个关键信号也拉上,就可以看到图啦
具体波形就不讲为什么了,原理上面都有。有兴趣的朋友可以动手做一下~
这里需要注意,如果是自己写的IP想接到这里的话,在package IP哪里会有让你对应AXI总线对线的操作,这样就可以在仿真的时候以这种好看的形式存在了,但是这个后面才会说到。
小结
经过这一节,相信大家会对AXI-Lite有更深的认识了,怎么去做自己的AXI-Lite其实在上面状态机中就哭呀归纳出来了,但实际上AXI-Lite只是AXI协议中最简单的一部分,后一节会开始讲AXI-FULL,然后会讲一两个IP来实际操作,再后面才会讲到AXI-Stream,但是AXI-Stream的实战会多一点。
也欢迎关注我的个人公众号,小何的芯像石头: