本文转载自:孤独的单刀的CSDN博客
写在前面
在带你快速入门AXI4总线--AXI4-Lite篇(2)----XILINX AXI4-Lite接口IP源码仿真分析(Slave接口)中我们已经对Slave接口的代码做了分析,并观察了其仿真波形,在本文我们将生成AXI4-Lite_Master接口的IP来对其解析。
1、调用IP
具体步骤不讲,请参看Slave接口的文章,只需要将IP的接口类型改为Master即可,其他一致。
2、Master接口的源码分析
打开生成的源码(注意:我删除了源码的注释,不然太长了。再优化了一下格式,主要是对齐。顺便再吐槽一下CSDN不能折叠代码):
代码较长,我将其分成NO.1-13共13个部分来进行讲解。只讲大体思路,其他内容请看代码注释。
NO.1:
`timescale 1 ns / 1 ps //NO.1--------------------------------输入输出端口------------------------------------------- module myip_axi_lite_master_v1_0_M00_AXI # ( parameter C_M_START_DATA_VALUE = 32'hAA000000, //初始写入数据的值 parameter C_M_TARGET_SLAVE_BASE_ADDR = 32'h40000000, //写入地址的基地址 parameter integer C_M_AXI_ADDR_WIDTH = 32, //地址位宽 parameter integer C_M_AXI_DATA_WIDTH = 32, //数据位宽 parameter integer C_M_TRANSACTIONS_NUM = 4 //传输的最大事务个数 ) ( //工具人型信号 input wire INIT_AXI_TXN, //传输开始信号 output reg ERROR, //错误 output wire TXN_DONE, //传输完成 //全局信号 input wire M_AXI_ACLK, input wire M_AXI_ARESETN, //写地址通道信号 output wire [C_M_AXI_ADDR_WIDTH-1 : 0] M_AXI_AWADDR, output wire [2 : 0] M_AXI_AWPROT, output wire M_AXI_AWVALID, input wire M_AXI_AWREADY, //写数据通道信号 output wire [C_M_AXI_DATA_WIDTH-1 : 0] M_AXI_WDATA, output wire [C_M_AXI_DATA_WIDTH/8-1 : 0] M_AXI_WSTRB, output wire M_AXI_WVALID, input wire M_AXI_WREADY, //写响应通道信号 input wire [1 : 0] M_AXI_BRESP, input wire M_AXI_BVALID, output wire M_AXI_BREADY, //读地址通道信号 output wire [C_M_AXI_ADDR_WIDTH-1 : 0] M_AXI_ARADDR, output wire [2 : 0] M_AXI_ARPROT, output wire M_AXI_ARVALID, input wire M_AXI_ARREADY, //读数据通道信号 input wire [C_M_AXI_DATA_WIDTH-1 : 0] M_AXI_RDATA, input wire [1 : 0] M_AXI_RRESP, input wire M_AXI_RVALID, output wire M_AXI_RREADY );
这部分主要是模块端口及参数例化。
参数例化:初始写入数据的值;写入地址的基地址;数据位宽32位;地址位宽4位;传输的最大事务个数
模块端口:
AXI4-Lite协议的端口。不记得可以看这里:带你快速入门AXI4总线--AXI4-Lite篇(1)----AXI4-Lite总线
其他工具人型信号
NO.2:
//NO.2--------------------------------以2为底取对数的function------------------------------------------- //以2为底取对数 function integer clogb2 (input integer bit_depth); begin for(clogb2=0; bit_depth>0; clogb2=clogb2+1) bit_depth = bit_depth >> 1; end endfunction localparam integer TRANS_NUM_BITS = clogb2(C_M_TRANSACTIONS_NUM-1); //计算最大传输事务计数的位宽
这部分主要是写了一个求2为底的对数的FUNCTION,这个function一般用来计算变量的位宽;计算了传输事务最大个数的位宽。
NO.3:
//NO.3--------------------------------reg、wire、参数定义------------------------------------------- //状态机状态定义 parameter [1:0] IDLE = 2'b00, //初始状态 INIT_WRITE = 2'b01, //写事务状态 INIT_READ = 2'b10, //读事务状态 INIT_COMPARE = 2'b11; //比较读、写结果状态 //axi总线信号 reg [1:0] mst_exec_state; reg axi_awvalid; reg axi_wvalid; reg axi_arvalid; reg axi_rready; reg axi_bready; reg [C_M_AXI_ADDR_WIDTH-1 : 0] axi_awaddr; reg [C_M_AXI_DATA_WIDTH-1 : 0] axi_wdata; reg [C_M_AXI_ADDR_WIDTH-1 : 0] axi_araddr; wire write_resp_error; //写响应错误 wire read_resp_error; //读响应错误 reg start_single_write; //单次写事务触发信号 reg start_single_read; //单次读事务触发信号 reg write_issued; //拉高表示此时正在发起进行写事务 reg read_issued; //拉高表示此时正在发起进行读事务 reg writes_done; //所有写事务完成 reg reads_done; //所有读事务完成 reg error_reg; //错误寄存器,拉高表示此模块读写有误 reg [TRANS_NUM_BITS : 0] write_index; //写事务计数 reg [TRANS_NUM_BITS : 0] read_index; //读事务计数 reg [C_M_AXI_DATA_WIDTH-1 : 0] expected_rdata; //理论上应该读取的数据(即被写入的数据) reg compare_done; //比较读写结果完成 reg read_mismatch; //读、写结果不匹配 reg last_write; //最后一次写事务 reg last_read; //最后一次读事务 reg init_txn_ff; //传输开始信号打1拍 reg init_txn_ff2; //传输开始信号打2拍 reg init_txn_edge; //没用到的信号 wire init_txn_pulse; //传输开始信号的上升沿捕获信号
这部分定义了一系列的变量,具体作用请看注释。
NO.4:
//NO.4--------------------------------部分赋值模块------------------------------------------- assign M_AXI_AWADDR = C_M_TARGET_SLAVE_BASE_ADDR + axi_awaddr; //写地址 = 基地址 + 偏移地址 assign M_AXI_WDATA = axi_wdata; assign M_AXI_AWPROT = 3'b000; //保护类型 assign M_AXI_AWVALID = axi_awvalid; assign M_AXI_WVALID = axi_wvalid; assign M_AXI_WSTRB = 4'b1111; //写入数据全部选中 assign M_AXI_BREADY = axi_bready; assign M_AXI_ARADDR = C_M_TARGET_SLAVE_BASE_ADDR + axi_araddr; //读地址 = 基地址 + 偏移地址 assign M_AXI_ARVALID = axi_arvalid; assign M_AXI_ARPROT = 3'b001; //保护类型 assign M_AXI_RREADY = axi_rready; assign TXN_DONE = compare_done; //比较完成信号赋值给传输完成信号
这部分对部分变量进行了赋值,避免了输出信号的直接操作。
NO.5:
//NO.5--------------------------------对传输开始信号打拍并求其上升沿(异步信号)------------------------------------------- assign init_txn_pulse = (!init_txn_ff2) && init_txn_ff; //求上升沿 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这个信号进行了上升沿捕捉,这是控制主机模块开始写事务的使能信号,由外部传入,属于异步信号。
NO.6:
//NO.6--------------------------------状态机实现流程控制------------------------------------------- always @ ( posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 1'b0) begin mst_exec_state <= IDLE; start_single_write <= 1'b0; write_issued <= 1'b0; start_single_read <= 1'b0; read_issued <= 1'b0; compare_done <= 1'b0; ERROR <= 1'b0; end else begin case (mst_exec_state) IDLE: //初始状态 if ( init_txn_pulse == 1'b1 ) //传输开始 begin mst_exec_state <= INIT_WRITE; ERROR <= 1'b0; compare_done <= 1'b0; end else begin mst_exec_state <= IDLE; end INIT_WRITE: //写传输事务状态 if (writes_done) // 全部4次写事务完成 begin mst_exec_state <= INIT_READ; //跳转到读事务状态 end else begin mst_exec_state <= INIT_WRITE; //空闲状态下生成start_single_write、write_issued if (~axi_awvalid && ~axi_wvalid && ~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; //写入响应后表示写事务可以结束(有1个周期的时延) end else begin start_single_write <= 1'b0; //其他情况不进行写事务 end end INIT_READ: //读传输事务状态 if (reads_done) // 全部4次读事务完成 begin mst_exec_state <= INIT_COMPARE; //跳转到比较读写结果状态 end else begin mst_exec_state <= INIT_READ; //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) //读取响应后表示读事务可以结束(有1个周期的时延) begin read_issued <= 1'b0; end else begin start_single_read <= 1'b0; //其他情况不进行读事务 end end INIT_COMPARE: //比较读、写数据结果 begin mst_exec_state <= IDLE; ERROR <= error_reg; compare_done <= 1'b1; end default : begin mst_exec_state <= IDLE; end endcase end end
这个部分使用一个状态机实现了整个代码的流程控制,状态流程大致如下:
整个流程想实现的功能是:首先进入初始状态,等外部发送可以开始进行传输信号后(init_txn_pulse),进入写事务状态。在写事务状态如果完成了所有的写事务(4个),则跳转到读事务状态。若没有完成则继续保持在该状态。在读事务状态如果完成了所有的读事务(4个),则跳转到比较状态。若没有完成则继续保持在该状态。在比较状态,对读、写数据进行比较,观察是否由错误。
NO.7:
//NO.7--------------------------------写地址通道------------------------------------------- //写地址有效 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_awvalid <= 1'b0; end else begin if (start_single_write) //单次写入有效 begin axi_awvalid <= 1'b1; end else if (M_AXI_AWREADY && axi_awvalid) //写地址通道握手完成 begin axi_awvalid <= 1'b0; end end end //写次数计数 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin write_index <= 0; end else if (start_single_write) begin write_index <= write_index + 1; end end //写地址 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_awaddr <= 0; end else if (M_AXI_AWREADY && axi_awvalid) begin axi_awaddr <= axi_awaddr + 32'h00000004; end end
这个部分主要对三个信号赋值:
写地址有效信号axi_awvalid:当单次写事务使能信号有效时,拉高axi_awvalid。当完成握手后,拉低axi_awvalid。
写次数计数信号write_index:每当单次写事务使能信号有效时(表示进行了一次写事务),其值+1,用来统计写事务的次数。
写地址信号axi_awaddr:每当握手完成后,其值加4。即两次写事务的写入地址相差4
NO.8:
//NO.8--------------------------------写数据通道------------------------------------------- //写数据有效 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_wvalid <= 1'b0; end else if (start_single_write) begin axi_wvalid <= 1'b1; end 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 axi_wdata <= C_M_START_DATA_VALUE; end else if (M_AXI_WREADY && axi_wvalid) begin axi_wdata <= C_M_START_DATA_VALUE + write_index; end end
这个部分主要对2个信号赋值:
写数据有效信号axi_wvalid:当单次写事务使能信号有效时,拉高axi_wvalid。当完成握手后,拉低axi_wvalid。
写数据信号axi_wdata:每当握手完成后,其值在首次写入值的基础上 + 当前写事务的次数。
NO.9:
//NO.9--------------------------------写响应通道------------------------------------------- //准备接收写响应 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_bready <= 1'b0; end else if (M_AXI_BVALID && ~axi_bready) begin axi_bready <= 1'b1; end else if (axi_bready) begin axi_bready <= 1'b0; end else axi_bready <= axi_bready; end assign write_resp_error = (axi_bready & M_AXI_BVALID & M_AXI_BRESP[1]); //判断响应是否有效
这个部分主要对2个信号赋值:
写响应准备信号axi_bready:当从机发送的响应有效信号有效时,拉高axi_bready。当完成握手后,拉低axi_bready。
写响应错误信号write_resp_error:根据响应值对响应结果进行正确性判断
NO.10:
//读次数计数 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin read_index <= 0; end else if (start_single_read) begin read_index <= read_index + 1; end end //读地址有效 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_arvalid <= 1'b0; end else if (start_single_read) begin axi_arvalid <= 1'b1; end else if (M_AXI_ARREADY && axi_arvalid) begin axi_arvalid <= 1'b0; end end //读地址 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_araddr <= 0; end else if (M_AXI_ARREADY && axi_arvalid) begin axi_araddr <= axi_araddr + 32'h00000004; end end
这个部分主要对三个信号赋值:
读地址有效信号axi_arvalid:当单次读事务使能信号有效时,拉高axi_arvalid。当完成握手后,拉低axi_arvalid。
读次数计数信号read_index:每当单次读事务使能信号有效时(表示进行了一次读事务),其值+1,用来统计读事务的次数。
读地址信号axi_araddr:每当握手完成后,其值加4。即两次读事务的读地址相差4(这一点与写地址一致)
NO.11:
//NO.11--------------------------------读数据通道(含读响应)------------------------------------------- //准备读数据 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) begin axi_rready <= 1'b0; end else if (M_AXI_RVALID && ~axi_rready) begin axi_rready <= 1'b1; end else if (axi_rready) begin axi_rready <= 1'b0; end end assign read_resp_error = (axi_rready & M_AXI_RVALID & M_AXI_RRESP[1]); //判断响应是否有效 //生成理论上应该读到的值,后续与实际读出的值做对比 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 else if (M_AXI_RVALID && axi_rready) begin expected_rdata <= C_M_START_DATA_VALUE + read_index; end end
这个部分主要对3个信号赋值:
准备读数据信号axi_rready:当从机告知可以读数据时,拉高axi_rready。当完成握手后,拉低axi_rready。
读响应错误信号read_resp_error:根据响应值对响应结果进行正确性判断
理论上应该被读出的信号expected_rdata:根据之前写入的信号判断当前理应被读出的信号是多少
NO.12:
//NO.12--------------------------------读、写事务状态判断------------------------------------------- //最后一次写事务 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) last_write <= 1'b0; else if ((write_index == C_M_TRANSACTIONS_NUM) && M_AXI_AWREADY) //达到最大写次数,且第四次写地址已完成 last_write <= 1'b1; else last_write <= last_write; end //全部写事务完成 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) writes_done <= 1'b0; else if (last_write && M_AXI_BVALID && axi_bready) writes_done <= 1'b1; else writes_done <= writes_done; end //最后一次读事务 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) last_read <= 1'b0; else if ((read_index == C_M_TRANSACTIONS_NUM) && (M_AXI_ARREADY) ) last_read <= 1'b1; else last_read <= last_read; end //全部读事务完成 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) reads_done <= 1'b0; else if (last_read && M_AXI_RVALID && axi_rready) reads_done <= 1'b1; else reads_done <= reads_done; end
这个部分主要对4个信号赋值:
最后一次写事务信号last_write:当开始了最后一次写事务后,拉高此信号。
最后一次读事务信号last_read:当开始了最后一次读事务后,拉高此信号。
全部写事务完成信号writes_done:当完成了所有写事务后,拉高此信号。
全部读事务完成信号reads_done:当完成了所有读事务后,拉高此信号。
NO.13:
//NO.13--------------------------------错误判断------------------------------------------- //判断读出的数据是否与写入的数据匹配 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) read_mismatch <= 1'b0; else if ((M_AXI_RVALID && axi_rready) && (M_AXI_RDATA != expected_rdata)) //读、写数据不匹配 read_mismatch <= 1'b1; else read_mismatch <= read_mismatch; end //错误判断的三种类型:1、读写不匹配;2、写响应不正确;3、读响应不正确 always @(posedge M_AXI_ACLK) begin if (M_AXI_ARESETN == 0 || init_txn_pulse == 1'b1) error_reg <= 1'b0; else if (read_mismatch || write_resp_error || read_resp_error) error_reg <= 1'b1; else error_reg <= error_reg; end endmodule
这个部分主要对2个信号赋值:
判断读、写是否匹配的信号read_mismatch:若读出的数据与写入的数据不一致则拉高此信号。
错误error_reg:若存在以下三种错误则拉高此信号:
1、读写不匹配;
2、写响应不正确;
3、读响应不正确
3、仿真波形
代码分析完了,接下来使用Vivado自带的仿真器来进行仿真,观看仿真结果,加深对设计方法的理解:
3.1、AXI4-Lite总线的仿真波形
我们先把自动生成的仿真信号删除,添加如下的波形信号:
仿真结果如下:
可以看到仿真结果是用这个彩条+字符的形式表示的,非常清晰。这就是添加了AXI VIP IP的效果。
在AXI4-Lite总线上共发生了8个事务:先是连续的4个写事务,接着4个读事务。下面的五个通道分别示意了此时通道内执行的握手操作,将鼠标放在其中任意一处上,会出现如下信息(顺序1、地址40000000等):
左键点击,会显示具体的事务流程如下:
从上图的箭头我们可以直到一次写事务的流程:写地址----写数据----写响应。再看看读事务的流程:
可以看到读事务的流程:读地址----读数据。
3.2、主机IP的master接口仿真波形
看完了AXI4-Lite总线的仿真波形,我们再看下上面具体解析代码(可以理解为底层驱动)的仿真波形。按如下方法添加:
将信号按通道或用途做好分类(我还删除了一些不要紧的信号),状态机部分的仿真结果如下:
不讲解了,直接看图吧。
AXI4-Lite总线部分的仿真结果如下:
在上图中,主机先发起了4次写事务,分别往地址h0000_0000、h0000_0004、h0000_0000和h0000_000c中写入了数据aa00_0000、 aa00_0001、aa00_0002和aa00_0003。
接着又发起了4次读事务,分别从之前写入的地址中读取到了数据aa00_0000、 aa00_0001、aa00_0002和aa00_0003(这里图没截好),与写入的一致,证明验证成功。
4、其他
可以看到其实AXI4-Lite总线的使用还是相对比较简单的,只要设计好各个通道的握手时序,以及读写的时序关系就好了。