4. Verilog基础(二):组合逻辑与时序逻辑
好,咱们接着聊。上一章我们把Verilog的基本语法过了一遍,这一章要动真格的了——组合逻辑和时序逻辑,这是数字电路的两大基石。说白了,你写的所有RTL代码,最终都会映射成这两种逻辑的混合体。
我个人习惯把组合逻辑比作「直来直去的路」,信号进去,立马出来,不带任何记忆。时序逻辑呢,就像「带寄存器的流水线」,信号要等时钟沿来了才更新。搞懂这两者的区别,你写代码时心里就有底了。
4.1 组合逻辑基础
组合逻辑的特点是:输出只取决于当前输入,跟历史状态无关。没有时钟,没有存储,纯纯的「即时响应」。
4.1.1 基本门电路:与或非
先看最简单的与或非门。Verilog里用 &、|、~ 表示。我在项目中见过不少新手把位运算和逻辑运算搞混,这里要特别注意。
// 与或非门示例
module basic_gates (
input wire a, b,
output wire y_and,
output wire y_or,
output wire y_not
);
assign y_and = a & b; // 与门
assign y_or = a | b; // 或门
assign y_not = ~a; // 非门
endmodule
嗯,这里要注意:assign 语句是连续赋值,只要右边变了,左边立刻更新。这就是组合逻辑的本质。
4.1.2 多路选择器
多路选择器(MUX)是组合逻辑里的常客。我做过一个数据通路的设计,里面用了上百个MUX,选得我眼花缭乱。
用 always @(*) 配合 case 语句写MUX,是最直观的方式:
// 4选1多路选择器
module mux4 (
input wire [1:0] sel,
input wire [3:0] in0, in1, in2, in3,
output reg [3:0] out
);
always @(*) begin
case (sel)
2'b00: out = in0;
2'b01: out = in1;
2'b10: out = in2;
2'b11: out = in3;
default: out = 4'b0;
endcase
end
endmodule
你想想看,为什么这里用 reg 类型?其实在 always 块里赋值的变量,必须声明为 reg,但它综合出来还是组合逻辑。别被名字骗了。
4.1.3 加法器
加法器是算术运算的核心。最简单的写法就是用 + 运算符,综合工具会自动帮你生成合适的加法器结构。
// 8位加法器
module adder8 (
input wire [7:0] a, b,
input wire cin,
output wire [7:0] sum,
output wire cout
);
assign {cout, sum} = a + b + cin;
endmodule
我曾经在项目中遇到一个坑:两个16位数相加,结果只取了低16位,进位直接扔了。仿真没问题,上板子就出bug。后来查了半天,才发现是位宽没对齐。所以加法器一定要考虑进位,尤其是级联的时候。
组合逻辑避坑指南
- 所有组合逻辑的输入变化,输出必须立刻变化——不能有锁存器(latch)
always @(*)块里,每个分支都要给所有输出赋值,否则综合出latch- 位宽匹配:运算结果不要截断,除非你明确知道要丢弃高位
4.2 时序逻辑基础
时序逻辑就不一样了。它靠时钟驱动,有记忆能力。说白了,就是「等时钟来了再干活」。
4.2.1 触发器与寄存器
触发器(Flip-Flop)是时序逻辑的最小单元。寄存器就是一组触发器拼在一起。在Verilog里,用 always @(posedge clk) 来描述。
// D触发器
module dff (
input wire clk,
input wire rst_n,
input wire d,
output reg q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
// 8位寄存器
module reg8 (
input wire clk,
input wire rst_n,
input wire [7:0] d,
output reg [7:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 8'b0;
else
q <= d;
end
endmodule
注意看,这里用的是 <=(非阻塞赋值),不是 =。为什么?后面会细讲。
4.2.2 计数器
计数器是时序逻辑的经典应用。我做过一个分频器,计数器从0数到N-1,然后翻转输出时钟。看起来简单,但边界条件很容易搞错。
// 模16计数器
module counter16 (
input wire clk,
input wire rst_n,
input wire en,
output reg [3:0] count,
output reg overflow
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 4'b0;
overflow <= 1'b0;
end else if (en) begin
if (count == 4'd15) begin
count <= 4'b0;
overflow <= 1'b1;
end else begin
count <= count + 1'b1;
overflow <= 1'b0;
end
end
end
endmodule
嗯,这里有个细节:count == 4'd15 这个判断,如果你写成 count == 4'b1111 也行,但可读性差一些。我个人习惯用十进制数,一眼就能看出模值。
4.3 阻塞赋值与非阻塞赋值
这是Verilog里最容易踩坑的地方,没有之一。我见过太多人因为赋值方式用错,仿真和综合结果不一致,debug到崩溃。
4.3.1 阻塞赋值 =
阻塞赋值是「立即生效」的。执行完当前语句,变量立刻更新,后面的语句能看到新值。它用在组合逻辑的 always @(*) 块里。
// 阻塞赋值示例
always @(*) begin
a = b;
c = a; // 这里c拿到的是b的新值
end
4.3.2 非阻塞赋值 <=
非阻塞赋值是「延迟生效」的。所有赋值语句同时计算右值,然后在时钟沿统一更新左值。它用在时序逻辑的 always @(posedge clk) 块里。
// 非阻塞赋值示例
always @(posedge clk) begin
a <= b;
c <= a; // 这里c拿到的是a的旧值
end
黄金法则:
- 组合逻辑用阻塞赋值
= - 时序逻辑用非阻塞赋值
<= - 同一个
always块里,不要混用两种赋值
我曾经接手过一个项目,前任工程师在时序逻辑里用了阻塞赋值,结果仿真波形看起来没问题,但综合出来的电路多了一级不必要的延迟。后来花了整整两天才定位到问题。从那以后,我写代码前都会默念一遍:时序用非阻塞,组合用阻塞。
4.4 本章知识体系
下面这张图,是我梳理的本章核心逻辑。你可以把它当作一个「思维导图」,方便回顾。
我的小建议:刚开始学的时候,可以先把组合逻辑和时序逻辑分开写。组合逻辑用 assign 或 always @(*),时序逻辑用 always @(posedge clk)。等熟练了再考虑混合使用。这样不容易出错。
好了,这一章的内容就到这儿。组合逻辑和时序逻辑是FPGA设计的左右手,缺一不可。赋值方式的选择更是决定了你的代码能不能正确综合。多写、多仿真、多对比,慢慢就有感觉了。
公众号:蓝海资料掘金营,微信deep3321