模块基本结构

一个Verilog module定义了一个设计中的一个基本构建块. 它类似于软件编程中的一个函数或类. 一个Verilog module封装了一部分电路的功能, 并可以通过端口与其他module进行交互.

一个Verilog module的基本结构包括以下几个部分:

  1. Module Declaration (模块声明):
  • module 关键字开始一个模块的定义.
  • 模块名紧随其后. 模块名应该具有描述性, 并遵循Verilog的命名规则 (字母, 数字和下划线, 且不能以数字开头) .
  • 端口列表在模块名后面的括号中声明. 端口是module与外部环境进行通信的接口.
module module_name (port1, port2, ..., portN);
  1. Port Declaration (端口声明):
  • 端口需要在模块声明之后进行声明, 指定端口的方向 (input, output, inout) 和数据类型 (例如 wire, reg) .
  • input 端口接收来自外部的信号.
  • output 端口将信号发送到外部.
  • inout 端口是双向端口, 可以接收和发送信号.
  • 端口还可以声明为 wirereg 类型. wire 用于连接不同的元件, 而 reg 用于存储数据 (通常在 always 块中使用) .
input  wire  port1;       // 输入端口, wire类型
output reg   port2;       // 输出端口, reg类型
inout  wire  port3;       // 双向端口, wire类型
input  wire [7:0] port4;    // 8位输入端口, wire类型
  1. Internal Declarations (内部声明):
  • 在模块内部, 你需要声明内部信号 (wirereg) 和参数.
  • wire 用于连接不同的逻辑门或其他模块.
  • reg 用于存储数据, 通常在时序逻辑中使用.
  • parameter 用于定义常量, 可以在模块实例化时被覆盖.
  • localparam 用于定义局部常量, 只能在模块内部使用.
wire   internal_wire;       // 内部wire信号
reg    internal_reg;        // 内部reg信号
parameter WIDTH = 8;        // 参数定义
localparam DELAY = 1;       // 局部参数定义
reg [WIDTH-1:0] data_bus;   // 基于参数定义的reg
  1. Behavioral Description (行为描述): 这部分描述了模块的功能. Verilog提供了多种方式来描述电路的行为, 包括:
  • Continuous Assignment (连续赋值): 使用 assign 关键字, 用于描述组合逻辑.
  • Procedural Blocks (过程块): 使用 alwaysinitial 关键字. always 块可以描述组合逻辑或时序逻辑, 而 initial 块通常用于初始化.
  • Gate-Level Instantiation (门级实例化): 使用 Verilog 预定义的门元件 (例如 and, or, not) .
  • Module Instantiation (模块实例化): 将其他模块作为子模块使用.
// 连续赋值
assign internal_wire = port1 & port2;
 
// always 块 (时序逻辑)
always @(posedge clock) begin
	if (reset) begin
		internal_reg <= 0;
	end else begin
		internal_reg <= port1;
	end
end
 
// always 块 (组合逻辑)
always @(*) begin
	port2 = internal_wire | internal_reg;
end
 
// 模块实例化
another_module instance_name (
	.input1(port1),
	.output1(internal_wire)
);
  1. Module End (模块结束):
  • 使用 endmodule 关键字结束模块的定义.
endmodule

Example (例子):

一个简单的加法器模块:

module adder (
    input  wire  [7:0] a,
    input  wire  [7:0] b,
    output reg   [7:0] sum,
    output reg   overflow
);
 
reg [8:0] temp_sum; // 用于检测溢出的临时变量
 
always @(a, b) begin
    temp_sum = a + b;
    sum = temp_sum[7:0];
    overflow = temp_sum[8];
end
 
endmodule

在这个例子中:

  • module adder (...) 声明了一个名为 adder 的模块, 它有四个端口: 两个8位输入 ab, 一个8位输出 sum 和一个输出 overflow.
  • 端口声明指定了每个端口的方向和类型.
  • reg [8:0] temp_sum; 声明了一个内部的9位寄存器, 用于存储加法结果.
  • always @(a, b) 块描述了加法器的行为. 当输入 ab 发生变化时, 块内的代码将被执行.
  • temp_sum = a + b; 计算 ab 的和, 并将结果存储在 temp_sum 中.
  • sum = temp_sum[7:0];temp_sum 的低8位赋值给输出 sum.
  • overflow = temp_sum[8];temp_sum 的最高位 (第9位) 赋值给输出 overflow, 用于指示是否发生溢出.
  • endmodule 结束模块的定义.

基本语法

间隔符

Verilog使用空格、制表符 (\t) 和换行符来作为间隔符. 间隔符用于分隔Verilog代码中的各个元素, 提高代码的可读性. Verilog编译器通常会忽略多余的间隔符, 但合理的间隔可以使代码更易于理解和维护.

注释符

Verilog支持两种注释方式:

  • 单行注释:// 开头, 到行尾结束. 例如:
  • 多行注释:/* 开始, 以 */ 结束. 可以跨越多行. 多行注释不允许嵌套.

标识符

标识符是用来命名Verilog代码中的各种元素, 如模块名、端口名、变量名、信号名等. Verilog标识符的命名规则如下:

  • 可以由字母 (a-z, A-Z) 、数字 (0-9) 、下划线 (_) 和美元符号 ($) 组成.
  • 必须以字母或下划线开头.
  • 区分大小写. 例如, mySignalMySignal 是不同的标识符.
  • 建议使用有意义的名称, 提高代码可读性.
  • 避免使用Verilog的关键词作为标识符. 合法的标识符示例:
  • my_module
  • signal_1
  • _data_in
  • Count 不合法的标识符示例:
  • 1signal (以数字开头)
  • my-signal (包含非法字符 -)

关键词

关键词是Verilog语言中预定义的具有特殊含义的单词, 用于表示Verilog的语法结构和控制语句. 关键词不能用作标识符. 以下是一些常见的Verilog关键词:

  • module
  • input
  • output
  • inout

逻辑值集合

  • 0 (逻辑 0, 假)
    • 表示低电平或者逻辑假.
    • 在电路中, 通常对应于接近地电位的电压值.
  • 1 (逻辑 1, 真)
    • 表示高电平或者逻辑真.
    • 在电路中, 通常对应于接近电源电压的电压值.
  • x (未知)
    • 表示信号的值是未知的.
    • x 可能出现在以下情况中:
      • 未初始化的信号: 在仿真开始时, 如果没有给寄存器或线网赋初值, 它们的值通常为 x.
      • 竞争冒险: 当信号由于延迟不匹配而产生不确定的状态时.
      • 故意忽略: 在某些情况下, 设计者可能使用 x 来表示"无关紧要"或"不关心"的状态, 从而允许综合器进行优化.
    • 在仿真中, x 通常显示为红色或其他特殊的颜色, 以提醒设计者注意潜在的问题.
  • z (高阻抗)
    • 表示信号线处于高阻抗状态, 即没有驱动源.
    • z 通常用于三态缓冲器或共享总线.
    • 当一个三态缓冲器的使能端无效时, 它的输出将呈现高阻抗状态, 允许其他设备驱动该信号线.
    • 在 Verilog 中, 可以使用 highz0 (拉低电阻) 和 highz1 (拉高电阻) 来模拟弱驱动的高阻抗状态.

例子:

module example (
  input  enable,
  output reg data_out
);
 
  always @(*) begin
    if (enable) begin
      data_out = 1; // 输出高电平
    } else begin
      data_out = z; // 输出高阻抗
    end
  end
 
endmodule

在这个例子中, data_out 的值取决于 enable 信号. 当 enable 为真 (1) 时, data_out 输出 1. 当 enable 为假 (0) 时, data_out 输出 z, 进入高阻抗状态.

常量及其表示

Verilog 中的常量 (Constants) 是指在程序编译时就确定其值的量, 在程序运行过程中其值不能被改变. 常量用于表示固定的数值或字符串, 可以提高代码的可读性和可维护性.

Verilog 支持以下几种类型的常量:

  1. 整数常量 (Integer Constants)
  • 表示方法: 由数字和可选的前缀组成.
  • 格式: [size]'base value
    • size (可选): 指定常量的大小 (位宽) , 以十进制整数表示. 如果没有指定大小, 编译器会根据具体环境自动确定大小.
    • base: 指定常量的进制, 可以是以下几种:
      • bB: 二进制 (base-2)
      • oO: 八进制 (base-8)
      • dD: 十进制 (base-10)
      • hH: 十六进制 (base-16)
    • value: 表示常量的数值, 根据 base 对应的进制来书写.
  • 示例:
    • 8'b10101010: 8 位二进制数 10101010
    • 12'h3A7: 12 位十六进制数 3A7
    • 32'd255: 32 位十进制数 255
    • 'd10: 未指定大小的十进制数 10 (编译器自动确定大小, 通常为 32 位)
    • -8'd5: 8位十进制数 -5
  • 注意:
    • 如果没有指定大小, 则默认为 32 位.
    • 如果没有指定进制, 则默认为十进制.
    • 可以使用下划线 _ 来提高可读性, 例如 16'b1111_0000_1010_0101.
    • 负数表示: 可以在常量前加负号 -, 表示有符号数. 负数通常使用补码表示.
  1. 实数常量 (Real Constants)
  • 表示方法: 可以使用十进制或科学计数法表示.
  • 示例:
    • 3.14159
    • 2.0
    • 1.0e-6
    • 2.3E5
  • 注意:
    • 实数常量在 Verilog 中通常用于仿真, 综合工具可能不支持实数.
    • 实数常量默认为双精度浮点数.
  1. 字符串常量 (String Constants)
  • 表示方法: 用双引号 " 括起来的字符序列.
  • 示例:
    • "Hello, World!"
    • "Verilog is fun"
  • 注意字符串常量可以包含转义字符, 例如 \n (换行), \t (制表符), \\ (反斜杠), \" (双引号) 等.
  1. 参数常量 (Parameter Constants)
  • 表示方法: 使用 parameter 关键字声明的常量.
  • 特点: 可以在模块实例化时被覆盖 (override) , 从而实现参数化设计.
  • 示例:
module adder #(
	parameter WIDTH = 8  // 默认宽度为 8 位
) (
	input  logic [WIDTH-1:0] a, b,
	output logic [WIDTH-1:0] sum
);
 
	assign sum = a + b;
 
endmodule
 
// 实例化时可以覆盖参数
module top;
	logic [7:0]  x, y, z;
	logic [15:0] p, q, r;
 
	adder #( .WIDTH(8) )  adder8  (x, y, z);  // 使用默认宽度
	adder #( .WIDTH(16) ) adder16 (p, q, r);  // 覆盖参数, 宽度为 16 位
endmodule
  • 注意:
  • parameter 声明的常量在编译时确定, 但可以在模块实例化时通过 . 操作符覆盖.
  • localparam 声明的常量类似于 parameter, 但不能在模块实例化时被覆盖. localparam 常用于模块内部的局部常量.

数据类型

线网类型 (Net Data Types)

线网代表物理连接, 它们没有存储能力, 必须由驱动源持续驱动. 如果没有驱动源, 线网的值为高阻态 z (除了 tri0tri1 线网).

常见的线网类型包括:

  • wire: 最常用的线网类型, 代表简单的连接线.
  • tri: 三态线网, 可以有多个驱动源, 但只有一个能有效驱动.
  • tri0: 三态线网, 如果没有驱动源, 则默认为0.
  • tri1: 三态线网, 如果没有驱动源, 则默认为1.
  • supply0: 代表逻辑0, 通常用于接地.
  • supply1: 代表逻辑1, 通常用于电源.
  • wand (wired-AND): 线与, 多个驱动源连接到同一线网, 结果为所有驱动源的逻辑与.
  • wor (wired-OR): 线或, 多个驱动源连接到同一线网, 结果为所有驱动源的逻辑或.
  • trireg: 具有电荷保持能力的线网, 可以存储电荷一段时间, 类似于动态存储器.

声明线网的例子:

wire data_bus;
tri enable_line;
supply1 power;
supply0 ground;
wand interrupt_line;

变量类型 (Variable Data Types)

变量类型可以存储数据, 在程序执行过程中可以改变其值. 变量必须在 reg, integer, time, real, realtime, genvar, event块中声明.

常见的变量类型包括:

  • reg: 最基本的变量类型, 可以存储一位或多位数据. reg 并不一定代表寄存器, 它只是一个变量, 可以存储值.
  • integer: 用于存储整数值.
  • time: 用于存储时间值, 通常用于仿真.
  • real: 用于存储浮点数值.
  • realtime: 用于存储浮点数时间值.
  • genvar: 用于生成块的变量, 只能在生成块中使用.
  • event: 用于事件触发.

声明变量的例子:

reg data;
reg [7:0] counter;  // 8位寄存器
integer index;
time current_time;
real voltage;
event trigger;

运算符

类型符号功能说明类型符号功能说明
算术运算符+二进制加关系运算符>大于
-二进制减<小于
-2的补码>=大于或等于
*二进制乘小于或等于
/二进制除==相等
%取余(求模)!=不相等
**指数幂===case 中使用的等于
!==case 中使用的不等于
按位逻辑运算符~非(取反)缩位运算符 (一元运算符)&缩位与
&按位与~&缩位与非
|按位或|缩位或
^按位异或~|缩位或非
^^按位同或^缩位异或
^^缩位同或
逻辑运算符!逻辑非移位运算符>>逻辑右移
&&逻辑与<<逻辑左移
||逻辑或>>>算术右移
<<<<算术左移
位拼接运算符 {,} ||||将多个操作数拼接成一个操作数条件运算符?:根据条件表达式是否成立, 选择表达式

缩位运算符

假设有一个 4 位宽的信号 data, 其值为 4'b1011.

  • &data 的结果是 1 & 0 & 1 & 1 = 0
  • |data 的结果是 1 | 0 | 1 | 1 = 1
  • ^data 的结果是 1 ^ 0 ^ 1 ^ 1 = 1
  • ~&data 的结果是 ~(1 & 0 & 1 & 1) = ~0 = 1
  • ~|data 的结果是 ~(1 | 0 | 1 | 1) = ~1 = 0
  • ~^data 的结果是 ~(1 ^ 0 ^ 1 ^ 1) = ~1 = 0

位拼接运算符

位拼接运算符在 Verilog 中用于将多个较小的表达式或信号连接起来, 形成一个更大的表达式或信号. 符号是 { }.

基本语法: {expression1, expression2, ..., expressionN} 其中, expression1expression2 等可以是:

  • 单个 bit
  • 多 bit 信号 (向量)
  • 常量
  • 表达式

举例说明:

  1. 连接两个单 bit 信号:
module example;
  reg a, b;
  wire [1:0] result;
 
  initial begin
	a = 1'b1;
	b = 1'b0;
	$display("a = %b, b = %b", a, b);
	$display("result = %b", result);
	#1 $finish;
  end
 
  assign result = {a, b}; // 将 a 和 b 连接成一个 2 bit 信号
endmodule

输出:

a = 1, b = 0
result = 10
  1. 连接多个向量:
module example;
  reg [1:0] vec1, vec2;
  wire [3:0] result;
 
  initial begin
	vec1 = 2'b10;
	vec2 = 2'b01;
	$display("vec1 = %b, vec2 = %b", vec1, vec2);
	$display("result = %b", result);
	#1 $finish;
  end
 
  assign result = {vec1, vec2}; // 将 vec1 和 vec2 连接成一个 4 bit 信号
endmodule

输出:

vec1 = 10, vec2 = 01
result = 1001
  1. 复制操作: 位拼接运算符还可以用于复制信号. 语法是 {重复次数{信号}}
module example;
  reg a;
  wire [3:0] result;
 
  initial begin
	a = 1'b1;
	$display("a = %b", a);
	$display("result = %b", result);
	#1 $finish;
  end
 
  assign result = {4{a}}; // 将 a 复制 4 次
endmodule

输出:

a = 1
result = 1111

注意点:

  • 位拼接运算符中的表达式必须是确定位宽的.
  • 位拼接的结果的位宽是所有表达式位宽的总和.
  • 复制操作中的重复次数必须是常量表达式.

基本门级元件

Verilog 的内置逻辑门对 x (未知) 和 z (高阻) 信号的处理遵循一套特定的规则.

  1. x 的传播: 如果逻辑门的任何输入为 x, 则输出通常也为 x. 这是因为如果输入未知, 则无法确定输出.
  2. z 的行为: z 通常被视为 "弱" 信号. 它的行为取决于门的类型和其他输入信号.

下面是针对一些常见 Verilog 内置门对 xz 的处理的具体情况:

  • andnand:
    • 如果任何输入为 0, 则 and 的输出为 0, nand 的输出为 1, 与输入是 x 还是 z 无关. 这是因为 0 起主导作用.
    • 如果所有输入都为 1, 则 and 的输出为 1, nand 的输出为 0.
    • 其他情况下 (例如, 一个输入为 1, 另一个输入为 xz) , 输出为 x.
  • ornor:
    • 如果任何输入为 1, 则 or 的输出为 1, nor 的输出为 0, 与输入是 x 还是 z 无关. 这是因为 1 起主导作用.
    • 如果所有输入都为 0, 则 or 的输出为 0, nor 的输出为 1.
    • 其他情况下 (例如, 一个输入为 0, 另一个输入为 xz) , 输出为 x.
  • xorxnor:
    • 如果任何输入为 xz, 则输出为 x. 这是因为异或和同或对输入的值非常敏感.
  • notbuf:
    • 如果输入为 xz, 则输出为 x.
    • not(x) = x
    • not(z) = x
    • buf(x) = x
    • buf(z) = x
  • 三态门 (bufif0, bufif1, notif0, notif1):
    • bufif1: 如果控制信号为 1, 则表现如同 buf; 如果控制信号为 0, 则输出为 z; 如果控制信号为 xz, 则输出为 z.
    • bufif0: 如果控制信号为 0, 则表现如同 buf; 如果控制信号为 1, 则输出为 z; 如果控制信号为 xz, 则输出为 z.
    • notif1: 如果控制信号为 1, 则表现如同 not; 如果控制信号为 0, 则输出为 z; 如果控制信号为 xz, 则输出为 z.
    • notif0: 如果控制信号为 0, 则表现如同 not; 如果控制信号为 1, 则输出为 z; 如果控制信号为 xz, 则输出为 z.

基础例子

与门

module and_gate (
  input  a,
  input  b,
  output y
);
 
  assign y = a & b;
 
endmodule

非门

module not_gate (
  input  a,
  output y
);
 
  assign y = ~a;
 
endmodule
 

D 触发器

module d_ff (
  input  clk,
  input  rst,
  input  d,
  output reg q
);
 
  always @(posedge clk or posedge rst) begin
    if (rst) begin
      q <= 1'b0;
    end else begin
      q <= d;
    end
  end
 
endmodule

always 块指定了一个过程块, 该块将在特定的事件发生时执行. 在这种情况下, 块将在 clk 信号的上升沿 (posedge clk) 或 rst 信号的上升沿 (posedge rst) 执行

q <= 1'b0;: 将输出 q 设置为逻辑 0. <= 是非阻塞赋值, 这意味着当前语句的执行不会等待赋值操作完成, 而是继续执行后续语句. 所有的非阻塞赋值会在 always 块结束时同时更新. 1'b0 表示1位二进制数0