我们终于可以继续学习了,也是没有办法,其实工作的80%的时间都是在忙杂事,就像打游戏一样,其实大部分时间都在打小怪,清理现场,真正打终极BOSS的时间是很少的,但是不清小怪,打BOSS就束手束脚,也很难通关啊。
我们先来复习一下前面的学习内容:
看上去还是做了不少事情啊,我们甚至还不求甚解地霸蛮做了个应用程序,居然还把它跑起来了。从前面可以看到,其实我们还差一个汇编器,就是从门级网表到HDL4SE c语言代码表达的转换程序,这也是后面编译器的第一个目标,首先做出来的编译器要能够接受门级网表,其中要能够接受HDL4SE的基本单元表达方式。下一步我们一方面继续学习verilog语言,另一方面逐步完善编译器,达到完全能够接受RTL描述。在verilog学习的后期会逐步以RISC-V CPU核作为目标,穿插到学习过程中,逐步把它设计出来,完成前面定下来的小目标。
ASIC设计的前辈们其实是直接设计门级网表的,一般是用一个CAD工具画出各种ASIC工艺库提供的基本单元以及它们之间的连接关系,然后转换成verilog门级代码,这个有点像早期的软件高手都用汇编语言编程一样。
然而用门级网表编程还是太麻烦了,不符合人的思维习惯,而且基本上无法移植,可读性和可维护性都很差,唯一的优点是高手能编出效率超高的代码出来,当然低手编出来的门级代码那是惨不忍睹的,能正确运行就不错了。在门级网表中,一个if语句都要用几个mux2来表达,另外,没有算术表达式和逻辑表达式,一个counter<=counter+1的赋值语句都要生成一个常数1基本单元,一个加法基本单元和一个寄存器基本单元,还要把它们连接在一起。如果表达式复杂点,把表达式解析成一个个的基本单元实现本身就是一件很麻烦的事情,这其实就是用汇编语言编程和用高级语言编程的差别。更重要的是,编出来的程序还失去了通用性,使用HDL4SE基本单元编的汇编代码只能在HDL4SE上编译运行,可能有些基本单元用verilog提供了内部逻辑实现,但是不能指望所有的基本单元都能够用verilog表示出来。使用Altera FPGA编的汇编代码(门级网表)一般也只能在Altera FPGA对应型号上编译运行,如果硬要在Xilinx的FPGA下使用,那就得做个转换器,将Altera FPGA的基本单元用Xilinx的基本单元表示出来,理论上是可行的,但是仍然有实现上的困难,甚至可能因为时序关系等原因实际上无法准确转译过去。这个有点像在X86上提供ARM的模拟器一样,代价是很高的。
因此我们需要一种与平台无关的表达方式来编写verilog应用,一般认为满足RTL的描述就可以了。这其中,表达式和赋值就是重要的表达方式。我们肯定更加喜欢这种表达方式:dst = a * b + c * d; 而不是X86汇编语言(用MSVC编译后生成的汇编代码截了一段):
; 13 : dst = a * b + c * d;
mov ecx, DWORD PTR _a$[ebp]
imul ecx, DWORD PTR _b$[ebp]
mov edx, DWORD PTR _c$[ebp]
imul edx, DWORD PTR _d$[ebp]
add ecx, edx
mov DWORD PTR _dst$[ebp], ecx
或者是前面几节的HDL4SE的汇编代码:
wire [31:0] a, b, c, d, dst, ab, cd;
hdl4se_binop #(32, 32, 32, BINOP_MUL) mul_ab(a, b, ab);
hdl4se_binop #(32, 32, 32, BINOP_MUL) mul_cd(c, d, cd);
hdl4se_binop #(32, 32, 32, BINOP_ADD) add_ab_cd(ab, cd, dst);
可读性差,可移植性差,可维护性差。如果这样的代码多了,很难看清楚其中的表达的算法。
本节我们来学习表达式与赋值,以及讨论如何编译成汇编代码。本节的大部分内容直接来自于IEEE. 1364-2005,严格讲不能算原创了。其中并没有完整抄录,翻译可能也有瑕疵,因此如果疑问,以IEEE. 1364-2005英文版为准。
verilog的表达式,与c语言的表达式差不多,但是要注意verilog中的数字是带宽度的,宽度理论没有上限,因此可能涉及到不同宽度的数字之间的运算的问题,类似于c语言的16位与32位数字的混合运算的问题,还是有些不一样,特别是对带符号数字,初学的时候有些适应不过来。带符号数都是按补码来表达的。表达式是一种计算机语言中比较复杂的部分,也是计算机语言表达能力最强的部分,要耐心学习,仔细比较与c语言有何不同。
按照IEEE.1364-2005的规定,基本的数据类型可以是reg, integer, time, real, realtime和string,在HDL4SE中,我们强调可编译性,RTL的描述,因此不支持time, real和realtime,string只在一些特定的地方使用,不参与运算。按照规范,integer的位数可以由实现指定,但是不得少于32位,在HDL4SE中我们将integer视为32位整数。这样,实际参加运算的基本单元如下:
verilog中的运算符包括:
符号 | 含义 |
---|---|
{} {{}} | 位连接及重复 |
unary + unary - | 单目运算符 |
+ - * / ** | 算术运算 **是指数运算 |
% | 求余数 |
> >= < <= | 算术表达式之间的大小比较,结果为逻辑值 |
! | 逻辑非 |
&& | 逻辑与 |
|| | 逻辑或 |
== | 逻辑等,其实也是算术等 |
!= | 逻辑不等,也是算术不等 |
=== | Case意义上的相等,主要跟x,?,z相关 |
!== | Case 不等 |
~ | 逐位取反 |
& | 逐位与 |
| | 逐位或 |
^ | 逐位异或 |
^~ or ~^ | 逐位异或非(相等) |
& | 操作数每位与起来,得到一位结果 |
~& | 操作数每位与起来再取非,得到一位结果 |
| | 操作数每位或起来,得到一位结果 |
~| | 操作数每位或起来再取非,得到一位结果 |
^ | 操作数每位异或起来,得到一位结果 |
~^ or ^~ | 操作数每位异或起来再取非,得到一位结果 |
<< | 位左移,右边补零 |
>> | 位右移, 左边补零 |
<<< | 算术左移,右边补零 |
>>> | 算术右移,左边补符号位 |
? : | 条件选择 |
因为我们不支持real数据类型,如果你想更深地了解它们,建议阅读IEEE. 1364-2005相关的内容,上面的表格中并不是所有的运算符都能够对real数据类型有效。
verilog语言中的表达式中,运算符与c语言一样有优先级,也就是如果连续出现的运算符,优先级高的先计算,相同优先级的则先出现的先计算。verilog语言中的运算符优先级规定如下:
运算符 | 优先级 |
---|---|
+ - ! ~ & ~& | ~| ^ ~^ ^~ (单目) | 最高优先级 |
** | |
*/% | |
+ - (双目) | |
<< >> <<< >>> | |
< <= > >= | |
== != === !== | |
& (双目) | |
^ ^~ ~^ (双目) | |
| (双目) | |
&& | |
|| | |
?: (条件表达式) | |
{} {{}} | 最低优先级 |
当然,圆括号肯定最先计算,不过圆括号不算运算符,所以没有列在上述表格中。
表达式中出现整数常量时,如果不指定宽度,在HDL4SE中按照32位处理。整数常量中超出位宽的部分,只保留低位。整数常量如果不用s修饰,则为无符号整数,带s标志则是带符号的。比如:
整数 | 表示含义 |
---|---|
12 | 不带宽度的带符号整数12,按32位处理,等价于32’b0000_0000_0000_1100,值为12 |
’d12 | 不带宽度的无符号整数,按32位处理,等价于32’b0000_0000_0000_1100,值为12 |
'sd12 | 不带宽度的带符号整数,按32位处理,等价于32’sb0000_0000_0000_1100,值为12 |
4’d12 | 宽度为4位的无符号整数,等价于4’b1100,值为12 |
4’sd12 | 宽度为4位的带符号整数,等价于4’sb1100,值为-4 |
这样,下面表达式的值对应:
表达式 | 值 |
---|---|
-12 / 3 | 12是带符号数,等于32’h0000_000C,因此-12的值等价于32’hFFFF_FFF4, -12/3的值等于32’hFFFF_FFFC,由于是带符号数,因此为十进制-4。 |
-'d 12 / 3 | 'd12是无符号数,等于32’h0000_000C,因此-'d12等于32’hFFFF_FFF4,-’d12/3等于32’h5555_5551,因此等于十进制无符号数1431655761. |
-'sd 12 / 3 | ‘sd12是带符号数,等于32’h0000_000C,因此-'sd12等于32’hFFFF_FFF4,-’sd12/3等于32’hFFFF_FFFC,因此等于十进制数-4 |
-4’sd 12 / 3 | 4’sd12是带符号数,等于4’sb1100,-4‘sd12就是4’sb0100,-4’sd12/3结果是4/3=1余1 |
可见在verilog数字的表达要小心,因为有位数的关系,很容易溢出。另外默认的是无符号的数,这个跟c语言有点不一样,c语言默认是带符号的。4’sd12其实是个负数,这点不是很直观了,有点象c语言中的short a; a = 65535;的赋值一样,结果其实是-1,所以想通了还是一样的,但是不那么直观。
verilog中算术运算包括五种:
算术运算 | 含义 |
---|---|
a + b | a 加 b |
a - b | a 减 b |
a * b | a 乘以 b |
a / b | a 除以 b, 整数除法的结果会截断,不进行舍入 |
a % b | a 除以 b的余数,余数的符号与被除数a相同 |
a ** b | a 的b次方 |
算术运算与c语言中基本上一致,除法和求余数计算时,如果除数b为0,结果为x(结果未确定,实际的电路中,跟实现相关)。如果任何一个操作数为实数,则结果为实数。
整数的指数计算的规则如下:
a<-1 | a=-1 | a=0 | a=1 | a>1 | |
---|---|---|---|---|---|
b>0 | a ** b | b是奇数为–1,b是偶数为1 | 0 | 1 | op1 ** op2 |
b=0 | 1 | 1 | 1 | 1 | 1 |
b<0 | 0 | b是奇数为–1,b是偶数为1 | 'bx | 1 | 0 |
下面是一些表达式的计算例子:
表达式 | 结果 | 说明 |
---|---|---|
10 % 3 | 1 | 10除以3余数为1 |
11 % 3 | 2 | 11除以3余数为2 |
12 % 3 | 0 | 12除以3余数为0 |
–10 % 3 | –1 | -10除以3余数为-1,结果的符号跟被除数 |
11 % –3 | 2 | 11除以-3余数为2 |
–4’d12 % 3 | 1 | –4’d12实际上是一个很大的正数1431655761,它除以3余数为1 |
3 ** 2 | 9 | 3 * 3 |
2 ** 3 | 8 | 2 * 2 * 2 |
2 ** 0 | 1 | 任何数的0次方结果为1 |
0 ** 0 | 1 | 0的0次方也为1 |
2.0 ** –3’sb1 | 0.5 | 2.0是实数,因此结果是实数 |
2 ** –3 'sb1 | 0 | 2 ** –1 = 1/2 整数除法截断到0 |
0 ** –1 | 'bx | 0 ** –1 = 1/0,整数除以0结果为 'bx. |
9 ** 0.5 | 3.0 | 实数开根号 |
9.0 ** (1/2) | 1.0 | 括号中的整数除法截断到0,9.0的0次方为1.0 |
–3.0 ** 2.0 | 9.0 | 2.0是偶数,因此相当于平方 |
算术计算中,如果变量参与计算,根据变量的类型不一样,解释的方式不一样:
变量类型 | 在算术计算中的解释 |
---|---|
unsigned net | 无符号 |
signed net | 带符号,补码 |
unsigned reg | 无符号 |
signed reg | 带符号,补码 |
integer | 带符号,补码 |
time | 无符号 |
real, realtime | 带符号实数 |
不同长度的变量或常数参与计算,结果的长度后面会有更详细的说明。带符号数参与计算时,如果需要调整位宽度,则需要进行位扩展,保证补码表示的正确性。
两个数字比较时,如果结果为真,则结果为1’b1,如果结果为假,则为1’b0,如果任何一个操作数中包括x或z,则结果是1‘bx。如果两个操作数中有一个是无符号数,比较操作解释为无符号数之间的比较,也就是短的一个应该用0扩展高位到长的一个,然后按照无符号数比较来进行比较。
相等与不等比较,有两种,一种是逻辑相等/不相等(==, != )表示每一位比较,如果遇上x,z结果为x。另一种case意义上的相等/不相等 ( ===, !==),此时每一位比较,遇上x,z也必须一致(case意义上,参见后面),结果总是0或1,不会出现x。
比较的结果是逻辑值,0或者1,逻辑值之间可以进行逻辑运算,逻辑运算包括!取非,&&表示逻辑与,||表示逻辑或,事实上,!运算等价于操作数等于零,操作数可以是任何整数。
位运算是按位进行的,位运算总是假定操作数都是无符号整数,也就是说,如果其中一个操作数位数比较少,应该用0填充高位,结果与长的操作数的长度一致,位运算的规则如下:
与运算
& | 0 | 1 | x | z |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
1 | 0 | 1 | x | x |
x | 0 | x | x | x |
z | 0 | x | x | x |
或运算
| | 0 | 1 | x | z |
---|---|---|---|---|
0 | 0 | 1 | x | x |
1 | 0 | 1 | 1 | 1 |
x | x | 1 | x | x |
z | x | 1 | x | x |
异或运算
^ | 0 | 1 | x | z |
---|---|---|---|---|
0 | 0 | 1 | x | x |
1 | 1 | 0 | x | x |
x | x | x | x | x |
z | x | x | x | x |
异或非运算
~^或^~ | 0 | 1 | x | z |
---|---|---|---|---|
0 | 1 | 0 | x | x |
1 | 0 | 1 | x | x |
x | x | x | x | x |
z | x | x | x | x |
非运算
~ | |
---|---|
0 | 1 |
1 | 0 |
x | x |
z | x |
缩位运算是一个verilog特有的运算,它作用到一个无符号整数上,如果是带符号的数,则解释为无符号数。然后每一位参与计算,最终得到一位的结果。其中的计算包括与,或,异或,与非,或非,异或非计算,应该是模拟多位的与门,或门,异或门,与非门,或非门以及异或非门。其中的位运算按照按照前面的位运算规则进行。下面是几个例子:
操作数 | & | ~& | | | ~| | ^ | ~^ | 备注 |
---|---|---|---|---|---|---|---|
4’b0000 | 0 | 1 | 0 | 1 | 0 | 1 | 全零 |
4’b1111 | 1 | 0 | 1 | 0 | 0 | 1 | 全1 |
4’b0110 | 0 | 1 | 1 | 0 | 0 | 1 | 偶数个1 |
4’b1000 | 0 | 1 | 1 | 0 | 1 | 0 | 奇数个1 |
有两种移位运算,一种逻辑移位<<和>>,另一种是算术移位<<<和>>>。移位运算不改变变量的位宽,左移运算<<和<<<实际上是一样的,移位后右边多出来的位补零。逻辑右移时多出来的位补零,算术右移时多出来的位如果操作数是无符号,则也补零,如果操作数是带符号数,则补符号位(原操作数的最高位)。移位规则与c语言是一致的,这里不多说明了。
条件表达式的格式是expression1 ? { attribute_instance } expression2 : expression3。相当于expression1与0相比,如果相等,则整个表达式的值为expression3,否则为expression2,如果expression1与0相比的结果是x,则整个表达式结果为x。
连接运算也是verilog语言中特有的一种运算,它能将多个变量或常数按位连接在一起,形成一个位宽为所有操作数位宽相加的值。宽度不明确的常数不允许出现在连接运算中,因为这样无法确定结果的宽度。
比如,对reg a; wire w; reg[31:0] b;声明的变量,表达式{a, b[3:0], w, 3’b101},将构成一个9位的值,由高到低分别是:a, b[3], b[2], b[1], b[0], w, 1, 0, 1当然也等价于{a, b[3], b[2], b[1], b[0], w, 1’b1, 1’b0, 1’b1},注意常数一定要给出明确的宽度。
连接中当然也可以出现连接表达式:比如:{a, b[3:0], {w, 3’b101}}与前面的{a, b[3:0], w, 3’b101}等价。连接中出现多个一样的模式时,可以将模式表达成连接,然后将重复次数放在前面,比如:
{1’b1, w, 1’b1, w, 1’b1, w, a, 3’b101}与{{1’b1, w},{1’b1, w},a, 3’b101}等价,前面两部分重复了,可以写成{{2{1’b1, w}}, a, 3’b101}。
一个4位带符号寄存器 reg signed [3:0] b4;符号扩展到16位数可以表示成{{12{b4[3]}}, b4}。
如果重复部分是调用函数产生的,其中的函数只会调用一次,结果重复多次,不会多次调用函数(不过对verilog语言来说,调用多次似乎也没有什么区别啊)。
字符串可以当做一个无符号的位常量参与运算,每个字符按8位计算,字符串常量的长度是字符数乘以八。这样字符串也可以用在前面的各种运算中,比如连接运算,比较运算等。比如:
reg [814:1] stringvar;
stringvar = “Hello world”;
stringvar = {stringvar,"!!!"};
值得注意的是,这样的赋值可能由于字符串宽度没有变量的长,赋值后变量的高位会填充0,这样:
reg [810:1] s1, s2;
s1 = “Hello”;
s2 = " world!";
s1和s2中的值是:
s1 = 80’h000000000048656c6c6f
s2 = 80’h00000020776f726c6421
{s1,s2} = 160’h000000000048656c6c6f00000020776f726c6421
而不是{“Hello”, " world!"}=96‘h48656c6c6f20776f726c6421=“Hello World!”
其实字符串在verilog中是用得比较少的,verilog中没有字符常量之说,用单字符字符串代替,效果是一样的。空字符串""等价于一个8’b0,如果不是空字符串,这个结尾的0字符是不出现的。
表达式及其中间结果的位宽,在某些运算下可能比较容易确定,但是在某些情况下就复杂一些。有些情况下表达式代表一种物理上的量,比如延迟等,这样位宽是确定的,更多的情况下表达式的位宽不仅仅跟表达式的操作数相关,也跟表达式赋值的左值表达式宽度相关,具体的计算规则如下,其中i,j,k是操作数,L(i)是i的长度:
表达式 | 位宽 | 注释 |
---|---|---|
未给定长度常数 | 跟integer一样,在HDL4SE中是32位 | |
给定长度常数 | 给定的长度 | |
i op j, 其中op是: +, -, *, /, %, &, |, ^, ^~, ~^ | max(L(i),L(j)) | 此时可能会发生溢出,此时结果保留低位,高位就丢失了 |
op i, 其中op是: +, -, ~, | L(i) | |
i op j, 其中op是: ===, !==, ==, !=, >, >=, <, <= | 1 | 操作数的长度调整到max(L(i),L(j))参与计算 |
i op j, 其中op是: &&, || | 1 | 各操作数自己决定长度 |
op i, 其中op是: &, ~&, |, ~|, ^, ~,^, ^~, ! | 1 | 操作数自己决定长度 |
i op j, 其中op是: >>, <<, **, >>>, <<< | L(i) | j自己决定长度 |
i ? j : k | max(L(j),L(k)) | i自己决定长度,j,k调整到max(L(j), L(k)) |
{i,…,j} | L(i)+…+L(j) | 各操作数自己决定长度 |
{i{j,…,k}} | i * (L(j)+…+L(k)) | 各操作数自己决定长度 |
其中应该关注溢出的问题,比如:
reg [15:0] a, b, answer;
answer = a + b;
计算的中间结果是16位的,可能会溢出,但是
answer = 0 + a + b,由于0是无长度整数,为32位,这样0+a+b按照32计算,结果就不会溢出了。
reg [3:0] a,b,c;
reg [4:0] d;
a = 9;
b = 8;
c = 1;
表达式c ? (a&b) : d的值计算如下,(a&b)是4位的,由于d是5位,所以表达式的值是5位的。
表达式宽度计算要特别小心,坑特别多,很容易出现一些莫名其妙的问题,一个简单的办法是编程序时程序员来保证每个表达式中的操作数宽度足够宽不会造成溢出。
表达式的符号也是一个容易出问题的地方。首先,有两个系统函数是来做带符号数和无符号数的转换的,$unsigned(a)返回一个无符号数,$signed(a)返回带符号数。这种转换不改变值,只是解释变了,比如:
reg [7:0] regA, regB;
reg signed [7:0] regS;
regA = $unsigned(-4); // regA = 8’b11111100
regB = $unsigned(-4’sd4); // regB = 8’b00001100
regS = $signed (4’b1100); // regS = -4
事实上赋值时会根据左值表达式的符号进行自动转换的,这两个系统函数对中间结果的类型转换有意义。下面是决定表达式符号类型的规则:
进行表达式(或中间结果)运算时,应按照下面的步骤进行:
unsigned short a = 10;
char b = -3;
printf("%d\n", a+b);
执行的结果是7。
但是
module testadd;
reg [15:0] c;
reg [15:0] a;
reg signed [7:0] b;
initial begin
#10 a=10;
#10 b=-3;
#10 c=a+b;
end
endmodule
执行后,c的值是16’b0000_0001_0000_0111,下面是模拟结果:
把a声明为reg signed [15:0] a;结果就是7了:
如果宽度都一样,结果也是预料中的。看来尽可能避免不同宽度的无符号数与带符号数直接运算,否则脑壳会被搞晕掉去,惹不起咱们躲着点。
表达式按照运算符的优先级,计算有先后顺序,如果优先级一致,则规定从左到右计算。这里似乎有某种顺序执行的含义在里边,事实上,在c语言及其编译过程中,这会决定编译后的执行顺序。在verilog语言的编译中,可以消除这种运算的执行顺序。编译时会生成表达式的语法树,先计算的运算在子节点,越后计算的运算在离根越近,整个表达式的结果在根节点计算出来。我们将每个运算符编译为一个基本单元,表达式的运算顺序其实决定了基本单元之间的连接关系,并不意味这基本单元之间执行上有某种顺序,这样就消除了隐含的执行顺序问题,将隐含的执行顺序化为组合逻辑上的有向图中的上下游关系。
这样就避免不可综合的电路产生,一般的verilog表达式是可以用于RTL可综合电路描述的。如果其中出现函数调用,则应该保证函数实现过程是RTL可综合的。
写了半天表达式,如果不把表达式的值赋予都某个变量中,表达式就没有意义了,因此赋值也是计算机语言中的重要环节。像c语言这样在通用计算机上运行的语言,赋值语句一般会生成一条变量写语句,表达式的结果放在一个寄存器中,通过一条比如MOV/STORE之类的LOAD/STORE指令写入到存储器中去。在verilog语言中,我们描述的是电路,因此没有LOAD/STORE类型的指令,事实上verilog语言编译后并不生成CPU的指令,而是生成电路,赋值往往生成线网之间或者是线网与模块端口的连接。
赋值从形式上,可以分为两种,一种是对线网赋值,其实就是将线网与表达式的值直接连接在一起,称为持续性赋值(Continuous Assignment),用assign LValue=expression格式表达。另一种是向线网以外的变量赋值,称为程序性赋值(Procedural Assignment),所谓线网以外的变量,包括寄存器,time, integer等类型的变量,需要根据情况用LValue=expression格式或LValue <= expression格式表达。赋值语句的右边是表达式,左边就是赋值的对象,称为左值表达式。并不是任何一个表达式都能够成为左值表达式,两种赋值的目标(左值)如下:
赋值语句类型 | 左值形式 |
---|---|
持续性赋值 | 线网 (一位或多位), 线网变量的常数位选择, 线网变量的常数位段, 线网变量的常数索引位段选择, 上述左值的连接或者嵌套连接 |
程序性赋值 | 非线网变量, reg, integer,或time变量的位选择 reg, integer 或time变量的常数位段选择 reg, integer或time变量的索引位段选择 内存字(Memroy word) 上面左值的连接或者嵌套连接 |
持续性赋值在电路上是用表达式的值去驱动线网,有两种格式的赋值语句,一种是声明线网的同时赋值:
wire wEnable = wInputValid;
声明时可以指定驱动强度以及上下拉特性等,比如:
wire (strong1, pull0) [31:0] bAddrKeyboard = bAddrBase + 32’h0204;
所谓驱动强度和上下拉特性跟后端的特征相关,我们这里不再关注。
还有一种是先声明线网变量,然后通过assign对它进行持续性赋值,比如:
wire mynet ;
assign (strong1, pull0) mynet = enable ;
下面是一个完整的四位全加器的verilog代码,中间有个用连接做左值的赋值语句:
module adder (sum_out, carry_out, carry_in, ina, inb);
output [3:0] sum_out;
output carry_out;
input [3:0] ina, inb;
input carry_in;
wire carry_out, carry_in;
wire [3:0] sum_out, ina, inb;
assign {carry_out, sum_out} = ina + inb + carry_in;
endmodule
下面这段代码是一个总线选择器,使用了四个带条件持续性赋值的格式:
module select_bus(busout, bus0, bus1, bus2, bus3, enable, s);
parameter n = 16;
parameter Zee = 16'bz;
output [1:n] busout;
input [1:n] bus0, bus1, bus2, bus3;
input enable;
input [1:2] s;
tri [1:n] data; // net declaration
// net declaration with continuous assignment
tri [1:n] busout = enable ? data : Zee;
// assignment statement with four continuous assignments
assign
data = (s == 0) ? bus0 : Zee,
data = (s == 1) ? bus1 : Zee,
data = (s == 2) ? bus2 : Zee,
data = (s == 3) ? bus3 : Zee;
endmodule
其实这种表达,已经与用case来控制的程序性赋值一样了。
持续性赋值的意义在于,只要右边的表达式的值发生变化,左边的变量值就会变化,不需要任何条件事件,对应到电路上就是直接将线网变量连接到表达式的输出上,以表达式输出的值来驱动线网。
线网声明时还可以声明自己的延时,可以理解为表达式变化后到线网上看到对应的值的间隔时间,这种表示属于不可综合(编译)特征,只是在时序仿真时有参考意义,可以统计一个组合逻辑电路的建立时间能否满足时钟周期,在HDL4SE中不关心这些参数。
持续性赋值语句作为一个module中的一个module_item角色出现,可以作为一个module单元出现在module的端口和参数声明后,endmodule之前的任何地方。
可以在一个module中多次对一个线网进行多次赋值,这样就相当于该线网被多个值驱动,一般在总线中使用。就像上面的例子一样,一般由一个选择器进行选通,没有选通的赋值语句赋予高阻态,一个线网一般同时只有一个赋值不是高阻态,如果有两个以上的赋值不在高阻态,这两个赋值可能会产生不可预知的结果,此时一般结果是x。
程序性赋值分为两种,一种所谓的阻塞赋值,一种是非阻塞赋值。阻塞赋值可以出现在声明的时候,比如:
reg [3:0] a = 4;
integer i = 0, j;
real ra = 3e2, rb=2.6 + 0.7;
此时右边只能出现常数表达式,不能出现编译时无法确定值的表达式。也可以在always语句和initial语句中出现,还能够在function和task中出现。
程序性赋值操作其实涉及的比较广,本节先开个头,下次介绍model中行为级描述的时候,再进行详细介绍。
【请参考】
1.HDL4SE:软件工程师学习Verilog语言(五)
2.HDL4SE:软件工程师学习Verilog语言(四)
3.HDL4SE:软件工程师学习Verilog语言(三)
4.HDL4SE:软件工程师学习Verilog语言(二)
5.HDL4SE:软件工程师学习Verilog语言(一)
6.LCOM:轻量级组件对象模型
7.LCOM:带数据的接口
8.工具下载:在64位windows下的bison 3.7和flex 2.6.4
9.git: verilog-parser开源项目
10.git: HDL4SE项目
11.git: LCOM项目
12.git: GLFW项目