乒乓操作常用与对数据流的处理,他可以节约缓存空间,对数据流无缝处理。
理论学习
外部输入数据流通过输入数据流选择单元将数据流输入到数据缓存模块,比较常用的存储单元有双口 RAM, FIFO, SDRAM 等。在第一个缓冲周期,数据流通过“输入数据流选择单元”将数据写入“数据缓冲模块 1”。写完之后进入第二个缓冲周期,在第二个缓冲周期数据流通过“输入数据流选择单元”将数据写入到“数据缓冲模块 2”的同时“输出数据流选择单元”将“数据缓冲模块 1”的数据流读出,此时进入第三个缓冲周期。在第三个缓冲周期数据流通过“输入数据流选择单元”将数据写入到“数据缓存模块 1”的同时将“数据缓冲模块 2”的数据读出。如此反复循环地操作,即为乒乓操作。
乒乓操作整个输入与输出的数据流都是连续不断,没有任何停顿的,因此非常适合对数据流进行流水线式的处理。常用与流水线式算法,完成数据的无缝缓冲与处理。,其次可以节约缓存空间,使用双存储单元比单存储单元更加节省空间。乒乓操作还可以实现低速模块处理高速数据,这种处理方式可以实现数据的串并转换,就是数据位宽之间的转换,是面积与速度互换原则的体现
程序框图
由于我们是使用不同时钟进行数据输入输出,所以我们需要使用不同的 RAM读写时钟,由于我们是使用 50MHz 时钟输入数据, 25MHz 时钟输出数据,由于我们设置的读写时钟不一致,而要实现乒乓操作的无缝缓存与处理,这里我们就需要设置不同的写入数据位宽与不同的读出数据位宽才能与读写时钟相匹配。这里就是我们前面理论部分所说的面积与速度互换原则的体现,这里我们设置的输入时钟的频率是输出时钟的两倍,即输入数据的速度是输出数据速度的两倍,所以这里我们需要设置输出数据的位宽是输入数据位宽的两倍,即面积的两倍。换句话说就是我们输入速度与面积的乘积与输出速度与面积的乘积要相等,即输入和输出的时间相等,这样才能保证在“数据缓冲模块 1”读/写完的同时“数据缓冲模块 2”也写/读完,才能保证输入与输出数据的无缝传输与处理。这就是其低高速数据传输特点的原理,只要我们遵从输入与输出数据的频率与位宽的乘积相等,那么我们就可以实现不同模块频率之间的数据传输。
模块介绍
时钟生成模块
使用IP,生成工程需要的时钟信号:50MHz与25MHz,其中 50MHz为RAM写入时钟信号,25MHz为RAM读取时钟信号。
数据生成
生成测试所需要的数据(0~199循环输出)
RAM控制模块
控制RAM1与RAM2执行乒乓操作
代码
时钟信号以及RAM均使用IP生成
需要注意的是
这里不要勾选输出寄存器,如果勾选,数据输出将会延时一拍,则需要将代码修改。并且由于输入输出时钟不一致,为使数据连续输出,我将输入位宽设为8位,输出设为16位,上文有详细的讲解为什么这样设置。
RAM控制
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2024/02/27 11:33:09
// Design Name:
// Module Name: ram_ctrl
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module ram_ctrl(
input wire clk_50m,
input wire clk_25m,
input wire rst_n,
input wire [7:0] data_in,
input wire data_en,
input wire [15:0] ram1_rd_data,
input wire [15:0] ram2_rd_data,
output reg ram1_wr_en,
output reg ram2_wr_en,
output reg ram1_rd_en,
output reg ram2_rd_en,
output wire [7:0] ram1_wr_data,
output wire [7:0] ram2_wr_data,
output reg [6:0] ram1_wr_addr,
output reg [6:0] ram2_wr_addr,
output reg [5:0] ram1_rd_addr,
output reg [5:0] ram2_rd_addr,
output reg [15:0] data_out
);
//parameter define
//状态机
parameter IDLE = 2'b00;
parameter WR_RAM1 = 2'b01;
parameter WR_RAM2_RD_RAM1 = 2'b10;
parameter WR_RAM1_RD_RAM2 = 2'b11;
//reg define
reg [1:0] state;
reg [7:0] data_in_reg;
//***************************************************************//
//***************************mian code***************************//
//***************************************************************//
//使用写数据时钟下降沿寄存数据,使数据写入存储器时上升沿能踩到稳定的数据
always @(negedge clk_50m or negedge rst_n) begin
if(rst_n == 1'b0)
data_in_reg <= 8'd0;
else
data_in_reg <= data_in;
end
//RAM1\RAM2 写数据输入
assign ram1_wr_data = (ram1_wr_en == 1'b1) ? data_in_reg: 8'd0; //ram时序要求使能与数据同时变化
assign ram2_wr_data = (ram2_wr_en == 1'b1) ? data_in_reg: 8'd0;
//状态机跳转
always @(negedge clk_50m or negedge rst_n) begin
if(rst_n == 1'b0)
state <= IDLE;
else
case(state)
IDLE: begin
if(data_en == 1'b1)
state <= WR_RAM1;
else
state <= state;
end
WR_RAM1: begin
if(ram1_wr_addr == 7'd99)
state <= WR_RAM2_RD_RAM1;
else
state <= state;
end
WR_RAM2_RD_RAM1: begin
if((ram1_rd_addr == 6'd49)&&(ram2_wr_addr == 7'd99))
state <= WR_RAM1_RD_RAM2;
else
state <=state;
end
WR_RAM1_RD_RAM2: begin
if((ram2_rd_addr == 6'd49)&&(ram1_wr_addr == 7'd99))
state <= WR_RAM2_RD_RAM1;
else
state <= state;
end
default: state <= IDLE;
endcase
end
//RAM1\RAM2 写使能控制(时钟50M)
always @(*) begin
if(rst_n == 1'b0)begin
ram1_wr_en <= 1'b0;
ram2_wr_en <= 1'b0;
end
else
case (state)
IDLE :begin
ram1_wr_en <= 1'b0;
ram2_wr_en <= 1'b0;
end
WR_RAM1 :begin
ram1_wr_en <= 1'b1;
ram2_wr_en <= 1'b0;
end
WR_RAM2_RD_RAM1 :begin
ram1_wr_en <= 1'b0;
ram2_wr_en <= 1'b1;
end
WR_RAM1_RD_RAM2 :begin
ram1_wr_en <= 1'b1;
ram2_wr_en <= 1'b0;
end
default: begin
ram1_wr_en <= 1'b0;
ram2_wr_en <= 1'b0;
end
endcase
end
//RAM1\RAM2 写地址控制
always @(negedge clk_50m or negedge rst_n) begin
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 + 7'd1;
end
always @(negedge clk_50m or negedge rst_n) begin
if(rst_n == 1'b0)
ram2_wr_addr <= 7'd0;
else if(ram2_wr_addr == 7'd99)
ram2_wr_addr <= 7'd0;
else if(ram2_wr_en == 1'b1)
ram2_wr_addr <= ram2_wr_addr + 7'd1;
end
//RAM1\RAM2 读使能控制(时钟25M)
always @(negedge clk_25m or negedge rst_n) begin
if(rst_n == 1'b0)
ram1_rd_en <= 1'b0;
else if(state == WR_RAM2_RD_RAM1)
ram1_rd_en <= 1'b1;
else
ram1_rd_en <= 1'b0;
end
always @(negedge clk_25m or negedge rst_n) begin
if(rst_n == 1'b0)
ram2_rd_en <= 1'b0;
else if(state == WR_RAM1_RD_RAM2)
ram2_rd_en <= 1'b1;
else
ram2_rd_en <= 1'b0;
end
//RAM1\RAM2 读地址控制
always @(negedge clk_25m or negedge rst_n) begin
if(rst_n == 1'b0)
ram1_rd_addr <= 6'd0;
else if(ram1_rd_addr == 6'd49)
ram1_rd_addr <= 6'd0;
else if(ram1_rd_en == 1'b1)
ram1_rd_addr <= ram1_rd_addr + 6'd1;
end
always @(negedge clk_25m or negedge rst_n) begin
if(rst_n == 1'b0)
ram2_rd_addr <= 6'd0;
else if(ram2_rd_addr == 6'd49)
ram2_rd_addr <= 6'd0;
else if(ram2_rd_en == 1'b1)
ram2_rd_addr <= ram2_rd_addr + 6'd1;
end
//输出数据
always @(negedge clk_25m or negedge rst_n) begin
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 <= data_out;
end
endmodule
此外,本模块所有的信号均由时钟下降沿触发,因为对于RAM来说无论是写入还是读取都是时钟的上升沿进行的,而如果我们使用时钟的上升沿产生使能、地址、数据的话、写入或读取时上升沿采到的就是数据变化的那一刻,这样采到的信号可能就是不稳定的状态,从而导致数据出错,所以这里我们使用时钟的下降沿 去产生这些信号的话,上升沿就能采到数据的稳定状态了。
数据生成
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2024/02/27 09:56:23
// Design Name:
// Module Name: data_gen
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module data_gen(
input wire clk_50m,
input wire rst_n,
output reg [7:0] data_out,
output reg data_en
);
//***************************************************************//
//***************************mian code***************************//
//***************************************************************//
//data_en 控制数据输出使能
always @(posedge clk_50m or negedge rst_n) begin
if(rst_n == 1'b0)
data_en <= 1'b0;
else
data_en <= 1'b1;
end
//data_out 数据输出,输入从8'd0 ~ 8'd199
always @(posedge clk_50m or negedge rst_n) begin
if(rst_n == 1'b0)
data_out <= 8'd0;
else if(data_out == 8'd199)
data_out <= 8'd0;
else if(data_en == 1'b1)
data_out <= data_out + 8'd1;
else
data_out <= data_out;
end
endmodule
顶层模块
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2024/02/27 15:58:08
// Design Name:
// Module Name: top_pingpang
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module top_pingpang(
input wire sys_clk,
input wire sys_rst_n
);
//wire define
//clk_gen
wire clk_in1;
wire reset;
wire clk_out1;
wire clk_out2;
wire locked;
//ram_ctrl
wire clk_50m;
wire clk_25m;
wire rst_n;
wire [7:0] data_out;
wire data_en;
wire [15:0] ram1_rd_data;
wire [15:0] ram2_rd_data;
wire ram1_wr_en;
wire ram2_wr_en;
wire ram1_rd_en;
wire ram2_rd_en;
wire [7:0] ram1_wr_data;
wire [7:0] ram2_wr_data;
wire [6:0] ram1_wr_addr;
wire [6:0] ram2_wr_addr;
wire [5:0] ram1_rd_addr;
wire [5:0] ram2_rd_addr;
wire [15:0] data_out_1;
//
assign rst_n = (sys_rst_n) & (locked);
ram_ctrl ram_ctrl_inst(
.clk_50m(clk_50m),
.clk_25m(clk_25m),
.rst_n(rst_n),
.data_in(data_out),
.data_en(data_en),
.ram1_rd_data(ram1_rd_data),
.ram2_rd_data(ram2_rd_data),
.ram1_wr_en(ram1_wr_en),
.ram2_wr_en(ram2_wr_en),
.ram1_rd_en(ram1_rd_en),
.ram2_rd_en(ram2_rd_en),
.ram1_wr_data(ram1_wr_data),
.ram2_wr_data(ram2_wr_data),
.ram1_wr_addr(ram1_wr_addr),
.ram2_wr_addr(ram2_wr_addr),
.ram1_rd_addr(ram1_rd_addr),
.ram2_rd_addr(ram2_rd_addr),
.data_out(data_out_1)
);
clk_gen clk_gen_inst(
.clk_out1(clk_50m),
.clk_out2(clk_25m),
.reset(~sys_rst_n),
.locked(locked),
.clk_in1(sys_clk)
);
data_gen data_gen_inst(
.clk_50m(clk_50m),
.rst_n(rst_n),
.data_en(data_en),
.data_out(data_out)
);
ram1 ram1_inst(
.dina (ram1_wr_data ), //写数据
.addra (ram1_wr_addr ), //写地址
.clka (clk_50m ), //写时钟
.wea (ram1_wr_en ), //写使能
.ena (1'b1),
.addrb (ram1_rd_addr ), //读地址
.clkb (clk_25m ), //读时钟
.enb (ram1_rd_en ), //读使能
.doutb (ram1_rd_data ) //读数据
);
ram2 ram2_inst(
.dina (ram2_wr_data ), //写数据
.addra (ram2_wr_addr ), //写地址
.clka (clk_50m ), //写时钟
.ena (1'b1),
.wea (ram2_wr_en), //写使能
.addrb (ram2_rd_addr ), //读地址
.clkb (clk_25m ), //读时钟
.enb (ram2_rd_en ), //读使能
.doutb (ram2_rd_data ) //读数据
);
endmodule
仿真程序
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2024/02/27 16:42:26
// Design Name:
// Module Name: tb_pingpang
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module tb_pingpang();
//reg define
reg sys_clk ;
reg sys_rst_n ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#200
sys_rst_n <= 1'b1;
end
//sys_clk:模拟系统时钟,每 10ns 电平取反一次,周期为 20ns,频率为 50MHz
always #10 sys_clk = ~sys_clk;
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//-------------pingpang_inst-------------
top_pingpang pingpang_inst
(
.sys_clk (sys_clk ), //系统时钟
.sys_rst_n (sys_rst_n ) //复位信号,低有效
);
endmodule
仿真结果
可以看到,程序正常且连续的输出数据,其中C7C6为198、199拼接成的16进制数据。之后程序继续输出0100、0302........
复盘
对于生成的简单双口RAM,A口只可进行写操作,B口只可进行读操作。
wea: | 使能A口写操作。 |
---|---|
ena | 使能A端口 |
enb | 使能B端口 |
写优先模式write_first:(又称为Read after Write,即先写后读或写优先):当写使能WEA有效时(高有效),此时读依然有效,故该模式下从地址bb和cc读出的是新写入的数据1111和2222,也意味着写入地址bb和cc的数据分别为1111和2222 ;即写入数据的同时,端口也将写入的数据输出。
读优先模式read_first:(又称为Read before Write,即先读后写或读优先):读出的是该地址上的原有数据,同时会把新数据写入该地址 ,即写入数据的同时,端口也将该地址存储的原始数据输出。
保持模式no_change:(No Read on Write,保持模式)一旦写操作有效,读操作即无效,此时输出端口保持写操作之前读出的数据不变;
具体可以参照: RAM三种模式
源代码:
链接: https://pan.baidu.com/s/12sVGOc6rdOtzNWfdo5QOoA?pwd=z8sb 提取码: z8sb 复制这段内容后打开百度网盘手机App,操作更方便哦
参考文献
[1] 正点原子. 达芬奇之FPGA开发指南
[2] 野火. FPGA+Verilog开发实战指南——基于Xilinx+Spartan6
[3] bleauchat.RAM IP core(3) write_first/read_first/no_change什么区别 CSDN(https://blog.csdn.net/bleauchat/article/details/88965307)