赞
踩
主要内容是对有符号数和无符号数在设计时,数据是怎样传递的,符号位是怎样来的,以及相关的几种运算设计应当遵循怎样的想法。
最近对加减乘除运算很困惑,主要是对于有符号数的运算的困扰,如果运算出现负数怎么办。对于数据的赋值应该是怎样的,里面的数据是怎样存储的。
找来找去不如直接看Verilog标准verilog 2001里面写的很清楚。
具体的部分是在4.1.5小节中讲述的。
几种运算符的使用如下:
运算 | 功能 |
---|---|
a + b | a加b |
a - b | a减b |
a * b | a乘以b |
a / b | a除以b |
a % b | a除以b的余数 |
a ** b | a的b次方(在设计时,b只能是常量) |
说明:
1、 整数除法将截断任何小数部分,也就是向下取整。对于除法运算符(/)或模数运算符(%),如果第二个操作数为零,则整个结果值应为x。模数运算符,例如y % z,当第一个操作数被第二个操作数除时给出余数,因此当z正好除y时为零。模数运算的结果应取第一个操作数的符号。
2、幂运算符(**)的第一个和第二个操作数以及其赋值结果,应当保证,全为有符号数或全为无符号数。如果第一个操作数为零且第二个操作数为非正,或者第一个操作数为负且第二个操作数不是整数值,则幂运算符的结果是未指定的。
对于取模运算符,其各种情况的运算结果如下:
运算 | 取余 |
---|---|
3%2 | 1 |
-3%2 | -1 |
2%2 | 0 |
-3’d3%2 | 1(-3‘d3被视为一个大的正数,除以2时余下1) |
在理解有符号和无符号数的运算之前,我觉得至少要明白我们在设计中,寄存器都是怎样进行赋值的,数据是怎样进行传递的,只有知道了其存储数据和传递数据的方式了,才能正确使用Verilog代码进行设计有符号数和无符号数的运算。
首先看其定义
Data type | Interpretation |
---|---|
unsigned net | 无符号数 |
signed net | 有符号数 (按二进制补码存储) |
unsigned reg | 无符号数 |
signed reg | 有符号数 (按二进制补码存储) |
integer | 有符号数 (按二进制补码存储)(可以理解为C语言的int型数据) |
time | 无符号数 |
real ,realtime | 有符号数, 浮点类型 |
有符号数一般定义就是在数据类型前面加上signed,如果不加就是默认成无符号数。
如果用常量直接对数据进行赋值,如下代码方式:
reg [3:0] regA; //无符号
//reg signed [3:0] regA; //有符号定义
always@(posedge clk)
begin
regA <= 4'd10; //正数赋值
//regA <= 4'd10; //负数赋值
end
结果如上图,如果直接赋值的话,正数是直接按照被赋值寄存器的位宽进行赋值的,而负数,无论是定义的有符号还是无符号寄存器
,都是赋值其补码(不包括其符号位)(剩余的按符号位补)。如下图:
这里写一个被赋值寄存器位宽超过赋值的常量位宽的代码进行验证:
reg [5:0] a;
initial
begin
a = 4'd10; //1
#4
a = -4'd10;
end
再写一个被赋值的寄存器位宽小于常量位宽的情况:
reg [2:0] a;
initial
begin
a = 4'd10;
#4
a = -4'd10;
end
仿真显示,跟上面分析的完全一致,所以被赋值的寄存器无论是否有符号定义,其赋值结果都是一样的。
这里需要注意一点,我们在用常量对寄存器进行赋值的时候,代码设计要写成十进制(或者16进制以及8进制)的形式才会按照这种高位补符号位,如果写成二进制,而二进制数据是默认为无符号数的,因此高位是直接补0的(无论赋给有符号数还是无符号数)。因此在设计运算电路的时候,尽量不使用二进制代码
这里在学习的过程中发现了一个常数表示方法:
reg [3:0] regA //这里设置为4位,最高位作为符号位
always@(posedge clk)
begin
regA <= 3'sd5;
end
sd
表示有符号十进制的意思,也就是说把上面5
转换成二进制为101
,其第一位为符号位,因此存入regA
的数据是101再加上其符号位1101
。如果表达式写成这样
regA <= -3'sd5;
应当这样进行理解,-5的二进制补码为011
则其最高位为0
所以最后存入寄存器regA
的是0011
。
上一节已经分析了常量赋值到寄存器,因此下面的理解应该就很轻松了,可以提前思考一下,如果是无符号寄存器传数据,应该是位宽不够时,向高位补0,如果是有符号寄存器传数据,此时应该就是跟常量赋值类似,位宽不够,向高位补符号位。下面进行验证。
注:
上面已经说了被赋值的寄存器的定义是否为signed,其赋值的方案都是一样的,因此下面验证时,被赋值的寄存器均设置为无符号了。
reg [3:0] a; reg [6:0] b; //验证高位补什么数据 reg [1:0] c; //验证位宽较低时怎样截数据 always@(posedge clk) begin b <= a; c <= a; end initial begin clk = 1; #4 a= 4'b1010; #4 a= 4'b0101; #20 $stop; end
从仿真的二进制结果来看,将a定义成无符号数的时候,如果被赋值寄存器的位宽较大,则向高位补0,较小,则直接截取对应低位。与前面的想法一样。
这里只将上面的a和b定义成有符号数即可。
reg signed [3:0] a;
reg signed [6:0] b; //当a的输入也是有符号数的时候,b可以不定义成无符号数。
//因为默认向高位补符号位
为什么b也定义成有符号数:因为如果不这样,当写代码的时候a的输入用二进制表示的,会导致不能向高位补符号位,而是补0,这里把b也定义成有符号数,就是为了实现当a的位宽不够时,能够向高位补符号位。
同样看二进制结果,因为a和b被定义成了有符号寄存器,所以最高位为符号位,则在赋值的时候,向高位补符号位,而不是0。
综上:
所有的常数赋值,正数的数据都是直接转换成二进制,然后向高位补符号位,最后存入寄存器中,而负数数据都是转换成二进制补码,然后向高位补符号位,存入寄存器中的。
对各种传输数据进行一个总结:
有符号常量就是指用10进制、16进制、8进制的表示,无符号常量就是指2进制表示。
因此我们在进行有符号运算的时候,一定要保证输入输出都定义成有符号数,同时也要注意输入寄存器的输入端的数据是有符号还是无符号,以及他们的位宽是多少。
代码:
reg [3:0] a,b;
reg [4:0] c; //结果多写一位,主要是防止溢出。
always@(posedge clk)
begin
c <= a + b;
end
仿真正确。
代码:
reg signed [3:0] a,b; //实际上,前面已经说了,无论输入是否定义成有符号数,其存入的数据都是补码。但是加法器的输出必须被定义成有符号数,这样才能正确传递符号位。
reg signed [4:0] c; //结果多写一位,主要是防止溢出。
always@(posedge clk)
begin
c <= a + b;
end
对于输入的数据a和b(位宽width),我们需要保证输入数据的绝对值最大不超过width-1
也就是范围在-2(width-1)~2(width-1);
测试:
a = 4'd4;
b = -4'd3;
#20
a = -4'd4;
b = 4'd2;
#20
验证正确。
注:
如果输入或输出均不定义成有符号数
其运算出的结果应当是这样的,综合之后使用的是5位加法器,其最终结果如果转换成有符号,为-15,无符号,则为17 。
因此在设计的时候,还是全部带上有符号数,这样也方便后续数据传递。
代码:
reg [3:0] a,b;
reg [4:0] c; //结果多写一位,主要是防止溢出。
always@(posedge clk)
begin
c <= a - b;
end
测试
注:
无符号肯定要保证输入输出都是正数才行,也就是被减数必须大于等于减数。
代码:
reg signed [3:0] a,b;
reg signed [4:0] c; //结果多写一位,主要是防止溢出。
always@(posedge clk)
begin
c <= a - b;
end
这里看一下硬件电路的实现:
代码:
module top( input clk, input signed [3:0] a, input signed [3:0] b, output [4:0] o ); reg signed [3:0] a0,b0; //实际上,前面已经说了,无论输入是否定义成有符号数,其存入的数据都是补码。但是加法器的输出必须被定义成有符号数,这样才能正确传递符号位。 reg signed [4:0] c; //结果多写一位,主要是防止溢出。 always@(posedge clk) begin a0 <= a; b0 <= b; c <= a0 - b0; end assign o = c; endmodule
结果如上图,是通过加法器来实现减法运算的,通过综合出的电路可知道
是一个6位的加法器
被减数和减数经过一位拼接送入加法器A端,即
A[5:0] = {a0[3],a0,1'b1}
B[5:0] = {~{a0[3],a0},1'b1}
然后加法器输出舍弃最低位,输出到下一级寄存器,即
c[4:0] = OUT[5:1]
这里手算一下:
除法比较简单,结果就是两数相除之后取整
reg [3:0] a,b;
reg [3:0] c; //结果不需要扩展,因为除完只会变小,不会变大
always@(posedge clk)
begin
c <= a / b;
end
module top( input clk, input signed [3:0] a, input signed [3:0] b, output [3:0] o ); reg [3:0] a0,b0; reg [3:0] c; always@(posedge clk) begin a0 <= a; b0 <= b; c <= a0 / b0; end assign o = c; endmodule
reg signed [3:0] a,b;
reg signed [4:0] c; //结果多写一位,主要是防止溢出。
always@(posedge clk)
begin
c <= a / b;
end
可见其结果的符号与两个操作数有关,也就是符合我们正常的除法运算规律。
module top( input clk, input signed [3:0] a, input signed [3:0] b, output signed [3:0] o ); reg signed [3:0] a0,b0; reg signed [3:0] c; always@(posedge clk) begin a0 <= a; b0 <= b; c <= a0 / b0; end assign o = c; endmodule
查看最后综合出来的电路,有符号和无符号的电路都是一样的,但是其内部不知道是怎样算的,不过功能是正确的。
wire signed [3:0] a;
wire signed [3:0] c;
assign c = (a + 2) >>> 2;//加上2的n-1次方再进行移位
—待更新—
reg [3:0] a,b;
reg [7:0] c;
always@(posedge clk)
begin
c <= a * b ;
end
reg signed [3:0] a,b;
reg signed [7:0] c; //输出的位宽只需要大于等于a和b的位宽之和减一就可以了
always@(posedge clk)
begin
c <= a * b ;
end
在设计的时候尝尝会有常数参与运算,当进行有符号运算时,比如
wire signed [3:0] a;
wire signed [7:0] c;
assign c = a + 3'd2;
如果输入端有负数,这就会导致结果出现错误,如下图的第三组和第五组。
如果将表达式写成
assign c = a + 2;
或改成
wire signed [2:0] data;
assign data = 2'd2;
assign c = a + data;
结果:
这样才是正确的。
所以在设计的时候,需要注意这个点。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。