找回密码
立即注册
搜索
发回帖 发新帖

6015

积分

0

好友

758

主题
发表于 14 小时前 | 查看: 0| 回复: 0

做过 SDIO、eMMC、HyperRAM 的人,多半写过这么一段代码:用 PLL 或 MMCM 对系统时钟分频,取其中一位 assign 到引脚,就当设备时钟使了。

它能跑。卡能读,数据能写,上板一次就通。

但这个做法有频率限制。同一根时钟既要在 400kHz 上慢慢起头,又要在 200MHz 上高速传数据,还要在两者之间干净地来回切换。到这时候,那个分频出来的时钟就开始不听话了。

一、分频直接输出,坑到底在哪?

先把话说明白,这不是「分频不能用」。分频本身没问题,问题出在把时钟直接 assign 到引脚上。

第一个坑,时钟被当成了组合逻辑信号。 综合工具眼里它就是一个普通网线,走通用布线,路上随便一段组合逻辑都能给它添延迟,添出个毛刺来。

第二个坑,分频时钟经常被人顺手配上两种沿。 always @(posedge dev_clk) 之外,再写一个 always @(negedge dev_clk),DDR 采样那类场景特别容易这么干。这一下就把单时钟域的原则破掉了,两个沿之间的路径,工具没法替你兜底。

第三个坑,路径延迟不可预测。 同一个设计,综合前仿真和综合后仿真行为不一样,板子上再跑一遍又是另一个结果。这类问题最难受的地方不在于难修,在于难复现。

这些坑合起来其实只有一句话:分频分出来的那个时钟,看着像时钟,其实是个普通信号。

同一根设备时钟的两种做法对比图:左侧分频直接输出走通用布线不可靠,右侧宽时钟加 OSERDES 走专用车道可靠

二、换个思路,把时钟当数据发

ZipCPU 的作者 Dan Gisselquist 给的解法,第一句话就有点反直觉:不要去做一个时钟产生电路,把时钟编码成数据发出去。

他给这个东西起名叫宽时钟,Wide Clock。做法是这样的。

ZipCPU 原文 Device Clock Generation 截图,讨论如何为外部器件生成设备时钟,包含基本 SOC 与时钟返回结构示意

一个系统时钟周期切成 4 个等长的时间段,从 3 数到 0。然后定义一个 N 位的信号,每一位代表对应的那段时间里,引脚上该是什么电平。

容易混的是这一点:4 段说的是相位划分,决定边沿能摆在哪几个位置;宽信号的位宽跟着 SERDES 档位走,8 倍串行就 8 位,4 倍串行就 4 位。

这个宽信号不直接接引脚,它被喂进 OSERDES。OSERDES 有 8 倍和 4 倍两档串行化,8 倍那一档一次推 8 位,4 倍那一档一次推 4 位。OSERDES 是 IOB 里自带的资源,专门干串行化这件事,它把宽信号一位一位推到引脚上,推的速度比系统时钟快好几倍。

有意思的地方就在这里。用 8 倍串行那一档,100MHz 的系统时钟一个周期喂进去 8 位,这 8 位正好能塞下两个完整的方波周期,输出就是 200MHz。前面说的频率限制,就这么绕过去了。

更关键的是所有逻辑全都留在源时钟域里。你写的每一个 always 块,触发条件还是那个系统时钟。设备时钟线上发生的事,在你自己的逻辑里只是「一位一位被推出去的数据」。单时钟域,没有 CDC,也就不需要同步器。

节奏靠两个使能信号控制。一个叫 new_edge,标记新时钟周期的开始;一个叫 half_edge,标记半周期的位置,DDR 模式下的数据就是在这一刻换的。

时序上最要紧的一条是关断。停这根时钟,不是把输出直接拉低,而是等当前这个时钟周期走完,把一个完整脉冲走完,再把输出保持成 0。这样停干净了,下一次起来才不带毛刺。SDIO 这类协议要求时钟能随时暂停、随时恢复,靠的就是这个动作。

停钟要停干净恢复要立刻的波形图,展示关断请求在最后一个脉冲走完后生效,以及整个设计只有一个时钟域的数据流向

时钟不是被造出来的,是被送出去的。 这句话值得多念两遍。

三、能直接抄的编码表

原理说完,得给能抄的东西。

假设系统时钟 100MHz,用 8 倍串行的 OSERDES,宽信号取什么值,就决定了输出是什么时钟。这张表就是全部。

模式 宽信号值 wide_clock[7:0] 输出频率 说明
最高速 SDR 8'h33 200MHz 每拍翻转,靠 8 倍串行做出的倍频档
最高速 DDR 90° 8'h66 200MHz DDR 边沿相对数据错开,给 DDR 用
半速 SDR 8'h0f ↔ 8'hf0 交替 50MHz 两个输出周期才翻转一次
半速 DDR 90° 8'h3c 100MHz DDR OSERDES 专用配置
1/4 速 SDR 8'h00 两个周期 → 8'hff 两个周期 25MHz 宽值保持两个时钟周期
关断 8'h00 0Hz 干净停钟

如果用 4 倍串行的 OSERDES,或者 2 倍串行的 ODDR,思路一模一样:位宽折半、值等比缩小,最高速 SDR 就从 8'h33 变成 4'h3。位宽折半,同一档的输出频率也跟着降一档。

表里最值得盯一眼的是 8'h33 这一行。写成二进制是 00110011,四位一截,正好两个完整周期,8 位一拍推完,200MHz 就出来了。

8'h33 如何产生 200MHz 时钟的波形示意图,展示 8 位数据在一个系统时钟周期内塞下两个完整方波周期

代码骨架长这样,宽值和使能位一起提交:

reg [7:0] outgoing_clock;   // 宽时钟,喂给 OSERDES
reg       active_clock;     // 时钟使能

always @(posedge src_clk)
if (i_reset) begin
    outgoing_clock <= 8'h0;
    active_clock   <= 1'b0;
end else if ((S_VALID && S_READY) || (new_edge && active_clock)) begin
    outgoing_clock <= wide_clock;   // 提交新的一个时钟周期
    active_clock   <= 1'b1;
end else if (new_edge) begin
    outgoing_clock <= 8'h0;         // 没有数据要发,停钟
    active_clock   <= 1'b0;
end

ZipCPU 那个 sdspi 控制器里,这个模块叫 sdckgen.v,是这套方法的一个完整实现。

它的计数器分两段用。高两位管相位,从 3 走到 0;低八位只有当配置的分频系数大于等于 3 时才当分频计数器用。new_edge 在相位从 3 翻到 0 时拉起来,half_edge 在 3 到 0 或 1 到 0 时拉起来。速度配置分四档:写 0 是 200MHz,写 1 是 100MHz,写 2 是 50MHz,宽值在 8'h0f 和 8'hf0 之间交替,大于等于 3 就按 100 除以「配置值减 1」去分频。

关断那段代码值得单看一眼。请求关断之后,它把计数器直接置到相位 3 的位置,等当前周期走完,输出就停在那儿。撤销关断则立刻恢复,不用等相位对齐。一个走完再停,一个说到就到。

四、这套方法也有边界

先说一个容易忽略的点:这东西不是「跑起来就行」。

宽信号能取的值只有那几个:8'h33、8'h66、8'h0f、8'hf0、8'h3c、8'h00、8'hff。代码一旦拼出别的值,就意味着这根时钟已经有了你没打算要的形状。速度、相位、关断恢复这几类组合一交叉,穷举量很快就上去了,靠仿真盯波形盯不住。

所以这套设计要做形式验证。但这事儿没有想象中重,它有一个很轻的切法,叫接口属性分离。

把生成器和后面收数据的那部分之间定义成一份接口属性文件。证明生成器的时候,把对端的假设当成 assert,意思是我这边保证它成立;证明对端的时候,同一份文件里改成 assume,意思是先假定它成立。同一份属性,两边各证一次,最麻烦的一块就拆完了。

真正要证的其实不多,四条就够了:时钟的高电平低电平不能短于最快时钟的半个周期;频率切换只能发生在时钟周期之间;new_edge 和 half_edge 不会同时出现,最高速那一档除外;宽信号只取有效值。

再划两个边界。

一个是这套方法并非 ZipCPU 独创。Xilinx 的官方建议里就写着,输出时钟要用 ODDR 来转发;官方的支持记录也说过,这比把内部时钟直接驱动到 OBUF 可靠。这样内部时钟会一直停留在专用时钟网络上,直到 ODDR 才出去,不必走进通用布线。最常见的接法是 ODDR 的 D1 接 1、D2 接 0。

另一个是器件差异别忽略。7 系和 UltraScale 的 OSERDES,原语名字、位宽、级联方式都不一样。上面那些十六进制的值是特定位宽下的编码,换器件就要跟着换。动代码之前先翻自己那颗器件的手册。

最后是适用面。这套方法解决的是「要给外部器件送一根时钟」这一类问题,SDIO、eMMC、SPI、HyperRAM 这些接口都在里面。纯片内逻辑用不着它。这类 FPGA 高速接口的坑在云栈社区也经常被翻出来讨论,想深挖的可以去找找相关帖子。




上一篇:程序化生成中国山水画:6000 星开源项目用 JavaScript 绘制 SVG 无限卷轴
下一篇:FPGA 门级网表看不懂?HAL 开源硬件分析器帮你拆寄存器、LUT 和时钟树
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-14 19:31 , Processed in 0.844464 second(s), 45 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表