HDL4SE:软件工程师学习Verilog语言(六)

6 表达式与赋值

我们终于可以继续学习了,也是没有办法,其实工作的80%的时间都是在忙杂事,就像打游戏一样,其实大部分时间都在打小怪,清理现场,真正打终极BOSS的时间是很少的,但是不清小怪,打BOSS就束手束脚,也很难通关啊。
我们先来复习一下前面的学习内容:

  1. 我们对数字电路有了基本的概念,了解verilog语言的运行与c语言还是有很大差别的。数字电路有两种基本的类型,一种是组合电路,数学上对应一个布尔函数,输出能够用输入完全确定,一个组合电路(或者说一个布尔函数)可以用真值表等价地确定,所谓真值表就是把输入的各种组合都列举出来,然后将对应的函数值放在一起形成的表格,比如九九乘法表,就是一个一位十进制数乘法运算的真值表。组合电路的特征是只要输入有变化,输出就会有变化,当然从输入变化结束到输出变化结束之间有一定延迟时间,这个时间称为组合电路的建立时间。另一种是时序电路,数学上对应一个布尔量的时间序列函数,输出跟电路的初始状态和历史上的输入相关。用电路描述,就是电路内部能够记住一组状态值,时序电路周期性根据目前的状态和输入来修改新的状态值并生成输出值。这个周期性信号是一般是一个周期性在01之间变化的信号,称为时钟信号,其频率就是时序电路的主频。RTL电路描述必须保证组合电路的建立时间小于时钟信号的周期,这样才能保证在周期的结束能够保存有效的状态,从而输出有效的结果,因此要求其组合电路部分不能有局部的自相关,也就是在计算网络中不能有圈。这种能够存储状态的单元称为寄存器,其物理特征是能够在连接在上面的时钟信号的沿到达时刻锁定输入信号到内部的存储单元,输出则由锁定的值确定。
  2. verilog语言是用来描述电路的,所以它的执行对应到电路的运行,与用CPU执行指令有本质的不同。电路运行是单元之间的没有顺序执行的概念,因此任何企图规定所描述电路单元之间执行顺序的描述,或者规定执行时间的描述都是无法用电路实现的,也就是常说的不可综合电路描述。每个基本单元都是独立运行的,可以勉强对应到c语言程序中的线程。前面的例子中,统计下来总共有各种单元313个,也就是说我们有一个相当于313个线程的程序在跑,然而这些线程跑的代码只有那么几种。软件工程师估计最怕调试的就是这种多线程程序了,而且还是海量那种,如果一个软件中有成千上万个线程,用通常的设置断点,观察中间输出值得方式来调试已经无效了,可能会有很多个单元运行到你设置的断点,没有办法设置到指定的单元(当然通过断点条件可以设置中断到你期望的单元,不过事情会变得非常复杂以至于不可用了)。因此verilog程序的调试一般是在每个时钟周期记录感兴趣的信号的值,形成的数据称为波形文件,然后在时间轴上对比各个信号的值,来判断程序的运行状态,这种调试方式更象硬件调试过程中使用信号分析仪的办法。
  3. 我们学习了verilog语言中的基本词法,这是构成verilog的基石,包括标识符,关键字,常数,字符串常量等。verilog中的数据宽度是以二进制位为单位的,一个数据可以任意宽度,当然最大宽度跟实现相关,HDL4SE中的宽度限制是2^30,对一般的应用应该是足够用的。verilog语言中还包括编译指示,注释等元素,方便编程,提高程序的可读性,这些通过预处理器进行处理。
  4. verilog程序的结构以module为基础。module对外的交互靠它的端口进行。一个module的端口可以有很多个,端口可以是输入,输出或者输入输出三类。一个module定义可以有实例化参数,这样实例化使用时可以带参数,也就是说定义一个module实际上等于定义了一类电路,它们的功能类似,但是内部的参数不一样,比如位宽,执行的功能等,则可以根据实例化参数不同而不同。module中可以声明其他module的实例,当然不能你中有我,我中有你,相互实例化。
  5. 并不是每个用verilog语言写出来的描述都能够编译成实际的电路。所谓可综合RTL(寄存器传输级)描述,是指verilog仅仅用来描述电路中的寄存器以及组合电路之间的连接,每个时钟周期组合电路的结果都被锁存到寄存器中去,看着似乎是从一组寄存器经过计算到另外一组寄存器中去了,所以称为寄存器传输级。这种描述满足两个条件:1.组合电路的计算网络不能构成有向圈,有圈的组合电路无法确保在一个时钟周期内输出稳定的值(双稳态电路似乎是一个反例);2.我们必须确保综合后的电路没有规定运行顺序,因此任何假定运行时有某种顺序的描述都是不能综合成电路的,比如用#表达的延迟时间,以及企图在组合电路描述中描述有顺序执行的语句,比如循环语句,如果不能展开,就被认为不是可综合RTL描述。再比如条件语句,表面上似乎包含了先计算条件,再计算结果,实际上可以编译为条件和结果同时计算,然后用条件来选择结果,这样就消除了顺序。本节的后面会看到表达式本身的计算顺序也必须消除,否则也无法综合成电路。组合电路中的赋值语句其实是对线网进行赋值,编译后是表达式的结果与线网的连接,也不存在c语言中的赋值语句中的赋值指令,因此也就无所谓先计算再赋值的顺序关系这一说,逻辑上允许计算和赋值并发运行。
  6. 本质上所有的电路单元可以用module以及module之间的连接表达出来。一个应用程序由一个顶层模块组成,顶层模块的接口在FPGA中对应FPGA的I/O管脚,在ASIC中一般也可以跟管脚对应,在我们的模拟系统中则是一个固定的总线接口。顶层模块下面可以实例化其他的module,然后一层套一层,形成一个module实例的树状结构。
  7. 不再细分的module称为基本单元,在ASIC中叫工艺库,在FPGA中可以是查找表或者是DSP等提供的不可再分的单元。基本单元一般是由硬件直接实现的,内部逻辑不能再修改,当然可以修改部分参数。HDL4SE中的基本单元是用LCOM的对象实现的,它们实现了统一规定的IHDL4SEUnit接口,可以实现IHDL4SEDetector接口以支持调试。verilog编写的module则用LCOM对象实现,除了实现前面的两个接口之外,还实现了IHDL4SEModule接口,该接口支持增加module的端口,增加内部的单元实例等操作,为将来verilog语言中的module模块编译提供支持。
  8. 可以用verilog来编写基本单元的规格说明。对HDL4SE中的基本单元,在module声明前用attribute_instance来说明这是一个HDL4SE模拟器的基本单元,其中应该声明HDL4SE="LCOM"以及对应的LCOM实现对象的CLSID,还包括一个可选的实现软件库名称。编译器在编译module定义时发现这一组attribute_instance时,就认为它是一个基本单元,会调用hdl4seCreateUnit来生成模块对象。我们提供了HDL4SE中内置的基本单元的声明和LCOM实现,做verilog应用时可以直接使用。用户也可以根据这个模式建立自己的基本单元库,提供verilog模块定义以及c语言实现的LCOM规范的单元库。这样设计允许一个大的项目在设计的初期很多模块直接用c语言或其他语言实现,而不是必须用verilog实现到RTL,模拟也可能大大加速,有利于设计迭代,设计过程中可以逐步将c语言设计模块替换成更小的模块,直到全部用verilog的RTL实现。可以期待这种模式下的部分模块外包,设计IP化。
  9. module中的module实例之间可以用线网连接,线网或者子模块的端口也可能连接到父模块的端口上。线网把module连接在一起,构成了电路。
  10. 全部由某种开发下的平台基本单元和线网组成的module称为门级表达,我们软件工程师可以称之为是verilog汇编语言。如果一个verilog中除了顶层module之外的所有模块都象软件中的inline函数一样展开,也就是整个应用只有一个顶层module,而且是门级表达,一般称为门级网表,类似于c语言生成了汇编程序。一般FPGA开发工具可以选择生成这种门级网表, ASIC做后端设计是一般也是从这种门级网表开始的。
  11. 软件方面我们完成了verilog的语法描述文件,从IEEE.1364-2005 附录A中的BNF格式语法转换到bison中能够接受的等效YACC语法。实际上把规范中附录A中的BNF语法描述拆成三个部分,即预处理器,词法分析器,语法分析器。其中预处理器处理verilog语法中的编译指示,比如重要的`include, `ifdef ,`else, 'endif, `define,以及`define定义的宏的使用等等,输入带编译指示的verilog源代码,输出的代码中已经完成了处理,消除了所有的预处理命令。词法分析器处理verilog语法中跟词法相关的部分,将预处理后的源代码切分成一个一个的单词,以便由语法分析器接着处理,词法分析器使用flex工具,我们编写了flex能够接受的词法描述文件。语法分析器则用bison实现,我们编写了bison能够接受的YACC格式语法描述,不过目前功能实现部分还几乎是空的,后面会慢慢填充,先能够接受HDL4SE的汇编代码,然后接受RTL代码,最终要生成能够实际执行的HDL4SE目标代码。我们选择用LCOM框架下的c语言代码作为HDL4SE的目标代码,最终目标代码可以编译连接成一个动态连接库,由模拟器调用执行。
  12. 软件方面我们还定义了HDL4SE的基本单元库,用verilog描述出来,能够在verilog语言中直接使用。对应的我们还是实现了基本单元库的LCOM版本,以支持模拟器运行。
  13. 我们还设计了一个大整数运算支持模块,可以完成verilog中任意宽度的整数的字符串转换,可以支持从字符串解析到大整数对象以及从大整数对象生成字符串,支持大整数之间的赋值以及部分赋值,支持大整数的运算,包括算术运算,比较运算,移位运算,逻辑运算,位运算,缩位运算,可以在基本单元库的支持下实现连接运算。
  14. 软件方面我们还定义了模拟器接口,并在基本单元库中实现,给出了模拟器中主模块的接口以及设备的接口。我们还实现了一个模拟器对象,能够将主模块和设备模块连接起来,并进行逐周期进行模拟。我们还设计了调试器接口,能够用csv格式记录调试过程中感兴趣的信号在每个周期内的取值,可以用Excel来进行结果分析,达到调试的效果。将来可能生成通用的波形文件格式,这样可以用其他更专业的波形查看软件来看波形。
  15. 作为一个例子,我们实现了一个数码管控制和键盘控制的LCOM模块,可以作为HDL4SE模拟器的设备挂接到模拟器中运行。我们用verilog语言编写了计数器应用的verilog主模块,然后手工把它编译为HDL4SE汇编代码,就是用HDL4SE基本单元表达的门级网表,然后还是手工把这个门级网表转换成HDL4SE目标代码,就是c语言代码,然后把它们连接在一起运行,还用调试器进行了调试,改正了其中的一个设计bug,然后…呃,就没有然后了。

看上去还是做了不少事情啊,我们甚至还不求甚解地霸蛮做了个应用程序,居然还把它跑起来了。从前面可以看到,其实我们还差一个汇编器,就是从门级网表到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英文版为准。

6.1 表达式运算

verilog的表达式,与c语言的表达式差不多,但是要注意verilog中的数字是带宽度的,宽度理论没有上限,因此可能涉及到不同宽度的数字之间的运算的问题,类似于c语言的16位与32位数字的混合运算的问题,还是有些不一样,特别是对带符号数字,初学的时候有些适应不过来。带符号数都是按补码来表达的。表达式是一种计算机语言中比较复杂的部分,也是计算机语言表达能力最强的部分,要耐心学习,仔细比较与c语言有何不同。

6.1.1 基本运算单元

按照IEEE.1364-2005的规定,基本的数据类型可以是reg, integer, time, real, realtime和string,在HDL4SE中,我们强调可编译性,RTL的描述,因此不支持time, real和realtime,string只在一些特定的地方使用,不参与运算。按照规范,integer的位数可以由实现指定,但是不得少于32位,在HDL4SE中我们将integer视为32位整数。这样,实际参加运算的基本单元如下:

  • 常数 ,包括字符串
  • 参数(Parameter),包括局部参数,及其位选择和位段选择
  • 线网(wire),及其位选择和位段选择
  • 寄存器(reg),及其位选择和位段选择
  • 整数(integer),及其位选择和位段选择
  • 数组元素,及其位选择和位段选择
  • 返回上述元素的用户自定义函数或系统定义函数
    所谓位选择,就是在相关元素后面加一个[]括起来的整数,表示从元素中取对应的位,这等价于一个一位的数字。
    比如 对这样声明 reg [7:0] a; 的变量a,a[3]表示a的第三位。
    所谓位段选择,就是在相关元素后面加两个用[]括起来的并用:隔开的整数,表示从元素中取对应的起始位到终止位。
    比如 对这样声明 reg [7:0] a; 的变量a,a[3:1]表示a的第3, 2,1组成的一个三位数字,如果两个整数相等,则表示一位,跟位选择等价。
    变量的声明,数据类型后面加一个位段,然后后面是一系列声明的变量,其中位段是可选的,如果不给出位段,则表示声明一位的变量,比如reg [7:0] a, b;声明了两个宽度为8的变量,wire nwReset, wRead则声明了两个一位的线网。声明中的整数可以是一个常量表达式,其中可以出现参数,原则上编译时可以计算出值的表达式都可以,编译指示中定义的宏由预处理器处理后展开参与解析的。
    如果要声明数组,则在声明的变量名后面加一个[]括住的整数,可以声明多维数组,每加一维在后面加一个[]括住的整数即可,比如 reg [31:0] bData[0:15];声明了一个宽度为32位,有16个元素的数组,而reg[31:0] bData2[0:16][0:3];则声明了一个二维数组。注意到只有单个数组的元素能够参与计算,因此参与计算的合法数组元素是bData[3],bData[2][12:4],bData2[2][1],像bData[0:3],bData或bData2[1]这样指代了多个数组元素的用法是不允许参与计算的。数组元素后面也可以加位选择和位段选择。
    这里值得注意的一个问题是,声明时的位段标识中的两个整数可以任意取,甚至可以是负数,可以前面的比后面的大,这样使用的时候的位选择和位段选择就必须在它们规定的范围内。特别是前面比后面大的情况,位选择也必须符合同样的顺序。这样做其实把问题搞得很复杂,估计当时设计是为了保持信号索引的一致性,比如:
    wire [31:0] bBus;
    wire [31:24] bBusHigh;
    assign bBusHigh = bBus[31:24];
    这样的声明下,bBus[30]和bBusHigh[30]是同一个信号。
    位段选择还有另外一种表达方法,就是用起始位加宽度的方式,中间用+:和-:来隔开(而不是一个:),前面的数字表示其实序号,后面的数字表示宽度。注意这种表达法跟声明时前后两个数字的大小相关,前后大小估计是为了照顾有些厂商的习惯,有些厂商的信号表达是前面的序号小,后面的序号大,这样他们的信号定义就像reg[0:31],还有些厂商则相反,感觉verilog的规范似乎被厂商影响了似的。来看几个例子:
    reg [31: 0] big_vect;
    reg [0 :31] little_vect;
    reg [63: 0] dword;
    integer sel;
    big_vect[ 0 +: 8] 与 big_vect[ 7 : 0]等价。
    big_vect[15 -: 8] 与 big_vect[15 : 8] 等价。
    little_vect[ 0 +: 8] 与 little_vect[0 : 7]等价。
    little_vect[15 -: 8] 与 little_vect[8 :15]等价。
    两个位序不同的变量参与计算时,注意little_vect的应用中,序号小的在高位,序号大的在低位,在big_vect中则刚好反过来。另外,如果低位的序号不为零,则最低位就是序号最大(little)或最小的位(big)。
    按照verilog的规范,位选择和位段选择中的数字都可以是一般的表达式。但是实际上如果用一个带变量的表达式来做位选择或位段选择,在编译成电路时会生成很复杂的数据选择电路。比如big_vert[k]这样的表达方式生成的电路可能是一个32选1的数据选择电路,如果是big_vert[ka+:kw]这种表达法生成的电路就更加复杂了,可能是kw个32选1的数据选择电路。因此使用时要非常小心。
    我们的基本单元库的变量定义中的位段都是[WIDTH-1:0]的格式,编译器在处理位选择和位段选择时应该做变换处理。另外,位段声明时如果前面一个比后面一个小,则应该把对该类变量的访问进行一个位反序处理(增加一个位反序基本单元?还是编译时把它以及对它的所有引用全部反过来?还没想清楚)。为了避免复杂性,我们干脆在初期就规定声明时只允许出现[WIDTH-1:0]类型的声明好了。
    变量声明时,默认是无符号的,如果要声明带符号的变量,则在基本的类型后面加signed 标识,数字常量中如果要声明带符号的,则也需要在数字常量中在基数之前加s标志,具体方式见前面的词法部分。

6.1.2 运算符

verilog中的运算符包括:

符号 含义
{} {{}} 位连接及重复
unary + unary - 单目运算符
+ - * / ** 算术运算 **是指数运算
% 求余数
> >= < <= 算术表达式之间的大小比较,结果为逻辑值
! 逻辑非
&& 逻辑与
|| 逻辑或
== 逻辑等,其实也是算术等
!= 逻辑不等,也是算术不等
=== Case意义上的相等,主要跟x,?,z相关
!== Case 不等
~ 逐位取反
& 逐位与
| 逐位或
^ 逐位异或
^~ or ~^ 逐位异或非(相等)
& 操作数每位与起来,得到一位结果
~& 操作数每位与起来再取非,得到一位结果
| 操作数每位或起来,得到一位结果
~| 操作数每位或起来再取非,得到一位结果
^ 操作数每位异或起来,得到一位结果
~^ or ^~ 操作数每位异或起来再取非,得到一位结果
<< 位左移,右边补零
>> 位右移, 左边补零
<<< 算术左移,右边补零
>>> 算术右移,左边补符号位
? : 条件选择

因为我们不支持real数据类型,如果你想更深地了解它们,建议阅读IEEE. 1364-2005相关的内容,上面的表格中并不是所有的运算符都能够对real数据类型有效。
verilog语言中的表达式中,运算符与c语言一样有优先级,也就是如果连续出现的运算符,优先级高的先计算,相同优先级的则先出现的先计算。verilog语言中的运算符优先级规定如下:

运算符 优先级
+ - ! ~ & ~& | ~| ^ ~^ ^~ (单目) 最高优先级
**
*/%
+ - (双目)
<< >> <<< >>>
< <= > >=
== != === !==
& (双目)
^ ^~ ~^ (双目)
| (双目)
&&
||
?: (条件表达式)
{} {{}} 最低优先级

当然,圆括号肯定最先计算,不过圆括号不算运算符,所以没有列在上述表格中。

6.1.3 表达式中的整数

表达式中出现整数常量时,如果不指定宽度,在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,所以想通了还是一样的,但是不那么直观。

6.1.4 算术运算

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 带符号实数

不同长度的变量或常数参与计算,结果的长度后面会有更详细的说明。带符号数参与计算时,如果需要调整位宽度,则需要进行位扩展,保证补码表示的正确性。

6.1.5 比较运算

两个数字比较时,如果结果为真,则结果为1’b1,如果结果为假,则为1’b0,如果任何一个操作数中包括x或z,则结果是1‘bx。如果两个操作数中有一个是无符号数,比较操作解释为无符号数之间的比较,也就是短的一个应该用0扩展高位到长的一个,然后按照无符号数比较来进行比较。
相等与不等比较,有两种,一种是逻辑相等/不相等(==, != )表示每一位比较,如果遇上x,z结果为x。另一种case意义上的相等/不相等 ( ===, !==),此时每一位比较,遇上x,z也必须一致(case意义上,参见后面),结果总是0或1,不会出现x。

6.1.6 逻辑运算

比较的结果是逻辑值,0或者1,逻辑值之间可以进行逻辑运算,逻辑运算包括!取非,&&表示逻辑与,||表示逻辑或,事实上,!运算等价于操作数等于零,操作数可以是任何整数。

6.1.7 位运算

位运算是按位进行的,位运算总是假定操作数都是无符号整数,也就是说,如果其中一个操作数位数比较少,应该用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

6.1.8 缩位运算

缩位运算是一个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

6.1.9 移位运算

有两种移位运算,一种逻辑移位<<和>>,另一种是算术移位<<<和>>>。移位运算不改变变量的位宽,左移运算<<和<<<实际上是一样的,移位后右边多出来的位补零。逻辑右移时多出来的位补零,算术右移时多出来的位如果操作数是无符号,则也补零,如果操作数是带符号数,则补符号位(原操作数的最高位)。移位规则与c语言是一致的,这里不多说明了。

6.1.10 条件表达式

条件表达式的格式是expression1 ? { attribute_instance } expression2 : expression3。相当于expression1与0相比,如果相等,则整个表达式的值为expression3,否则为expression2,如果expression1与0相比的结果是x,则整个表达式结果为x。

6.1.11 连接运算

连接运算也是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语言来说,调用多次似乎也没有什么区别啊)。

6.1.12 字符串

字符串可以当做一个无符号的位常量参与运算,每个字符按8位计算,字符串常量的长度是字符数乘以八。这样字符串也可以用在前面的各种运算中,比如连接运算,比较运算等。比如:
reg [814:1] stringvar;
stringvar = “Hello world”;
stringvar = {stringvar,"!!!"};
值得注意的是,这样的赋值可能由于字符串宽度没有变量的长,赋值后变量的高位会填充0,这样:
reg [8
10: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字符是不出现的。

6.2 表达式计算过程中的几个问题

6.2.1 表达式的位宽

表达式及其中间结果的位宽,在某些运算下可能比较容易确定,但是在某些情况下就复杂一些。有些情况下表达式代表一种物理上的量,比如延迟等,这样位宽是确定的,更多的情况下表达式的位宽不仅仅跟表达式的操作数相关,也跟表达式赋值的左值表达式宽度相关,具体的计算规则如下,其中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位的。
表达式宽度计算要特别小心,坑特别多,很容易出现一些莫名其妙的问题,一个简单的办法是编程序时程序员来保证每个表达式中的操作数宽度足够宽不会造成溢出。

6.2.2 表达式的符号

表达式的符号也是一个容易出问题的地方。首先,有两个系统函数是来做带符号数和无符号数的转换的,$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
事实上赋值时会根据左值表达式的符号进行自动转换的,这两个系统函数对中间结果的类型转换有意义。下面是决定表达式符号类型的规则:

  • 表达式的类型只取决于操作数,与赋值的左值无关。
  • 不带基数表示的十进制数是带符号的。
  • 带基数表示的数字默认是无符号的,如果明确带了符号声明则是带符号的。
  • 位选择的结果是无符号的,不管操作数是否带符号。
  • 位段选择是无符号的,不管操作数是否带符号,哪怕位选择选择了操作数的所有位,也是无符号的。比如
    reg [15:0] a;
    reg signed [7:0] b;
    b=-4; //b = 8’sb1111_1100,补码表示
    a = b[7:0]; // 此时b[7:0]是无符号的,因此赋值时进行0扩展,a=16’b0000_0000_1111_1100
    a = b; //此时b是带符号的,因此赋值时进行符号位扩展, a=16’b1111_1111_1111_1100
  • 连接的结果是无符号的,不管各个操作数是否带符号。
  • 比较运算的结果(1, 0)是无符号的,不管各个操作数是否带符号
  • 实数转为整数的符号跟实数的符号一致
  • 对自决定符号,类型和位宽的操作数,符号和位宽由自己决定,与表达式的其他部分无关
  • 对自己无法决定的操作数,按照下面的规则决定:
    任何一个操作数是实数,运算结果是实数
    任何一个操作数是无符号的,结果是无符号的,不管运算符号是什么,比如-3’b110,结果也是无符号数3’b010,不过-3’sb110,结果3’sb010,如果此时用系统函数$unsigned或$signed转换,不会影响结果。
    如果每个操作数都是带符号的,则结果是带符号的,不管是什么操作符,除非是系统函数$unsigned。

进行表达式(或中间结果)运算时,应按照下面的步骤进行:

  • 先按照前面的规则决定表达式的位宽
  • 在按照前面的规则决定表达式的符号
  • 反过去决定不能自己决定宽度和符号的操作数,一般情况下,所有操作数被设置到同样的宽度和符号参与运算,有两个例外,一个是如果操作符的结果是实数,但是有操作数不是实数,此时操作数仍然被视为是自定义类型符号位宽的,只有在参与计算时才被视为实数。另一个是比较运算(包括相等/不相等比较),由于结果是1位无符号数,因此操作数的类型,位宽和符号不受其他操作数和运算符的影响。
  • 如果操作数是一个简单的操作数,操作数会被扩展到指定的类型和位宽,如果操作数需要扩展,则只有在反向的类型是带符号是才进行带符号扩展。这个意思是说,比如一个无符号数与一个位宽较小的有符号数相加,结果是无符号的,此时有符号数参与计算前按照无符号数参与,这样就进行无符号扩展,而不是先进行带符号扩展,然后作为无符号数参与运算。这点很绕啊,跟c语言中不一样了。
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,下面是模拟结果:
HDL4SE:软件工程师学习Verilog语言(六)_第1张图片
把a声明为reg signed [15:0] a;结果就是7了:
HDL4SE:软件工程师学习Verilog语言(六)_第2张图片

如果宽度都一样,结果也是预料中的。看来尽可能避免不同宽度的无符号数与带符号数直接运算,否则脑壳会被搞晕掉去,惹不起咱们躲着点。

6.2.3 表达式的编译

表达式按照运算符的优先级,计算有先后顺序,如果优先级一致,则规定从左到右计算。这里似乎有某种顺序执行的含义在里边,事实上,在c语言及其编译过程中,这会决定编译后的执行顺序。在verilog语言的编译中,可以消除这种运算的执行顺序。编译时会生成表达式的语法树,先计算的运算在子节点,越后计算的运算在离根越近,整个表达式的结果在根节点计算出来。我们将每个运算符编译为一个基本单元,表达式的运算顺序其实决定了基本单元之间的连接关系,并不意味这基本单元之间执行上有某种顺序,这样就消除了隐含的执行顺序问题,将隐含的执行顺序化为组合逻辑上的有向图中的上下游关系。
HDL4SE:软件工程师学习Verilog语言(六)_第3张图片
这样就避免不可综合的电路产生,一般的verilog表达式是可以用于RTL可综合电路描述的。如果其中出现函数调用,则应该保证函数实现过程是RTL可综合的。

6.3 赋值概述

写了半天表达式,如果不把表达式的值赋予都某个变量中,表达式就没有意义了,因此赋值也是计算机语言中的重要环节。像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)
上面左值的连接或者嵌套连接

6.3.1 持续赋值

持续性赋值在电路上是用表达式的值去驱动线网,有两种格式的赋值语句,一种是声明线网的同时赋值:
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。

6.3.2 程序性赋值概述

程序性赋值分为两种,一种所谓的阻塞赋值,一种是非阻塞赋值。阻塞赋值可以出现在声明的时候,比如:
reg [3:0] a = 4;
integer i = 0, j;
real ra = 3e2, rb=2.6 + 0.7;
此时右边只能出现常数表达式,不能出现编译时无法确定值的表达式。也可以在always语句和initial语句中出现,还能够在function和task中出现。
程序性赋值操作其实涉及的比较广,本节先开个头,下次介绍model中行为级描述的时候,再进行详细介绍。

6.4 进展报告

  1. 目前已经根据verilog的特点修改了大整数的接口,可以支持后面的运算操作,其中还有一些接口函数没有实现,比如除法,求余数,指数函数等。
  2. 软件前面已经实现了编译器的预处理器和词法分析
  3. 这次严格按照IEEE. 1364-2005 整理了verilog的语法,编写了能够被bison接受的YACC描述文件,目前功能部分还是空的,会慢慢增加内容,尽快达到能够实现汇编器的功能。
  4. 模拟器部分正在考虑升级总线的功能,满足将来RISC-V核的要求。目前这个总线结构还是简单了点。
  5. 调试器的波形文件待设计,波形查看器也有待设计,期待QT高手的加入,为我们设计一个基于QT的波形文件查看器。当然不排除用其他的GUI中间件来设计波形查看器,实在不行难道又要自己做一个?

【请参考】
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项目

你可能感兴趣的:(笔记,编程语言,verilog)