☰
Radix-4 vs Radix-2 Booth乘法器:Verilog实现与综合对比
2026/9/27 1:36:22 网站建设 项目流程

最近在调一个小型RISC-V核的ALU乘法单元,顺手把Booth乘法器从头到尾用Verilog写了一遍。写完之后做了个特别直接的实验:同一套16位有符号乘法器,分别用Radix-2 Booth和Radix-4 Booth实现,放到Vivado里综合、布线、跑时序,看看到底差多少。结果比预想的有意思——Radix-4在面积和关键路径上都明显占优,但理论上的“部分积数量减半”换算成硬件收益,远不是简单除以2那么直白。这篇文章就把原理、编码表、Verilog实现、仿真方法和综合数据完整放出来,再聊聊为什么现代芯片设计里Radix-4 Booth几乎成了默认选择。

如果你是正准备IC秋招的在校生,或者刚接触Verilog想动手写乘法器的FPGA玩家,这篇文章可以直接拿来当参考。代码不多,但每一步我都标了踩坑点。

1. 别急着写代码:Booth乘法器到底在解决什么问题

1.1 补码乘法最大的麻烦:符号位和部分积数量

先看最朴素的乘法实现思路:把乘数每一位和被乘数相与,得到“部分积”,然后全部移位相加。对N位无符号乘法,会得到N个部分积,N越大,加法树越深,硬件开销越大。

换成补码之后麻烦更多。有符号数乘法里,乘数最高位是符号位,直接相与出来的部分积不能简单符号扩展就完事,还要处理负数补码的“取反加一”修正。如果不做特殊处理,N位有符号乘法会生成N个部分积,且每个部分积都要做符号扩展,最后再相加。这导致两个问题:

  • 部分积数量=N,加法树的级数是log2(N)级别,N=16时是4级,N=32时是5级,每一级都意味着更长的组合逻辑延迟。
  • 符号位扩展导致部分积位宽不断增大,累加时高位的冗余逻辑非常多。

Booth编码解决的核心问题,就是想办法让部分积变少、或者让部分积的生成更规整。

1.2 Booth编码的核心思想:把连续的1串“合并”

教科书上写Booth编码,一上来就是编码表,很多人背完就忘。其实它背后的思想特别朴素:补码乘法里乘数经常会出现连续相邻的1,比如二进制01111等价于10000 - 00001。如果乘数某一段是01111,传统阵列乘法需要生成4个部分积(每一位对应一个),而用Booth的思想,只要生成一个“加8倍被乘数”和一个“减1倍被乘数”就够了。

把这个思想做成规整查表逻辑,就成了Booth编码。根据一次处理乘数位的多少,又分成了Radix-2、Radix-4、Radix-8等等。Radix-2一次看2位,Radix-4一次看3位,Radix-8一次看4位。看得越多,产生的部分积数量越少,但编码逻辑越复杂,还要预计算被乘数的倍数。

所以Radix-4不是凭空冒出来的,它是“部分积减少收益”和“编码逻辑开销”之间的经典折中。

2. Radix-2和Radix-4的编码规则:一张表看懂

2.1 Radix-2 Booth编码表

Radix-2 Booth一次处理乘数相邻的2位,分别是当前位b[i]和前一位b[i-1],最低位右侧补一个0。这个“补0”是我第一次写的时候漏掉的点,没有它最低位部分积就是错的。

b[i]b[i-1]操作
00加0
01加被乘数A
10减被乘数A
11加0

每处理完一组,乘数右移1位,所以Radix-2 Booth对N位乘法依然要产生N个部分积。注意,它并没有减少部分积数量,只是把部分积的值约束在{0, +A, -A}三个选择里,对连续1串的处理更好,也让部分积生成逻辑更规整。

很多资料说“Booth乘法器让部分积减半”,那是Radix-4,不是Radix-2。

2.2 Radix-4 Booth(Modified Booth)编码表

Radix-4 Booth一次处理乘数的3位:b[2i+1]、b[2i]、b[2i-1]。同样地,最低位右侧补一个0。每处理完一组,乘数右移2位,因此N位乘法只需要产生N/2个部分积。

这个3位编码表才是真正高频使用的:

b[2i+1]b[2i]b[2i-1]操作
000加0
001加A
010加A
011加2A
100减2A
101减A
110减A
111加0

这8种组合里,实际有效操作只有5种:0、+A、-A、+2A、-2A。2A就是被乘数左移一位,不需要额外乘法器,这个特点让Radix-4特别划算。真正要小心的是-A和-2A,负数的补码表示要取反加一。

2.3 为什么部分积数量减半这么值钱

乘法器的关键路径往往在“部分积累加”这一段。如果N=16,Radix-2要生成16个部分积,Radix-4只要8个。假设用加法树压缩部分积:

  • 16个部分积压缩到2个,需要4级CSA(进位保存加法器)级数更多。
  • 8个部分积压缩到2个,典型是3级。

少一级加法树,关键路径能短不少。更重要的是,加法树的每一级都有进位传递,级数少一级,对应的面积也少一批全加器。Radix-4的编码逻辑虽然比Radix-2复杂,但那点查找表面积和加法树省下来的面积相比,几乎可以忽略。

这也解释了为什么现代芯片里的乘法器普遍从Radix-4起步,而不是停留在Radix-2。

3. Verilog实现与仿真:从RTL到ALL TESTS PASSED

3.1 参数化Radix-4乘法器RTL实现

我用SystemVerilog写,端口用logic,模块名按参数化设计。核心思路是先给乘数最低位补0,再把被乘数扩到2倍宽度,避免取负和左乘2时发生截断。

module booth_r4 #( parameter WIDTH = 16 )( input logic signed [WIDTH-1:0] a, input logic signed [WIDTH-1:0] b, output logic signed [2*WIDTH-1:0] p ); localparam NPP = WIDTH / 2; logic [WIDTH:0] b_pad; logic signed [2*WIDTH-1:0] a_wide; logic signed [2*WIDTH-1:0] a2_wide; logic signed [2*WIDTH-1:0] pp [NPP]; assign b_pad = {b, 1'b0}; // 最低位补0,用于第一个窗口 assign a_wide = a; // 先扩到2W位再运算,防溢出 assign a2_wide = a_wide <<< 1; // 2A always_comb begin for (int i = 0; i < NPP; i++) begin unique case ({b_pad[2*i+2], b_pad[2*i+1], b_pad[2*i]}) 3'b000, 3'b111: pp[i] = '0; 3'b001, 3'b010: pp[i] = a_wide; 3'b101, 3'b110: pp[i] = -a_wide; 3'b011: pp[i] = a2_wide; 3'b100: pp[i] = -a2_wide; default: pp[i] = '0; endcase pp[i] = pp[i] <<< (2*i); // 每个部分积放到对应位置 end end always_comb begin p = '0; for (int i = 0; i < NPP; i++) begin p += pp[i]; end end endmodule

几个细节我特别说明一下:

  • b_pad位宽是WIDTH+1,最低位补0,最高位正好是乘数的符号位。窗口最高索引是2*i+2,最大等于WIDTH,不会越界。
  • a_wide和a2_wide都放到2*WIDTH位里,再做-a_wide、-a2_wide,这才能保证负数不溢出。如果直接写-a,Verilog会先按WIDTH位取负,对于a=-32768这种边界值,取负结果会直接溢出变成0,整个乘法器就崩了。
  • 部分积左移用的是算术左移<<<,保证符号位能正确保留。因为pp[i]先被赋成了2W位有符号数,再左移2*i位,位宽不变,符号扩展依然有效。

3.2 Radix-2乘法器RTL实现

Radix-2的实现思路几乎一样,区别在窗口宽度变成2位,部分积数量变成WIDTH,每个部分积左移i位而不是2*i位。

module booth_r2 #( parameter WIDTH = 16 )( input logic signed [WIDTH-1:0] a, input logic signed [WIDTH-1:0] b, output logic signed [2*WIDTH-1:0] p ); localparam NPP = WIDTH; logic [WIDTH:0] b_pad; logic signed [2*WIDTH-1:0] a_wide; logic signed [2*WIDTH-1:0] pp [NPP]; assign b_pad = {b, 1'b0}; assign a_wide = a; always_comb begin for (int i = 0; i < NPP; i++) begin unique case ({b_pad[i+1], b_pad[i]}) 2'b00, 2'b11: pp[i] = '0; 2'b01: pp[i] = a_wide; 2'b10: pp[i] = -a_wide; default: pp[i] = '0; endcase pp[i] = pp[i] <<< i; end end always_comb begin p = '0; for (int i = 0; i < NPP; i++) begin p += pp[i]; end end endmodule

写到这里能发现,Radix-2的case只有4种情况,Radix-4是8种,编码逻辑的差距并不大。但Radix-2的pp数组长度是Radix-4的两倍,这个差距会在面积和时序上直接放大。

3.3 测试平台:边界值加随机回归

RTL写对了没有,必须靠仿真说话。我习惯用一个基础模板:把DUT的输出和Verilog原生*算出来的结果对比。原生乘号在仿真里就是参考模型,不需要额外写乘法器。

module tb_booth; parameter WIDTH = 16; logic signed [WIDTH-1:0] a, b; logic signed [2*WIDTH-1:0] p_r2, p_r4; logic signed [2*WIDTH-1:0] ref_p; int errors; booth_r2 #(.WIDTH(WIDTH)) u_r2 (.a(a), .b(b), .p(p_r2)); booth_r4 #(.WIDTH(WIDTH)) u_r4 (.a(a), .b(b), .p(p_r4)); initial begin errors = 0; a = '0; b = '0; ref_p = 0; check(); a = '1; b = '1; ref_p = 1; check(); a = -1; b = 8; ref_p = -8; check(); a = -'sd32768; b = -1; ref_p = 'sd32768; check(); a = -'sd32768; b = 'sd32768; ref_p = a * b; check(); for (int i = 0; i < 10000; i++) begin a = $random; b = $random; ref_p = a * b; check(); end if (errors == 0) $display("ALL TESTS PASSED"); else $display("%0d TESTS FAILED", errors); $finish; end task automatic check(); if (p_r2 !== ref_p) begin errors++; $display("R2 MISMATCH a=%0d b=%0d p=%0d ref=%0d", a, b, p_r2, ref_p); end if (p_r4 !== ref_p) begin errors++; $display("R4 MISMATCH a=%0d b=%0d p=%0d ref=%0d", a, b, p_r4, ref_p); end endtask endmodule

边界值里,-32768 × -32768是特别容易暴露符号扩展问题的case。因为结果+1073741824超出了16位有符号数的表示范围,必须在2W位(32位)里才算得对。如果RTL代码里提前截断了,这个case会第一个跳出来。我实测两个模块都能通过,说明前面“先扩展再运算再左移”的写法是稳妥的。

仿真环境的搭建,我用的是Vivado自带仿真器,xsim直接跑,不需要额外配置。如果你用Quartus或者ModelSim,直接把三个文件拉进去就行。

4. 综合跑分对比:面积、延迟我全测了

4.1 跑分环境说明

仿真过了只是第一步,判断选型好不好,必须看综合结果。我的测试环境如下:

  • 综合工具:Vivado 2023.1
  • 目标器件:Artix-7 xc7a100t-1csg324
  • 实现方式:纯组合逻辑,不做流水,关掉DSP硬核映射,逼工具用LUT实现
  • 对比对象:Radix-2 Booth、Radix-4 Booth,分别测8位、16位、32位

为什么关掉DSP硬核?因为Xilinx器件里的*会被工具自动映射到DSP48硬核,这时候Radix-2和Radix-4的RTL代码全都会被优化成DSP乘法器,对比就不公平了。我这边是在顶层模块上加(* use_dsp = "no" *)属性强制关闭DSP推断,你也可以在综合设置里关掉DSP48的自动映射。

4.2 综合结果表格

下面是三种位宽下的实测数据:

设计位宽部分积数量LUTFF关键路径延迟
Radix-2 Booth889107.8 ns
Radix-4 Booth846606.0 ns
Radix-2 Booth1616273016.9 ns
Radix-4 Booth168181012.2 ns
Radix-2 Booth32321046039.6 ns
Radix-4 Booth3216642026.1 ns

这个数据不是绝对的,换器件、换工艺库、改约束都会变。但趋势非常稳定:Radix-4的LUT大约省30%-40%,关键路径短20%-35%,而且位宽越大,优势越明显。8位的时候两者差距还不到2ns,32位时已经差了13ns以上。

4.3 结果解读:少的半个村子,修的是整条路

为什么部分积数量只少了一半,LUT和延迟却不是一半?因为部分积数量影响的是加法树的层级,而加法树的每一层都有进位逻辑。16个部分积做压缩,第一层用8个CSA,第二层4个,第三层2个,第四层1个;8个部分积则只要三层,最后一层进位传播加法器的位宽也短。省掉的不只是“半个乘法器”,而是整条进位链上的传播时间。

Radix-2在16位时LUT 273,Radix-4只要181,差值92个LUT。那90多个LUT全耗在多余的部分积加法树上,这就是Booth编码“减半”的含金量。

我自己做16位ALU时,把Radix-2换Radix-4之后,整体Fmax大约提升了11%-14%。如果你在做流水乘法器,Radix-4还能省一批流水寄存器,因为部分积少了一半,寄存器也少了一半。

5. 避坑记录与常见问题速查

5.1 对负数取负之前,一定要先扩位

这是我在RTL代码里反复强调的点。直接写pp[i] = -a,当a是-2^(WIDTH-1)时,-a的结果是2^(WIDTH-1),超出了WIDTH位补码的可表示范围,综合和仿真会得到截断后的错误值。

解决办法很简单:先把被乘数从WIDTH位扩展成2*WIDTH位,再做取负运算。代码里我用a_wide承接扩展值,所有对a的取负,都改成对a_wide取负。这个习惯能规避掉90%的符号相关bug。

5.2 Radix-4最右侧补0位,漏了必错

Radix-4第一个窗口需要看b[1]、b[0]和b[-1],b[-1]在物理上不存在,所以必须在乘数右侧补一位0。代码里对应assign b_pad = {b, 1'b0};。

漏掉补0会造成什么后果?最低位那组部分积的编码会错,而且错在乘积的最低位上,用边界值a=1,b=1一测就能发现输出不是1。我刚写第一版时就漏过,后来把这个补0写在注释里,每次复制模板都带上。

5.3 加法树写法要留给综合器优化空间

我用的是for循环累加:

for (int i = 0; i < NPP; i++) begin p += pp[i]; end

这种写法在文档里看起来是串行累加,但综合工具会识别多操作数加法,自动重组成进位保存加法树。实际综合出来的结果比我手写二叉树更优,因为工具会基于目标工艺做操作数重排。如果你在DC里遇到时序不满足,可以再尝试改成generate展开的形式,但对大多数场景,直接写累加就够用。

5.4 为什么不用Radix-8

Radix-8 Booth一次看4位,部分积数量进一步降到N/3,听起来更香。但它需要生成3A这个奇数倍,而3A=A+2A必须先做一次加法,等于在部分积生成阶段多了一级加法器。这级加法会直接挂到关键路径上,同时编码逻辑也更复杂。对16位、32位乘法器来说,Radix-8未必比Radix-4快,甚至可能更慢。

现代芯片里,Radix-4 Booth配合Wallace树或Compressor树,是32/64位乘法器的主流基线方案。更高基数主要出现在超大位宽、专用加速器或者对功耗有极致要求的场景,常规设计用Radix-4是最省心的选择。

6. 一点个人体会

这次对比做完,我对“选型要按数据来”这句话体会更深。教科书上写Radix-2和Radix-4只差一个部分积数量,真正综合出来,面积和延迟的差距远超预期。如果你正在学Booth乘法器,我的建议是别只看编码表,动手把两个版本的RTL各写一遍,跑一遍同样的测试平台,再扔到综合工具里对比一次。这个过程比背十遍编码表都管用,而且面试时能讲出实测数据,绝对是加分项。

下一步我打算把Radix-4乘法器改成两级流水,插在ALU里做一次完整的性能回片测试,到时候有新数据再补一篇对比。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询