ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA原语实战:IODELAY、ODDR、BUFGMUX与BRAM

FPGA原语实战:IODELAY、ODDR、BUFGMUX与BRAM 1. 需求拆解为什么这四个原语总被凑到一起FPGA原语这东西属于那种“知道了一辈子用不上不知道就卡一整天”的存在。前几天帮同事看他那块板子的接口时序报告里一堆 IDELAYE2 的 tap 值对不齐转头又发现他的 BUFGMUX 选择信号是直接接的组合逻辑再往下翻BRAM 的输出寄存器压根没开ODDR 反倒是写对了。这四个东西——IODELAY、ODDR、BUFGMUX 和 VIVADO BRAM——基本覆盖了一线 FPGA 工程里最常见的四类需求引脚时序调整、随路时钟输出、时钟源切换、片内数据缓存。这篇就把我在实际项目里验证过的写法、踩过的坑整理一遍。代码可以拿来直接用参数是怎么算出来的我也会把过程写清楚不会只丢一个结论。不管你是刚上手 7 系列的新人还是做了几年但一直靠 IP 核点鼠标过日子的老手看完应该都能捞到点东西。整篇围绕 Xilinx 7 系列Artix-7、Kintex-7、Zynq-7000 这几类器件都适用展开部分结论对 UltraScale 也成立但有差异的地方我会单独点出来。1.1 场景还原一套源同步接口到底需要什么想象一个很典型的活儿FPGA 通过一组并行走线连到外部器件比如 ADC、DAC、或者一颗通信 PHY。数据是源同步的也就是对方会把数据和时钟一起发过来或者我方要把数据和时钟一起送出去。这里头有几件事必须解决。接收方向上对方发来的数据和随路时钟之间因为 PCB 走线长度差异、器件输出偏斜、温度漂移到达 FPGA 引脚时已经不是你想要的“时钟居中、数据稳定”的理想关系了。这时候你就需要一个能在 ps 级别微调输入延迟的旋钮这就是IODELAY存在的理由。发送方向上FPGA 内部的所有时钟都是单沿工作的都在上升沿翻转但对外很多时候要送出一路时钟而且这路时钟和数据的相位关系必须严格可控。你要是直接用内部时钟拉一根线到引脚Vivado 会给你一堆报警抖动和偏斜也没法控制。正确做法是用ODDR把时钟“重造”一遍再送出去。还有些场景系统里有主备两路时钟源或者需要在工作时钟和测试时钟之间切换。直接写assign clk sel ? clk1 : clk0;就是在给自己埋雷组合逻辑切换时钟必然产生毛刺下游触发器可能直接进入亚稳态。这种活儿得交给BUFGMUX它是专门的无毛刺时钟切换单元。最后数据进来出去总得有个地方存跨时钟域缓冲、帧缓存、系数表、查找表这些都落在片上 RAM 上。7 系列每块 BRAM 是 36Kb用好了能跑很高频率用不好就是各种时序违例和读写冲突。VIVADO BRAM的配置项看着多其实每一栏背后都有明确的取舍逻辑。1.2 四种原语的定位与分工先上一张表把四者的角色理清楚后面每一节再展开。原语物理位置解决的核心痛点最典型的误用IODELAYE2 / ODELAYE2I/O Bank 内的 ILOGIC/OLOGIC引脚级 ps 精度延迟调整忘了例化 IDELAYCTRLtap 值不生效ODDROLOGIC 内单沿信号转 DDR 输出输出随路时钟用普通逻辑直接输出时钟到引脚BUFGMUX全局时钟网络入口两路时钟无毛刺切换选择信号不做同步切换时刻不确定BRAMRAMB36E1片内专用存储列大容量、高带宽片上缓存写模式选错导致读出数据不可预测这四者在物理上其实是分开的IODELAY 和 ODDR 在 I/O Bank 的 I/O 逻辑单元里BUFGMUX 在全局时钟资源里BRAM 在器件中部的专用存储列里。但因为一个完整的接口工程往往四样都要用所以它们在代码里经常一起出现调试时也常常互相牵连。比如说你的 IDELAY 校准没做好时序对齐不准你会怀疑 BRAM 的读写延迟算错了其实问题根本不在 BRAM。我的建议是写代码时把这四类原语分文件、分模块放例化时把关键参数全部显式写出来不要依赖默认值。下面一个一个说。2. IODELAY引脚时序的微调旋钮2.1 tap 的物理含义与延迟计算IDELAYE2 的内部是一串延迟单元每个单元的延迟量由一个参考时钟校准。你通过 tap 值0 到 31来选择接在哪一级。很多人上来就问“一个 tap 多少 ps”其实这个值不是固定常量它跟参考时钟频率绑定。具体关系是单个 tap 的延迟 ≈ 1 / (32 × REFCLK 频率)。这个公式的来由是IDELAYCTRL 用一个参考时钟来锁定整个延迟链让 32 个 tap 的总延迟大致等于一个参考时钟周期。所以REFCLK 取 200 MHz 时一个 tap 约 78 ps整个链约 2.4 nsREFCLK 取 300 MHz 时一个 tap 约 52 ps整链约 1.6 nsREFCLK 取 400 MHz 时一个 tap 约 39 ps整链约 1.2 ns我自己做 DDR 类接口时习惯把 REFCLK 定在 200 MHz原因有两个一是 200 MHz 是个整数频率容易从系统时钟用 MMCM 分出来二是 tap 粒度 78 ps 对常见接口已经够用了太细反而不好控制。这里有个必须强调的点REFCLK_FREQUENCY 这个属性必须和你实际喂给 IDELAYCTRL 的时钟频率完全一致。我见过有人实际给 200 MHz属性里写 300.0结果延迟算出来和预想差了一倍查了半天布线报告才发现是这个参数的问题。Vivado 不会因为你写错就报错它只是默默按你给的频率算延迟然后你的采样点就跑偏了。2.2 IDELAYCTRL最容易被忽略的那个必须例化的模块新手最容易犯的错就是代码里例化了 IDELAYE2但忘了 IDELAYCTRL。结果是什么综合能过实现能过甚至时序报告看着还挺正常但实际跑起来延迟完全不可控或者干脆就是错的。IDELAYCTRL 的作用是持续校准延迟链抵消 PVT工艺、电压、温度漂移。它需要两样东西一个干净的参考时钟一个复位信号。输出一个 RDY 信号告诉你校准完成了。在 RDY 拉高之前所有 IDELAYE2 的延迟值都是不可信的这一点在复位逻辑里一定要处理不能让数据通路在校准完成前就开始工作。还有一个分布问题。7 系列里每一个 clock region 需要一个独立的 IDELAYCTRL。如果你用的 IDELAYE2 分布在多个 clock region 里比如两个不同的 I/O Bank那就得例化多个 IDELAYCTRL一个 region 配一个。Vivado 不会自动帮你补缺了会报 “IDELAYCTRL not found for IDELAYE2” 之类的错误或者在实现阶段给出警告。2.3 实操一个可扫描的延迟控制模块实际工程里我更倾向于把 IDELAYE2 配成 VAR_LOAD 模式而不是 VARIABLE 模式。原因是 VAR_LOAD 可以直接用 CNTVALUEIN 端口一次性写入目标 tap 值不用靠 INC/CE 一个 tap 一个 tap 地挪做延迟扫描时逻辑简单很多。下面是我常用的一个输入延迟模块DELAY_SRC 设为 IDATAIN表示延迟对象是来自 IOB 的输入数据。IDELAYE2 #( .IDELAY_TYPE (VAR_LOAD), .IDELAY_VALUE (5d0), .DELAY_SRC (IDATAIN), .REFCLK_FREQUENCY (200.0), .HIGH_PERFORMANCE_MODE(TRUE), .SIGNAL_PATTERN (DATA), .PIPE_SEL (FALSE), .CINVCTRL_SEL (FALSE) ) u_idelay ( .CNTVALUEOUT (cnt_value_out), .DATAOUT (data_delayed), .C (clk_div), // 用于 LD/CE 的控制时钟 .CE (1b0), .CINVCTRL (1b0), .CNTVALUEIN (tap_value), // 目标 tap 值, 0~31 .DATAIN (1b0), .IDATAIN (data_from_ibuf), .INC (1b0), .LD (tap_load), // 拉高一拍即加载 CNTVALUEIN .LDPIPEEN (1b0), .REGRST (idelay_rst) );对应的 IDELAYCTRL 单独例化一份IDELAYCTRL u_idelayctrl ( .RDY (idelay_rdy), .REFCLK (refclk_200m), .RST (idelay_rst) );要做延迟扫描找眼图中心就写个简单的状态机复位释放、等idelay_rdy拉高然后从 0 到 31 逐个 tap 写入每个 tap 停留足够长时间比如几微秒让信号稳定同时把误码率或采样结果记录下来同步给上位机。扫完一轮你会得到一条“浴盆曲线”中间误码最低的区域就是最佳采样窗口取它的中心点作为工作 tap 值。提示LD信号只需要在 clk_div 域里拉高一个周期就够了千万别让它一直挂着高电平否则 tap 值会被反复加载延迟链不断被扰动。一个细节HIGH_PERFORMANCE_MODE设为 TRUE 时延迟链的抖动更小、功耗更高适合处理时钟类信号处理普通数据时设 FALSE 更省电抖动稍大但对数据眼图影响有限。这个取舍看你的接口裕量裕量紧就上 TRUE。3. ODDR把时钟和数据一起送出去3.1 为什么内部时钟不能直接拉到引脚很多新人第一次要把时钟送到片外时的写法是assign clk_out clk_int;然后顶层的 clk_out 接一个 output 引脚。这段代码在综合阶段就会给你一堆警告因为 FPGA 内部的时钟网络和普通 I/O 走线是两套完全不同的资源。时钟信号直接走普通布线资源会带来几个问题走线延迟不确定、抖动大幅增加、到各个引脚之间的偏斜没法控制最要命的是 Vivado 的时序分析根本不知道你送出的是什么你的set_output_delay约束就变成了空中楼阁。正确做法是用 ODDR 输出时钟。ODDR 是 OLOGIC 里的 DDR 输出触发器它有两个数据输入 D1 和 D2分别在时钟的上升沿和下降沿被采样合成到输出的 Q 上。你只要把 D1 固定接 1、D2 固定接 0再给它一个内部分频到目标频率的时钟输出就是一路占空比接近 50% 的干净时钟而且这路时钟和数据经过同样的 OLOGIC 资源相位关系天然可控。3.2 OPPOSITE_EDGE 和 SAME_EDGE 该选哪个ODDR 有个关键属性DDR_CLK_EDGE取值是 OPPOSITE_EDGE 或 SAME_EDGE这个选择直接影响时序收敛难度。OPPOSITE_EDGE 是默认行为D1 在时钟上升沿被捕获并送到输出的前半周期D2 在下降沿被捕获送到后半周期。问题在于D2 需要在下降沿之前就由组合逻辑准备好等于说你给 D2 的路径只有半个时钟周期的裕量而且路径的起点和终点跨越了不同的时钟沿时序分析起来很别扭。SAME_EDGE 则是两个数据都在时钟上升沿被捕获然后由内部电路分配到输出的两个半周期。所有时序路径都归到上升沿路径统一工具分析清晰收敛起来轻松很多。只要没有特殊理由选 SAME_EDGE。我做 DDR 接口时踩过一个坑一开始用了默认的 OPPOSITE_EDGE综合过的实现阶段时序差一点点怎么调约束都收不紧。换成 SAME_EDGE 之后那几条关键路径的裕量直接多出来几百 ps问题就没了。这个改动成本几乎为零收益却很明显。3.3 完整代码输出一路随路时钟下面这段是我在多个项目里用过的输出时钟写法。假设系统有一个 200 MHz 的内部时钟clk_int现在要对外输出一路 200 MHz 的随路时钟。ODDR #( .DDR_CLK_EDGE (SAME_EDGE), .INIT (1b0), .SRTYPE (SYNC) ) u_oddr_clk ( .Q (clk_fwd), .C (clk_int), .CE (1b1), .D1 (1b1), .D2 (1b0), .R (1b0), .S (1b0) ); OBUF u_obuf_clk ( .O (clk_out_pin), .I (clk_fwd) );这里有几个要点。第一ODDR 的 C 端口必须由时钟资源驱动也就是clk_int要来自 BUFG、MMCM 或者 BUFIO 的输出。如果你从普通逻辑分频出来一根线直接接上去Vivado 会报CLOCK_DEDICATED_ROUTE错误。这个错误看着吓人其实就是告诉你时钟走错路了。第二ODDR 输出之后我手动例化了 OBUF。虽然 Vivado 在多数情况下能自动推断出 OBUF但显式写出来更稳特别是当你后面还要加set_output_delay或create_generated_clock约束时电路结构清清楚楚不会出现工具推断和你预期不一致的情况。第三输出的时钟需要独立的时序约束一般这么写create_clock -name sys_clk -period 5.000 [get_ports sys_clk_p] create_generated_clock -name fwd_clk \ -source [get_pins u_oddr_clk/C] \ -divide_by 1 \ [get_ports clk_out_pin]-source指向 ODDR 的 C 引脚这样工具才知道这路输出时钟是从哪个内部节点派生出来的后面基于fwd_clk写set_output_delay才有意义。3.4 ODDR 和 OSERDES 的关系有人会问ODDR 和 OSERDESE2 有什么区别。简单说ODDR 是 2:1 的串化器只能把两路数据合成一路 DDR 输出OSERDESE2 能做到 4:1、8:1 甚至更高用于高速接口。如果你只是要输出一路随路时钟或者做 2:1 的 DDR 数据输出ODDR 就够代码简单、资源占用少。真要上到 4:1 以上才需要请出 OSERDESE2那时候的约束和布局复杂度是另一个量级。另外提醒一句SRTYPE属性建议设为 SYNC让 ODDR 内部的置位复位同步化。有些工程用异步复位在高频下会引入额外的时序不确定性。4. BUFGMUX时钟切换不求人4.1 切换原理与“无毛刺”的含义BUFGMUX 有四个端口I0、I1 是两路输入时钟S 是选择信号O 是输出。当 S 为 0 时输出 I0S 为 1 时输出 I1。它的价值在于切换过程中不会产生窄脉冲——也就是毛刺。为什么普通组合逻辑切换时钟会有毛刺因为sel ? clk1 : clk0这个表达式的输出完全跟随 sel 的变化而 sel 是异步的。如果 sel 恰好在 clk0 的高电平期间翻转输出就会从高电平被硬生生切断产生一个宽度不确定的窄脉冲。下游的触发器遇到这种脉冲可能采到半个周期的异常值也可能在高频电路里直接进入亚稳态。BUFGMUX 内部的机制是它不会在时钟的高电平期间切断当前时钟而是等到一个安全窗口再完成切换。这个窗口保证输出不会产生窄脉冲。这也是为什么我一直强调时钟切换必须用 BUFGMUX不能自己写逻辑。4.2 选择信号为什么要同步虽然 BUFGMUX 本身不会产生毛刺但 S 信号如果直接接异步逻辑切换的时刻是完全不确定的。这在工程上会带来麻烦你不知道切换发生在哪个周期下游逻辑无法预判状态机的行为可能变得不可复现。标准做法是把选择信号分别同步到两个时钟域用两级触发器打拍只有当两个域都看到一致的选择值时才真正驱动 BUFGMUX 的 S 端。这样切换点就落在了一个可预期的范围内。下面是我常用的安全切换模块module clk_mux_safe ( input wire clk0, input wire clk1, input wire sel_raw, // 异步选择, 1 表示选 clk1 output wire clk_out ); (* ASYNC_REG TRUE *) reg sel_s0_a, sel_s0_b; (* ASYNC_REG TRUE *) reg sel_s1_a, sel_s1_b; always (posedge clk0) begin sel_s0_a sel_raw; sel_s0_b sel_s0_a; end always (posedge clk1) begin sel_s1_a sel_raw; sel_s1_b sel_s1_a; end // 两个域都确认选择一致时才切换 wire sel_safe sel_s0_b sel_s1_b; BUFGMUX u_bufgmux ( .O (clk_out), .I0 (clk0), .I1 (clk1), .S (sel_safe) ); endmoduleASYNC_REG TRUE这个属性是给工具看的告诉它这两个触发器是跨时钟域同步用的布局时会尽量放近一点减少亚稳态风险。别小看这个属性不加的话工具可能把它们撒得老远MTBF 会明显下降。4.3 实际工程里还要考虑什么上面那段是简化版实际项目里我还会加两个条件。一是目标时钟必须已经稳定。比如你要从内部时钟切到 MMCM 输出的时钟那必须等 MMCM 的 locked 信号拉高之后再允许切换。切换到一个还没锁定的时钟上输出可能直接死掉。二是切换要防止抖动。如果 sel_raw 来自软件寄存器可能会出现连续翻转或者按键抖动最好在 sel_raw 后面加一个简单的去抖或使能逻辑确保一次只执行一次切换。关于 BUFGMUX 和 BUFGMUX_CTRL 的区别UG472 里说得很清楚两者都是无毛刺切换单元端口完全一致差异只在内部切换时机的处理细节上。日常工程里用 BUFGMUX 就够了BUFGMUX_CTRL 在早期器件或某些特殊场景里才会用到不建议新手去纠结这个细枝末节。注意BUFGMUX 只能放在全局时钟资源的入口处它的输出驱动的是全局时钟网络。如果你只是想在局部切换一根普通信号不要用 BUFGMUX用 LUT 加同步逻辑处理就行。5. VIVADO BRAM从 IP 配置到手工例化5.1 三种写模式到底差在哪BRAM 的三种写模式是新手最容易选错的地方因为它们的行为差异只在“写操作和读操作撞在同一时刻”时才体现出来平时测试看不出来一上量就出问题。写模式写操作期间读端口输出什么我一般什么时候用READ_FIRST保持写之前的旧数据默认首选行为可预测WRITE_FIRST输出刚写入的新数据需要写后立即读的场景NO_CHANGE输出保持不变省功耗实际少用READ_FIRST 之所以是默认首选是因为它的输出对写操作不敏感读数据的时序模型简单容易做流水线对齐。WRITE_FIRST 虽然少一拍延迟但要求写地址和读地址在行为上有明确的耦合关系一旦你的读逻辑和写逻辑来自不同的时钟域或者不同的状态机就很容易读出错误数据。我记得有个项目同事用了 WRITE_FIRST在仿真里看起来一切正常因为仿真的读写顺序是确定的。上板之后数据偶尔错一拍查了很久才发现是读写地址在某个时刻发生了重叠WRITE_FIRST 直接返回了新写入的值而他的下游逻辑假设的是旧值。改成 READ_FIRST 之后问题消失。这种问题仿真跑一万次都不一定复现只有真实时序下地址碰撞才会暴露。5.2 输出寄存器与读延迟BRAM 的读延迟是可以配置的。不带输出寄存器时从地址被捕获到数据出现在 dout 上是 1 个时钟周期开启输出寄存器Optional Output Register之后变成 2 个时钟周期。多出来这一拍换来的是更好的时序裕量和更高的可达频率。7 系列的 BRAM 在 -2 速度等级下不加输出寄存器大概能跑到 400 MHz 出头加上之后能稳定跑到 500 MHz 以上。所以如果你的设计时钟超过 350 MHz我强烈建议开启输出寄存器。关键是要把这一拍的延迟算进你的流水线里。很多人在写读逻辑时只按 1 拍延迟设计改了输出寄存器之后忘了改控制路径结果读出的数据和控制信号错位一格。我的做法是把读地址、读使能、以及所有跟读数据相关的标记位都用同样级数的寄存器打一遍保证它们和 dout 严格对齐。// 读通路三级对齐, 匹配 BRAM 2 拍读延迟 reg [ADDR_W-1:0] raddr_d1, raddr_d2; reg ren_d1, ren_d2; always (posedge clk) begin raddr_d1 raddr; raddr_d2 raddr_d1; ren_d1 ren; ren_d2 ren_d1; end // BRAM 输出 dout 与 ren_d2 / raddr_d2 同拍有效这段代码看着啰嗦但它是保证数据和控制不错位的关键。宁可多写几行也不要事后拿波形一帧一帧地对。5.3 COE 初始化和 xpm_memory 的取舍BRAM 的初始化最简单的方式是用 COE 文件配合 IP 核。COE 的格式是固定的memory_initialization_radix16; memory_initialization_vector 00,01,02,03,04,05,06,07, 08,09,0a,0b,0c,0d,0e,0f;radix指定进制可以是 2、10、16。vector后面跟着用逗号或空格分隔的数据最后用分号结束。文件里的数据个数必须和 BRAM 深度一致少一个工具都会报错。这个格式看着简单但逗号、分号、换行这些细节最容易被手写错我现在的习惯是用脚本生成不手敲。另一个选择是直接在 Verilog 里例化 XPM 宏比如xpm_memory_sdpram。它的好处是跨器件系列可移植代码里看得见配置不用依赖.xci文件。缺点是参数名比较长第一次用需要对着模板抄。大致长这样xpm_memory_sdpram #( .MEMORY_SIZE (32768), .MEMORY_PRIMITIVE (block), .CLOCKING_MODE (common_clock), .ECC_MODE (no_ecc), .WRITE_DATA_WIDTH_A (32), .READ_DATA_WIDTH_B (32), .ADDR_WIDTH_A (10), .ADDR_WIDTH_B (10), .READ_LATENCY_B (2), .WRITE_MODE_B (read_first) ) u_bram ( .doutb (rd_data), .addra (wr_addr), .dina (wr_data), .ena (wr_en), .wea (wr_we), .clka (clk), .addrb (rd_addr), .enb (rd_en), .clkb (clk), .rstb (1b0), .regceb (1b1), .sleep (1b0) );READ_LATENCY_B设 2 就对应输出寄存器开启设 1 就是不开。这个参数和前面说的延迟对齐是同一回事一定要和你的控制逻辑匹配。具体参数名以 Vivado 里 XPM 模板为准不同版本可能略有出入。5.4 级联深度与布线压力单块 36Kb BRAM 的最大深度是 32K位宽为 1 时。如果你需要更深的存储比如 64K 深度Vivado 会自动把两块 BRAM 级联起来。级联能实现更大容量但代价是延迟增加、布线资源占用上升、时序收敛变难。我的经验是当深度超过单块容量、需要级联两块以上时先想清楚是不是真的需要这么大的片内缓存。有时候把数据分块处理、或者用外部存储分担比硬堆 BRAM 更划算。真要用级联记得在 IP 配置里显式设置级联高度并且在时序约束里给更高的关注度因为级联路径上的布线延迟可能成为新的关键路径。另外BRAM 在器件里的分布是固定的列如果你的设计同时用了很多 BRAM 和很多 DSP工具在布局时会互相抢地方导致布线拥塞。这种情况下可以试试用CASCADE_HEIGHT和布局约束去引导但更根本的办法是评估资源使用率别把器件用到 90% 以上。6. 联调实录问题速查与踩坑记录6.1 常见问题速查表下面这张表是我这几年积攒下来的出问题时按现象查命中率挺高。现象最可能的原因处理办法IDELAY tap 写入后延迟没变化未例化 IDELAYCTRL 或 RDY 未拉高补 IDELAYCTRL复位逻辑里等 RDY延迟值与计算差一倍左右REFCLK_FREQUENCY 属性写错改成与实际参考时钟一致输出时钟抖动大、偏斜大用普通逻辑输出时钟改用 ODDR OBUF实现报 CLOCK_DEDICATED_ROUTE时钟走了普通布线资源时钟源接 BUFG/BUFIO/MMCM时钟切换后偶发死锁切换到未锁定的时钟加 locked 判断作为切换条件时钟切换毛刺导致误触发用 LUT 切换时钟换 BUFGMUXS 端做同步BRAM 读出数据与预期错一拍输出寄存器延迟未对齐控制信号同步打拍匹配读延迟BRAM 读写地址碰撞时数据错写模式选了 WRITE_FIRST改 READ_FIRSTBRAM 时序在高速下收不紧未开输出寄存器或级联太多开输出寄存器减少级联高度COE 初始化报错数据个数与深度不符用脚本生成别手写6.2 几个我印象深刻的坑第一个坑是 IDELAYCTRL 的复位。我早期的代码里把复位信号接成了常 0觉得“不复位最省事”。结果某个批次的板子上电后 RDY 一直不拉高延迟链根本没校准接口误码率飙升。后来才知道IDELAYCTRL 需要一次有效的复位来启动校准流程。现在我的写法是上电后给一个足够宽的复位脉冲比如几百个时钟周期然后死等 RDY 拉高RDY 不拉高就不放数据通路出去。第二个坑是 ODDR 输出时钟的相位。有一次我做了两路输出时钟一路给外部器件当采样时钟一路给自己内部的逻辑做参考。我天真地以为这两个时钟的相位是一致的因为都来自同一个 MMCM。实际上ODDR 输出到引脚的那一路经过 OBUF 和 PCB 走线之后和内部时钟之间已经有了固定的延迟差。这个差值必须通过约束和外部测量来补偿不能想当然。后来我在 XDC 里给输出时钟加了明确的set_output_delay并把内部逻辑的采样点往后挪了一点才算稳定。第三个坑是 BUFGMUX 的切换窗口。我做过一个需要在线切换时钟频率的设计切过去之后发现下游有个状态机偶尔会停在中间状态。查波形发现切换的瞬间正好赶上状态机在做一个多周期操作时钟一换计数器节奏变了状态机就乱了。解决办法很简单切换前先让状态机回到空闲态切换完成后再重新启动。这个逻辑不是 BUFGMUX 的锅是切换时序没设计好。时钟切换不只是切时钟所有依赖此时钟的逻辑都得考虑过渡状态。第四个坑是 BRAM 的 ECC。有一版设计为了省资源没开 ECC结果在现场偶尔出现单比特翻转数据错得很隐蔽日志里看不出来只有特定功能异常。后来把 ECC 打开虽然多占了一点资源但至少能检测到错误并上报。做可靠性要求高的项目时别省这点资源。6.3 我个人的使用习惯最后分享几个我固化下来的习惯都是踩坑换来的。例化原语时所有参数全部显式写出来哪怕和默认值一样也要写。这样后面别人接手或者自己隔几个月回头看一眼就知道当初是怎么配的不用去翻手册查默认值。每种原语都配套写一个简单的自检模块。IODELAY 就做一个扫描控制ODDR 就做环回测试BUFGMUX 就做切换计数BRAM 就做读写校验。这些自检在调试阶段能省下大量时间尤其是板子刚回来的时候先跑自检确认基础功能没问题再往上叠业务逻辑。约束文件和代码同步维护。每次改了一个原语的配置对应的create_clock、create_generated_clock、set_output_delay都要跟着改不能只管代码不管约束。我见过太多“代码对、约束错”导致的时序问题查起来比代码 bug 还费劲。这套东西说到底就是四个字匹配、对齐。IODELAY 是让输入数据和采样时钟匹配ODDR 是让输出数据和输出时钟对齐BUFGMUX 是让切换时刻和系统状态对齐BRAM 是让读数据和控制信号对齐。把这四个“对齐”想明白了剩下的就是查手册和调参数的事。
返回列表