(7)CNN卷积层verilog实现

📅 2026/7/10 8:20:39
(7)CNN卷积层verilog实现
CNNCNNConvolutional Neural Network卷积神经网络是一种广泛应用于计算机视觉领域的深度学习模型。其核心思想是利用卷积运算提取图像特征再通过激活函数、池化层和全连接层完成特征压缩与分类决策。其中卷积层包含大量乘加运算是网络计算量最集中的部分因此常作为 FPGA 硬件加速的重点对象以提高推理速度并降低功耗。卷积层卷积层Convolution Layer是 CNN 中最核心的组成部分主要负责从输入图像或特征图中提取有效特征。其基本原理是利用卷积核Kernel在输入数据上滑动通过对应元素相乘并求和的方式生成新的特征图Feature Map。随着网络层数的加深卷积层能够逐步学习图像中的边缘、纹理、角点以及更高层次的目标特征。由于卷积运算包含大量乘加MAC操作因此卷积层通常占据 CNN 网络绝大部分的计算量也是 FPGA、GPU 等硬件平台重点加速的对象。对于 FPGA 实现而言卷积层通常采用并行乘法器和流水线加法树结构来提高运算吞吐率和处理性能。verilog实现卷积层1加载权重在 FPGA 实现 CNN 卷积层时卷积核权重通常预先存储在 ROM 中并在卷积运算过程中根据地址依次读出。系统上电后ROM 通过预初始化的.mem文件加载训练得到的权重参数卷积模块按照设定的地址顺序读取对应权重并与输入特征图像素进行乘法运算。由于卷积核参数在推理过程中通常保持不变因此采用 ROM 存储不仅能够节省逻辑资源还可以提高数据访问效率。在 FPGA 实现中ROM 一般由 Block RAMBRAM或 Distributed RAM 构成通过地址计数器控制权重读取并与卷积计算单元形成流水线结构从而实现卷积层的高速并行计算。示例代码如下。//init kernel weights always (posedge clk or negedge aresetn) begin if(~aresetn)begin addr 0; end else if(ena)begin if(addr param_num - 1)begin addr addr 1; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin r_addr 0; r_ena 0; end else begin r_addr addr; r_ena ena; end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin ena 0; end else if(addr param_num - 1)begin ena 1; end else begin ena 0; end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin for(i0; iparam_num; ii1) weight[i] 8d0; end else if(r_ena)begin weight[r_addr] data; end end xpm_memory_sprom #( .ADDR_WIDTH_A (8 ), .MEMORY_SIZE (2048 ), .READ_DATA_WIDTH_A (8 ), .MEMORY_INIT_FILE (../rtl/coef.mem ), .SIM_ASSERT_CHK (1 ), .READ_LATENCY_A (1 ), .MESSAGE_CONTROL (1 ), .MEMORY_PRIMITIVE (block ) )u_rom( .clka (clk ), .rsta (1b0 ), .ena (ena ), .regcea (1b1 ), .addra (addr ), .injectsbiterra (1b0 ), .injectdbiterra (1b0 ), .douta (data ) );25x5窗口在卷积运算开始之前需要从输入特征图中提取与卷积核尺寸对应的数据窗口。对于 5×5 卷积核FPGA 通常采用4 个行缓存Line Buffer配合移位寄存器的方式构建 5×5 滑动窗口。当图像数据按行连续输入时行缓存用于保存前四行像素数据而当前输入行作为第五行数据通过移位寄存器不断更新每一行的最近 5 个像素。随着每个时钟周期输入一个新像素窗口整体向右滑动一个像素位置从而实时输出完整的 5×5 邻域数据供卷积计算单元使用。该方法无需存储整帧图像具有资源占用少、处理延迟低和易于流水线实现等优点因此被广泛应用于 FPGA 图像处理和 CNN 卷积加速器设计中。示例代码如下。//generate 5x5 window //reset fifo reg [23:0] tdata_d1,tdata_d2,tdata_d3,tdata_d4,tdata_d5; reg tvalid_d1,tvalid_d2,tvalid_d3,tvalid_d4,tvalid_d5; reg tready_d1,tready_d2,tready_d3,tready_d4,tready_d5; reg [9:0] col_cnt,row_cnt; wire fifo_rst s_axis_tuser; wire [3:0] wr_en; wire [23:0] wr_data[0:3]; wire [3:0] rd_en; wire [23:0] rd_data[0:3]; assign wr_en[0] tready_d5 tvalid_d5; assign wr_en[1] rd_en[0]; assign wr_en[2] rd_en[1]; assign wr_en[3] rd_en[2]; assign wr_data[0] tdata_d5; assign wr_data[1] rd_data[0]; assign wr_data[2] rd_data[1]; assign wr_data[3] rd_data[2]; assign rd_en[0] wr_en[0] (row_cnt 1); assign rd_en[1] wr_en[0] (row_cnt 2); assign rd_en[2] wr_en[0] (row_cnt 3); assign rd_en[3] wr_en[0] (row_cnt 4); always (posedge clk or negedge aresetn) begin if(~aresetn)begin col_cnt 0; end else if(fifo_rst)begin col_cnt 0; end else if(wr_en)begin if(col_cnt WIDTH - 1)begin col_cnt 0; end else begin col_cnt col_cnt 1; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin row_cnt 0; end else if(fifo_rst)begin row_cnt 0; end else if(wr_en)begin if(col_cnt WIDTH - 1)begin row_cnt row_cnt 1; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin tdata_d1 0; tdata_d2 0; tdata_d3 0; tdata_d4 0; tdata_d5 0; tvalid_d1 0; tvalid_d2 0; tvalid_d3 0; tvalid_d4 0; tvalid_d5 0; tready_d1 0; tready_d2 0; tready_d3 0; tready_d4 0; tready_d5 0; end else begin tdata_d1 s_axis_tdata; tdata_d2 tdata_d1; tdata_d3 tdata_d2; tdata_d4 tdata_d3; tdata_d5 tdata_d4; tvalid_d1 s_axis_tvalid; tvalid_d2 tvalid_d1; tvalid_d3 tvalid_d2; tvalid_d4 tvalid_d3; tvalid_d5 tvalid_d4; tready_d1 s_axis_tready; tready_d2 tready_d1; tready_d3 tready_d2; tready_d4 tready_d3; tready_d5 tready_d4; end end genvar j; generate for(j 0; j 4; jj1)begin:gen_line_buffer xpm_fifo_sync #( .FIFO_MEMORY_TYPE (block ), // auto / block / distributed / ultra .FIFO_WRITE_DEPTH (FIFO_DEPTH ), .WRITE_DATA_WIDTH (24 ), .READ_DATA_WIDTH (24 ), .READ_MODE (fwft ) ) u_lane_buffer( .wr_clk (clk ), .rst ((~aresetn)|fifo_rst), .din (wr_data[j] ), .wr_en (wr_en[j] ), .full ( ), .dout (rd_data[j] ), .rd_en (rd_en[j] ), .empty ( ) ); end endgenerate wire [23:0] w04; wire [23:0] w14; wire [23:0] w24; wire [23:0] w34; wire [23:0] w44; reg [23:0] w00,w01,w02,w03; reg [23:0] w10,w11,w12,w13; reg [23:0] w20,w21,w22,w23; reg [23:0] w30,w31,w32,w33; reg [23:0] w40,w41,w42,w43; wire wd_valid; assign w04 rd_data[3]; assign w14 wr_data[3]; assign w24 wr_data[2]; assign w34 wr_data[1]; assign w44 wr_data[0]; assign wd_valid wr_en (row_cnt 4) (col_cnt 4); always (posedge clk or negedge aresetn) begin if(~aresetn)begin w03 0; w02 0; w01 0; w00 0; w13 0; w12 0; w11 0; w10 0; w23 0; w22 0; w21 0; w20 0; w33 0; w32 0; w31 0; w30 0; w43 0; w42 0; w41 0; w40 0; end else begin w03 w04; w02 w03; w01 w02; w00 w01; w13 w14; w12 w13; w11 w12; w10 w11; w23 w24; w22 w23; w21 w22; w20 w21; w33 w34; w32 w33; w31 w32; w30 w31; w43 w44; w42 w43; w41 w42; w40 w41; end end3乘法累加乘法累加Multiply-AccumulateMAC是卷积层计算的核心操作。在卷积运算过程中5×5 窗口中的像素数据与对应位置的卷积核权重分别进行乘法运算得到多个乘积结果再将所有乘积项累加求和从而生成当前输出特征图中的一个像素值。以 5×5×3 卷积为例需要完成 75 次乘法和 74 次加法运算。为了提高运算性能FPGA 通常利用 DSP 资源实现并行乘法并采用流水线加法树对乘积结果进行逐级累加从而在保证高时钟频率的同时实现高吞吐量的卷积计算。示例代码如下//multi wire signed [8:0] s_data[74:0]; //flatten assign s_data[0] {1b0,w00[23:16]}; assign s_data[1] {1b0,w01[23:16]}; assign s_data[2] {1b0,w02[23:16]}; assign s_data[3] {1b0,w03[23:16]}; assign s_data[4] {1b0,w04[23:16]}; assign s_data[5] {1b0,w10[23:16]}; assign s_data[6] {1b0,w11[23:16]}; assign s_data[7] {1b0,w12[23:16]}; assign s_data[8] {1b0,w13[23:16]}; assign s_data[9] {1b0,w14[23:16]}; assign s_data[10] {1b0,w20[23:16]}; assign s_data[11] {1b0,w21[23:16]}; assign s_data[12] {1b0,w22[23:16]}; assign s_data[13] {1b0,w23[23:16]}; assign s_data[14] {1b0,w24[23:16]}; assign s_data[15] {1b0,w30[23:16]}; assign s_data[16] {1b0,w31[23:16]}; assign s_data[17] {1b0,w32[23:16]}; assign s_data[18] {1b0,w33[23:16]}; assign s_data[19] {1b0,w34[23:16]}; assign s_data[20] {1b0,w40[23:16]}; assign s_data[21] {1b0,w41[23:16]}; assign s_data[22] {1b0,w42[23:16]}; assign s_data[23] {1b0,w43[23:16]}; assign s_data[24] {1b0,w44[23:16]}; assign s_data[25] {1b0,w00[15:8]}; assign s_data[26] {1b0,w01[15:8]}; assign s_data[27] {1b0,w02[15:8]}; assign s_data[28] {1b0,w03[15:8]}; assign s_data[29] {1b0,w04[15:8]}; assign s_data[30] {1b0,w10[15:8]}; assign s_data[31] {1b0,w11[15:8]}; assign s_data[32] {1b0,w12[15:8]}; assign s_data[33] {1b0,w13[15:8]}; assign s_data[34] {1b0,w14[15:8]}; assign s_data[35] {1b0,w20[15:8]}; assign s_data[36] {1b0,w21[15:8]}; assign s_data[37] {1b0,w22[15:8]}; assign s_data[38] {1b0,w23[15:8]}; assign s_data[39] {1b0,w24[15:8]}; assign s_data[40] {1b0,w30[15:8]}; assign s_data[41] {1b0,w31[15:8]}; assign s_data[42] {1b0,w32[15:8]}; assign s_data[43] {1b0,w33[15:8]}; assign s_data[44] {1b0,w34[15:8]}; assign s_data[45] {1b0,w40[15:8]}; assign s_data[46] {1b0,w41[15:8]}; assign s_data[47] {1b0,w42[15:8]}; assign s_data[48] {1b0,w43[15:8]}; assign s_data[49] {1b0,w44[15:8]}; assign s_data[50] {1b0,w00[7:0]}; assign s_data[51] {1b0,w01[7:0]}; assign s_data[52] {1b0,w02[7:0]}; assign s_data[53] {1b0,w03[7:0]}; assign s_data[54] {1b0,w04[7:0]}; assign s_data[55] {1b0,w10[7:0]}; assign s_data[56] {1b0,w11[7:0]}; assign s_data[57] {1b0,w12[7:0]}; assign s_data[58] {1b0,w13[7:0]}; assign s_data[59] {1b0,w14[7:0]}; assign s_data[60] {1b0,w20[7:0]}; assign s_data[61] {1b0,w21[7:0]}; assign s_data[62] {1b0,w22[7:0]}; assign s_data[63] {1b0,w23[7:0]}; assign s_data[64] {1b0,w24[7:0]}; assign s_data[65] {1b0,w30[7:0]}; assign s_data[66] {1b0,w31[7:0]}; assign s_data[67] {1b0,w32[7:0]}; assign s_data[68] {1b0,w33[7:0]}; assign s_data[69] {1b0,w34[7:0]}; assign s_data[70] {1b0,w40[7:0]}; assign s_data[71] {1b0,w41[7:0]}; assign s_data[72] {1b0,w42[7:0]}; assign s_data[73] {1b0,w43[7:0]}; assign s_data[74] {1b0,w44[7:0]}; reg signed [16:0] r_mul_result[74:0]; always (posedge clk or negedge aresetn) begin if(~aresetn)begin for(i0;i75;ii1)begin r_mul_result[i] 0; end end else if(wd_valid)begin for(i0;i75;ii1)begin r_mul_result[i] s_data[i] * weight[i]; end end else begin for(i0;i75;ii1)begin r_mul_result[i] 0; end end end//Accumulate //total 2^7 128 75 ,so total 7 times add. reg signed [17:0] step_0[37:0]; reg signed [18:0] step_1[18:0]; reg signed [19:0] step_2[9:0]; reg signed [20:0] step_3[4:0]; reg signed [21:0] step_4[2:0]; reg signed [22:0] step_5[1:0]; reg signed [23:0] conv_sum; reg conv_sum_vld; reg valid_d1,valid_d2,valid_d3,valid_d4,valid_d5,valid_d6,valid_d7; always (posedge clk or negedge aresetn) begin if(~aresetn)begin valid_d1 0; valid_d2 0; valid_d3 0; valid_d4 0; valid_d5 0; valid_d6 0; valid_d7 0; conv_sum_vld 0; end else begin valid_d1 wd_valid; valid_d2 valid_d1; valid_d3 valid_d2; valid_d4 valid_d3; valid_d5 valid_d4; valid_d6 valid_d5; valid_d7 valid_d6; conv_sum_vld valid_d7; end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin for(i0;i38;ii1)begin step_0[i] 0; end end else if(valid_d1)begin for(i0;i38;ii1)begin if(i 37)begin step_0[i] r_mul_result[2*i]; end else begin step_0[i] r_mul_result[2*i] r_mul_result[2*i 1]; end end end else begin for(i0;i38;ii1)begin step_0[i] 0; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin for(i0;i19;ii1)begin step_1[i] 0; end end else if(valid_d2)begin for(i0;i19;ii1)begin step_1[i] step_0[2*i] step_0[2*i 1]; end end else begin for(i0;i19;ii1)begin step_1[i] 0; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin for(i0;i10;ii1)begin step_2[i] 0; end end else if(valid_d3)begin for(i0;i10;ii1)begin if(i 9)begin step_2[i] step_1[2*i]; end else begin step_2[i] step_1[2*i] step_1[2*i 1]; end end end else begin for(i0;i10;ii1)begin step_2[i] 0; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin for(i0;i5;ii1)begin step_3[i] 0; end end else if(valid_d4)begin for(i0;i5;ii1)begin step_3[i] step_2[2*i] step_2[2*i 1]; end end else begin for(i0;i5;ii1)begin step_3[i] 0; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin for(i0;i3;ii1)begin step_4[i] 0; end end else if(valid_d5)begin for(i0;i3;ii1)begin if(i 2)begin step_4[i] step_3[2*i]; end else begin step_4[i] step_3[2*i] step_3[2*i 1]; end end end else begin for(i0;i3;ii1)begin step_4[i] 0; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin for(i0;i2;ii1)begin step_5[i] 0; end end else if(valid_d6)begin for(i0;i2;ii1)begin if(i 1)begin step_5[i] step_4[2*i]; end else begin step_5[i] step_4[2*i] step_4[2*i 1]; end end end else begin for(i0;i2;ii1)begin step_5[i] 0; end end end always (posedge clk or negedge aresetn) begin if(~aresetn)begin conv_sum 0; end else if(valid_d7)begin conv_sum step_5[0] step_5[1]; end else begin conv_sum 0; end end该模块可以实现任意尺寸的图像输出并通过mem文件加载权重灵活性非常高。仿真验证1加载权重5x5x3一共是75个权重参数。验证时给的数据是有规律的方便判断。25x5窗口在window的valid信号拉高时观察窗口的25个值发现没问题验证时给的数据是有规律的方便判断。3乘法累加每一个conv_sum_vld拉高时cnt加164x64x3经过卷积核后是60x60x1正好是3600个值因此这部分也正确。4python与verilog结果对比随机产生卷积核并且量化成int8类型同时给到python和rtl。kernel_f np.array([ [[ 0.12, -0.35, 0.07], [ 0.21, 0.08, -0.18], [ 0.05, -0.11, 0.02], [-0.19, 0.14, 0.17], [-0.08, 0.06, -0.03]], [[ 0.18, -0.22, 0.09], [ 0.31, 0.05, -0.27], [ 0.03, -0.16, 0.12], [-0.25, 0.18, 0.06], [-0.07, 0.02, -0.11]], [[ 0.15, -0.28, 0.10], [ 0.27, 0.01, -0.21], [ 0.00, 0.00, 0.00], [-0.21, 0.16, 0.04], [-0.09, -0.01, -0.13]], [[ 0.11, -0.24, 0.08], [ 0.25, 0.03, -0.23], [ 0.04, -0.12, 0.08], [-0.23, 0.20, 0.02], [-0.05, 0.04, -0.08]], [[ 0.09, -0.19, 0.05], [ 0.17, 0.07, -0.15], [ 0.02, -0.08, 0.01], [-0.14, 0.12, 0.03], [-0.03, 0.01, -0.06]] ], dtypenp.float32) scale 127.0 / np.max(np.abs(kernel_f)) kernel np.round(kernel_f * scale).astype(np.int8) weight_flat kernel.transpose(2, 0, 1).reshape(-1) weight_flat weight_flat.reshape(-1) with open(coef.mem,w) as f: for w in weight_flat: f.write(f{int(w) 0xFF:02X}\n)仿真的激励为224x224尺寸的lena图case主要实现rtl对输入图片进行卷积运算并接收结果然后和python的golden ref作一个对比。cocotb.test() async def tb_conv(dut): ori_img load_img(IMG_PATH) gold_ref golden_ref(ori_img,kernel) h, w, c ori_img.shape out_buf np.zeros((h-4, w-4), dtypenp.int32) #sim begin await init_sys(dut,10) await Timer(200,unitsns) #启动接收图像 recv_task cocotb.start_soon(recv_gray_task(dut, h-4, w-4, out_buf)) await load_img_2_dut(dut,ori_img) await recv_task await Timer(20000,unitsns) if COMPARE_RESULT_ON: compare_result(gold_ref,out_buf) else: passgolden ref 的代码如下def golden_ref(img,kernel): img img.astype(np.int32) H, W, C img.shape out_h H - 5 1 out_w W - 5 1 output np.zeros((out_h, out_w), dtypenp.int32) for y in range(out_h): for x in range(out_w): window img[y:y5, x:x5, :] # 5x5x3 output[y, x] np.sum(window * kernel) return output卷积结果误差对比热力图如下可以发现误差为0因为权重量化过后python与rtl的行为完全一致所以卷积结果的误差为0是正常的结果也证明了逻辑实现的功能完全正确。卷积后的效果图总结本文实现卷积层采用 Verilog HDL 进行设计支持任意分辨率图像的卷积运算。输入图像数据通过行缓存Line Buffer和移位寄存器构建 5×5 滑动窗口卷积核权重则通过预初始化的 MEM 文件加载至 ROM 中实现卷积参数的灵活配置与快速更新。模块内部采用并行乘法器与流水线加法树结构完成乘法累加MAC运算在保证高吞吐率的同时有效提高系统时钟频率。通过修改 MEM 文件即可切换不同卷积核因此具有良好的可扩展性、可移植性和参数化配置能力。