首页 > 技术知识 > 正文

前言

如果不是做信号处理,我们时常接触到的运算很少,如果有,很多也都是无符号运算,实际上我们没有明确声明为有符号类型的运算都默认为无符号运算。 作为一个逻辑设计工程师,对于Verilog中的无符号运算如何处理,如果不明白的话,也许不会暂时影响你的工作,但一定是有遗憾与缺失的。

本文重点介绍了有符号数的一些操作,给出了正反两种对比,我们推荐的做法一级不推荐的做法,还有一些我们认为没问题的操作,实际上结果却是不符合我们预期的,这都是有符号运算的一些陷阱,一起看看吧。

有符号算术运算

对于有符号运算,使用“signed”类型关键字来定义变量或者使用$signed来强行处理无符号数,强行转换类型; 如下例子:

signed类型 input signed [7:0] a, b; output signed [15:0] z;

assign z = a * b; // -> signed 8×8=16 bit multiply

2. 强制转换类型 “`c input [7:0] a, b; output [15:0] z; wire signed [15:0] z_sgn; assign z_sgn = $signed(a) * $signed(b); assign z = $unsigned(z_sgn); // -> signed 8×8=16 bit multiply

不好的风格,或不推荐的方式是使用无符号数的运算来模拟有符号数运算,例如:

input [7:0] a, b; output [15:0] z; // a, b sign-extended to width of z assign z = {{8{a[7]}}, a[7:0]} * {{8{b[7]}}, b[7:0]}; // -> unsigned 16×16=16 bit multiply

没有使用关键字,默认是无符号数;

注意尽量不要使用“integer”来定义变量类型,除非特殊情况,例如,在for循环中的循环变量,可以使用integer来定义;

integer i; always@(posedge clk or posedge rst) begin for(i = 0;i < N; i = i + 1) begin data_pos[i] <= data_d1[i] & ~data_d2[i]; end end 符号扩展

尽可能不要手动符号扩展。正确的处理方式:

Verilog中的扩展会自动完成,例如:

input signed [7:0] a, b; output signed [8:0] z; // a, b implicitly sign-extended assign z = a + b;

VHDL中可以使用标准的函数来进行扩展:

resize in ieee.numericstd, conv* in ieee.std_logic_arith

例如:

port (a, b : in signed(7 downto 0); z : out signed(8 downto 0)); — a, b explicitly (sign-)extended z <= resize (a, 9) + resize (b, 9); 不要混合有符号与无符号类型

这个小标题的意思是,不要在同一个表达式中混合使用有符号以及无符号类型数据运算;

在Verilog中,如果有一个操作数是无符号数,那么整个表达式都会被认为是无符号数运算;

如下负面例子:

input [7:0] a; input signed [7:0] b; output signed [15:0] z; // expression becomes unsigned assign z = a * b; // -> unsigned multiply

混合的有符号数以及无符号数运算,会被解释为无符号数运算。

正确的做法是要么全部定义为有符号数,要么强制转换: 如下扩展后,强制转换类型:

input [7:0] a; input signed [7:0] b; output signed [15:0] z; // zero-extended, cast to signed (add 0 as sign bit) assign z = $signed({1b0, a}) * b; // -> signed multiply

注意,常数也是无符号数,如下做法也是错误的:

input signed [7:0] a; output signed [11:0] z; // constant is unsigned assign z = a * 4b1011; // -> unsigned multiply

Verilog中会认为它是无符号数乘法。

正确的做法是将常数强制转换为有符号数或者标记常数为有符号数;

input signed [7:0] a; output signed [15:0] z1, z2; // cast constant into signed assign z1 = a * $signed(4b1011); // mark constant as signed assign z2 = a * 4sb1011; // -> signed multiply

在FPGA的编译中可通过查看Warning来检查这种隐含的无符号到有符号或者有符号到无符号的转换。

部分选择以及数据拼接的正确操作

需要注意的两点:

部分选择的结果是无符号的,也就是说,如果定义了一个有符号的向量,对其进行部分选择操作,则结果为无符号数,即使部分选择,选择的是整个向量;

例如:

input signed [7:0] a, b; output signed [15:0] z1, z2; // a[7:0] is unsigned -> zero-extended assign z1 = a[7:0]; // a[6:0] is unsigned -> unsigned multiply assign z2 = a[6:0] * b;

a和b都是有符号数,无论怎么进行部分选择,结果都是无符号数;

正确的操作应该是:

input signed [7:0] a, b; output signed [15:0] z1, z2; // a is signed -> sign-extended assign z1 = a; // cast a[6:0] to signed -> signed multiply assign z2 = $signed(a[6:0]) * b;

强制类型转换是关键。

第二点就是数据拼接操作,例如有符号数a和b的拼接结果也是无符号数;

input signed [7:0] a, b; output signed [15:0] z1, z2; z1是无符号数; assign z1 = {a, b}; 表达式的宽度

对于表达式的宽度,要有明确的表示:避免误用的方式是使用中间信号和额外的赋值来是算术表达式的宽度明确,例如:

input [7:0] a, b; output [8:0] z; assign z = a + b; // expression width is 9 bits input [3:0] a; input [7:0] b; output [9:0] z; assign z = a * b; // expression width is 10 bits

表达式的宽度由左侧的操作数决定,即使右侧运算本身结果很大,最终的宽度也取决于左侧的操作数。 注意扩展与截断;

还比如,如下两个对立的行为:

正面教材:

input signed [3:0] a; input signed [7:0] b; output [11:0] z; wire signed [11:0] z_sgn; // product width is 12 bits assign z_sgn = a * b; assign z = $unsigned(z_sgn); // -> 4×8=12 bit multiply

反面教材:

input signed [3:0] a; input signed [7:0] b; output [11:0] z; // product width is 8 bits (not 12!) assign z = $unsigned(a * b); // -> 4×8=8 bit multiply

正面教材使用了中间变量,并将中间结果变量赋值给最终的变量的做法,可以明确的知道结果的位宽,结果一定是没问题的。 而反面教材,使用括号表达式,将a*b的中间结果直接赋值给输出,实际上,结果是不符合预期的,结果取决于a与b之间的最大位宽数据,也就是8bit。

还比如: 正面教材:

input [7:0] a, b, c, d; output z; wire [8:0] s; wire [15:0] p; assign s = a + b; // -> 8+8=9 bit add assign p = c * d; // -> 8×8=16 bit multiply assign z = s > p; // -> 9>16=1 bit compare

这个例如是9bit的结果与16bit的结果之间的比较操作,结果是1bit;

反面教材:

input [7:0] a, b, c, d; output z; assign z = (a + b) > (c * d);

而,这种操作是8bit的加操作结果与8bit的乘操作结果之间的比较,最终结果虽然也是1bit,但其已经不符合预期了。

还比如: 正面教材:

input [15:0] a, b; output [31:0] z; wire [15:0] zh, zl; assign zh = a[15:8] * b[15:8]; assign zl = a[ 7:0] * b[ 7:0]; assign z = {zh, zl}; // -> two 8×8=16 bit multiplies

反面教材:

input [15:0] a, b; output [31:0] z; assign z = {a[15:8] * b[15:8], a[ 7:0] * b[ 7:0]}; // -> two 8×8=8 bit multiplies, bits z[31:16] are 0

正面教材符合预期; 而你能想到反面教材的结果吗?

z[31:16]竟然为0;

看了这几个反面教材的结果,是不是觉得很惊讶呢?如果怀疑,可以实际仿真验证下来看看,有探索精神总是好的。

最后想说的是,我们要在规则之内去创造设计,这才是好的设计,只是我以为,往往会走很多弯路,且难以发现。及时储备知识,别只在调试中去摸索。

猜你喜欢