Verilog HDL 基础语法

最近在学习数电和 Verilog,写此就当复习了罢。依照 HDLBits 编写。

基础语法

Wire

导线是一种具有一个输入和一个输出的模块,用来表示电路中两点之间的连接。可以通过 assign 语句将两个 Wire 连接起来。例如:

input 和 output 声明的为 Wire 类型,除非显式声明 Reg。

1
2
3
module top_module(input in, output out);
assign out = in;
endmodule

Gates

Gate 有以下类型:AND(&), OR(|), XOR(^) 及各自的否定。声明如下:

1
2
3
4
5
6
7
8
module top_module( input a, input b, output out );
assign out = a & b; //AND
assign out = ~(a & b); //NAND
assign out = a | b; //OR
assign out = ~(a | b); //NOR
assign out = a ^ b; //XOR
assign out = ~(a ^ b); //NXOR
endmodule

按位运算符 vs 逻辑运算符

  • a & b(按位与):逐位运算,结果是向量
  • a && b(逻辑与):把操作数当作整体判断真假,结果是 1 bit(0 或 1)。
  • 例如 4'b1010 & 4'b1100 = 4'b1000 4'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
2
3
4
5
6
module top_module( input [31:0] in, output [31:0] out );
assign out[31:24] = in[7:0];
assign out[23:16] = in[15:8];
assign out[15:8] = in[23:16];
assign out[7:0] = in[31:24];
endmodule

当然也可以像 C++ 那样单独取出一位,即

1
assign out[0] = in[0];

拼接 {}:把多个信号按顺序拼成向量,例如 {a, b} 表示 a 在高位、b 在低位;

复制 {n{}}:把某个信号重复 n 份再拼接,如 {4{1'b0}} 等于 4'b0000

Module

模块实例化就是在电路中调用一个已经定义好的模块,把它当作一个子电路使用。复杂的电路就是由一层层模块例化 (instantiate) 堆叠出来的,每例化一次就生成一份独立的实例 (instance)

假设已经定义了一个子模块:

1
2
3
module mod_a ( output out, input in1, input in2, input in3 );
...
endmodule

有以下两种连接方式:

Connecting by position

按端口声明的顺序,依次把信号传入子模块,顺序必须与子模块声明的端口顺序完全一致。

1
2
3
module top_module ( ... );
mod_a instance1 ( out_wire, in1_wire, in2_wire, in3_wire );
endmodule

Connecting by name

指定每个端口的名字,用 .端口名(信号名) 的形式连接。

1
2
3
4
5
6
7
8
module top_module ( ... );
mod_a instance2 (
.out(out_wire),
.in1(in1_wire),
.in2(in2_wire),
.in3(in3_wire)
);
endmodule

Always

always过程语句 (procedural statement) 块,其描述”在特定时刻执行的逻辑”。

组合逻辑

敏感列表应包含块内用到的所有输入信号,通常写作 always @(*),让工具自动推断敏感列表,避免遗漏导致仿真与综合结果不一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
module top_module(
input a,
input b,
input sel,
output reg out
);
always @(*) begin
if (sel)
out = a;
else
out = b;
end
endmodule

时序逻辑

用于描述时序逻辑(触发器)时,敏感列表通常是时钟的边沿,例如 posedge clk(上升沿)或 negedge clk(下降沿)。

1
2
3
4
5
6
7
8
9
module top_module(
input clk,
input d,
output reg q
);
always @(posedge clk) begin
q <= d;
end
endmodule

if and case

if-else

if-else 语句只能写在 always 块内部,用法与 C 基本一致:

1
2
3
4
5
6
7
8
always @(*) begin
if (condition1)
out = a;
else if (condition2)
out = b;
else
out = c;
end

当然也可以使用三元运算符:

1
2
3
4
5
6
7
8
9
assign out = sel ? a : b;

// 等价于:
always @(*) begin
if (sel)
out = a;
else
out = b;
end

case

当分支较多、且是对同一个信号的不同取值进行判断时,case 语句比一长串 if-else if 更加清晰:

1
2
3
4
5
6
7
8
9
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = 1'bx;
endcase
end

和 if 一样,case 也应写上 default 分支覆盖未列出的情况,避免产生锁存器。

casez

casez 是 case 的变体,允许在比较时使用 ? 作为通配符,表示该位不参与比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module (
input [3:0] in,
output reg [1:0] pos
);
always @(*) begin
casez (in)
4'bzzz1: pos = 0; // 最低有效位(LSB)为 1
4'bzz1z: pos = 1;
4'bz1zz: pos = 2;
4'b1zzz: pos = 3;
default: pos = 2'bxx;
endcase
end
endmodule

上面这个例子实现了一个简单的优先级编码器:从高位往下看,casez 会按照分支书写的顺序依次匹配,第一个匹配成功的分支生效,z(或 ?)位不影响匹配结果。

for-loop

Verilog 是硬件描述语言,for 循环是在**综合/仿真时被展开(unroll)**成多份重复的硬件逻辑——它不是软件里那种”运行时循环”,而是编译期”批量写代码”的工具。

Verilog 中的 for 循环主要有两种使用场景:过程语句内的 for(在 always 块中) 和 生成语句的 for(generate for,用于例化多个模块或产生重复的硬件结构)。

always 块中的 for 循环

用于在组合或时序逻辑中,对一组信号做重复性的操作,比如逐位处理向量:

1
2
3
4
5
6
7
8
9
10
11
12
module top_module(
input [99:0] in,
output reg [99:0] out
);
integer i;

always @(*) begin
for (i = 0; i < 100; i = i + 1) begin
out[i] = in[99 - i]; // 反转向量的位顺序
end
end
endmodule

这种写法本质上仍是组合逻辑(如果用的是 always @(*)),for 只是帮你少写 100 行 out[i] = ...,展开后和手写 100 条语句是等价的。

generate for(生成块)

当需要例化多个相同的子模块,或者生成一组重复的硬件结构时,使用 generate … for … endgenerate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module(
input [99:0] a,
input [99:0] b,
output [99:0] out
);
genvar i;

generate
for (i = 0; i < 100; i = i + 1) begin : gen_xor
xor_gate inst (
.a(a[i]),
.b(b[i]),
.out(out[i])
);
end
endgenerate
endmodule
  • 循环变量必须声明为特殊类型 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
2
3
4
5
6
module half_adder( 
input a, b,
output cout, sum );
assign sum = a ^ b;
assign cout = a & b;
endmodule

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
2
3
4
5
6
module full_adder( 
input a, b, cin,
output cout, sum );
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (cin & (a ^ b));
endmodule

时序逻辑

在时序逻辑电路中,任一时刻的输出信号不仅取决于当时的输入信号,而且还取决于电路原来的状态。

一般来说,时钟 (clk) 会交替输出低电平和高电平信号,我们将由低电平变为高电平的瞬间称为 positive edge (posedge, 上升沿),反之为 negative edge (negedge, 下降沿)。

DFF (D flip-flop)

DFF 是一种可以存储 1 bit 内容并且随着时钟更新其内容的电路。通常随着 positive edge 更新。它是时序逻辑中最基本的单元,寄存器、计数器、状态机都由 DFF 构成。例如:

1
2
3
4
5
6
7
8
9
10
module top_module (
input clk, // 时序电路中需存在时钟
input d,
output reg q );

always @(posedge clk) begin
q <= d; // 使用非阻塞赋值
end

endmodule

工作过程:时钟上升沿到来前,q 一直保持旧值;上升沿那一瞬间,d 的值被”采样”并锁存到 q,之后再次保持不变,直到下一个上升沿。于是 q 相当于把 d 延迟了一个时钟周期——这就是”打拍”(延迟一拍)的基本实现。

Synchronous reset

同步复位是仅在 positive edge 和 negative edge 时才生效的复位。

1
2
3
4
5
6
7
8
9
10
11
12
13
module top_module (
input clk,
input reset, // Synchronous reset
input [7:0] d,
output [7:0] q
);
always @(posedge clk) begin
if (reset)
q <= 8'b0;
else
q <= d;
end
endmodule

Asynchronous reset

异步复位是只要复位信号变为 1,即立刻复位,不需要等待时钟边沿才生效的复位。

1
2
3
4
5
6
7
8
9
10
11
12
13
module top_module (
input clk,
input areset, // active high asynchronous reset
input [7:0] d,
output [7:0] q
);
always @(posedge clk or posedge areset) begin
if (areset)
q <= 8'd0;
else
q <= d;
end
endmodule

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module top_module(
input clk,
input load,
input ena,
input [1:0] amount,
input [63:0] data,
output reg [63:0] q);

always @(posedge clk) begin
if (load)
q <= data;
else if (ena) begin
case (amount)
2'b00: q <= {q[62:0], 1'b0};
2'b01: q <= {q[55:0], 8'b0};
2'b10: q <= {q[63], q[63:1]};
2'b11: q <= {{8{q[63]}}, q[63:8]};
endcase
end
end

endmodule

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module(
input clk,
input reset, // 高电平有效的同步复位,复位到 5'h1
output [4:0] q
);
always @(posedge clk) begin
if (reset)
q <= 5'h1;
else begin
q[0] <= q[4] ^ q[2]; // 抽头异或成一个反馈信号,注入最低位
q[1] <= q[0]; // 其余位照常移位
q[2] <= q[1];
q[3] <= q[2];
q[4] <= q[3];
end
end
endmodule

整个电路只有一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module(
input clk,
input reset, // 高电平有效的同步复位,复位到 5'h1
output [4:0] q
);
always @(posedge clk) begin
if (reset)
q <= 5'h1;
else begin
q[4] <= q[0]; // 输出位反馈到最高位(位置 5 的 XOR 输入为 0,等价于直连)
q[3] <= q[4]; // 无抽头位:直接移位
q[2] <= q[3] ^ q[0]; // 抽头位:新值 = 上一位移来的值 异或 输出位
q[1] <= q[2]; // 无抽头位:直接移位
q[0] <= q[1]; // 无抽头位:直接移位
end
end
endmodule

Fibonacci vs Galois 小结

Fibonacci 型 Galois 型
别称 外部 XOR 型(many-to-one) 内部 XOR 型(one-to-many)
反馈方式 所有抽头异或成一个信号,注入一个位置 输出位分发到每个抽头位,就地各异或一次
XOR 门数量 抽头数 − 1(本例 1 个) 每个抽头位各 1 个
特点 结构直观,抽头到反馈点的路径较长 抽头就地异或,关键路径短,更适合高速实现