乒乓操作可以看做成另一种形式的流水线技术。乒乓缓存结构如下图所示。
输入数据流通过输入数据选择单元时,时间等分地将数据流分配到两个数据缓冲模块。数据缓冲模块可以是RAM,FIFO等。通过将数据交替存放到数据缓冲模块1、数据缓冲模块2。并交替从数据缓冲模块1、数据缓冲模块2读取数据送入到数据流运算处理模块。工作过程如下:
第一段乒乓缓冲周期:将数据流缓存到数据缓冲模块1。
第二段乒乓缓冲周期:将数据流缓存到数据缓冲模块2,并从数据缓冲模块1读取数据送入数据流运算处理模块。
第三段乒乓缓冲周期:将数据流缓存到数据缓冲模块1,并从数据缓冲模块2读取数据送入数据流运算处理模块。
接下来就是上面描述的第二段、第三段乒乓缓冲周期的循环往复。
这样的结构应用于哪比较好呢?比如,对于LSTM(RNN)神经网络里面的上下timestep的输入数据以及输出数据就可以采用这种乒乓缓存。在WCDMA基带应用中,1帧由15个时隙组成,当需要将一帧数据延时一个时隙后处理时,也可以采用内存空间为一个时隙数据量的数据缓冲模块进行乒乓访存。
基于上面的乒乓缓存结构,可以得出一个乒乓操作结构如下图。这样的结构可以达到用低速模块处理高速数据流的效果,提高电路的吞吐率。
假设端口A的输入数据流的速率为 100Mbps,而一个数据预处理模块运算速度只有50Mbps,如果不采用乒乓操作结构,很明显数据预处理模块是处理不了数据端口A的数据流的。
当采用上图的乒乓操作方式之后。假设两个数据缓冲模块的内存大小都是1Mb,故每段乒乓缓冲周期为10ms。但是此时留给每个数据预处理模块的运算时间不再是10ms,而是20ms了,20ms内数据预处理模块是可以处理掉1Mb数据的。为什么留给数据预处理模块的时间有20ms呢?这20ms等于往自己写入数据的10ms加上往另外一个缓冲模块写入数据的10ms。因为这20ms内数据预处理模块都可以从自己本级的缓冲模块读取数据做运算。
这就做到了采用低速的数据预处理模块处理高速的数据流。上图是2级乒乓操作结构(因为数据流速率是预处理模块速率的2倍),如果数据流速率是预处理模块速率的n倍,就可以采用n级乒乓操作结构。这也是面积换速度的方法。
下面给出一个乒乓操作的特殊示例。假设原始电路如下图所示。组合逻辑加法器的运算时间需要3个时钟周期,而数据流a,b每个时钟都会有新的需要处理的数据。明显,这个电路处理不了。
那么我们可以采用如下电路来进行处理。这就是3级乒乓操作的特例,其中好比缓冲模块的内存大小为32bit(a,b均为16bit数据)。其中输入数据a,b按时钟周期依次送入三个不同的加法器;第三个时钟周期后,此时第一个加法器正好完成第一个时钟周期送进来的数据的运算,那么新来的数据又可以送入第一个加法器,以此循环往复。这样就做到了对数据的实时处理。
这里想分享下我设计的一段代码,首先是源代码。
module pingpang
(
input clk,
input 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 clk or negedge rst_n)
begin
if(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 clk or negedge rst_n)
begin
if(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 clk or negedge rst_n)
begin
if(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
其次是仿真测试代码:
module tb_pingpang();
// ------------------------------------------------------ //
// Inputs
reg clk;
reg rst_n;
reg [7:0] data_in;
// Outputs
wire [7:0] data_out;
// ------------------------------------------------------ //
pingpang pingpang_ins
(
.clk(clk),
.rst_n(rst_n),
.data_in(data_in), // 输入数据
.data_out(data_out) // 输出数据
);
// ------------------------------------------------------ //
initial
begin
clk = 0;
rst_n = 0;
data_in = 0;
#100;
rst_n = 1;
end
// ------------------------------------------------------ //
always #10 clk = ~clk;
// ------------------------------------------------------ //
always @ (posedge clk or negedge rst_n)
begin
if(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递增的方式输入数据,两个缓存区交替存储数据,最后依次输出数据,不过输出数据会有两个时钟的延迟。