网上很多教程都是推荐大家在Vivado创建一个IP,然后提供一个AXI模板,但是呢,这样有很多冗余逻辑其实我们用不上,自己写一个也超级简单的,看一看我实现的超简单AXI BRAM,比官方的最小实现还要节约.当然这里实现只是一个好玩的测试,时序收敛可能并不好,有敏感需求的最好还是自己看着办哦.
官方实现:https://download.amd.com/docnav/documents/ip_attachments/axi-bram-ctrl.html
我这里开始以我的思路开始说,而不是直接给出全篇代码,想要全部代码自行到下面链接.
https://gist.github.com/nickfox-taterli/e932a0fddfac1cca1fd8ef42cd8e66ee
我们先确定有用到的AXI端口,把必要提供的端口都提供起来,所以整个模块定义就长这样.
module bram_axil(
input wire clk,
input wire rst,
input wire [31:0] s_axil_awaddr,
input wire [2:0] s_axil_awprot,
input wire s_axil_awvaild,
output wire s_axil_awready,
input wire [31:0] s_axil_wdata,
input wire [3:0] s_axil_wstrb,
input wire s_axil_wvalid,
output wire s_axil_wready,
output wire [1:0] s_axil_bresp,
output wire s_axil_bvalid,
input wire s_axil_bready,
input wire [31:0] s_axil_araddr,
input wire [2:0] s_axil_arprot,
input wire s_axil_arvalid,
output wire s_axil_arready,
output wire [31:0] s_axil_rdata,
output wire [1:0] s_axil_rresp,
output wire s_axil_rvalid,
input wire s_axil_rready
);
当我作为子模块需要输出的信号时候,会通过一个reg中继,比如当我想令AWREADY设置为1,那么我需要设置AWREADY_reg为1,而AWREADY_reg需要assign给AWREADY,所以也不能直接赋值AWREADY,而需要在触发条件后进行,所以我就定义这么一个块,当XXXX_next赋值,在下一个CLK来临的时候,他就会成为真正的输出.
reg s_axil_awready_reg = 1'b0, s_axil_awready_next;
reg s_axil_wready_reg = 1'b0, s_axil_wready_next;
reg s_axil_bvalid_reg = 1'b0, s_axil_bvalid_next;
reg s_axil_arready_reg = 1'b0, s_axil_arready_next;
reg [31:0] s_axil_rdata_reg = 32'd0, s_axil_rdata_next;
reg s_axil_rvalid_reg = 1'b0, s_axil_rvalid_next;
assign s_axil_awready = s_axil_awready_reg;
assign s_axil_wready = s_axil_wready_reg;
assign s_axil_bresp = 2'b00;
assign s_axil_bvalid = s_axil_bvalid_reg;
assign s_axil_arready = s_axil_arready_reg;
assign s_axil_rdata = s_axil_rdata_reg;
assign s_axil_rresp = 2'b00;
assign s_axil_rvalid = s_axil_rvalid_reg;
always @(posedge clk) begin
s_axil_awready_reg <= s_axil_awready_next;
s_axil_wready_reg <= s_axil_wready_next;
s_axil_bvalid_reg <= s_axil_bvalid_next;
s_axil_arready_reg <= s_axil_arready_next;
s_axil_rdata_reg <= s_axil_rdata_next;
s_axil_rvalid_reg <= s_axil_rvalid_next;
if(rst) begin
s_axil_awready_reg <= 1'b0;
s_axil_wready_reg <= 1'b0;
s_axil_bvalid_reg <= 1'b0;
s_axil_arready_reg <= 1'b0;
// s_axil_rdata_reg assign in other module.
s_axil_rvalid_reg <= 1'b0;
end
end
那现在解决第二个问题,各种信号何时变化,我们知道AWVALID,WVALID有效后,说明数据要写了,我们由于是写入到BRAM,在CLK后刚好也能写完,所以一旦发生这两个有效,我们就可以BVALID了.
if(s_axil_awvaild && s_axil_wvalid && !s_axil_bvalid) begin
s_axil_awready_next <= 1'b1;
s_axil_wready_next <= 1'b1;
s_axil_bvalid_next <= 1'b1;
end
那我们BVALID之后,主机收到了,肯定得准备下一个数据或者暂时休息,并且主机取走数据后,BREADY肯定就拉低了,我们这时候也可以让BVALID失效.一句话就可以了.
s_axil_bvalid_next = s_axil_bvalid_reg && !s_axil_bready;
因为当前BVALID如果为1,BREADY也为1,说明主机能取走数据,所以下一周期BVALID_next就为0了,RVALID同理.
最后就是让Vivado推断BRAM了.
if(s_axil_awvaild && s_axil_wvalid && !s_axil_bvalid) begin
if(s_axil_wstrb[0]) memory[s_axil_awaddr[3:2]][7:0] <= s_axil_wdata[7:0];
if(s_axil_wstrb[1]) memory[s_axil_awaddr[3:2]][15:8] <= s_axil_wdata[15:8];
if(s_axil_wstrb[2]) memory[s_axil_awaddr[3:2]][23:16] <= s_axil_wdata[23:16];
if(s_axil_wstrb[3]) memory[s_axil_awaddr[3:2]][31:24] <= s_axil_wdata[31:24];
end
s_axil_rdata_next <= memory[s_axil_araddr[3:2]];
要注意读取可别放在判断里了,不然就推断不出来了,你可以读了但是不RVALID,主机就不会认为你数据有什么用啦,可以放心.
挂处理器上实践一下,通过添加System ILA监控的Interface结果正常表现,这是Zynq访问的情况,因为外设众多,所以都是轮流请求的,因此CPU请求外设的效率其实也就那样.
这么简单的代码应该不用解释了吧.
unsigned int *ptr = (unsigned int *)0x40000000;
for (int i = 0; i < 4; ++i) {
*(ptr + i) = 0x12345678 + i;
buf[i] = 0;
}
for (int i = 0; i < 4; ++i) {
buf[i] = *(ptr + i);
}