RTL可拓展设计:VerilogHDL实现2^N深度卷积器

@

参考资料

我的开源RTL代码库

使用方法

文件构成

  • multiplier/unsigned/Makefile:用于测试的make脚本文件
  • convolution_core/convolution_core.v:2^N深度卷积器主模块
  • multiplier/unsigned/testbench.sv:测试激励文件
  • multiplier/td:安路测试工程(仅用于资源使用估计,不可直接下载运行)

主模块信号及其参数配置

// convolution_core/convolution_core.v
module convolution_core #(
    parameter CONV_CORE_DEPTH = 128,
    parameter DATA_BITWIDTH = 16,
    parameter OUTPUT_SHIFT_BITS = 4
) (
    input clk,
    input rstn,

    // Data flow
    input                     data_in_enable,
    input [DATA_BITWIDTH-1:0] data_in,

    output                     data_pp_out_enable,
    output [DATA_BITWIDTH-1:0] data_pp_out,
    output [DATA_BITWIDTH-1:0] data_res_out,


    // APB interface
    input             p_sel,
    input      [ 3:0] p_strb,
    input      [31:0] p_addr,
    input      [31:0] p_wdata,
    input             p_ce,
    input             p_we,
    output reg        p_rdy,
    output reg [31:0] p_rdata
);
参数 备注
CONV_CORE_DEPTH 卷积器深度
DATA_BITWIDTH 卷积器数据位宽(最大32)
OUTPUT_SHIFT_BITS 输出右移的位数

这个参数还是需要说明一下的:CONV_CORE_DEPTH的取值只能2N,N大于4;DATA_BITWIDTH指示的是用于运算的输入数据的位宽,但是用于输入卷积器参数的APB总线的数据位宽最大只能是32,因此将DATA_BITWIDTH设置为32以上并无意义,仿真肯定是过不了的,在实际设计中也可能导致数据错误。

OUTPUT_SHIFT_BITS就是用来获取正常的输出数据的。众所周知,卷积其实就是乘完了再加起来。如果用这个卷积器来实现一个直接型FIR,那么FIR的参数最好就要做做处理,让所有FIR参数加起来是2N,那么将乘完加完的数值除以2N就可以进行归一化操作,也就是让输出数据跟输入数据至少在同一个数量级上。那么,除以2N就相当于右移N位,在卷积器输出的地方右移就行了。

以下是数据流相关信号的说明:

方向 位宽 信号 备注
输入 1 clk 系统时钟信号
输入 1 rstn 同步复位信号,低有效
输入 1 data_in_enable 数据流使能
输入 DATA_BITWIDTH data_in 数据流输入
输出 1 data_pp_out_enable 数据流流水线输出使能
输出 DATA_BITWIDTH data_pp_out 数据流流水线输出
输出 DATA_BITWIDTH data_res_out 卷积结果数据流

关于APB总线的信号及其时序,这里不多加赘述,这个模块上的APB是从机。

信号时序

流水线输入输出和结果输出的时序图

流水线输入输出和结果输出的时序图如上图所示。data_in信号是需要输入的数据,注意输入时需要将data_in_enbale同时拉高以开启流水线输入;data_pp_out信号是流水线末端输出的数据,可以看作是延迟了CONV_CORE_DEPTH-1时钟周期的data_in输入数据(对应使能输出信号 data_pp_out_enable,可以直接接入下一级卷积器的data_in_enbale),图中bdx = adx (x = 1,2,3,4),可以将这个数据连同送入下一级卷积器的data_in_enbale,用于拓展卷积器长度。

data_res_out就是卷积器的输出数据,因为卷积结构是一定历史时期数据的与卷积核做向量数乘的结果,而在这个全流水线结构中每从data_in输入一个数据,就会做一次乘法,并在下一个时钟周期将结果送入并行加法器,因此不好定义从data_in输入到data_res_out输出的时钟周期延迟(全并行流水线结构其实对这个也不是很在意)。

设计原理

先上代码:

// convolution_core/convolution_core.v
module convolution_core #(
    parameter CONV_CORE_DEPTH = 128,
    parameter DATA_BITWIDTH = 16,
    parameter OUTPUT_SHIFT_BITS = 4
) (
    input clk,
    input rstn,

    // Data flow
    input                     data_in_enable,
    input [DATA_BITWIDTH-1:0] data_in,

    output                     data_pp_out_enable,
    output [DATA_BITWIDTH-1:0] data_pp_out,
    output [DATA_BITWIDTH-1:0] data_res_out,


    // APB interface
    input             p_sel,
    input      [ 3:0] p_strb,
    input      [31:0] p_addr,
    input      [31:0] p_wdata,
    input             p_ce,
    input             p_we,
    output reg        p_rdy,
    output reg [31:0] p_rdata
);

  // APB Logic
  localparam CONF_BUF_LENGTH = CONV_CORE_DEPTH;
  localparam STATE_BUF_LENGTH = 8;
  reg  [31:0] conf_buf    [ 0:CONF_BUF_LENGTH - 1];
  wire [31:0] state_buf   [0:STATE_BUF_LENGTH - 1];
  reg  [31:0] conf_buf_ci;
  reg  [ 7:0] apb_state;
  localparam APB_RESET = 8'h00;
  localparam APB_IDEL = 8'h01;
  localparam APB_WRITE = 8'h02;
  localparam APB_READ = 8'h03;
  always @(posedge clk) begin
    if (!rstn) begin
      conf_buf_ci <= 32'h0;

      p_rdy       <= 1'b0;
      p_rdata     <= 32'h0;

      apb_state   <= APB_RESET;
    end else begin
      case (apb_state)
        APB_RESET: begin
          conf_buf[conf_buf_ci] <= 32'h0;

          if (conf_buf_ci < CONF_BUF_LENGTH - 1) begin
            conf_buf_ci <= conf_buf_ci + 1;
            apb_state   <= APB_RESET;
          end else begin
            conf_buf_ci <= 32'h0;
            apb_state   <= APB_IDEL;
          end

          p_rdy   <= 1'b0;
          p_rdata <= 32'h0;
        end
        APB_IDEL: begin
          conf_buf_ci <= 32'h0;

          p_rdy       <= 1'b0;
          p_rdata     <= 32'h0;

          if (p_sel) begin
            if (p_we) begin
              apb_state <= APB_WRITE;
            end else begin
              apb_state <= APB_READ;
            end
          end else begin
            apb_state <= APB_IDEL;
          end
        end
        APB_WRITE: begin
          conf_buf_ci <= 32'h0;

          if (p_ce) begin
            p_rdy            <= 1'b1;
            conf_buf[p_addr] <= p_wdata;

            apb_state        <= APB_IDEL;
          end else begin
            p_rdy <= 1'b0;
            if (p_addr < CONF_BUF_LENGTH) begin
              conf_buf[p_addr] <= conf_buf[p_addr];
            end

            apb_state <= APB_WRITE;
          end

          p_rdata <= 32'h0;
        end
        APB_READ: begin
          conf_buf_ci <= 32'h0;

          if (p_ce) begin
            p_rdy     <= 1'b1;
            p_rdata   <= (p_addr < CONF_BUF_LENGTH) ? conf_buf[p_addr] : state_buf[p_addr];

            apb_state <= APB_IDEL;
          end else begin
            p_rdy     <= 1'b0;
            p_rdata   <= 32'h0;

            apb_state <= APB_READ;
          end
        end
        default: begin
          conf_buf_ci <= 32'h0;

          p_rdy       <= 1'b0;
          p_rdata     <= 32'h0;

          apb_state   <= APB_RESET;
        end
      endcase
    end
  end

  genvar gen_i;
  genvar gen_j;
  genvar gen_k;

  // Generate a pipeline for input data
  reg [DATA_BITWIDTH-1:0] conv_pipeline[0:CONV_CORE_DEPTH - 1];
  generate
    for (gen_i = 1; gen_i < CONV_CORE_DEPTH; gen_i = gen_i + 1) begin : g_conv_pipeline
      always @(posedge clk) begin
        if (!rstn) begin
          conv_pipeline[gen_i] <= {DATA_BITWIDTH{1'b0}};
        end else begin
          if (data_in_enable) begin
            conv_pipeline[gen_i] <= conv_pipeline[gen_i-1];
          end else begin
            conv_pipeline[gen_i] <= conv_pipeline[gen_i];
          end

        end
      end
    end
  endgenerate
  always @(posedge clk) begin
    if (!rstn) begin
      conv_pipeline[0] <= {DATA_BITWIDTH{1'b0}};
    end else begin
      if (data_in_enable) begin
        conv_pipeline[0] <= data_in;
      end else begin
        conv_pipeline[0] <= conv_pipeline[0];
      end
    end
  end
  assign data_pp_out = conv_pipeline[CONV_CORE_DEPTH-1];

  // generate data_pp_out enable signal
  reg [CONV_CORE_DEPTH - 1:0] data_pp_out_enable_buf;
  generate
    for (gen_i = 1; gen_i < CONV_CORE_DEPTH; gen_i = gen_i + 1) begin : g_data_pp_out_enable_buf
      always @(posedge clk) begin
        if (!rstn) begin
          data_pp_out_enable_buf[gen_i] <= 1'b0;
        end else begin
          data_pp_out_enable_buf[gen_i] <= data_pp_out_enable_buf[gen_i-1];
        end
      end
    end
  endgenerate
  always @(posedge clk) begin
    if (!rstn) begin
      data_pp_out_enable_buf[0] <= 1'b0;
    end else begin
      data_pp_out_enable_buf[0] <= data_in_enable;
    end
  end
  assign data_pp_out_enable = data_pp_out_enable_buf[CONV_CORE_DEPTH-1];

  //Multipliers
  wire [(DATA_BITWIDTH*2)-1:0] mul_res[0:CONV_CORE_DEPTH - 1];
  generate
    for (gen_i = 0; gen_i < CONV_CORE_DEPTH; gen_i = gen_i + 1) begin : g_mul_res
      wire [31:0] conv_arg = conf_buf[gen_i];
      multiplier_unsigned #(
          .BITWIDTH_INPUT(DATA_BITWIDTH)
      ) multiplier_unsigned_inst (
          .clk (clk),
          .rstn(rstn),

          .a(conv_pipeline[gen_i]),
          .b(conv_arg[DATA_BITWIDTH-1:0]),
          .q(mul_res[gen_i])
      );
    end
  endgenerate

  // Adders
  reg [(DATA_BITWIDTH*2)-1:0] adder_bufs[0:(CONV_CORE_DEPTH-1)-1];
  generate
    for (gen_i = 0; gen_i < $clog2(CONV_CORE_DEPTH); gen_i = gen_i + 1) begin : g_adders

      wire [(DATA_BITWIDTH*2)-1:0] res_upper_layer[0:2**(gen_i+1)-1];
      for (gen_j = 0; gen_j < 2 ** (gen_i + 1); gen_j = gen_j + 1) begin : g_upper_layers
        if (gen_i == $clog2(CONV_CORE_DEPTH) - 1) begin : g_layer_0
          assign res_upper_layer[gen_j] = mul_res[gen_j];
        end else begin : g_else_layers
          assign res_upper_layer[gen_j] = adder_bufs[(2**(gen_i+1))-1+gen_j];
        end
      end

      for (gen_j = 0; gen_j < 2 ** gen_i; gen_j = gen_j + 1) begin : g_buf_adders
        always @(posedge clk) begin
          if (!rstn) begin
            adder_bufs[(2**gen_i)-1+gen_j] <= {(DATA_BITWIDTH * 2) {1'b0}};
          end else begin
            adder_bufs[(2**gen_i)-1+gen_j] <= res_upper_layer[2*gen_j] + res_upper_layer[2*gen_j+1];
          end
        end
      end

    end
  endgenerate

  wire [(DATA_BITWIDTH*2)-1:0] adder_bufs_0 = adder_bufs[0];
  assign data_res_out = adder_bufs_0[DATA_BITWIDTH-1+OUTPUT_SHIFT_BITS:OUTPUT_SHIFT_BITS];

endmodule  //convolution_core

模块的总流程框图如下:

请添加图片描述

数据先进入输入流水线,然后每一个时钟周期都会通过乘法器组与对应的系数相乘(系数未在图中画出),然后结果进入加法器树进行相加运算,最后得到的结果从data_res_out输出。

因为一个时钟周期做2个数以上的加法将会综合出一大片组合逻辑,极度不利于时序约束,因此需要加法器树对结果进行逐层相加。这个加法器树就是可拓展性实现路上的一个很大的绊脚石。稍微调动一下数据结构的相关知识,我们可以发现:最后一个加法器可以是这颗满二叉树的根结点,乘法器就相当于这颗二叉树的叶子结点。N级的流水线对应N个乘法器,也就是叶子结点数为N。设这个满二叉树有k层,稍稍调用一下这个式子:

$总结点数n=2^{k}-1$
$叶子结点数N=2^{k-1}$

所以:

$k=log_2(N)+1$
$n=2^{log_2(N)+1}-1=2*N-1$

我们就可以得到:加法器的个数为总结点数减去叶子结点数的结果,因此一共是CONV_CORE_DEPTH-1个。如果每一个加法器都需要一个寄存器做结果缓存,那么就有以下定义语句:

reg [(DATA_BITWIDTH*2)-1:0] adder_bufs[0:(CONV_CORE_DEPTH-1)-1];

因为这里的乘法器用的是文章《RTL可拓展设计:VerilogHDL实现2^N位宽无符号整数乘法器》中设计的乘法器,因此乘法器输出的位宽是DATA_BITWIDTH*2。

然后开始分配这些空间。比如,在一个8深度卷积器的示例中,共有0-2层(共3层)加法器,adder_bufs的深度即为7。每一层对adder_bufs的占用如下图所示:

请添加图片描述

其中,第0层(也就是输出寄存器)在adder_bufs的第0项,第1层占用1-2项,第2层占用3-6项。其数据流向如示例图所示。

再看代码,g_adders用于循环构建每一层的加法器,res_upper_layer抽象出本层的上一层的寄存器的地址:比如当前gen_i==1,则处于第1层,上一层的地址即为第2层的地址,也就是3到6。然后通过g_buf_adders循环构建加法器,将上一层的数据做加法运算并缓存到本层。

补充说明

posted @ 2026-09-18 10:03  LogicField  阅读(4)  评论(0)    收藏  举报