1. 阵列乘法器基础概念
第一次接触阵列乘法器时,我完全被那些密密麻麻的全加器连线图吓到了。后来才发现,这东西就像搭积木一样有趣。简单来说,阵列乘法器就是用全加器搭建的乘法计算电路,特别适合在FPGA上实现。和我们平时手算乘法的原理很像,都是先逐位相乘再相加。
4x4阵列乘法器能计算两个4位二进制数的乘积,输出8位结果。比如计算1101(13) x 1011(11),正确结果应该是10001111(143)。这种结构最大的优点是规整性好,所有加法操作可以并行进行,速度比串行乘法器快得多。我在Xilinx Artix-7开发板上实测过,4x4阵列乘法器能在10ns内完成计算。
2. Vivado开发环境准备
工欲善其事,必先利其器。建议直接安装Vivado 2022.2版本,这个版本对初学者最友好。安装时记得勾选WebPACK版本(免费)和Artix-7器件支持。我第一次装漏了器件支持包,结果新建工程时找不到目标器件,白白折腾半天。
新建工程时要注意几个关键设置:
- 工程类型选择RTL项目
- 添加源文件时先不填,我们后面手写代码
- 目标器件选xc7a35ticsg324-1L(对应常用的Basys3开发板)
- 在Simulation设置页勾选XSim仿真器
有个小技巧:建议把"Project is an extensible Vitis platform"的勾去掉,这个选项对我们纯FPGA设计没用。创建完工程后,先别急着写代码,我习惯先建好目录结构:
project/ ├── sources/ ├── sim/ └── constraints/3. 模块化设计思路
看过原理图就知道,阵列乘法器最适合模块化设计。我的方案是把电路拆分成5个模块:
- 顶层模块(zhenliechengfa)
- 第一列专用模块(lie1)
- 通用列模块(lie234)
- 超前进位加法器(chaoqian3)
- 全加器基础模块(fa)
这种划分方式有个好处:如果要改成8x8乘法器,只需要增加lie234的实例数量,其他模块完全复用。我在项目里测试过,用模块化设计比写一个超大verilog文件要节省30%的开发时间。
重点说下lie234这个通用列模块。它包含3个全加器,负责处理中间各列的计算。通过参数化设计,我把进位输入(cin)、上层结果(u,aa)都做成端口,这样不同列的连接关系就非常清晰。调试时发现个小技巧:给每个端口加上[3:0]这样的位宽声明,能避免很多隐式类型转换的坑。
4. Verilog代码实现细节
先看最简单的全加器模块(fa),这是整个设计的基石:
module fa( input a, b, cin, output sum, cout ); wire S1, T1, T2, T3; xor x1 (S1, a, b); // 半加器第一步 xor x2 (sum, S1, cin); // 半加器第二步 and A1 (T3, a, b); // 进位生成 and A2 (T2, b, cin); and A3 (T1, a, cin); or O1 (cout, T1, T2, T3); // 进位合并 endmodule超前进位加法器是提速的关键,我参考了经典的三级超前进位结构:
module chaoqianjinwei( input [2:0]x, [2:0]y, c0, output [2:0]c ); wire [2:0]G, P; assign P = x | y; // 传播信号 assign G = x & y; // 生成信号 // 超前进位逻辑 assign c[0] = G[0] | (c0&P[0]); assign c[1] = G[1] | (P[1]&G[0]) | (P[1]&P[0]&c0); assign c[2] = G[2] | (P[2]&G[1]) | (P[2]&P[1]&G[0]) | (P[2]&P[1]&P[0]&c0); endmodule有个容易出错的地方:超前进位模块的输出进位c[2]要接到顶层模块的z[7],这个连接关系在原理图上不太明显,我当初就接错过导致结果高位总是差1。
5. 仿真验证技巧
仿真文件我建议这样写:
`timescale 1ns / 1ps module tb_zhenlie(); reg [3:0] x, y; wire [7:0] z; // 实例化被测模块 zhenliechengfa uut (.x(x), .y(y), .z(z)); initial begin // 边界值测试 x=4'b0000; y=4'b0000; #10; x=4'b1111; y=4'b1111; #10; // 随机测试 for(int i=0; i<20; i++) begin x=$random; y=$random; #10; $display("x=%b, y=%b, z=%b", x, y, z); end // 特殊用例 x=4'b1010; y=4'b0101; #10; $finish; end endmodule仿真时我发现一个常见问题:当输入变化太快时,组合逻辑的毛刺会导致输出短暂异常。解决方法是在测试用例之间留够时间间隔(比如#10)。另外建议在波形窗口把x和y设置为无符号十进制显示,这样检查结果更直观。
6. 实际调试经验
第一次烧写到开发板时,我的结果总是比预期少1。后来用SignalTap抓波形才发现,是超前进位加法器的c0端口没接低电平。这个教训让我明白:所有输入端口必须显式初始化,不能依赖默认值。
资源占用情况大概是这样:
- LUT: 58个
- 寄存器: 32个
- 最大时钟频率: 125MHz
如果资源紧张,可以考虑两点优化:
- 把全加器的xor/and门实现改成更节省资源的写法
- 超前进位加法器改用两级结构
7. 性能优化方向
想让乘法器跑得更快?可以试试这些方法:
- 流水线设计:在每列之间插入寄存器
- 改用Booth编码:减少加法器数量
- 优化进位链:使用更高效的超前进位结构
我在Artix-7上实测过,加入两级流水后频率能从125MHz提升到200MHz,代价是延迟从1周期变成3周期。具体选择哪种优化,得看你的应用场景是要求低延迟还是高吞吐。
8. 常见问题排查
Q: 仿真结果全是x? A: 检查所有wire是否都有驱动,特别是进位信号
Q: 输出高位总是0? A: 很可能是超前进位加法器的输出没接对
Q: 时序仿真失败? A: 尝试降低时钟频率或插入寄存器
有个小工具特别有用:Vivado的Schematic Viewer。当代码行为不符合预期时,用它查看综合后的电路图,能快速定位连接错误。我后来养成了习惯:每写完一个模块,先用这个工具检查一遍电路结构。
9. 扩展思考
虽然我们做的是4x4乘法器,但这个架构很容易扩展。比如要改成8x8:
- 增加lie234模块的实例数量
- 扩展超前进位加法器的位宽
- 调整顶层模块的端口定义
另一个改进方向是支持有符号数乘法。只需要:
- 在输入处增加符号位处理
- 最后对结果进行符号校正
- 注意溢出情况的处理
记得保存这个项目模板,以后做其他算术运算电路时,很多模块都可以直接复用。我后来做FIR滤波器时,就直接用了这里的乘法器结构,节省了大量开发时间。