设备时钟生成
摘要
一篇技术博客文章,讨论在FPGA和ASIC设计中为与NOR闪存和NAND闪存等外设接口生成设备时钟的方法。
暂无内容
查看缓存全文
缓存时间: 2026/06/12 05:51
# 设备时钟生成
来源:https://zipcpu.com/blog/2025/12/17/devclk.html
在构建了CPU(https://zipcpu.com/about/zipcpu.html)、用于处理总线互连的工具库(https://github.com/ZipCPU/wb2axip)、多个DMA和内存控制器之后,我发现自己的时间往往集中在构建设计与外部外设之间的接口上。这似乎是我大部分业务落地的领域。这些外设通常需要一个由设计输出的时钟,因此我想花些时间描述如何生成这样一个“设备”时钟。
图1:一个带有外设的基本SoC
实际上,在处理现代高速外设设计时,需要讨论两个主题。一个是*生成*发送给外设的时钟,如上图图1所示。第二个主题涉及*处理*从外设返回的时钟,如下方图2所示。这是高速设计(如DDR内存、eMMC、HyperRAM,甚至NAND闪存协议)的关键组成部分。第二个主题我们将在以后的文章中再讨论。
图2:随时钟返回的数据
今天,我想讨论如何生成一个时钟来控制设备交互。
我第一次遇到这个问题是在构建一个NOR闪存控制器(https://zipcpu.com/blog/2019/03/27/qflexpress.html)时,该控制器先后基于一个SPI接口(https://zipcpu.com/blog/2018/08/16/spiflash.html)和一个Quad SPI接口(https://zipcpu.com/blog/2019/03/27/qflexpress.html)。我的控制器(https://github.com/ZipCPU/qspiflash)是为FPGA设计的,因此时钟可以用单一频率构建。这个设计还有一个额外的复杂性:时钟需要不时地暂停。具体来说,当无任何操作时,时钟需要关闭。同样,在拉低(即激活)片选引脚后,需要关闭一个周期的时钟;在事务完成后但在拉高(去激活)片选引脚之前,需要关闭几个周期的时钟。
在控制HyperRAM时,我也遇到了类似的问题,但……那个设计(https://github.com/ZipCPU/wbhyperram)在我还没有准备好正确处理返回时钟时就失败了。我确实说过这值得单独写一篇文章,不是吗?在返回时钟上正确处理数据可能是一个挑战。
后来,我为ASIC平台(https://www.arasan.com/product/xspi-psram-master/)构建了一个类似的设计。与FPGA不同,最终的时钟速度在运行时才能知道。设计可能以较低的时钟速度启动,然后在运行时才加速到全速率。与可以后期修复的FPGA不同,ASIC工作(https://zipcpu.com/blog/2017/10/13/fpga-v-asic.html)中几乎没有失败的空间。至少在FPGA中,如果我的电路板不支持某个特定频率,我可以重新构建设计来适应它支持的时钟频率。但这对于ASIC来说行不通——因为当你决定将设计与一个比你设计时更慢的部件连接时,事后重新构建设计的成本通常过高无法承受。
我参与的下一个设计是NAND闪存设计(https://www.arasan.com/product/onfi-4-2-controller-phy/)。NAND闪存可能是个挑战,因为协议要求你以较低的频率开始,只有在你建立连接后才能切换到更高的频率。这个特定设计(https://www.arasan.com/product/onfi-4-2-controller-phy/)是为ASIC环境构建的,因此它依赖于一个模拟组件来生成我需要的所有时钟。这工作得很好,直到有人想购买这个设计在FPGA上运行,然后另一个客户也想在FPGA上运行它,接着又一个,如此类推。
图3:单数据率(SDR) vs 双数据率(DDR)
SDR
DDR
为了给问题增加另一个复杂性,许多协议要求在时钟的两个时钟沿上都有数据转换,这种协议通常被称为“双数据率”(DDR)。与上述其他设计不同,这些协议通常需要一个与数据相位差90度的时钟——这样每个时钟转换都发生在每个数据有效窗口的中间,而不是窗口的边缘。这种“偏移”时钟对于保证从设备内的建立和保持时间是必要的。图3显示了DDR所需的时钟与数据关系,与传统“单数据率”(SDR)时钟进行了对比。
到了我开发SDIO/eMMC控制器(https://github.com/ZipCPU/sdspi)时,我想我终于解决了时钟分频的问题。SDIO控制器(https://github.com/ZipCPU/sdspi)需要以400kHz启动SD卡,然后根据卡、PCB和控制器,速度可能提高到25MHz、50MHz、100MHz甚至200MHz。当没有数据发送或接收时,或者当SoC无法向控制器加载或卸载数据时,时钟也可以停止。例如,你可能要求SD卡读取并产生多个数据块,然后将前两个块读入内部缓冲区,却发现CPU在清空这些缓冲区方面很慢。在这种情况下,你需要在外部卡尝试向你发送第三个无处可去的数据块之前停止接口时钟。
其他设备需要用户可编程的设备时钟控制器,例如:
- 10M/100M/1Gb以太网控制器(https://github.com/ZipCPU/videozip/tree/master/rtl/ethernet) 尽管这些速度中的每一个都可能使用单个时钟,但构建一个真正的三模式控制器需要一些额外的工作。
- (DDR) SDRAM控制器(https://zipcpu.com/zipcpu2025/05/28/memtest.html) 从FPGA的角度来看,SDRAM控制器往往很简单:只需产生一个时钟。但是,你可以关闭时钟以获得更好的功耗性能。是的,有一些规则……但我们今天不会在这里深入讨论。
- I2S 我们之前讨论过以完全任意频率生成I2S时钟(https://zipcpu.com/blog/2019/06/28/genclk.html)。
- I2C(https://zipcpu.com/blog/2021/11/15/ultimate-i2c.html) 通常,I2C速度太慢,不是本文的重点。有一个基于I2C的I3C协议。我们今天讨论的技术可能适用于I3C主设备,但我对它们不太熟悉。
- SPI – 不仅仅是NOR闪存(https://github.com/ZipCPU/wbspi) 虽然SPI*从设备*也有设备时钟,但处理这些时钟与本文描述的方法根本不同。我今天关注的是为控制外部设备而*生成*时钟信号——例如SPI主设备可能需要做的。
具体来说,今天我想查看并讨论生成具有以下一个或多个特征的时钟:
- **输出信号:**我们讨论的是接口时钟——由接口的“主设备”生成的时钟。它们是*数字*信号,从FPGA(或ASIC)设备输出。输出可以通过像ODDR(https://zipcpu.com/blog/2020/08/22/oddr.html)或OSERDES这样的组件完成,后面可能带有或不带额外的模拟延迟。
- **非连续:**时钟可以是非连续的。许多协议(闪存(https://zipcpu.com/blog/2019/03/27/qflexpress.html)、SDIO/eMMC(https://github.com/ZipCPU/sdspi)等)允许甚至要求时钟停止,或者仅在发送或接收数据时切换。如上所述,停止时钟对于在源缓冲区耗尽或输入缓冲区溢出之前暂停正在进行的传输也很有用。
- **动态频率:**通常,输出时钟需要在操作过程中根据协议要求改变频率。例如,SDIO协议需要从400kHz开始,然后增加到25MHz(或更高)。因此,一个好的时钟生成器需要能够根据协议要求自然地生成多个时钟频率。
- **最小脉冲宽度:**频率之间的切换必须遵循规则:必须完全禁止并保证无时钟毛刺。不能允许过短的时钟脉冲。时钟高电平和低电平持续时间必须至少是最快允许时钟的半个周期。
- **DDR信令的90度相移:**如图3所示,许多现代协议要求正负边沿信令(DDR)。这使所需时钟频率降低2倍,从而减少了为实现相同数据速率而在PCB上必须承载的带宽。然而,支持此类DDR信令所需的时钟信号通常需要与数据有90度的延迟,以便在数据有效期的中间进行转换。
- **快于控制器时钟:**更糟糕的是,在我的eMMC设计(https://github.com/ZipCPU/sdspi)中,我需要从100MHz系统时钟生成200MHz的DDR设备时钟。
所有这一切都说明,我们今天的目标将是使用数字逻辑(而不是模拟逻辑)来创建一个分频时钟。(是的,我能听到我的模拟工程师朋友们在这里插话,评论说“一切都是模拟的!”愿上帝保佑你们,我的朋友们。)
## 问题
我经常看到解决这个问题的第一种方法是直接的整数时钟分频方法。通常,它看起来像这样:
```verilog
always @(posedge src_clk)
if (reset)
counter <= 0;
else if (!active_clock)
counter <= 0;
else // if (active_clock)
counter <= counter + 1;
assign dev_clk = (high_speed) ? (src_clk && active_clock)
: counter[user_selected_bit];
```
在这个例子中,`active_clock`控制时钟是否步进,`user_selected_bit`控制我们感兴趣的时钟分频级别。至于`src_clk`,它可以是系统时钟,也可以是生成协议所需最快时钟频率所需的任何时钟。
请注意,我们并没有做任何工作来保证这个时钟在速度选择之间不会产生毛刺,也不能保证两种时钟速率的最小值。我们稍后会回到这些要求,但会采用不同(更好)的实现方式。
起初,使用这个时钟的用户逻辑看起来非常简单:
```verilog
always @(posedge dev_clk or posedge reset)
if (reset)
begin
// 复位逻辑
end else begin
pedge_data <= // 基于dev_clk控制任何触发器的逻辑
end
```
当协议要求在时钟的两个边缘都有数据时,正确获取第二个时钟边缘的数据也很重要。但是,我们应该如何在我们凭空创建的时钟的下降沿上输出数据呢?我们需要在下降沿上进行转换才能做到这一点。
```verilog
always @(negedge dev_clk or posedge reset)
if (reset)
begin
// 复位逻辑
end else begin
nedge_data <= // 控制负时钟数据的逻辑
end
assign output_data = (dev_clk || !ddr_mode) ? pedge_data : nedge_data;
```
这种方法给我们留下了两个问题。第一个是当我们赋值`dev_clk`可能等于我们的源时钟时,我们正在将我们的时钟用作逻辑信号。第二个问题是我们在该时钟上转换用户逻辑。更糟糕的是,我们现在在时钟的两个边缘都转换用户逻辑。这违反了优秀数字逻辑设计的*规则*(https://zipcpu.com/blog/2017/08/21/rules-for-newbies.html)。
在构建ASIC设计时,这些问题不一定是问题。然而,在FPGA设计中,这个时钟需要以某种方式进入时钟网络主干,而这并不是自动的。更糟糕的是,即使它们频率相同,这个新时钟也*不是*原始`src_clk`。两个时钟之间总会存在延迟——这种延迟可能无法在综合前的仿真中捕获,因此在构建这个逻辑时,这可能是一个工程师没有预料到的危险延迟。
这导致了两个商业ASIC设计挑战。首先,在设计ASIC IP时,你希望尽可能多地在FPGA上测试IP。与FPGA不兼容的逻辑需要移到设计的边缘并仔细控制。其次,从业务角度来看能够将ASIC设计同时出售给FPGA客户和ASIC客户是有帮助的。所以,即使你在ASIC上“可以”做类似的事情,但并不意味着你“应该”做。
还有其它问题。
- **时钟域交叉(CDC)**(https://zipcpu.com/blog/2017/10/20/cdc.html) 由于`src_clk`和`dev_clk`现在是两个独立且不同的时钟域,你需要正确管理这两个时钟域之间的每个时钟域交叉(https://zipcpu.com/blog/2017/10/20/cdc.html)。这可能会通过本应是高速逻辑的路径引入额外的延迟。同样,同一个时钟的正边缘和负边缘(从技术上讲)也是独立的时钟域。在它们之间移动是“可能”的,但“不推荐”。
- **门控** 你可能已经注意到,我们在上面没有正确地门控我们的时钟。当然,我们使用了一个`active_clock`信号来提供门控,但这个信号并不能保证输出时钟的最大频率。然而,这是一个小问题,大多数阅读本博客的工程师应该能够通过一点额外的逻辑轻松解决。
然而,有两个特定的问题在这种类型的设计中成为决定性因素。第一个是DDR接口通常需要一个相对于数据延迟90度的时钟,如上图图3所示。简单的方法不会产生这样的90度延迟。虽然可以使用模拟延迟元件(如Xilinx ODELAY元件)来将时钟信号延迟适当量,但这只适用于高速时钟,而不适用于50MHz或更低的时钟。第二个问题是,当你需要比`src_clk`更快的设备时钟时怎么办?就像我在SDIO/eMMC控制器(https://github.com/ZipCPU/sdspi)设计中那样。
因此,我们确实需要另一种方法。
## 解决方案
基本的解决方案是回归*规则*(https://zipcpu.com/blog/2017/08/21/rules-for-newbies.html),从而完全避免在设备时钟边缘进行任何转换。相反,我们将继续在我们的源时钟上转换,然后使用ODDR(https://zipcpu.com/blog/2020/08/22/oddr.html)或OSERDES来生成最终的输出时钟。同时,我们将把新生成的设备时钟视为传统的逻辑信号——而不是我们设计中的“时钟”。也就是说,让它保持并始终是*逻辑*信号。
让我们先看一下上面的图3,并将时钟周期划分为若干部分,如下方图4所示。
图4:划分时钟周期
通常,每个时钟我们只需要两个部分——每个对应DDR传输中的一块数据。不幸的是,这还不够,因为时钟可能需要偏移90度。因此,我们需要将每个时钟周期分解为四个逻辑上不同的时间段。我们可以给这些时间段标记为3:0,从最左边或最高有效位3到最右边和最低有效位0。
从这里,我们可以生成我称之为“宽”时钟的东西,一次四个位。这个宽时钟随后将通过4:1的OSERDES输出——如果它要与我们设计中的源时钟保持同步的话。在其最快速度下,这个时钟将是`0011`(其中最高有效位'0'被“首先”传输),或者如果DDR传输需要90度偏移时钟(如图4所示),则为`0110`。在下一个最慢速度下,时钟将是`0000`后面跟着`1111`,或者`0011`后面跟着`1100`。进一步的时钟分频将使用`0000`或`1111`的宽时钟。
如果你想使用ODDR(https://zipcpu.com/blog/2020/08/22/oddr.html)而不是4:1的OSERDES,你仍然可以使用这种方法,只是你一次生成2个宽时钟位而不是四个。最快的时钟将是重复的`01`,但是
相似文章
Super Nintendo 的核心
对 Super Nintendo 中时钟生成硬件的详细分析,解释了用于产生所需频率的两个振荡器和可调电容器。
RTX 50系列的外部时钟生成技术
来自Xtreme Systems的超频玩家针对RTX 5090开发了一种外部时钟生成技术,通过使用Elmor External Clock Board(ECB),绕过了Nvidia在软件层面对显存和交叉开关时钟频率的限制。该方法目前仍在完善中,通过硬件级信号注入将GPU时钟提升至出厂限制以上,在基准测试中取得了显著的性能提升。
从零开始在FPGA上设计科学计算器
一系列详细的博客文章,记录了从零开始使用FPGA设计和实现科学计算器的过程,涵盖了数值方法、CPU架构、微码和硬件原型设计。
Rust异步与ARM通用定时器
一篇技术博客文章,探讨了在ARM架构上使用ARM通用定时器进行Rust异步编程,比较了定时器外设,并讨论了Embassy和RTIC等框架。
回归严谨的全系统时序仿真
这篇博客文章主张在计算机体系结构中回归严谨的全系统时序仿真,以克服“时序仿真墙”并准确捕捉现代系统行为,提倡使用统计上可靠的方法测量正确的执行区间,而不是详细仿真一切。