时间:2024.11.9
一、学习内容
1.Verilog HDL简介
1.1语言简介
Verilog HDL是一种硬件描述语言,以文本的形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。
1.2操作思路
确定目标功能-->用Verilog HDL语言通过代码的编写对目标的逻辑功能进行描述-->使用Quartus软件对Verilog HDL语言编写的代码进行分析综合、布局布线等一系列操作-->将生成的网表文件下载到FPGA开发板中-->在FPGA中生成一个实际的硬件电路,硬件电路实现的就是我们的目标功能
1.3常用的硬件描述语言
一种是 Verilog HDL(以下简称 Verilog),另一种是 VHDL
两种语言的对比
VHDL 与 Verilog 相比,有以下优势:
语法比 Verilog 严谨,通过 EDA 工具自动语法检查,易排除许多设计中的疏忽。有很好的行为级描述能力和一定的系统级描述能力,而Verilog 建模时,行为与系统级抽象及相关描述能力不及 VHDL。
VHDL 与 Verilog 相比,有以下不足之处:
VHDL 代码较冗长,在相同逻辑功能描述时,Verilog 的代码比 VHDL 少许多。VHDL 对数据类型匹配要求过于严格,初学时会感到不是很方便,编程耗时也较多;而 Verilog 支持自动类型转换,初学者容易入门。VHDL 对版图级、管子级这些较为底层的描述级别几乎不支持,无法直接作集成电路底层建模。
1.4Verilog HDL 和 C 语言的比较
Verilog 在很多语法上都和 C 语言极其相似,甚至有些语法是通用的,这也是 Verilog 语言容易上手的一个很重要的原因。Verilog 语言本身就是从 C 语言继承并发展而来的,但是它主要用于描述硬件,和 C 语言这种软件语言思想完全不同。
C 语言所描述的代码功能在执行时都是一行一行顺序执行的(顺序执行),而 Verilog 语言在设计完成后执行时则是并行执行的,C 语言所描述的代码功能并不会真实的映射成最后的硬件,只是对内存的操作和进行数据的搬移,而用Verilog 语言所描述的代码功能则会真真正正的生成所对应 的硬件电路,所以这也是Verilog 语言被称为“硬件描述语言”的原因,C 语言和 Verilog 语言之间的关系就是软件和硬件之间的关系,所以不要混为一谈。
可以通过 C 语言的语法基础来辅助学习Verilog 语法,但是切不可生搬硬套,特别在代码的风格和理解上一定要区别对待。
2.Verilog HDL基础语法
2.1 标识符
标识符用于定义常数、变量、信号、端口、子模块或参数名称。Verilog 语言是区分大小写的,也就是说同一个名称,用大写和用小写就代表了两个不同的符号,这一点与VHDL 不同,因此书写的时候要格外注意。
在 Verilog 语言中,所有的关键字(又叫保留字)都为小写。完整的 Verilog 关键字在编辑器会以高亮的形式突出出来。Verilog 的内部信号名(又称标识符)使用大写和小写都可以。标识符可以是字母、数字、$(美元符号)和下划线的任意组合,只要第一个字符是字母或者下划线即可。
2.2 逻辑值
在二进制计数中,单比特逻辑值只有“0”和“1”两种状态,而在 Verilog 语言中,为了对电路了进行精确的建模,又增加了两种逻辑状态,即“X”和“Z”。
当“X”用作信号状态时表示未知,当用作条件判断时(在 casex 或 casez)表示不关心;“Z”表示高阻状态,也就是没有任何驱动,通常用来对三态总线进行建模。在综合工具眼中,或者说在实际实现的电路中,并没有什么 X 值,只存在 0、1 和 Z 三种状态。
在实际电路中还可能出现亚稳态,它既不是 0,也不是 1,而是一种不稳定的状态。
Verilog 语言中的所有数据都是由以上描述的 4 种基本逻辑值“0”、“1”、“X”和“Z”构成的,同时,“X”和“Z”是不区分大小写的,例如 0z1x 和 0Z1X 表示同一个数据。
2.3 常量
1.常量的类型
常量是 Verilog 中不变的数值,Verilog 中的常量有三种类型
(1) 整数型;
(2) 实数型;
(3) 字符串型。
2.整数型常量的表示
我们可以使用简单的十进制表示一个整数型常量,例如:
(1) 直接写 16 表示位宽为 32bit 的十进制数 16;
(2) -15 表示十进制的-15,用二进制补码表示至少需要 5bit,即 1_0001,最高一位为符
号位;如果用 6bit 表示,则为 11_0001,同样最高一位为符号位。
3.基数表示法表示整数型常量
整数型常量也可以采用基数表示法表示,这种写法清晰明了,所以更推荐这种表示方
法,例如:
(1) 8’hab 表示 8bit 的十六进制数,换算成二进制是 1010_1011;
(2) 8’d171 表示 8bit 的十进制数,换算成二进制是 1010_1011;
(3) 8’o253 表示 8bit 的八进制数,换算成二进制是 1010_1011;
(4) 8’b1010_1011 表示 8bit 的二进制数,二进制就是 1010_1011。
虽然上面的表示方式不同,但都表示的是相同的值,数值经过运算后的结果也都相同。
4. 基数表示法的基本格式
(1)格式: [换算为二进制后位宽的总长度][’][数值进制符号][与数值进制符号对应的数值]
其中[位宽的总长度]可有可无,[数值进制符号]中如果是[h]则表示十六进制,如果是[o]则表示八进制,如果是[b]则表示二进制,如果[d]则表示十进制。当[换算为二进制后位宽的总长度]比[与数值进制符号对应的数值]的实际位数多,则自动在[与数值进制符号对应的数值]的左边补足 0,如果位数少,则自动截断[与数值进制符号对应的数值]左边超出的位数。
(2) 如果将数字写成“’haa”,那么这个十六进制数的[换算为二进制后位宽的总长度]就取决于[与数值进制符号对应的数值]的长度。
(3) 在基数表示法中如果遇到 x,则在十六进制数中表示 4 个 x,在八进制中表示 3 个x。
(4) 数字中的下划线没有任何意义,但是可以很好的增强可读性,推荐每 4 个bit 后加一个下划线,例如:4’b11011011 和 4’b1101_1011 表示的是一样的值,但是后面的看上去更容易识别。
5.实数型变量的表示
Verilog 语言中的实数型变量可以采用十进制,也可以采用科学计数法,例如:
13_2.18e2 表示 13218
6.字符串
字符串是指双引号中的字符序列,是 8 位 ASCII 码值的序列,例如:“Hello World”,该字符串包含 11 个 ASCII 符号(两个单词共 10 个符号,单词之间的空格位一个符号,共 11 个 ASCII 符号),一个 ASCII 符号需要 1 个 byte 存储,所以共需要 11 个 byte 存储。
2.4 变量
Verilog 语言中主要的两种变量类型
1. 线网型:表示电路间的物理连接;
2. 寄存器型:Verilog 中一个抽象的数据存储单元。
线网型和寄存器类型具体又包含很多种变量,线网型变量最常用的变量就是 wire,而
寄存器型最常用的变量是 reg。
wire 可以看成直接的连接,在可综合的逻辑中会被映射成一根真实的物理连线;而 reg 具有对某一个时间点状态进行保持的功能,如果在可综合的时序逻辑中表达,会被映射成一个真实的的物理寄存器,而在 Verilog 仿真器中,寄存器类型的变量通常要占据一个仿真内存空间。
因此在设计逻辑的时候要明确定义每个信号是 wire 还是 reg 属性。凡是在 always 或initial 语句中被赋值的变量(赋值号左边的变量),不论表达的是组合逻辑还是时序逻辑,都一定是 reg 型变量;凡是在 assign 语句中被赋值的变量,一定是 wire 型变量。
2.5 参数
参数是一种常量,通常出现在 module 内部,常被用于定义状态机的状态、数据位宽和
计数器计数个数大小等。
可以在编译时修改参数的值,因此它又被常用于一些参数可调的模块中,使用户在实
例化模块时,可以根据需要配置参数。
parameter 是出现在模块内部的局部定义,只作用于声明的那个文件,可以被灵活改
变,这是 parameter 的一个重要特征。
2.6 赋值语句
赋值语句的赋值方式有两种,分别为“<=”(非阻塞赋值)和“=”(阻塞赋值)。
//阻塞赋值"="
//可以理解成顺序执行,每条语句执行完毕以后,才能进行下一条语句的执行
a=1;
b=2;
c=3;
begin
a=b;
c=a;
end
a=2;
b=2;
c=2;
//非阻塞赋值"<="
//并行执行
a=1;
b=2;
c=3;
begin
a<=b;
c<=a;
end
a=2;
b=2;
c=1;
1. 以赋 值 操 作 符 “ <= ”来 标 识 的 赋 值 操 作 称 为 “ 非 阻 塞 型 过 程 赋 值 ( Nonblocking
Assignment)”。
非阻塞型过程赋值语句的特点如下:
(1) 在 begin-end 串行语句块中,一条非阻塞过程语句的执行不会阻塞下一语句的执行,也
就是说在本条非阻塞型过程赋值语句对应的赋值操作执行完之前,下一条语句也可以
开始执行;(并行执行)
(2) 仿真过程在遇到非阻塞型过程赋值语句后首先计算其右端赋值表达式的值,然后等到
仿真时间结束时再将该计算结果赋值变量。也就是说,这种情况下的赋值操作是在同
一仿真时刻上的其他普通操作结束后才得以执行。
2. 以赋 值 操 作 符 “ = ”来 标 识 的 赋 值 操 作 称 为 “ 阻 塞 型 过 程 赋 值 ( Blocking
Assignment)”。
阻塞型过程赋值语句的特点如下:
(1) 在 begin-end 串行语句块中的各条阻塞型过程赋值语句将以它们在顺序块后排列次序依
次得到执行;(顺序执行)
(2) 阻塞型过程赋值语句的执行过程是:首先计算右端赋值表达式的值,然后立即将计算
结果赋值给“=”左端的被赋值变量。
阻塞型过程赋值语句的这两个特点表明:
仿真进程在遇到阻塞型过程赋值语句时将计算表达式的值并立即将其结果赋给等式左边的被赋值变量;
在串行语句块中,下一条语句的执行会被本条阻塞型过程赋值语句所阻塞,只有在当前这条阻塞型过程赋值语句所对应的赋值操作执行完后下一条语句才能开始执行。
2.7 注释
Verilog 中双反斜线“//”可以实现对一行的注释,除此之外“/*......*/”也是一种注释,进行注释时“/*......*/”之间的语句都将被注释掉,所以“/*......*/”不仅仅可以实现一行的注释,还可以实现对多行的注释,注释对整个代码的功能没有任何影响,只是设计者为了增强代码的可读性而增加的内容。
2.8 关系运算符
1.关系运算符种类:
(1) a < b,a 小于 b
(2) a > b,a 大于 b
(3) a <= b,a 小于或者等于 b
(4) a >= b,a 大于或者等于 b
在进行关系运算时,如果声明的关系是假的(false),则返回值是 0;如果声明的关系是真的(true),则返回值是 1;如果某个操作数的值不定,则关系是模糊的,返回值是x.
所有的关系运算符都有着相同的优先级别,但关系运算符的优先级要比算数运算符的低。
2.9 归约运算符、按位运算符和逻辑运算符
(1) 归约运算符和按位运算符
“&”操作符有两种用途,既可以作为一元运算符(仅有一个参与运算的量),也可以作为二元运算符(有两个参与运算的量)。
当“&”作为一元运算符时表示归约与。&m 是将 m 中所有比特相与,最后的结果为1bit。
当“&”作为二元运算符时表示按位与。m&n 是将 m 的每个比特与 n 的相应比特相与,在运算的时候要保证 m 和 n 的比特数相等,最后的结果和 m(n)的比特数相同。
(2) 逻辑运算符
我们在写 Verilog 代码时常常当 if 的条件有多个同时满足时就执行使用“&&”逻辑与操作符。m&&n 是判断 m 和 n 是否都为真,最后的结果只有 1bit,如果都为真则输出1’b1,如果不都为真则输出 1’b0。要注意和“&”的功能区分。
2.10 移位运算符
移位运算符是二元运算符,左移符号为“<<”,右移符号为“>>”,将运算符左边的操作数左移或右移指定的位数,用 0 来补充空闲位。如果右边操作数的值为 x 或 z,则移位结果为未知数 x.
在应用以为运算符的时候一定要注意它的这个特性,那就是空闲位用 0来填充,也就是说,一个二进制数不管原数值是多少,只要一直移位,最终全部会变为0。
例如:4'b1000 >> 3 后的结果为 4'b0001,4'b1000 >> 4 的结果为 4'b0000。
移位运算符在使用时,左移一位可以看成是乘以 2,右移一位可以看成是除以 2。
所以移位运算符用在计算中,代替乘法和除法。尤其是除法,使用移位的方式,可以节省资源。但使用的前提是数据位宽要进行拓展,否则就会出现移位后全为 0 的情况。
2.11 条件运算符
如果在条件语句中,只执行单个的赋值语句时,用条件表达式会更方便。条件运算符为“ ? : ”,它是一个三元运算符,即有三个参与运算的量。
由条件运算符组成的条件表达式的一般形式为:表达式 1 ? 表达式 2 : 表达式 3
执行过程是:当表达式 1 为真,则表达式 2 作为条件表达式的值,否则以表达式 3 作为条件表达式的值。
例如:当 a = 6, b = 7,条件表达式(a > b) ? a : b 的结果为 7。
注意:
(1) 使用条件表达式时“?”和“:”是一对,不可以只是用一个;
(2) 条件运算符从右向左结合。
虽然后面要讲到的 if-else 也可以实现这种功能,但是 if-else 只能在 always 块中使用,不能在 assign 中使用,如果我们想在 assign 中使用就需要用到条件运算符。
2.12 优先级
总的优先级关系为:归约运算符 > 算数运算符 > 移位运算符 > 关系运算符 > “= =”和“!=”> 按位运算符 > “&&”和“||”> 条件运算符,总的来说是一元运算符 > 二元运算符 > 三元运算符。
如果在编写代码的时候对这些关系容易混淆,最好的方式就是使用“( )”增加优先级。
2.13 位拼接运算符
位拼接运算符由一对花括号加逗号组成“{ , }”,拼接的不同数据之间用“,”隔开。
位拼接运算符的作用主要有两种,一种是将位宽较短的数据拼接成一个位宽长的数据;另一种是可以通过位拼接实现移位的效果。
1、 实现增长位宽的作用
如果需要将 8bit 的 a、3bit 的 b、5bit 的按顺序拼接成一个 16 位的 d。
2、 实现移位的作用
din 是 1bit 的串行数据,假如刚开始传来的数据是 1,后面的数据都是 0,则第一个时钟时 4bit dout 的值为 4'b1000,第二个时钟时 dout 的高三位放到最后,新来的 0 放到 dout的最高位,变为 4'b0100,从而实现了数据的右移功能。
2.14 if-else 与 case
Verilog HDL 语言中存在两种分支语言:
1、 if-else 条件分支语句
2、 case 分支控制语句
同样的逻辑,可能我们用 if-else 语句可以实现,用 case 语句也可以实现。但是在很多的场合,我们又会发现 case 语句和 if-else 语句又总是同时出现,互相嵌套,密切配合。
if-else 条件分支语句:
if-else 条件分支语句的作用是根据指定的判断条件是否满足来确定下一步要执行的操作。
case 分支控制语句
case 分支语句是另一种用来实现多路分支控制的分支语句。与使用 if-else 条件分支语句相比,采用 case 分支语句来实现多路控制将显得更为方便与直观。case 分支语句通常用于对微处理器指令译码功能的描述以及对有限状态机的描述。case 分支语句有“case”、“casez”、“casex”三种形式。
<控制表达式>代表着对程序流向进行控制的控制信号:各个<分支表达式>则是控制表达式的某些具体状态取值,在实际使用中这些分支项表达式通常是一些常量表达式:各个“语句”则指定了在各个分支下所要执行的操作,它们也可以是由单条语句构成,处于最后的、以关键词 default 开头的那个分支项称为“default”分支项,它是可以缺省的。
case 语句的执行过程:
(1) 当“控制表达式”的取值等于“分支项表达式 1”时,执行第一个分支项所包含的语句块 1;
(2) 当“控制表达式”的取值等于“分支项表达式 2”时,执行第二个分支项所包含的语句块 2…;
(3) 当“控制表达式”的取值等于“分支项表达式 n”时,执行第 N 个分支项所包含的语句块 n;
(4) 在执行了某一分支项内的语句后,跳出 case 语句结构,终止 case 语句的执行。case 语
句中各个“分支项表达式”的取值必须是互不相同的,否则就会出现矛盾现象。
2.15 inout 双向端口
在定义端口列表的时候我们知道输入用 input,输出用 output,其实还有一种双向端口,我们定义时使用 inout,在后面的实例中会用到,例如 IIC 和 SDRAM 的数据线都是双向端口。定义为 inout 的端口表示该端口是双向口,既可以作为数据的输入端口也可以作为数据的输出端口,在 Verilog 中的使用方式如下:
2.16 Verilog 语言中的系统任务和系统函数
Verilog 语言中预先定义了一些任务和函数,用于完成一些特殊的功能,它们被称为系统任务和系统函数,这些函数大多数都是只能在 Testbench 仿真中使用的,使我们更方便的进行验证。
`timescale 1ns/1ns //时间尺度预编译指令 时间单位/时间精度
时间单位和时间精度由值 1、10、和 100 以及单位 s、ms、us、ns、ps 和 fs 组成。
时间单位:定义仿真过程所有与时间相关量的单位。
仿真中使用“#数字”表示延时相应时间单位的时间,例#10 表示延时 10 个单位的时间,即 10ns。
时间精度:决定时间相关量的精度及仿真显示的最小刻度。
`timescale 1ns/10ps 精度 0.01,#10.11 表示延时 10110ps。
下面这种写法就是错误的,因为时间单位不能比时间精度小。
`timescale 100ps/1ns
主要的函数有如下这些,在支持 Verilog 语法的编辑器中都会显示为高亮关键字
二、知识点及小技巧
1.常用关键字
//module 模块开始
module example //模块开始 模块名
//input 输入信号
//output 输出信号
//inout 输入输出信号
I2C协议里的sda既可以作为指令的输入,也可以作为指令的输出
//通过一些变量或参数对输入信号进行处理,从而得到输出信号
//线网型变量
wire [0:0] flag;
wire可看做是直接的连接,它在可综合的逻辑中会被映射成为一条真实存在的物理连线
//寄存器型变量
reg [7:0] cnt;
reg具有对某一时间点状态进行保持的功能,它在可综合的时序逻辑中会被映射成为一个真实的物理寄存器
//参数
parameter CNT_MAX =100;
localparm CNT_MAX =100; //localparm 只能在模块内部使用,不能进行实例化
如果我们使用的参数关键字是parameter,那么我们可以在顶层文件通过实例化来对此功能模块的参数进行修改
//模块实例化
example
#(
.CNT_MAX (8'd100 ) //实例化时参数可修改
)
//endmodule 模块结束
2.always
//always
always(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt <= 8'd0;
else if(cnt == CNT_MAX)
cnt <= CNT_MAX;
else
cnt <= cnt + 8'd1;
//当复位信号有效时,我们给变量cnt初值设置为0,如果它计数到最大值(我们设定的参数)时,它就一直保持最大值,如果它没有计数到最大值(我们设定的参数)时,每个时钟周期增加1
3.assign
//assign
assign po_flag = (cnt == CNT_MAX) ? 1'b1 :1'b0;
//如果括号里的条件满足的话,就把第一个值赋值给po_flag,否则把第二个值赋值给po_flag