深入理解AMBA总线(十八)一个简单的AXI2SRAM设计

简介: 深入理解AMBA总线(十八)一个简单的AXI2SRAM设计

到目前为止,我们已经完整的学习了APB、AHB、AXI这三组协议。希望之前的文章对大家能够有所帮助。本篇文章讲解一个简单的AXI Slave的设计,其它复杂的AXI Master或者Slave,就留给大家自己去探索了。因为这种代码动辄就是上千行,在网上讲也很难讲清楚。希望能够用一个简单的例子带大家巩固AXI的基本设计思路,话不多说,让我们进入正题。

1、AXI2MEM简介

本模块是ETH开源项目PULP的一个小模块,其完整代码链接如下:

https://link.zhihu.com/?target=https%3A//github.com/pulp-platform/axi_mem_if/blob/master/src/axi2mem.sv

该模块的作用,是将AXI协议转换为SRAM的读写时序,就比如在下图这个例子中,Native接口的SRAM是无法直接和AXI的接口直接相连的,我们必须借助这种协议转换模块,完成两种协议之间的通信。

我们设计的模块接口非常简单,一边是AXI矩阵传来的AXI接口,另外一边是SRAM的native接口,如下图所示。

2、AXI2MEM设计思路及代码讲解

我们设计的AHB2MEM规格说明如下:

  • 不支持Outstanding,阻塞型读写
  • 我们连接的SRAM是单端口SRAM,因此我们的AXI2MEM不支持也不必支持同时读写
  • 支持突发读写、不跨4KB边界
  • 对于wrapping burst,burst length必须是2、4、8、16
  • 一旦突发读写开始,不允许中途停止

既然是阻塞性读写,再结合AXI的通道依赖关系,自然而然应该想到,可以用状态机去设计

此外明确我们设计的是AXI的Slave,设计的时候应该去回顾AXI的握手协议和读写时序等。

我们先只考虑怎么去读,读的话有两个通道,分别是AR通道和R通道。且读的话AR通道必须在前、R通道在后。理论上给了读地址通道的控制信号,就可以跳到读状态,然后一直等数据回复、直到最后一笔数据回来即可。因此读的话应该是涉及两个状态,我们将其定义为IDLE、READ状态 。什么时候可以从IDLE跳转到READ状态呢?那自然是来了读地址通道的CMD,什么时候从READ跳回到IDLE状态呢?应该是完成了一次突发传输,即收到了rlast信号。

我们看一下下面的代码,一开始为IDLE状态,意味着此时没有处理任何的读写。可以接收读请求或者写请求,当Master侧传来了ar_valid信号,相应的可以把CMD全部给采集起来,然后跳转到READ状态。此时我们可以直接把req_o和addr_o赋值,这样下一个时钟周期的上升沿,即跳转到READ状态的时候,已经开始采集数据了。相应的可以节约一个时钟周期。此外还需要用寄存器记录一下此时的地址和对应的cnt(有什么用呢?接下来就说)。

case (state_q)
            IDLE: begin
                // Wait for a read or write
                // ------------
                // Read
                // ------------
                if (slave.ar_valid) begin
                    slave.ar_ready = 1'b1;
                    // sample ax
                    ax_req_d       = {slave.ar_id, slave.ar_addr, slave.ar_len, slave.ar_size, slave.ar_burst};
                    state_d        = READ;
                    //  we can request the first address, this saves us time
                    req_o          = 1'b1;
                    addr_o         = slave.ar_addr;
                    // save the address
                    req_addr_d     = slave.ar_addr;
                    // save the ar_len
                    cnt_d          = 1;

我们再看一下READ状态的代码,如果Master可以接收数据,那么就应该接着去读数据,突发一共有三种分类,我们根据此分类去计算相应的地址。当最后一笔读数据回来的时候,应该重新回到IDLE状态。

此外,对于ax_req_d、cnt、addr信号,都需要用寄存器去寄存。我们使用的时候用的是其q端,然后基于Q端用组合逻辑给其D端赋值,相应的用寄存器从D打到Q端。实际上这也是状态机。(和状态机的cs和ns相对应!)

READ: begin
                // keep request to memory high
                req_o  = 1'b1;
                addr_o = req_addr_q;
                // send the response
                slave.r_valid = 1'b1;
                slave.r_data  = data_i;
                slave.r_user  = user_i;
                slave.r_id    = ax_req_q.id;
                slave.r_last  = (cnt_q == ax_req_q.len + 1);
                // check that the master is ready, the slave must not wait on this
                if (slave.r_ready) begin
                    // ----------------------------
                    // Next address generation
                    // ----------------------------
                    // handle the correct burst type
                    case (ax_req_q.burst)
                        FIXED, INCR: addr_o = cons_addr;
                        WRAP:  begin
                            // check if the address reached warp boundary
                            if (cons_addr == upper_wrap_boundary) begin
                                addr_o = wrap_boundary;
                            // address warped beyond boundary
                            end else if (cons_addr > upper_wrap_boundary) begin
                                addr_o = ax_req_q.addr + ((cnt_q - ax_req_q.len) << LOG_NR_BYTES);
                            // we are still in the incremental regime
                            end else begin
                                addr_o = cons_addr;
                            end
                        end
                    endcase
                    // we need to change the address here for the upcoming request
                    // we sent the last byte -> go back to idle
                    if (slave.r_last) begin
                        state_d = IDLE;
                        // we already got everything
                        req_o = 1'b0;
                    end
                    // save the request address for the next cycle
                    req_addr_d = addr_o;
                    // we can decrease the counter as the master has consumed the read data
                    cnt_d = cnt_q + 1;
                    // TODO: configure correct byte-lane
                end
            end
    // --------------
    // Registers
    // --------------
    always_ff @(posedge clk_i or negedge rst_ni) begin
        if (~rst_ni) begin
            state_q    <= IDLE;
            ax_req_q  <= '0;
            req_addr_q <= '0;
            cnt_q      <= '0;
        end else begin
            state_q    <= state_d;
            ax_req_q   <= ax_req_d;
            req_addr_q <= req_addr_d;
            cnt_q      <= cnt_d;
        end
    end

看完了读,我们再来看写。对于AXI的写,实际上写地址通道和写数据通道谁在前都可以,但很多时候为了简化设计,我们的Slave可以先看写地址通道,写地址通道没有握手,不允许写数据通道握手。对于AXI Master侧实际上是感知不到这件事的,它只会看到没有握手,然后一直拉高Valid。所有AW先拉高还是W先拉高都是没有关系的。我们看具体的代码实现:

可以看到,优先只看aw_valid,当写地址通道valid拉高以后,就可以拉高ready握手成功,然后采样相应的控制信号,并将地址先赋值。然后我们看w_valid是否拉高,如果拉高直接跳到写状态,并将写使能、req都拉高。如果我们只写这一笔数据即last拉高,就直接跳到等待Back的SEND_B状态,否则跳到WRITE状态。

end else if (slave.aw_valid) begin
                    slave.aw_ready = 1'b1;
                    slave.w_ready  = 1'b1;
                    addr_o         = slave.aw_addr;
                    // sample ax
                    ax_req_d       = {slave.aw_id, slave.aw_addr, slave.aw_len, slave.aw_size, slave.aw_burst};
                    // we've got our first w_valid so start the write process
                    if (slave.w_valid) begin
                        req_o          = 1'b1;
                        we_o           = 1'b1;
                        state_d        = (slave.w_last) ? SEND_B : WRITE;
                        cnt_d          = 1;
                    // we still have to wait for the first w_valid to arrive
                    end else
                        state_d = WAIT_WVALID;
                end
            end
            // ~> we are still missing a w_valid
            WAIT_WVALID: begin
                slave.w_ready = 1'b1;
                addr_o = ax_req_q.addr;
                // we can now make our first request
                if (slave.w_valid) begin
                    req_o          = 1'b1;
                    we_o           = 1'b1;
                    state_d        = (slave.w_last) ? SEND_B : WRITE;
                    cnt_d          = 1;
                end
            end

然后我们再看写状态和等待写响应的状态,WRITE状态和READ状态差不多。重点看一下SEND_B状态,这个状态下把b_valid拉高,等待Master侧bready握手即可。握手成功跳回IDLE态,完成整个写过程。

// ~> we already wrote the first word here
            WRITE: begin
                slave.w_ready = 1'b1;
                // consume a word here
                if (slave.w_valid) begin
                    req_o         = 1'b1;
                    we_o          = 1'b1;
                    // ----------------------------
                    // Next address generation
                    // ----------------------------
                    // handle the correct burst type
                    case (ax_req_q.burst)
                        FIXED, INCR: addr_o = cons_addr;
                        WRAP:  begin
                            // check if the address reached warp boundary
                            if (cons_addr == upper_wrap_boundary) begin
                                addr_o = wrap_boundary;
                            // address warped beyond boundary
                            end else if (cons_addr > upper_wrap_boundary) begin
                                addr_o = ax_req_q.addr + ((cnt_q - ax_req_q.len) << LOG_NR_BYTES);
                            // we are still in the incremental regime
                            end else begin
                                addr_o = cons_addr;
                            end
                        end
                    endcase
                    // save the request address for the next cycle
                    req_addr_d = addr_o;
                    // we can decrease the counter as the master has consumed the read data
                    cnt_d = cnt_q + 1;
                    if (slave.w_last)
                        state_d = SEND_B;
                end
            end
            // ~> send a write acknowledge back
            SEND_B: begin
                slave.b_valid = 1'b1;
                slave.b_id    = ax_req_q.id;
                if (slave.b_ready)
                    state_d = IDLE;
            end

综上整个设计完成,可以看到其足够应付基本的AXI读写功能请求,在连接单端口SRAM的时候,其浪费的Cycle很少。并且在读写过程中,其aw_ready和ar_ready都会拉低,用于反压上级。这个接口和SRAM整体看,就是握手型的SRAM,很多工业界代码也会使用这种SRAM,大家可以认真学习感受其设计思想。

目录
相关文章
|
芯片 SoC
深入理解AMBA总线(零)绪论
深入理解AMBA总线(零)绪论
375 0
|
存储 网络性能优化 vr&ar
深入理解AMBA总线(十七)AXI是如何提高性能的
深入理解AMBA总线(十七)AXI是如何提高性能的
2097 1
|
芯片
深入理解AMBA总线(一)APB总线入门(上)
深入理解AMBA总线(一)APB总线入门
1097 0
|
SoC
深入理解AMBA总线(十六)AXI设计的关键问题(二)
深入理解AMBA总线(十六)AXI设计的关键问题(二)
879 0
深入理解AMBA总线(十六)AXI设计的关键问题(二)
|
SoC
深入理解AMBA总线(七)AHB设计要点和AHB2APB同步桥设计前言
深入理解AMBA总线(七)AHB设计要点和AHB2APB同步桥设计前言
511 0
深入理解AMBA总线(七)AHB设计要点和AHB2APB同步桥设计前言
|
存储 SoC
深入理解AMBA总线(十一)AXI协议导论
深入理解AMBA总线(十一)AXI协议导论
1730 0
|
存储 安全
深入理解AMBA总线(四)AHB-lite总线
深入理解AMBA总线(四)AHB-lite总线
1162 0
|
缓存 内存技术
深入理解AMBA总线(十六)AXI设计的关键问题(一)
深入理解AMBA总线(十六)AXI设计的关键问题
485 0
|
缓存 SoC
深入理解AMBA总线(八)AHB2APB同步桥设计
深入理解AMBA总线(八)AHB2APB同步桥设计
777 0
|
异构计算 SoC 内存技术
深入理解AMBA总线(九)AHB2SRAM设计
深入理解AMBA总线(九)AHB2SRAM设计
802 0