Verilog 学习笔记(3)——Verilog 语言基础(1)
Contents
Verilog 语言基础(1)
Verilog 的三种描述方法
Verilog 语言主要有三种描述硬件的方法:
- 数据流描述:采用 assign 语句,主要描述信号之间的赋值关系。这种语句又被称为连续赋值语句。
- 行为描述:使用 always 或 initial 语句块,更加侧重于描述电路的行为。其中出现的语句被称为过程赋值语句。
- 结构化描述:实例化已有的功能模块。
我们先介绍一些基本的词法和数据流描述。
基本词法
Verilog 区分大小写,但是还是建议不要通过大小写来区分定义的变量。在 Verilog 语言中,所有的关键字(保留字)都为小写。内部信号名(标识符)大小写都可以,可以是字母、数字、$、_、以及它们的任意组合,但是第一个字符要是字母或下划线。
注释方式也是//与/* */。
注意,在有的地方(比如一些逻辑综合工具中),为了控制工具的编译过程,一些指令也是以注释的方式出现的(不过目前不会碰到)。
模块与端口
大型设计往往是由一个个模块构成的,Verilog 中,模块(module)是基本的组成单位。通常建议一个 Verilog 文件只放一个 module 定义,并且使文件名称与 module 名称一致。下面我以上一章的四位加法器为例介绍 Verilog 的 module 中的各个组成部分。
module adder_4( //adder_4为模块名称
input [3:0] A,
input [3:0] B,
output [4:0] sum ); //端口的声明,[n:0]表示位宽,后面会讲
//注意最后一个变量后没有分号,而括号后面有分号
//如果有中间变量可以在这里声明,或者端口也可以在这里声明
assign sum = A+B; //语句
endmodule //模块结束
同时端口也可以在声明部分进行声明,比如上面的部分可以写成
module adder_4( A,B,sum );
input [3:0] A;
input [3:0] B;
output [4:0] sum;
另外,也可以把 input(output)一起声明
module adder_4(
input [3:0] A, [3:0] B,
output [4:0] sum );
但是切记!input 和 output 在这里之间分隔是用逗号!不要因为换了行就用分号。
编译指令
Verilog 语言中提供了一些编译指令,指令前与 C 语言中的#不同,使用反引号`来标识。
比如上一章 testbench 中的第一句
`timescale 1ns/1ns
指延时规定,前者1ns 指延时单位为1ns ,后者表示时间精度为1ns。当该指令被编译后,如语句
assign #1.6 sum=A+B;
由于延时单位为1ns,故#1.6即指1.6ns 后执行该语句,同时由于时间精度为1ns,所以该语句将被四舍五入到2ns 后执行。其他的编译指令之后如果讲到了再介绍。
逻辑值与常量
逻辑值
众所周知,一般情况下单比特逻辑值是二值的(0和1)。而 Verilog 中增加了 X 和 Z :
- X:表示未知,在条件判断语句中表示不关心
- Z:表示高阻,即没有任何驱动
综合工具中并没有 X,只存在0,1,Z。
Z与X不区分大小写,即 0z1x 和 0Z1X 表示同一个逻辑值。
常量
暂时仅介绍整数常量,用[长度]'数值符号 数值 的形式表示。
比如 4'b1001 就表示一个位宽4比特的二进制整数1001。
若不表明位宽和数值符号则默认十进制数,比如:
- 16表示十进制数16;
- -15表示十进制-15,若二进制补码表示则为10001。
若注明长度比后面数值实际位宽多,则左边补0,若少,则左边多出部分截断。
线网
Verilog 中主要有两类变量类型:
- 线网:表示电路间的物理连线。
- 寄存器:抽象的存储数据单元。
线网变量用 assign 语句赋值,寄存器变量用 always 或 initial 语句赋值。暂时先只介绍线网中的 wire 类型。
wire
wire 表示电路中的物理连线,并不像其他的线网类型具有逻辑运算或保持功能。
input 和 output 一般默认为 wire 型。
但是注意,wire 并不是那个线,而是信号。举个例子。
wire A;
wire B;
assign B=A;
这里的 wire 是指信号 A 和信号 B,而 assign 才表示它们之间的驱动关系。
另外与 C 语言中不同的是,C 中的赋值指把此刻 A 的值赋值给 B,而 assign 却是一种连续驱动,即 B 的值时刻由 A 的值决定。
并且该语句具有方向性,而不是简单的线连接,即 A 能驱动 B 但 B 不能驱动 A。
向量(或许是这么翻译?)
即 vector ,一组信号的集合,通过赋予集合名称以便于访问其中的信号。
举个例子:
wire [7:0] w;
这个语句声明了一个类型为 wire,名为 w,位宽为8bit 的 vector。注意与数组的声明的区别,位宽要写在向量名之前。
取出某个或某片 wire 信号的方式与数组类似:
wire [2:0] out;
assign out=w[6:4];
vector 的声明
一般情况下向量的声明中,高位在前,低位在后,并最低位为零。但是理论上也可以有其他的声明方式,举些例子:
wire [7:0] w; //8比特 wire
reg [4:1] x; //4比特 reg,reg是寄存器类型之后会讲
output reg [0:0] y; //位宽为 1bit 的 reg 输出
input wire [3:-2] z;//6比特 wire 输入
wire [0:7] b; //8比特 wire,但是 b[0] 为 MSB(most-significant bit)
虽然你可以随意声明,但中括号内前者代表 MSB,后者为 LSB(相关知识在数电中应该有)。另外,声明与使用中一定要保持一致。所以我还是建议统一用大值在前,0在后的声明方式。
可以同时声明多个位宽相同的变量:
wire [7:0] a1,a2;
该语句为声明两个位宽为 8bit 的 wire 型变量。
综合器会将未定义声明的信号声明为 1bit wire 型信号,可能会导致 bug,赋值过程中高位可能被截断。你可以在 warning 中发现隐式声明。如果不想看 warning ,可以使用编译指令
`default_nettype none
关闭隐式声明功能。
vector 的片选
举一些片选的例子(结合声明中的例子部分来看):
w[3:0] //片选 w 中的低4位
x[1:1] //片选 x 中的最低位
b[3:0] //由于声明中是[0:3],故该片选不能实现
assign w[3:0]=b[0:3]//将上述b[0]赋值给w[3],……,b[3]赋值给w[0]
vector 的连接
可以使用{}将数个小的 vector 连接为大的 vector,如:
wire [7:0] x;
assign x={4'b1001,4'b0010}; //x 被赋值为 8'b10010010
注意括号内的 vector 必须已标明位宽。
同时连接的 vector 也可以作左值,比如:
input [15:0] in;
output [15:0] out;
assign {out[7:0],out[15:8]}=in; //交换了输入前后八位的顺序后输出
一些 verilog 中的操作符
全列出来太多了而且其实我也并不全知道,就列几个常用的,如果有需要可以自己去查:
- 算术运算:+ - * / % (我觉得应该都知道意思)
- 位操作符:
- ~:按位取反
- &:按位与
- |:按位或
- ^:按位异或
- 规约操作符:大致同位操作符,不过位操作符是在两个变量之间操作,而规约操作是在一个变量中对所有比特操作,如 &m 即将 m 中的所有比特相与(1位结果)。
- 逻辑操作:! && || (在1位情况下与位操作相同,但多位下不是各位操作而是整个值的逻辑操作,且只有1位结果)
- 关系操作:< > <= >=
- 移位操作:<< >>
- 条件操作:? :,如 sel ? m:n
优先级啥的基本和 C 语言差不多,拿不准就加括号,另外一个好的设计在一个语句中不需要太多复杂的操作。
牛刀小试
说了这么多,试着自己编写一个 module 吧。
试构建一个模块,其中输入为两个 3bit 位宽的变量 a,b,要求分别输出 a,b的按位或,逻辑或以及 a,b 合并按位取反的结果(其中 b 在高位)。
这里推荐一个网站,你可以理解为 HDL 的 oj,你可以在网站上输入自己的 Verilog 代码,而网站在线综合并仿真,以验证功能,如果输出与要求不符,还会给出正确的波形与你的波形以便于 debug。缺点是网站是外网有时候可能有点卡。
HDLBits:Problem(14)——Vectorgates
当然你也可以根据我上一章的方法试着自己编写一个 Testbench,使用 ModelSim 进行功能仿真。
下面给出该题的解答,仅供参考:
module top_module(
input [2:0] a, b,
output [2:0] out_or_bitwise,
output out_or_logical,
output [5:0] out_not );
assign out_or_bitwise = a|b;
assign out_or_logical = a||b;
assign out_not = {~b,~a};
endmodule
No Comments