1.1 基本语法说明
1.1.1 语法与注释
Verilog语言与C语言类似,是区分大小写的语言,代码可多行书写也可以单行书写,一条语句结束后要以英文分号;结尾。
注释语言分为单行注释和多行注释,单行注释使用双斜线//后跟代码,多行注释使用/ /,中间写入注释。
示例:
wire A, B, C; // 这是一行代码
/
这是一行代码
/
assign Count = A & B;
1.1.2 数值系统:
数值 说明
0 低电平,逻辑上为False
1 高电平,逻辑上为True
x 未知电平,有可能为低电平,有可能为高电平,也有可能都不是,也可以写作X
z 高阻态,简单而言是信号没有驱动,没有输出,也可以写作Z
Verilog中基数格式:<bits>'<radix><value>
其中<bits>为二进制位宽,即位的个数,如果空缺不填则会由编译器根据后面的数值自动分配。
需要声明的一点:若二进制位宽小于实际位宽,则会进行截断,
比如:3'hfff实际会截断为3'b111。
<radix>为进制,共有4种进制:二进制(b,B),八进制(o,O),十进制(d,D),十六进制(h,H)。
在十六进制中,a~f这6个字母不区分大小写。
<value>为实际数值,在其中为了可读性可以加上下划线_,比如十进制数中三位一分或其他进制中四位一分等。
1.1.3 标识符与变量
标识符 说明
wire 线网型数据。表示以 assign 语句内赋值的组合逻辑信号,默认初始值为 z (高阻态),且 wire 是默认数据类型
reg always 语句内部进行赋值操作的信号(凡是 always 语句内部赋值的信号都应定义为 reg 类型),对应一种存储单元,默认初始值为 x (未知电平)
需要说明的一点是, reg 类型变量不一定对应一个寄存器,仅声明一个 always 语句中进行赋值的信号,受 always 描述的影响。
声明变量格式:wire/reg [width - 1 : 0] (<var_name>,...)其中width为位宽,若省略width,则默认为1,即[0 : 0];将width=1的变量称为标量(Scalar),width>1的变量称为向量(Vector)。
表达式中,可任意选择向量的一位或相邻几位,分别称为位选择(bit-select)和域选择(part-select)。
此外,Verilog中也有数组这一概念:wire/reg [width - 1 : 0] <var_name> [0 : width - 1];
附:integer类型为有符号reg类型,一般用于描述循环变量或计算,一般32位。
在Verilog语言中,类似于i++、i--的运算是不允许的。
1.1.4 运算符
算数运算符:+、-、*、/、%。其中前两个也可以作为单目操作符,表示操作数的正负性,优先级最高。
关系运算符:>、<、>=、<=、==、!=、===、!==。前六位在别的编程语言中也很常见,其中最后两个表示“全等”和“非全等”。关系运算符的结果为0(False)或1(True),是1bit的值。对前六个运算符,如果某一操作数中有一位x或z,则运算结果全为x。而对于最后两个运算符,则对是x或z的位也进行比较,只有操作数完全一致,才为相应的1(对于===),或者0(对于!==)。
逻辑运算符:&&(逻辑与)、||(逻辑或)、!(逻辑非)。其运算结果也是0(False)或1(True),是1bit的值。与刚才说的六种关系运算符相同,如果任意一位为x或z,则运算结果为x。
按位运算符:~、&、|、^、~^或^~,分别表示按位非、按位与、按位或、按位异或和按位同或(后两个按位运算符都是按位同或)。对 2 个操作数的每 bit 数据进行按位操作。如果 2 个操作数位宽不相等,则用 0 向左扩展补充较短的操作数。按位非是单目运算符,它对操作数的每 bit 数据进行取反操作。
归约运算符:&、~&、|、~|、^、~^。是单目运算符,对多位操作数进行逐位操作,最终产生一个1bit的结果。其中&可用来判断是否为全1;~|可用来判断是否为全0;^和~^可用来做奇偶校验:对于^,若1的个数为奇数,结果为1,对于~^,若1的个数为偶数,则结果为1。需要说明的一点是:带~的归约运算符,先归约后面的运算符,后对结果取反。
条件运算符:条件表达式 ? 真分支 : 假分支,与其他编程语言应用类似。
移位运算符:<<、>>、<<<、>>>,分别为:逻辑左/右移和算数左/右移。对于算数左移和逻辑左移:右边低位补0;对于逻辑右移:左边高位补0;对于算数右移:左边高位补充符号位。比如说:
A = 8'b 1100_1101;
A >> 1; // 8'b 0110_0110
A >>> 2; // 8'b 1111_0011
A << 2; // 8'b 0011_0100
拼接运算符:{表达式1, 表达式2, ..., 表达式N}。实现将多个操作数拼接成新的操作数,既可以是常数,也可以是变量,但位宽必须确定且不可变。而且还有另一种用法,对于某个表达式要重复拼接许多次:{重复次数{表达式}},在将重复操作嵌入拼接操作时,需要用大括号把重复操作整体括起来,就如同我刚才写的那样。
1.2 Verilog语句
1.2.1 连续赋值 assign(并行,同时赋值)
对wire型变量进行赋值。语法为:
assign LHS = RHS;
其中LHS必须为wire型,不能是reg型;RHS类型无要求。下面举个实例说明:
wire Count, A, B;
assign Count = A & B;
或者写作:
wire A, B;
wire Count = A & B;
只要RHS表达式的操作数有事件发生(值的变化)时,RHS就会立刻重新计算,同时复制给LHS。
1.2.2 过程赋值 always、initial
对reg型变量进行赋值。这两种关键字不可嵌套使用,彼此间并行执行(执行顺序与其在模块中的前后顺序无关)。若语句内包含多个语句,则需要begin和end组成一个块语句。每个initial语句或always语句都会产生一个独立的控制流,执行时间都是从0时刻开始。
initial语句仅在0时刻开始执行一次内部的语句(单次);always语句从0时刻开始执行,执行完最后一条语句后,便再次执行语句块中第一条语句,如此循环反复(循环)。
always语句常用格式如下:
always @ (敏感变量列表) 过程语句
其中敏感变量列表:
触发always语句执行的条件;
仅在列表中的变量发生变化时,才执行内部的过程语句;
若敏感变量过多,Verilog 2001允许使用*表示缺省,根据always块内部内容自动识别敏感变量;
支持posedge:上升沿和negedge:下降沿,如always @(posedge clk)begin ... end;
不允许混合信号(边沿敏感信号与普通变量混合)。
1.2.3 阻塞赋值和非阻塞赋值
阻塞赋值:顺序执行(同C语言),使用=。
非阻塞赋值:并行同时执行,使用<=。
注意:在实际使用中,不推荐混合使用阻塞赋值与非阻塞赋值,否则时序不容易控制。
在设计电路时,always时序逻辑块使用非阻塞赋值,组合逻辑块使用阻塞赋值;在仿真电路时,initial块使用阻塞赋值。
举例:clk上升沿交换a、b的值:
reg temp;
always @(posedge clk) begin
temp = a;
a = b;
b = temp;
end
这是我们在学习C语言等时常用的写法,但是在Verilog中,可以使用非阻塞赋值使代码更加简单:
always @(posedge clk) begin
a <= b;
b <= a;
end
如果是对同一变量的多次非阻塞赋值,只有最后一次赋值是有效的,比如:
a <= a + 1;
a <= a + 2;
这里实际上只有第二个语句生效。
1.2.4 条件语句 if else / case endcase
一般在always语句块中使用,不单独在模块中直接使用。
其语句格式为:
if (条件)
过程语句
[else 过程语句]
这里else 过程语句可以省略,但是不推荐,因为可能产生其他的问题。
case(case 表达式)
case 条目表达式 1:过程语句
case 条目表达式 2:过程语句
...
default:过程语句
endcase
这里default语句同样可忽略,但是至多有一条default语句。
1.3 模块
1.3.1 模块声明
以关键字module开始,endmodule结束。从module开始到第一个分号之间的部分是模块声明,包括模块名称与输入输出端口列表。
模块内部由内部变量声明、数据流赋值语句(assign),过程赋值语句(always)以及底层模块例化组成,它们顺序不固定。
端口是模块与外界交互的接口,外界只能看到模块的端口。共有三种端口类型:输入端口(input)、输出端口(output)、双向端口(inout)。其中输入端口和双向端口不能声明为reg类型,输出端口可以声明为wire类型或reg类型。
在数据类型声明中,wire关键字可省略,reg关键字不可省略。
综上,模块基本结构可总结为:
module 模块名(
输入端口定义, // 端口定义之间用英文逗号,分开,输入端口只能是wire型
输出端口定义 // 输出端口可以是wire或reg型
);
内部信号定义语句 // 可以按需设置wire和reg型变量
模块实例化语句 // 其他模块接入电路
assign 语句
always 语句
endmodule
1.3.2 模块例化
格式一般为:
<模块名> <例化标识符> (端口连接);
类似于其他编程语言中的函数实例化。
模块例化有2种方式:
基于位置的端口关联:端口需与声明时顺序一致,如:
F f(n1, n2, cin, sum, cout);
基于名字的端口关联:顺序可以不一致,如:
F f(.a(n1), .b(n2), .cin(cin), .s(sum), .cout(cout));
一般input端口例化时不能删除,output端口可以删除。而对于这两种方式,方式也不一样(这里假设cout为输出端口):
基于位置的端口关联:
F f(n1, n2, cin,, cout); // 省略例化时,逗号不能省
基于名字的端口关联:
F f(.a(n1), .b(n2), .cin(cin), .s(sum), .cout()); // 省略时,括号不能省
下面再说一下端口连接规则:
端口 连接规则
input 输入端口 模块例化时,可wire或reg;模块声明时,必须wire
output 输出端口 模块例化时,必须wire;模块声明时,可wire或reg
inout 双向端口 模块例化和声明时,都必须wire
例化时,output悬空时,可省略;input悬空时,逻辑值为z(高阻态),而且不建议将input端口悬空,且例化时不能将悬空的input端口删除。不能将模块例化放入过程赋值语句(always)中。
1.3.3 参数传递
模块声明时使用parameter关键字指定参数。如:
module M
(parameter a = 8)
(...) // 模块声明
... // 其他代码
例化时,M#(4) m(...);;或module ();,先不定义参数,例化后,defparam m.a = 8;。
注意:
只能将参数值传递给比顶层模块低一级的底层模块中;
localparam,局部参数,不能通过顶层模块传参。