4. 单通道3x3卷积模块

任务目标

设计一个单通道3x3卷积RTL模块,接收图像像素流,输出卷积结果流。模块采用行缓冲结构实现3x3窗口滑动,完成乘累加运算后输出单像素卷积结果。

接口定义


clk         : 时钟
rst_n       : 异步复位,低有效
pixel_in    : 输入像素,8bit无符号
pixel_valid : 输入有效标志
row_start   : 新一行开始标志(第一个像素时拉高一个周期)
conv_out    : 卷积结果,16bit有符号
out_valid   : 输出有效标志

卷积核与参数

使用固定3x3卷积核(可综合为常量):


+1 +0 -1
+2 +0 -2
+1 +0 -1   (Sobel垂直边缘检测核)

输入图像宽度可配置,通过参数 IMG_WIDTH 指定,默认64像素。像素为8bit无符号,卷积结果为有符号16bit(最大范围:255×8 = 2040,16bit足够)。

关键完整代码


module conv3x3_single_ch #(
    parameter IMG_WIDTH = 64
)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire [7:0]  pixel_in,
    input  wire        pixel_valid,
    input  wire        row_start,
    output reg  [15:0] conv_out,
    output reg         out_valid
);

    // 行缓冲:存储前两行像素
    reg [7:0] line_buf [0:1][0:IMG_WIDTH-1];
    
    // 3x3窗口寄存器
    reg [7:0] win [0:2][0:2];
    
    // 行缓冲写地址
    reg [$clog2(IMG_WIDTH)-1:0] wr_addr;
    reg [$clog2(IMG_WIDTH)-1:0] rd_addr;
    
    // 行计数与像素计数
    reg [1:0] row_cnt;
    reg       is_valid_row;  // 当前行是否为有效数据行(第3行起)
    
    // 写地址控制
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_addr <= 0;
            row_cnt <= 0;
        end else if (pixel_valid) begin
            if (row_start) begin
                wr_addr <= 0;
                if (row_cnt == 2'd2)
                    row_cnt <= 2'd2;  // 保持最大2
                else
                    row_cnt <= row_cnt + 1'b1;
            end else begin
                wr_addr <= wr_addr + 1'b1;
            end
        end
    end
    
    // 写入行缓冲(交替写入两行)
    always @(posedge clk) begin
        if (pixel_valid) begin
            if (row_cnt == 2'd0)
                line_buf[0][wr_addr] <= pixel_in;
            else if (row_cnt == 2'd1)
                line_buf[1][wr_addr] <= pixel_in;
            // row_cnt==2时,当前行直接进入窗口,不写缓冲
        end
    end
    
    // 读地址(滞后写地址一拍,用于窗口移位)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            rd_addr <= 0;
        else if (pixel_valid) begin
            if (row_start)
                rd_addr <= 0;
            else
                rd_addr <= rd_addr + 1'b1;
        end
    end
    
    // 窗口更新
    wire [7:0] buf0_data = line_buf[0][rd_addr];
    wire [7:0] buf1_data = line_buf[1][rd_addr];
    
    always @(posedge clk) begin
        if (pixel_valid) begin
            // 第0行数据
            win[0][0] <= win[0][1];
            win[0][1] <= win[0][2];
            win[0][2] <= (row_cnt == 2'd0) ? pixel_in : buf0_data;
            
            // 第1行数据
            win[1][0] <= win[1][1];
            win[1][1] <= win[1][2];
            win[1][2] <= (row_cnt == 2'd1) ? pixel_in : buf1_data;
            
            // 第2行数据(当前输入行)
            win[2][0] <= win[2][1];
            win[2][1] <= win[2][2];
            win[2][2] <= pixel_in;
        end
    end
    
    // 有效输出标志:第3行起且列位置≥2
    reg [1:0] valid_delay;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            valid_delay <= 2'd0;
            is_valid_row <= 1'b0;
        end else if (pixel_valid) begin
            if (row_start) begin
                if (row_cnt == 2'd2)
                    is_valid_row <= 1'b1;
                else if (row_cnt == 2'd0)
                    is_valid_row <= 1'b0;
            end
            // 列位置判断:rd_addr>=2 且当前行有效
            if (is_valid_row && (rd_addr >= 2))
                valid_delay <= 2'd3;  // 等待窗口完全填充
            else if (valid_delay != 2'd0)
                valid_delay <= valid_delay - 1'b1;
        end
    end
    
    // 卷积计算
    wire signed [15:0] p00 = $signed({8'b0, win[0][0]});
    wire signed [15:0] p01 = $signed({8'b0, win[0][1]});
    wire signed [15:0] p02 = $signed({8'b0, win[0][2]});
    wire signed [15:0] p10 = $signed({8'b0, win[1][0]});
    wire signed [15:0] p11 = $signed({8'b0, win[1][1]});
    wire signed [15:0] p12 = $signed({8'b0, win[1][2]});
    wire signed [15:0] p20 = $signed({8'b0, win[2][0]});
    wire signed [15:0] p21 = $signed({8'b0, win[2][1]});
    wire signed [15:0] p22 = $signed({8'b0, win[2][2]});
    
    wire signed [15:0] sum = p00 - p02 + (p10 << 1) - (p12 << 1) + p20 - p22;
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            conv_out <= 16'd0;
            out_valid <= 1'b0;
        end else begin
            conv_out <= sum;
            out_valid <= (valid_delay == 2'd1) ? 1'b1 : 1'b0;
        end
    end

endmodule

运行与验证

Testbench验证要点:

  1. 输入一个8×8测试图像(需满足 IMG_WIDTH=8 重新例化),按行逐像素输入,每行第一个像素拉高 row_start。
  2. 前两行输入时 out_valid 保持低电平;从第3行第3个像素起,out_valid 拉高并输出对应卷积值。
  3. 对比手工计算的Sobel结果,例如中心像素周围全为常数100时,输出应为0(平坦区域梯度为0)。

通过条件:仿真波形中 out_valid 时序正确,且输出值与手工计算一致。典型测试:输入全0图像,输出恒为0;输入垂直边缘图像(左半0,右半255),边缘列输出非零值。

仿真命令(Vivado/XSim)


# 在Vivado Tcl控制台或终端执行
xvlog conv3x3_single_ch.v tb_conv3x3.v
xelab tb_conv3x3 -debug typical
xsim tb_conv3x3 -runall

常见问题

  • 输出延迟:从输入到输出共3个时钟周期延迟(窗口填充2拍+计算1拍),属于正常流水延迟。
  • 边界处理:本设计不输出图像边界像素(前2行和前2列),如需边界填充需额外逻辑。
  • 行缓冲深度:参数 IMG_WIDTH 必须与实际图像宽度一致,否则窗口数据错位。