乒乓操作
开发环境
ISE14.7、verilog、spartan6
实验目标
使用两个读写时钟不同的双口RAM实现低速模块处理高速数据的乒乓操作.循环产生0-99和100-199两个数据包,输入时钟为50MHz输出时钟为25MHz
理论学习
外部输入数据流通过输入数据流选择单元将数据流输入到数据缓存模块,比较常用的存储单元有双口RAM,FIFO,SDRAM等。在第一个缓冲周期,数据流通过“输入数据流选择单元”将数据写入“数据缓冲模块1”。写完之后进入第二个缓冲周期,在第二个缓冲周期数据流通过“输入数据流选择单元”将数据写入到“数据缓冲模块2”的同时“输出数据流选择单元”将“数据缓冲模块1”的数据流读出,此时进入第三个缓冲周期。在第三个缓冲周期数据流通过“输入数据流选择单元”将数据写入到“数据缓存模块1”的同时将“数据缓冲模块2”的数据读出。如此反复循环地操作,即为乒乓操作。
乒乓操作的优点有以下三种:
- 乒乓操作的最大特点是通过“输入数据选择单元”和“输出数据选择单乒乓操作的最大特点是通过“输入数据选择单元”和“输出数据选择单元’′按节拍的切换,将经过缓冲的数据流没有停顿地送到“数据流运算处理模块”进行运算处理。把乒乓操作当作一个整体,站在这个模块的两端看数据,输入数据和输出数据都是连续不断的,因此非常适合对数据流进行流水线式处理,完成数据的无缝缓冲与处理。
- 乒乓操作的第二个优点是可以节约缓冲区空间.比如在WCDMA基带应用中,1个帧是由15个时隙组成的,有时需要将1整帧的数据延时一个时隙后处理,比较直接的办法是将这帧数据缓存起来,然后延时1个时隙进行处理。这时缓冲区的长度是1整帧数据长,假设数据速率是3.84Mbps,1帧长10 ms,则此时需要缓冲区长度是38400位。如果采用乒乓操作,只需定义两个能缓冲1个时隙数据的RAM(单口RAM即可)。当向一块RAM写数据的时候,从另一块读数据,然后送到处理单元处理,此时每块RAM的容量仅需2560位即可,2块RAM加起来也只有5120位的容量。
- 另外,I巧妙运用乒乓操作还可以达到用低速模块处理高速数据流的效果。如图2所示,数据缓冲模块采用了双口RAM,并在DPRAM后引入了一级数据预处理模块,这个数据预处理可以根据需要的各种数据运算,比如在WCDMA设计中,对输入数据流的解扩、解扰、去旋转等。
在这里主要分析假设端口A的输入数据流的速率为100Mbps,乒乓操作的缓冲周期是10ms。以下分析各个节点端口的数据速率。
100 Mbps,乒乓操作的缓冲周期是10 ms。以下分析各个节点端口的数据速率
在这里主要分析一下如何将高速数据流降速的
- 假设端口A的输入数据流的速率为100Mbps,乒乓操作的缓冲周期是10ms。以下分析各个节点端口的数据速率。
仔细分析就会发现到第3个缓冲周期时,留给DPRAM1读取数据并送到“数据预处理模块1”的时间一共是20ms。有的工程师困惑于DPRAM1的读数时间为什么是20ms,这个时间是这样得来的:首先,在在第2个缓冲周期向DPRAM2写数据的10ms内,DPRAM1可以进行读操作;另外,在第1个缓冲周期的第5ms起(绝对时间为5ms时刻),DPRAM1就可以一边向500KI以后的地址写数据,一边从地址0读数,到达10ms时,DPRAM1刚好写完了1Mb数据,并且读了500K数据,这个缓冲时间内DPRAM1读了5ms;在第3个缓冲周期的第5ms起(绝对时间为35ms时刻),同理可以一边向500K以后的地址写数据一边从地址O读数,又读取了5个ms,所以截止DPRAM1第一个周期存入的数据被完全覆盖以前,DPRAM1最多可以读取20ms时间,而所需读取的数据为1Mb,所以端口C1的数据速率为50 Mbps,仔细分析就会发现到第3个缓冲周期时,留给DPRAM 1读取数据并送到“数据预处理模块1”的时间一共是20 ms。
20 ms这个时间是这样得来的:首先,在在第2个缓冲周期向DPRAM 2写数据的10 ms内,DPRAM 1可以进行读操作;另外,在第1个缓冲周期的第5ms起(绝对时间为5ms时刻),DPRAM 1就可以一边向500 KI以后的地址写数据,一边从地址0读数,到达10 ms时,DPRAM 1刚好写完了1 Mb数据,并且读了500 K数据,这个缓冲时间内DPRAM 1读了5ms;在第3个缓冲周期的第5ms起(绝对时间为35 MS时刻),同理可以一边向500 K以后的地址写数据一边从地址O读数,又读取了5个ms,所以截止DPRAM 1第一个周期存入的数据被完全覆盖以前,DPRAM 1最多可以读取20 ms时间,而所需读取的数据为1 Mb,所以端口C1的数据速率为:1Mb/20ms =50Mbps。因此,“数据预处理模块1”的最低数据吞吐能力也仅仅要求为50Mbps。同理,“数据预处理模块2”的最低数据吞吐能力也仅仅要求为50Mbps。换言之,通过乒乓操作,“数据预处理模块”的时序压力减轻了,所要求的数据处理速率仅仅为输入数据速率的1/2。
1MB/20 ms=50 Mbps。因此,“数据预处理模块1”的最低数据吞吐能力也仅仅要求为50 Mbps。同理,“数据预处理模块2”的最低数据吞吐能力也仅仅要求为50 Mbps。换言之,通过乒乓操作,“数据预处理模块”的时序压力减轻了,所要求的数据处理速率仅仅为输入数据速率的1/2.
这是典型的速度换取面积的例子
整体设计
工程的整体模块框图如下图所示
模块名 | 功能 |
---|---|
ram | 数据缓冲模块 |
ram_ctrl | 输入输出数据流选择模块 |
clk_gen | 生成时钟 |
data_gen | 生成数据 |
pingpang | 顶层设计 |
下面单独介绍几个重要的模块设计思路
数据缓冲模块
我们需要调用两个简单爽口RAM,并且输出数据的位宽应为输入数据尾款的两倍,在这里我们设置输入数据的位宽为8位,则输出数据的位宽为16位,输入数据的时钟为50MHz输出数据的时钟为25MHz,这样才能保证在“数据缓冲模块1”读/写完的同时“数据缓冲模块2”也写/读完
数据生成模块
该模块需要生成输入RAM中的数据,这里为了方便产生,我们循环生成数据8’d0-8’d99作为第一包数据写入第一个缓冲模块8’d100-8’d199作为第二包数据写入第二个缓冲模块,依次循环写入。当然大家也可生成不同的数据流,只要满足我们RAM中设置的深度及位宽即可。计数器自加1的条件可以设置为输入数据使能信号。
输入输出数据选择模块
这个模块主要是产生两个双口RAM的读写相关信号,同时将两个RAM中读出的数据进行输入,通过简单的处理后对读出的数据进行输出。
这里的设计思路主要是状态机,如下图所示。
WRAM1状态主要是写RAM1,在最开始的时候我们只往RAM1中写入数据因为RAM2中并没有数据,所以我们并不会对RAM2进行读取数据,在第一包数据写完之后跳转到WRAM2_RRAM1状态。
代码介绍
data_gen.v
module data_gen
(
input wire clk_50m , //模块时钟,频率50MHz
input wire rst_n , //复位信号,低电平有效
output reg data_en , //数据使能信号,高电平有效
output reg [7:0] data_in //输出数据
);
//********************************************************************//
//******************************* Main Code **************************//
//********************************************************************//
//data_en:让其一直为高电平,一直输出数据
always@(posedge clk_50m or negedge rst_n)
if(rst_n == 1'b0)
data_en <= 1'b0;
else
data_en <= 1'b1;
//data_in:循环生成写入的数据(8'd0 ~ 8'd199)
always@(posedge clk_50m or negedge rst_n)
if(rst_n == 1'b0)
data_in <= 8'd0;
else if(data_in == 8'd199)
data_in <= 8'd0;
else if(data_en == 1'b1)
data_in <= data_in + 1'b1;
else
data_in <= data_in;
endmodule
ram_ctrl.v
module ram_ctrl
(
input wire clk_50m , //写ram时钟,50MHz
input wire clk_25m , //读ram时钟,25MHz
input wire rst_n , //复位信号,低有效
input wire [15:0] ram1_rd_data, //ram1读数据
input wire [15:0] ram2_rd_data, //ram2读数据
input wire data_en , //输入数据使能信号
input wire [7:0] data_in , //输入数据
output reg ram1_wr_en , //ram1写使能
output reg ram1_rd_en , //ram1读使能
output reg [6:0] ram1_wr_addr, //ram1写地址
output reg [5:0] ram1_rd_addr, //ram1读地址
output wire [7:0] ram1_wr_data, //ram1写数据
output reg ram2_wr_en , //ram2写使能
output reg ram2_rd_en , //ram2读使能
output reg [6:0] ram2_wr_addr, //ram2写地址
output reg [5:0] ram2_rd_addr, //ram2读地址
output wire [7:0] ram2_wr_data, //ram2写数据
output reg [15:0] data_out //输出乒乓操作数据
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//parameter define
parameter IDLE = 4'b0001, //初始状态
WRAM1 = 4'b0010, //写RAM1状态
WRAM2_RRAM1 = 4'b0100, //写RAM2读RAM1状态
WRAM1_RRAM2 = 4'b1000; //写RAM1读RAM2状态
//reg define
reg [3:0] state ; //状态机状态
reg [7:0] data_in_reg ; //数据寄存器
//********************************************************************//
//******************************* Main Code **************************//
//********************************************************************//
//使用组合逻辑赋值,这样使能和数据地址才能对应
assign ram1_wr_data = (ram1_wr_en == 1'b1) ? data_in_reg: 8'd0;
assign ram2_wr_data = (ram2_wr_en == 1'b1) ? data_in_reg: 8'd0;
//使用写数据时钟下降沿寄存数据,使数据写入存储器时上升沿能踩到稳定的数据
always@(negedge clk_50m or negedge rst_n)
if(rst_n == 1'b0)
data_in_reg <= 8'd0;
else
data_in_reg <= data_in;
//状态机状态跳转
always@(negedge clk_50m or negedge rst_n)
if(rst_n == 1'b0)
state <= IDLE;
else case(state)
IDLE://检测到数据使能信号为高时,跳转到下一状态将数据写到RAM1
if(data_en == 1'b1)
state <= WRAM1;
WRAM1://RAM1数据写完之后,跳转到写RAM2读RAM1状态
if(ram1_wr_addr == 7'd99)
state <= WRAM2_RRAM1;
WRAM2_RRAM1://RAM2数据写完之后,跳转到写RAM1读RAM2状态
if(ram2_wr_addr == 7'd99)
state <= WRAM1_RRAM2;
WRAM1_RRAM2://RAM1数据写完之后,跳转到写RAM2读RAM1状态
if(ram1_wr_addr == 7'd99)
state <= WRAM2_RRAM1;
default:
state <= IDLE;
endcase
//RAM1,RAM2写使能赋值
always@(*)
case(state)
IDLE:
begin
ram1_wr_en = 1'b0;
ram2_wr_en = 1'b0;
end
WRAM1:
begin
ram1_wr_en = 1'b1;
ram2_wr_en = 1'b0;
end
WRAM2_RRAM1:
begin
ram1_wr_en = 1'b0;
ram2_wr_en = 1'b1;
end
WRAM1_RRAM2:
begin
ram1_wr_en = 1'b1;
ram2_wr_en = 1'b0;
end
default:;
endcase
//RAM1读使能,使用读时钟赋值
always@(negedge clk_25m or negedge rst_n)
if(rst_n == 1'b0)
ram1_rd_en <= 1'b0;
else if(state == WRAM2_RRAM1)
ram1_rd_en <= 1'b1;
else
ram1_rd_en <= 1'b0;
//RAM2读使能,使用读时钟赋值
always@(negedge clk_25m or negedge rst_n)
if(rst_n == 1'b0)
ram2_rd_en <= 1'b0;
else if(state == WRAM1_RRAM2)
ram2_rd_en <= 1'b1;
else
ram2_rd_en <= 1'b0;
//RAM1写地址
always@(negedge clk_50m or negedge rst_n)
if(rst_n == 1'b0)
ram1_wr_addr <= 7'd0;
else if(ram1_wr_addr == 7'd99)
ram1_wr_addr <= 7'd0;
else if(ram1_wr_en == 1'b1)
ram1_wr_addr <= ram1_wr_addr + 1'b1;
//RAM2写地址
always@(negedge clk_50m or negedge rst_n)
if(rst_n == 1'b0)
ram2_wr_addr <= 7'b0;
else if(ram2_wr_addr == 7'd99)
ram2_wr_addr <= 7'b0;
else if(ram2_wr_en == 1'b1)
ram2_wr_addr <= ram2_wr_addr + 1'b1;
//RAM1读地址
always@(negedge clk_25m or negedge rst_n)
if(rst_n == 1'b0)
ram1_rd_addr <= 6'd0;
else if(ram1_rd_addr == 6'd49)
ram1_rd_addr <= 6'b0;
else if(ram1_rd_en == 1'b1)
ram1_rd_addr <= ram1_rd_addr + 1'b1;
//RAM2读地址
always@(negedge clk_25m or negedge rst_n)
if(rst_n == 1'b0)
ram2_rd_addr <= 6'd0;
else if(ram2_rd_addr == 6'd49)
ram2_rd_addr <= 6'b0;
else if(ram2_rd_en == 1'b1)
ram2_rd_addr <= ram2_rd_addr + 1'b1;
//将乒乓操作读出的数据选择输出
always@(negedge clk_25m or negedge rst_n)
if(rst_n == 1'b0)
data_out <= 16'd0;
else if(ram1_rd_en == 1'b1)
data_out <= ram1_rd_data;
else if(ram2_rd_en == 1'b1)
data_out <= ram2_rd_data;
else
data_out <= 16'd0;
endmodule
顶层文件top.v
module pingpang
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n //复位信号,低有效
);
//********************************************************************//
//******************** Parameter And Internal Signal *****************//
//********************************************************************//
//wire define
wire clk_50m ; //50MHz时钟
wire clk_25m ; //100MHz时钟
wire rst_n ; //复位信号
wire [15:0] ram1_rd_data ; //ram1读数据
wire [15:0] ram2_rd_data ; //ram2读数据
wire data_en ; //输入数据使能信号
wire [7:0] data_in ; //输入数据
wire ram1_wr_en ; //ram1写使能
wire ram1_rd_en ; //ram1读使能
wire [6:0] ram1_wr_addr ; //ram1写地址
wire [5:0] ram1_rd_addr ; //ram1写地址
wire [7:0] ram1_wr_data ; //ram1写数据
wire ram2_wr_en ; //ram2写使能
wire ram2_rd_en ; //ram2读使能
wire [6:0] ram2_wr_addr ; //ram2写地址
wire [5:0] ram2_rd_addr ; //ram2写地址
wire [7:0] ram2_wr_data ; //ram2写数据
wire [15:0] data_out ; //输出乒乓操作数据
wire locked ; //PLl核输出稳定时钟标志信号,高有效
//时钟不稳定时视为复位
assign rst_n = sys_rst_n & locked;
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//----------- ram_ctrl_inst -----------
ram_ctrl ram_ctrl_inst
(
.clk_50m (clk_50m ), //写ram时钟,50MHz
.clk_25m (clk_25m ), //读ram时钟,25MHz
.rst_n (rst_n ), //复位信号,低有效
.ram1_rd_data(ram1_rd_data ), //ram1读数据
.ram2_rd_data(ram2_rd_data ), //ram2读数据
.data_en (data_en ), //输入数据使能信号
.data_in (data_in ), //输入数据
.ram1_wr_en (ram1_wr_en ), //ram1写使能
.ram1_rd_en (ram1_rd_en ), //ram1读使能
.ram1_wr_addr(ram1_wr_addr ), //ram1读写地址
.ram1_rd_addr(ram1_rd_addr ), //ram1读地址
.ram1_wr_data(ram1_wr_data ), //ram1写数据
.ram2_wr_en (ram2_wr_en ), //ram2写使能
.ram2_rd_en (ram2_rd_en ), //ram2读使能
.ram2_wr_addr(ram2_wr_addr ), //ram2写地址
.ram2_rd_addr(ram2_rd_addr ), //ram2读地址
.ram2_wr_data(ram2_wr_data ), //ram2写数据
.data_out (data_out ) //输出乒乓操作数据
);
//----------- data_gen_inst -----------
data_gen data_gen_inst
(
.clk_50m (clk_50m ), //模块时钟,频率50MHz
.rst_n (rst_n ), //复位信号,低电平有效
.data_en (data_en ), //数据使能信号,高电平有效
.data_in (data_in ) //输出数据
);
//----------- clk_gen_inst -----------
clk_gen clk_gen_inst
(
.RESET (~sys_rst_n ), //异步复位
.CLK_IN1 (sys_clk ), //输入时钟
.CLK_OUT1 (clk_50m ), //输出时钟,频率50MHz
.CLK_OUT2 (clk_25m ), //输出时钟,频率25MHz
.LOCKED (locked ) //时钟稳定输出标志信号
);
//------------ dq_ram1-------------
dp_ram dp_ram1
(
.dina (ram1_wr_data ), //写数据
.addra (ram1_rd_addr ), //写地址
.clka (clk_25m ), //写时钟
.ena (ram1_rd_en ), //写使能
.addrb (ram1_wr_addr ), //读地址
.clkb (clk_50m ), //读时钟
.enb (ram1_wr_en ), //读使能
.doutb (ram1_rd_data ) //读数据
);
//------------ dq_ram2-------------
dp_ram dp_ram2
(
.dina (ram2_wr_data ), //写数据
.addra (ram2_rd_addr ), //写地址
.clka (clk_25m ), //写时钟
.ena (ram2_rd_en ), //写使能
.addrb (ram2_wr_addr ), //读地址
.clkb (clk_50m ), //读时钟
.enb (ram2_wr_en ), //读使能
.doutb (ram2_rd_data ) //读数据
);
endmodule