这篇笔记记录了Verilog基本知识的学习
参考的资料和网站
Chapter 1 基本认识
Verilog 是一种硬件描述语言(HDL,Hardware Description Language)

Verilog描述的是门电路的逻辑,在Verilog编写好后,往往还需要使用EDA工具形成与、或、非门构成的逻辑网表
当然,在一些精细的设计场景,往往也有直接设计门电路的案例
Chapter 2 基本语法
这部分的内容跟着Verilog在线仿真平台学习
Basics
Wires
需要把每个节点想象成一个连接线,Verilog描述了这些连接线怎么相互连接

连接两个连接线的语句是
assign out = in ;
这句话把out和in连接在了一起
当然,需要连接多个连接线时,可以使用如下语句

assign {w,x,y,z} = {a,b,b,c} ;
基本逻辑门
非门
使用非门连接的两个输出如下
assign out = ~ in ; //按位非
assign out = ! in ; //逻辑非
与门
使用与门连接的两个输出如下
assign out = a & b ; //按位与
assign out = a && b ; //逻辑与
或门
使用或门连接的两个输出如下
assign out = a | b ; //按位或
assign out = a || b ; //逻辑或
异或门
使用异或门连接的两个输出如下
assign out = a ^ b ;
中间连接线
在逻辑较为复杂时候,需要声明中间连接线
语法如下

wire not_in;
复杂组合逻辑实现
使用上面提到的方法可以实现复杂的组合逻辑

这个芯片的实现代码如下
module top_module (
input p1a, p1b, p1c, p1d, p1e, p1f,
output p1y,
input p2a, p2b, p2c, p2d,
output p2y );
wire tp1,tp2,tp3,tp4;
assign p2y=tp1 || tp2;
assign p1y=tp3 || tp4;
assign tp1=p2a&p2b;
assign tp2=p2c&p2d;
assign tp3=p1a&p1b&p1c;
assign tp4=p1d&p1e&p1f;
endmodule
Vectors
Vectors 用于使用一个名称对相关信号进行分组
声明Vector
声明向量的语法如下
type [upper:lower] vector_name;
//type通常是wire或reg
//example
wire [99:0] my_vector; // Declare a 100-element vector
assign out = my_vector[10]; // Part-select one bit out of the vector

在关系图中,旁边带有数字的刻度线表示向量(总线)的宽度
Vector的字节序
Vector的字节序是最低有效位是具有较低的索引(小端,[3:0])还是较高的索引(大端, [0:3])。在 Verilog 中,一旦 vector 声明了特定的字节序,它必须始终以相同的方式使用。
隐式网络
在代码中使用一个没有事先声明(比如用 wire 或 reg 关键字)的信号时,Verilog 不会报错,而是会自动为你创建一个默认的、1位宽度的 wire 类型信号。
wire [2:0] a, c; // 声明了 a 和 c 是3位的向量 (vector)
assign a = 3'b101; // 给 a 赋值 101
assign b = a; // 变量 b 没有被声明。
// Verilog 不会报错,而是隐式创建了一个1位的 wire `b`。
// 它会将 a 的值 (101) 赋给 b,但因为 b 只有1位
// 所以只会取 a 的最低位,即 1。
// 因此,b 的值实际上是 1
assign c = b; // 将 b (值为1) 赋给3位的 c。
// 因此 c 的值变成了 3'b001。
// 本意是想让 c 的值也等于 a (101)。
// 但由于隐式网络 b 的存在,c 的值最终错误地变成了 001
// 在编译时不会有任何警告或错误
为了发现这个报错,可加入添加 default_nettype none,使第二行代码成为错误
Vector索引的访问与取值
Verilog定义了两种方式(见上文的字节序)来定义Vector的访问
[0:3][低位:高位] 大端格式
[3:0][高位:低位] 小端格式
在取低位和高位的时候需要注意
同样Verilog支持负索引,此时需要依据负数的大小确定大小端格式
部分选择
{}用于连接Vector
assign out = {in[7:0] , in[15:8] };
需要注意的是,当out超过16位时,发生补0
不足16位时,发生截断
- 补充知识:进制的表示(位数+符号+数)
| 二进制 | 八进制 | 十进制 | 十六进制 |
|---|---|---|---|
| b(Binary) | o(Octal) | d(Decimal) | h(Hexadecimal) |
| 3’b111 | 2’o11 | 4’d10 | 4’ha |
需要指定位宽,当不指定时,编译器自己一句上下文决定
位宽>实际宽度,补0;
位宽<实际宽度,截断;
连接运算符
连接运算符可以复用Vector
assign out = {3{in}} ;
//这句话把in复制了3次赋值给out
//在数组中需要作为整体使用
assign out ={3'd5, {2{3'd6}}} ;
Module
基本认识
Module通过模块化的方式编写代码
之前所有的代码都是通过Module编写
Module的基本结构如下
module mod_a ( input in1, input in2, output out );
// Module body
endmodule
Module的存在使一个模块可以复用,类似于一个可复用的数字单元
将外部连接线连接到Module
有两种方式
mod_a instance1 ( wa, wb, wc );
//这种方法需要严格对应各个参数的位置
mod_a instance2 ( .out(wc), .in1(wa), .in2(wb) );
//这种方法用input和output来区分输入输出,因此不需要关心位置
其中,实例化mod_a成instance1和instance2是必须的,相当于给一个元件实际化成了实际使用的器件
通俗的理解:触发器D1,D2,D3
通常这种结构可以嵌入另外一个Module成为一个大的芯片单元

这段用3个D触发器形成的3位寄存器的代码实现
module top_module ( input clk, input d, output q );
wire q1,q2;
my_dff d1(clk,d,q1);
my_dff d2(clk,q1,q2);
my_dff d3(clk,q2,q);
endmodule
加法器
加法器是使用module的典型,高位的加法器可以用低位的加法器级联而成
具体的方法是将高位和低位分别输入两个加法器中,同时处理进位

由图可知,16个一位加法器组成了16位加法器,随后两个16位加法器又组成了32位加法器
这个组合单元的实现代码
module top_module (
input [31:0] a,
input [31:0] b,
output [31:0] sum
);//
wire cout1;
wire cout2;
wire [15:0] sum1,sum2;
add16 instance1(a[15:0],b[15:0],0,sum1,cout1);
add16 instance2(a[31:16],b[31:16],cout1,sum2,cout2);
assign sum={sum2,sum1};
endmodule
module add1 ( input a, input b, input cin, output sum, output cout );
// Full adder module here
assign{cout,sum}=a+b+cin;
endmodule
可控加减法器
可用控制信号sub控制加减法运算,其中取补码需要用到反转加一的操作,这个操作可以用异或被操作数,并将sub作为第一个加法器的进位来实现

这个器件实现了如下功能:
当sub=1,执行a-b
当sub=0,执行a+b
具体的实现代码如下
module top_module(
input [31:0] a,
input [31:0] b,
input sub,
output [31:0] sum
);
wire [31:0] b_real;
wire cout1,cout2;
wire [15:0]sum1,sum2;
assign b_real=b ^ {32{sub}};
add16 instance1(a[15:0],b_real[15:0],sub,sum1,cout1);
add16 instance2(a[31:16],b_real[31:16],cout1,sum2,cout2);
assign sum={sum2,sum1};
endmodule
Procedures
always语句
always语句和assign语句都可以用来赋值,但它们的使用情景不同
assign表示纯组合逻辑,表示硬件的硬连线
always可用于边沿触发等时序逻辑,它是变量变化时才执行的
always语句的基本使用如下,其中@(*)代表always在监测到后面任何一个变量变化后计算一次,@(posedge clk)代表监测到时钟上升沿后计算一次
always @(*);
always @(posedge clk);
需要注意的是,assign赋值的只能是常量(例如wire),always赋值的只能是变量(例如reg)
reg代表可赋值单元,可以代表线网,也可以代表触发器的状态等
阻塞赋值与非阻塞赋值
阻塞赋值(=):需要等一个变量更新后再计算下一个变量
非阻塞赋值(<=):所有变量并行地全部更新
一般规定,在always @(*)语句中,使用阻塞赋值,在always @(posedge clk)语句中,使用非阻塞赋值
数据选择器
使用always语句构造2-to-1 MUX
always @(*) begin
if (condition) begin
out = x;
end
else begin
out = y;
end
end
其中always监测condition,x,y三个变量,发生改变则开始执行begin到end间的语句
if语句检测condition是否为真,为真则开始执行begin到end间的语句
也可以简写成
assign out = (condition) ? x : y;
如果 (condition) 为真,那么 out 的值等于 ? 后面的 x
如果 (condition) 为假,那么 out 的值等于 : 后面的 y
需要注意的是,需要为所有的情况都指定输出,否则会导致latch
case语句
上述讲到的是if语句配合always使用
case语句也可以用于表示分支结构
always @(sensitivity_list) begin
case (expression)
value1: statement1; // 如果 expression 的值等于 value1,执行 statement1
value2: statement2; // 如果 expression 的值等于 value2,执行 statement2
...
valuen: statementn;
default: default_statement; // 可选:如果以上所有值都不匹配,执行 default_statement
endcase
end
casez语句
可以无视case输入量的某些位,从而减少case语句的使用次数
always @(*) begin
casez (in[3:0])
4'bzzz1: out = 0; // in[3:1] can be anything
4'bzz1z: out = 1;
4'bz1zz: out = 2;
4'b1zzz: out = 3;
default: out = 0;
endcase
end
这是一个优先编码器,输出最低位的1,逻辑是:识别到哪一位有1,就直接输出,因此case的排列顺序也要有要求,从低到高
More Verilog Features
三元运算符
(condition ? value_if_true : value_if_false)
计算condition的值,为真即取value_if_true,为假即取value_if_false
三元运算符可以描述许多的数字电路(这部分借鉴了AI的解说)
基本求值
(0 ? 3 : 5) // 结果是 5,因为条件是 0 (false)
condition:0:假value_if_true:3value_if_false:5- 因为条件为假,所以表达式的结果取
value_if_false,即5
2-to-1 MUX
(sel ? b : a) // 一个由 sel 控制的,在 a 和 b 之间选择的二路选择器
- 如果
sel为1(真),输出b - 如果
sel为0(假),输出a
T-Flip-Flop
always @(posedge clk)
q <= toggle ? ~q : q; // T 触发器
toggle信号是使能信号- 在时钟
clk的上升沿,判断toggle信号 - 如果
toggle为1(真),q的下一个状态是它当前值的反相 (~q),实现翻转功能 - 如果
toggle为0(假),q的下一个状态是它当前值 (q),实现保持功能
- 在时钟
FSM 状态转移逻辑
// 一个单输入 FSM 的状态转移逻辑
always @(*)
case (state)
A: next = w ? B : A;
B: next = w ? A : B;
endcase
用于决定有限状态机(FSM)的下一个状态
next当处于状态
A时 (state == A):next = w ? B : A;:如果输入w为1,下一个状态转移到B;否则,下一个状态保持在A
当处于状态
B时 (state == B):next = w ? A : B;:如果输入w为1,下一个状态转移到A;否则,下一个状态保持在B
Tri-state Buffer (三态缓冲器)
assign out = ena ? q : 1'bz; // 一个三态缓冲器
condition:ena(enable, 使能)value_if_true:q(驱动正常信号)value_if_false:1'bz(高阻态)- 如果
ena为1(真),输出端口out被正常驱动,其值为q - 如果
ena为0(假),输出端口out被置为高阻态 (z)。
- 如果
嵌套实现 3-to-1 MUX
// 一个 3-to-1 MUX
((sel[1:0] == 2'h0) ? a : // 如果 sel=00, 输出 a
(sel[1:0] == 2'h1) ? b : // 否则, 如果 sel=01, 输出 b
c ) // 否则 (sel=10 或 11), 输出 c
用于实现多路选择,等价于下面的 if-else if-else 结构:
if (sel[1:0] == 2'b00) begin
out = a;
end
else if (sel[1:0] == 2'b01) begin
out = b;
end
else begin
out = c; // 包括了 sel=10 和 sel=11 的情况
end
- 检查
sel[1:0] == 2'h0 - 如果为真,整个表达式的结果就是
a - 如果为假,整个表达式的结果是第二个(内层)三元运算符
(sel[1:0] == 2'h1) ? b : c的值 - 接着对内层进行判断,如果
sel[1:0] == 2'h1为真,结果为b;否则为c
归纳运算符
可用于多位相与
& a[3:0] // AND: a[3]&a[2]&a[1]&a[0]. Equivalent to (a[3:0] == 4'hf)
| b[3:0] // OR: b[3]|b[2]|b[1]|b[0]. Equivalent to (b[3:0] != 4'h0)
^ c[2:0] // XOR: c[2]^c[1]^c[0]
for循环
for (initialization; condition; step) begin
// 循环体内的语句
end
括号里的三个内容与C语言几乎一样,可直接套用
这个循环变量需要声明为interger(整形)
integer i
避免多重驱动
与c语言不同,Verilog中的每一段代码都是并行执行的,这就带来了一个问题:不能在两个模块同时对同一根wire进行赋值,从硬件的角度看,一根电线不可能同时接受高低两个电平,这会带来冲突
generate语句
同样在generate语句内部引入for/if,但是与for不同
- always:描述行为,会被eda工具展开成组合或时序逻辑电路
- generate:并行构造多个硬件实例
generate
// 声明一个专门用于 generate 块的循环变量
genvar i;
// for 循环
for (i = 0; i < N; i = i + 1) begin : block_name
// 在这里放置要重复生成的硬件代码
// block_name是给begin起的名字,方便调试
end
endgenerate
注意:generate语句内部的赋值需要使用assign
在generate模块中使用循环模块化硬件模块时,可以不加声明,编译器会自动分配
以上是Verilog的基本语法部分,之后还剩一些数字电路的练习题,这部分内容会缓慢更新