Menu Close

Vivado下利用TDP RAM 实现乒乓Buffer实例

在上节课Vivado真双端口(TDP)RAM IP核的生成与配置详解中学习了在Vivado下如何配置一个真双端口RAM IP核。本文将使用之前生成的RAM IP核,设计一个乒乓Buffer(更多信息关于乒乓Buffer见文章数据缓冲与乒乓Buffer)。

1.乒乓Buffer简介

在工程中,乒乓Buffer(或Double Buffer)是通信工程中经常使用的一种手段,主要在以下场景使用:

  • 异步过程
  • 异时钟域下数据的转换
  • 不同位宽数据的转换
  • 数据需要缓存

乒乓Buffer的优点是可以提高数据的处理效率,并可以分离时钟域,分离两层不同的操作逻辑。乒乓Buffer的应用非常广泛,本文在这里先给大家介绍最基本的原理,以及简单的代码实现,为将来在真正工程中的使用打下基础。

乒乓Buffer 的原理是使用两个内存块Buffer 0,Buffer 1,当有数据进来的时候,就先填充两个内存块中没有被占用,可以写的端口,比如先将数据填充进Buffer 0端的内存,当Buffer 0端被填满后,如果还有数据继续进来的话,就换成填充Buffer 1端的内存,同时处理Buffer 0端的数据。等到Buffer 1端也充满后,转而填充已经处理完数据的Buffer 0端。当数据填充Buffer 0端时,Buffer 1端口正在处理数据。以此类推,Buffer 0,Buffer 1端交替填充数据和处理数据。因为这两个操作是并行的,所以乒乓Buffer的处理速度会提升一倍,这也是FPGA使用面积换取效率的一个典型应用。

乒乓Buffer也有其局限性,在接收严重不等长的数据包(数据颠簸)时,可能会因为两个Buffer处理数据的速度不一样,而不能确保数据输入的连续性或是顺序性。比如1个大数据包后接着1个小数据包写入乒乓Buffer,因为小数据包的处理速度远快于大数据包,所以在写完1个大数据包和1个小数据包后(写完小数据包,大数据包的读操作还在进行),准备写入第3个数据包时,因为大数据包的数据处理速度不够快,数据写操作会被迫中止,如图1所示。数据颠簸的解决办法是在乒乓Buffer后加上有弹性的调节FIFO,具体实例的使用在之后的工程中介绍。

图1 乒乓Buffer数据颠簸

 

实际上,乒乓Buffer还可以扩充成4个端口,8个端口等,原理与两个端口的情况一致,速度上可以提高更多倍数。乒乓Buffer 的应用,可以参见文章以太网工程中双端口的使用。本文中使用的乒乓Buffer(生成的RAM IP核参见文章Vivado真双端口(TDP)RAM IP核的生成与配置详解)的示意图如下所示:

图2 本工程里使用的乒乓Buffer示意图

前面生成的真双端口RAM的大小是256 X 8,所以这里将整个RAM分成了两个128 X 8的Buffer,分别为Buffer 0和Buffer 1。注意对于真双端口RAM来说,Port A和Port B都是可以进行读写操作的,所以使得乒乓Buffer更加灵活。本工程实现了一个简单的乒乓Buffer,使Port A和Port B的口线宽度相同,并且使用的是同步同频的时钟。

在Buffer 0的127处和Buffer 1的255处,存储的不是数据,而是一个单bit的控制信号buf_flag和7-bit的数据长度data_length。Buf_flag的作用是标志当前的Buffer里的数据是否有效。在本节课的实例中,数据有效时,buf_flag的值为1;数据无效时,buf_flag的值为0。在读或写的操作完成后,将buf_flag + 1,即可切换读写模式。

2.工程代码

乒乓Buffer读写的代码如下:

`timescale 1ns / 1ps


module top
(
    input  OSC_CLK,
    output [7:0] LED
);

wire clk_50m;

wire locked;
SYS_MMCM  SYS_MMCM_inst
(
    // Clock in ports
    .clk_in1    (OSC_CLK),  // input clk_in1
    // Clock out ports
    .clk_out1   (clk_50m),  // output clk_out1
    // Status and control signals
    .reset      (1'b0),     // input reset
    .locked     (locked)    // output locked
);



wire clk = clk_50m;
wire reset = ~locked;
//=============================================================
localparam BUF_SIZE = 7;
localparam BUF_ID = 1;

reg  wea;
reg  [BUF_ID - 1: 0] addra_id;			//switch between Buffer 0 and Buffer 1
reg  [BUF_SIZE - 1: 0] addra_sub;	
wire [BUF_SIZE + BUF_ID - 1 :0] addra = {addra_id, addra_sub};
reg  [7:0] dina;
wire [7:0] douta;

reg  web;
reg  [BUF_ID - 1: 0] addrb_id;			//switch between Buffer 0 and Buffer 1
reg  [BUF_SIZE - 1: 0] addrb_sub;
wire [BUF_SIZE + BUF_ID - 1 :0] addrb = {addrb_id, addrb_sub};
reg  [7:0] dinb = 0;
wire [7:0] doutb;



// ---------------- Ping Pong Buffer -----------
tdp_ram ping_pong_inst (
  .clka		(clk),    	// input wire clka
  .wea		(wea),      // input wire [0 : 0] wea
  .addra	(addra),  	// input wire [7 : 0] addra
  .dina		(dina),    	// input wire [7 : 0] dina
  .douta	(douta),  	// output wire [7 : 0] douta
  .clkb		(clk),    	// input wire clkb
  .web		(web),      // input wire [0 : 0] web
  .addrb	(addrb),  	// input wire [7 : 0] addrb
  .dinb		(dinb),    	// input wire [7 : 0] dinb
  .doutb	(doutb)  	// output wire [7 : 0] doutb
);

//================== write operation =========================
reg [2:0] porta_st = 0;

always @ (posedge clk or posedge reset)
if(reset)
begin
    addra_id <= 0;
    porta_st <= 0;
end
else
begin
    case (porta_st)
    0:
    begin
        dina <= {addra_id, 7'h0};  		//data 0
        wea <= 0;
        addra_sub <= {BUF_SIZE{1'b1}};	//addr = 127
        porta_st <= 1;
    end
    1:
    begin
        porta_st <= 2;			//hold for
    end
    2:
    begin
        if( douta[7] == 0 )
            porta_st <= 3;
    end
    3:
    begin
        if(addra_sub == 63)
        begin
            wea <= 0;
            porta_st <= 4;
        end
        else
        begin
            addra_sub <= addra_sub + 1;
            dina <= dina + 1;
            wea <= 1;
        end
    end
    4:
    begin
        addra_sub <= {BUF_SIZE{1'b1}};
        dina <= {1'b1, 7'b100_0000};
        wea <= 1;
        porta_st <= 5;
    end
    5:
    begin
        wea <= 0;
        addra_id <= addra_id + 1;
        porta_st <= 0;
    end
    default: porta_st <= 0;
    endcase
end
//==================== read operation ========================
reg [2:0] portb_st = 0;

reg [6:0] len = 0;
reg [6:0] cnt = 0;

reg [7:0] led_r = 0;
always @ (posedge clk or posedge reset)
if(reset)
begin
    addrb_id <= 0;
    portb_st <= 0;
end
else
begin
    case (portb_st)
    0:
    begin
        web <= 0;
        cnt <= 0;
        addrb_sub <= {BUF_SIZE{1'b1}};
        portb_st <= 1;
    end
    1:
    begin
        portb_st <= 2;
    end
    2:
    begin
        if( doutb[7] == 1 )
        begin
            addrb_sub <= 0;
            portb_st <= 3;
        end
    end
    3:
    begin
        len <= doutb[6:0];
        addrb_sub <= 1;
        portb_st <= 4;
    end
    4:
    begin
        if(cnt == len)
        begin
            portb_st <= 5;
        end
        else
        begin
            cnt <= cnt + 1;
            led_r <= doutb;
            addrb_sub <= addrb_sub + 1;
        end
    end
    5:
    begin
        addrb_sub <= {BUF_SIZE{1'b1}};
        web <= 1;
        portb_st <= 6;
    end
    6:
    begin
        web <= 0;
        addrb_id <= addrb_id + 1;
        portb_st <= 0;
    end
    default: portb_st <= 0;
    endcase
end

assign LED = led_r;
endmodule




 

注意这里的变量声明都是参数化。比如BUF_SIZE,这个表示当前每个Buffer的大小是2^BUF_SIZE。比如当BUF_SIZE 等于7时,每个Buffer的大小是128,因为双端口RAM的总大小是256,所以通过BUF_SIZE可以知道当前的双端口里分为两个Buffer。如果想要使用单个Buffer,那么只需要修改BUF_SIZE为8,Buffer的大小变为256;或者如果想要使用4个Buffer,可以将BUF_SIZE设置为6,每个Buffer的大小为64。同时注意将BUF_ID调整为2-bit,可以识别4个不同的Buffer。

以A端口举例,以下代码:

reg [BUF_ID - 1: 0] addra_id;
reg [BUF_SIZE - 1: 0] addra_sub;

wire [BUF_SIZE + BUF_ID - 1 :0] addra = {addra_id, addra_sub};

表示将双端口RAM的地址分为两个部分,addra_sub是基地址。最高位可以在Buffer之间切换,比如对于addra_sub = 7’b111_1111时,如果addra_id = 0,表示当前地址是{0, 111_1111} = 127,即Buffer 0的最高位,里面存储的是Buffer 0的buf_flag(指示当前Buffer是否可以读/写)以及Buffer中的数据长度;当addra_id = 1,表示当前地址是{1, 111_1111} = 255,即Buffer 1的最高位,里面存储的是Buffer 1的buf_flag和Buffer中的数据长度。由此看出,将地址分成addra_id和addra_sub使得在两个Buffer中可以灵活切换。

3.状态机分析

第一个状态机(porta_st)这里在A端口自Buffer 0开始对两个Buffer分别进行了写操作。将0-63, 共64个数据写入Buffer。其状态转移如下图所示:

图3

状态0:将基地址addra_sub调整到最高位,准备读出buf_flag,来指示写操作什么时候开始。复位后默认的addra_id是0,即先准备对Buffer 0进行操作。

状态1:跳到了状态2,没有做任何其它的事,这是因为双端口RAM读数据需要一个时钟之后才能完成,所以这里插入了一个空状态。

状态2:判断当前Buffer的buf_flag,如果指示可以进行写操作,跳到状态3,否则继续等待。

状态3:向当前Buffer写入数据,并记录数据长度。写了64个数据后,跳到状态4.

状态4:将基地址addra_sub调整到最高位,调整buf_flag,表明当前的Buffer的写操作已经完成,可以进行读操作,并跳到状态5。

状态5:切换Buffer,准备下一次写操作,跳到状态0。

 

第二个状态机(portb_st)在B端口自Buffer 0开始对两个Buffer分别进行了读操作。其状态转移如下图所示:

图4

状态0:将基地址addrb_sub调整到最高位,准备读出buf_flag,来指示是否可以进行读操作。复位后默认的addrb_id是0,即先准备对Buffer 0进行操作。

状态1:跳到了状态2,没有做任何其它的事,这是因为双端口RAM读数据需要一个时钟之后才能完成,所以这里插入了一个空状态。

状态2:判断当前Buffer的buf_flag,如果指示可以进行读操作,读出最高位中的数据长度len,设置读数据的初始位置为0,跳到状态3,否则继续等待在状态2。

状态3:读数据地址设置为1,跳到状态4。添加这个状态也是因为双端口RAM读数据需要一个时钟之后才能完成。

状态4:记录已经读出的数据长度cnt,如果等于len,则表明双端口RAM中的数据已经读完,转到状态5。否则每读出一个数据,将cnt + 1,并将读数据的地址addrb_sub + 1,并将读出的数据显示在LED上。

状态5:将基地址addrb_sub调整到最高位,调整buf_flag,表明当前的Buffer的读操作已经完成,可以再次进行写操作,并跳到状态6。

状态6:切换Buffer,准备下一次读操作,跳到状态0。

注意这里将从双端口RAM读出的数据输出到了LED上。

 

4.仿真

因为在porta_st状态机中直接写入了数据,所以在仿真时,不需要再次给激励,只需要例化顶层文件,直接观察波形即可。

仿真testbench代码如下:

`timescale 1ns / 1ps
module tb(
);


reg clk = 0;
always #10 clk = ~clk;


wire [7:0] LED;

top top_inst
(
    .OSC_CLK    (clk),
    .LED        (LED)
);


endmodule

 

仿真图如下所示:

 

图5

从图5中可以看出在复位信号被拉低后,给出A端口写使能wea,数据dina和地址addra,将数据写入双端口RAM的Buffer 0。

图6

在写完Buffer 0数据后,地址转到Buffer 1,继续写如数据。

 

图7

从图7可以看出是先进行写操作,写完后buf_flag被置位,B端口才能进行读操作。

 

图8

图8中显示的是读操作。

从以上的仿真图中可以看出使用双端口RAM IP制作的乒乓Buffer可以正确的使用。

 

5.扩展作业

  • 将两个端口的乒乓Buffer分为4个端口,设计相应的仿真程序,并验证。
  • 设计一个异步时钟域的双端口RAM,并使用不同的口线宽度来实现乒乓Buffer。
Posted in FPGA, FPGA 教材教案, FPGA开发板, Verilog, Verilog, Vivado, Vivado, 开发工具, 开发工具, 开发板, 开发语言, 教材与教案, 文章, 编程语言

2 Comments

发表回复

相关链接