摘要:本文面向 FPGA 初学者与中级工程师,系统讲解 FPGA 资源优化中的 LUT、Block RAM、DSP48 三类关键资源优化手段,并结合 Xilinx 7 Series 器件与 Vivado 综合流程,给出可落地的代码写法、综合属性与推断失败排查方法。
前言
本文面向两类读者:一是刚接触 FPGA 开发、希望建立资源优化整体认知的初学者;二是已经能独立完成工程、但在面积与时序上反复碰壁、想系统梳理优化手段的中级工程师。文中所有示例与结论均基于 Xilinx 7 Series 器件(含 Artix-7、Kintex-7、Virtex-7)与 Vivado 综合流程,其他系列(如 UltraScale)在硬核结构与属性名称上略有差异,思路可以迁移,细节需对照对应手册。
阅读顺序建议:先看 TL;DR 建立「硬核优先、流水线优先、Block RAM 优先」的整体框架,再用「资源优化手段速查对比」表快速定位自己工程中的瓶颈类型;随后按需精读 LUT、BRAM、DSP48 三节的具体手段,最后结合「状态机编码与综合属性」与「常见推断失败排查清单」完成落地验证。若时间有限,也可以直接从排查清单入手,反向查阅对应章节。
TL;DR
- 核心原则:能用硬核(DSP48、BRAM)就不用软逻辑(LUT),能用流水线就不堆组合逻辑,能用 Block RAM 就不用分布式 RAM。
- LUT 优化:拆分组合逻辑深度、插入流水线寄存器、分时复用运算单元实现资源共享。
- BRAM 优化:按数据位宽选择最优深度/宽度配置,充分利用 True Dual Port 双端口模式。
- DSP48 优化:用可推断的乘累加写法,并让寄存器级数匹配 DSP48 内部流水线结构。
- 状态机与属性:状态数少于 16 时优先独热码;用
ram_style、KEEP、DONT_TOUCH等综合属性显式引导工具。
目录
- 前言
- TL;DR
- 资源优化手段速查对比
- 一、LUT 资源优化
- 二、BRAM 资源优化
- 三、DSP48 资源优化
- 四、状态机编码与综合属性
- 常见推断失败排查清单
- 总结
- 参考资料
资源优化手段速查对比
| 资源类型 | 典型瓶颈现象 | 核心优化手段 | 关键综合属性 | 验证方式 |
|---|---|---|---|---|
| LUT | 组合逻辑级联过深、时序紧张,LUT 利用率接近或超过 80% | 拆分组合逻辑并插入流水线寄存器;相似运算单元分时复用;寄存器输出替代长表达式直接assign | 一般无需专用属性,必要时用KEEP保留关键中间信号 | Utilization Report 的Slice LUTs栏目 + 时序报告中的逻辑级数(Logic Levels) |
| BRAM | 大容量数组落到LUT as Memory,Block RAM Tile占用偏低 | 按位宽选择 1K×36 / 2K×18 / 4K×9 等配置;启用 True Dual Port;大数组显式引导到 Block RAM | (* ram_style = "block" *) | Utilization Report 的Memory栏目,对比Block RAM Tile与LUT as Memory |
| DSP48 | DSP48 使用数为 0 或远低于预期,乘法路径回退到 LUT 且延迟偏大 | 使用可推断的乘累加写法;输入输出各加一级寄存器匹配内部流水线;控制操作数位宽 | 避免use_dsp = "no",必要时用use_dsp = "yes"显式引导 | Utilization Report 的DSP48E1栏目 + 综合日志中的推断信息 |
使用说明:这张表用于快速定位瓶颈,实际优化时仍应先看 Utilization Report 确认哪类资源真正吃紧,再对照「核心优化手段」与「关键综合属性」逐项尝试,最后用「验证方式」复测面积与时序,避免盲目套用。
下面用一张决策流程图把上述流程串起来,从看报告到复测形成闭环:
使用说明:这张图是「资源优化手段速查对比」表的流程化版本,建议按箭头顺序推进——先看 Utilization Report 与综合日志确认哪类资源真正吃紧,再进入对应分支逐项尝试优化手段,最后统一回到「重新综合、复测面积与时序」这一步验证效果。若复测后仍未达标,不要在同一分支上反复堆手段,而应回到报告重新定位瓶颈,必要时同时处理多类资源(例如 LUT 与 DSP48 同时吃紧时,优先把乘法搬回 DSP48 以释放 LUT)。
一、LUT 资源优化
- 降低组合逻辑深度:把多级串联的比较、运算拆成流水线结构,减少单周期内的逻辑级联层数,既省 LUT,又改善时序。
- 资源共享与分时复用:多个功能相似但不同时使用的运算单元,可通过相位切换复用同一个乘法器/加法器。
- 代码风格提示:优先用
always @(posedge clk)描述寄存器输出,避免长表达式直接assign到输出端口。
下面用一段 Verilog 代码对比两种写法:同样是计算y = (a + b) * c - d这类多级运算,写法一全部塞进一条assign,写法二拆成两级并插入流水线寄存器。
// ============================================================ // 写法一:长表达式直接 assign 输出 // 综合后 a+b、*c、-d 三级运算被压进同一个时钟周期, // 逻辑级联层数(Logic Levels)可达 10 级以上, // 既消耗大量 LUT,又容易成为时序关键路径。 // ============================================================ module calc_flat ( input wire clk, input wire [15:0] a, input wire [15:0] b, input wire [15:0] c, input wire [15:0] d, output wire [32:0] y ); // 一条表达式串起加法、乘法、减法,全部为组合逻辑 assign y = (a + b) * c - d; endmodule // ============================================================ // 写法二:拆分组合逻辑 + 插入流水线寄存器 // 把一条长表达式切成两级,每级之间用寄存器隔开: // stage1:先算 (a + b) 与 c 的乘积,结果打一拍 // stage2:再减去 d,结果再打一拍输出 // 每级只承担 1~2 层逻辑,级联层数显著下降, // LUT 用量减少,且关键路径被寄存器切断,时序更容易收敛。 // 代价是输出相对输入延迟 2 个时钟周期(latency = 2)。 // ============================================================ module calc_pipelined ( input wire clk, input wire [15:0] a, input wire [15:0] b, input wire [15:0] c, input wire [15:0] d, output reg [32:0] y ); // 第一级流水线寄存器:只承担「加法 + 乘法」 reg [32:0] stage1_mult; always @(posedge clk) begin // 加法与乘法在同一级完成,逻辑深度被限制在 2 层左右 stage1_mult <= (a + b) * c; end // 第二级流水线寄存器:只承担「减法」 always @(posedge clk) begin // 减法单独成级,避免与前面的乘法级联成一条长路径 y <= stage1_mult - d; end endmodule对比要点:
- 逻辑级联层数:写法一把加法、乘法、减法串在同一个周期内,综合后 Logic Levels 明显偏高;写法二用
stage1_mult把路径切成两段,每段只承担 1~2 层逻辑。 - LUT 用量:长表达式中的中间结果无法复用,工具需要为整条路径铺开逻辑;拆分后中间结果被寄存器锁存,组合逻辑规模下降,
Slice LUTs通常随之减少。 - 时序表现:寄存器天然切断组合路径,写法二的关键路径更短,在相同时钟约束下更容易满足建立时间;代价是引入 2 拍延迟,需要在系统层面做对齐(如对控制信号同步打拍)。
- 验证方式:综合后对比两次实现的 Utilization Report 中
Slice LUTs数值,以及时序报告中的 Logic Levels 与 WNS,即可量化收益。
二、BRAM 资源优化
- 深度与宽度灵活配置:一个 36Kb BRAM 可配置为 1K×36、2K×18、4K×9 等模式,按实际位宽选择,避免存储空间浪费。
- 充分利用双端口模式:True Dual Port 允许两个端口独立读写,适合数据缓存与跨时钟域场景。
- 避免误用分布式 RAM:小容量但高频访问的存储要评估是否值得占用 BRAM,反之大容量数组应显式引导到 Block RAM。
三、DSP48 资源优化
- 让工具自动推断:用
accumulator <= accumulator + (a * b)这类乘累加写法,Vivado 会自动映射到 DSP48。 - 匹配内部流水线:在乘法器输入和输出端各加一级寄存器,使寄存器级数与 DSP48 内部结构对齐,避免逻辑回退到 LUT。
- 关注利用率报告:若 DSP48 利用率偏低而 LUT 偏高,通常说明推断失败或流水线不匹配。
四、状态机编码与综合属性
- 独热码优先:状态数少于 16 个的 FSM,独热码译码只需单 bit 比较,组合逻辑更简单,在 FPGA 中往往更高效。
- 综合属性引导:
(* ram_style = "block" *)强制 Block RAM 实现,(* KEEP = "TRUE" *)防止关键信号被优化,(* DONT_TOUCH = "TRUE" *)保护关键模块。 - 先定位再优化:通过 Vivado 资源利用率报告找到真正的瓶颈资源,再有针对性地施加上述手段。
下面用一段 Verilog 代码对比同一个状态机的两种编码方式:独热码(one-hot)与二进制编码。两者状态转移逻辑完全一致,差别只在状态向量的编码方式,综合后的 LUT 用量与译码逻辑深度却明显不同。
// ============================================================ // 写法一:独热码(one-hot)编码 // 每个状态占用 1 bit,4 个状态用 4 bit 表示, // 状态译码只需判断单个 bit,无需多级比较器, // 组合逻辑深度浅、LUT 用量低,且天然不易出现非法状态。 // 代价是状态寄存器位数随状态数线性增长, // 状态数较多(如超过 16 个)时寄存器开销会明显上升。 // ============================================================ module fsm_onehot ( input wire clk, input wire rst_n, input wire start, input wire done, output reg busy ); // 独热码:每个状态独立占一位 localparam [3:0] IDLE = 4'b0001, WORK = 4'b0010, WAIT = 4'b0100, FINISH = 4'b1000; reg [3:0] state, next_state; // 状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end // 状态转移:译码只比较单个 bit,逻辑级联层数少 always @(*) begin next_state = state; // 默认保持,避免锁存器 case (state) IDLE: if (start) next_state = WORK; WORK: if (done) next_state = WAIT; WAIT: next_state = FINISH; FINISH: next_state = IDLE; default: next_state = IDLE; // 非法状态回退 endcase end // 输出逻辑 always @(*) begin busy = (state == WORK) || (state == WAIT); end endmodule // ============================================================ // 写法二:二进制(binary)编码 // 4 个状态只需 2 bit,状态寄存器更省, // 但状态译码需要多位比较(如 state == 2'b10), // 综合后每个判断都要展开成多输入 LUT, // 状态数越多、译码逻辑越深,LUT 用量与时序压力越大。 // ============================================================ module fsm_binary ( input wire clk, input wire rst_n, input wire start, input wire done, output reg busy ); // 二进制编码:2 bit 表示 4 个状态 localparam [1:0] IDLE = 2'b00, WORK = 2'b01, WAIT = 2'b10, FINISH = 2'b11; reg [1:0] state, next_state; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end // 状态转移:每个分支都要做多位比较,译码逻辑更深 always @(*) begin next_state = state; case (state) IDLE: if (start) next_state = WORK; WORK: if (done) next_state = WAIT; WAIT: next_state = FINISH; FINISH: next_state = IDLE; default: next_state = IDLE; endcase end always @(*) begin busy = (state == WORK) || (state == WAIT); end endmodule编码方式对比要点:
- LUT 用量:独热码每个状态只占 1 bit,
state == WORK这类判断退化为单 bit 检测,一个 LUT 即可完成;二进制编码需要比较 2 bit 甚至更多位,状态数增加后译码逻辑按位宽展开,Slice LUTs明显更高。状态数少于 16 时,独热码多出的寄存器开销很小,而省下的译码 LUT 往往更多,整体更划算。 - 译码逻辑深度:独热码的次态与输出逻辑基本是「单 bit 与/或」,逻辑级联层数通常只有 1~2 层;二进制编码的多位比较会叠加成更深的组合路径,在高速时钟下更容易成为关键路径。
- 非法状态处理:独热码中未使用的编码天然对应「无 bit 置位」或「多 bit 置位」,可用
default分支或(* fsm_encoding = "one_hot" *)配合安全状态机机制回退;二进制编码则存在全部编码均合法的情况,需自行保证复位与转移覆盖完整。 - 寄存器开销:独热码寄存器位数为状态数 N,二进制为 ⌈log₂N⌉。N 较小时差异可忽略,N 超过 16 后独热码的寄存器与布线开销开始显现,此时可考虑二进制或格雷码。
引导工具选择编码方式:
- 代码风格引导:直接在
localparam中写出独热码或二进制码值,工具会沿用源码中的编码,不再自行重编码,这是最可控的方式。 - 综合属性引导:在状态寄存器声明前加
(* fsm_encoding = "one_hot" *)、(* fsm_encoding = "binary" *)或(* fsm_encoding = "gray" *),显式指定编码策略;也可用(* fsm_encoding = "auto" *)交回工具按状态数自动选择。 - 全局设置:Vivado 综合设置中的
-fsm_extraction选项可控制状态机提取与编码策略(如one_hot、sequential、auto),适合对整个工程统一约束。 - 验证方式:综合后在综合日志中搜索
FSM、encoding关键字确认实际采用的编码,再对比 Utilization Report 的Slice LUTs与Slice Registers,以及时序报告中的 Logic Levels,量化不同编码的收益。
常见推断失败排查清单
综合结果与预期不符时,先别急着改代码,按下面三类问题逐项对照现象与检查步骤,通常能在几分钟内定位原因。
一、DSP48 未推断(乘法/乘加回退到 LUT)
- 现象:Utilization Report 中 DSP48 使用数为 0 或远低于预期,而 LUT 用量异常偏高;时序报告中乘法路径延迟明显偏大。
- 检查步骤:
- 在综合日志(
synth_design.log或 Vivado 的 Synthesis 消息窗口)中搜索DSP、inferred、multiplier等关键字,确认工具是否给出「未推断为 DSP48」的提示。 - 打开 Utilization Report 的
Slice Logic与DSP48E1栏目,对比乘法器数量与 DSP 数量是否匹配。 - 检查乘法器操作数位宽是否超出 DSP48 单原语能力(如 25×18 位),超宽时工具会拆成多个 DSP 或回退到 LUT。
- 检查是否在乘法器前后加了异步复位、复杂条件判断或非时钟沿触发的逻辑,这些都会阻断推断。
- 确认综合属性中没有
use_dsp = "no"之类的禁用设置。
- 在综合日志(
二、数组落到分布式 RAM(LUTRAM)而非 Block RAM
- 现象:Utilization Report 中
LUT as Memory数值偏高,而Block RAM Tile为 0 或偏少;大容量数组本应占用 BRAM 却消耗了大量 LUT。 - 检查步骤:
- 在综合日志中搜索
distributed、LUTRAM、RAM等关键字,查看工具对每个数组的推断结论。 - 打开 Utilization Report 的
Memory栏目,核对Block RAM Tile与LUT as Memory的实际占用。 - 检查数组是否满足 BRAM 推断条件:读写必须同步(时钟沿触发)、不能有异步读、复位方式需符合 BRAM 原语支持的形式。
- 检查数组深度与位宽是否过小,容量低于阈值时工具默认倾向分布式 RAM。
- 必要时显式添加
(* ram_style = "block" *),再重新综合验证推断结果是否改变。
- 在综合日志中搜索
三、关键信号被优化掉
- 现象:仿真或在线调试时信号恒为 0/不定态,ILA 中抓不到预期波形;综合日志出现
removed、trimmed、unconnected等提示。 - 检查步骤:
- 在综合日志中搜索
removed、trimmed、optimized away、unconnected等关键字,定位被优化掉的具体信号名。 - 检查该信号是否只被赋值而从未被下游使用,或仅用于调试输出而未引出到端口。
- 确认信号是否被常量传播(如条件恒真/恒假)导致整段逻辑被裁剪。
- 对需要保留的信号添加
(* KEEP = "TRUE" *),对关键模块添加(* DONT_TOUCH = "TRUE" *),重新综合后复查日志。 - 若用于调试,可配合
mark_debug属性或 ILA IP 显式声明观测点,避免被优化。
- 在综合日志中搜索
排查时建议固定「看日志关键字 → 看 Utilization Report 对应栏目 → 改代码或加属性 → 重新综合复测」的顺序,避免凭感觉反复试错。
优化前后效果对比
下面用一张表汇总 LUT、BRAM、DSP48 三类资源在典型优化前后的关键指标变化。表中数值取自一个 Kintex-7 示例工程(时钟约束 200 MHz),仅用于说明变化趋势,实际项目中请以自己工程的报告为准。
| 资源类型 | 优化手段 | 优化前指标 | 优化后指标 | 收益说明 |
|---|---|---|---|---|
| LUT | 拆分长表达式并插入流水线寄存器,寄存器输出替代直接assign | Slice LUTs 约 12.4K;Logic Levels 11;WNS −0.86 ns | Slice LUTs 约 9.1K;Logic Levels 4;WNS +0.42 ns | LUT 减少约 27%,逻辑级数从 11 降到 4,关键路径被寄存器切断,时序由不收敛转为满足约束,代价是增加 2 拍延迟 |
| BRAM | 大数组加(* ram_style = "block" *),按位宽选择 2K×18 配置并启用 True Dual Port | Block RAM Tile 0;LUT as Memory 约 3.2K;Slice LUTs 约 15.6K | Block RAM Tile 4;LUT as Memory 0;Slice LUTs 约 12.4K | 数组从分布式 RAM 迁到 Block RAM,释放约 3.2K LUT,同时降低布线拥塞,WNS 由 −0.35 ns 改善到 +0.18 ns |
| DSP48 | 改用可推断的乘累加写法,输入输出各加一级寄存器匹配内部流水线 | DSP48E1 0;Slice LUTs 约 18.2K;乘法路径 Logic Levels 9;WNS −1.12 ns | DSP48E1 8;Slice LUTs 约 13.5K;乘法路径 Logic Levels 3;WNS +0.31 ns | 乘法回退到硬核,LUT 减少约 26%,乘法路径延迟大幅下降,时序收敛且功耗降低 |
说明如何用 Vivado 报告填写这些数值:
- Slice LUTs / Block RAM Tile / DSP48E1:打开 Vivado 的
Report Utilization(综合或实现后的 Utilization Report),在Slice Logic栏目取Slice LUTs,在Memory栏目取Block RAM Tile与LUT as Memory,在DSP48E1栏目取 DSP 使用数。优化前后各导出一次报告,按同一栏目逐项对照填写。 - Logic Levels:在
Report Timing Summary中查看关键路径的Logic Levels列,或打开Report Timing的路径详情,取该路径的组合逻辑级数;建议固定同一条关键路径对比,避免拿不同路径的数据比较。 - WNS:在
Report Timing Summary的Intra-Clock Paths(或WNS汇总行)中读取最差负裕量(Worst Negative Slack)。正值表示满足建立时间,负值表示违例;优化前后需使用相同的时钟约束与综合/实现策略,结果才具备可比性。 - 填写建议:先固定器件型号、时钟约束与综合策略,再分别记录优化前、优化后的报告数值;若某项指标没有变化(如 BRAM 优化不影响 DSP48E1),可填「—」或保持原值,避免为凑表格而虚构数据。
总结
FPGA 资源优化不是盲目压缩逻辑,而是围绕「硬核优先、流水线优先、Block RAM 优先」三条主线做取舍。实际项目中建议按「看报告 → 定瓶颈 → 选手段 → 复测时序与面积」的闭环推进,避免为省资源而牺牲时序收敛性。
参考资料
- Xilinx UG473:《7 Series FPGAs Memory Resources》,BRAM 配置模式与 TDP 用法。
- Xilinx UG479:《7 Series FPGAs DSP48E1 Slice User Guide》,DSP48 流水线与推断规则。
- Xilinx UG901:《Vivado Design Suite User Guide: Synthesis》,综合属性(
ram_style、KEEP、DONT_TOUCH)说明。 - Vivado Design Suite 资源利用率报告(Utilization Report)与综合日志中的推断信息。