2. 数字设计基础回顾:组合逻辑与时序逻辑、同步设计原则、Verilog HDL核心语法速览
各位同学,欢迎来到第二章。说实话,每次开课讲到这一节,我都有点感慨。很多初学者一上来就追着RISC-V架构跑,结果写出来的代码在FPGA上一跑就冒烟。我自己刚入行那会儿也吃过这个亏——花了两周写了个流水线,综合时报了一堆时序违例,查了半天才发现是最基本的同步设计原则没搞对。
所以这一章,咱们把地基夯实。别嫌基础,你想想看,盖楼的时候地基要是歪了,后面装修得再漂亮也没用。
2.1 组合逻辑与时序逻辑
数字电路就两大块:组合逻辑和时序逻辑。说白了,组合逻辑就是「输入一变,输出立马变」,没有记忆功能。时序逻辑呢,就是「等到时钟沿来了,我才变」,它有状态,能记住东西。
核心区别一句话:
- 组合逻辑:输出 = f(当前输入)
- 时序逻辑:输出 = f(当前输入 + 之前的状态)
我在项目中遇到过一位同事,写了个计数器,组合逻辑里直接赋值 reg 变量,结果仿真怎么都对,上板子就乱跳。为什么?因为他忘了组合逻辑里不能有「记忆」——你想想看,没有时钟沿,它凭什么记住上次的值?
2.1.1 组合逻辑的典型写法
组合逻辑用 assign 或者 always @(*) 来实现。我个人习惯用 always @(*),因为可读性更好,尤其是处理复杂逻辑时。
// 组合逻辑:全加器
module full_adder(
input a, b, cin,
output sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
2.1.2 时序逻辑的典型写法
时序逻辑必须用 always @(posedge clk) 或 always @(negedge clk)。这里有个坑——我见过有人把敏感列表写成 always @(clk),结果综合出来是个锁存器,不是触发器。嗯,这里要注意,时序逻辑必须指定时钟沿。
// 时序逻辑:带同步复位的D触发器
module dff_sync(
input clk, rst_n,
input d,
output reg q
);
always @(posedge clk) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
⚠️ 避坑指南:我曾经犯过一个低级错误——在时序逻辑里用阻塞赋值 = 代替非阻塞赋值 <=。仿真时看起来没问题,但综合后电路行为完全不对。记住:时序逻辑用非阻塞赋值,组合逻辑用阻塞赋值。这是铁律。
2.2 同步设计原则
同步设计,说白了就是「所有时序逻辑都听同一个时钟的指挥」。为什么非要这样?因为异步设计太难控制了。我早期做过一个项目,用了两个不同频率的时钟,结果跨时钟域的数据老是出错,查了整整三天才找到问题。
同步设计的三个基本原则,我建议你刻在脑子里:
- 单时钟域原则:一个模块尽量只用一个时钟。如果必须用多个时钟,一定要做跨时钟域同步处理。
- 避免组合逻辑反馈:组合逻辑的输出不能直接连回自己的输入,否则会形成组合环路。这玩意儿综合器有时候检查不出来,但流片回来必死。
- 寄存器输出原则:模块的输出信号最好都经过一级寄存器打一拍。这样能有效减少毛刺,也方便时序收敛。
💡 小技巧:我习惯在顶层模块的每个输出端口后面都加一级 reg 打拍。虽然多了一个时钟周期的延迟,但换来了整个系统的稳定性。在FPGA上,这一拍通常不会成为瓶颈。
2.3 Verilog HDL 核心语法速览
Verilog 语法其实不多,常用的就那么几个。我把它分成三块来讲:模块结构、数据类型、过程块。
2.3.1 模块结构
每个 Verilog 文件就是一个 module,端口定义写在括号里。我个人习惯把输入和输出分开写,清晰明了。
module counter #(
parameter WIDTH = 8 // 参数化设计,方便复用
)(
input clk,
input rst_n,
input en,
output reg [WIDTH-1:0] count
);
// 功能代码写在这里
endmodule
2.3.2 数据类型
Verilog 里最常用的数据类型就三种:wire、reg、integer。初学者最容易搞混的是 wire 和 reg。
| 类型 | 用途 | 赋值方式 |
|---|---|---|
wire |
组合逻辑连线 | assign 或模块端口连接 |
reg |
时序逻辑变量 | always 块内赋值 |
integer |
循环变量或通用整数 | 一般用于 for 循环 |
注意:很多人以为 reg 就一定是寄存器,其实不是。在 always @(*) 里赋值的 reg 综合出来是组合逻辑。只有 always @(posedge clk) 里的 reg 才是真正的触发器。
2.3.3 过程块
Verilog 里最重要的过程块就是 always。我总结了三种常用模式:
- 组合逻辑模式:
always @(*),敏感列表用*,自动包含所有输入信号。 - 时序逻辑模式:
always @(posedge clk),只对时钟上升沿敏感。 - 锁存器模式:
always @(enable or data),这个要慎用,我建议你尽量别用。
// 组合逻辑:多路选择器
always @(*) begin
case(sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
default: out = d;
endcase
end
// 时序逻辑:移位寄存器
always @(posedge clk) begin
if (!rst_n)
shift_reg <= 8'b0;
else
shift_reg <= {shift_reg[6:0], data_in};
end
2.4 本章知识体系
下面这张图是我自己画的,把本章的核心知识点串起来了。你仔细看看,能帮你理清思路。
这张图你看懂了吗?组合逻辑和时序逻辑是两大支柱,同步设计原则是连接它们的桥梁。而 Verilog 语法,就是实现这一切的工具。说白了,你只要掌握了 module、wire、reg、always 这四个东西,就能写出 90% 以上的数字电路。
💡 我的建议:初学者不要急着去背语法手册。你先拿一个小项目练手,比如写个 8 位计数器、一个简单的状态机。遇到不会的语法再查,这样记得更牢。我当年就是这么过来的。