Verilog语法-1

Verilog语法#

逻辑值#

  • 逻辑0:低电平(GND)
  • 逻辑1:高电平(VCC)
  • 逻辑X:电平未知
  • 逻辑Z:高阻态,悬空状态

数据进制格式#

二进制(binary)、八进制(Octal)、十进制(Decimal)、十六进制(Hexadecimal)

  • 二进制: 4'b0101 表示4位二进制数字0101
  • 十进制: 4'd2 表示4位十进制数字2
  • 十六进制: 4'ha 表示4位十六进制数字a

可以发现,Verilog对数字的定义为 大小[size] + ' + 进制[base_format] + 数值[number]

数值的每四位建议下划线( _ )分割,以增加程序的可读性。

例如: 16'b1001_1010_1010_1001=16'h9AA9

对于未指定 进制[base_format] 的数值,默认为十进制数;对于未指定 大小[size] 的数字,采用默认位数(取决于设备类型)

对于负数,符号应当放置在 大小[size] 前,而不应放在其他位置

1
2
-4'd2	//correct
4'd-2 //illegal

字符串#

不可分行书写,用 " " 包含。

1
2
3
4
5
"Hello World!"        // string with 12 characters -> require 12 bytes
"x + z" // string with 5 characters

"How are you
feeling today ?" // illegal for a string to be split into multiple lines

标识符#

用于定义模块名、端口名、信号名等。

可是 字母数字$下划线 的组合。

注意:与C语言相同,标识符的第一个字母必须是 字母下划线 ,且对大小写敏感。

关键字#

语言保留关键字,不可用做标识符名称。

Verilog关键字

数据类型#

寄存器类型 reg#

用来表示存储单元

1
2
reg [25:0] cnt;	//size为26位
reg clk; //size为1位

reg 类型不可直接赋初值,只能在 alwaysinitial 语句中赋值。

  • [bit+: width] : 从起始 bit 位开始递增,位宽为 width。
  • [bit-: width] : 从起始 bit 位开始递减,位宽为 width。
1
2
3
4
5
6
7
//下面 2 种赋值是等效的
A = data1[31-: 8] ;
A = data1[31:24] ;

//下面 2 种赋值是等效的
B = data1[0+ : 8] ;
B = data1[0:7] ;

线网类型 wire#

用于表示硬件单元间的物理连线,缺省值为 Z

1
2
3
wire   interrupt ;
wire flag1, flag2 ;
wire gnd = 1'b0 ;

数组:#

在 Verilog 中允许声明 reg, wire, integer, time, real 及其向量类型的数组。维数不限。

1
2
3
4
5
6
7
8
9
10
11
integer          flag [7:0] ; //8个整数组成的数组
reg [3:0] counter [3:0] ; //由4个4bit计数器组成的数组
wire [7:0] addr_bus [3:0] ; //由4个8bit wire型变量组成的数组
wire data_bit[7:0][5:0] ; //声明1bit wire型变量的二维数组
reg [31:0] data_4d[11:0][3:0][3:0][255:0] ; //声明4维的32bit数据变量数组

flag [1] = 32'd0 ; //将flag数组中第二个元素赋值为32bit的0值
counter[3] = 4'hF ; //将数组counter中第4个元素的值赋值为4bit 十六进制数F,等效于counter[3][3:0] = 4'hF,即可省略宽度;
assign addr_bus[0] = 8'b0 ; //将数组addr_bus中第一个元素的值赋值为0
assign data_bit[0][1] = 1'b1; //将数组data_bit的第1行第2列的元素赋值为1,这里不能省略第二个访问标号,即 assign data_bit[0] = 1'b1; 是非法的。
data_4d[0][0][0][0][15:0] = 15'd3 ; //将数组data_4d中标号为[0][0][0][0]的寄存器单元的15~0bit赋值为3

字符串#

1
2
3
4
reg [0: 14*8-1]       str ;
initial begin
str = "Hello Verilog!";
end

参数#

parameter 声明,只能赋值一次。局部参数用 localparam 声明。

1
2
3
parameter      data_width = 10'd32 ;
parameter i=1, j=2, k=3 ;
parameter mem_size = data_width * 10 ;

其他类型#

integerrealtime and so on 此处略去。

表达式与运算符#

运算优先级与C语言相同

1
2
3
4
5
6
7
8
9
real a,b,c;
c=a+b;

reg [3:0] m,n;
wire [3:0] tmp;
always @(posedge clk) begin
n=m^n;
//tmp=m-n; //非法操作,always块里赋值对象不能是wire型
end
  • 算术运算符:即加( + )、减( - )、乘( * )、除( / )、求幂( ** )、取模( % ),注意定义的数据位宽。

    1
    2
    3
    4
    5
    6
    reg [3:0] a,b;
    reg [4:0] c,d,e;
    a=4'b0010;//a=2
    b=4'b1001;//b=9
    c=a+b;//result:c=4'b1011 即c=11
    c=b/a;//result:c=4 取整

    若操作数某一位为 x ,Verilog 会认为 “输入有不确定信号”,结果也会被标记为 x

    1
    2
    3
    4
    5
    6
    7
    8
    //example
    a=4'b1010;
    b=4'bx001;
    c=a+b;//result:c=4'bx011
    //不只是加法,其他运算也有同样的结果
    c = a & b; // 结果 c=4'bx000(因为 b[3] 是 X,与运算后这一位也 X)
    d = a | b; // 结果 d=4'bx011(b[3] 是 X,或运算后这一位也 X)
    e = ~b; // 结果 e=4'bx110(b[3] 是 X,取反后还是 X)

    表示负数时,应当指定位宽 size (因为负数用的二进制补码表示),否则会出现意想不到的后果。

  • 关系运算符:大于( > )、小于( < )、大于等于( >= )、小于等于( <= )。

    基本规则与C语言相同。真为 1 ,假为 0

    值得注意的是,若操作数有一位为 x or z 则返回结果为 x

  • 等价运算符:逻辑相等( == )、逻辑不等( != )、全等( === )、非全等( !== )。

    基本规则与C语言相同。真为1,假为0。

    值得注意的是,执行逻辑比较时,若操作数有一位为 x or z 则返回结果为 x ;执行全等比较时,若两数同时在同一位有 x or z ,结果也能返回 1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    A = 4 ;
    B = 8'h04 ;
    C = 4'bxxxx ;
    D = 4'hx ;
    A == B //为真
    A == (B + 1) //为假
    A == C //为X,不确定
    A === C //为假,返回值为0
    C === D //为真,返回值为1
  • 逻辑运算符:逻辑与( && )、逻辑或( || )、逻辑非( !

    基本规则与C语言相同。操作数不为0,等价于 1 ;操作数为0,等价于 0 ;操作数为 x or z ,等价于 x

    值得注意的是,如果任意一个操作数包含 x ,逻辑操作符运算结果不一定为 x

  • 位运算符:取反( ~ )、与( & )、或( | )、异或( ^ )、同或( ~^ )。

    具体规则详见我的另一篇文章:https://skina.cn/bit_operation/

    这里简单补充同或规则:两个输入值相同时,结果为真。该运算的结果与异或相反,故满足如下条件 A~^B == ~(A^B)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    A = 4'b1010 ;
    B = 4'b0110 ;
    C = 4'bx101 ;

    ~A // 4'b0101
    A & B // 4'b0010
    A | B // 4'b1110
    A^B // 4'b1100
    A ~^ B // 4'b0011
    B | C // 4'bx111
    B & C // 4'bx100
  • 移位运算符:左移( << )、右移( >> )、算术左移( <<< )、算数右移( >>> )。

    具体规则详见我的另一篇文章:https://skina.cn/bit_operation/

    这里补充算数左移和算术右移的规则。

    算术左移:将操作数的二进制位向左移动指定的位数,低位补 0 ,高位丢弃。

    算术右移:将操作数的二进制位向右移动指定的位数,对于有符号数,高位用符号位填充,低位丢弃。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 无符号数,用于逻辑移位、算术左移(无符号数算术左移和逻辑左移行为一致)
    reg [7:0] unsigned_num = 8'b1010_1100; // 十进制 172,二进制 10101100
    // 有符号数,用于算术右移对比
    reg signed [7:0] signed_num = 8'b1101_0011; // 十进制 -45(补码),二进制11010011

    // 移位结果存储
    reg [7:0] arith_left_res;
    reg [7:0] arith_right_res;
    reg [7:0] logic_left_res;
    reg [7:0] logic_right_res;


    arith_left_res = unsigned_num << 1;//算术左移(<<):无符号数左移,低位补 0,相当于 ×2(不溢出时)
    arith_right_res = signed_num >> 1;//算术右移(>>):有符号数右移,高位补符号位(这里最高位是 1,补 1)
    logic_left_res = unsigned_num <<< 1;//逻辑左移(<<<):和算术左移对无符号数效果一样,低位补 0
    logic_right_res = signed_num >>> 1;//逻辑右移(>>>):不管符号,高位补 0

    归约运算符:将一个多位宽的向量逐位进行逻辑运算,最终得到一个 1 位的结果。有 &(与)、|(或)、^(异或)、~&(与非)、~|(或非)、~^(同或)等运算符。语法为: 规约运算符 + 表达式

    1
    2
    3
    4
    reg [3:0] a = 4'b1011;
    wire and_result = &a; // 逐位与:1&0&1&1 = 0
    wire or_result = |a; // 逐位或:1|0|1|1 = 1
    wire xor_result = ^a; // 逐位异或:1^0^1^1 = 1

    常用于判断向量是否全为 1(&a)或全为 0(~|a)。

  • 拼接运算符:将多个信号或常量按位拼接成一个新的向量。语法为: {信号1, 信号2, ...}

    1
    2
    3
    4
    reg [3:0] a = 4'b1010;
    reg [1:0] b = 2'b11;
    wire [5:0] c = {a, b}; // 拼接结果:6'b101011
    wire [7:0] d = {2'b00, a, 1'b1}; // 拼接常量和变量:8'b00101001

    值得注意的是,拼接时需明确指定每部分的位宽,该语法常用于构建更大的向量(如地址、数据总线等)

  • 三目运算符:与C语言相同,不再赘述。

编译指令#

`define`undef#

类似C语言中的 #defineundef

与之配套的有`ifdef`ifndef`else`endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
`define 宏名 [值]
`undef 宏名

//example
`define WIDTH 8 // 定义常量宏
`define DEBUG // 定义开关宏

reg [`WIDTH-1:0] data; // 使用宏定义位宽

`ifdef DEBUG
initial $display("Debug mode enabled");
`endif

`undef DEBUG // 取消宏定义

宏可以是常量、文本片段或表达式,且宏在定义后全局有效,直到被undef或文件结束。

`include#

将另一个文件的内容插入到当前位置(类似 C 语言的#include)。

1
2
3
4
5
`include "defines.v"  // 包含自定义定义文件

module top;
// 使用defines.v中定义的内容
endmodule

`timescale#

指定模块的时间单位和精度。

  • 时间单位和精度必须是 1、10 或 100,后跟fspsnsusmss
  • 影响时间相关函数。

语法规则为: `timescale 时间单位/时间精度

1
2
3
4
5
6
7
8
9
10
`timescale 1ns / 1ps  // 时间单位为1纳秒,精度为1皮秒

module delay_example;
reg clk;
initial begin
clk = 0;
#5 clk = 1; // 延时5ns
#5 clk = 0; // 再延时5ns
end
endmodule

`default_nettype#

用于指定未声明的标识符的默认网络类型。

语法规则为:`default_nettype 网络类型

1
2
3
4
5
6
7
8
`default_nettype wire  // 默认网络类型为wire

module test;
reg clk;
signal a; // 未声明,默认被视为wire类型
endmodule

`default_nettype none // 禁用默认类型(推荐做法)

`resetall#

重置所有编译指令为默认状态(常用于文件末尾),清除所有 define 宏、恢复 default_nettype

语法规则为:`resetall

`celldefine`endcelldefine#

用于将模块标记为单元模块,他们包含模块的定义。例如一些与、或、非门,一些 PLL 单元,PAD 模型,以及一些 Analog IP 等。

1
2
3
4
5
6
7
8
9
`celldefine
module (
input clk,
input rst,
output clk_pll,
output flag);
……
endmodule
`endcelldefine

连续赋值#

assign#

可用 assign 语句对 wire 类型信号进行连续赋值。

语法规则: assign 目标信号=表达式; 其中目标信号必须是 wire 类型

1
2
3
wire a, b, c;
assign c = a & b; // 逻辑与运算
assign c = a ? b : 0; // 三目运算符

全加器#

全加器是一个实现三个一位二进制数相加的电路

输入为:

  • AB:两个待相加的二进制位
  • Cin:进位输入

输出为:

  • Sum:本位和
  • Cout:进位输出

逻辑表达式为: \[ Sum = A \oplus B \oplus Cin \\ Cout = ( A \& B ) + ( B \& Cin ) + ( A \& Cin ) \] 使用 assign 语句实现全加器的方法如下

1
2
3
4
5
6
7
8
module full_adder(
input wire A, B, Cin,
output wire Sum, Cout
);
// 使用assign语句直接实现逻辑表达式
assign Sum = A ^ B ^ Cin;
assign Cout = (A & B) | (B & Cin) | (A & Cin);
endmodule

一些解释:

  • 进位输入 Cin 的作用:来自 低位的进位信号,表示低位相加后是否产生进位。

    在计算 2 位二进制数相加(如 \(11_2+01_2\) )时:

    • 最低位(第 0 位):没有来自更低位的进位,因此 \(C_{in}=0\)
    • 次低位(第 1 位):最低位相加后产生进位(\(1+1=0\)\(1\) ),因此 \(C_{in}=1\)
  • 进位输出 Cout 的作用:当前位相加后 产生的进位信号,传递给 更高位 作为进位输入。

    \(11_2+01_2\) 为例:

    • 最低位(第 0 位)\(1+1=0\)\(1\) ,因此 \(Sum=0\)\(Cout=1\)
    • 次低位(第 1 位)\(1+0+C_{in}(1)=2\) ,即 \(Sum=0\)\(Cout=1\)。 最终结果为 \(100_2\)(十进制 \(4\) )。

延时#

连续赋值时延#

  • 普通时延
1
2
3
//A&B的计算结果延时10个单位时间再赋值给Z
wire Z,A,B;
assign #10 Z=A&B;
  • 隐式时延
1
2
3
//声明一个wire类型变量时,对其包含一定时延的连续赋值
wire A,B;
wire #10 Z=A&B;
  • 声明时延
1
2
3
4
//声明一个wire型变量是指定一个时延。因此对该变量所有的连续赋值都会被推迟到指定的时间。除非门级建模中,一般不推荐使用此类方法建模。
wire A, B;
wire #10 Z ;
assign Z =A & B
  • 惯性时延与传输时延

    • 惯性时延:默认行为,若输入脉冲宽度小于时延,输出不会响应。

    • 传输时延:使用 #(时延) 语法,所有输入变化都会传递到输出。

    1
    2
    3
    4
    5
    // 惯性时延(宽度小于5ns的脉冲会被过滤)
    assign #5 out = in;

    // 传输时延(所有输入变化都会传递)
    assign #(5) out = in;

过程赋值时延#

  • 阻塞赋值时延: #时延 变量 = 表达式; 先延迟,再执行赋值
  • 非阻塞赋值时延: 变量 <= #时延 表达式; 先计算表达式,在未来时刻赋值
1
2
3
4
5
6
7
always @(posedge clk) begin
// 阻塞赋值:延迟5ns后执行赋值
#5 a = b;

// 非阻塞赋值:当前周期计算,下一时延点赋值
c <= #3 d;
end

其他时延#

如 事件控制时延,组合与时序逻辑中的时延 自行查找资料,不再赘述。

用时延模拟触发器#

1
2
3
4
5
6
7
8
9
10
11
12
13
module d_flip_flop(
input wire clk, rst_n, d,
output reg q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
q <= #0.5 1'b0; // 复位延迟0.5ns
end
else begin
q <= #1 d; // 数据延迟1ns
end
end
endmodule

过程结构#

initial语句#

initial 块中的代码在仿真开始时执行一次,执行完毕后不再重复,多个 initial 块之间相互独立,互不影响(可以理解为同步执行?)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
module testbench;
reg clk, rst_n, a, b;
wire out;

// 实例化被测模块
my_module uut (.clk(clk), .rst_n(rst_n), .a(a), .b(b), .out(out));

// initial块1:生成时钟信号
initial begin
clk = 0;
forever #5 clk = ~clk; // 10个时间单位的时钟周期
end

// initial块2:生成复位信号
initial begin
rst_n = 0;
#20 rst_n = 1; // 20个时间单位后释放复位
$display("[%0t] 复位释放", $time);
end

// initial块3:生成输入激励
initial begin
a = 0; b = 0;
#30 a = 1; // 30个时间单位后a置1
#20 b = 1; // 再20个时间单位后b置1
#40 a = 0; b = 0; // 40个时间单位后a、b置0
end

// initial块4:监控输出
initial begin
$monitor("[%0t] a=%b, b=%b, out=%b", $time, a, b, out);
#100 $finish; // 100个时间单位后结束仿真
end
endmodule

时序图如下

image-20250708231819501

always语句#

always语句是重复执行的,他从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便从头开始执行

基本语法:

1
2
3
always @(敏感列表) begin
// 语句块
end
  • 边沿触发(时序逻辑): @(posedge 信号 or negedge 信号)

    1
    2
    3
    4
    always @(posedge clk or negedge rst_n) begin  // 时钟上升沿或复位下降沿触发
    if (!rst_n) q <= 1'b0; // 异步复位
    else q <= d; // 同步数据更新
    end
  • 电平触发: @(信号1, 信号2, ...)@(*)

    1
    2
    3
    4
    5
    6
    7
    8
    always @(a, b, c) begin  // 所有输入信号变化时触发
    y = (a & b) | c;
    end

    // 等效于:
    always @(*) begin // 自动包含所有被读取的信号
    y = (a & b) | c;
    end

过程赋值#

阻塞赋值#

按顺序依次执行,前一条语句完成后才执行下一条。

适用于存在组合逻辑的情况。

1
2
3
4
always @(*) begin
temp = a & b; // 第一条语句立即执行
y = temp | c; // 使用temp的最新值
end

非阻塞赋值#

在当前时间步结束时并行执行,避免竞争条件。

适用于时序逻辑中。

1
2
3
4
always @(posedge clk) begin
q1 <= d; // 当前时钟沿计算,下一周期更新
q2 <= q1; // 使用q1的旧值(上一周期的值)
end

(未完待续...)

参考资料#

RUNOOB

正点原子手把手教你学FPGA

Chipverify