每日一摘:乒乓操作

FPGA设计的四种常用思想与技巧:乒乓操作、串并转换、流水线操作、数据接口同步化。

其中乒乓操作是一个常应用于数据流控制的处理技巧,典型的乒乓操作方法如图:

 

 乒乓操作的处理流程为:输入数据流通过“输入数据选择单元”将数据流分配到两个数据缓冲区,数据缓冲模块可以为任何存储模块,比较常用的存储单元为双端口RAM(DPRAM)、单口RAM(SPRAM)、FIFO等。

在第一个缓冲周期,将输入的数据流缓存到“数据缓冲模块1”;

在第二个缓冲周期,通过“输入数据选择单元”的切换,将输入的数据流缓存到“数据缓存模块2”,同时将“数据缓存模块1”缓存的第一个缓冲周期的数据通过“输出数据流选择单元”的选择,送到“数据流运算处理模块”进行运算处理。

在第三个缓冲周期,通过“输入数据选择单元”的再次切换,将输入的数据流缓存到“数据缓冲模块1”,同时将“数据缓冲模块2”缓存的第二个缓冲周期的数据通过“输出数据选择单元”的选择,送到“数据流运算处理模块”进行运算处理。如此循环。

代码示范:

module ping_pong
    (
    input            sys_clk,
    input            sys_rst_n,
    input      [7:0] data_in,   // 输入数据
    output reg [7:0] data_out   // 输出数据
    );
// ------------------------------------------------------ //
    reg [7:0] buffer1;  // 缓存1
    reg [7:0] buffer2;  // 缓存2
    reg       wr_flag;  // 写标志,wr_flag=0,写buffer1,wr_flag=1,写buffer2
    reg       rd_flag;  // 读标志,rd_flag=0,读buffer2,rd_flag=1,读buffer1
    reg       state;    // 状态机,0:写1读2,1:写2读1,状态转移和输出分开编码
// ------------------------------------------------------ //    
    // 状态转移
    always @ (posedge sys_clk or negedge sys_rst_n)
    begin
        if(sys_rst_n == 1'b0)
        begin
            state <= 1'b0;
        end
        else
        begin
            case(state)
                1'b0    : state <= 1'b1;    // 写1读2->写2读1
                1'b1    : state <= 1'b0;    // 写2读1->写1读2
                default : state <= 1'b0;
            endcase
        end
    end
// ------------------------------------------------------ // 
    // 状态输出
    always @ (state)
    begin
        case(state)
            1'b0:
            begin
                wr_flag = 1'b0; // 写1
                rd_flag = 1'b0; // 读2
            end
            1'b1:
            begin
                wr_flag = 1'b1; // 写2
                rd_flag = 1'b1; // 读1
            end
            default:
            begin
                wr_flag = 1'b0;
                rd_flag = 1'b0;
            end
        endcase
    end
// ------------------------------------------------------ //  
    // 写buffer数据   
    always @ (posedge sys_clk or negedge sys_rst_n)
    begin
        if(sys_rst_n == 1'b0)
        begin
            buffer1 <= 8'b0;
            buffer2 <= 8'b0;
        end
        else
        begin
            case(wr_flag)
                1'b0 : buffer1 <= data_in;  // wr_flag = 0,写buffer1
                1'b1 : buffer2 <= data_in;  // wr_flag = 1,写buffer2
                default:
                begin
                    buffer1 <= 8'b0;
                    buffer2 <= 8'b0;
                end
            endcase
        end
    end    
// ------------------------------------------------------ // 
    // 读buffer数据
    always @ (posedge sys_clk or negedge sys_rst_n)
    begin
        if(sys_rst_n == 1'b0)
        begin
            data_out <= 8'b0;
        end
        else
        begin
            case(rd_flag)
                1'b0    : data_out <= buffer2;   // rd_flag=0,读buffer2
                1'b1    : data_out <= buffer1;   // rd_flag=1,读buffer1
                default : data_out <= 8'b0;
            endcase
        end
    end
// ------------------------------------------------------ //     
endmodule
综合代码
`timescale 1ns/1ns
module ping_pong_tb();
// ------------------------------------------------------ //
    // Inputs
    reg       sys_clk;
    reg       sys_rst_n;
    reg [7:0] data_in;
    // Outputs
    wire [7:0] data_out;
// ------------------------------------------------------ //
ping_pong u_ping_pong
    (
    .sys_clk(sys_clk),
    .sys_rst_n(sys_rst_n),
    .data_in(data_in),   // ????
    .data_out(data_out)   // ????
    );
// ------------------------------------------------------ //
    initial
    begin
        sys_clk = 0;
        sys_rst_n = 0;
        data_in = 0;
        #100;
        sys_rst_n = 1;
    end
// ------------------------------------------------------ //
    always #10 sys_clk = ~sys_clk;
// ------------------------------------------------------ //
    always @ (posedge sys_clk or negedge sys_rst_n)
    begin
        if(sys_rst_n == 1'b0)
        begin
            data_in <= 8'b0;
        end
        else
        begin
            data_in <= data_in + 1'b1;
        end
    end
// ------------------------------------------------------ //
endmodule
仿真代码

代码结果:

结果分析:从仿真波形中可以看出按照0、1、2、3......递增的方式输入数据,两个缓存区交替存储数据,最后依次输出数据,不过输出数据会有两个时钟的延迟。

乒乓操作的最大特点是通过“输入数据选择单元”和“输出数据选择单元”按节拍、相互配合的切换,将经过缓冲的数据流没有停顿地送到“数据流运算处理模块”进行运算处理。

把乒乓操作模块当作一个整体,站在这个模块的两端看数据,输入数据流和输出数据流都是连续不断的,没有任何停顿,因此非常适合对数据流进行流水线式处理。所以乒乓操作常常应用于流水线式算法,完成数据的无缝缓冲与处理。

乒乓操作的第二个优点是可以节约缓冲区空间。比如在WCDMA基带应用中,1个帧是由15个时隙组成的,有时需要将1整帧的数据延时一个时隙后处理,比较直接的办法是将这帧数据缓存起来,然后延时1个时隙进行处理。这时缓冲区的长度是1整帧数据长,假设数据速率是3.84Mbps,1帧长10ms,则此时需要缓冲区长度是38400位。如果采用乒乓操作,只需定义两个能缓冲1个时隙数据的RAM(单口RAM即可)。当向一块RAM写数据的时候,从另一块RAM读数据,然后送到处理单元处理,此时每块RAM的容量仅需2560位即可,2块RAM加起来也只有5120位的容量。

       另外, 巧妙运用乒乓操作还可以达到用低速模块处理高速数据流的效果。 如图2所示, 数据缓冲模块采用了双口 RAM, 并在 DPRAM 后引入了一级数据预处理模块, 这个数据预处理可以根据需要的各种数据运算, 比如在WCDMA 设计中, 对输入数据流的解扩、 解扰、去旋转等。 假设端口 A 的输入数据流的速率为 100Mbps, 乒乓操作的缓冲周期是 10ms。 以下分析各个节点端口的数据速率。

 

  A 端口处输入数据流速率为 100Mbps, 在第1 个缓冲周期10ms 内, 通过“ 输入数据选择单元” , 从B1 到达DPRAM1。B1 的数据速率也是100Mbps,DPRAM1 要在10ms 内写入1Mb 数据。同理, 在第2 个 10ms, 数据流被切换到DPRAM2, 端口 B2 的数据速率也是 100Mbps, DPRAM2在第 2 个 10ms 被写入 1Mb 数据。 在第 3 个 10ms, 数据流又切换到 DPRAM1, DPRAM1 被写入1Mb数据。仔细分析就会发现到第 3 个缓冲周期时,留给 DPRAM1 读取数据并送到“ 数据预处理模块 1”的时间一共是 20ms。 有的工程师困惑于 DPRAM1 的读数时间为什么是 20ms, 这个时间是这样得来的: 首先, 在在第 2 个缓冲周期向DPRAM2 写数据的 10ms 内, DPRAM1 可以进行读操作;

另外, 在第 1 个缓冲周期的第 5ms起(绝对时间为5ms 时刻),DPRAM1 就可以一边向500K 以后的地址写数据, 一边从地址0 读数, 到达10ms 时,DPRAM1 刚好写完了1Mb 数据, 并且读了500K 数据, 这个缓冲时间内DPRAM1 读了5ms; 在第3 个缓冲周期的第5ms 起(绝对时间为35ms 时刻), 同理可以一边向500K 以后的地址写数据一边从地址0 读数, 又读取了5 个ms, 所以截止DPRAM1 第一个周期存入的数据被完全覆盖以前,DPRAM1 最多可以读取20ms时间, 而所需读取的数据为1Mb, 所以端口C1 的数据速率为:1Mb/20ms=50Mbps。 因此, “ 数据预处理模块1” 的最低数据吞吐能力也仅仅要求为50Mbps。 同理, “ 数据预处理模块2”的最低数据吞吐能力也仅仅要求为50Mbps。 换言之, 通过乒乓操作, “ 数据预处理模块”的时序压力减轻了, 所要求的数据处理速率仅仅为输入数据速率的1/2。

 通过乒乓操作实现低速模块处理高速数据的实质是:通过DPRAM 这种缓存单元实现了数据流的串并转换, 并行用“ 数据预处理模块1” 和“ 数据预处理模块2” 处理分流的数据, 是面积与速度互换原则的体现!

posted @ 2020-12-13 18:05  LiYiRui  阅读(536)  评论(0编辑  收藏  举报