我设计了图1所示“双存储器”结构实现上述两条数据结构功能,其物理载体都是Cyclone系列中的M4K存储块(M4K RAM blocks)。图中绿色部分是存储输入数据s[k]的缓冲区,是“简单双端口RAM”。它有两个数据端口:一个负责向缓冲区存入新数据,每个A/D采样周期存入一次;另一个负责读取乘加运算所需的数据,每个A/D采样周期读取16次(即N/L次)。其存取采用“环形队列”数据结构。图中蓝色部分是卷积系数h[k]的存储器,它每个A/D采样周期读取16次,数据不更新。
另外,当互相关/卷积的模板具备对称性时,也就是对应的FIR滤波器具有线性相位的情况下,图1所示的算法电路还能进一步简化,计算量降低为N/2次乘加MAC运算,这里就不讨论了。
二、卷积节控制电路的设计
通过上文的描述,相信读者可以感觉得到:整个卷积电路的重点在每个卷积节的实现,而卷积节实现的关键是能够正确的控制“数据缓冲区”、“系数存储器”和“乘加器(MAC)”的协调工作。其工作内容是在每个A/D采样结果到来,而下一个结果未到之前,完成:
1、产生一个写地址,将A/D结果存入缓冲区最老数据所在位置,同时将最老的数据输出给下一个卷积节;
2、同步产生两组、各16个读地址,分别从数据缓冲区和系数存储器读取一遍其中的所有数据;
3、将顺序读取的两组数据顺序乘加在一起。
实现上述功能的电路Verilog-HDL实现如下:
1 module conv_ctlr(rst_n,clk,start,coe_rom_addr,rd_data_dpram_addr,acc_clr,wren,wr_data_dpram_addr,flag,mac_en); 2 //卷积控制信号产生电路模块 3 //可以控制一个MAC电路,分多个时钟周期,实现多个数值的相加。 4 //这里实现的是控制16级卷积的功能 5 input rst_n;//低电平复位信号,可以复位首地址指针 6 input clk;//工作时钟信号 7 input start;//启动一次控制逻辑输出的启动信号,下降启动一轮操作 8 output[3:0] coe_rom_addr;//参与卷积的固定信号rom地址信号 9 output[3:0] rd_data_dpram_addr;//读取双口RAM缓冲中数据的地址信号 10 output acc_clr; //累加器清零信号,高电平用于清除MAC电路的结果,准备下一次卷积计算 11 output wren; //写使能信号,每个start周期向下一个DPRAM的缓冲(本级的缓冲区也可以使用这个写入信号)里写入一个新数据的使能信号 12 output[3:0] wr_data_dpram_addr;//写入DPRAM缓冲区的新数据的地址线 13 output flag;//标志信号,表示卷积电路在工作 14 output mac_en;//乘加器的使能信号,为1时每个上升沿执行乘加 15 wire flag;//状态标志,为1时表示正在进行卷积操作,否则表示没有操作 16 reg[9:0] flag_cnt;//状态计数器,start信号到来后还是计数,当其大于一定值后切换标志 17 reg[3:0] current_pt;//当前位置指针,会在每次start到来卷积后加一,以指向下一次数据缓冲器的首地址 18 assign acc_clr = start; 19 assign mac_en = ((flag_cnt[9:0]>=10'd3)&(flag_cnt[9:0]<=10'd20))? 1'b1 : 1'b0;//只有当flag_cnt等于3时系数和dpram才能读出所需的数据,注意读锁存器造成的一个周期的延迟。 20 ///////向后续卷积节传递最老的一个数据的控制信号 21 assign wren = (flag_cnt[9:0] == 10'd20)? 1'b1 : 1'b0;//这里要看仿真时序图才能明白,flag_cnt=20的周期刚好读出本轮以后不再使用的数据 22 assign wr_data_dpram_addr[3:0] = current_pt[3:0]; 23 always @ (posedge start or negedge rst_n) 24 begin 25 if(!rst_n) 26 current_pt[3:0] <= 4'b1111;//复位后为最大值,第一个start脉冲使其从0开始 27 else 28 current_pt[3:0] <= current_pt[3:0] + 4'd1;//首地址缓冲器,会在每次采样(或start)信号到来后加一,为下次输入新数据移动缓冲器做好准备 29 end 30 assign flag = ((flag_cnt[9:0]>=10'd2)&(flag_cnt[9:0]<=10'd20))? 1'b1 : 1'b0; 31 always @ (negedge clk or posedge start)//注意为了让flag从clk下降沿开始,以上升沿为每个周期的中心,这里用用flag的下降沿计数 32 begin 33 if(start) 34 begin 35 flag_cnt[9:0] <= 10'd0; 36 end 37 else begin 38 if(flag_cnt[9:0] <= (10'd16 + 10'd1 + 10'd3)) 39 //计到15再加1,则一共1-16个脉冲;加1是为了留出第一个时钟周期作为dpram的地址潜伏期;加2是为了等待乘法器的潜伏期(潜伏期为3,但最后一个不用乘加) 40 begin 41 flag_cnt[9:0] <= flag_cnt[9:0] +1'd1; 42 end 43 else 44 begin 45 flag_cnt[9:0] <= flag_cnt[9:0]; 46 end 47 end 48 end 49 pset_addr_cnt i_rd_addr_cnt(//例化读缓冲当前地址计数器 50 .rst_n(rst_n), 51 .load(!flag),//每产生一个启动信号,就要重新对首地址置新值 52 .clk(clk), 53 .pre_value(current_pt), 54 .addr_cnt(rd_data_dpram_addr) 55 ); 56 addr_cnt i_coe_rom_addr_cnt(//例化系数地址产生计数器 57 .en(flag), 58 .clk(clk), 59 .addr_cnt(coe_rom_addr) 60 ); 61 endmodule 62 63 module addr_cnt(en,clk,addr_cnt); 64 //本模块产生4位计数值,作为地址产生器 65 input en;//高电平有效 66 input clk; 67 output[3:0] addr_cnt; 68 reg[3:0] addr_cnt; 69 always @ (negedge clk or negedge en)//为了使地址再时钟下降沿产生,上升沿位于数据中央,clk使用了下降沿 70 begin 71 if(!en) 72 addr_cnt[3:0] <= 4'd0; 73 else 74 addr_cnt[3:0] <= addr_cnt[3:0] + 4'd1; 75 end 76 endmodule 77 78 //带预置功能的地址计数器,4位 79 module pset_addr_cnt(rst_n,load,clk,pre_value,addr_cnt); 80 input rst_n; 81 input load;//load高电平时同步加载初值 82 input clk; 83 input[3:0] pre_value;//输入的预置初值 84 output[3:0] addr_cnt;//计数输出 85 reg[3:0] addr_cnt; 86 always @(negedge clk or negedge rst_n)//为了使地址再时钟下降沿产生,上升沿位于数据中央,clk使用了下降沿 87 begin 88 if(!rst_n) 89 addr_cnt[3:0] <= 4'd0;//pre_value[3:0]; 90 else begin 91 if(load == 1'b1) 92 addr_cnt[3:0] <= pre_value[3:0]; 93 else 94 addr_cnt[3:0] <= addr_cnt[3:0] + 4'd1; 95 end 96 end 97 endmodule