模块基本结构
一个Verilog module定义了一个设计中的一个基本构建块. 它类似于软件编程中的一个函数或类. 一个Verilog module封装了一部分电路的功能, 并可以通过端口与其他module进行交互.
一个Verilog module的基本结构包括以下几个部分:
- Module Declaration (模块声明):
module
关键字开始一个模块的定义.- 模块名紧随其后. 模块名应该具有描述性, 并遵循Verilog的命名规则 (字母, 数字和下划线, 且不能以数字开头) .
- 端口列表在模块名后面的括号中声明. 端口是module与外部环境进行通信的接口.
module module_name (port1, port2, ..., portN);
- Port Declaration (端口声明):
- 端口需要在模块声明之后进行声明, 指定端口的方向 (
input
,output
,inout
) 和数据类型 (例如wire
,reg
) . input
端口接收来自外部的信号.output
端口将信号发送到外部.inout
端口是双向端口, 可以接收和发送信号.- 端口还可以声明为
wire
或reg
类型.wire
用于连接不同的元件, 而reg
用于存储数据 (通常在always
块中使用) .
input wire port1; // 输入端口, wire类型
output reg port2; // 输出端口, reg类型
inout wire port3; // 双向端口, wire类型
input wire [7:0] port4; // 8位输入端口, wire类型
- Internal Declarations (内部声明):
- 在模块内部, 你需要声明内部信号 (
wire
或reg
) 和参数. 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
- Behavioral Description (行为描述): 这部分描述了模块的功能. Verilog提供了多种方式来描述电路的行为, 包括:
- Continuous Assignment (连续赋值): 使用
assign
关键字, 用于描述组合逻辑. - Procedural Blocks (过程块): 使用
always
和initial
关键字.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)
);
- 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位输入a
和b
, 一个8位输出sum
和一个输出overflow
.- 端口声明指定了每个端口的方向和类型.
reg [8:0] temp_sum;
声明了一个内部的9位寄存器, 用于存储加法结果.always @(a, b)
块描述了加法器的行为. 当输入a
或b
发生变化时, 块内的代码将被执行.temp_sum = a + b;
计算a
和b
的和, 并将结果存储在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
) 、下划线 (_
) 和美元符号 ($
) 组成. - 必须以字母或下划线开头.
- 区分大小写. 例如,
mySignal
和MySignal
是不同的标识符. - 建议使用有意义的名称, 提高代码可读性.
- 避免使用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 支持以下几种类型的常量:
- 整数常量 (Integer Constants)
- 表示方法: 由数字和可选的前缀组成.
- 格式:
[size]'base value
size
(可选): 指定常量的大小 (位宽) , 以十进制整数表示. 如果没有指定大小, 编译器会根据具体环境自动确定大小.base
: 指定常量的进制, 可以是以下几种:b
或B
: 二进制 (base-2)o
或O
: 八进制 (base-8)d
或D
: 十进制 (base-10)h
或H
: 十六进制 (base-16)
value
: 表示常量的数值, 根据base
对应的进制来书写.
- 示例:
8'b10101010
: 8 位二进制数 1010101012'h3A7
: 12 位十六进制数 3A732'd255
: 32 位十进制数 255'd10
: 未指定大小的十进制数 10 (编译器自动确定大小, 通常为 32 位)-8'd5
: 8位十进制数 -5
- 注意:
- 如果没有指定大小, 则默认为 32 位.
- 如果没有指定进制, 则默认为十进制.
- 可以使用下划线
_
来提高可读性, 例如16'b1111_0000_1010_0101
. - 负数表示: 可以在常量前加负号
-
, 表示有符号数. 负数通常使用补码表示.
- 实数常量 (Real Constants)
- 表示方法: 可以使用十进制或科学计数法表示.
- 示例:
3.14159
2.0
1.0e-6
2.3E5
- 注意:
- 实数常量在 Verilog 中通常用于仿真, 综合工具可能不支持实数.
- 实数常量默认为双精度浮点数.
- 字符串常量 (String Constants)
- 表示方法: 用双引号
"
括起来的字符序列. - 示例:
"Hello, World!"
"Verilog is fun"
- 注意字符串常量可以包含转义字符, 例如
\n
(换行),\t
(制表符),\\
(反斜杠),\"
(双引号) 等.
- 参数常量 (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
(除了 tri0
和 tri1
线网).
常见的线网类型包括:
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}
其中, expression1
、expression2
等可以是:
- 单个 bit
- 多 bit 信号 (向量)
- 常量
- 表达式
举例说明:
- 连接两个单 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
- 连接多个向量:
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
- 复制操作:
位拼接运算符还可以用于复制信号. 语法是
{重复次数{信号}}
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
(高阻) 信号的处理遵循一套特定的规则.
x
的传播: 如果逻辑门的任何输入为x
, 则输出通常也为x
. 这是因为如果输入未知, 则无法确定输出.z
的行为:z
通常被视为 "弱" 信号. 它的行为取决于门的类型和其他输入信号.
下面是针对一些常见 Verilog 内置门对 x
和 z
的处理的具体情况:
and
、nand
:- 如果任何输入为
0
, 则and
的输出为0
,nand
的输出为1
, 与输入是x
还是z
无关. 这是因为0
起主导作用. - 如果所有输入都为
1
, 则and
的输出为1
,nand
的输出为0
. - 其他情况下 (例如, 一个输入为
1
, 另一个输入为x
或z
) , 输出为x
.
- 如果任何输入为
or
、nor
:- 如果任何输入为
1
, 则or
的输出为1
,nor
的输出为0
, 与输入是x
还是z
无关. 这是因为1
起主导作用. - 如果所有输入都为
0
, 则or
的输出为0
,nor
的输出为1
. - 其他情况下 (例如, 一个输入为
0
, 另一个输入为x
或z
) , 输出为x
.
- 如果任何输入为
xor
、xnor
:- 如果任何输入为
x
或z
, 则输出为x
. 这是因为异或和同或对输入的值非常敏感.
- 如果任何输入为
not
、buf
:- 如果输入为
x
或z
, 则输出为x
. not(x)
=x
not(z)
=x
buf(x)
=x
buf(z)
=x
- 如果输入为
- 三态门 (
bufif0
,bufif1
,notif0
,notif1
):bufif1
: 如果控制信号为1
, 则表现如同buf
; 如果控制信号为0
, 则输出为z
; 如果控制信号为x
或z
, 则输出为z
.bufif0
: 如果控制信号为0
, 则表现如同buf
; 如果控制信号为1
, 则输出为z
; 如果控制信号为x
或z
, 则输出为z
.notif1
: 如果控制信号为1
, 则表现如同not
; 如果控制信号为0
, 则输出为z
; 如果控制信号为x
或z
, 则输出为z
.notif0
: 如果控制信号为0
, 则表现如同not
; 如果控制信号为1
, 则输出为z
; 如果控制信号为x
或z
, 则输出为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