同步发布于个人博客

简要说明

使用Verilog实现核大小为3*3的简单卷积。

计算作用的图像来自MNIST数据集(自行查阅),图像大小为28*28,像素为8bit,长这样:

digit_1

图像的像素点很少可以直接存入BRAM中:图像按行顺序展开为一行784个的8bit数,然后用coe文件存入Vivado提供的ROM中。卷积计算的结果为26*26大小的数组,按行展开存入RAM中。

注:因为只是简单尝试实现卷积,没有过于考虑速度和资源消耗

模块设计

rom

用于存储图像,使用BRAM IP核

在Vivado中操作:

  • 选择类型为单端口ROM

image-20241218154943397

  • 设置数据位宽和地址深度,设置总是使能
  • 注意:不使用输出寄存器。输出寄存器会导致时许偏移,详细自行查阅

image-20241218154910151

  • 设置coe文件初始化ROM

image-20241218154828555

  • 打包一下

    `timescale 1ns / 1ps
    
    module rom (
        input in_clk,
        input [9:0] in_rom_addr,
    
        output [7:0] out_rom_data
    );
    
        rom_gen_0 u_rom1 (
            .clka(in_clk),
            .addra(in_rom_addr),
            .douta(out_rom_data)
        );
    
    endmodule
    

image_read

从存储图像的ROM中读取用于卷积计算的9个像素点

端口配置如下:

module image_read(
    input in_clk,
    input in_rst_n,
    input in_conv_cal,
    input [7:0] in_rom_data,  // unsigned 8bit

    output [9:0] out_rom_addr,
    output out_image_end,
    output out_data_valid,
    output [71:0] out_pixels
    );

in_rom_dataout_rom_addr为与ROM数据口和地址口连接的端口,

9个像素读取完成时,out_data_valid置为高,9个像素从out_pixels送出,

卷积模块读取像素后进行计算时,in_conv_cal被置为高,下一轮读取ROM开始,

当整张图像都被读取完成,out_image_end置为高,模块不再与ROM交互。

image_read模块设计的难处在于怎么从被展开为一行的784个像素中读取需要的9个像素,这9个像素位于不同的行和列。

设计的思想:用行和列坐标表示3*3像素数组的左上角坐标,根据该读取第几个像素来计算需要读取的像素在0~783的位置。

挑重点讲:

  • 主要的信号为:

        reg [3:0] r_read_cnt;  // 0~8
        reg [4:0] r_read_ptr_x;  // 0~25
        reg [7:0] r_read_ptr_y;  // 0~25*7
        reg [9:0] r_rom_addr;  // 0~783
    

    r_read_cnt从0计数到9,记录当前读取到第几个像素,

    r_read_ptr_xr_read_ptr_y记录前述左上角的坐标,很显然这个坐标只需要从(0,0)到(25,25)即可,所以r_read_ptr_x的大小为025。但是如果`r_read_ptr_y`也是025,那么计算坐标时需要将r_read_ptr_y与28相乘,

    秉持尽量不使用乘号的原则换个思路:r_read_ptr_y可以每次加28,此时表示的位置是784个像素中一行像素的起始位置,但是r_read_ptr_y就需要存储大至25*28大小的数,注意到28是4的倍数,就是说r_read_ptr_y如果存储28的倍数,那么低两位为00,所以可以隐含,r_read_ptr_y每次加7即可

  • 综上,ROM地址r_rom_addr的计算可以表示如下:

        always @(*) begin
            case (r_read_cnt)
                'd0 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2);
                'd1 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2) + 'd1;
                'd2 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2) + 'd2;
                'd3 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2) + 'd28;
                'd4 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2) + 'd29;
                'd5 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2) + 'd30;
                'd6 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2) + 'd56;
                'd7 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2) + 'd57;
                'd8 : r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2) + 'd58;
                default: r_rom_addr = r_read_ptr_x + (r_read_ptr_y << 2);
            endcase
        end
    

PE

PE是计算卷积的单元,3*3的卷积一共有9个PE单元,PE单元的卷积参数可以单独配置。

PE单元实现的功能是A*B,A为像素数据,B为卷积参数,

A*B的实现使用Vivado提供的DSP IP核:

  • 选择计算指令为A*B

image-20241219103921529

  • 不加入中间寄存器,在后续模块的设计时会使用寄存器存储结果

    image-20241219103945628

  • 设置输入输出的位宽

    A(像素数据)设置为9位,因为DSP处理的是有符号数,8bit的像素为无符号数,需要高位补0后计算,

    B(卷积参数)为5位;P(输出结果)的位宽配置为14位

    image-20241219104027774

代码示例:

`timescale 1ns / 1ps

module PE (
    input [7:0] in_pixel,  // unsigned 8bit
    input signed [4:0] in_weight,  // signed 5bit
    
    output signed [13:0] out_res  // signed 14bit
    );

    dsp_macro_0 u_dsp (
        .A({1'b0, in_pixel}),
        .B(in_weight),

        .P(out_res)
    );

endmodule

conv

conv模块实例化PE模块,读取image_read的9个像素后协调PE模块进行计算,

计算的思路:9个像素分别送入各个PE模块,再将这些结果累加

注意:DSP IP核支持A*B+C的运算,但是仿真时结果一直不正确,并且发现A+C的运算也不正确,所以没有用DSP算加法,问题的原因未知,DSP IP的版本是xilinx.com:ip:dsp_macro:1.0

端口定义:

module conv(
    input in_clk,
    input in_rst_n,
    input in_data_valid,
    input [71:0] in_pixels,

    output out_conv_cal,
    output [9:0] out_ram_addr,
    output [13:0] out_ram_data,
    output out_ram_en,
    output out_ram_we
    );

ram

RAM中存储卷积计算后的26*26个结果,位宽为14bit,与conv计算的结果对应

  • 选择类型为单端口RAM

image-20241219105300023

  • 配置位宽等参数

image-20241219105344242

  • 打包一下

    `timescale 1ns / 1ps
    
    module ram(
        input in_clk,
        input [9:0] in_ram_addr,
        input [13:0] in_ram_data,
        input in_ram_en,
        input in_ram_we
        );
    
        ram_gen_0 u_ram (
            .clka(in_clk),
            .addra(in_ram_addr),
            .dina(in_ram_data),
            .ena(in_ram_en),
            .wea(in_ram_we),
    
            .douta()
        );
    endmodule
    

注意:RAM只用于存储数据,没有读取,综合时可能会被Vivado优化

顶层设计

使用Block Design添加模块并连线:

image-20241219105621632

仿真

在Vivado进行一个简单的仿真

特别注意:DPS IP的仿真文件是VHDL语言的,需要把仿真语言设置为Mixed才能仿真

image-20241219114258770.png

从ROM中读出的9个像素为FC FC FC FC FC FC FC FC FC(十六进制),卷积核为[[1, -1, 1], [1, -1, 1], [1, -1, 1]]

卷积计算的结果为2F4,正好为FC的三倍,结果正确。可以看到,计算一次卷积大概需要11个时钟,处理速度的瓶颈在于读取ROM数据和累加计算

在仿真时把写入RAM的数据存入txt文本,可以看到正好26*26=676个数image-20241219212508456

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐