1. 基础概念
1.1. 逻辑值
- 逻辑0,低电平,对应电路中接地GND。
- 逻辑1,高电平,对应电路中的电源VCC。
- 逻辑Z,高阻态,对应电路的悬空。
- 逻辑X,未知态,数据仿真中可能存在,如2个信号同时驱动1个信号,异常状态。
1.2. 标识符
Verilog标识符可以使用字母、数字、下划线和$组合,且必须以字母或下划线开头,且大小写敏感。
- 模块名,模块名称应该简洁、描述性强,反映模块的功能,推荐驼峰命名法(CamelCase)。
- 端口名,端口名称应该简洁、描述性强,反映端口的功能,使用小写字母开头,采用蛇形命名法(snake_case)。
- 信号名称,信号名称应该简洁、描述性强,反映信号的功能,使用小写字母开头,采用蛇形命名法(snake_case)。
- 参数名称,使用全大写字母,采用下划线分隔的形式,如: DATA_WIDTH, ADDR_BITS, FIFO_DEPTH。
- 常量名称,全大写字母,采用下划线分隔的形式。
1.3. 数字进制
Verilog支持2进制、8进制、10进制和16进制。形式如下:
wire [7:0] data1 = 'b10101010; // 二进制
wire [7:0] data2 = 'o325; // 八进制
wire [7:0] data3 = 123; // 十进制
wire [7:0] data4 = 'hAB; // 十六进制
1.4. 数据类型
- Wire (线网):
-
- 用于连接模块之间的信号。
- 声明方式: wire [<msb>:<lsb>] <signal_name>;
- 例如: wire [7:0] data_bus;
- Reg (寄存器):
-
- 用于存储和操作数据。
- 声明方式: reg [<msb>:<lsb>] <signal_name>;
- 例如: reg [3:0] counter;
- Integer (整数):
-
- 用于表示有符号的整数,一般为32bit。
- 声明方式: integer <variable_name>;
- 例如: integer result;
- Real (浮点数):
-
- 用于表示浮点数。
- 声明方式: real <variable_name>;
- 例如: real pi = 3.14;
- Time (时间):
-
- 用于表示仿真时间。
- 声明方式: time <variable_name>;
- 例如: time start_time, end_time;
- Parameter (参数):
-
- 用于定义常量。
- 声明方式: parameter <parameter_name> = <value>;
- 例如: parameter DATA_WIDTH = 8;
- Localparam (局部参数):
-
- 用于定义模块内部的常量。
- 声明方式: localparam <parameter_name> = <value>;
- 例如: localparam BUFFER_SIZE = 1024;
1.5. 运算符
Verilog运算符和C类似,其优先级也和C相近,多用()显式优先级。
- 算术运算符:
-
- +、-、*、/、%(取余)
- 位运算符:
-
- &(按位与)、|(按位或)、^(按位异或)、~(按位取反)
- <<(左移)、>>(右移)
- 逻辑运算符:
-
- &&(逻辑与)、||(逻辑或)、!(逻辑非)
- 关系运算符:
-
- <、>、<=、>=、==(等于)、!=(不等于)
- 条件运算符:
-
- ?:(三目运算符)
- 位选择运算符:
-
- [](选择位宽)
- 连接运算符:
-
- {}(连接)
- 归约操作符
- 归约操作符是一种特殊的操作符,用于对一个向量或位宽信号执行逻辑运算,并返回一个单个的布尔值结果。
- 归约与操作符 (&)
- 归约或操作符 (|)
- 归约异或操作符 (^)
- 归约 NAND 操作符 (~&)
- 归约 NOR 操作符 (~|)
- 归约 XNOR 操作符 (~^)
1.6. 注释
Verilog注释和C语言注释相同,一种是以“/*”符号开始,“*/”结束,另外一种以"//"开头。
1.7. 关键字
常见关键字如下:
- 基本关键字:
-
- module、endmodule: 定义模块的开始和结束
- input、output、inout: 定义模块的输入输出端口
- wire、reg: 定义信号类型
- always: 定义时序逻辑块
- assign: 定义组合逻辑
- 数据类型关键字:
-
- integer、real、time: 定义变量的数据类型
- parameter、localparam: 定义参数
- 流程控制关键字:
-
- if、else、case、default: 条件语句
- for、while、repeat: 循环语句
- begin、end: 语句块定界
- 时间延迟关键字:
-
- #: 定义延迟时间
- @: 定义事件触发
- 其他关键字:
-
- initial: 定义初始化块
- task、function: 定义任务和函数
- specify、specparam: 定义时序约束
- include: 包含外部文件
- macromodule: 定义宏模块
2. 模块
2.1. 概念
模块(module)是 Verilog 的基本设计单位,是用于描述某个设计的功能或结构及与其他模块通信的外部端口。
模块在概念上可等同一个器件,就如调用通用器件(与门、三态门等)或通用宏单元(计数器、ALU、CPU)等。整个IC设计 是由多个模块嵌套构成。
模块有五个主要部分:端口定义、参数定义(可选)、 I/O 说明、内部信号声明、功能定义。模块总是以关键词 module 开始,以关键词 endmodule 结尾。
2.2. 构成
- 端口定义:
-
- 使用 input、output、inout 关键字定义模块的输入、输出和双向端口。
- 端口可以是线网类型(wire)或寄存器类型(reg)。
- 端口宽度可以是单位宽度,也可以是总线宽度。
- 参数定义(可选):
-
- 使用 parameter 或 localparam 关键字定义模块级参数。
- 参数可以是常量表达式,在模块实例化时进行传递。
- I/O 说明:
-
- 描述各个端口的功能和用途。
- 有助于模块的理解和使用。
- 内部信号声明:
-
- 使用 wire 或 reg 关键字声明模块内部的信号。
- 这些信号用于连接模块内部的逻辑电路。
- 功能定义:
-
- 使用 assign 语句定义组合逻辑电路。
- 使用 always 语句块定义时序逻辑电路。
- 可以包含参数、任务和函数等其他 Verilog 语法元素。
2.4. 模块例化
调用指定模块是通过模块例化实现的。
- 基本语法
module_instance_name instance_name (
.port1(signal1),
.port2(signal2),
...,
.portN(signalN)
);
- 参数例化
模块的参数可以指定,也可以使用默认的。
module_name #(
.param1(value1),
.param2(value2),
...,
.paramN(valueN)
) instance_name (
.port1(signal1),
.port2(signal2),
...,
.portN(signalN)
);
2.3. 示例
// 模块定义
module EightMultiplier (
// 输入输出端口定义
input [7:0] a, // 8位输入操作数a
input [7:0] b, // 8位输入操作数b
output [15:0] prod // 16位输出乘积
);
// 参数定义(可选)
parameter DELAY = 5; // 乘法器内部延迟时间
// 内部信号声明
reg [15:0] temp;
// 功能定义
always @(*) begin
#DELAY; // 模拟内部延迟
temp = a * b; // 8位乘法运算
end
// 输出赋值
assign prod = temp;
endmodule
// 顶层模块定义
module TopModule (
input [7:0] a,
input [7:0] b,
output [15:0] product
);
// 例化 8bit_multiplier 模块
EightMultiplier u_multiplier (
.a(a),
.b(b),
.prod(product)
);
endmodule
3. 函数
3.1. 概念
在 Verilog 中,函数是一种重要的功能模块,可以帮助我们封装一些常用的操作,提高代码的可重用性和可读性。
语法形式:
function [return_type] function_name;
input [input_type] input_port1, input_port2, ..., input_portN;
output [output_type] output_port1, output_port2, ..., output_portM;
reg [internal_type] internal_variable1, internal_variable2, ..., internal_variableK;
begin
// 函数体
function_body;
// 返回值
return return_value;
end
endfunction
3.2. 示例
加法函数:
function [31:0] add_32bit;
input [31:0] a, b;
begin
add_32bit = a + b;
end
endfunction
4. 语法
4.1. 阻塞和非阻塞赋值
- 阻塞赋值 (
-
- 阻塞赋值会立即执行,并阻塞当前过程的执行,直到赋值完成。
- 在一个过程中,阻塞赋值语句的执行顺序与它们在代码中的顺序一致。
- 阻塞赋值通常用于组合逻辑电路的描述。
示例:
always @(a, b) begin
c = a + b;
d = c * 2;
end
在这个例子中,c 的值会先被计算并赋值,然后 d 的值才会被计算并赋值。
- 非阻塞赋值 (
-
- 非阻塞赋值会将右侧表达式的值暂存,并在当前过程结束时一次性更新左侧变量的值。
- 在一个过程中,非阻塞赋值语句的执行顺序与它们在代码中的顺序无关,而是在过程结束时一次性更新。
- 非阻塞赋值通常用于时序逻辑电路的描述。
示例:
always @(posedge clk) begin
c <= a + b;
d <= c * 2;
end
在这个例子中,c 和 d 的值会在时钟上升沿时一次性更新,而不是按照代码中的顺序执行。
4.2. 组合逻辑电路和时序逻辑电路
组合逻辑电路和时序逻辑电路是两种基本的数字电路类型,它们在结构和行为上有明显的区别:
- 组合逻辑电路:
-
- 组合逻辑电路的输出仅取决于当前的输入,不依赖于任何历史状态或时序信号。
- 组合逻辑电路没有存储元件,如寄存器或触发器,只有逻辑门电路。
- 组合逻辑电路的输出会立即响应输入的变化,没有延迟。
- 组合逻辑电路通常用于实现简单的逻辑功能,如加法器、译码器等。
- 在 Verilog 中,组合逻辑电路通常使用阻塞赋值 (=) 描述。
- 时序逻辑电路:
-
- 时序逻辑电路的输出不仅取决于当前的输入,还依赖于电路的历史状态。
- 时序逻辑电路包含存储元件,如寄存器或触发器,用于存储和保持状态信息。
- 时序逻辑电路的输出会根据时钟信号的变化而更新,存在一定的延迟。
- 时序逻辑电路通常用于实现复杂的状态机、计数器、移位寄存器等功能。
- 在 Verilog 中,时序逻辑电路通常使用非阻塞赋值 (<=) 描述。
组合逻辑电路和时序逻辑电路的设计和分析方法也有所不同:
- 组合逻辑电路的设计通常基于布尔代数和逻辑门电路,可以使用卡诺图、Quine-McCluskey 算法等方法进行化简和优化。
- 时序逻辑电路的设计需要考虑时钟信号、状态转移图、状态编码等因素,通常需要使用状态机分析和设计方法。
4.3. always语句
Verilog 中的 always 语句是用于描述电路行为的重要语句,它可以用来描述组合逻辑电路和时序逻辑电路。always 语句的基本语法如下:
always @(sensitivity_list)
statement;
其中:
- sensitivity_list 是一个由信号名组成的列表,用于指定 always 语句的敏感性。当列表中的任何一个信号发生变化时,always 语句中的语句就会被执行。
- statement 是一个或多个 Verilog 语句,用于描述电路的行为。这些语句可以是赋值语句、条件语句、循环语句等。
always 语句的使用方式有两种:
- 组合逻辑电路描述:
-
- sensitivity_list 包含所有相关的输入信号。
- 使用阻塞赋值 (=) 描述输出信号的计算过程。
- 输出信号会立即响应输入信号的变化。
例如:
always @(a, b, c)
out = (a & b) | c;
- 时序逻辑电路描述:
-
- sensitivity_list 包含时钟信号和可能的异步复位/置位信号。
- 使用非阻塞赋值 (<=) 描述状态寄存器的更新过程。
- 输出信号会根据时钟信号的变化而更新。
例如:
always @(posedge clk or negedge rst_n)
if (~rst_n)
q <= 1'b0;
else
q <= d;
在实际的 Verilog 代码中,我们通常会结合使用多个 always 语句来描述复杂的数字电路。合理地使用 always 语句可以帮助我们更好地理解和设计数字电路的行为。
4.4. for语句
Verilog 中的 for 循环语句用于实现重复性的操作。它的基本语法如下:
for (initialize; condition; step)
statement;
其中:
- initialize: 初始化语句,在循环开始前执行一次。通常用于设置循环计数器的初始值。
- condition: 循环条件,在每次循环开始前检查。只有当条件为真时,才会执行循环体。
- step: 循环计数器的更新语句,在每次循环结束后执行。通常用于增加或减少计数器的值。
- statement: 循环体,包含一个或多个 Verilog 语句。这些语句会在满足循环条件时重复执行。
下面是一个简单的例子,用 for 循环实现 8 位加法器:
module adder8(a, b, sum);
input [7:0] a, b;
output [7:0] sum;
reg [7:0] sum;
always @(a or b) begin
sum = 0;
for (int i = 0; i < 8; i = i + 1)
sum[i] = a[i] ^ b[i] ^ sum[i];
end
endmodule
在这个例子中:
- 循环初始化将 sum 寄存器清零。
- 循环条件 i < 8 确保循环执行 8 次,对应 8 位加法器的位宽。
- 循环步进 i = i + 1 会在每次循环结束后将计数器 i 增加 1。
- 循环体中的语句实现了单位位的异或运算,得到最终的 8 位加法结果。
4.5. generate语句
Verilog 中的 generate 语句用于在编译时动态生成 Verilog 代码。它可以用于创建重复性的电路结构,如数组、寄存器堆等。
generate 语句的基本语法如下:
generate
if (condition) begin:block_name
// code block
end
else begin:block_name
// code block
end
endgenerate
或者:
generate
for (initialize; condition; step) begin:block_name
// code block
end
endgenerate
generate 语句中的代码块会在编译时展开,而不是在仿真时执行。这意味着 generate 语句中的代码不会被视为普通的 Verilog 语句,而是用于生成新的 Verilog 代码。
generate语句后的代码不能真实对应真实的电路,需要编译展开来对应真实的电路。展开的过程中,通过block_name来标识展开的代码。
4.6. task语句
在 Verilog 中,task 是一种特殊的语句块,用于定义可重复使用的代码段。它类似于函数,但与函数不同的是,task 可以包含任意数量的语句,并且可以修改外部变量的值。
task 的基本语法如下:
task task_name;
// 任务内部的声明和语句
begin
// 任务内部的代码
end
endtask
task 的使用方法如下:
task_name;
4.7. initial语句
在 Verilog 中,initial 语句是一种特殊的语句,用于在仿真开始时执行一次性的初始化操作。
initial 语句的基本语法如下:
initial
begin
// 初始化代码块
end
initial 语句的主要特点包括:
- 仅执行一次: initial 语句只会在仿真开始时执行一次,而不会在仿真过程中重复执行。
- 独立执行: initial 语句独立于 Verilog 模块的其他部分,不受时钟信号或其他事件的控制。
- 可包含任意语句: initial 语句可以包含任意数量的 Verilog 语句,如赋值语句、条件语句、循环语句等。
- 常用于初始化: initial 语句通常用于在仿真开始时初始化寄存器、内存、状态机等硬件元素的初始状态。
初始化寄存器或变量的值:
initial begin
$display("Simulation started at %0t", $time);
#100 $finish;
end
4.8. case 语句
case语句是Verilog中的一种选择结构,用于基于给定的表达式值进行多路选择。
module Example (
input [1:0] sel,
output reg out
);
always @*
begin
case (sel)
2'b00: out = 0; // 当sel等于00时,out为0
2'b01: out = 1; // 当sel等于01时,out为1
2'b10, 2'b11: out = 2; // 当sel等于10或11时,out为2
default: out = 0; // 默认情况下,out为0
endcase
end
endmodule
在上述示例中,根据sel的值进行了多路选择。当sel等于2'b00时,out被赋值为0;当sel等于2'b01时,out被赋值为1;当sel等于2'b10或2'b11时,out被赋值为2;如果sel的值不匹配任何给定的情况,则执行默认的语句将out赋值为0。
5. 编码
5.1. 格雷码
格雷码:格雷码是一种二进制编码方式,其中相邻的两个状态只有一个位的差异。这种编码方式在状态转换时只有一个位发生变化,因此可以有效地减少状态转换过程中可能引起的冲突和震荡问题。格雷码的主要特点是每个状态只有一个位为1,其余位都为0。例如,对于3个状态的状态机,使用格雷码编码如下:
状态 | 格雷码 |
00 | 00 |
01 | 01 |
10 | 11 |
格雷码编码对于设计具有较大状态数量的状态机非常有用,因为它可以减少硬件资源的使用量,并提高系统的可靠性。
5.2. 独热码
独热码:独热码也是一种二进制编码方式,其中每个状态都有唯一的编码,只有一个位为1,其余位为0。这种编码方式使得每个状态之间的转换更加简单和直观。但是,当状态数量增加时,独热码的硬件资源使用量也会随之增加。对于状态机具有较少状态数量且要求直观的状态编码时,独热码是一个不错的选择。
例如,对于3个状态的状态机,使用独热码编码如下:
状态 | 独热码 |
00 | 100 |
01 | 010 |
10 | 001 |
6. 状态机
在Verilog中,状态机是描述系统行为的重要构建块之一。它可以通过组合逻辑或时序逻辑的方式实现,并且通常包含以下几类状态机:
6.1. Moore状态机
在Moore状态机中,输出仅取决于当前状态。状态转移和输出逻辑是分离的。当状态转移发生时,输出信号保持不变直到进入下一个状态。Moore状态机更加简单、易于设计和理解。以下是一个使用Moore状态机实现的示例:
module MooreStateMachine (
input clk,
input reset,
output reg out
);
typedef enum logic [1:0] {
STATE_A,
STATE_B,
STATE_C
} state_t;
reg [1:0] current_state, next_state;
always @(posedge clk or posedge reset)
begin
if (reset) begin
current_state <= STATE_A;
end
else begin
current_state <= next_state;
case (current_state)
STATE_A: begin
next_state = STATE_B;
end
STATE_B: begin
next_state = STATE_C;
end
STATE_C: begin
next_state = STATE_A;
end
default: begin
next_state = STATE_A;
end
endcase
end
end
always @(current_state)
begin
case (current_state)
STATE_A: begin
out = 0;
end
STATE_B: begin
out = 1;
end
STATE_C: begin
out = 1;
end
default: begin
out = 0;
end
endcase
end
endmodule
6.2. Mealy状态机
与Moore状态机相反,Mealy状态机的输出除了依赖于当前状态,还取决于输入信号。因此,输出可以在状态转移过程中发生变化。以下是一个使用Mealy状态机实现的示例:
module MealyStateMachine (
input clk,
input reset,
input in,
output reg out
);
typedef enum logic [1:0] {
STATE_A,
STATE_B,
STATE_C
} state_t;
reg [1:0] current_state, next_state;
always @(posedge clk or posedge reset)
begin
if (reset) begin
current_state <= STATE_A;
end
else begin
current_state <= next_state;
case (current_state)
STATE_A: begin
if (in == 1'b1) begin
next_state = STATE_B;
end
else begin
next_state = STATE_C;
end
end
STATE_B: begin
if (in == 1'b0) begin
next_state = STATE_A;
end
else begin
next_state = STATE_C;
end
end
STATE_C: begin
if (in == 1'b1) begin
next_state = STATE_A;
end
else begin
next_state = STATE_B;
end
end
default: begin
next_state = STATE_A;
end
endcase
end
end
always @(current_state or in)
begin
case (current_state)
STATE_A: begin
if (in == 1'b0) begin
out = 0;
end
else begin
out = 1;
end
end
STATE_B: begin
if (in == 1'b0) begin
out = 1;
end
else begin
out = 0;
end
end
STATE_C: begin
if (in == 1'b0) begin
out = 1;
end
else begin
out = 1;
end
end
default: begin
out = 0;
end
endcase
end
endmodule
6.3. 异步状态机
这种状态机可以处理来自异步输入源的不同时间延迟和频率的信号。异步状态机需要考虑到可能存在的冲突和不确定性。在Verilog中,异步状态机通常使用组合逻辑和if语句来实现状态转移和输出逻辑。以下是一个简单的异步状态机示例:
module AsynchronousStateMachine (
input reset,
input in1,
input in2,
output reg out
);
typedef enum logic [1:0] {
STATE_A,
STATE_B,
STATE_C
} state_t;
reg [1:0] current_state, next_state;
always @*
begin
if (reset) begin
current_state = STATE_A;
end
else begin
case (current_state)
STATE_A: begin
if (in1 && !in2) begin
next_state = STATE_B;
end
else if (!in1 && in2) begin
next_state = STATE_C;
end
else begin
next_state = STATE_A;
end
end
STATE_B: begin
if (!in1 || in2) begin
next_state = STATE_A;
end
else begin
next_state = STATE_B;
end
end
STATE_C: begin
if (!in2 || in1) begin
next_state = STATE_A;
end
else begin
next_state = STATE_C;
end
end
default: begin
next_state = STATE_A;
end
endcase
end
end
always @(current_state)
begin
case (current_state)
STATE_A: begin
out = 0;
end
STATE_B: begin
out = 1;
end
STATE_C: begin
out = 1;
end
default: begin
out = 0;
end
endcase
end
endmodule
7. 竞争和冒险
7.1. 概念
在数字电路设计中,信号传输或者状态变换时在硬件电路上都会有一定的延时。
在组合逻辑电路中,不同路径的输入信号变化传输到同一点门级电路时,在时间上有先有后,这种先后所形成的时间差导致输出产生不应有的尖峰干扰脉冲的现象叫做竞争。
由于竞争的存在,输出信号需要经过一段时间才能达到期望状态,过渡时间内可能产生瞬间的错误输出,这种现象被称为冒险。
竞争不一定有冒险,但冒险一定会有竞争。
7.2. 示例
例如,对于给定逻辑 out = in1 & in0',电路如图所示:
波形:
可以看到,由于in0/in1到AND gate输入pin上delay的不匹配,导致AND的输出out出现一个logic 1的小脉冲,一般也叫毛刺(glitch)。
对于一个简单的AND gate,就会产生毛刺;那么对于一个更复杂的电路,比如:加法器,乘法器,glitch更是起起伏伏,直到一定的时间后,才会输出稳定的值。
这就是信号的竞争与冒险:逻辑上(真值表)输入的变化本来不会导致组合逻辑输出的变化;但是因为在输入逻辑gate的PIN上,输入信号变化时间上的差异,导致组合逻辑的输出端产生一些不必要的0-->1/1-->0变化,出现glitch。
7.3. 消除竞争冒险
消除竞争冒险的几个方法:
- 加封锁脉冲。在输入信号产生竞争冒险的时间内,引入一个脉冲将可能产生尖峰干扰脉冲的门封锁住。封锁脉冲应在输入信号转换前到来,转换结束后消失。
- 加选通脉冲。对输出可能产生尖峰干扰脉冲的门电路增加一个接选通信号的输入端,只有在输入信号转换完成并稳定后,才引入选通脉冲将它打开,此时才允许有输出。在转换过程中,由于没有加选通脉冲,因此,输出不会出现尖峰干扰脉冲。
- 接入滤波电容。由于尖峰干扰脉冲的宽度一般都很窄,在可能产生尖峰干扰脉冲的门电路输出端与地之间接入一个容量为几十皮法的电容就可吸收掉尖峰干扰脉冲。
- 修改逻辑设计,增加冗余项。利用卡诺图,在两个相切的圆之间,增加一个卡诺圈,并加在逻辑表达式之中。
- 同步电路信号的变化都发生在时钟边沿。对于触发器的 D 输入端,只要毛刺不出现在时钟的上升沿并且不满足数据的建立和保持时间,就不会对系统造成危害,因此可认为 D 触发器的 D 输入端对毛刺不敏感。 利用此特性,在时钟边沿驱动下,对一个组合逻辑信号进行延迟打拍,可消除竞争冒险。延迟一拍时钟时,会一定概率地减少竞争冒险的出现。实践表明,最安全的打拍延迟周期是 3 拍,可有效减少竞争冒险的出现。
- 采用格雷码计数器,计数时相邻的数之间只有一个数据 bit 发生了变化,所以能有效的避免竞争冒险。