3. RTL 综合基础:使用 yosys 进行逻辑综合

RTL 综合,说白了就是把我们写的 Verilog 代码,翻译成芯片能用的门级网表。这个过程有点像把建筑设计图转成施工图——你画的是功能模块,综合工具给你换成具体的门电路和连线。

我刚开始接触综合时,总觉得这步很简单。直到有一次,我写了一个看起来很漂亮的状态机,综合出来面积大得吓人。嗯,从那以后我才明白,综合不是简单的翻译,而是一场性能、面积、功耗的博弈。

3.1 综合的本质:从行为到结构

综合工具到底在做什么?我习惯把它拆成三步:

  1. 解析:读入 RTL 代码,理解你的设计意图
  2. 转换:把行为级描述转成布尔逻辑表达式
  3. 映射:把逻辑表达式映射到目标工艺库的单元上

你想想看,同样的功能,用不同的写法,综合出来的结果可能天差地别。举个例子:

// 写法一:case 语句
always @(*) begin
    case (sel)
        2'b00: out = a;
        2'b01: out = b;
        2'b10: out = c;
        2'b11: out = d;
    endcase
end

// 写法二:if-else 链
always @(*) begin
    if (sel == 2'b00)      out = a;
    else if (sel == 2'b01) out = b;
    else if (sel == 2'b10) out = c;
    else                   out = d;
end

这两种写法功能完全一样,但综合出来的电路结构不同。case 语句会生成优先级编码器吗?不会,它生成的是并行多路选择器。而 if-else 链会生成优先级结构——这在时序上可能有隐患。

我的习惯:写多路选择时优先用 case 语句,除非你真的需要优先级逻辑。if-else 链的优先级结构会导致关键路径变长,这在高速设计中是个坑。

3.2 yosys 实战:从 RTL 到网表

好,理论说完了,咱们直接上手。用 yosys 做综合,我一般按这个流程走:

# 1. 读取设计文件
read_verilog counter.v

# 2. 指定顶层模块
hierarchy -top counter

# 3. 预处理:展开宏、处理参数
proc
opt

# 4. 技术映射到标准单元
techmap
opt

# 5. 输出门级网表
write_verilog counter_synth.v

这个流程看起来简单,但每一步都有讲究。我重点说说几个关键命令:

命令 作用 我踩过的坑
proc 把 always 块转成逻辑表达式 不执行 proc 直接 techmap,会报错
opt 逻辑优化,消除冗余 优化力度不够时,面积会偏大
techmap 映射到工艺库 库文件路径不对,映射会失败
dfflibmap 映射触发器 时序单元映射要单独处理
注意:yosys 默认不带工艺库。你需要准备一个 Liberty 格式的库文件(.lib),或者用 yosys 自带的 synth 命令做粗略综合。我在项目中通常先用 synth -top top_module 快速验证功能,再用完整流程做精细综合。

3.3 综合策略:面积、速度、功耗的三角博弈

综合不是一键搞定的事。你需要告诉工具:你更看重什么?

我一般把综合策略分成三类:

  • 面积优先:资源共享、逻辑复用、减少冗余
  • 速度优先:展平逻辑、减少级数、插入流水线
  • 功耗优先:门控时钟、操作数隔离、低功耗单元

这三者往往互相矛盾。你想想看,要速度快,就得用并行结构,面积自然就大了。要面积小,就得共享资源,时序就会变差。

我在一个无线通信项目中就遇到过这种困境。客户要求芯片面积控制在 5mm² 以内,同时要跑 500MHz。我试了三次综合:

  1. 第一次:面积优先,结果时序违例 200ps
  2. 第二次:速度优先,面积超标 30%
  3. 第三次:手动调整关键路径,面积刚好达标,时序留了 50ps 余量

嗯,综合策略不是选一个就完事了,很多时候需要手动干预。

3.4 约束:告诉工具你的真实需求

约束是综合中最容易被忽视的部分。没有约束,工具就不知道你的目标是什么。

我常用的约束包括:

  • 时钟约束:频率、占空比、抖动
  • 输入延迟:数据从外部到达芯片的时间
  • 输出延迟:数据从芯片输出到外部的时间
  • 伪路径:不需要时序检查的路径
  • 多周期路径:需要多个时钟周期才能稳定的路径

在 yosys 中,约束通常通过 SDC 文件传入。不过 yosys 对 SDC 的支持有限,我一般用 Tcl 脚本直接设置:

# 设置时钟周期为 10ns(100MHz)
create_clock -name clk -period 10 [get_ports clk]

# 设置输入延迟
set_input_delay -clock clk 2 [get_ports data_in]

# 设置输出延迟
set_output_delay -clock clk 3 [get_ports data_out]

# 标记伪路径
set_false_path -from [get_ports rst_n]
关键点:约束不是越紧越好。我曾经把时钟约束设得比实际需求严 20%,结果综合工具花了 3 倍时间,面积也大了 15%。合理的约束应该留 10%-15% 的余量,既保证时序收敛,又不浪费资源。

3.5 综合结果分析:别只看面积和频率

综合完成后,你会得到一堆报告。我建议重点看这几个指标:

指标 含义 我的经验值
Cell Count 标准单元数量 控制在预估的 ±10% 以内
Area 总面积(μm²) 不要超过预算的 5%
Worst Slack 最差时序余量 至少留 50ps 余量
Fanout 最大扇出 超过 8 就要考虑插入缓冲器
Transition 信号跳变时间 不要超过库规定的 80%

有一次,我看到综合报告里面积很小,但时序余量是负的。我以为是约束设错了,查了半天才发现是某个模块的扇出太大——一个信号驱动了 32 个触发器,导致跳变时间过长。插入两级缓冲器后,时序就收敛了。

3.6 知识体系总览

下面这张图总结了 RTL 综合的核心流程和关键决策点:

RTL 综合知识体系 RTL 代码 工艺库 (.lib) 约束 (SDC) yosys 综合引擎 解析 → 转换 → 优化 → 映射 面积优先 速度优先 功耗优先 门级网表 (.v) 结果分析:面积 / 时序 / 扇出 / 跳变时间

这张图把整个综合流程串起来了。从 RTL 代码、工艺库、约束三个输入开始,经过 yosys 引擎的处理,再根据你的策略偏好进行优化,最终输出门级网表。最后一步是分析结果,看看是否满足你的预期。

避坑指南:我曾经在综合时忘了加约束,结果工具默认把所有路径都设成 1ns 周期。我的设计实际只需要 10ns,但工具为了满足 1ns 的约束,插入了大量缓冲器,面积翻了一倍。嗯,从那以后,我每次综合前都会先检查约束文件。