Verilog HDL 基础语法
最近在学习数电和 Verilog,写此就当复习了罢。依照 HDLBits 编写。
基础语法
Wire
导线是一种具有一个输入和一个输出的模块,用来表示电路中两点之间的连接。可以通过 assign 语句将两个 Wire 连接起来。例如:
input 和 output 声明的为 Wire 类型,除非显式声明 Reg。
1 | module top_module(input in, output out); |
Gates
Gate 有以下类型:AND(&), OR(|), XOR(^) 及各自的否定。声明如下:
1 | module top_module( input a, input b, output out ); |
按位运算符 vs 逻辑运算符:
a & b(按位与):逐位运算,结果是向量;a && b(逻辑与):把操作数当作整体判断真假,结果是 1 bit(0 或 1)。- 例如
4'b1010 & 4'b1100 = 4'b10004'b1010 && 4'b1100 = 1'b1。
Vector
Vector 类似于 C++ 中的 Vector,可以认为是一种数组,其实际只是用一个名称表示许多信号。向量 (vector) 就是多位宽的信号,与之相对的是只有 1 位的标量 (scalar)。
使用以下内容声明:
1 | wire [99:0] my_vector; //声明了一个100位的 Vector |
声明格式:[最高位:最低位]。[99:0] 表示位 99 是最高位 (MSB)、位 0 是最低位 (LSB),共 100 位;也可以写成 [0:99](此时位 0 是 MSB),一般 [高:低] 的写法。
1 | module top_module( input [31:0] in, output [31:0] out ); |
当然也可以像 C++ 那样单独取出一位,即
1 | assign out[0] = in[0]; |
拼接 {}:把多个信号按顺序拼成向量,例如 {a, b} 表示 a 在高位、b 在低位;
复制 {n{}}:把某个信号重复 n 份再拼接,如 {4{1'b0}} 等于 4'b0000。
Module
模块实例化就是在电路中调用一个已经定义好的模块,把它当作一个子电路使用。复杂的电路就是由一层层模块例化 (instantiate) 堆叠出来的,每例化一次就生成一份独立的实例 (instance)。
假设已经定义了一个子模块:
1 | module mod_a ( output out, input in1, input in2, input in3 ); |
有以下两种连接方式:
Connecting by position
按端口声明的顺序,依次把信号传入子模块,顺序必须与子模块声明的端口顺序完全一致。
1 | module top_module ( ... ); |
Connecting by name
指定每个端口的名字,用 .端口名(信号名) 的形式连接。
1 | module top_module ( ... ); |
Always
always 是过程语句 (procedural statement) 块,其描述”在特定时刻执行的逻辑”。
组合逻辑
敏感列表应包含块内用到的所有输入信号,通常写作 always @(*),让工具自动推断敏感列表,避免遗漏导致仿真与综合结果不一致。
1 | module top_module( |
时序逻辑
用于描述时序逻辑(触发器)时,敏感列表通常是时钟的边沿,例如 posedge clk(上升沿)或 negedge clk(下降沿)。
1 | module top_module( |
if and case
if-else
if-else 语句只能写在 always 块内部,用法与 C 基本一致:
1 | always @(*) begin |
当然也可以使用三元运算符:
1 | assign out = sel ? a : b; |
case
当分支较多、且是对同一个信号的不同取值进行判断时,case 语句比一长串 if-else if 更加清晰:
1 | always @(*) begin |
和 if 一样,case 也应写上 default 分支覆盖未列出的情况,避免产生锁存器。
casez
casez 是 case 的变体,允许在比较时使用 ? 作为通配符,表示该位不参与比较:
1 | module top_module ( |
上面这个例子实现了一个简单的优先级编码器:从高位往下看,casez 会按照分支书写的顺序依次匹配,第一个匹配成功的分支生效,z(或 ?)位不影响匹配结果。
for-loop
Verilog 是硬件描述语言,for 循环是在**综合/仿真时被展开(unroll)**成多份重复的硬件逻辑——它不是软件里那种”运行时循环”,而是编译期”批量写代码”的工具。
Verilog 中的 for 循环主要有两种使用场景:过程语句内的 for(在 always 块中) 和 生成语句的 for(generate for,用于例化多个模块或产生重复的硬件结构)。
always 块中的 for 循环
用于在组合或时序逻辑中,对一组信号做重复性的操作,比如逐位处理向量:
1 | module top_module( |
这种写法本质上仍是组合逻辑(如果用的是 always @(*)),for 只是帮你少写 100 行 out[i] = ...,展开后和手写 100 条语句是等价的。
generate for(生成块)
当需要例化多个相同的子模块,或者生成一组重复的硬件结构时,使用 generate … for … endgenerate:
1 | module top_module( |
- 循环变量必须声明为特殊类型 genvar,它只在”生成”阶段(编译期)存在,不是真实的硬件信号。
- begin: gen_xor 中的标签(这里是 gen_xor)是必须的,用来给生成出来的每一份实例命名(如 gen_xor[0]、gen_xor[1] ……),便于调试和引用(可用层次名引用,如
gen_xor[3].inst)。
组合逻辑
在组合逻辑电路中,任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无关。
Half adder
组合逻辑中最简单的就是半加器 (half adder),其可以接受两个输入并输出他们的和 和 是否存在进位 (carry-out),却无法处理进位 (carry-in) 的输入。
列出四个量的真值表,不难发现其逻辑关系:
| a | b | sum | carry-out |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 1 | 0 | 1 |
1 | module half_adder( |
Full adder
全加器 (full adder) 在半加器的基础上可以处理进位信号。真值表如下:
| a | b | carry-in | sum | carry-out |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 0 | 1 |
| 0 | 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 | 1 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |
写出 sum 的表达式是轻松的,但 carry-out 不是很显然。从进位来看有两种情况:
- a 和 b 本身就都是 1,无论 cin 是什么,肯定进位,即 a & b
- a 和 b 恰好一个是1、一个是0 (即 a ^ b = 1),此时如果有 cin = 1,就会产生进位,即 cin & (a ^ b)
1 | module full_adder( |
时序逻辑
在时序逻辑电路中,任一时刻的输出信号不仅取决于当时的输入信号,而且还取决于电路原来的状态。
一般来说,时钟 (clk) 会交替输出低电平和高电平信号,我们将由低电平变为高电平的瞬间称为 positive edge (posedge, 上升沿),反之为 negative edge (negedge, 下降沿)。
DFF (D flip-flop)
DFF 是一种可以存储 1 bit 内容并且随着时钟更新其内容的电路。通常随着 positive edge 更新。它是时序逻辑中最基本的单元,寄存器、计数器、状态机都由 DFF 构成。例如:
1 | module top_module ( |
工作过程:时钟上升沿到来前,q 一直保持旧值;上升沿那一瞬间,d 的值被”采样”并锁存到 q,之后再次保持不变,直到下一个上升沿。于是 q 相当于把 d 延迟了一个时钟周期——这就是”打拍”(延迟一拍)的基本实现。
Synchronous reset
同步复位是仅在 positive edge 和 negative edge 时才生效的复位。
1 | module top_module ( |
Asynchronous reset
异步复位是只要复位信号变为 1,即立刻复位,不需要等待时钟边沿才生效的复位。
1 | module top_module ( |
Shift Register
移位寄存器 (shift register) 由一串级联的触发器组成:每个时钟沿,所有位同时向左或向右移动一位。常见用途包括:
- 串行 ↔ 并行数据转换(SPI、UART 等接口的收发核心);
- 延迟线 / 打拍:让数据推迟若干个时钟周期;
- 乘除运算:左移 1 位相当于 ×2,右移 1 位相当于 ÷2;
- 构成计数器、FIFO、LFSR 等复杂结构。
下面构建一个 64 位算术移位寄存器 (arithmetic shift register),带同步加载 (synchronous load) 功能。移位器可以向左或向右移动,每次移动 1 位或 8 位,由 amount 信号选择。
算术右移 (arithmetic right shift) 移入的是寄存器中数的符号位(这里是 q[63]),而逻辑右移 (logical right shift) 移入的是 0。换个角度看:算术右移假定被移位的数是一个有符号数,并始终保持其符号不变,因此算术右移 n 位等价于有符号数除以 2ⁿ(向负无穷方向取整);逻辑右移则只是把二进制位原样搬移,与”数值”无关。
左移没有逻辑与算术之分:无论哪种左移,低位补入的都是 0,效果都是乘以 2 的幂。
各信号含义:
load:为 1 时把data[63:0]载入寄存器,而不是移位;ena:使能信号,为 1 时才移位;amount:选择移位的方向和位数:2'b00:左移 1 位;2'b01:左移 8 位;2'b10:右移 1 位;2'b11:右移 8 位;
q:移位寄存器的当前内容。
1 | module top_module( |
LFSR
线性反馈移位寄存器 (Linear Feedback Shift Register, LFSR) 是一种带反馈的移位寄存器:通常只靠少量 XOR 门,把某些位(称为抽头,tap)异或后反馈回去,生成寄存器的下一个状态。
如果抽头位置选择得当(对应本原多项式 (primitive polynomial)),LFSR 可以达到最大长度 (maximum-length):一个 n 位的最大长度 LFSR 会依次遍历 2ⁿ − 1 个状态后才重复。
同样的抽头可以搭出两种经典结构:Fibonacci(斐波那契)型与 Galois(伽罗瓦)型。两者反馈多项式相同、状态周期也相同,区别只在于反馈的”汇入方式”。
Fibonacci LFSR
Fibonacci 型(外部 XOR 型,many-to-one):所有抽头位先异或成一个反馈信号,再注入移位链的一个位置;其余位只是简单移位。多个抽头”汇总”到单个反馈点,故称 many-to-one。
以 5 位、抽头在第 5 位和第 3 位为例(抽头位置通常从 1 开始编号,第 1 位是输出位 q[0],所以两个抽头对应按 0 编号的 q[4] 和 q[2]):
1 | module top_module( |
整个电路只有一个 XOR 门:q[4] ^ q[2] 的结果直接送入最低位 q[0],其余四位只是把数据依次向后传递。数据在每个时钟沿”流”过整条移位链,反馈信号则从链的入口重新注入,周而复始。
Galois LFSR
Galois 型(内部 XOR 型,one-to-many):带抽头 (tap) 的位在移位时与输出位异或后得到新值,不带抽头的位直接移位。输出位被”分发”到各个抽头位置就地异或,每个抽头各有一个 XOR 门,故称 one-to-many。
下图是一个 5 位最大长度 Galois LFSR,抽头位于第 5 位和第 3 位。图中第 5 位的 XOR 门是为了画图一致性而画的,它的一个输入恒为 0。

对照上图,代码实现如下(抽头 5 和 3 对应按 0 编号的 q[4] 和 q[2]):
1 | module top_module( |
Fibonacci vs Galois 小结:
| Fibonacci 型 | Galois 型 | |
|---|---|---|
| 别称 | 外部 XOR 型(many-to-one) | 内部 XOR 型(one-to-many) |
| 反馈方式 | 所有抽头异或成一个信号,注入一个位置 | 输出位分发到每个抽头位,就地各异或一次 |
| XOR 门数量 | 抽头数 − 1(本例 1 个) | 每个抽头位各 1 个 |
| 特点 | 结构直观,抽头到反馈点的路径较长 | 抽头就地异或,关键路径短,更适合高速实现 |