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

运行与验证

  1. 仿真运行:使用Vivado/Xcelium/ModelSim运行testbench,观察窗口输出。
  2. 通过条件:当输入为4行×8列递增数据(0~31)时,第一个有效窗口应出现在第3行第3列像素(值18)输入后的下一个时钟,窗口内容为:
    [8 9 10; 16 17 18; 24 25 26](对应行2~4、列2~4)。
  3. 资源验证:综合后BRAM占用为2个(每行1个),无额外DDR访问逻辑。

关键说明

  • 行缓冲器深度等于图像宽度,宽度参数化可适配不同分辨率。
  • 窗口输出比输入延迟2个时钟周期(流水线打拍),符合卷积计算时序要求。
  • 图像边界(前2行/列)不产生有效窗口,由win_valid信号屏蔽。
像素输入流 px_in (行顺序) 行0 (直通) 行缓冲器1 延迟1行 行缓冲器2 延迟2行 3×3 窗口寄存器 win_r0~r2 × c0~c2 卷积核 每个时钟周期: 1次BRAM读 + 1次BRAM写 输出1个完整3×3窗口 外部DDR访问量:仅1次/像素