FPGA的设计艺术(26)数据处理的误区之有符号运算的若干问题

李锐博恩 2021-05-15 23:39:10 4919
前言

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

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

有符号算术运算

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

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

assign z = a * b;
// -> signed 8x8=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 8x8=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 16x16=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({1'b0, a}) * b; 
// -> signed multiply 

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

input  signed  [7:0] a;
output signed [11:0] z;

// constant is unsigned 
assign z = a * 4'b1011;
// -> unsigned multiply

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

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

input  signed  [7:0] a; 
output signed [15:0] z1, z2; 

// cast constant into signed 
assign z1 = a * $signed(4'b1011); 
// mark constant as signed 
assign z2 = a * 4'sb1011; 
// -> 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); 
// -> 4x8=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); 
// -> 4x8=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;  // -> 8x8=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 8x8=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 8x8=8 bit multiplies, bits z[31:16] are 0 

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

z[31:16]竟然为0;

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

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

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 92 5 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
李锐博恩
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区