FPGA 基础(02):Verilog 设计基础——组合逻辑、时序逻辑、状态机、同步设计
更新时间:2026-09-02。本文是
fpga/FPGA 领域第 02 篇,接 FPGA 架构原理。Verilog 是 FPGA 设计的核心语言,用 Verilog 描述硬件行为,综合工具映射到 LUT+FF。理解组合逻辑和时序逻辑的区别,是写好 FPGA 的基础。
本文要回答的问题
- Verilog 的三个用途:RTL 描述、仿真、综合分别要注意什么?
- 组合逻辑和时序逻辑怎么区分?为什么不能用 always 随便写?
- 状态机怎么写?Mealy 和 Moore 区别?
- 怎么避免锁存器(Latch)?怎么写可综合代码?
一、Verilog 的三种用途
Verilog 有三种不同的用途,写法不同:
1. RTL 描述(写硬件)
→ 可综合,映射到 LUT+FF
→ 用 assign / always@(posedge clk)
→ 考虑时序,考虑面积
2. 仿真验证(写 testbench)
→ 不可综合,只是仿真
→ 用 initial / forever / #10
→ 关注功能正确性
3. 时序分析(综合后)
→ 综合工具输出门级网表
→ 检查时序约束是否满足
→ 关注 setup/hold
本文只讲 RTL 描述,可综合的写法。二、组合逻辑
组合逻辑:输出只取决于当前输入,和时钟无关
写法 1:assign
assign y = a & b | c;
写法 2:always@(*)
always@(*) begin
if (sel)
y = a;
else
y = b;
end
注意:
always@(*) 必须写完整,所有分支都赋值
否则综合出锁存器(Latch)常见的组合逻辑:
verilog
// 多路选择器
assign y = sel ? a : b;
// 译码器
always@(*) begin
case (addr)
2'b00: y = 4'b0001;
2'b01: y = 4'b0010;
2'b10: y = 4'b0100;
2'b11: y = 4'b1000;
default: y = 4'b0000;
endcase
end
// 加法器
assign sum = a + b;三、时序逻辑
时序逻辑:输出取决于当前输入 + 前一状态,和时钟边沿有关
写法:
always@(posedge clk) begin
if (rst)
q <= 0;
else
q <= d;
end
特性:
always@(posedge clk) → 上升沿触发
用 <= (非阻塞赋值) → 在时钟沿统一更新
用 if (rst) → 复位常见的时序逻辑:
verilog
// 寄存器(D Flip-Flop)
always@(posedge clk or posedge rst) begin
if (rst)
q <= 0;
else
q <= d;
end
// 移位寄存器
always@(posedge clk) begin
shift_reg <= {shift_reg[14:0], din};
end
// 计数器
always@(posedge clk or posedge rst) begin
if (rst)
cnt <= 0;
else if (cnt == MAX)
cnt <= 0;
else
cnt <= cnt + 1;
end阻塞赋值 vs 非阻塞赋值
= (阻塞赋值):
顺序执行,立刻赋值
用于组合逻辑 always@(*)
<= (非阻塞赋值):
并行执行,时钟沿统一更新
用于时序逻辑 always@(posedge clk)
规则:
always@(*) 用 = (组合)
always@(posedge clk) 用 <= (时序)
混用会出问题,不推荐四、有限状态机(FSM)
FSM 是 FPGA 设计的基本模式
Moore 机:
输出只取决于当前状态
结构:状态转移 → 当前状态 → 输出
Mealy 机:
输出取决于当前状态 + 输入
结构:状态转移 + 输入 → 输出Moore 状态机模板:
verilog
// 状态编码
localparam IDLE = 2'b00;
localparam S1 = 2'b01;
localparam S2 = 2'b10;
// 状态寄存器
reg [1:0] state, next_state;
// 状态转移(时序)
always@(posedge clk or posedge rst) begin
if (rst)
state <= IDLE;
else
state <= next_state;
end
// 次态逻辑(组合)
always@(*) begin
case (state)
IDLE: if (start) next_state = S1;
else next_state = IDLE;
S1: if (done) next_state = S2;
else next_state = S1;
S2: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 输出逻辑(组合)
always@(*) begin
case (state)
IDLE: y = 0;
S1: y = 1;
S2: y = 0;
endcase
endFSM 编码风格:
- 三段式:状态寄存器 + 次态逻辑 + 输出逻辑,可读性好,时序好
- 二段式:状态寄存器 + 合并逻辑,省面积但不易调试
- 一段式:全写在一起,不推荐
五、避免锁存器(Latch)
锁存器是不想要的:
触发条件:
always@(*) 中,某条分支没赋值
case 语句没写 default
if 语句没写 else
示例:
always@(*) begin
if (sel) // 如果 sel=0,y 保持原值
y = a; // 综合出 Latch
end
正确写法:
always@(*) begin
if (sel)
y = a;
else
y = b; // 所有分支都赋值
end
为什么 Latch 不好:
- 时序分析困难
- 容易产生毛刺
- 设计不清晰
- 综合工具看到 Latch 会报警告六、同步设计原则
1. 所有时序逻辑用同一个时钟沿
always@(posedge clk) // 统一用上升沿
2. 复位要统一
全部同步复位或全部异步复位,不要混用
3. 跨时钟域要处理
慢时钟域 → 快时钟域:打两拍(双触发器同步)
快时钟域 → 慢时钟域:握手或异步 FIFO
4. 组合逻辑不要有组合反馈
assign y = y + 1; // 错误!组合反馈 → 振荡
5. 门控时钟要小心
尽量不要用组合逻辑产生时钟
用时钟使能(clock enable)代替七、常见坑对照
| 错误 | 现象 | 正确做法 |
|---|---|---|
| always@(*) 没写全 | 综合出 Latch | 所有分支都赋值,case 写 default |
| 混用 = 和 <= | 仿真结果不对 | 组合用 =,时序用 <= |
| 组合反馈 | 仿真振荡 | 时序逻辑 + 寄存器 |
| 门控时钟 | 时钟歪斜 | 用 clock enable |
| 跨时钟域直接打 | 亚稳态传播 | 至少打两拍 |
相关与延伸
下一篇:常用 IP 核——PLL、BRAM、FIFO、DSP48、SerDes、DDR 控制器;FPGA 架构,见 FPGA 架构原理。
一句话总结
Verilog 设计:组合逻辑用 assign 或 always@(*) 配合阻塞赋值 =;时序逻辑用 always@(posedge clk) 配合非阻塞赋值 <=;状态机推荐三段式(状态寄存器 + 次态逻辑 + 输出逻辑);always@(*) 不写全分支会综合出 Latch,必须所有分支都赋值 + case 写 default;同步设计原则:统一时钟沿、统一复位、跨时钟域打两拍、不用组合反馈、不用门控时钟。