KEEP、DONT_TOUCH、MARK_DEBUG,到底差在哪?

由 judy 提交于

文章来源:FPGA入门到精通

本文收获

✅ 分得清三个属性各押什么,代价落在谁头上

✅ 手里有一张四类机制与三个属性的交叉判定表

✅ 会顺着数据流反查,找回改了名的信号

综合跑过了,时序也过了,打开 Synthesized Design 想看一眼状态机信号。get_nets -hierarchical -filter {NAME =~ next_state} 敲了三遍,返回空。

回头翻 RTL,(* mark_debug = "true" *) 就写在 next_state 定义的前一行,一字不差。属性没写错。

社区里的答复,十有八九是「再加个 dont_touch」。加了,还是找不到,于是结论往「Vivado 有 bug」那一侧滑。跟 bug 没关系。信号找不回来有三种完全不同的原因,属性只对其中一部分有用,而且每一种都要拿别的东西去换。

一、三个属性各管一层

先看它们各自保护什么,以及押掉什么。

1KEEP

保护的是逻辑网络,不让它被重命名、被合并。生效在综合阶段。

它不管三件事:不阻止常量传播,不阻止资源吸收,也不强制布局布线保留。

押掉的是面积。带 KEEP 的信号会顶掉 BRAM 推断和扇出复制。

2DONT_TOUCH

保护的是 Cell 和 Net,不让它们被优化、复制、重定时。综合和实现两个阶段都管。

它也有做不到的:网络本身消失之后无处可挂,跨层级时可能被忽略。

押掉的是时序。关键路径搬不动。

3MARK_DEBUG

它只是告诉工具,这个网络要调试。RTL 或综合后网表上都能写。

它不是约束,只是个提示。

所以它什么都不押。也正因为不押什么,经常不生效。

谁押了什么,才是分水岭。讲属性的文章大都在讲「加什么」,很少讲「加了以后你拿什么去换」。

UG901 的属性章节里有两条 CAUTION 值得贴在显示器边上。一条说 KEEP 用在「RTL 后续不再使用」的信号上,综合会保留它,但它不驱动任何东西,后面的流程容易出问题。另一条说 KEEP 和其他属性冲突时,通常 KEEP 优先。

还有一条 RECOMMENDED 常被跳过。KEEP 要写在 RTL 里,不要写进 XDC。需要保留的信号,往往在 XDC 被读取之前就已经被优化掉了。

二、三种反噬

代价不是抽象说法,落到具体三处。

1BRAM 推断失败

设计里设了 RAM_STYLE = "block",那组本该被推断成 block RAM 的寄存器,只要带了 KEEP,推断直接不做。为了留住一个调试信号,一整块存储退成分布式 RAM,LUT 占用从三位数跳到四位数。

漏出来的地方是综合日志里 block RAM 那一栏:它是 0,而你明明有一组深度 64 的查找表。

2扇出复制被拦

上游信号带 MAX_FANOUT,工具本来会给它复制几份去驱动不同区域。下游某个信号带了 KEEP,复制就不发生。工具乖乖保留了那个网络,代价是这条高扇出路径的时序没了。

漏出来的地方是时序报告:某条高扇出路径的 driver 只剩一个,不再有复制出来的那几份。

3重定时被禁

DONT_TOUCH 挂在触发器上,寄存器重定时想把这级逻辑搬到路径的另一头,搬不动。关键路径该优化的地方全都优化不了。

漏出来的地方是综合报告的重定时那一节:盯着的那几级触发器不见了。

反噬不会当场报错,只能从上面这三个地方漏出来。看到这三样,先回头翻属性,别急着怀疑工具。

于是问题反过来问自己一句。为了抓一个信号,你愿意付多少面积、多少时序?答不上来的时候,多半是不该加。

三、四类机制 × 三个属性

信号消失有四类机制,属性对它们的作用差得很远。

1常量传播

次态逻辑折进现态寄存器的 D 端,整条网络消失。KEEP 部分有效,DONT_TOUCH 部分有效,MARK_DEBUG 无效。

2资源吸收

信号被吞进 BRAM / DSP 内部。KEEP 有效,但会把推断顶掉;DONT_TOUCH 有效;MARK_DEBUG 无效。

3层级打平

默认 rebuilt 把模块打平,名字全变。KEEP 部分有效,DONT_TOUCH 部分有效,MARK_DEBUG 无效。

4总线拆分

32 位总线只保住其中 12 位。KEEP 无效,DONT_TOUCH 无效,MARK_DEBUG 部分有效。

常量传播,就是工具把这个信号算成了常数,直接代进去了,网络这一层上它已经不存在,属性没有地方可挂。FSM 的 next_state 加了 mark_debug,综合后照样消失,原因就在这里。不是属性没生效,是次态组合逻辑被折进了现态寄存器的 D 端。

这里还有个反直觉的细节。MARK_DEBUG 加在某个 bit_vector 的某一位上,整条总线都会拿到这个属性。碰上宽总线被逐位优化的情况,你标了一位,工具以为你要整条,最后留在网表里的可能还是 12 位。

层级打平这一类还有个常见变体。属性写在子模块内部,父层综合时把子模块打平,这个属性有可能跟着层次一起被忽略。要跨层级保住它,就得在上一层把 scope 写全,或者干脆给整条层次链挂 dont_touch。路径少写一级,等于没加。

这张对照的用法是先判机制,再选属性。看到信号没了,先问它属于哪一类,别一上来就加属性。加错层,等于什么都没加,还白付一笔代价。

四、八步排查

顺序有讲究,前四步在动属性之前。

1属性写在 RTL 里了吗

写在 XDC 往往已经太晚。XDC 被读取之前,需要保留的信号可能已经被优化掉了。

2是不是只加了 mark_debug

它是提示级,缺的可能是 dont_touch。

3这个信号驱动了别的东西吗

没有的话,大概率被常量传播吃掉。

4是不是宽总线

要保护就逐位或分段保护,别指望一个属性管住 32 位。

5-flatten_hierarchy 是不是默认的 rebuilt

调试期临时改成 none,原始层次名才能在网表里找到。代价是资源和 QoR 变差,调完必须改回去。

6模糊搜名字

set nets [get_nets -hierarchical -filter {NAME =~ next_state}]

puts "Nets: $nets"

找到了但名字变了,比如变成 my_fsm_i/next_state_reg,直接给它挂属性就完事。

7搜不到,就顺着它的负载反查

已知 next_state 驱动 current_state 的 D 端,那就从 D 端往回找:

set cur_cells [get_cells -hierarchical -filter {NAME =~ current_state_reg}]

set driven_net [get_nets -of_object \

  [get_pins -of_object $cur_cells -filter {REF_PIN_NAME == D}]]

set_property MARK_DEBUG true $driven_net

那个 driven_net 就是优化后和原始信号功能等价的网络。名字换了,功能还在。

8全都失败,跑完实现再在网表里找

用 Set up Debug 搜 current_state_reg 的 D 引脚,或者直接找代表状态转移逻辑的 LUT 输出。

第七步是整篇的枢纽,它成立只靠一句话。优化器保证功能等价,不保证名字等价。 按名字找一定会输,按数据流找才收敛。

五、后仿真那个坑

MARK_DEBUG 只对硬件在线调试有效。后仿波形里看不到它标记的信号,仿真网表里没有调试逻辑,名字也早被优化。

想让它出现在后仿波形里,得用 KEEP 或 DONT_TOUCH 强制保留,再用功能仿真模式导出网表:

write_verilog -mode funcsim <netlist>.v

导出的网表和器件库、testbench 一起编译,用 get_nets 确认真实名字,再加进波形窗口。

这两个需求长得像,用途完全不同。要在线抓现场,mark_debug 才有用;要在仿真里看波形,只有 KEEP 那条路。

还有一条顺序上的纪律容易被忽略。先把 ILA 探针 arm 上,再让被测逻辑重新跑一遍。触发条件必须在故障第一次出现之前就等在那里,事后补探针,抓不到第一次。

六、最后

回到开头那个 next_state。它没消失,只是改了名字、换了位置,被折进了别人的 D 端。

属性是对优化器提要求,数据流才是能复算的东西。

三个属性记成一句话就够了。KEEP 押面积,DONT_TOUCH 押时序,MARK_DEBUG 只押一个提示位。想清楚这一笔押在哪,再决定加哪个。