FPGA基础设计:Verilog数据类型和表达式

1. 数据类型

Verilog HDL中数据类型的作用是表示硬件中的数据存储和传输,总体上数据类型可以分为两类,代表不同的赋值方式和硬件结构。

网络(net):表示不同结构实体之间的物理连接。net本身不能存储值,它的值由它的驱动器上的值决定。如果net类型的数据没有和驱动器连接,则默认值为’z’表示高阻。(只有一个例外是trireg类型的数据,不符合上述描述)。网络数据类型包括wire、wand、wor、tri、triand、trior、tri0、tri1、trireg、supply0、supply1、uwire共12种。FPGA设计时一般只用wire,其它类型要不然是综合工具不支持,要不然是我还没碰到过。

变量(variable):表示数据存储单元,过程块中对其赋值会改变物理上数据存储单元中的值。reg、time、integer类型的数据初始值为x表示未知;real和realtime类型的数据初始值为0.0。

更早的Verilog标准将reg、integer、time、real、realtime这些类型的数据称作寄存器(register),2005版标准中已经废除了这个叫法。

根据标准,如果网络类型和reg类型的数据没有声明位宽,则默认为1bit宽度,称作标量(scalar);声明了多个bit的位宽,则称作矢量(vector)。

我们定义16bit位宽的矢量时,通常用 “reg [15:0] data;” 这种形式。[ ]中的范围规定,作用只是给矢量中的每个bit一个对应的地址,左边的值对应MSB,右边的值对应LSB。

Verilog标准并没有规定[ ]中的范围值该怎么取,事实上选择正数、负数、0都可以。MSB对应的地址也不是必须大于LSB对应的地址。如下面代码中都是定义了16bit位宽的数据,将MSB赋值为1:

module test
(
    input clk,
    output [15:0] a, 
    output [16:1] b, 
    output [0:15] c,
    output [5:-10] d
);

assign a[15] = 1, b[16] = 1, c[0] = 1, d[5] = 1;

endmodule

矢量数据的最大bit位宽,由各软件工具自行规定,但不得小于65536。

2. 变量(variable)

网络类型一般只用wire,不再细说,主要记录一下变量这种数据类型。

reg类型的数据最为常用,在过程块中赋值,可以对硬件中的寄存器进行建模,如边沿敏感性(触发器)、电平敏感型(复位-置位寄存器、锁存器)存储单元。但reg数据也可以用来表示组合逻辑(如always块的敏感列表为‘*’)。

除reg以外,其它类型的变量更多是提供一些代码设计上的便捷。

  •   integer类型:相当于32bit的带符号数;

  •   time类型:相当于64bit的无符号数,只是通常用于仿真,与$time系统任务配合使用,存储仿真时间;

  •   real和realtime:都是浮点数,没有本质上的区别,可以认为等效;

integer和time本质上仍然是整数,因此和reg一样支持位选和段选;real和realtime属于浮点数,不支持段选和位选。这四种数据类型都可以用于综合设计中,但是real和realtime两个实数类型在综合设计中只能为常数。

下面给出一个示例代码:

module test
(
    input clk,
    output reg [15:0] c,
    output reg [15:0] d    
);

integer i = -500;
realtime j = 20.5;

always @ (posedge clk) begin
    c <= i*2;
    d <= j*3;
    i <= i + 1;
end

endmodule

仿真结果可以看到,c的值为-1000、-998、-996…随时钟变化,d的值固定为63(实数转换为整数)。

看一下行为仿真阶段的RTL原理图:


integer类型的变量被当作32bit的reg型寄存器来处理;real类型的变量直接当作常数赋值来处理。

3. 数组

Verilog中所有网络和变量数据都可以定义为数组的形式。数组大小(即地址范围)在标识符后面声明,一个地址范围代表一个维度。比如:

reg [15:0] mem_data [255:0];

reg表示数据类型;[15:0]表示单个数据的矢量大小;mem_data是标识符;[255:0]是一维数组的地址。同样,Verilog标准也没有规定数组地址的取值,可以是正数、负数或0。

只要数据的矢量长度相同,Verilog便支持在同一行中声明,如下:

reg [7:0] arra [3:0], arrb, arrc [15:0];

在同一行中声明了矢量数组arra、单个数据arrb、矢量数组arrc,它们的矢量长度都是8bit。

数组不支持在一个赋值语句中同时对多个数组元素进行赋值,只能通过索引一个一个的赋值。数组的索引除了固定的常数外,也可以是表达式。Verilog的这个机制可以让电路中的其它变量和网络类型数据作为数组的索引。

Verilog的数组不能集体赋值在一定程度上带来不便,实现某些设计意图时可以使用一些方法简化代码设计。比如将上面的长度为256的mem_data数组初始化为0时,可以用integer和for循环,代替一个一个的初始化。示例如下:

reg [15:0] mem_data [255:0];
integer i;

always @ (posedge clk or negedge rst_n) begin
    if (!rst_n) 
        for (i = 0; i < 256; i=i+1)
            mem_data[i] <= 'd0;
    else 
        ...
end

上面的代码便等效于写了256条初始化语句。

通常,reg类型的一维数组也称作内存(memory),可以用来建模ROM、RAM或reg file。不过要注意“n个1bit reg的一维数组: reg mema [1:n];”和“n-bit宽度的reg矢量:reg [1:n] rega;”是不同的,使用上有差别。

4. 表达式

表达式(Expression) 是一种连接操作数(operand)和运算符(operator)的结构,可以认为是一个操作数值和运算符语义的函数。如果这个“函数”的结果是常数,则称作常数表达式(constant expression)。

4.1 运算符

下面列出几个不太常用或者有特别注意事项的运算符:

除法 / 和求余 %

a / b 和 a % b,如果第二个操作数b为0,则表达式结果会是‘x’未知状态。

整数除法运算结果采用截断所有小数位的方式(和前面提到过的实常数赋值不同),因此1/2、2/3、-1/2、-2/3的运算结果都是0。

求余运算结果的符号与第一个操作数的符号相同,因此

8 % 3 = 2
-8 % 3 = -2 
8 % -3 = 2
-8 % -3 = -2   // 第二个操作数的符号不影响结果

幂运算 **

a ** b 表示数学上的幂运算a^b。下面的这些规则一般都不会碰到,主要是对违反幂运算规则的情况做一些规定:

  •   如果a和b都是整数:当a为0,b为0时,结果为1;当a为0,b为负数时,结果为x未知状态。

  •   如果a和b有一个为实数,则结果为实数:当a为负数,b不是整数、或a为0,b为负数时,结果值无意义,一般处理为0或者inf。

另外要注意运算优先级的问题,看下面的例子:

real a = 9 ** 0.5;            // = 3
real b = 9 ** (1/2);          // = 1
real c = 9 ** (1.0/2);         // = 3
real d = 9 ** (1.0/2.0);        // = 3

a的结果毫无疑问是3.0,表示9的开方;b的结果却应该是1,因为先进行1/2的整数除法结果为0,再进行幂运算结果为1。如果采用c和d这样实数除法的写法,结果便是3。

数学运算时unsigned和sigend的问题

前面说过,unsigend和signed的声明并不会改变每bit上的值和二进制的运算规则,只是表明设计者、运算符、IP核看到这个对象时的一种“身份”。但如果没有这个身份,有些运算结果会与预期不符。

下面是一个计算 -12/3 的示例代码:

module test
(
    input clk,
    output  reg  [15:0] a,b,c,d
);

wire [15:0] aa, bb;
assign aa = -'sd12;
assign bb = 'sd3;

always @ (posedge clk) begin
    a <= aa + bb;
    b <= aa - bb;
    c <= aa * bb;
    d <= aa / bb;
end

endmodule 

网络类型的aa和bb没有特别声明为signed,则默认为unsigned。进行加、减、乘、除四种运算,仿真结果如下(Radix全部设置为signed decimal):


加、减、乘仍然可以得到正确的结果(因为运算规则一样),而除法结果不是预期的-4。

虽然代码中 -'sd12 和 'sd3 都加了’s’表示常数是带符号数,但将其赋值给无符号类型的wire数据后,再执行 “d <= aa / bb;” 等数学运算时,aa和bb的“身份”仍然是无符号数,因此除法产生了错误的结果(aa = 0xfff4,当作带符号数看待时为-12,当作无符号数看待时为65524,65524/3=21841,为上图中的仿真结果)。

因此需要将代码 “wire [15:0] aa, bb;” 补充上带符号数的声明 “wire signed [15:0] aa, bb;”,除法运算将操作数当作带符号数看待,即可得到正确的结果-4。

关系运算符

>、<、<=、>=四个关系运算符,主要注意操作数的类型。看下面的示例代码:

module test
(
    input clk,
    output  reg  [15:0] a,b,c
);

wire signed [15:0] aa = 11, bb = -5;
wire [15:0] cc = 11;
real dd = 11.1;

always @ (posedge clk) begin
    a <= cc > bb;    // false,unsigend compare, bb无符号数为 65531
    b <= aa > bb;    // true, signed compare
    c <= dd > cc;    // true, real compare
end

endmodule 

虽然aa和cc都是11,但一个是signed,一个是unsigned,与bb(-5)比较时结果也不同。要熟悉这些关系运算规则:

  •   无符号比较:两个操作数中有一个是unsigned类型,执行的便是无符号值的比较。如上面在无符号比较时,cc=11,bb=65531,cc > bb 的结果自然是false;

  •   带符号比较:两个操作数都是signed类型,执行的才是带符号值的比较。aa = 11, bb = -5,因此 aa > bb 的记过为true。

  •   实数比较:两个操作数有一个是实数,则另一个操作数也会转换为实数,执行实数值的比较。如上面 dd=11.1,cc=11.0(转换后),dd > cc 的结果为true。

相等运算符

比较两个数据“相等”或“不相等”,比较的是两个操作数每bit的值,如果两个操作数位宽相同,则signed或unsigend不会对结果产生影响。

但如果两个操作数的位宽不相同,signed和unsigend会影响到低位宽操作数的位宽扩展。看如下示例代码:

wire signed [15:0] aa = -11;
wire signed [12:0] bb = -11;
wire [12:0] cc = -11;
wire signed [15:0] dd = 16'h5x12, ee = 16'h5x12;

always @ (posedge clk) begin
    a <= aa == bb;    // true,sign-extended
    b <= aa == cc;    // false, zero-extended
    c <= dd == ee;    // logical equality, x
    d <= dd === ee;   // case equality, 1
end

虽然aa、bb、cc的值都为-11,但aa和bb判断为相等,而aa和cc判断为不相等。注意如下规则:

  •   两个操作数中,只要有一个是unsigned,相等运算时,低位宽数据的位宽扩展是在前面补0。上面16bit的aa和12bit的cc做相等运算时,由于有一个操作数是unsigend,cc扩展为16bit时高位补了4个0,值发生了变化,导致 aa == cc 的运算结果为false;

  •   两个操作数都是sigend时,低位宽数据的位宽扩展是在前面补符号位。如上面16bit的aa和16bit的bb做相等运算时,bb扩展为16bit时高位补了4个符号位,值仍然是-11,因此 aa == bb 的运算结果为true;

  •   操作数也可以是实数,另一个操作数也会转换为实数进行相等运算。

相等运算包括logical equality(==和!=)、case equality(===和!==)两种,区别在于:logical相等运算在比较包含z和x值的操作数时,结果为x;而case相等运算对z和x两种值也会进行判断。不过case相等是不可综合的,综合工具会将其用logical相等代替。

[Synth 8-589] replacing case/wildcard equality operator === with logical equality operator == 

位操作运算符

异或运算 "^ “和同或运算符 “~^”(”^~"也可以),用的比较少可能会记得不是太熟。

   c <= 16'h5A5A ^ 16'hA5A5;  // 异或  // 结果 16'hFFFF
   d <= 16'h5A5A ^~ 16'hA5A5; // 同或  // 结果 16'h0000

位操作完全不关心数据是signed或unsigned,如果两个操作数位宽不相同,低位宽操作数前面固定补0,这样才能保证位操作运算的意义。

缩位运算符

对reduction这个词常见到“缩减、缩位、归约”几种翻译。这是一种一元运算符,对一个操作数进行运算,运算结果为1个bit。

&、|、^ 分别称作缩位与、缩位或、缩位异或,依次对操作数中的各bit执行与、或、异或操作。如下面两种写法的功能是等效的:

wire [4:1] data = 4'b1000;
wire res1 = ^ data;
wire res2 = data[4] ^ data[3] ^ data[2] ^ data[1];

~&、~|、~^ 这三个也属于缩位运算符,分别相当于是 &、|、^ 的结果取反。

移位运算符

包括逻辑移位(>>、<<)和算术移位(>>>、<<<),二者的关系如下:

  •   对于左移操作:<<和<<<,二者等同,都是在空缺位置补0;

  •   对于右移操作:>>仍然是在空缺位置补零。>>>与操作数类型有关,如果操作数是无符号数,在空缺位置补0;如果操作数是带符号数,则空缺位置补符号位。

看下面的示例代码:

module test
(
    input clk,
    output  reg  [3:0] a,b,c,d
);

wire [3:0] aa = 4'b1001;
wire signed [3:0] bb = 4'b1001;

always @ (posedge clk) begin
    a <= aa >> 2;     // 0010,补0
    b <= bb >>> 2;    // 1110,补符号位
    c <= aa << 2;     // 0100,补0
    d <= bb <<< 2;    // 0100,补0
end

endmodule 

对于signed类型的bb,使用算术右移,在空位填充了符号位,这也是逻辑移位和算术移位的唯一区别。另外,算数移位也是可以综合的。

4.2 操作数

操作数支持位选和段选,我们通常用的段选方法,如 “wire [7:0] out = data[15:8]” 称作常数段选(constant part-select)。Verilog表中还支持另一种段选方法,叫索引段选(indexed part-select),如下面代码所示:

module test
(
    input clk,
    output  reg  [7:0] a,b,c,d
);

wire [31:0] aa = 32'h00000012;
wire [0:31] bb = 32'h00000012;

always @ (posedge clk) begin
    a <= aa[0+:8];     // = aa[7:0],
    b <= aa[7-:8];     // = aa[7:0],
    c <= bb[24+:8];    // = bb[24:31]   
    d <= bb[31-:8];    // = bb[24:31]
end

endmodule 

采用不同的索引段选方式截取出数据的低8位。这种方式比较麻烦,使用起来感觉不太方便。

4.3 位宽问题

我们都知道加法、乘法会扩展结果的位宽,但这里讨论的是出现位宽扩展时,Verilog所“表现”的一些特性。熟悉这些特性,才能让代码设计效果按照我们的预期进行。

一个复杂的表达式会有一些我们看不见的中间结果,比如 d = (a + b) * c,假如a、b、c、d都是8bit数,那么先执行a+b产生的中间值位宽是多少呢?是加法进位后的9bit吗?如果没有搞清楚这个问题,会在代码设计上带来麻烦。

事实上“中间值的位宽=整个表达式中所有操作数(赋值语句包括等号左边的数)中最大的位宽”。看下面的示例代码:

module test
(
    input clk,
    output reg [7:0] a,c,d,
    output reg [8:0] b
);

reg [7:0] adda = 8'd145, addb = 8'd125;

always @ (posedge clk) begin
    a <= adda + addb;
    b <= adda + addb;
    c <= (adda + addb) >> 1;
    d <= (adda + addb + 0) >> 1; 
end

endmodule 

仿真结果如下:


8位的145+125结果为270,需要9bit数据保存。如果设计要求需要截位,都是保留最高有效位,舍弃低位。

  •   对于a,a <= adda + addb,整个表达式中操作数的最大位宽是8bit,因此运算结果丢掉了进位,得到14;

  •   对于b,b <= adda + addb,整个表达式中操作数的最大位宽是9bit的b,因此运算结果保留了进位,得到270;

上面两个结果是显而易见的,看下面两条。

  •   对于c,设计希望对加法结果进行截尾,通过右移1bit来实现保留最高有效位,舍弃低位,然而仿真结果却是7;

  •   对于d,在c的基础上,多加了一个0,仿真结果便是135,正确的保留了最高位(右移1bit相当于除以2)。

对于 c <= (a + b) >> 1 这种写法,整个表达式中所有操作数的最大位宽只有8bit,因此a+b的中间结果也是8bit(丢掉进位后的14),这样不能起到保留最高有效位的效果。

但是如果写成 d <= (a + b + 0) >> 1,表达式中多了一个未声明位宽的常数0,其默认位宽为32bit,这样加法的中间结果便不会丢掉进位。

4.4 符号问题

从前面描述的特性中可以看出,表达式中任意一个操作数是无符号类型的,整个表达式的运算便是无符号的;只有所有操作数都是带符号的,表达式运算才是带符号的(赋值语句表达式结果是否带符号,与等号左边的数据无关)。

另外值得注意,位选、段选、拼接、比较的结果固定为无符号数,与操作数无关。以段选为例,看下面的示例代码:

module test
(
    input clk,
    output reg [7:0] a, b, c, d
);

reg signed [3:0] adda = 4'sb1100;

always @ (posedge clk) begin
    a <= adda;                    //  11111100,带符号数
    b <= adda[3:0];               //  00001100,无符号数
    c <= $unsigned(adda);         //  00001100,无符号数
    d <= $signed(adda[3:0]);      //  11111100,带符号数
end

endmodule 

  •   对于a,由于表达式是带符号数,赋值结果进行了符号位扩展;

  •   对于b,即使通过段选选择了矢量数据的所有bit,表达式结果仍然是无符号数,赋值后高位扩展0;

  •   对于c,使用$unsigned系统任务将带符号数转换为无符号数,则表达式结果变成了无符号,赋值后高位扩展0;

  •   对于d,使用$signed系统任务将段选结果转换为带符号数,则表达式结果变成了带符号,赋值后进行符号位扩展。

5. 赋值

赋值包括两种:

  •   连续赋值用于给网络(net)类型的数据赋值,相当于在驱动网络。无论赋值语句右边的值何时发生变化,赋值操作会马上执行。连续赋值是一种对组合逻辑的建模方法。

  •   过程赋值用于给变量(variable)类型的数据赋值。过程赋值必须在always、initial、task、function四个过程块中进行

这里只记录一个不太常用的特性,赋值语句的左边(=或<=的左边)可以是一个拼接运算。下面的代码完全是合理的:

wire aa, bb, cc, dd;
assign {aa, bb, cc, dd} = 4'b1011;

reg [3:0] dataa, datab;
always @ (posedge clk) begin
    {dataa, datab} <= 8'hFE;
end

习惯了这种写法还是可以经常使用的,比如截取DDS IP核输出数据的sin和cos、FFT IP核输出数据的实部和虚部时都可以这样写。


文章来源:FPGADesigner的博客
*本文由作者授权转发,如需转载请联系作者本人

推荐阅读