3. 行缓冲器设计用于卷积滑动窗口
任务目标
在FPGA卷积加速器中,卷积核需要按行滑动读取输入图像的窗口数据。若每次窗口移动都直接访问外部DDR,将产生大量冗余突发传输,带宽利用率极低。本案例实现一个基于FPGA片上BRAM的行缓冲器(Line Buffer),仅需顺序读取一次输入行数据,即可在输出端连续生成3×3卷积窗口,将外部存储器访问量降至最低。
设计原理
行缓冲器利用行延迟机制:将输入像素流分别延迟0行、1行、2行,再对同一列位置的三行像素同时采样,即可拼出3×3窗口。窗口数据在像素时钟驱动下每个周期输出一个完整窗口,无需额外地址计算。
Verilog实现
module line_buffer_3x3 #(
parameter DATA_WIDTH = 8,
parameter IMG_WIDTH = 640, // 图像行宽
parameter IMG_HEIGHT = 480 // 图像行数
)(
input wire clk,
input wire rst_n,
input wire px_valid, // 输入像素有效
input wire [DATA_WIDTH-1:0] px_in, // 输入像素(按行顺序)
output reg win_valid, // 窗口有效
output wire [DATA_WIDTH-1:0] win_r0_c0, win_r0_c1, win_r0_c2,
output wire [DATA_WIDTH-1:0] win_r1_c0, win_r1_c1, win_r1_c2,
output wire [DATA_WIDTH-1:0] win_r2_c0, win_r2_c1, win_r2_c2
);
// 行计数与列计数
reg [9:0] col_cnt;
reg [9:0] row_cnt;
// 行缓冲器:2条延迟线,各存储一行
reg [DATA_WIDTH-1:0] line_buf_0 [0:IMG_WIDTH-1];
reg [DATA_WIDTH-1:0] line_buf_1 [0:IMG_WIDTH-1];
// 当前输入像素写入位置
wire [9:0] wr_addr = col_cnt;
// 延迟行读出位置:当前列的前2个位置(组合逻辑)
wire [9:0] rd_addr_0 = (col_cnt >= 2) ? col_cnt - 2 : (IMG_WIDTH + col_cnt - 2);
wire [9:0] rd_addr_1 = (col_cnt >= 1) ? col_cnt - 1 : (IMG_WIDTH + col_cnt - 1);
// 行缓冲器写使能
wire wr_en_0 = px_valid && (row_cnt >= 1); // 第1行开始写入line_buf_0
wire wr_en_1 = px_valid && (row_cnt >= 2); // 第2行开始写入line_buf_1
// 写入数据
always @(posedge clk) begin
if (wr_en_0) line_buf_0[wr_addr] <= px_in;
if (wr_en_1) line_buf_1[wr_addr] <= px_in;
end
// 行/列计数
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
col_cnt <= 0;
row_cnt <= 0;
end else if (px_valid) begin
if (col_cnt == IMG_WIDTH - 1) begin
col_cnt <= 0;
row_cnt <= (row_cnt == IMG_HEIGHT - 1) ? 0 : row_cnt + 1;
end else begin
col_cnt <= col_cnt + 1;
end
end
end
// 窗口输出寄存器(流水线打拍)
reg [DATA_WIDTH-1:0] r0_c0, r0_c1, r0_c2;
reg [DATA_WIDTH-1:0] r1_c0, r1_c1, r1_c2;
reg [DATA_WIDTH-1:0] r2_c0, r2_c1, r2_c2;
reg valid_dly;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
r0_c0 <= 0; r0_c1 <= 0; r0_c2 <= 0;
r1_c0 <= 0; r1_c1 <= 0; r1_c2 <= 0;
r2_c0 <= 0; r2_c1 <= 0; r2_c2 <= 0;
valid_dly <= 0;
end else if (px_valid) begin
// 第0行(当前行)
r0_c0 <= r0_c1;
r0_c1 <= r0_c2;
r0_c2 <= px_in;
// 第1行(延迟1行)
r1_c0 <= r1_c1;
r1_c1 <= r1_c2;
r1_c2 <= (row_cnt >= 1) ? line_buf_0[rd_addr_0] : {DATA_WIDTH{1'b0}};
// 第2行(延迟2行)
r2_c0 <= r2_c1;
r2_c1 <= r2_c2;
r2_c2 <= (row_cnt >= 2) ? line_buf_1[rd_addr_1] : {DATA_WIDTH{1'b0}};
// 窗口有效:从第2行第2列开始
valid_dly <= (row_cnt >= 2) && (col_cnt >= 2);
end
end
assign win_valid = valid_dly;
assign win_r0_c0 = r0_c0; assign win_r0_c1 = r0_c1; assign win_r0_c2 = r0_c2;
assign win_r1_c0 = r1_c0; assign win_r1_c1 = r1_c1; assign win_r1_c2 = r1_c2;
assign win_r2_c0 = r2_c0; assign win_r2_c1 = r2_c1; assign win_r2_c2 = r2_c2;
endmodule
Testbench验证
module tb_line_buffer;
reg clk = 0, rst_n = 0, px_valid = 0;
reg [7:0] px_in = 0;
wire [7:0] w0c0,w0c1,w0c2,w1c0,w1c1,w1c2,w2c0,w2c1,w2c2;
wire win_valid;
line_buffer_3x3 #(.IMG_WIDTH(8), .IMG_HEIGHT(4)) dut (
.clk(clk), .rst_n(rst_n), .px_valid(px_valid), .px_in(px_in),
.win_valid(win_valid),
.win_r0_c0(w0c0), .win_r0_c1(w0c1), .win_r0_c2(w0c2),
.win_r1_c0(w1c0), .win_r1_c1(w1c1), .win_r1_c2(w1c2),
.win_r2_c0(w2c0), .win_r2_c1(w2c1), .win_r2_c2(w2c2)
);
always #5 clk = ~clk;
// 输入4行×8列递增数据
initial begin
integer i, j;
#20 rst_n = 1;
for (i = 0; i < 4; i = i + 1) begin
for (j = 0; j < 8; j = j + 1) begin
@(posedge clk);
px_valid = 1;
px_in = i*8 + j;
end
end
@(posedge clk);
px_valid = 0;
#100 $finish;
end
// 打印窗口
always @(posedge clk) begin
if (win_valid)
$display("t=%0t win=[%0d %0d %0d; %0d %0d %0d; %0d %0d %0d]",
$time, w0c0,w0c1,w0c2,w1c0,w1c1,w1c2,w2c0,w2c1,w2c2);
end
endmodule
运行与验证
- 仿真运行:使用Vivado/Xcelium/ModelSim运行testbench,观察窗口输出。
- 通过条件:当输入为4行×8列递增数据(0~31)时,第一个有效窗口应出现在第3行第3列像素(值18)输入后的下一个时钟,窗口内容为:
[8 9 10; 16 17 18; 24 25 26](对应行2~4、列2~4)。 - 资源验证:综合后BRAM占用为2个(每行1个),无额外DDR访问逻辑。
关键说明
- 行缓冲器深度等于图像宽度,宽度参数化可适配不同分辨率。
- 窗口输出比输入延迟2个时钟周期(流水线打拍),符合卷积计算时序要求。
- 图像边界(前2行/列)不产生有效窗口,由
win_valid信号屏蔽。