Veloris.
返回索引
概念基础 2026-02-23

从代码到电路:MUX、触发器和锁存器的 Verilog 综合实战

9 分钟
2.8k words

从代码到电路:MUX、触发器和锁存器的 Verilog 综合实战

💡 你写完了一段 Verilog,点击综合,然后打开 Vivado 的 “Open Elaborated Design”——屏幕上出现了一张由 LUT、MUX 和触发器组成的电路原理图。你盯着它看了半分钟,突然意识到:这和你脑海中想象的电路完全不一样。

这个瞬间,就是从”会写 Verilog”到”会设计电路”的分水岭。

前面四篇文章,我们学完了 Verilog 的核心语法。但语法只是工具,真正的目标是让综合工具把你的代码变成正确、高效的硬件电路。这篇收官之作,我们要回答一个贯穿始终的核心问题:你写的每一行 Verilog,综合后到底变成了什么?

我们会通过三个最基础也最重要的电路结构——MUX(多路选择器)、D 触发器(DFF)和锁存器(Latch)——来建立”代码 → 电路”的映射直觉。

📌 系列衔接:本文是「FPGA 入门系列」Verilog 语法子系列的收官篇,承接上一篇 Verilog 行为级语句与编译指令


目录


1. MUX:从 if-else 和 case 到硬件选择器

MUX(Multiplexer,多路选择器)是组合逻辑中最常见的结构。在 Verilog 中,if-elsecase 和条件运算符 ?: 都会综合出 MUX。

不同代码 → 不同电路

下面两段代码功能相同,但综合出来的电路结构完全不同

// 写法A:先选择再计算
always @(*) begin
    if (sel)
        out = a + b;
    else
        out = c + d;
end
// 写法B:先计算再选择
always @(*) begin
    if (sel) begin
        temp1 = a;
        temp2 = b;
    end else begin
        temp1 = c;
        temp2 = d;
    end
end
assign out = temp1 + temp2;

写法 A 综合结果:2 个加法器 + 1 个 MUX

a,b → [加法器1] → [MUX] → out
c,d → [加法器2] ↗   ↑
                    sel

写法 B 综合结果:2 个 MUX + 1 个加法器

a,c → [MUX1] → [加法器] → out
b,d → [MUX2] ↗

       sel

加法器在 FPGA 中比 MUX 更消耗资源。写法 B 只用了 1 个加法器,资源效率更高。同样的功能,不同的写法,综合出完全不同的硬件——这就是为什么 FPGA 工程师必须”心中有电路”。

💡 工程师手记:刚开始写 RTL 的时候,我只关心”功能对不对”,从来不看综合报告。后来做一个信号处理项目,资源用了 95%,怎么优化都放不下。最后导师让我看综合后的原理图,才发现一段 if-else 里的加法器被重复综合了 4 份。改了几行代码,资源直接降了 30%。从那以后我明白了:写 Verilog 不是写软件,每一行代码都有面积成本。

if-else vs case 的硬件区别

特性if-elsecase
综合结构优先级 MUX 链并行 MUX
有无优先级✅ 有❌ 无
适用场景条件有主次(如复位 > 使能)条件互斥且权重相同

设计反思:在写代码之前,先问自己两个问题——面积优先还是性能优先?需要优先级还是不需要? 答案决定了你该用 if-else 还是 case。


2. D 触发器:时序逻辑的基石

D 触发器(D Flip-Flop,DFF)是 FPGA 中最基本的时序逻辑单元。在 Xilinx 7 系列 FPGA 中,每个 Slice 包含 8 个触发器,它们是你的核心时序资源。

DFF 的硬件端口

端口功能类型
D数据输入同步(时钟沿采样)
CLK时钟全局/局部布线
Q数据输出同步
CE时钟使能同步(高电平有效)
PRE异步置位异步(强制 Q=1)
CLR异步复位异步(强制 Q=0)

关键约束PRECLR 互斥——同一个触发器只能选择其中一个,不可共存。

Verilog 代码 → DFF 变体

(1) 基本 DFF(无复位)

always @(posedge clk) begin
    q <= d;
end

综合结果:最简单的 DFF,只使用 DCLKQ 端口。

(2) 异步复位 DFF

always @(posedge clk or posedge rst) begin
    if (rst)
        q <= 1'b0;    // 异步复位,高电平有效
    else
        q <= d;
end

综合结果:rst 信号连接到 DFF 的 CLR 端。注意敏感列表中必须包含复位信号or posedge rst),否则不是异步复位。

(3) 异步置位 DFF

always @(posedge clk or posedge set) begin
    if (set)
        q <= 1'b1;    // 异步置位,高电平有效
    else
        q <= d;
end

综合结果:set 信号连接到 DFF 的 PRE 端。

(4) 同步复位 DFF

always @(posedge clk) begin
    if (rst)
        q <= 1'b0;    // 同步复位
    else
        q <= d;
end

综合结果:复位逻辑通过 D 端的组合逻辑实现(复位信号与数据输入复用),不占用 CLR/PRE 端口

💬 你可能会问:同步复位和异步复位该选哪个?

两者各有优劣。异步复位不依赖时钟,上电即可复位,但容易引入亚稳态。同步复位时序更干净,但需要时钟在运行。Xilinx 官方推荐同步复位(节省 CLR/PRE 资源),但某些场景下异步复位是必要的(如上电复位)。具体选择取决于项目需求。

(5) 时钟使能 DFF

always @(posedge clk) begin
    if (rst)
        q <= 1'b0;
    else if (ce)
        q <= d;
    // else: q 保持不变(隐含)
end

综合结果:使用 DFF 的 CE 端口。当 ce=0 时,时钟被屏蔽,Q 保持不变。

注意:以下两种写法综合结果完全相同——else q <= q; 可以省略:

// 写法1:省略 else(推荐,更简洁)
always @(posedge clk) begin
    if (rst)     q <= 1'b0;
    else if (ce) q <= d;
end

// 写法2:显式写出 else
always @(posedge clk) begin
    if (rst)     q <= 1'b0;
    else if (ce) q <= d;
    else         q <= q;    // 等效于省略
end

避免错误:异步控制冲突

// ❌ 错误:同一个 DFF 不能同时有 PRE 和 CLR
always @(posedge clk or posedge clr or posedge pre) begin
    if (clr)      q <= 0;
    else if (pre) q <= 1;
    else          q <= d;
end
// 综合工具会报错或忽略其中一个

3. 锁存器:你最不想看到的电路

锁存器(Latch)是 FPGA 设计中的头号公敌。它不是你设计的,而是你的代码意外生成的

锁存器 vs 触发器

特性触发器(Flip-Flop)锁存器(Latch)
触发方式边沿敏感(时钟上升/下降沿)电平敏感(高/低电平)
数据采样仅在时钟边沿使能期间持续透明
FPGA 实现专用 FF 资源LUT 模拟(低效)
时序分析容易困难
毛刺敏感不敏感极易传播毛刺

生成锁存器的四种代码模式

(1) 不完整的 if-else

// ❌ 缺少 else → 锁存器
always @(*) begin
    if (enable) q = d;
end

(2) 不完整的 case

// ❌ 缺少 default → 锁存器
always @(*) begin
    case (sel)
        2'b00: q = a;
        2'b01: q = b;
    endcase
end

(3) 部分位未赋值

// ❌ q[0] 未赋值 → 该位生成锁存器
always @(*) begin
    q[3:1] = d[3:1];
end

(4) 电平敏感的敏感列表

// ❌ 对电平敏感 + 不完整条件 → 锁存器
always @(enable or d) begin
    if (enable) q = d;
end

锁存器的三大危害

  1. 时序问题:电平敏感特性难以满足建立/保持时间,导致时序违例
  2. 毛刺传播:组合逻辑产生的短脉冲可能被锁存,导致功能错误
  3. 资源浪费:FPGA 中锁存器由 LUT 反馈实现,比触发器更耗资源

如何避免锁存器

// ✅ 方法1:补全所有分支
always @(*) begin
    if (enable) q = d;
    else        q = 1'b0;
end

// ✅ 方法2:case 加 default
always @(*) begin
    case (sel)
        2'b00: q = a;
        2'b01: q = b;
        default: q = 1'b0;
    endcase
end

// ✅ 方法3:在 always 块开头给默认值
always @(*) begin
    q = 1'b0;          // 先给默认值
    if (enable) q = d; // 再按条件覆盖
end

// ✅ 方法4:改用时序逻辑(边沿触发)
always @(posedge clk) begin
    if (enable) q <= d;  // DFF,不是锁存器
end

检查综合报告

Vivado 中锁存器会触发 Warning:

[Synth 8-3277] inferring latch for variable 'q'

养成习惯:每次综合后检查 Warning 列表,搜索 “latch” 关键字。

💡 工程师手记:有一次我写了一个状态机,仿真完美通过,上板后偶尔输出错误数据。排查了一周,最后发现是 case 语句少了一个 default,综合出了一个锁存器。在某些极端时序条件下,毛刺被锁存导致状态跳转错误。从此我的综合后第一件事就是 grep latch——零容忍。

(建议替换为你自己的真实经历,读者会更有共鸣)


4. 组合逻辑 vs 时序逻辑:设计模式总结

组合逻辑模板

// 模板:组合逻辑
always @(*) begin
    // 先给所有输出默认值(防止锁存器)
    out1 = 1'b0;
    out2 = 1'b0;
    
    // 再按条件赋值
    if (条件) begin
        out1 = xxx;
        out2 = xxx;
    end
end

关键规则:

  • 敏感列表用 @(*)
  • 使用阻塞赋值 =
  • 所有分支都必须完整,或在开头给默认值

时序逻辑模板

// 模板:时序逻辑(同步复位 + 时钟使能)
always @(posedge clk) begin
    if (rst) begin
        // 复位值
        q <= 初始值;
    end else if (en) begin
        // 正常逻辑
        q <= 新值;
    end
    // else: 隐含保持,不会生成锁存器
end

关键规则:

  • 敏感列表是时钟边沿(posedge clk
  • 使用非阻塞赋值 <=
  • 必须考虑复位

信号赋值速查

场景赋值方式信号类型综合结果
assign a = b;连续赋值wire连线
always @(*) a = b;阻塞赋值reg组合逻辑
always @(posedge clk) a <= b;非阻塞赋值reg触发器

5. 可综合 vs 不可综合

最后,回顾一个贯穿本系列的核心概念:不是所有 Verilog 语法都能变成电路

可综合语法(RTL 设计使用)

语法综合结果
assign组合逻辑(连线、门电路)
always @(posedge clk)触发器
always @(*)组合逻辑
if-else / caseMUX
+ - *加法器、乘法器
& | ^ ~逻辑门
? :MUX
for(固定次数)展开为并行硬件
generate批量生成硬件
模块实例化子电路连接

不可综合语法(仅用于仿真/Testbench)

语法用途
initial初始化仿真变量
#10(延时)控制仿真时序
$display / $monitor打印调试信息
$readmemh / $readmemb从文件读取数据
$finish / $stop控制仿真器
fork...join并行仿真激励
/(除法)/ %(取模)通常不可综合(需 IP 核)
=== / !==四值精确比较

判断法则:如果你想象不出这段代码对应什么硬件电路,它大概率不可综合。


6. 总结

电路结构Verilog 来源关键注意
MUXif-else、case、?:不同写法 → 不同电路结构和资源
触发器(DFF)always @(posedge clk) + <=PRE/CLR 互斥,推荐同步复位
锁存器(Latch)不完整的组合逻辑零容忍,必须消除
组合逻辑assignalways @(*) + =补全所有分支

本系列核心认知回顾

经过这 5 篇文章,你应该建立起以下核心认知:

  1. Verilog 是描述电路,不是编程——每一行代码对应真实硬件
  2. module 是基本积木——通过实例化组合成大系统
  3. wire 和 reg 的区别在于赋值位置——不要被名字误导
  4. 运算符 = 硬件电路——加法器、MUX、逻辑门都有面积成本
  5. 阻塞 = 组合,非阻塞 = 时序——黄金规则,没有例外
  6. if-else 有优先级,case 没有——选择取决于设计需求
  7. 锁存器是 Bug——补全分支、检查综合报告
  8. 心中有电路——这是从 Verilog 新手到 FPGA 工程师的分水岭

下一步建议

  • 动手实践:在 HDLBits(hdlbits.01xz.net)上完成基础练习
  • 学会看综合报告:每次综合后检查资源使用和 Warning
  • 学会看 RTL 原理图:Vivado 的 “Open Elaborated Design” 可以直观展示代码对应的电路

常见问题

Q1:同步复位和异步复位在资源消耗上有什么区别?

在 Xilinx FPGA 中,异步复位使用 DFF 的专用 CLR/PRE 端口,不额外消耗 LUT。同步复位通过 D 端的组合逻辑实现,可能需要额外的 LUT。但 Xilinx 的综合工具对同步复位有专门优化(利用 SRST 端口),实际开销通常很小。另外需注意,高电平有效和低电平有效的复位在资源消耗上可能不同。

Q2:锁存器有没有合法的使用场景?

极少。理论上在异步接口(老式总线协议)和某些低功耗设计中可能用到锁存器。但在 FPGA 设计中,99.9% 的情况下锁存器都是 Bug。如果你不确定是否需要锁存器,答案几乎一定是”不需要”。

Q3:怎么判断综合结果是不是我想要的?

三个方法:(1) 看 Vivado 的综合报告,检查资源使用量(LUT、FF、DSP、BRAM);(2) 打开 “Schematic” 查看 RTL 原理图,确认电路结构;(3) 搜索 Warning 中的 “latch”、“multi-driven” 等关键字。

Q4:这个系列之后应该学什么?

建议路径:(1) 状态机设计(FSM)——数字系统的控制核心;(2) 时序约束和时序分析——让电路跑在目标频率上;(3) IP 核使用——PLL、FIFO、BRAM 等;(4) 实际项目实战——UART、SPI、I2C 等通信协议的 RTL 实现。


参考资料


系列导航:本文是「FPGA 入门系列」第 11 篇,也是 Verilog 语法子系列的收官篇。

如果这个系列对你有帮助,欢迎点赞、收藏、转发。从”会写 Verilog”到”写好 Verilog”,中间隔的就是”心中有电路”这四个字。希望这个系列能帮你迈出这关键一步。

End of file.