AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

AXI实战(二)-跟着产品手册设计AXI-Lite外设

图片[1]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

看完在本文后,你将可能拥有:

  1. 一个AXI_Lite转串口的从端(Slave)设计
  2. 使用SV仿真AXI-Lite总线的完整体验
  3. 实现如何在读通道中实现存储器读延迟
前文提要:
AXI实战(一)-搭建简单仿真环境

小何的AXI实战系列开更了,以下是初定的大纲安排:

图片[2]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

欢迎感兴趣的朋友关注并支持,代码获取方式见文末,以下为正文部分


  • 写在前面
  • 概要-PG导读
    • IP框图查看
    • 地址偏移
    • 读FIFO
    • 写FIFO
    • 控制寄存器 Control Register (CTRL_REG)
    • 状态寄存器Status Register (STAT_REG)
    • 中断控制
  • 串口模块实现
    • 串口发送模块
    • 串口接收模块
  • 控制寄存器,状态寄存器与中断的实现
    • 控制寄存器
    • 状态寄存器
    • 中断的实现
  • AXI-Lite总线的实现
    • 定义地址偏移量
    • AddrWrite(AW)通道
    • Write(W)通道
    • 写响应(B)通道
    • AddrRead(AR)通道
    • Read(R)通道
  • 仿真
    • 时钟和复位
    • DUT例化连线,增加回环设备
    • 测试总线错误
    • 测试读写功能
    • 查看波形
    • 仿真IP
  • 结语

写在前面

  1. AXI-Lite的实现方式有很多,在这里我们先使用Xilinx家的实现方法,比较直观也比较符合我们上一个系列的介绍:

AXI 总线(一)通道握手-AXI-Lite

中所解析的Xilinx的写法,与此同时,在有精力的情况下小何也会介绍一些开源项目的AXI总线实现方式。但实际上Xilinx的写法并不能很好利用AXI总线,我们会在后面的outstanding和out-of-order实现中介绍到。

  1. 本文主要参照Xilinx的AXI Uartlite IP进行仿照开发,相应的PG(Product Guide)为PG142.在仿真阶段也会对IP实现和RTL实现进行分别仿真。

本文使用

simulator: modelsim 2019.2

综合(compile)环境: Vivado 2019.2

概要-PG导读

在PG142中,可以从 Overview中看到其实现的框图:

图片[3]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

可以看到,在AXI到UART中,是通过寄存器和FIFO进行中介的。因为从AXI总线往里看,其控制的是就是地址上所映射的寄存器。可以看到在这个IP中包含以下几部分:

  1. AXI总线:实现总线握手和指定读写操作

  2. UART Lite 寄存器:

    • 状态寄存器(STAT_REG)
    • 控制寄存器(CTRL_REG)
    • 接收数据FIFO(Receive Data FIFO)
    • 发送数据FIFO(Transmit Data FIFO)
  3. 串口控制模块:

    • 发送控制
    • 接收控制
    • 中断控制

所以本文所需要实现的东西也非常简单,主要包括一个能与FIFO交互的串口模块,AXI的总线控制,以及一些寄存器的设置和终端控制就可以了。

为了仿真期间可以更好地验证,所以我们还是可以加入一个串口回传模块进行检验,所以所实现的框图如下:

图片[4]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

其中,Device在仿真的TB中加入。实际上也不需要加,直接将tx线连到rx就可以了。

然后通过IP框图看看如何其顶层有什么可定义的功能:

IP框图查看

图片[5]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

可以看到,在此IP中需要定义AXI总线的时钟频率,波特率,数据位以及奇偶检验位。实现过串口的朋友应该知道,这里需要定义AXI的时钟和波特率显然是为了确定波特率时钟的计数值。而数据位与奇偶校验位是为了确定并串转换以及最后滑动寄存器长度所需要。这些我们后面的代码介绍都会讲到。

需要注意的是,这里的AXI CLK Frequency是有上限的,详见PG142-Ch2 Performance节,有兴趣的朋友也可以在实现完之后加点约束看看本文所实现的模块能跑多快~

地址偏移

此处对应PG142-CH2 Register Space节。这里需要为上面所说到的配置寄存器,状态寄存器和读写FIFO安排地址。而在IP中设计的是相对地址:

图片[6]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

因为总线位宽是32,AXI按字节序排地址,所以地址偏移每个寄存器+4。

读FIFO

读FIFO的寄存器定义为:

图片[7]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

在IP中,读FIFO默认深度为16,当Master对空的读FIFO发起读的时候需要返回总线错误(SLVERR),而且Master对读FIFO发起写事务将无效。其复位值为:

图片[8]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

写FIFO

写FIFO的寄存器定义为:

图片[9]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

同样的,写FIFO的默认深度也是16.当Master对满的写FIFO写数据的时候需要返回总线错误(SLVERR),而且Master对写FIFO发起读事务将返回0。其复位值为:

图片[10]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

控制寄存器 Control Register (CTRL_REG)

控制寄存器的定义为:

图片[11]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

控制寄存器包含中断使能位和读FIFO和写FIFO的复位控制。这是一个只写寄存器。向控制寄存器发出读请求会返回0。每一位的定义为:

图片[12]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

这里需要注意,中断使能位复位值为0,所以后续的中断处理我们需要先将中断打开。

状态寄存器Status Register (STAT_REG)

状态寄存器的定义为:

图片[13]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

状态寄存器中包含了读写FIFO的空满状态,中断使能位与三个错误位:

  1. Parity Error:奇偶校验错误位,未指定时为常0
  2. Frame Error :帧错误,停止位为0
  3. Overrun Error :过载错误,收FIFO满的情况下仍然接收到新的数据

错误信息会一直保持到下一次读状态寄存器后归0。需要注意的是,最后一位并不是读FIFO的空信号,而是其”非空“信号。

中断控制

这是一直贯穿在前面的中断线管理,其作用在文档中没有单独列出。但可见于Overview中的 Interrupt Control :当中断被使能时,读FIFO的非空信号以及发FIFO的空信号将以上升沿的方式给出中断。

串口模块实现

这一部分了解串口模块的RTL实现的朋友可以跳过。在这个模块中将实现串口与FIFO之间的控制逻辑以及错误信号的生成。

串口发送模块

串口发送模块中采用状态机+波特率时钟计数编写

波特率时钟

首先根据系统时钟和波特率计算出计算周期和定时器位宽,并定义出一个波特率周期和半波特率周期:

parameter CLK_FREQ  = 100,  // Mhz
parameter BAUD_RATE = 9600,
······
localparam Num_cycle = CLK_FREQ*1_000_000/BAUD_RATE; // Mhz
localparam CNT_DW = $clog2(Num_cycle);

reg [CNT_DW-1:0] cnt_baud;




wire Baud_A_period    = (cnt_baud == Num_cycle-1);
wire Baud_Half_period = (cnt_baud == Num_cycle/2 -1);

 

然后根据计数值标志位Baud_A_period可以写出计数器:

// baud cycle counter
always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
  cnt_baud <= {CNT_DW{1'b0}};
else if(state!= next_state || Baud_A_period)
  cnt_baud <= {CNT_DW{1'b0}};
else
  cnt_baud <= cnt_baud + 1'b1;
end

 

最后因为开始位和结束位的位宽都是1, 所以针对发送的数据需要做数据位宽个计数器cnt_bit,由于奇偶检验位的存在,这里还需要计算数据位宽(这里奇偶检验位定义与Xilinx保持一致):

parameter DW = 8,
parameter PARITY    = 2'b10 // 00:no,2'b01:odd, 2'b10:even, 2'b11error
······
localparam Num_bit = DW + ^PARITY;
reg [3:0] cnt_bit;
wire Send_done = (cnt_bit == Num_bit -1);

always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
cnt_bit <= 4'b0;
else if(state == S_SEND) begin
if(Baud_A_period)
cnt_bit <= cnt_bit + 1'b1;
else
cnt_bit <= cnt_bit;
end
else
cnt_bit <= 4'b0;
end

 

发送状态机

状态机中包括:开始位,发送,以及结束位

// FSM encoding
localparam S_IDLE  = 2'b00;
localparam S_START = 2'b01; // start bit rec
localparam S_SEND  = 2'b10; // data bits
localparam S_STOP  = 2'b11; // stop bit

 

第一段,状态转移:

reg [1:0] state;
reg [1:0] next_state;
always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
  state <= S_IDLE;
else
  state <= next_state;
end

 

第二段,状态转移条件:

wire Send_done = (cnt_bit == Num_bit -1);

always @(*) begin
case (state)
S_IDLE  :
next_state = (!tx_fifo_empty) ? S_START : S_IDLE;
S_START :
next_state = Baud_A_period    ? S_SEND  : S_START;
S_SEND  :
next_state = (Baud_A_period && Send_done) ? S_STOP : S_SEND;
S_STOP  :
next_state = Baud_A_period ? S_IDLE : S_STOP;
default:
next_state = S_IDLE;
endcase
end




第三段,状态输出:

reg tx_reg;
always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
      tx_reg <= 1'b1;
  else
      case(state)
          S_IDLE,S_STOP:
              tx_reg <= 1'b1; 
          S_START:
              tx_reg <= 1'b0; 
          S_SEND:
              tx_reg <= _send_data[cnt_bit];
          default:
              tx_reg <= 1'b1; 
      endcase

 

输出数据准备

首先是计算奇偶检验位(如果存在的话):

reg parity_cal;
always @(posedge clk_i) begin
if(!rst_n_i)
    parity_cal <= 1'b0;
else if(state==S_START)
    case (PARITY)
        2'b00,2'b11:parity_cal <= 1'b0;
        2'b01:
            parity_cal <= ^tx_fifo_do ? 1'b0 : 1'b1;
        2'b10:
            parity_cal <= ^tx_fifo_do ? 1'b1 : 1'b0;
    endcase
else
    parity_cal <= parity_cal;
end

 

然后准备发送的数据,这里由于为了图方便,FIFO用了FWFT,所以时序对齐比较简单,这里会根据奇偶检验位动态生成:

reg [Num_bit-1:0] _send_data;
wire [Num_bit-1:0] _send_data_t;

generate
if(^PARITY == 1'b1)
assign _send_data_t = {parity_cal, tx_fifo_do};
else
assign _send_data_t = tx_fifo_do;
endgenerate




always @(posedge clk_i) begin
if(!rst_n_i)
_send_data <= {Num_bit{1'b0}};
else if(state == S_START)
_send_data <= _send_data_t;
else
_send_data <= _send_data;
end

 

这里的数据线序需要理一下,因为发送按tx_reg <= _send_data[cnt_bit];输出,所以_send_data的最高位是奇偶校验位最晚输出。

串口接收模块

这里与发送模块十分近似,仅列出错误计算部分:

奇偶检验出错

这里只需要采用连异或就可以了:

// parity_err check
reg parity_err_cal;
always @(*) begin
if(state==S_STOP&&Baud_Half_period)
case (PARITY)
  2'b01:  // odd
    parity_err_cal = (^_rec_data == 1'b1) ? 1'b0:1'b1;  // '1' occur odd -> right
  2'b10:  // even
    parity_err_cal = (^_rec_data == 1'b0) ? 1'b0:1'b1;  // '1' occur even -> right
  default:
    parity_err_cal = 1'b0;
endcase
else
  parity_err_cal = 1'b0;
end

reg parity_err; // output
always @(posedge clk_i) begin
if(!rst_n_i)
parity_err <= 1'b0;
else
parity_err <= parity_err_cal;
end

 

停止位为0错误

对应顶层的Frame error,判断也非常简单:

// -> parity_err
always @(posedge clk_i) begin
if(!rst_n_i)
  parity_err <= 1'b0;
else
  parity_err <= parity_err_cal;
end

reg stop_0_err; // output
always @(posedge clk_i) begin
if(!rst_n_i)
stop_0_err <= 1'b0;
else
stop_0_err <= stop_0_err_cal;
end

 

至于overrun error直接判断读FIFO就好了,在写读FIFO的同时读FIFO为满即为overrun。

控制寄存器,状态寄存器与中断的实现

在完成串口模块后,此时引入AXI中的寄存器定义,完成对FIFO的控制和状态回传。

对读写不同的寄存器,在这里引进了一个refresh信号指示当前什么寄存器被读或写了,有:

input    [1:0] refresh, //refresh = {stat_refresh, ctrl_refresh}

 

控制寄存器

重看其定义如下:

图片[12]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

配置寄存器非常简单,只需要配合refresh进行使能中断和FIFO复位即可,如:

always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
    tx_fifo_rst <= 1'b0;
else if(refresh[0])
    tx_fifo_rst <= ctrl_reg[0];
else 
    tx_fifo_rst <= 1'b0;
end

 

状态寄存器

重看其定义如下:

图片[13]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

头三个是错误位,后面指示的是FIFO的空满状态,由于错误状态一旦被读就会刷新,则可以定义:

reg [7:0] _stat_r;
reg [7:0] _stat_w;

wire [2:0] _err_r = {parity_err,stop_0_err, rx_fifo_wr&&rx_fifo_full};
always @(*) begin
if(refresh[1])
_stat_w = {3'b0, interrupt_en,
tx_fifo_full, tx_fifo_empty,rx_fifo_full,~rx_fifo_empty};
else
_stat_w = {_err_r|_stat_r[7-:3], interrupt_en,
tx_fifo_full, tx_fifo_empty,rx_fifo_full,~rx_fifo_empty};
end




always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
_stat_r <= 8'b0000_0100;
else
_stat_r <= _stat_w;
end




assign stat_reg = {24'd0, _stat_r};

 

中断的实现

在中断的设计中,需要在读FIFO的非空信号以及发FIFO的空信号将以上升沿的方式给出中断,所以控制为:

reg _uart_interrupt;
always @(posedge clk_i or negedge rst_n_i) begin
if(!rst_n_i)
    _uart_interrupt <= 1'b0;
else if(interrupt_en && ~_uart_interrupt ) begin
    if(tx_fifo_empty| ~rx_fifo_empty)
        _uart_interrupt <= 1'b1;
    else 
        _uart_interrupt <= 1'b0;
end
else 
    _uart_interrupt <= 1'b0;
end

 

AXI-Lite总线的实现

在经过以上的抽象后,我们仅需在AXI-Lite上放出这两个寄存器和两个FIFO就可以了,同时注意有部分特殊情况需要返回总线错误。为了方便衔接,在本文中绝大多数实现方式与Xilinx实现方式是一致的,也方便后续在提到优化的时候来指出Xilinx的代码问题。

定义地址偏移量

这里的定义在后续的文章中可能会不一样,但是整体是一致的:

localparam ADDRLSB = $clog2(C_AXI_DATA_WIDTH/8); // 字节序再取log -> 偏移位宽
// address mapping
localparam [C_AXI_ADDR_WIDTH-1 -ADDRLSB:0]  UART_RX_FIFO = 'd0,
                                            UART_TX_FIFO = 'd1,
                                            UART_STA_REG = 'd2,
                                            UART_CTR_REG = 'd3;
reg [C_AXI_ADDR_WIDTH-1 -ADDRLSB:0] axi_awaddr;
reg [C_AXI_ADDR_WIDTH-1 -ADDRLSB:0] axi_araddr;

 

需要注意这里的地址定义的是0->1->2->3 是经过ADDRLSB的计算移位的,在32位的情况下,ADDRLSB=2,所以当0123左移四位便得到了048C。

By the way, 这个ADDRLSB的写法纠正了Xilinx之前的求法:

localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32)+1;

 

Xilinx的这种写法在位宽大于32的时候是跟上面的写法对等的,但是显然上面的写法更加适合理解并适用于低位宽的AXI总线。

AddrWrite(AW)通道

此处除了写入地址外,基本就是Xilinx的示例代码

// --------------------------- AW channel -----------------------------------
reg aw_en;
always @( posedge S_AXI_ACLK )begin
if ( S_AXI_ARESETN == 1'b0 ) begin
  axi_awready <= 1'b0;  //  协议ready
  aw_en <= 1'b1;   //  模块ready
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 // 写回应结束,模块内恢复ready
    axi_awready <= 1'b0;
    aw_en <= 1'b1;
  end
  else begin
    axi_awready <= 1'b0;
  end
end
end

// latch aw addr
always @( posedge S_AXI_ACLK )begin
if ( S_AXI_ARESETN == 1'b0 )begin
axi_awaddr <= 2'b00;
end
else begin
if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en) begin
// Write Address latching 
axi_awaddr <= S_AXI_AWADDR[C_AXI_ADDR_WIDTH-1: ADDRLSB];
end
end
end

 

这个axi_awaddr就是前面所说过的偏移量,直接截掉低两位后048C将变为0123。

Write(W)通道

在写通道中,不像是例程中简单,需要根据不同地址进行数据写入。在这个IP中,只有写FIFO和控制寄存器被允许写入。这里的数据写入判断和Xilinx例程也是类似的,但是会精简了S_AXI_WSTRB的判断,因为我们的寄存器都只有低八位是有效的:

首先是握手:

always @( posedge S_AXI_ACLK ) begin
if ( S_AXI_ARESETN == 1'b0 )
    axi_wready <= 1'b0;
else if (~axi_wready && S_AXI_WVALID && S_AXI_AWVALID && aw_en ) 
    axi_wready <= 1'b1;
else
    axi_wready <= 1'b0;
end

 

然后是根据地址判断写入的信息,第一个是写FIFO:

reg _tx_fifo_wr;
reg [31:0] _tx_fifo_di;
always @( posedge S_AXI_ACLK ) begin
if ( S_AXI_ARESETN == 1'b0 ) begin
  _tx_fifo_di <= 32'd0;
  _tx_fifo_wr <= 1'b0;
end
else if (axi_wready && axi_awaddr==UART_TX_FIFO &&
      S_AXI_AWVALID && (S_AXI_WSTRB[0] == 1'b1)) begin
  _tx_fifo_wr <= ~tx_fifo_full;
  _tx_fifo_di <= S_AXI_WDATA;
end
else begin
  _tx_fifo_wr <= 1'b0;
  _tx_fifo_di <= _tx_fifo_di;
end
end

 

最后是控制寄存器

reg [31:0] _ctrl_reg;
always @( posedge S_AXI_ACLK ) begin
if ( S_AXI_ARESETN == 1'b0 ) begin
  _ctrl_reg <= 32'd0;
  ctrl_refresh <= 1'b0;
end
else if (axi_wready && axi_awaddr==UART_CTR_REG &&
        S_AXI_AWVALID && (S_AXI_WSTRB[0] == 1'b1)) begin
    _ctrl_reg <= S_AXI_WDATA;
    ctrl_refresh <= 1'b1;
  end
else begin
  ctrl_refresh <= 1'b0;
  _ctrl_reg <= _ctrl_reg;
end
end

 

这里会同步有一个使能信号,如上文所述,到后面assign到refresh里面去了。

写响应(B)通道

这里需要注意,当手册上写到,当Master往满的FIFO写数据时,需要报总线错误RESP_SLVERR,所以有:

// --------------------------- B channel -----------------------------------
always @( posedge S_AXI_ACLK ) begin
  if ( S_AXI_ARESETN == 1'b0 ) begin
    axi_bvalid <= 0;
    axi_bresp  <= 0;
  end 
  else begin    
    if (axi_awready && S_AXI_AWVALID && ~axi_bvalid 
        && axi_wready && S_AXI_WVALID) begin
        axi_bvalid <= 1'b1;
        axi_bresp  <= (axi_awaddr==UART_TX_FIFO && tx_fifo_full) ? 2'b10 : 2'b00;
      end   
    else if (axi_bvalid && S_AXI_BREADY) begin
        // Read data is accepted by the master
        axi_bvalid <= 1'b0;
      end                
  end
end

 

AddrRead(AR)通道

在AR通道上和AW是一样的

// --------------------------- AR channel -----------------------------------
always @( posedge S_AXI_ACLK ) begin
if ( S_AXI_ARESETN == 1'b0 ) begin
    axi_arready <= 1'b0;
    axi_araddr  <= 2'b0;
end 
else begin    
    if (~axi_arready && S_AXI_ARVALID) begin
        // indicates that the slave has acceped the valid read address
        axi_arready <= 1'b1;
        // Read address latching
        axi_araddr  <= S_AXI_ARADDR[C_AXI_ADDR_WIDTH-1: ADDRLSB];
    end
    else begin
        axi_arready <= 1'b0;
    end
end 
end

 

Read(R)通道

现在到了这个设计最难的一步了,就是假设我们的读FIFO并不是一个FWFT的FIFO,那么其读延迟会有两个时钟周期的延迟,所以这个时候需要先不拉高rvalid。所以这里小何的办法是当其读FIFO的时候寄存读使能,当两拍后检测到其下降沿时再拉高rvalid,代码如下:

// --------------------------- R channel -----------------------------------
reg _rx_fifo_rd,_rx_fifo_rd0,_rx_fifo_rd1;
always @( posedge S_AXI_ACLK ) begin
  if ( S_AXI_ARESETN == 1'b0 ) begin
      axi_rvalid  <= 0;
      axi_rresp   <= 0;
      _rx_fifo_rd <= 1'b0;
      stat_refresh <= 1'b0;
  end 
  else begin    
      stat_refresh <= 1'b0;
      _rx_fifo_rd  <= 1'b0;
      if (axi_arready && S_AXI_ARVALID && ~axi_rvalid) begin
          // Valid read data is available at the read data bus
            axi_rvalid <= 1'b1;
            if(axi_araddr==UART_RX_FIFO && ~rx_fifo_empty) begin  // 2 clock latency 
                axi_rvalid <= 1'b0;
                _rx_fifo_rd <=1'b1;
            end
            else if(axi_araddr==UART_STA_REG)      // read register 
                stat_refresh <= 1'b1;
          //When a read request to empty FIFO, a bus error (SLVERR) is generated
            axi_rresp  <= (axi_araddr==UART_RX_FIFO && rx_fifo_empty) ? 2'b10 : 2'b00;
          end
      else if({_rx_fifo_rd1,_rx_fifo_rd0} == 2'b10)
            axi_rvalid <= 1'b1;
      else if (axi_rvalid && S_AXI_RREADY) 
          // Read data is accepted by the master
          axi_rvalid <= 1'b0;
      else begin
        axi_rvalid <=axi_rvalid;
        axi_rresp <= axi_rresp;
      end
  end
end

 

这里利用了axi_arready握手完就会拉低的逻辑,然后时读使能的寄存:

always @( posedge S_AXI_ACLK ) begin
if ( S_AXI_ARESETN == 1'b0 ) 
    {_rx_fifo_rd0,_rx_fifo_rd1} <= 2'b00;
else     
    {_rx_fifo_rd1,_rx_fifo_rd0} <= {_rx_fifo_rd0,_rx_fifo_rd};
end

 

最后是选择数据:

always @( posedge S_AXI_ACLK ) begin
if (!S_AXI_RVALID || S_AXI_RREADY) begin
    casez (axi_araddr)
        UART_RX_FIFO: axi_rdata <= rx_fifo_do;
        UART_TX_FIFO: axi_rdata <= 32'd0;
        UART_STA_REG: axi_rdata <= stat_reg;
        UART_CTR_REG: axi_rdata <= 32'd0;
    endcase
  end
end

 

至此,AXI-Lite转UART的全过程就设计完毕了~

 

仿真

在仿真章节中用到了上一小节中所介绍的”完美”主机,未知的朋友可以回看上一节的介绍,要是不管实现的话其实非常容易使用。如以下的使用步骤:

时钟和复位

和一般TB写法一致,此处略过

DUT例化连线,增加回环设备

在这里需要把我们设计的模块连进interface,并且将串口模块做一个简单的回传:

也就是说,这里我们并不需要实现Device,直接把用线回传就可以了

图片[16]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

如上一节讲的一样,这里我们直接连线:

module tb_axi_lite_uart #(
  parameter  DW = 8,
  parameter  CLK_FREQ = 100,
  parameter  BAUD_RATE = 115200,
  parameter  PARITY = 2'b10, // 00 -> no parity -> 2'b01:odd -> 2'b10:even -> 2'b11error
    ......
  Axil_uart_top #(
    .C_AXI_ADDR_WIDTH(C_AXI_ADDR_WIDTH ),
    .C_AXI_DATA_WIDTH(C_AXI_DATA_WIDTH ),
    .DW(DW ),
    .CLK_FREQ(CLK_FREQ ),
    .BAUD_RATE(BAUD_RATE ),.PARITY ( PARITY ))
  Axil_uart_top_dut (
    .S_AXI_ACLK (clk ),
    .S_AXI_ARESETN (rst_n ),
    .S_AXI_AWVALID (master.aw_valid ),
    .S_AXI_AWREADY (master.aw_ready ),
    .S_AXI_AWADDR (master.aw_addr ),
    .S_AXI_AWPROT (master.aw_prot ),
    .S_AXI_WVALID (master.w_valid ),
    .S_AXI_WREADY (master.w_ready),
    .S_AXI_WDATA (master.w_data ),
    .S_AXI_WSTRB (master.w_strb ),
    .S_AXI_BVALID (master.b_valid ),
    .S_AXI_BREADY (master.b_ready ),
    .S_AXI_BRESP (master.b_resp ),
    .S_AXI_ARVALID (master.ar_valid ),
    .S_AXI_ARREADY (master.ar_ready ),
    .S_AXI_ARADDR (master.ar_addr ),
    .S_AXI_ARPROT (master.ar_prot ),
    .S_AXI_RVALID (master.r_valid),
    .S_AXI_RREADY (master.r_ready ),
    .S_AXI_RDATA ( master.r_data ),
    .S_AXI_RRESP (master.r_resp ),
    .uart_rx_phy (uart_tx_phy ),
    .uart_tx_phy (uart_tx_phy ),
    .uart_interrupt(uart_interrupt)
  );

 

测试总线错误

根据Spec要求,需要在发FIFO满的时候再发送数据和当读FIFO为空时需要返回总线错误RESP_SLVERR,所以我们会有一下测试方法:

// testing RESP_SLVERR
lite_axi_master.read(axi_addr_t'(32'h0000_0000), axi_pkg::prot_t'('0), stat_data, resp);
assert (resp == axi_pkg::RESP_SLVERR) else $fatal(1, "Fail to assert RESP_SLVERR");
$display("%0t > RESP_SLVERR Received stat reg : %h RESP: %h", $time(), stat_data, resp);

while(wrong_resp == axi_pkg::RESP_OKAY)
lite_axi_master.write(axi_addr_t'(32'h0000_0004), axi_pkg::prot_t'('0),
axi_data_t'(0), axi_strb_t'(4'hF), wrong_resp);
$display("%0t > RESP_SLVERR Send FIFO : FULL RESP: %h", $time(),wrong_resp);




lite_axi_master.write(axi_addr_t'(32'h0000_000C), axi_pkg::prot_t'('0),
axi_data_t'(32'h0000_0003), axi_strb_t'(4'hF), resp);
assert (resp == axi_pkg::RESP_OKAY) else $fatal(1, "Fail to Reset ALL FIFO");
$display("%0t > Reset ALL FIFO RESP: %h", $time(),resp);

 

仿真后提示为:

图片[17]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

可以看到成功触发了错误。

测试读写功能

首先第一步是打开中断:

// Write to config register -> enable interrupt 
lite_axi_master.write(axi_addr_t'(32'h0000_000C), axi_pkg::prot_t'('0),
    axi_data_t'(32'h0000_0010), axi_strb_t'(4'hF), resp);
assert (resp == axi_pkg::RESP_OKAY) else $fatal(1, "Fail to enable interrupt");
$display("%0t > Success to enable interrupt, with addr", $time(), data, resp);

lite_axi_master.read(axi_addr_t'(32'h0000_0008), axi_pkg::prot_t'('0), stat_data, resp);
$display("%0t > Received stat reg : %h RESP: %h", $time(), stat_data, resp);

 

此时我们可以监听中断信号,然后去读状态寄存器,发现读FIFO非空就可以读数据出来了,所以会一直读状态寄存器。小何在这里把Lite Master的读display给注释掉了,代码如下:

for(int i=0;i<40;i++) begin
  lite_axi_master.write(axi_addr_t'(32'h0000_0004), axi_pkg::prot_t'('0),
  axi_data_t'(i), axi_strb_t'(4'hF), resp);
  // axi_data_t'(32'h0000_0052), axi_strb_t'(4'hF), resp);
  assert (resp == axi_pkg::RESP_OKAY) else $fatal(1, "Fail to send data");
  $display("%0t > transmit data %h RESP:%h ", $time(), i, resp);
  // Read from it.
  @(posedge uart_interrupt);
  lite_axi_master.read(axi_addr_t'(32'h0000_0008), 
                       axi_pkg::prot_t'('0), stat_data, resp);
  assert (resp == axi_pkg::RESP_OKAY) else $fatal(1, "Fail to recv stat_data.");
  while(stat_data[0]!=1'b1) begin
    lite_axi_master.read(axi_addr_t'(32'h0000_0008), 
                         axi_pkg::prot_t'('0), stat_data, resp);
    // $display("%0t > Received stat reg : %h RESP: %h", $time(), stat_data, resp);
  end
  lite_axi_master.read(axi_addr_t'(32'h0000_0000), axi_pkg::prot_t'('0), data, resp);
  assert (resp == axi_pkg::RESP_OKAY) else $fatal(1, "Fail to recv data.");
    // Checking of the expecetd read data is handled in `proc_check_read_data`.
  $display("%0t > receive data %h RESP:%h ", $time(), data, resp);
end

 

然后看一下display

图片[18]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

数据也没有什么大问题

查看波形

作为老仿真人,还是从波形来看看AXI的握手情况:

SystemVerilog下的仿真掉波形非常简单,只要点interface类型,然后直接ctrl+W就可以了:

图片[19]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

仿真IP

对相同的IP,在这里我们也做一次一样的仿真,防止仿真平台自己出错:

图片[20]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

得到了相同的效果,此时查看波形:

图片[21]-AXI实战(二)-跟着产品手册设计AXI-Lite外设-Xilinx-AMD社区-FPGA CPLD-ChipDebug

可以看到读通道有一段是未知态,但是其中没有握手就没事情了。大体的握手波形是一致的。

结语

在这节中,我们以串口为例介绍了AXI-Lite的Slave实现,并利用了上一节中介绍的仿真工具进行了仿真,为了验证仿真工具自己的正确性还仿真了一下Xilinx的自带IP。

 

请登录后发表评论

    没有回复内容