type
status
date
slug
summary
tags
category
icon
password
3.1 Verilog HDL 概述
3.1.1 Verilog HDL简介
- Verilog简介:Verilog HDL是一种硬件语言,最终是为了产生实际的硬件电路或对硬件电路进行仿真
需要注意的是,虽然看着Verilog好像是一个编程语言,但是实际上它就对应着实际的硬件电路。所以需要写Verilog的时候要摈弃原来编程语言串行执行的思路,因为硬件电路在物理上时并行工作的,并且电路行为的先后顺序通过时钟节拍顺序来体现。
由于电路都是并行工作的,所以在Verilog中所有描述语句(包括连续赋值语句assign、行为语句块always/initial、模块实例化)都是并发执行的
3.1.1.1 Verilog的多种描述方式
Verilog提供了多种描述方式:
- 开关级描述方式(开关级建模):能够使用内置开关级原语对设计完整建模;开关级基本结构模型:内置pmos、nmos等
- 门级描述方式(门级建模):能够使用内置门级原语对设计完整建模;门级基本结构模型:内置and、or、nand等
- 数据流级描述方式(数据流级建模):能够使用内置数据流级原语assign和位运算符对设计完整建模;位运算符有:&、︳、~、 ˆ、~ˆ等。
所以在使用数据流级描述的时候,需要特别注意不能用到其他原语或者符号了
- 行为级描述方式(行为级建模)
能够使用结构和算法对设计完整建模;常用语句有:initial(只执行一次),always (循环执行);还提供一些高级语言结构,如if语句、case语句、循环语句等。
需要注意的是,虽然initial只执行一次,但是他还是会和always并行执行。
感觉循环语句好像都没有用到过。
3.2 Verilog HDL 基本语法
3.2.1 标识符
Verilog HDL中的标识符是由任意的字母、数字、$符和下划线组成的字符序列。
相较于高级程序设计语言而言,多了一个$符
但是标识符的第一个字符必须是字母或者下划线,此外,标识符是区分大小写的。例如,count和COUNT是不同的。
此外,由于Verilog中定义了一系列的关键字,自定义的标识符不能与这些关键字相同。
由于标识符是区分大小写的,所以ALWAYS与always是不同的,因此ALWAYS不是关键字
3.2.2 数值、常量、数据类型与变量
3.2.2.1 数值
Verilog HDL的数值有四类:
- 0:逻辑0
- 1:逻辑1
- Z/z:高阻
这里的高阻就是指三态门中的高阻态,也就是相当于断路
- X/x:未知
Verilog中的未知不是使用d来表示的
3.2.2.2 常量
我感觉这个叫做字面量更合适一点
- 整数型常量
- 定义格式:<位宽><’[s或S]进制><数字>
- 一些例子:3’b001、’b001
- 注意点
- x或z在十六进制数值中代表4位二进制x或z,在八进制数值中代表3位二进制x或z,在二进制数值中代表1位x或z。
- 若定义的位宽比常量指定的位宽大,对于无符号数则在数的左边填0补齐,对于有符号数则在左边填符号位补齐
- 若定义的位宽比常量指定的位宽小,则最左边多余的位将被截断。
- 数字部分并不代表一个整数型常量的真实值,其只是这个整数型常量的二进制编码
实际上在Verilog中应该是有两种整数表示方法的:十进制数格式与基数格式。十进制数格式实际上就是使用我们常用的十进制有符号(+-号来进行区分)数来表示Verilog中的整数,并且都将使用补码进行存储。
但是PPT上仅仅介绍了基数格式,因此在写Verilog的时候就全部都使用基数格式吧,不要管十进制数格式了。
位宽部分表示的是该常量用二进制表示的位数。并且位宽不能使用类似于“2+3”的表达式进行表示。
进制部分可取的值为:o(O)表示八进制;b(B)表示二进制;d(D)表示十进制;h(H)表示十六进制。这些进制跟其他编程语言中的几乎是一样的,唯一需要注意的是十六进制是H,而不是X。
另外,在进制部分最开始可以使用s(S)字母以表示当前整数为有符号数。
是否为负数将由进制部分的“s”以及整数型常量对应的二进制数(该二进制数为实际对应的十进制数的补码表示)决定,而不是直接在数字部分加上“-”,这是非法的。
这里的’b001比较特殊。这里没有指明位宽,因此将使用默认位宽(默认位宽通常由机器决定,但是最少为32位),而不是根据后面整数型常量的二进制位数自动推断为3。
但如果数最左边一位是x或z,则相应地在左边填x或z补齐
如果最左边一位是x或z,那么就不需要考虑这个数是有符号数还是无符号数了。
因为可能是有符号数,最后还需要做补码运算。
- 实数型(小数)常量:由十进制计数法或科学计数法的形式组成。
- 字符串型常量
- \n:表示换行符
- \t:表示制表符
- \\:表示反斜杠(\)本身
- \”:表示双引号字符(”)
由双引号内的字符序列组成,用一串8位二进制ASCII码的形式表示,每一个8位二进制ASCII码表示一个字符。例如:”hello”、”abc”
在Verilog中同样有转义字符:
3.2.3 数据类型及变量
3.2.3.1 数据类型
- 线网类型:线网表示Verilog结构化元件间的物理连线。它的值由驱动元件的值决定。当没有驱动元件连接到线网时,线网的缺省值将为Z(高阻)
- 常用的线网类型:Verilog中最常用的线网类型是wire。Verilog HDL的输入输出信号在缺省情况时将被自动定义为wire型。
- 线网类型的定义格式:wire [n:0] 变量名1,变量名2,…,变量名n
在Verilog中,线网类型如果不是输入的话(实际上线网类型为输入时,其值由我们外部操作控制),即我们自己定义的线网类型的变量,如果没有使用assign(或门输出)的对其进行赋值的话,该线网类型的变量输出就将是Z(因为线网没有被连接)
另外,可以稍微了解一下assign语句。assign语句又被称为是连续赋值语句,在电路上就好像是信号从一个线网传递到了另一个线网上一样,因此通常情况下,assign语句只能用于为线网类型赋值。而assign语句又不能被使用在过程语句(initial、always)中,这样就导致在使用Verilog的时候,线网类型的变量通常是不能在过程块中赋值的
在wire后面的[n:0]缺省的情况下,默认是声明了若干个一位的线网类型变量
- 寄存器类型:寄存器类型表示一个抽象的数据存储单元,它只能在always语句和initial语句中被赋值,并且它的值从一个赋值到另一个赋值被保存下来。寄存器类型的变量的缺省值为X(未知)
- 常用的寄存器类型:Verilog中最常用的是寄存器类型是reg。
- 寄存器类型变量的定义格式:reg [n:0] 变量名1,变量名2,……..变量名n
缺省值为未知这是因为寄存器中总是会存储一个值的,只不过这个值在使用之前可能不知道是多少,也可能是无效的,因此寄存器变量的缺省值为X。
寄存器类型变量与线网型变量的根本区别是:寄存器型变量只能在过程语句中被明确赋值,并且在被重新赋值前一直保持原值。
同样如果未明确指定寄存器类型的变量的位数,那么默认就是声明了若干个一位的寄存器变量。
3.2.4 数组
这个应该没啥用。。。
3.2.4.1 数组的声明
需要注意的是,wire [7:0] y[5:0]与wire y[5:0][7:0]本质上是一样的,都是表示6个8位线网类型,但是通常情况下一种定义方式使用的比较多
3.2.4.2 数组的赋值
3.2.4.3 数组的访问
3.3 Verilog HDL 的操作符
3.3.1 算术操作符
Verilog支持6中算术运算符:加法(+)、减法(-)、乘法(*)、除法(/)、取模(%)和幂运算(**)
- 算术运算的注意点
- 整数除法(/)将截断结果的小数部分,例如:7/4的运算结果为1。
- 取模(%)操作符求出与第一个操作符符号相同的余数,例如:7%4的运算结果为3,-7%4的运算结果为-3。
- 算术操作符中任意操作数中只要有一位为x或z,则整个运算结果为X
例如:’b10x1+’b01111的运算结果为不确定数’bxxxxx。
3.3.1.1 算术运算结果的位宽
算术表达式运算结果的位宽由最大操作数的位宽决定
这里说的是,两个整数型常量的算术运算的结果的位宽由最大操作数的位宽决定(即只是赋值语句右端的运算结果,还没有到赋值的过程)
在赋值语句中,算术运算结果的位宽也由赋值等号左端目标变量的位宽决定。
所以在算术运算的过程中,右侧算出来的值可能位宽较大,但是可能由于左端位宽不足,导致位数被截断,所以表现出来就是“算术运算结果的位宽也由赋值等号左端目标变量的位宽决定”。
这里给出一个例子:
摆这个例子的主要原因还是说,在Verilog中,变量也是可以进行运算的
3.3.1.2 有符号数和无符号数
- 有符号数与无符号数的存储
- 有符号数被存储在十进制形式的整数、有符号的线网、有符号的寄存器变量或用s(有符号)标记的基数格式表示的整型数中。
- 无符号数被存储在线网、寄存器变量或用普通(没有符号标记s)的基数格式表示的整型数中。
- $signed与$unsigned:在Verilog HDL中有两个系统函数$signed和$unsigned可以进行有符号形式和无符号形式之间的转换。
如:
上面的式子结果为-3是因为,进行有符号数和无符号数的转换的时候,实际上就是直接告诉机器应该怎么解释这个二进制串,这并不会改变二进制串的值。因此,由于1101为-3的补码表示,因此这个值为-3。
3.3.2 关系操作符
Verilog HDL支持4种关系操作符:大于(>)、小于(<)、大于等于(>=)、小于等于(<=)。
3.3.2.1 关系操作符的注意点
- 关系操作符对两个操作数逐位进行比较,其结果为真(值为1)或假(值为0)
- 若操作数中有一位为x或z,则结果为x
- 若操作数的位宽不同,如果所有操作数都是无符号数,则位宽较小的操作数需在高位填0补齐;如果所有操作数都是有符号数,则位宽较小的操作数需在高位填符号位补齐。
实际上就是按照位宽拓展的规则进行位宽对齐。
- 若表达式中有一个操作数是无符号数,则该表达式的其余操作数都被当作无符号数处理。
这就解决了上一个注意点中可能存在的问题:要是一个数是有符号数,一个数是无符号数怎么办。
而实际上,在任意一个表达式中都是这样的。
一个例子:
3.3.3 等价操作符
Verilog HDL支持4种等价操作符:逻辑相等(= =)、逻辑不等(!=)、全等(= = =)、非全等(! = =)
3.3.3.1 等价操作符的注意点
- 等价操作符:对两个操作数逐位进行比较,若操作数中有一位为x或z,则结果为x。
- 全等操作符:对于全等(= = =)和非全等(! = =),则是将x或z当作数值,严格地按字符值进行比较的,因此其结果不是1就是0,没有未知的情况。
这里给出一个例子:
- 当位宽不同时,同样也是按照位宽扩展的规则进行扩展。如果表达式中有一个无符号数,那么所有的数值都将被转换为无符号数
3.3.4 位操作符
Verilog HDL支持5种位操作符:取反(~)、与(&)、或(|)、异或(^)和同或(^~或~^)
主要是记一下异或和同或的符号吧
3.3.4.1 位操作符的注意点
- 位操作符,顾名思义就是对输入的操作数进行逐位操作
对有符号数而言,进行位运算的时候也需要包含符号位。
- 当位宽不同时,同样将按照位宽扩展的原则进行位扩展。如果表达式中存在无符号数的话,那么其余数都将被视为无符号数。
- 只有当进行位运算的时候由于x或z的存在无法确定结果时,输出的对应位才为x。
这里举个例子:0&x=0,这是因为结果能确定为0;而1&x=x,这是因为结果无法确定。
3.3.5 逻辑操作符
Verilog HDL支持3种逻辑操作符:逻辑与(&&)、逻辑或(||)和逻辑非(!)
3.3.5.1 逻辑操作符的注意点
- 如果操作数中没有x或z,则逻辑操作的结果是1位宽的布尔值(0或1);如果操作数中有x或z,则逻辑操作的结果是1位宽的x。
- 逻辑操作符和位操作符不同。逻辑操作符用于连接布尔表达式,而位操作符用于电路信号的连接
因此在做assign啥的时候应该使用的是位操作符,而在if等分支语句中就应该使用逻辑运算符了
3.3.6 缩减操作符
Verilog HDL支持6种缩减操作符:缩减与(&)、缩减与非(~ &)、缩减或(|)、缩减或非(~ |)、缩减异或(^)、缩减同或(~ ^或^ ~)。
可能会认为缩减操作符的符号都跟位运算符是一样的,会不会混淆?实际上是不会的,因为缩减操作符是一个单目运算。
3.3.6.1 缩减运算的含义
缩减操作符对单个操作数上的所有位进行操作,产生1位的操作结果。
3.3.6.2 缩减运算
关于缩减与、缩减或、缩减异或这里就不再介绍了,实际上就是对一个数中所有的位做按位操作。唯一一个需要注意的点就是,操作数中只要存在x或者z,那么操作结果就是x。
另外这里还需要明确一个运算:~x=x,~z=x
- 缩减与非:对缩减与的操作结果求反便可以得到缩减与非的操作结果
- 缩减或非:对缩减或的操作结果求反便可以得到缩减或非的操作结果
- 缩减同或:对缩减异或的操作结果求反便可以得到缩减同或的操作结果
需要特别注意的是,这里的缩减同或应该是不能通过按位同或得到的。因为正常的异或和同或在奇数个变量的时候,结果是相同的;而在缩减运算中,缩减同或的结果始终与缩减异或相反。
3.3.7 移位操作符
Verilog HDL支持4种移位操作符:逻辑左移(<<)、逻辑右移(>>)、算术左移(<<<)、算术右移(>>>)
符号需要稍微记一下
3.3.7.1 移位操作符的注意点
- 移位操作符将位于操作符左侧的操作数向左或右移位,移位的次数由右侧的操作数决定,右侧的操作数总被认为是一个无符号数。若右侧的操作数为x或z,则移位操作的结果必定为x。
右侧数通常使用十进制表示直接给出
- 对逻辑移位操作符,由于移位而腾空的位总是填0
对于算术移位操作符,左移腾空的位总是填0;而在右移中,如果位于操作符左侧的操作数是无符号数,则腾空的位总是填0,如果位于操作符左侧的操作数是有符号数,则腾空的位总是填符号位。
这里的移位就是跟计组中的移位指令的结果是一样的。并且由于移出移位必然补充一位,因此移位操作是不会改变数的位宽的
3.3.8 条件操作符
条件操作符根据条件表达式的值从两个表达式中选择一个表达式。
- 条件操作符语句格式:
条件表达式?表达式1:表达式2;
若条件表达式为真,则返回表达式1,反之则返回表达式2
实际上就跟一般的编程语言中的三目运算符是一样的。
3.3.9 拼接和复制操作符
拼接操作符的用法是将各个操作数用花括号扩起来,每个操作数之间用逗号隔开,然后将这些操作数组成一个操作数。
3.3.9.1 拼接操作符的注意点
- 操作数类型可以是线网类型、寄存器类型,甚至是基数格式的整数型常量
- 拼接操作符的每个操作数必须有确定的位宽
如果是线网类型或寄存器变量,一般都是有确定的位宽的(在默认情况下也是声明一个一位的线网类型或寄存器类型)。而对于基数格式而言,就需要明确指出基数的位数了
拼接复制的操作结果的位数是自动推断的
- 如果需要多次重复拼接同一个操作数,可以使用常数表示需要重复拼接的次数。
通常会在需要赋值的操作数上再加上一层花括号。并且这里所说的常数,通常就是直接十进制表示的整数
3.4 Verilog HDL 的基本结构
3.4.1 Verilog-HDL语法基础
- 注释要用“/”与“/”,或在注释前用“//”。
- 标识符要规范命名。
- module与endmodule相呼应,成对出现
- input定义输入信号变量;output定义输出信号变量
- 在门级描述中,调用Verilog-HDL具有的内置门实例语句,描述顺序为“(输出,输入1,输入2········);”的形式
- 在数据流描述方式中,以保留字assign和位运算符来描述逻辑表达式
或者说数据流级描述通常只会使用位操作符和assign连续赋值语句实现(上课还说了可以使用三目运算符)
但是需要注意的是,并不是说在行为级描述中就不能使用assign语句了。在行为级描述中,不在过程块内仍然可以使用assign语句对线网类型进行赋值。
- endmodule后没有“;”(module行有“;”)
这里的内容应该都只需要稍微看看就好了,Verilog写多了就好了
3.4.2 模块的定义
这里给出一个Verilog模块的模板:
Verilog中有三种基本级别:门级、数据流级和行为级
这里少了一个开关级,但是开关级肯定不会考
3.4.2.1 门级描述
在Verilog-HDL中,属于内置门实例语句的有:and(与)、nand(与非)、or(或)、nor(或非)、xor(异或)、xnor(同或)、buf(缓冲器)
这些内置门都是小写的。并且需要使用调用模块的方式来调用这些内置门
这里举一个例子:二输入与门
需要注意的是,在上机实验中,会将input和output写在模块的参数列表中。但是PPT上都是在module内重新定义的,这里还是以ppt为准
3.4.2.2 数据流级描述
同样描述一个二输入与门,但使用数据流级描述:
再次提醒,在数据流级描述中,通常是只能使用连续赋值语句assign和位运算符的
复习一下五种位运算符:与(&),或(|),非(~),异或(^),同或(~^或^~)。
这些位运算符的优先级为:~、&、ˆ、~ˆ、|
由于行为级比较重要,所以后面单独开一节讲
3.5 Verilog HDL 的行为级描述
Verilog HDL的行为级描述可以使用过程块结构进行描述,过程块结构有initial过程块和always过程块
只有寄存器型数据能够在这两种语句中被赋值(线网类型不能在过程块中赋值)。在前面还提到了寄存器变量只能在过程块中赋值,这样寄存器变量的赋值相当于就和过程块相互绑定了。
此外,所有的initial过程块和always过程块(还有assign连续赋值语句)在0时刻并发执行
3.5.1 赋值操作
- 连续赋值:用于对线网(nets)的赋值
我感觉这个连续赋值的含义就是:如果右侧表达式改变了,那么左侧的值将立马改变,就是已经使用物理意义上的线网进行连接了
可以是组合逻辑,不需要使用逻辑门,直接使用逻辑表达式驱动线网。
- 过程赋值:即把值放在寄存器中,过程赋值没有持续时间,相反,寄存器将保持赋值的值,直到发生下一次对变量的赋值。过程赋值发生在过程块(always,initial)中,可以把它认为是触发器赋值。当执行到达过程块的赋值时,触发就发生。
总结一下,就是线网类型的变量只能使用assign进行赋值,并且不能使用在过程块中;而寄存器类型的变量只能在过程块中赋值;
那么就是在使用数据流级进行描述的时候,就只能使用位操作符和assign连续赋值语句;但是在使用行为级进行描述时,既可以使用过程块和过程性赋值语句,也可以在过程块外使用assign连续赋值语句。
另外有一个比较特殊的代码:
在上面拼接操作的时候,我认为这种写法是错误的,但是现在知道这种语句是默认使用initial语句的。
3.5.2 过程块结构的定义
3.5.2.1 initial过程块
initial过程块只会在0时刻开始执行,并且只会执行一次
- initial过程块格式:
3.5.2.2 always过程块
always过程块会在0时刻开始无限循环,反复执行。
- always过程块格式:
敏感事件表中可以填写通配符“*”以表示对所有事件敏感。并且敏感事件表中如果有多个敏感事件,就需要使用or关键字进行连接。
需要注意的是,在格式上,initial和always关键字后面是没有分号的;而在执行时间上initial过程块和always过程块都是在0时刻开始并发执行的(assign连续赋值语句也在并发执行)
3.5.3 顺序语句
顺序结构可以采用begin-end对来实现。在顺序块中出现的语句都是过程性赋值语句。
可以理解为:begin-end就是程序设计语言中语句块的花括号。因此当always、initial、分支语句中所包含的语句超过一句的时候,就需要使用begin-end将语句包括起来了。
3.5.3.1 过程性赋值语句的类型
- 阻塞过程性赋值:用“=”,赋值是按照顺序执行的,在其后所有的语句执行前执行,即在下一条语句执行前该赋值语句必须已全部执行完毕。
- 非阻塞过程性赋值:用“<=”,赋值安排在未来时刻,然后继续执行,即并不等到前式赋值完成后才执行下一句
换句话说,阻塞是同步的,而非阻塞是异步的?
这里给出一个例子:
从上面这个例子就能看出,阻塞赋值一定要等到前面的赋值语句执行结束,因此如果使用绝对时间规定赋值语句的执行时间就不一定有意义了(因为绝对时间并不一定能被满足),因此是使用相对时间来指定赋值语句执行的时间;而非阻塞赋值由于赋值语句之间没有同步的要求,因此会直接使用绝对时间来指定赋值语句的执行时间。
3.5.4 分支结构
3.5.4.1 if_else语句
- if_else语句基本格式
这里的条件表达式需要进行连接的时候,需要使用逻辑操作符(与&&、或||、非!,与程序设计语言中的运算符是相同的),而不是位操作符(与&、或|、非~、异或^、同或~^/^~)。
在格式上还需要注意的是,if关键字和后面的条件表达式之间有一个空格。
- if_else语句的一个使用示例
3.5.4.2 case语句
- case语句基本格式
格式上几个需要注意的点:每一个分支项后面都需要加上“:”,并且在case语句结束之后,需要加上endcase。另外,在Verilog中的case语句中不需要使用break关键字(实际上Verilog中好像也没有break关键字)
另外格式上的一个注意点:case和后面的控制表达式之间也是有一个空格的
- case语句的一个示例
有一个格式上的注意点:单独一行的关键字后面通常是不加分号的,比如initial、always、begin、end、case、endcase、if、else if(上面的if和else if后面会加上分号是因为将语句上提了,那个分号是语句的分号)。
3.5.5 循环结构
循环语句不考察
需要注意的是,无论是顺序结构、分支结构还是循环结构,都只能在行为级描述中使用。换句话说,就是begin、end、if、else、case、endcase等关键字只能在行为级描述中出现.
另外,顺序结构、分支结构、循环结构都是需要在过程块中使用的。
Q
看一下Verilog最后一个实验中的数组拷贝是怎么做的?
有符号的线网和寄存器是什么样的?
拼接复制操作可以用于有符号数吗?拼接复制操作的结果一定是一个无符号数吗?
感觉copilot跟我自己想的是差不多的,就是拼接操作是不会管有符号数的符号位的,而是仅仅简单地将这些二进制串拼接在一起
- 作者:Noah
- 链接:https://imnoah.top/article/DigLogic/Chapter3
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。