无符号乘法器
与无符号加法类似,无符号乘法器也要求两边的乘数是无符号的,一旦有一方为有符号数,则整个结果为有符号数,否则综合会出现不可预知的结果。与无符号加法不同的是,无符号的乘法,乘积结果位宽为两个乘数位宽相加,而非乘数最大位宽+1,其实从原理上是比较容易理解的,因为二进制乘法,就是几组二进制加法移位的结果,例如:
1101 4位
* 110 3位
---------------------------------
0000
+ 1101
+ 1101
----------------------------------
1001110 7位
乘法进行Verilog 编写,以前综合工具不是很优化,不能解析*,一般采用例会标准单元的方式,完成乘法运算:
传统古老方式Verilog 无符号乘法写法:
localparam A_WIDTH;
localparam B_WIDTH;
localparam PRDCT_WIDTH = A_WIDTH + B_WIDTH;
reg [A_WIDTH-1:0] a; // Default declaration type is unsigned
reg [B_WIDTH-1:0] b; // Default declaration type is unsigned
wire [PRDCT_WIDTH-1:0] prdct;
DW02_MULT #(
.A_WIDTH (A_WIDTH ),
.B_WIDTH (B_WIDTH )
)
U_DW_MULT
(
.TC (1'b0 ), // 0 for unsigned, 1 for signed
.A (a ),
.B (b ),
.PRODUCT (prdct )
);
随着工具不断优化,包括Synplify也被synopsys收购后,FPGA综合工具也支持*乘法识别,只需要代码中申明乘法参数的符号属性既可。
推荐乘法运算Verilog 代码:
localparam A_WIDTH = 8;
localparam B_WIDTH = 16;
localparam PRDCT_WIDTH = A_WIDTH + B_WIDTH;
reg unsigned [A_WIDTH-1:0] a; // Default declaration type is unsigned
reg unsigned [B_WIDTH-1:0] b; // Default declaration type is unsigned
reg unsigned [PRDCT_WIDTH-1:0] prdct;
always@(*) begin
prdct = a * b;
end
乘法不用显示把a和b位宽扩位到A_WIDTH+W_WIDTH,只要prdct 定义位宽为A_WIDTH+B_WIDTH,工具就不会报错。
以上讲解的是乘法器两边都是变量信号的无符号乘法运算,对于一个变量,一个常量的无符号运算,需要注意一下几点:
1. 常数的位宽要定义清楚;
2. 常数的符号类型要显示定义为无符号;
3. 对于常数无论是是否2的整数次幂,均按照* 写,不需要自己优化移位,因为综合的优化效果,不会比手动移位差。
示例:
localparam A_WIDTH = 8;
localparam B_WIDTH = 8;
localparam unsigned [B_WIDTH -1 : 0] B = 32;
localparam PRDCT_WIDTH = A_WIDTH + B_WIDTH;
reg unsigned [A_WIDTH-1:0] a; // Default declaration type is unsigned
reg unsigned [PRDCT_WIDTH-1:0] prdct;
always@(*) begin
prdct = a * B;
end
强烈不推荐:
always@(*) begin
prdct = a << 5;
end
原因: 1. 代码可扩展性上讲,后续常数B的值变化不是2的5次方,或者说不是2的整数次幂,这个地方就需要修改为*
2. 代码可读性上讲,推荐的方式容易看懂,就是两个数相乘,不推荐的方式,还需要推敲一下,这行代码功能
3. 代码可控性上讲,a往作移位,低位补0,还是补1,还是补a的最低位,工具都可以有不同理解,所以不同工具可能理解会不一样
4. 两边位宽还不匹配,语法检查工具也会报Warning
顺便讲解一下这个章节代码规范一些细节:
1. 信号定义和申明,一行对应一个信号,不要多个信号定义在一行,否则修改其中一个信号,可能会影响其他信号,另外一行太长,也影响阅读,不建议定义方式:
localparam A_WIDTH = 8,B_WIDTH = 16;
2. 对于模块例化,建议按照名字进行例化,不要按照位置进行例化,否则被例化模块端口有修改,例化的上层文件就要重新修改,即不建议这样的例化代码风格:
DW_MULT #(
A_WIDTH ,
B_WIDTH
)
U_DW_MULT
(
1'b0 ,
a ,
b ,
prdct
);
甚至很多教科书上的这种写法,可维护性更差,就更不推荐了哈:
DW_MULT #( A_WIDTH , B_WIDTH ) U_DW_MULT(1'b0,a,b,prdct);
原因很简答,如果a和b位置搞反了,a和b的位宽又不一样,就可能会报错,能够报错都算是不坏结果,就怕语法检查不跑错,最后仿真出错,定位问题会浪费较长时间。
3. 注意一下语法,例化赋值,或者用assign赋值的信号,我们定义为wire,在 always 块中的变量,无论是组合逻辑还是时序逻辑,都需要定义为reg。比如上面例子当中的prdct,第一个写法是通过例化模块得到值,所以定义为wire,第二个写法是在always块中得到,所以定义为reg。这个就是语法规定,没有什么理由,大家记住就行,否则工具就会报语法错误。
4. 在always 块中,组合逻辑采用非阻塞赋值 =, 时序逻辑采用阻塞赋值 <= ,具体原因,这里先不表述,前面章节主要讲组合逻辑,等讲到时序逻辑章节,会详细阐述原因,大家先有这个一个印象即可。
5.常数乘法给大家引申的一个写代码原则,尽量按照功能或者代码行为去写,只要是可综合风格即可,切忌自己觉得自己很聪明,对电路进行电路级的优化。这样会影响代码的可读性,扩展性以及可控性,同时现在综合工具优化功能很强,大家不必担心得不到最好的PPA(Power,Performance,Area),而且大家进行代码风格选择时,考虑的也不光是PPA这几个维度,也要从可以实现性,复杂度,可阅读星,开发周期多方面去考量。