自己写一个超级简单的AXI Lite外设

/ 0评 / 0

网上很多教程都是推荐大家在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);
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注