4. 综合优化技巧:时序、面积、功耗与DFT插入
综合这一步,说白了就是把RTL代码翻译成门级网表。但翻译得好不好,差别可大了去了。我见过不少新手,跑完综合一看时序过了就交差,结果后端布局布线时发现一堆问题。嗯,综合阶段的优化,其实是为后端铺路。
核心原则:综合不是终点,而是后端流程的起点。时序、面积、功耗三者需要权衡,DFT插入则要提前规划。
4.1 时序优化策略
时序优化,说白了就是让信号在规定时间内到达。我个人习惯把时序优化分成三步:约束、结构、单元。
4.1.1 约束驱动优化
没有合理的约束,综合工具就像没头苍蝇。我建议你至少把下面这几类约束写清楚:
| 约束类型 | 关键点 | 常见坑 |
|---|---|---|
| 时钟约束 | 频率、抖动、不确定性 | 时钟不确定性设太小,工具过度优化 |
| 输入延迟 | 外部到端口的延迟 | 设太紧导致面积爆炸 |
| 输出延迟 | 端口到外部寄存器的延迟 | 设太松导致后端时序难收敛 |
| 伪路径 | 跨时钟域、测试模式路径 | 漏设伪路径,工具白费力气 |
| 多周期路径 | 允许跨多个时钟周期的路径 | 设错周期数导致功能错误 |
我的经验:时钟不确定性我一般设时钟周期的5%~10%。设太小了,工具会拼命插缓冲器,面积和功耗都受不了。
4.1.2 结构优化技巧
约束写好了,接下来看代码结构。工具再聪明,也架不住烂代码。我遇到过几次,综合后关键路径都在一个巨大的多路选择器上。为什么会这样?因为代码里写了一大堆嵌套的if-else。
常用的结构优化方法:
- 关键路径寄存器平衡:把组合逻辑分散到多个时钟周期
- 操作符共享:复用加法器、乘法器等资源
- 逻辑重定时:移动寄存器位置来缩短关键路径
- 流水线插入:在长组合逻辑中插入寄存器
// 不好的写法:长组合逻辑链
always @(*) begin
result = a + b + c + d + e + f; // 6级加法
end
// 好的写法:流水线结构
always @(posedge clk) begin
sum1 <= a + b;
sum2 <= c + d;
sum3 <= e + f;
end
always @(posedge clk) begin
result <= sum1 + sum2 + sum3;
end
4.1.3 单元级优化
工具在单元选择上也有讲究。我记得有一次,一个关键路径总是差那么一点点。后来发现,工具默认用了标准阈值的单元。换成低阈值单元后,时序立马就过了。
| 单元类型 | 速度 | 漏电功耗 | 适用场景 |
|---|---|---|---|
| 高阈值(HVT) | 慢 | 低 | 非关键路径 |
| 标准阈值(SVT) | 中 | 中 | 一般路径 |
| 低阈值(LVT) | 快 | 高 | 关键路径 |
| 超低阈值(ULVT) | 最快 | 最高 | 最关键的路径 |
注意:低阈值单元用多了,漏电功耗会飙升。我曾经在一个项目中,为了赶时序,大面积用了LVT单元,结果功耗超标了30%。后来不得不重新优化,把非关键路径换回HVT。
4.2 面积优化策略
面积优化,说白了就是省钱。芯片面积直接决定成本。我见过一些团队,综合时完全不看面积报告,结果后端发现芯片放不下。
面积优化的常用手段:
- 资源共享:多个功能模块共用同一个运算单元
- 状态机优化:减少状态数,优化状态编码
- 逻辑简化:去除冗余逻辑,简化布尔表达式
- 门控时钟:减少不必要的寄存器翻转
- 模块边界优化:跨模块的寄存器合并
// 面积优化示例:资源共享
// 不好的写法:两个独立的加法器
assign sum_a = data_a + offset_a;
assign sum_b = data_b + offset_b;
// 好的写法:复用同一个加法器
always @(*) begin
if (sel) begin
result = data_a + offset_a;
end else begin
result = data_b + offset_b;
end
end
我的习惯:综合完成后,第一件事就是看面积报告。如果某个模块面积异常大,我会去检查代码,看看是不是有冗余逻辑或者不必要的寄存器。
4.3 功耗优化策略
功耗优化现在越来越重要。特别是移动设备和AI芯片,功耗直接决定产品竞争力。功耗优化可以从动态功耗和静态功耗两个角度入手。
4.3.1 动态功耗优化
动态功耗跟信号翻转率直接相关。信号翻得越频繁,功耗越大。我建议从下面几个方向入手:
- 门控时钟:这是最有效的动态功耗优化手段。不用的模块,直接把时钟关掉
- 操作数隔离:当运算结果不被使用时,阻止输入信号翻转
- 数据编码优化:减少总线上的翻转次数
- 低功耗状态机:使用格雷码或one-hot编码
// 门控时钟示例
// 不好的写法:时钟一直开着
always @(posedge clk) begin
if (enable) begin
data_out <= data_in;
end
end
// 好的写法:使用门控时钟
wire gated_clk = clk & enable;
always @(posedge gated_clk) begin
data_out <= data_in;
end
4.3.2 静态功耗优化
静态功耗主要来自漏电流。随着工艺节点越来越小,漏电功耗占比越来越高。我记得在28nm以下工艺,漏电功耗能占到总功耗的40%以上。
静态功耗优化方法:
- 电源门控:彻底关掉不用的模块的电源
- 多阈值单元:非关键路径用HVT,关键路径用LVT
- 衬底偏置:通过调整衬底电压来控制阈值
- 长沟道器件:在非关键路径使用长沟道器件降低漏电
避坑指南:我曾经在一个项目中,为了降低漏电,把所有非关键路径都换成了HVT单元。结果发现,有些路径虽然时序不紧张,但信号翻转率很高,用HVT反而导致动态功耗增加。所以,功耗优化一定要综合考虑动态和静态。
4.4 综合中的DFT插入
DFT(可测试性设计)插入,说白了就是给芯片装个"体检系统"。没有DFT,芯片生产出来坏了你都不知道坏在哪。
4.4.1 DFT的基本结构
综合阶段最常用的DFT技术是扫描链。我建议你在综合时就规划好扫描链的结构,不要等到后端再做。
| DFT技术 | 功能 | 面积开销 | 测试覆盖率 |
|---|---|---|---|
| 扫描链 | 将寄存器串成链,方便测试 | 5%~10% | 高 |
| BIST | 内建自测试,不需要外部设备 | 10%~20% | 中 |
| 边界扫描 | 测试芯片引脚连接 | 5%~15% | 高 |
| MBIST | 专门测试存储器 | 5%~10% | 高 |
4.4.2 扫描链插入流程
综合阶段做DFT插入,我一般按这个流程来:
- 检查RTL代码:确保所有寄存器都能被扫描
- 定义测试模式:设置测试时钟、测试使能等信号
- 插入扫描链:工具自动把寄存器串成链
- 验证扫描链:跑仿真确认扫描链功能正确
- 生成测试向量:ATPG工具生成测试向量
- 评估覆盖率:检查测试覆盖率是否达标
// DFT插入示例:扫描链配置
// 在综合脚本中设置
set_scan_configuration -chain_count 4
set_scan_configuration -clock_mixing no_mix
set_scan_configuration -create_dedicated_scan_ports true
// 指定扫描链的输入输出
set_scan_path test_chain1 -view spec \
-scan_data_in scan_in_1 \
-scan_data_out scan_out_1
我的经验:扫描链的数量要根据芯片面积和测试时间来决定。链越多,测试时间越短,但需要的测试引脚也越多。我一般控制在4~8条链,这样测试时间和引脚数量都比较平衡。
4.4.3 DFT插入的注意事项
DFT插入看起来简单,但坑不少。我遇到过几次问题,分享给你:
- 异步复位寄存器:要确保复位信号在测试模式下可控
- 时钟门控:测试模式下要旁路掉时钟门控
- 三态总线:测试模式下要避免总线冲突
- 存储器:要单独处理,通常用MBIST
- 模拟模块:数字和模拟的接口要特殊处理
避坑指南:我曾经在一个项目中,忘了处理异步复位寄存器。结果扫描测试时,复位信号不受控,导致测试覆盖率只有60%。后来不得不重新插入扫描链,浪费了两周时间。所以,DFT一定要在综合阶段就规划好。
综合优化不是一蹴而就的事。我做了这么多年,每次项目都会遇到新的挑战。但只要你掌握了时序、面积、功耗和DFT这四个维度的优化方法,大部分问题都能找到解决方案。记住,没有最好的优化,只有最适合的优化。