更新于 

硬件描述语言

硬件描述语言的起源

有名为HDL,是一种和C++,JAVA不同的硬件描述语言。他产生的原因是因为我们发现对于复杂逻辑的元件设计,使用卡诺图和组合逻辑电路很难实现,即使实现也会有许多细节上的纰漏,比如毛刺,延迟过大等问题,所以此时我们就需要学习硬件描述语言了。硬件描述语言和电子设计自动化工具搭配使用,可以轻松优化电路得到我们最满意的电路设计图。

我们首先要知道,HDL是具有特殊结构能够对硬件逻辑电路的功能和时序进行描述的一种高级编程语言,称之为硬件描述语言,他和我们之前学习的C++,JAVA等软件描述语言有以下的本质区别:

  • 信号连接:硬件描述语言一般是使用涉及循环,变量等语句描述门元件之间的连接形式的,而软件描述语言不会涉及到底层门元件的连接的
  • 功能时序:硬件描述语言反映了门元件的时序,延迟等信息,而软件描述语言不会考虑到这些问题
  • 抽象层次:可以在多种层次上进行建模设计等,而软件描述语言仅仅限制于软件算法层
  • 并行特性:和软件描述语言有很大不同,在HDL中,指令都是并行的,而不是串行的,因此指令的顺序并不会影响功能的实现

上图给出了两种不同的语言实现功能的方法,软件描述语言中是通过编译器将程序编译成01二进制代码来执行。而SystemVerilog(我们要掌握的一种硬件描述语言)编写的程序是通过综合器生成电路网表文件(即描述组合逻辑电路的文件)最终交给厂商去制造某一个元件。

我们前面提到过HDL可以在抽象层次上进行建模设计,如下图:

一个硬件描述语言可以在开关机,门级和寄存器传输级的任意一个层次上进行建模设计数字逻辑电路。而软件描述语言只能在算法级层次上进行设计。本次我们主要是学习在RTL寄存器传输级进行硬件描述语言应用的知识学习。

逻辑综合

在HDL中并不是所有语句都被综合形成描述逻辑电路的文件,只有一部分语句是被综合为逻辑电路的,称之为可综合HDL,而不能被综合的语句一般用于仿真验证(后面会讲到,实际上就是用来对设计的电路进行模拟测试的)。

一个门级网表的实现需要经过许多步骤,其中翻译,逻辑优化和实现与布局布线是非常重要的步骤,我们分别介绍:

  1. 翻译:翻译过程就是将RTL描述(即我们设计的语言程序)被EDA工具转换为一个未经优化的内部表示,不考虑目标工艺和设计约束。也就是仅仅将一个问题的解决办法生成为一个布尔表达式。
  2. 逻辑优化:取出冗余逻辑,使用大量与工艺无关的布尔逻辑优化技术,产生优化后的内部表示。即我们之前学到的卡诺图等优化方法将逻辑函数进行简化。
  3. 实现与布局布线:EDA工具接受内部表示,使用工艺库提供基本逻辑门进行实现,并根据设计约束进行优化。即使用代工厂(你可以理解为零件厂商)提供的门元件进行实现,这其中需要考虑成本,性能等问题来对设计进行约束优化(毕竟在实际电路实现中,并不是表达式越简越好)。

在实现与布局布线中我们发现有两个方面的内容,首先是工艺库,一般是由代工厂提供的门元件组成的标准单元库,我们在对设计进行实现和布局时要使用这些不同功能的门单元和宏单元(类似于一位加法器,乘法器等器件)。这些工艺元件由Foundry工厂提供,如:中兴国际(工艺在20nm左右),台积电(10nm左右),三星(10nm)和intel(7nm)。

注意英特尔虽然既设计电路,又提供实现电路的工艺元件,但是他只会为自己提供,即不为其他国家公司提供工艺库服务,所以虽然掌握最尖端的技术但是只是自己使用。

设计约束就是为了考虑整体电路性能(速度,工作频率),面积(成本)、功耗等外界因素对电路进行优化。

仿真验证

仿真验证实际上就是对我们设计的电路进行测试,以防出现漏洞或错误,但是我们不可能造出来这个门网表再进行测试,这样的话即使检测出来是有错误的也不能修改只能报废掉了,所以我们一般是使用虚拟仿真技术对我们设计的电路先进行测试。如下图:

  • 在特定的时间将激励信号送入待测模块(DUT)的输入端口
  • EDA工具根据DUT的逻辑功能模拟信号在电路中的运算和传输过程
  • 检查(人工或自动)输出相应以判断所涉及模块的功能是否正确
  • 最终输出的信号是右图中脉冲形式的信号图

SystemVerilog HDL程序的基本结构

一个硬件描述语言创建的模块结构如下图:

这是一个二选一电路的模块功能的设计,我们发现其实和软件描述语言的函数很类似。他有以下几个特点:

  1. SystemVerilog HDL程序的文件名通常以扩展名.sv结尾
  2. 代码中第一行和第八行通过"module…endmodule"定义了一个名字为mux2的逻辑电路模块,该模块一共具有3个输入端口(由input定义)和1个输出端口(由output定义)
  3. 代码第三行定义了两个logic类型的中间变量(内部信号)a和b,实际上中间变量就类似于函数的局部变量,只能模块内部使用,不能作为对外输出使用。
  4. 代码中4-6行定义了模块mux2的逻辑功能,他是实现某个元件功能的核心语句部分

模块

模块是SystemVerilog的基本建模单位,他用于描述逻辑电路的功能,一个模块可以包括整个系统或者一部分,他的定义关键词就是一module开始,以endmodule结束。模块声明的格式是

1
2
3
4
module 模块名 (端口1,端口2,...);


endmodule

注意没有大括号包裹,模块名是一个模块唯一的标识。

端口

端口是模块与外界进行通信的接口,他的定义需要指明四个要素(方向、类型、位宽、名字),其中类型和位宽可以缺省:

  • input [类型] [位宽] 端口名1,端口名2,端口名3,…
  • output [类型] [位宽] 端口名1,端口名2,端口名3,…
  • inout [类型] [位宽] 端口名1,端口名2,端口名3,…

input标识输入端口,output表示输出端口,inout表示双向端口。所有相同类型的端口都统一一起声明即可(都排列在一个关键字后面即可)。类型有logic等,位宽就是信号的位数,一般默认是1位,当然也可以声明为多位。端口的声明可以有多种方式,如下:

这三种都是正确的,也就是说在模块声明处可以将端口所有的属性全部声明完,也可以只声明一部分属性(但是无论如何端口名称必须在module声明语句中),然后再在内部对端口的方向,类型,位宽进行声明。

内部变量

上图中a和b都是内部变量,他们只在模块内部使用,主要是负责存储中间信号量,最终的输出信号需要若干个中间变量信号的逻辑组合来形成。

逻辑功能

逻辑功能的定义是一个模块中最核心的部分,明确了模块的行为和数据流动例如上面的4-6行,在SystemVerilog HDL中,定义模块的逻辑功能主要有两种建模(描述)风格:

  • 行为建模:描述输入和输出之间的因果关系(其中又分为基于持续赋值语句的建模和基于过程语句的建模)
  • 结构建模:调用其他已经定义过的模块对整个电路的功能进行描述。

在一个System HDL程序中,其代码模板如下:

要注意module语句后面是有分号的,而endmodule后面是不用分好的,其他中间的语句也是严格要求分号结尾的。

我们要注意声明部分必须写在逻辑功能定义部分的前面,而对于逻辑功能定一部分,由于HDL的并行性的特点,语句是并行的而不是串行的,因此语句之间的顺序并不会影响功能的实现,即下面的y定义语句也可以写在最前面:

但是为了便于我们理解,我们最好还是根据一定的逻辑顺序来构建代码。接下来我们再来学习以下HDL的语法要素。

HDL语法要素

间隔符和注释

即可以换行,他不会影响语句的实现,只是要注意语句的最后要加分号来表示一个语句的结束。一定的空格和换行使得代码风格优雅,同时注释规则和C一样,分为单行注释//和多行注释/*…*/。

标识符和关键字

接下来就是标识符和关键字,标识符用来给逻辑电路中的对象(如模块、输入和输出端口、中间变量)取名,规则和C一样,对字母大小写敏感。如:

关键字就是预留的表示特殊意义的字符串,用来定义语言的结构,通常是小写的,比如module,input,assign等。关键字不能作为标识符来使用。

逻辑值

在HDL中为了表示数字逻辑电路的逻辑状态,SystemVeilog规定了4种逻辑值,如下表:

逻辑值 电路的逻辑状态
0 逻辑零,逻辑假
1 逻辑1,逻辑真
x或X 不确定的值(未知状态)
z或Z 高阻态(浮空)

一般情况下,逻辑门的输出端口产生0或者1,对于三态门,在非连通状态下输出为高阻态Z,对于正常的运行的电路,逻辑X是不能存在的,必须在设计时就杜绝(例如一个节点连接多个输出端口导致的信号线同时驱动两个不同值的情况)。

常量

SystemVerilog HDL有三种常量:整数型常量、实数型常量和字符串型常量,其中整数型常量是可以综合的。

整数型常量的格式如下:

<+/><位宽><进制><数值><+/-><位宽><进制><数值>

其中除了数值,其他项都可以缺省。<+/->表示正负,<位宽>用10进制数来描述常量对应的二进制数的宽度,<进制>定义了整数型常量的进制格式,可以是二进制(用b或者B表示)、八进制表示(用o或O表示)、十进制数(用d或D表示),十六进制(用h或者H表示),数值表示具体的取值,EDA工具按照无符号数进行处理,因此如果是二进制格式,可以是0,1,x和z,如果是十六进制格式,那么A-F不区分大小写。

如果常量带有负号,那么EDA工具按照有符号数的补码进行处理。

上图给出了部分常量的表示方法,一定要注意位宽一定是二进制表示的位数,而负数常量是要按照有符号数补码的格式在EDA中表示。求补码的方法见《机组原理》

为了增加数字的可读性,可以在数字之间增加下划线(类似于银行显示余额时使用的逗号),比如8’b1001_0011就是表示的位宽为8位的二进制常量数10010011。

并且如果没有给出位宽,那么该常量被指定为当前表达式的位数当位宽比数值的实际二进制位数少时,高位部分被舍去即截取时永远从低位向高位截取,当位宽比数值的实际二进制位数多时,那么高位补0。比如assign w=b1101(没有声明位宽),那么如果w是2位,w=2’b01即只要低2位,当w时6位,那么w=6’b001101即高位补0,只有当w=4时,w=4’b1101。

注意低位截断时数值会发生变化可能不等于右侧表达式的值了,但是当位宽大于等于右侧表达式的长度时,那么数值并不会发生变化。

如果没有给出进制,那么默认是10进制,并且此时常量EDA工具将其作为有符号数,使用补码来处理。比如10=(01010)_补,-15=(10001)_补。

我们需要注意,某一个常量在电路中就是一个由0/1组成的二进制串,所以在电路层面并不会区分该常量是有符号数还是无符号数,或者是原码还是补码,这些信息都是由设计者或软件(EDA工具)负责解释的。比如:

数据类型

除logic外的其他变量类型,都属于二态类型(即只有0/1),如下所示:

  • bit:定义1位信号
  • byte:定义8位信号(1个字节),类似于C/C++的字符型
  • shortint:用于定义16位信号,类似于C/C++的short类型
  • int:定义32位信号(即4个字节),类似于C/C++的int类型

bit和logic很像,标量信号和向量信号均可以定义,并且支持域选,但是bit类型只有0/1两种取值:

1
2
3
4
5
6
7
8
//定义了两个16位的向量信号,每位只能是0或1
//注意是16-0位,其实可以类比于数组
bit [15:0] addrbus,databus;
//定义了两个1位的标量信号,每位只能是0或1
bit a,b;
//域选就是截取,这里可以域选bit的15-7位
//所以gugu向量信号的数值用addrbus的15-7位填充
bit [8:0] gugu=addrbus[15:7];

byte、shortint、int都具有预定义向量宽度(即固定的位宽),所以定义变量时不能使用位宽,也不支持域选。

1
2
3
4
5
//正确,定义了两个32位的向量信号,每位只能是0或者1
int addrbus,databus;
//错误,不能使用位宽
//int向量信号只能是32位
int [16:0] addrbus;

SystemVerilog HDL中的线网类型主要包括:wire,tri等。wire类型只能定义单源驱动信号,因为常使用logic来代替wire,在SystemVerilog中wire类型已经被弃用(但是端口信号默认为wire类型)。tri类型可以用于定义多源驱动信号,例如:

1
2
3
module tristate(input logic a,input logic en,output tri y);
assign y=en?A:1'bz;
endmodule
思考:单源驱动信号和多源驱动信号的区别?

单源驱动信号就是这个信号由一个输入信号来驱动,最常见的就是X->Y,因此wire信号完全可以用logic信号来代替,而多源驱动信号就是一个输出信号由多个输入信号决定,类似于{X1,X2}->Y。这种信号不同于前面讲到的常量信号,他可能会根据不同的情况变换,所以是多源线网型信号,需要使用tri来定义,很显然三态缓冲器和译码器等输出信号就是使用多源驱动信号。

tri类型信号的定义格式和logic几乎是一样的,支持域选和自定义位宽:

1
2
3
4
//定义了两个16位的tri类型向量信号
tri[15:0] addrbus,databus;
//支持域选
out[3:0]=addrbus[7:4]

运算符

先给出所有的运算符的含义和优先级。然后我们讲解几种细节问题:

当两个位数不同的操作数进行单符号位运算时表示的是按位进行处理,位数少的操作数需要进行零扩展到相同位数,比如:

a=4’b1011

b=8’b01010011

那么c=a|b时,a需要先零扩展为8’b00001011

我们之前学过这种按位的操作总是需要两个操作数,比如a&b,a^b等,但是在SystemVerilog HDL中也可以是单操作数自己进行这种运算,即为缩减运算,他的特点就是将操作数的各个位之间进行或,与等操作,最终一个很长位数的操作数就会变成一位数,比如:

1
2
3
4
5
module and8(intput logic [7:0]a,output logic y);
assign y=&a;
//那么就等同于下面的操作
//assign y=a[7]&a[6]&a[5]&a[4]&a[3]&a[2]&a[1]&a[0];
endmodule

假设a是10000000,那么最终的y输出信号就是0,如果a是11111111,那么y就是1。类似的还有|a等。

而当使用的是多符号是那么是将两个数的真值进行处理,并且所有非零的真值数都是按照1处理,零真值是0。比如:

在SystemVerilog HDL中还会经常使用到算术运算,算术运算包括加减乘除,其中除和取模是不可综合的。并且当两个位数不同的操作数进行算术运算时,如果操作数是无符号数,那么位数少的进行零扩展即可,如果操作数是有符号数,咋位数少的操作数需要进行位数扩展到相同位数。比如:

移位运算实际上和计算机系统基础中的移位操作相同,规则:

  • 逻辑左移:将操作数无符号数左移若干位,右侧产生的空余位使用0填充
  • 逻辑右移:将操作数无符号数右移若干位,左侧产生的空余位使用0填充
  • 算术左移:将操作数有符号数左移若干位,右侧产生的空余位使用0填充
  • 算术右移:将操作数有符号数右移若干位,左侧产生的空余位使用符号位填充

关系运算就是比较大小然后返还0/1(假/真)。注意如果表达式中有一个操作数为无符号数,那么表达式的其余操作数均会被当做无符号数进行处理:

1
2
3
4
5
6
//-3和5都是有符号数(前面讲过10进制数默认是按有符号数处理)
(-3*5)<10
//4'd5是无符号数,所以-3的二进制串也被当为无符号数处理
//(-3)_补=(1101)_补,那么作为无符号数被处理为13
//13*5=65>10
(-3*4'd5)<10
再次强调,在SystemVerilog

HDL中所有的数最终都是在硬件上进行二进制运算,所以这里的-3,5等都只是一个符号,他不能说明值是多少,最终数值都是取决于他们的二进制01串被处理后得到的真值。还有就是在HDL中没有true和false布尔值,都是用1来代表真,0代表假。

当然HDL中也存在条件运算–三目运算。和C的一样:

1
2
3
module mux2(intput logic [3:0]d0,d1,intput logic s,output logic [3:0] y);
assign y=s?d1:d0;
endmodule

这里再介绍一下特有的位混合(拼接)和复制运算,说来也简单,位混合运算就把多个信号的某些位拼接起来形成新的信号。格式为:

{信号1[n1:m1],信号2[n2:m2],...,信号n[nn:mn]}\{信号1[n_1:m_1],信号2[n_2:m_2],...,信号n[n_n:m_n]\}

位混合运算中的每一个操作数必须是确定位宽的数,不允许出现未指定位宽的常数。如果要多次拼接同一个操作数可以使用复制运算,格式为

{n{A}}\{n\{A\}\}

我们来看几个例子:

小练习