ZYNQ AXI协议学习(AXI_Lite,PS为主机)
上一篇文章大致介绍了AXI总线的基本知识。AXI的详细介绍参考手册UG-1037
创建一个AXI_lite接口的IP核(PL为从机,PS为主机,选择8个寄存器,位宽32)vivado版本用的2019
上面是创建好后的端口列表(input和output是对于PL端,PS端则相反),AXI_Lite使用的是GP接口
对于上面一些端口的理解
选择的是32位宽,读写数据就是32位的寄存器reg
选择8个寄存器,地址宽度就是5(这个位宽是根据所选寄存器个数计算出来的)
关于PORT端口,代码注释解释为通道保护类型,在官方代码逻辑里没有使用(可自行查看代码)
读写的回应端口 RESP,在代码里一直都是00,就是一直表示写回应OKAY(没有错误处理)
写操作
使用官方代码进行在线Debug(写地址 写数据 写回应)
图上是2个寄存器的写操作
由调试结果可知写地址和写数据有效传输在同一个时钟
写地址和写数据完成的下一个时钟,(PL)输出了一个写回应信号OKAY也就是00
PORT端口可以看到从101变成了001,可以理解为通道在使用时为001(具体控制是由PS端控制)。代码里无实际逻辑
写地址:
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
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
// Write Address latching
axi_awaddr <= S_AXI_AWADDR;//每次写准备好信号跳变时,赋新值
end
end
end
axi_awready信号只会存在一个时钟
写数据(只截取一个寄存器):
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
assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;
//只有在写地址有效 写地址准备好 写数据有效 写数据准备好的情况 slv_reg_wren才是有效
//前面代码可知 ready信号只有一个周期有效,那么slv_reg_wren也只有一个周期有效
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;
slv_reg4 <= 0;
slv_reg5 <= 0;
slv_reg6 <= 0;
slv_reg7 <= 0;
end
else begin
if (slv_reg_wren)
begin
case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )//ADDR_LSB = 2 OPT_MEM_ADDR_BITS = 2
//ADDR_LSB = 2用于调试32位寄存器,OPT_MEM_ADDR_BITS=2是因为我们只有8个寄存器 在地址上就是000-111 3位已经满足要求
3'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
slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
end
for循环结构其实就是将S_AXI_WDATA给寄存器slv_reg0。
这里只选取了第一个寄存器,后续的7个寄存器逻辑是一样的
写回应:
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
// indicates a valid write response is available
axi_bvalid <= 1'b1;
axi_bresp <= 2'b0; // 'OKAY' response
end // work error responses in future
else
begin
if (S_AXI_BREADY && axi_bvalid)
//check if bready is asserted while bvalid is high)
//(there is a possibility that bready is always asserted high)
begin
axi_bvalid <= 1'b0;
end
end
end
end
由代码可以看出读回应其实一直都是00,(这里不懂为什么这样设计)
读操作
使用官方代码进行在线Debug(读地址,读数据)
图上是2个寄存器的读操作
PORT端口可以看到从111变成了001,可以理解为通道在使用时为001(具体控制是由PS端控制)。代码里无实际逻辑
读操作的地址传输:
开始启动是,PORT端口和地址端口是一致的,PORT->001 ADDR就准备好了,下一个时钟读地址有效信号就拉高,再下一个时钟读准备好信号拉高,开始传输地址数据,传输完成后,PS端将下一个地址数据放到端口。
读操作的数据传输:
当地址传输完成,PL端马上拉高读数据有效(图上可以看到PS端的准备好信号一直为高,和手册写的必须先拉高VALID有差别),源数据同步准备好,在时钟上升沿开始传输数据
RESP端口一直为OKAY(和写操作一样)
(这里就不详细说明了,对比仿真图和上面的说明)
读地址:
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
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;
end
end
end
assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
//该信号只有一个周期有效
读数据:
//后面2个逻辑就是查找不同地址的寄存器数据
always @(*)
begin
// Address decoding for reading registers
case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
3'h0 : reg_data_out <= slv_reg0;
3'h1 : reg_data_out <= slv_reg1;
3'h2 : reg_data_out <= slv_reg2;
3'h3 : reg_data_out <= slv_reg3;
3'h4 : reg_data_out <= slv_reg4;
3'h5 : reg_data_out <= slv_reg5;
3'h6 : reg_data_out <= slv_reg6;
3'h7 : reg_data_out <= slv_reg7;
default : reg_data_out <= 0;
endcase
end
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; // register read data
end
end
end
AXI_Lite接口使用
PWM PWM_0 (
.clk(S_AXI_ACLK),
.rst(S_AXI_ARESETN),
.Cycle_set(slv_reg0), //设置PWM的周期
.Duty_set(slv_reg1), //设置占空比
.PWM(PWM[0])
);
上面是一个PWM的接口,使用了2个寄存器来设置PWM的周期和占空比,可根据寄存器的数值进行改变
至此AXI_Lite接口(PS端为主,PL端为从)就分析完了,AXI_Lite算简单的接口