RTL可拓展设计:VerilogHDL实现2^N深度卷积器
@
参考资料
使用方法
文件构成
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循环构建加法器,将上一层的数据做加法运算并缓存到本层。
浙公网安备 33010602011771号