Verilog学习笔记(一)入门和基础语法BY电棍233
由于某些不可抗拒的因素和各种的特殊原因,主要是因为我是微电子专业的,我需要去学习一门名为verilog的硬件解释语言,由于我是在某西部地区的神秘大学上学,这所大学的数字系统设计基础课程的考试居然是手写代码,十分的令人好笑,教育资源的匮乏可见一斑。这种编程课靠听课记笔记显然是没有办法学好的,怎么办,只有敲代码。下面的笔记是我借助AI+网上帖子+自己实践手敲代码验证写出来的。这部分笔记是从verilog入门到基础知识部分。
引入与描述性解释
verilog是硬件描述语言,诞生于1988年,现在所用的verilog标准是2005年的。
Verilog 主要用于以下领域:
- 数字电路设计:描述数字系统的行为和结构。
- 仿真:验证设计是否按预期工作。
- 综合:将 Verilog 代码转换成门级网表,进而可以用于制造芯片。
- 测试和验证:编写测试平台来验证设计的正确性。
语言特点
- 模块化:Verilog 设计是基于模块的,每个模块代表设计中的一个功能单元。
- 并行性:Verilog 代码模拟硬件的并行行为,模块内的语句并行执行。
- 时序:可以描述信号的时序关系,如延迟和时序控制。
- 数据流:通过连续赋值语句(assign)和过程块(always)来描述数据流。
基本语法
以下是一些 Verilog 的基本语法元素:
- 模块定义 :使用
module
关键字定义模块,endmodule
结束模块定义。 - 端口声明:模块的输入和输出通过端口声明来定义。
- 变量类型:包括 wire(用于连接逻辑门),reg(用于存储值),integer 等。
- 逻辑操作 :使用
&&
(逻辑与),||
(逻辑或),!
(逻辑非)等。 - 算术操作:加(+),减(-),乘(*),除(/)等。
- 条件语句:if-else,case,casex,casez 等。
- 循环语句:for,while,repeat 等。
模块
模块是verilog语法中的基础,module可以视为verilog中的基础,是一个具备输入和输出的黑盒子。verilog中的每一个模块必须是以.v为后缀的,且文件名即为模块名称。
verilog
module top_module(
input in1,
input in2,
output out
)
***
end module
但是在实际过程中往往不可能只用到一个模块,通常是会用到多个模块,类似于c语言中的头文件,在verilog中也可以相互去引入。在 Verilog 中,模块实例化是将一个模块(module)的实例(instance)放入另一个模块中的过程。这是构建复杂硬件设计的基础,允许设计者重用和组合较小的设计单元来创建更大的系统。举个例子,在主模块中调用与门模块。
verilog
module and_gate (input wire A, input wire B, output wire Y);
assign Y = A & B;//输出Y=A&B
endmodule
把上面的模块保存为and_gate.v
然后再定义一个top_module.v文件,在其中实例化应用and_gate 模块。
verilog
module top_module;
// 声明内部信号
wire A, B, Y;
// 实例化 and_gate 模块
and_gate instance_name (
.A(A), // 将 and_gate 的 A 端口连接到 top_module 的 A 信号
.B(B), // 将 and_gate 的 B 端口连接到 top_module 的 B 信号
.Y(Y) // 将 and_gate 的 Y 端口连接到 top_module 的 Y 信号
);
// 其他模块逻辑...
endmodule
逻辑块
在verilog中,逻辑块被用来描述硬件的行为和结构,主要有两种类型的逻辑块,分别是initial块和always块。
always逻辑块可以用来构建组合逻辑和时序逻辑,复杂的逻辑操作都需要出于此该逻辑块中。当然了,always逻辑块需要被写在模块module中。
下面是一些简单的always逻辑块的示例代码:
首先是组合逻辑的always逻辑块,其次是时许逻辑的always逻辑块。
verilog
always @(*) begin
out = a & b; // 输出是输入 a 和 b 的逻辑与
end
verilog
reg data_reg;//reg是一种数据存储类型
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
data_reg <= 1'b0; // 当复位信号为低时,将寄存器清零
end else begin
data_reg <= data_in; // 在时钟上升沿,将输入数据赋值给寄存器
end
end
其中posedge是上升沿,negedge是下降沿。
完整的示例代码如下,在模块中嵌套逻辑块:
verilog
module top_module();
always @(posedge clk) begin
....
end
endmodule
generate逻辑块通常是结合for循环使用的,在 Verilog 中,generate
逻辑块是一个非常有用的结构,它允许你根据参数或循环来生成硬件结构。这可以用来创建可参数化的模块,或者生成大量的相同或相似的硬件结构,而不需要手动编写重复的代码。主要的用途有:
1.操作向量
2.模块重复多次实例化。
Verilog 中有三种类型的 generate
逻辑块:
-
generate-for
:用于基于循环生成多个相同的硬件实例。 -
generate-if
:用于基于条件判断生成或不生成硬件。 -
generate-case
:用于基于多个条件生成不同的硬件结构。
下面是一个generate-for的实例:
verilog
module register_array(
input wire clk,
input wire reset,
input wire [3:0] data_in,
output wire [3:0] data_out
);
parameter NUM_REGISTERS = 4;
generate//generate-for逻辑块
genvar i;
for (i = 0; i < NUM_REGISTERS; i = i + 1) begin : gen_registers
reg [3:0] reg_i;
always @(posedge clk or posedge reset) begin
if (reset) begin
reg_i <= 4'b0000;
end else begin
reg_i <= data_in;
end
end
assign data_out[i] = reg_i;
end
endgenerate
endmodule//结束module的定义
initial逻辑块
在 Verilog 中,initial
逻辑块用于在仿真开始时执行一次性的初始化操作。它与 always
块不同,always
块在仿真过程中会无限循环执行,而 initial
块只在仿真开始时执行一次,之后就不会再执行。
initial
块通常用于以下几种情况:
-
初始化寄存器的值。
-
定义测试平台的激励(stimulus)。
-
配置仿真环境,如设置初始时间分辨率。
-
打印仿真开始时的消息。
initial逻辑块的示例代码如下:
verilog
module testbench;
reg my_reg;
initial begin
// 在仿真时间0时初始化my_reg为0
my_reg = 0;
#10; // 等待10个时间单位
my_reg = 1; // 在仿真时间10时将my_reg设置为1
#20; // 再等待20个时间单位
my_reg = 0; // 在仿真时间30时将my_reg重新设置为0
$display("Simulation ended at time %t",$time);
end
endmodule
赋值方式
在verilog中有三种赋值方式,分别是连续赋值,阻塞赋值与非阻塞赋值,如下:
在Verilog硬件描述语言中,赋值语句用于定义信号的行为。根据赋值的类型,Verilog中的赋值可以分为以下几种:阻塞赋值(Blocking Assignment)、非阻塞赋值(Non-Blocking Assignment)和连续赋值(Continuous Assignment)。阻塞赋值使用等号(=
)表示。在执行过程中,当前语句必须等待赋值完成之后,才会执行下一条语句。这意味着在同一个always块中,阻塞赋值的执行是顺序的。非阻塞赋值使用小于等于号(<=
)表示。在执行过程中,当前语句不会阻塞后续语句的执行。在同一个always块中,非阻塞赋值的操作是并行的,即它们同时开始但完成时间不一定相同。连续赋值使用等号(=
)表示,但它不同于阻塞赋值,因为它没有放在always
块中。连续赋值描述的是组合逻辑,只要右侧的表达式发生变化,赋值就会立即更新左侧的信号。
示例代码如下:
verilog
assign x=y//此为连续赋值,用assign关键词
// 组合块
always @(*) begin//此为阻塞赋值
out1 = a ;
a = b ;
out2 = a ;
end
// 时序块
always @(posedge clk) begin//此为非阻塞赋值
out1 <= a ;
a <= b ;
out2 <= a ;
end
基础语法
逻辑值与逻辑运算
逻辑值
在Verilog中,主要有以下四种逻辑值:
- 0:表示逻辑低电平,通常对应于电压的0V或GND。
- 1:表示逻辑高电平,通常对应于电源电压(如5V或3.3V)。
- x:表示未知逻辑值,可以是0或1。
- z:表示高阻态,通常用于三态门或双向端口,表示输出既不是高电平也不是低电平,而是处于不驱动状态。
逻辑运算
Verilog支持以下逻辑运算,下面这些是按位运算,假如要做逻辑运算,则用&&,||这些
- 逻辑与(&) :
- 运算规则:只有当两个操作数都为1时,结果才为1;否则结果为0。
- 示例:
4'b1010 & 4'b1100
的结果为4'b1000
。
- 逻辑或(|) :
- 运算规则:只要有一个操作数为1,结果就为1;只有当两个操作数都为0时,结果才为0。
- 示例:
4'b1010 | 4'b1100
的结果为4'b1110
。
- 逻辑异或(^) :
- 运算规则:当两个操作数不相同时,结果为1;当两个操作数相同时,结果为0。
- 示例:
4'b1010 ^ 4'b1100
的结果为4'b0110
。
- 逻辑同或(~^ 或 ^~) :
- 运算规则:当两个操作数相同时,结果为1;当两个操作数不相同时,结果为0。
- 示例:
4'b1010 ~^ 4'b1100
的结果为4'b1000
。
- 逻辑非(! 或 ~) :
- 运算规则:将操作数的逻辑值取反。
- 示例:
!4'b1010
的结果为4'b0101
,~4'b1010
的结果也是4'b0101
。
这里补充一下在verilog中的数字表示形式:'b在verilog中是二进制的意思,4'b1100是4位二进制数1100,这种表示方法我没学习的时候是一头雾水的。'b是二进制,'h是八进制,'d是十进制。
变量(wire,reg)
在Verilog中,wire
和reg
是两种不同的变量类型,它们用于表示硬件电路中的不同信号和功能。
wire
wire
类型用于表示硬件电路中的物理连线。它通常用于表示模块间的连接、组合逻辑的输出以及芯片上的实际电线。以下是关于wire
的一些特点:
-
wire
用于表示线网(net),即它是一个无状态的变量,它的值由驱动它的逻辑门或模块的输出决定。 -
wire
不能存储值,它的值是连续分配的,即它的值在任何时刻都是由连接到它的驱动器所决定的。 -
wire
主要用于表示组合逻辑的输出。 -
wire
可以在连续赋值语句(assign statement)中使用,用于连续赋值。
reg
reg
类型用于表示寄存器类型的变量,它通常用于表示时序逻辑中的存储元素。以下是关于reg
的一些特点:
reg
用于表示变量,它可以存储值,通常用于在always块中赋值。reg
并不总是代表硬件寄存器,它也可以用于表示用于仿真的内部变量。reg
可以在过程赋值语句(procedural assignment)中使用,如always块、initial块等。reg
通常用于表示触发器(flip-flop)的输出。
verilog
wire a, b, sum;
assign sum = a & b; // 使用assign语句将a和b的逻辑与结果赋值给sum,wire用在assign 语句中
reg a, b, q;
always @(posedge clk) begin
q <= a & b; // 在时钟上升沿将a和b的逻辑与结果赋值给q,reg用在always逻辑块中
end
区别如下
wire
用于组合逻辑,而reg
用于时序逻辑。wire
通过连续赋值语句赋值,而reg
通过过程赋值语句赋值。wire
不能在always块中直接赋值,而reg
可以在always块中赋值。
值得注意的是即使register(reg)暗示了其是一个寄存器,但实际上这还要和其在电路中是如何使用的对应上。
向量与参数(常量)
parameter 参数:
parameter是一种常量,通常出现在module内部,常被用于定义状态、数据位宽等
vector(向量):
在Verilog中,向量变量是一种表示多位宽信号的方式,它允许我们以单个变量的形式处理一组位。向量可以是线网(wire)或变量(reg)类型,并且可以用于表示组合逻辑或时序逻辑的输出。以下是关于Verilog中向量变量形式的详细介绍:
向量通过在变量名后面加上方括号[]
来定义,方括号内指定了向量的位宽。位宽可以是确定的数字,也可以是参数化的表达式。向量的位宽定义了向量包含的位数。
verilog
wire [3:0] w; // 定义一个4位的wire类型向量,位宽从3到0
reg [7:0] r; // 定义一个8位的reg类型向量,位宽从7到0
向量索引:
Verilog允许通过索引来访问向量中的单个位或连续的位段。索引可以是常数或计算得到的表达式。
verilog
reg [7:0] r;
// 访问单个位
r[0] = 1'b1; // 设置r的第0位为1
r[7] = 1'b0; // 设置r的最高位为0
// 访问连续的位段
r[3:1] = 3'b101; // 设置r的第3位到第1位为101
向量拼接:
允许将向量进行相连,如此看来向量和c语言中的字符串更加相似。
verilog
wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b1100;
wire [7:0] c;
// 向量拼接
c = {a, b}; // c将会是8'b10101100
向量赋值:
向量赋值允许整体赋值和部分赋值。
verilog
reg [7:0] r;//八位向量,[7:0]
// 整体赋值
r = 8'b10101010;
// 部分选择赋值
r[7:4] = 4'b1111; // r现在为8'b11111010
向量的操作:
向量间允许进行逻辑运算,算术运算和比较运算。向量间运算的时候,需要保持其长度一样。
verilog
reg [3:0] a = 4'b1010;
reg [3:0] b = 4'b1100;
reg [3:0] result;
// 逻辑运算
result = a & b; // 结果为4'b1000
在verilog中的向量一般来说是从最高位到最低位这样设定的,比如reg [4:0] a中就是从第四位到第0位如此设定的。
三元表达式
在Verilog中,三元表达式是一种简洁的书写方式,用于在单个表达式中实现条件赋值。它类似于C语言中的条件运算符 ? :
。三元表达式的语法如下:
verilog
condition ? expression1 : expression2
部分使用示例如下:
verilog
reg a, b, c;
wire d;
// 如果a为1,则d赋值为b,否则赋值为c
d = a ? b : c;
reg [3:0] a, b, result;
// 如果a的最高位为1,则result的最高4位赋值为b,否则赋值为a
result[3:0] = a[3] ? b[3:0] : a[3:0];
分支语句
在Verilog中,分支语句用于根据条件的真假来执行不同的代码块。Verilog提供了两种主要的分支语句:if
语句和case
语句。下面将详细介绍这两种分支语句的用法。
if语句:
if
语句是最基本的分支结构,它根据条件的真假来决定是否执行某个代码块。
verilog
reg [1:0] a;
reg b;
always @(posedge clk) begin
if (a == 2'b01) begin
b <= 1'b1; // 如果a等于01,则b赋值为1
end
else begin
b <= 1'b0; // 否则,b赋值为0
end
end
case语句:
case
语句是一种多路分支结构,它允许根据表达式的值选择多个代码块中的一个来执行。case语句中假如不满足所有的值就执行default条件。示例代码如下,类似于c语言中的case语句:
verilog
reg [1:0] a;
reg [2:0] b;
always @(posedge clk) begin
case (a)
2'b00: b <= 3'b000; // 如果a等于00,则b赋值为000
2'b01: b <= 3'b001; // 如果a等于01,则b赋值为001
2'b10: b <= 3'b010; // 如果a等于10,则b赋值为010
default: b <= 3'b111; // 如果a不匹配上述任何值,则b赋值为111
endcase
end
for循环语句
在Verilog中,for
循环语句是一种重复执行代码块的构造,它允许你根据给定的条件重复执行一系列操作。这在初始化寄存器数组或执行重复计算时非常有用。以下是for
循环的基本语法和用法。在verilog中的for循环语句和c语言中的for循环语句相似。
verilog
reg [7:0] my_array [0:99]; // 声明一个包含100个8位寄存器的数组
initial begin
for (int i = 0; i < 100; i = i + 1) begin
my_array[i] = 8'h00; // 将数组中的每个元素初始化为0
end
end
- 循环变量通常声明为整数类型(
int
),这是Verilog中的一个4态变量,可以表示正值、负值、0和x(未知)。 - 在Verilog中,
for
循环通常用于initial
和always
块中。 - 在可综合的Verilog代码中,
for
循环必须保证在有限的时间内完成。这意味着循环的迭代次数必须是可预测的,通常基于常数或参数。 - 在仿真中,
for
循环可以无限运行,但在实际硬件中,循环必须在一个时钟周期内完成,或者分解为多个时钟周期内的操作。 - 在可综合的代码中,
for
循环的索引变量不应在循环体内被赋值,除非是按照循环迭代的固定模式更新。
拼接运算符
在Verilog中,拼接运算符(Concatenation Operator)用于将多个位宽不同的信号或变量组合成一个连续的位宽更大的信号。拼接运算符有两种形式:连续拼接运算符 {}
和重复拼接运算符 {n{}}
。连续拼接运算符允许你将多个信号或值按顺序拼接起来,形成一个更大的向量。重复拼接运算符允许你将一个信号或值重复多次,形成一个更大的向量。示例代码如下:
verilog
wire [3:0] a = 4'b1010;
wire [1:0] b = 2'b11;
wire [5:0] c;
assign c = {a, b}; // c 将被赋值为 6'b101011
wire [1:0] a = 2'b11;
wire [7:0] b;
assign b = {4{a}}; // b 将被赋值为 8'b11111111
while循环
在Verilog中,while
循环是一种过程性语句,它允许在initial
块或always
块中根据给定的条件重复执行一段代码。while
循环的行为类似于C语言中的while
循环,它在每次迭代之前检查条件,如果条件为真(即非零),则执行循环体内的代码;如果条件为假(即零),则退出循环。示例代码如下:
verilog
reg [7:0] accumulator; // 8位累加器
reg [7:0] counter; // 8位计数器
initial begin
accumulator = 0; // 初始化累加器
counter = 0; // 初始化计数器
while (counter < 100) begin
accumulator = accumulator + counter; // 累加计数器的值
counter = counter + 1; // 计数器递增
end
$display("Accumulator value: %d", accumulator); // 显示累加器的最终值
end
在下面的示例中编写了test_bench文件并编译和查看结果:

repeat循环
在Verilog中,repeat
循环是一个用于重复执行一段代码的循环结构。它类似于 C 语言中的 for
循环,但是 repeat
循环的语法更简单,因为它只指定了循环次数,而不需要初始化表达式和递增表达式。示例代码如下:
verilog
module repeat_test;
reg [3:0] count; // 4位计数器
initial begin//initial逻辑块
count = 0; // 初始化计数器
repeat (10) begin
#5; // 等待5个时间单位
count = count + 1; // 计数器递增
$display("Count is %d", count); // 打印计数器的值
end
end
endmodule
实际上说明在verilog编译的时候不需要test_bench也是可行的,仅仅编写一个module文件也是可以编译并查看结果的,具体如下所示:

函数与任务
在Verilog中,函数(function)和任务(task)是两种用于封装和重用代码块的重要机制。它们在模块内部定义,可以用来执行特定的操作或计算。函数是一种返回单个值的操作,它不能包含任何时间延迟(#)或非阻塞赋值(<=)。函数通常用于计算和返回一个结果。
函数的特点:
- 函数必须有一个返回类型,可以是线网(wire)或寄存器(reg)类型,并且可以指定宽度。
- 函数可以接受输入参数,但不能有输出参数。
- 函数不能包含任何时间延迟(#)或非阻塞赋值(<=)。
- 函数在调用时必须有一个返回值。
verilog
function [7:0] add_bytes(input [7:0] a, input [7:0] b);//定义函数,输出是[7:0]的向量,输入是[7:0]a和[7:0]b,这是system verilog的语法,传统的verilog实际上不支持参数的声明在括号中
add_bytes = a + b;
endfunction
任务是一种可以执行多个操作的块,它可以包含时间延迟和非阻塞赋值。任务可以没有返回值,但可以有输入和输出参数。
任务的特点:
- 任务可以没有返回值,因此不需要指定返回类型。
- 任务可以接受输入参数和输出参数。
- 任务可以包含时间延迟(#)和非阻塞赋值(<=)。
- 任务可以调用其他任务或函数。
值得注意的是,任务的特点在于其可以包含时间延迟和非阻塞赋值,且其不一定得有一个返回值。
verilog
task delay_and_print(input [7:0] data, output reg done);
#10; // 延迟10个时间单位
$display("Data is %h", data);
done = 1'b1; // 设置完成标志
endtask
下面的代码示例展示了如何在verilog中编写并调用函数。
verilog
module task_test;
// 函数定义(传统Verilog语法)
function [7:0] myfunction;
input [7:0] a; // 在函数体内声明输入参数
input [7:0] b;
begin
myfunction = a + b; // 通过赋值给函数名返回结果
end
endfunction
reg [7:0] out;
initial begin
out = myfunction(8'b11000011, 8'b10101010);
$display("out is %b", out);
end
endmodule
