简介:一份聚焦低功耗常系数乘法器设计的PDF技术文献,适合数字集成电路设计人员、数字信号处理相关研发者及电子工程专业学生阅读。内容围绕DCT/IDCT变换等场景中乘法器的低功耗低面积需求,详细介绍了基于CSD编码与Wallace Tree乘法算法的新型设计,并结合截断处理、变数校正等优化技术,给出了SMIC 0.18μm工艺下面积139742μm²、100MHz时钟频率功耗0.69mW的综合结果,同时与DA算法、改进型BOOTH算法实现方案进行了比较分析,便于读者理解不同乘法器结构的性能与资源取舍。资源共1个文件,为PDF格式,压缩包大小149KB,内容精炼但涵盖原理、算法、设计与结论。已有118人学习,可作为一种高效的技术参考,帮助快速掌握低功耗常系数乘法器的关键实现路径。
1. 为什么低功耗常系数乘法器绕不开 CSD 与 Wallace Tree
DCT/IDCT 这类变换电路里,乘法器是最耗硬件也最耗电的部件之一。8×8 的 DCT 做行列分解后,核心运算就是输入数据乘上 8 个固定余弦系数,系数在流片前就定死了。如果用通用乘法器去实现,等于把每个周期都必须做动态 Booth 编码的硬件开销背在身上,这在低功耗 ASIC 里相当不划算。复旦大学这篇论文给出了一条非常务实的路径:用 CSD(Canonical Signed Digit)编码把固定系数的非零位压到最少,把乘法转成若干个移位相加,再用 Wallace Tree 把这些部分积并行压缩,最后通过截断处理和变数校正来砍面积与功耗。最终在 SMIC 0.18um 工艺下做到 100MHz、0.69mW,面积 13974um²,延迟 2.42ns。这个数字放在今天看依然有参考价值——它把一个 15bit×15bit 的乘法器功耗做到了 DA 乘法器的六成、改进 Booth 乘法器的四成。对正在做 ASIC 低功耗 IP、数字信号处理器或者 RTL 低功耗优化的工程师来说,这套设计思路可以直接迁移到自己的卷积核、FIR 滤波器和变换核里。
2. CSD 编码:把部分积行数先砍掉一半
2.1 固定系数乘法器的本质是部分积行数
常系数乘法器之所以能做优化,是因为乘数中有一个操作数在综合前就是确定的。15bit 的乘数用二进制补码展开,其中有几个 1,就意味着乘法器里要生成多少行部分积。比如系数 011111011000101,15 位里有 9 个非零位,对应的部分积就有 9 行,这 9 行最终要经过 8 次加法才能收敛出结果。部分积行数直接决定加法器的数量、走线密度和翻转功耗。CSD 编码的意义就在于:把二进制补码表示转换成“1 / -1 / 0”的三值编码,并且在转换过程中保证任意两个非零位不相邻,使得非零位个数达到理论最少。一个 n 位的普通二进制数,非零位平均数量级是 n/2 左右,CSD 编码后平均降到 n/3 左右。对 15 位系数来说,通常能把部分积行数从 7~9 行压到 4~6 行。
CSD 编码还有个容易忽略的性质:它的表示是唯一的。同一个数的二进制补码可以有很多种等价写法,但 CSD 只有一种最小非零位形式,这意味着硬件实现时不需要在多个等价编码里做搜索,直接按规范转换即可。以论文表 1 中 8 位数的例子来看,二进制补码 01111101 有 5 个非零位,CSD 表示后是 1000-10-10,只有 3 个非零位,而且相邻位不同时为非零。换成工程语言,部分积从 5 行减到 3 行,加法树少两级,面积和功耗自然下降。
2.2 从二进制补码到 CSD 的转换规则
转换算法从最低位向最高位扫描,规则就三条:
- 当前位为 0,且没有进位:写 0
- 当前位为 1,且下一位为 0,且没有进位:写 1
- 当前位为 1,且下一位也为 1,或者有进位:当前位写 -1,并产生一个进位到更高位
这个规则等价于把连续的“1”序列做替换。举例来说,4 位二进制 1111 表示 15,转换时从最低位开始连续四个 1,最终得到 1000-1,即 16-1,非零位只有 2 个。8 位二进制 01110110 表示 118,转换后是 1000-10-10,即 128-8-2,非零位 3 个。每一位的权值就是 2 的幂次,硬件上对应一次移位操作:非零位在第 k 位,就把输入左移 k 位,符号由 1 或 -1 决定,所有移位结果累加即完成乘法。
在 DCT/IDCT 的系数场景中,这个收益更加直接。论文表 3 列出了 cos(nπ/16) 的 15 位补码与 CSD 编码对比,节选几个系数如下:
| n | 二进制补码表示 | 非零位个数 | CSD 编码表示 | 非零位个数 |
|---|---|---|---|---|
| 1 | 011111011000101 | 9 | 100000-10-1000101 | 5 |
| 3 | 011010100110111 | 9 | 10-101010100-100-1 | 7 |
| 4 | 010110101000001 | 6 | 10-10-10101000001 | 6 |
| 7 | 000110001111100 | 7 | 0010-10010000-100 | 4 |
从表格能看出,n=1 的系数补码表示有 9 个非零位,CSD 后降到 5 个,部分积行数直接少 4 行;n=7 的系数从 7 个非零位降到 4 个。注意 n=4 这个特例,两种编码非零位个数相同,都是 6,说明 CSD 并非在所有系数上都能带来收益,但对于大多数余弦系数,减少 30%~40% 的部分积行数是常态。
2.3 在 RTL 里如何生成 CSD 部分积
CSD 编码在硬件上并不会真的存一个三值数的 ROM,而是把系数预先转成移位与加减的组合。 下面是一个工程上常见的参数化部分积生成代码:
// 常系数乘法器部分积生成: 系数以 CSD 位串形式给出 // csd[ pos ] = 1 -> 左移 pos 位后加 // csd[ pos ] = -1 -> 左移 pos 位后减 module csd_partial_products #( parameter DATA_W = 15, parameter COEF_W = 15 )( input wire [DATA_W-1:0] din, output wire [DATA_W+COEF_W-1:0] partial_products [0:5] // 最多 6 行 ); localparam [COEF_W-1:0] CSD_COEF = 15'b100000_10_1000101; // 示意值 genvar g; generate for (g = 0; g < COEF_W; g = g + 1) begin : gen_pp if (CSD_COEF[g] == 1) begin assign partial_products[g] = { {COEF_W{1'b0}}, din } << g; end else if (CSD_COEF[g] == 1'b1) begin // 实际工程中需区分 -1 assign partial_products[g] = ~({ {COEF_W{1'b0}}, din } << g) + 1; end else begin assign partial_products[g] = '0; end end endgenerate endmodule这段代码的核心逻辑是:遍历系数 CSD 位串的每一位,遇到 1 就生成一个左移后的部分积,遇到 -1 就生成取反加一后的补码形式,遇到 0 直接给全零行。代码里的partial_products数组每个元素宽度是DATA_W + COEF_W,因为 15bit 乘 15bit 的完整乘积有 29bit。参数DATA_W和COEF_W分别控制输入和系数位宽,实际使用中把 CSD 位宽固定成参数,便于在多个余弦系数之间复用同一个模块。
有一点需要提醒:CSD 中的 -1 位不能用 Verilog 的普通bit select直接表达,实际工程里要么把位串拆成正位掩码和负位掩码两个数组,要么在预处理阶段就把系数展开成一组“移位量 + 符号”的结构体。上面代码只是为了说明映射思路,真正可综合的版本应该用localparam声明正负位掩码,再用generate条件分支生成对应的加法和减法部分积行。
3. Wallace Tree 压缩:把 15 行部分积压成 2 行
3.1 为什么选择 Wallace Tree 而不是串行加法链
部分积生成之后,常规做法是像竖式乘法那样逐行累加,但每加一行都要等前一行出结果,关键路径是线性的,15 行部分积就需要十几级的加法延迟。Wallace Tree 的思路是改变相加顺序:不再让部分积逐行串行相加,而是把每一列上相同权重的 bit 独立出来,用全加器(3:2 压缩器)和半加器(2:2 压缩器)并行压缩。全加器输入 3 个同权重的 bit,输出 1 个本位和 1 个高一位进位,相当于把这一列的 bit 数从 3 降到 2;多轮压缩后,所有列的高度最终降到 2 行,最后再用一个超前进位加法器(CLA)一次性把这两行加起来。论文图 4 里画的就是这个结构:部分积矩阵先经过若干层全加器和半加器,最后一列方块是超前进位加法器。
Wallace Tree 的压缩层数与部分积行数呈对数关系。15 行部分积的压缩级数大约在 6 级左右,而串行累加至少需要 14 级加法,关键路径缩短了一半以上。论文里给的是 2.42ns 的延迟,相比改进 Booth 乘法器的 2.86ns 有约 15% 的优势,就是这个并行压缩带来的。
3.2 压缩过程的列高变化
以 15×15 乘法器为例,部分积矩阵有 15 行、29 列,初始列高分布是中间高两边低。使用 3:2 压缩器迭代后,各轮压缩的最大列高变化如下:
| 压缩层级 | 操作 | 最大列高 |
|---|---|---|
| 第 0 级 | 初始部分积矩阵 | 15 |
| 第 1 级 | 每列 3 个 bit 一组做全加器压缩 | 10 |
| 第 2 级 | 继续按列 3:2 压缩 | 7 |
| 第 3 级 | 继续压缩 | 5 |
| 第 4 级 | 部分列用半加器处理 | 4 |
| 第 5 级 | 最终压缩到两行 | 2 |
这里每一级的“3:2 压缩”指的是:某一列如果有 3 个 bit,送入一个全加器,产生 1 个本位和(留在本列)和 1 个进位(移到高一位列)。压缩后该列 bit 数减少 1,但高一位列会增加若干 bit,所以列高分布会动态变化,实际实现时需要通过列高统计来安排全加器的位置。
Wallace Tree 还有一个常被忽略的好处:进位不再需要沿着整条加法链逐位传递,而是在压缩树内部局部消化,只有最后一级 CLA 才需要真正处理进位传播。 这对降低动态功耗很有帮助,因为进位链上的毛刺和重复翻转被限制在最后几级,中间级只做局部压缩。
3.3 一个可综合的压缩框架
Wallace Tree 的 RTL 实现可以抽象成“按列分组、逐级调用 3:2 压缩”的循环结构:
// 3:2 压缩器: 输入三个同权重 bit, 输出 sum 和 carry module compress3_2 ( input wire a, b, c, output wire sum, carry ); assign sum = a ^ b ^ c; assign carry = (a & b) | (b & c) | (a & c); endmodule // Wallace Tree 压缩主逻辑 (示意) module wallace_compress #( parameter ROWS = 6, // 部分积行数 parameter WIDTH = 29 // 部分积列宽 ) ( input wire [WIDTH-1:0] pp [0:ROWS-1], output wire [WIDTH-1:0] sum_row, carry_row ); // level[0] 保存初始部分积 // 逐级调用 compress3_2 对每列做分组压缩 // 压缩过程中 sum 留在本列, carry 送到高一位列 // 直到所有列的高度 <= 2, 输出 sum_row 和 carry_row endmodule这段代码只是一个结构框架,真正的实现难点在列管理的细节:每一列的高度是动态的,压缩后高一位列会接收来自低位列的进位,因此需要维护一个“列高度数组”,每一轮都统计当前每列有多少个 bit,再按 3 个一组分配全加器、2 个一组分配半加器。实际做 RTL 时,我一般用generate块加function来生成压缩树,而不是手动例化每一级的全加器,否则 29 列的矩阵结构维护起来很容易出错。
ROWS参数在 CSD 优化后通常取 4 到 7,对应不同余弦系数的非零位个数。这里有个工程选择:如果为 8 个 DCT 系数分别做一套 Wallace Tree,面积开销太大,更常见的做法是让部分积行数最少的系数决定压缩树的规模,行数多的系数做流水共享,或者干脆把行数统一扩展到位宽上限,让所有系数复用同一棵压缩树。
3.4 CSD 与 Wallace Tree 的组合逻辑
CSD 减行、Wallace Tree 加速,这两者并不是独立的设计决策。Booth 编码也能减行,但它处理的是动态乘法的场景,乘数每个周期都在变,编码逻辑本身要消耗额外的面积和功耗。常系数场景下乘数固定,CSD 是在综合前就完成了编码,硬件上完全没有 Booth 编码器的开销。论文里把改进 Booth 乘法器的功耗做到 1.59mW,而 CSD+Wallace Tree 的功耗是 0.69mW,差异主要来自这里:Booth 编码器在每次乘法时都要实时运算,CSD 编码器则是一组固定的移位导线。
4. 截断处理与变数校正:误差与功耗的工程权衡
4.1 全精度输出在 DCT 场景下是浪费
15bit 输入乘以 15bit 系数,完整乘积是 29bit。但 DCT/IDCT 的中间结果并不需要 29bit 的全精度,最终输出只要 15bit(Q3 格式)。如果把 29bit 全部算出来再截断,等于为不需要的低位做了大量无用加法。这就带来截断乘法器的基本思路:直接把部分积矩阵的低列砍掉,只保留对最终输出高位有影响的列。论文图 2 展示的就是这个结构:右侧浅色区域是截断掉的低列部分积,左侧深色区域保留参与 Wallace Tree 压缩。
这样做的问题在于截断会引入系统性误差。低列部分积虽然权值小,但数量多,累积起来对高位的贡献不可忽略。如果完全不补偿,乘法器输出会存在明显的负向偏差。
4.2 常数校正与变数校正的数学对比
截断误差的补偿有两种主流方案。常数校正的思路是:假设被截断的每一列中逻辑 1 出现的概率是 1/2,那么截断误差的期望值可以算出来,在结果上加上一个固定常数 C 来抵消。补偿向量 C 的计算公式在论文中写为:
C = (2n + 2k - 1) / 2^(k+1)
其中 k 是截断的最低有效列到最高有效列的跨度,n 是输入位宽。这个公式的物理含义是:把被截断列的所有 bit 按概率期望求和,得到平均截断误差,再把它的相反数做成补偿常数。
常数校正实现非常简单,RTL 里就是加一个常数,但有两个明显的缺陷。第一,当乘法器输入为 0 时,输出应该为 0,但常数校正会输出一个非零常数 C,这在 DCT 变换中会导致直流分量计算错误。第二,它只能保证平均误差为 0,最大误差和方差都偏大。论文表 2 给出了 16bit×16bit、输出 18bit 截断乘法器的仿真对比:
| 误差指标 | 常数校正 | 变数校正 |
|---|---|---|
| 平均误差 | 1.64 | 0.26 |
| 最大误差 | 5.00 | 2.21 |
| 标准差 | 1.46 | 0.57 |
变数校正在三个误差指标上全面优于常数校正,尤其是最大误差从 5 降到 2.21,标准差从 1.46 降到 0.57。它的原理是:不用固定常数,而是用被截断区域的最高一列(第 n+k-1 列)的部分积作为补偿向量,把这个向量加到相邻的保留列上。这一列本来就要参与压缩,只是被判定为“可丢弃”,现在把它的一部分信息引入高位,相当于把截断误差的符号和大小随输入数据动态调整,所以叫变数校正。
4.3 变数校正在 Wallace Tree 里的实现方式
变数校正的硬件实现比想象中省资源。第 n+k-1 列的部分积有若干个 bit,把这些 bit 按位相加得到的值,恰好能反映出被截断列数据的大致量级。具体做法是:把这一列的部分积作为额外的补偿行,送入 Wallace Tree 的压缩级,与第 n+k-2 列的保留部分积相加。由于 Wallace Tree 本身就需要处理多个输入行,补偿行只是多占一个小列的压缩资源,并不会增加独立的加法器。
工程实现时要注意一个细节:补偿行不能直接和所有保留列一起参与压缩,因为它对某些列的影响是负向的。 正确做法是把补偿行的各位按权值分配到对应的列,并标记为减法项,在最后一级 CLA 之前统一处理符号。论文图 3 的矩阵图中,补偿项被安排在原第 n+k-2 列的位置,正是为了让它和周围列的部分积一起经过全加器压缩,避免额外的符号处理逻辑。
5. 从 RTL 到 SMIC 0.18um 综合:面积功耗实测与复现路径
5.1 功能仿真:从 Modelsim 到现代仿真器
论文使用 Verilog 完成行为描述,并在 Modelsim 5.5 下做了功能仿真。当时的验证流程在今天看来依然有效,只是工具版本可以替换成 VCS 或 QuestaSim。仿真流程的核心是三点:给足随机激励、对比全精度乘法器与截断乘法器的输出、统计误差是否在 DCT 容许范围内。一个最小可用的仿真脚本如下:
vlib work vlog -sv multiplier_15bit.v tb_multiplier.v vsim -novopt work.tb_multiplier add wave -position end sim:/tb_multiplier/* run -all仿真时建议在 testbench 里同时例化一个理想乘法器模型(直接用*运算符)作为参考。每跑一组随机输入,就计算一次截断乘法器与理想乘法器的差值,记录最大误差与均方误差。论文没有给出具体允许的误差阈值,但 DCT 变换的典型需求是峰值信噪比不低于 40dB,对应的截断误差标准差大概在 0.5 到 1 之间。变数校正把标准差压到 0.57,正好落在这个区间内。
5.2 综合约束与功耗分析
逻辑综合使用 DC 或 Genus,工艺库选择 SMIC 0.18um。综合时建议把时钟约束设得比目标频率略紧,论文目标是 100MHz,也就是时钟周期 10ns,实际综合约束可以设到 9.5ns 左右,给布局布线留出余量。一个典型 SDC 约束文件如下:
set CLK_PERIOD 10.0 create_clock -name clk -period $CLK_PERIOD [get_ports clk] set_clock_uncertainty 0.1 [get_clocks clk] set_clock_transition 0.1 [get_clocks clk] set_input_delay 1.0 -max -clock clk [all_inputs] set_output_delay 1.0 -max -clock clk [all_outputs] set_max_area 0 set_max_dynamic_power 0这里的set_clock_uncertainty通常设周期误差和片上偏差的总和,0.18um 工艺下 0.1ns 到 0.3ns 都算合理。set_input_delay表示外部逻辑到乘法器输入的到达时间,设得越紧,综合器对乘法器内部逻辑的时序要求越高,面积和功耗也会相应增加。实际优化时,建议先把输入输出延迟放宽到 2ns 跑一版,看面积功耗是否满足要求,再逐步收紧。
功耗分析需要注意操作条件设置。SMIC 0.18um 库通常提供ss_1.62v_125c和tt_1.8v_25c两种操作条件,前者是慢速高温情况,后者是典型情况。论文报告的 0.69mW 应该是在典型条件下的总功耗。做功耗分析时,还需要提供翻转率(toggle rate)约束,默认 0.2 的整体翻转率对流片后的实际功耗估计会偏乐观,建议按 0.3 到 0.4 反标。
5.3 三种乘法器的实测性能对比
论文表 4 给出了三种乘法器在 SMIC 0.18um 工艺下的综合结果:
| 指标 | DA 乘法器 | 改进 Booth 乘法器 | CSD+Wallace Tree 本设计 |
|---|---|---|---|
| 最高速度 (ns) | 2.40 | 2.86 | 2.42 |
| 面积 (um²) | 16730 | 22549 | 13974 |
| 功耗 (mW@100MHz) | 1.13 | 1.59 | 0.69 |
三列数据放在一起,能看出几个值得注意的位置:DA 乘法器速度最快,2.40ns,但面积比本设计大 20%,功耗高了近一倍,原因是 DA 需要 ROM 存储查表数据和流水寄存器;改进 Booth 乘法器面积最大、速度最慢、功耗最高,因为 Booth 编码器动态工作,且乘数和被乘数都不是常数时无法预优化;CSD+Wallace Tree 的设计在速度与 DA 接近的前提下,把面积和功耗做到了三者最低。功耗方面,本设计比 DA 低 39%,比 Booth 低 57%,这个差距主要来自部分积行数少带来的加法器数量下降,以及截断后低列不再翻转。
有一点需要说明:这是 15bit×15bit、输出 15bit 的特定场景结果,换成 32bit 乘法器,Booth 编码减少部分积行数的优势会更明显,CSD 的截断和校正策略也需要重新评估。 但 DCT/IDCT 这类图像处理场景,位宽和精度要求决定了 CSD+Wallace Tree 的这套组合在区域上有明显优势。
6. 让常系数乘法器更省电的三个进阶技巧
6.1 用 CSE 复用公共子表达式
DCT 的 8 个余弦系数之间存在大量公共移位项。例如 cos(π/16) 和 cos(7π/16) 都包含类似 x>>3、x>>5 这样的子项,与其让每个乘法器各自做一次移位,不如先算出公共子项再分发到各个系数。以式子的形式来看,假设 x 是输入,先计算 x3 = x>>3、x5 = x>>5、s35 = x3 + x5,然后再让每个系数乘法器去组合这几个子项。这样单个系数乘法器的部分积行数还能再降 1 到 2 行。实现时先在系统层面做一次公共子项扫描,列出所有系数的 CSD 位串,把出现次数超过 2 的移位项提取出来,无法提取的再保留在乘法器内部。
6.2 用误差预算脚本确定截断列数 k
截断列数 k 的取值直接决定面积和功耗,但论文中一笔带过。工程上我习惯用脚本做参数扫描,先在所有可能的 k 值下跑一遍数值误差模型,统计最大误差和均方误差,再对照 DCT 系统的 PSNR 预算确定 k。这个流程可以用一段简短的 Python 代码模拟:
import random def truncated_multiplier(x, c, keep_bits=15): full = x * c # 模拟硬件截断: 去掉低扩展位后保留高位 shift = 15 + 15 - keep_bits return full >> shift x_list = [random.randint(-16384, 16383) for _ in range(100000)] c = 16070 # cos(pi/16) 的 Q14 定点表示 err = [x*c - truncated_multiplier(x, c, 15) for x in x_list] mse = sum(e*e for e in err) / len(err) print("MSE =", mse)这段脚本把硬件截断简化成最终结果的移位,没有模拟 Wallace Tree 内部每列的精确截断,但用于在系统设计阶段快速筛选 k 值已经够用。keep_bits=15对应输出位宽,c=16070对应 Q14 格式的 cos(π/16)。注意这只是误差预算验证,真正定 k 还要跑完综合,比较不同 k 值下的面积功耗报告,通常 k 每加 1,面积能省 3% 到 5%,最大误差增大约 0.5 个 LSB。
6.3 操作数隔离与门控时钟
CSD 乘法器的功耗由两个来源组成:部分积生成级的移位翻转,以及 Wallace Tree 内部加法器的动态翻转。当输入数据有效信号拉低时,这两个来源可以通过操作数隔离来截断。做法是把部分积生成级的输入寄存器替换成使能控制的保持寄存器,valid无效时锁存上半周期的旧值,输出阶段保持稳定,Wallace Tree 内就没有多余翻转。另一种做法是在输入为 0 时直接旁路掉压缩树,输出清零,这个逻辑对 CSD 乘法器尤其有效,因为截断设计保证输入为 0 时输出也为 0,只是功耗不会因为输入为 0 自动降下来,必须显式加门控。
时钟树上的功耗同样值得处理。论文的 0.69mW 应该是未加门控时钟的裸功耗,如果在 8 个系数乘法器之间加上时钟门控,让没有数据到达的通道在空闲周期关闭时钟,整体功耗还能再降 20% 到 30%。具体做法是用一个 AND 门把clk_en和clk组合成局部时钟送进乘法器单元,综合库里的CLKGATE单元会自动处理时钟偏斜和毛刺过滤,不需要在 RTL 里手工例化门控逻辑。
本文还有配套的精品资源,点击获取