verilog学习笔记
module
-
module是verilog中的基础封装,表示设计的一个模块
-
module的参数表定义了input 和 output
1 |
|
assign
- verilog中所有线路都是单向的,通过
assign
语句连接
1 | module Test ( |
-
注意,这里的
=
是描述硬件的链接的,和传统语言中的赋值不一样,他是永久有效的,表示一个单向连接线! -
所以同一个连接方法,不同的
assign
顺序不一样的!
1 | /*以下两种链接方法等价!*/ |
基础逻辑门
-
verilog 的逻辑门实现很简单,因为verilog支持所有的c++运算符
-
not gate:
1 | module Invertor ( |
- and/or/xor gate:
1 | module AND( |
- nor 复合运算
1 |
|
内部连接线 wire
-
以上运算都是可以通过单个逻辑门实现的(input直接assign到output上面),但是对于复杂逻辑电路,我们却不能这么做
-
我们需要通过
wire
来设计内部连接线 -
eg:
- wire 本质上是一个中转用的“临时变量”(本质是导线,并不储存数据!!!)
1 | // wire 相当于提供一个中转(也是单向的!) |
- 练习: wire 的用法:
1 | // 没有强调 wire 的 `变量` 原本会默认为wire,这个加上后就禁止默认了 |
vector向量(bitwise 操作符 与 logical 操作符)
-
vector 是用于组合多个比特的数,比如传递 3 bit 的数的线 就要定义成
wire [2:0] a;
, 这里[2:0]
可以自己改,但是从MSB -> LSB递减,从n-1 : 0
是规范 -
你甚至可以定义负数索引!
wire [2:-1] a
-
vecotor 可以单独取出某个值(或者某组值)传递给复合这组值位宽的线:
1 |
|
-
logical 和 bitwise: 与 C++中的相同 (logical 操作符除非 整个vector都是0(此时为false值),否则为true值)
-
操作符两端位宽最好一致,不一致会进行extend: 如果操作数的位宽不一致,Verilog 会对较短的操作数进行位扩展以匹配较长的操作数的位宽。扩展的方式取决于操作数的类型:
- 对于无符号数,扩展的位会用 0 填充。
- 对于有符号数,扩展的位会用符号位(最左边的位)的值填充,这种方式称为符号扩展。
有符号与无符号
- 所有
reg, wire,input,output
值默认无符号,但是可以通过signed
关键字添加符号!
1 | wire signed [15:0] signed_wire; |
vector 拼接运算符 {}
:
- vector 可以被非常简单的拼接在一起 (用集线器实现):
1 |
|
- eg:
1 | module top_module( |
-
拼接运算符可以作为左值也可以作为右值!!!!
-
按照以下逻辑分配:
- out1 会得到拼接向量的最高4位。
- out2 会得到拼接向量的低5位。
1 | module top_module( |
- eg:
1 | module top_module ( |
重复运算符 {num{...}}
:
- 可以用
{num{...}}
重复一段字符:
1 |
|
output 与 output wire, output reg
- output 默认 output wire,但是也可以定义为 output reg, 输出寄存器!
wire [0:0] x, wire [1:1] x 与 wire x 的区别:
在 Verilog 中,定义 wire [0:0] x;
、wire [1:1] x;
和 wire x;
实际上涉及到了信号的位宽和索引范围,但它们都用于表示单比特的线(wire)。这些声明之间的区别主要体现在索引和表达的形式上,下面是对每种声明方式的解析:
wire [0:0] x;
- 这种声明方式定义了一个单比特的线
x
,其位索引为[0:0]
,表示x
只有一个位,且这个位的索引是 0。这是一种显式地指出位索引范围的声明方式。 - 这样声明的线可以在需要强调位索引时使用,比如在多位信号与单比特信号之间进行位选择或赋值时增加代码的清晰度。
wire [1:1] x;
- 类似地,
wire [1:1] x;
定义了一个单比特的线x
,但是这里的位索引被显式地指定为 1。这不影响x
作为一个单比特线的本质,但是在与其他信号交互时,比如进行位连接操作时,索引为 1。 - 这种声明方式较少见,但可能在需要与特定位宽信号的某个位进行对齐操作时有其特定的用途。
wire x;
wire x;
声明了一个单比特的线x
,没有显式地指定位索引。这是最简单也是最常见的单比特线声明方式。- 这种方式足够用于大多数情况,特别是当你不需要显式地处理信号的多个位或者位索引时。
区别和用途
- 所有这三种声明方式本质上都创建了一个单比特的线
x
。区别在于,[0:0]
和[1:1]
显式地指定了位的索引,而wire x;
没有指定位索引。在大多数实际应用中,简单地使用wire x;
就足够了,除非你有特定的原因需要显式地表达或操作信号的某个特定位。 - 使用
[0:0]
或[1:1]
等形式可能在某些特定上下文中有其用途,例如,当你需要与特定的位进行操作或强调位的索引时。然而,它们增加了代码的复杂度,通常只在需要明确指出信号的位索引时使用。
总的来说,选择哪种方式取决于具体需求和个人偏好,但在大多数情况下,简单的 wire x;
对于单比特信号已经足够。
BUG: 隐式网表
隐式网表(implicit nets)的概念:
在 Verilog 中,如果你在没有事先声明的情况下使用了一个信号名,这个信号会被隐式地创建为一个单比特宽的 wire
类型。这种行为可能会引入难以发现的错误,尤其是当你本意想要使用一个多位向量(vector)而不是单个比特时。
示例解释
1 | wire [2:0] a, c; // 两个三位向量 |
在这个例子中,因为 b
没有被显式声明,所以它被隐式创建为一个单比特的 wire
。当 b = a;
执行时,实际上只将 a
的最低位赋给了 b
。随后,当 c = b;
执行时,c
被错误地赋值为单比特 b
的值扩展到 c
的位宽,即 001
,而不是预期的 101
。
my_module i1 (d,e);
这里提到的 my_module i1 (d,e);
示范了如何通过模块端口连接隐式创建的单比特 wire
。如果 d
和 e
没有在之前被显式声明,并且你本意是将它们作为多位向量使用,那么这会导致错误。
避免隐式网表的创建
使用 default_nettype none
指令可以禁止隐式网表的创建。这意味着所有信号都必须显式声明其类型。如果试图使用未声明的信号,编译器会报错,从而帮助快速发现并修复潜在的问题。
1 |
加入这一行后,任何未声明的信号使用都会导致编译错误,从而使得上面 b
的隐式创建变成一个明显的错误,有助于提早发现和修正问题。这是一种提高代码质量和避免难以察觉的错误的好方法。
vector 部分赋值问题
verilog 向量赋值方法: 4'b1011
在 Verilog 中,如果你声明了一个多位宽的 wire
,如 wire [7:0] a;
,这意味着 a
是一个8位宽的向量。如果之后的代码中你只对这个向量的一部分进行了定义,例如只定义了 a[3:0]
,这将影响到 a
的部分位,而其余位将保持未定义状态,除非你另外为它们赋值。
示例和行为
考虑以下代码:
1 | wire [7:0] a; |
在这个例子中:
a[3:0]
被赋值为二进制数1010
。a[7:4]
(即a
的高4位)未被显式定义,因此它们的值在逻辑上是 ‘z’(高阻态)或 ‘x’(未知),具体取决于电路的其余部分以及是否有其他地方对这些位进行了赋值。
未定义的高位
对于未被显式赋值的部分(在此例中为 a[7:4]
),它们的状态是不确定的:
- 如果这是模拟或综合的一部分,这些未定义的位可能会被综合工具视为 ‘x’,这可能影响到电路的行为,尤其是在涉及条件判断时。
- 在实际的硬件实现中(如 FPGA 或 ASIC),未初始化的信号通常默认为 ‘0’,但这是一种不安全的做法,不应依赖于这种行为进行设计。
最佳实践
为了编写可靠和确定行为的 Verilog 代码,最佳实践是:
- 显式定义所有位:确保你的所有
wire
和reg
变量都被完全定义,避免未定义的行为。 - 使用
'default_nettype none
:通过在文件开头添加这条指令,强制所有信号都必须显式声明,有助于避免隐式创建单比特线导致的问题。
这样做可以确保代码的可读性和电路的可预测性,降低因为未定义或未知状态带来的风险。
自定义模块的使用:
如何使用我自定义的module呢?比如下图:
1 |
|
- 如何使用我的module?有两种方法:
1 | // 1. 按照参数表顺序接线 |
- 练习:单向shift register
1 | module top_module ( input clk, input d, output q ); |
如何导入其他文件中的module?
- ``include"path"
来导入
.v`文件
1 |
三目运算符:
- 三目运算符是唯一可以在always过程块之外实现 if-else 逻辑的方法!
1 |
|
reg 关键词: 不止register
在 Verilog 中,reg
关键字可能会引起一些混淆,因为它的名字暗示它与"寄存器"相关,但实际上它的含义更广泛。
reg
的本质
-
存储功能:在 Verilog 语言中,
reg
并不一定代表硬件中的物理寄存器。reg
类型的变量用于存储在过程块(如always
块)中赋值的值。它可以用来表示组合逻辑和时序逻辑中的变量。 -
过程块中的赋值:
reg
类型的变量可以在always
块中通过阻塞(=)
或非阻塞(<=
)赋值操作符进行赋值。这意味着它们可以被用来实现时序逻辑(如触发器或寄存器的行为)以及可以被重新赋值的组合逻辑。
reg
与 wire
wire
类型:相比之下,wire
类型的变量用于表示电路中的物理连接和那些通过连续赋值(使用assign
语句或直接连接到模块端口)获得值的信号。wire
默认不能在过程块中赋值,因为它们代表的是连续驱动的信号。
使用 reg
- 在描述时序逻辑时,
reg
用于存储状态信息,如触发器和寄存器的输出。 - 在描述组合逻辑时,即使逻辑输出并不需要"记忆"功能,
reg
也可以用来在always
块中临时存储计算结果。这样的使用并不会导致硬件中生成物理寄存器,而是由综合工具根据逻辑的实际需要来决定是否需要物理存储元件。
示例
1 | module example ( |
在这个示例中,out
被声明为 reg
类型,因为它在 always
带有敏感列表的过程块中被赋值。这里 out
的使用体现了时序逻辑的典型应用——它在每个时钟周期存储一个值,这种行为类似于硬件中的一个寄存器。
总之,reg
在 Verilog 中的使用并不局限于表示物理寄存器,它更广泛地用于在过程块中需要存储或更新值的任何地方。
为什么 always 中的左值不能用 wire
只能用 reg
?(右值随便)
在 Verilog 中,always
块是一种过程控制结构,用于描述在某些条件或事件发生时应该如何改变信号的值。这些条件或事件被称为"敏感列表",可以是时钟信号的上升沿或下降沿(用于描述时序逻辑),或者是任何信号的变化(用于描述组合逻辑)。always
块使得硬件描述语言(HDL)能够模拟电路中的逻辑行为。
为什么需要 reg
always
块通常用于实现那些基于特定事件发生时需要改变状态的逻辑。由于 Verilog 是一种用于硬件描述的语言,其变量类型和用法与传统的编程语言有所不同,特别是在描述硬件行为时对变量的存储和驱动方式的要求上。
-
reg
类型:在always
块中,reg
类型用于声明那些需要在过程控制块内赋值的变量。reg
不仅仅用来表示物理上的寄存器,它更广泛地用于表示需要在过程块中持有或更新值的变量。这包括时序逻辑中的状态寄存器,以及组合逻辑中的临时变量。 -
wire
类型:wire
类型用于表示两点之间的物理连接,它们的值通常由外部信号驱动,例如来自模块端口的信号或者assign
语句的结果。因为wire
类型的信号是连续被驱动的,它们不适合在always
块中赋值,因为always
块表示的是在特定条件下才执行的逻辑。
为什么不能用 wire
-
在 Verilog 的早期版本中,规范要求在
always
块内赋值的变量必须声明为reg
类型,即使这个变量用于描述组合逻辑。这是因为reg
类型的变量可以在仿真过程中改变其值,而wire
类型的变量则不能。 -
使用
wire
类型的变量进行连续赋值,意味着其值是由外部信号直接驱动的,不适合用于always
块内的逻辑描述,因为always
块内的逻辑可能只在特定条件下才执行。 -
eg:
1 | // synthesis verilog_input_version verilog_2001 |
reg 与 wire 之间的连接
-
reg不能直接 assign给wire,但是wire可以在always中赋值给reg,但是module的
wire
参数可以接上reg
为什么呢? -
核心:这样好生成电路!
-
所以如果要连接 wire 和 reg 的话,通过模块来间接连接就行了!
1 | module Reg2Wire ( |
always 过程块基础
由于数字电路由逻辑门和导线相连接构成,任何电路都可以表达为一些模块和 assign
语句的组合。然而,有时这并不是描述电路的最便捷方式。过程(always
块就是一个例子)提供了一种替代的语法来描述电路。
在硬件综合方面,有两种类型的 always
块是相关的:
- 组合逻辑:
always @(*)
- 时钟触发:
always @(posedge clk)
组合逻辑的 always
块等同于 assign
语句,因此总是有办法用这两种方式来表达一个组合电路。在两者之间的选择主要是哪种语法更方便的问题。过程块内部的代码语法与外部代码不同。过程块拥有更丰富的语句集(例如,if-then
、case
),不能包含连续赋值*,但也引入了许多新的非直观的错误制造方式。(*过程连续赋值确实存在,但与连续赋值有所不同,并且不可综合。)
例如,assign
和组合逻辑的 always
块描述了相同的电路。两者都创建了相同的组合逻辑块。每当任何输入(右侧)的值发生变化时,两者都会重新计算输出。
1 | assign out1 = a & b | c ^ d; |
在这个例子中,assign
语句和使用 always @(*)
的组合逻辑块都实现了相同的功能:根据输入 a
、b
、c
和 d
的值来计算 out1
和 out2
。这表明在设计组合逻辑时,可以根据具体情况选择使用 assign
语句或组合逻辑的 always
块。
always 过程块的本质
always @(...)
会监视括号中的信号(被称为敏感列表,* 代表监视所有module中的信号),如果信号变了我们就会更新左值(一定是reg)的值
always @(*)
语句是 Verilog 中的一种过程控制结构,用于描述在任何输入变化时需要重新执行的逻辑。这种结构体现了硬件描述语言(HDL)的一大特点:与传统的编程语言不同,它不是顺序执行的,而是用于描述电路的行为特性——即电路是如何响应各种输入变化的。
本质
-
敏感列表:在
always @(*)
中,*
代表自动敏感列表,意味着该过程块将对其内部使用的所有信号进行监视。一旦这些信号中的任何一个发生变化,过程块内的逻辑就会被重新评估和执行。这种机制使得 Verilog 能够模拟组合逻辑的行为,组合逻辑的输出是当前输入的直接函数,与之前的状态或时间无关。 -
描述组合逻辑:使用
always @(*)
常见于描述组合逻辑电路,它确保了输出能够实时反应输入的变化。这与使用assign
语句达到的效果相同,但允许使用更复杂的控制流语句(如if
、case
等),使得在描述较为复杂的逻辑时更加灵活和强大。
与 assign
语句的对比
虽然 always @(*)
和 assign
语句在功能上有所重叠,都能用于描述组合逻辑,但在具体应用上有一些区别:
-
assign
语句:更适合描述简单的、直接的逻辑关系,特别是单一的表达式或直接的信号映射。 -
always @(*)
块:由于可以包含复杂的控制流语句,因此更适合描述复杂的组合逻辑。当逻辑包含多个条件分支或需要根据多个输入信号计算输出时尤其有用。
使用注意
尽管 always @(*)
提供了强大的功能来描述组合逻辑,但在使用时也要注意一些问题,以避免引入非预期的行为或综合问题:
-
明确区分组合逻辑和时序逻辑:确保不要在旨在描述组合逻辑的
always @(*)
块中引入时序逻辑元素(如基于时钟的状态更新),因为这可能会导致逻辑行为与预期不符或难以综合。 -
变量类型:在
always @(*)
块中赋值的变量通常应声明为reg
类型,即使是用于组合逻辑。这可能与传统的寄存器(reg
)概念有所不同,但在 Verilog 中,reg
类型的变量可以用于组合逻辑的描述,其关键在于它们是否在时钟边缘触发的过程块中被赋值。
总之,always @(*)
的本质在于为 Verilog 提供了一种描述组合逻辑的强大机制,使设计者能够以灵活、直观的方式实现基于各种条件和输入信号的逻辑操作。
always 时钟激发
- always 过程块可以被上升/下降沿激发(flip-flop)
1 | always @(posedge clk) begin |
<=
非阻塞赋值 和 =
阻塞赋值:
- 在 时钟激发的always中用
<=
, 非时钟激发的always用=
!
always中的 if-elseif-else 逻辑
1 |
|
always 导致的未遇见 latch
-
始终注意,我们是在设计电路,不是在写代码!(所以要把你想的电路用代码表示出来,而不是写逻辑)
-
比如过热关机电路逻辑:
1 | always @(*) begin |
- 实际上会创建如下电路:
- 为了防止这种情况出现,我们要先想好电路,然后再写!(正确版本如下:)
1 | always @(*) begin |
Case: 和C++ 类似
1 | module MUX ( |