CDRClock Data Recovery时钟数据恢复这个话题做高速串行通信的FPGA工程师早晚都要撞上。不管是网口、PCIe、USB还是光模块数据在传输的时候都不带伴随时钟接收端必须自己想办法从码流里把时钟信息恢复出来再用恢复出的时钟去对准数据、完成采样。用户的主贴标题点得挺准——用FPGA实现简易CDR这其实是一块非常经典的“练手级”数字逻辑项目不依赖PLL、不碰模拟电路纯靠Verilog就能把CDR的核心机制跑通。本篇我打算用8倍过采样的方案做一套可仿真、可上板的最小CDR系统从原理拆到代码、从testbench写到调试心得标题里提到的“从原理到Verilog代码附仿真”我会全部覆盖到。适合两类读者一类是刚学完Verilog语法、想找一个有实际工程味道的小项目练手的FPGA初学者另一类是正准备做高速串行接口但被模拟CDR那套锁相环体系弄得头疼想先用数字方式理解底层机制的朋友。读完你应该能独立写出自己的过采样CDR模块并且能讲清楚每个设计参数是怎么定出来的。1. 项目概述与原理拆解1.1 为什么需要CDR串行通信的时钟困境并行通信大家很熟悉数据线旁边专门拉一根时钟线接收端直接拿这根时钟去采数据就行。但到了高速场景并行方式会遇到两个麻烦一是线间时延差会让时钟和数据到达接收端的相位关系变得不可预测二是线束太粗、成本太高远距离传输不现实。于是串行通信成了主线方案——收发之间通常只有一对差分线数据一位一位地串着发。问题也随之而来既然没有单独的时钟线接收端怎么知道每个bit从哪里开始、到哪里结束发送端不是没有时钟而是这个时钟只存在于发送端内部。接收端手里只有一串流过来的电平翻转信号它需要从翻转边沿里“推断”出时钟的频率和相位再用这个时钟去对准数据窗口。这个过程就是时钟数据恢复CDR。举个更容易理解的例子你站在马路对面看一对跑步的人他们保持固定步频但没告诉你节奏。你要判断的不仅是他们现在谁在前谁在后还要预先判断下一步落地的时间点。观察步频、卡准落地瞬间这就是CDR在做的事情。1.2 CDR的常见实现路线CDR的实现方式大致可以分成三类。第一类是模拟PLL型CDR这是SerDes里最主流的方式。接收端用压控振荡器和鉴相器构成负反馈环路让本地振荡频率和相位持续跟踪输入数据边沿。优点是恢复出的时钟抖动小、频率精度高能支持非常高的数据率几十Gbps。缺点是模拟电路设计门槛高对工艺、电压、温度都很敏感普通数字工程师很难直接上手调。第二类是数字PLL型CDR把部分模拟功能数字化。鉴相器输出的是数字差值通过数字环路滤波器控制高频时钟的分频比或相位选择器最终生成跟踪数据相位的恢复时钟。这种方案在FPGA的收发器IP内部大量使用灵活性和稳定性比纯模拟方案好一些。第三类是过采样型CDR也是本篇的主角。它不直接“恢复”一个物理时钟而是用一路远高于数据率的时钟比如8倍反复采样输入信号通过对跳变边沿的定位来确定数据窗口然后在窗口中心直接采样数据。恢复出的“时钟”实际是一个数据有效指示脉冲并不严格要求占空比。过采样方案实现最简单纯数字逻辑非常适合教学和低频串行接口。三者的取舍很清楚过采样胜在简单可控适合数据率几十Mbps到几百Mbps、连续相同bit数量不夸张的场景一旦数据率上到Gbps或者需要承受较大的收发频偏就必须上PLL类CDR。1.3 为什么FPGA学习先从过采样CDR入手很多初学者一接触CDR就直奔PCIe或者千兆以太网结果死在模拟PLL的公式推导上。我的建议是想理解CDR的本质先忘掉PLL。过采样CDR用最琐碎的逻辑把“如何从边沿中找到采样点”这个核心问题暴露得很清楚而且全程能用普通FPGA的通用IO和仿真工具验证不需要任何专用硬核。换句话说这个方法能让你在仿真波形里“看见”时钟恢复的过程——看见边沿检测信号、看见采样相位计数器的回绕、看见数据在中心点被稳定捕获。这种可见性对建立直觉非常重要。等搞透了数字过采样思路再回去看PLL型CDR的框图那些环路参数的意义一下子就顺了。2. 方案设计与架构选型2.1 整体方案8倍过采样先定指标再做设计这是工程的正常顺序。本设计的目标是恢复10Mbps的串行数据流换句话说每一个bit的持续时间是100ns。采样时钟取多少倍合适我选了8倍也就是采样时钟频率80MHz采样周期12.5ns。为什么是8倍不是4倍、16倍多一个bit周期里多几个采样点边沿检测的分辨率就高一些但逻辑频率也会上去资源占用同步增加。4倍过采样每个bit只有4个采样点理论上可行但跳变检测的相位误差最大会到一个采样周期25ns留给采样中心的稳定裕量太小稍有点信号抖动就容易在边界附近误采。16倍过采样当然更稳但对80Mbps这种测试场景系统工作频率要拉到160MHz没太大必要还会让初学者在布局布线阶段多操心。用8倍每个bit周期内有8个采样点边沿定位误差不超过12.5ns数据中心点和跳变边沿之间有约37.5ns的相位余量对于10Mbps数据来说非常宽裕。这个安全裕量对初学者非常友好即使上板时信号质量一般也不容易失败。2.2 系统架构与模块划分整个CDR模块我分成三个逻辑部分。第一部分是输入同步链。外部进来的din是异步信号直接采会有亚稳态风险必须先用两级D触发器打拍得到同步后的信号。第二部分是边沿检测把同步后的当前值和前一个值做异或一旦两者不同说明两个采样时钟之间发生了电平翻转产生一个跳变脉冲。第三部分是采样相位控制用一个3位计数器配合跳变脉冲做周期回绕在合适的计数值上产生采样使能信号。模块划分的原则很简单单点功能、单一职责、接口清晰。同步链单独写还可以方便后续加时序约束采样逻辑单独写则方便在调试时单独观察采样点是否落在正确位置。这里我不建议把同步链和采样逻辑揉在一个always块里那会严重降低代码可读性。2.3 采样点的计算与容差分析这是整个设计中最需要讲清楚“为什么”的地方。假设某个bit边沿跳变发生在t0时刻过采样下我们不一定能精确知道t0只能知道跳变发生在上一个采样点和当前采样点之间的某个位置。边沿检测脉冲有效时我们直接把相位计数器清零约定此刻为0。接下来采样点就按每周期递增1走第k个采样点距离检测到边沿的时刻是k个采样周期。因为一个bit一共8个采样周期那么数据窗口的中心应该在第4个采样点。所以采样使能就设在计数器值等于4的时刻。这段逻辑的本质是用计数器重建一个虚拟的位时钟然后把采样点强制放到bit窗口的正中央。这里的容差值得算一笔账。允许的相位误差由三部分构成采样时钟自身的抖动、输入数据的抖动、边沿检测的量化误差。边沿检测的量化误差最多1个采样周期即12.5ns。采样点定在中心左右各有约50ns的冗余刨掉量化误差后还有约37.5ns换算成采样周期约3个。换句话说输入数据的边沿抖动只要不超过3个采样周期本设计就不会采错。再考虑收发频偏。假设发送端频率是10.001MHz而非标称10MHz接收端采样仍是80MHz那么每个bit实际占用的采样周期数是8000ns的实际bit周期/12.5ns约为7.9992个。每个bit都会积累约0.0008个采样周期的相位误差。连续2500个bit不跳变误差积累会达到约2个采样周期接近容差边界。所以这类纯过采样CDR对长时间连续相同bit并不友好实际工程中要么限制连续同码长度要么改用PLL类方案。这个结论在后面章节还会展开。3. Verilog代码实现与关键细节3.1 顶层模块端口定义代码从一个参数化顶层开始我尽量保持可读性。输入是80MHz采样时钟、异步复位和串行数据输出是恢复的1bit数据以及数据有效脉冲。module cdr_top #( parameter OVS 8 // 过采样倍数 )( input wire clk, // 采样时钟80MHz input wire rst_n, // 异步复位低有效 input wire din, // 串行数据输入10Mbps output reg dout, // 恢复出的数据 output reg dout_valid // 数据有效脉冲每bit周期出现一次 );参数OVS保留了调整余地。本篇按8实现如果想改成4倍或16倍把参数和计数器位宽调整一下即可。3.2 亚稳态处理与边沿检测亚稳态是异步信号进同步域的经典坑。din从外部进来和采样时钟clk没有任何确定的相位关系直接采可能会采到正在变化中的电平导致后续逻辑判断错乱。处理办法很朴素连打两拍让第一拍的亚稳态在多出来的一个周期里稳定下来。这里有个细节很多初学者只打两拍就完事但对于边沿检测来说我们不仅关心当前电平还关心上一个周期的电平所以实际上需要三拍。前两拍负责同步和稳定第二拍和第三拍用于异或比较检测跳变。reg din_sync0; reg din_sync1; reg din_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin din_sync0 1b0; din_sync1 1b0; din_sync2 1b0; end else begin din_sync0 din; din_sync1 din_sync0; din_sync2 din_sync1; end end wire edge_detected din_sync1 ^ din_sync2;edge_detected拉高表示在最近一个采样周期里信号发生了电平切换。注意它不是瞬时边沿而是滞后了一个到两个采样周期的“检测结果”。这个滞后在设计中会被计数器自然消化不影响最终采样结果后面仿真会验证这点。3.3 相位计数与中心采样逻辑相位计数器是CDR的“虚拟时钟源”。8倍过采样对应3位计数器范围0到7。每当检测到数据跳变就把计数器强制清零表示重新锁定相位从跳变后的第一个采样点开始计时如果没有跳变计数器继续循环维持对位周期的推算。reg [2:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 3d0; else if (edge_detected) cnt 3d0; else cnt cnt 1b1; end计数器值等于4时对应数据窗口中心此时采样。采样结果直接作为恢复数据输出同时拉高有效脉冲。wire sample_point (cnt 3d4); always (posedge clk or negedge rst_n) begin if (!rst_n) begin dout 1b0; dout_valid 1b0; end else if (sample_point) begin dout din_sync2; dout_valid 1b1; end else begin dout_valid 1b0; end end注意到这里采样的是din_sync2也就是已经同步稳定后的数据。打拍导致的数据时机偏移已经在设计考量之内因为计数器重置和采样使能都是在采样时钟上升沿发生的而我们在该时刻看到的是同步链路输出端的电平它与外部数据之间有固定的相位关系只要这个关系稳定恢复就没问题。3.4 完整代码与注释把上述片段拼起来就是一份完整的、可以直接放进Vivado或Quartus工程的cdr_top模块。module cdr_top #( parameter OVS 8 )( input wire clk, input wire rst_n, input wire din, output reg dout, output reg dout_valid ); // 1. 两级同步消除亚稳态 reg din_sync0; reg din_sync1; reg din_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin din_sync0 1b0; din_sync1 1b0; din_sync2 1b0; end else begin din_sync0 din; din_sync1 din_sync0; din_sync2 din_sync1; end end // 2. 边沿检测 wire edge_detected din_sync1 ^ din_sync2; // 3. 相位计数器 reg [2:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) cnt 3d0; else if (edge_detected) cnt 3d0; else cnt cnt 1b1; end // 4. 中心采样 wire sample_point (cnt 3d4); always (posedge clk or negedge rst_n) begin if (!rst_n) begin dout 1b0; dout_valid 1b0; end else if (sample_point) begin dout din_sync2; dout_valid 1b1; end else begin dout_valid 1b0; end end endmodule这份代码没有用到任何厂商专用原语移植性很好。在Spartan-7、Cyclone IV这类入门级芯片上综合后资源占用都在个位数逻辑单元级别几乎不占资源。做PCB板级调试时可以把dout_valid引到示波器上观察每个bit周期应该出现一个周期性的窄脉冲。4. 仿真验证流程4.1 testbench设计数据源与抖动注入仿真要做的是验证CDR模块能否把一串已知的串行数据正确恢复出来。我设计了一个带发送侧时钟的数据源以100ns为bit周期发送一个固定序列。为了增加可信度我还向数据里注入了随机的边沿抖动——每次bit翻转时间比理想时刻提前或滞后几个纳秒模拟真实信道的不确定性。timescale 1ns/1ps module tb_cdr_top; reg clk; reg rst_n; reg din_tx; wire dout; wire dout_valid; // 采样时钟 80MHz always #6.25 clk ~clk; cdr_top u_cdr ( .clk(clk), .rst_n(rst_n), .din(din_tx), .dout(dout), .dout_valid(dout_valid) ); // 发送侧10Mbps数据源 localparam BIT_PERIOD 100; localparam DATA_SEQ 8b10110010; reg [2:0] bit_index; reg [9:0] tx_delay; initial begin clk 1b0; rst_n 1b0; din_tx 1b1; bit_index 3d0; tx_delay 0; // 复位 repeat(10) (posedge clk); rst_n 1b1; // 起始位拉低一个bit周期表示开始 tx_delay $urandom_range(0, 10); repeat(BIT_PERIOD) (posedge clk); din_tx 1b0; #tx_delay; // 发送数据从低位开始 repeat(8) begin din_tx DATA_SEQ[bit_index]; tx_delay $urandom_range(0, 10); #(BIT_PERIOD - tx_delay); tx_delay $urandom_range(0, 10); #(tx_delay); bit_index bit_index 1; end // 停止位 din_tx 1b1; repeat(4) (posedge clk); $finish; end // 监视恢复数据 integer err_count; always (posedge clk) begin if (dout_valid) begin $display(time%0t dout%b dout_valid%b, $time, dout, dout_valid); end end endmoduletestbench里故意在数据边沿处做了随机的脉冲延迟这样din不是严格的100ns周期跳变而是带抖动的数据流。仿真时会看到采样点仍然稳定地落在数据中心这就是CDR在起作用。4.2 仿真波形与结果分析仿真运行后重点是观察三个信号din_sync2、cnt和dout_valid。波形里能清楚看到cnt在每次数据跳变后被清零随后递增。当cnt等于4时dout_valid拉高此时的din_sync2必定等于当前的发送数据位。如果在data sequence是10110010的情况下接收端依次恢复出1、0、1、1、0、0、1、0说明CDR功能正确。实际仿真中我见过一个常见现象刚开始复位结束后如果数据线上电平没有跳变cnt会一直空转dout_valid也照常出现采到的数据可能是错误的。但一旦出现第一个跳变边沿计数器立即被同步到正确相位之后所有采样点都恢复正常。这个现象说明过采样CDR需要“看见”边沿才能锁定不存在任何提前锁定的机制。在后续章节我会专门说这个坑。如果想让恢复效果更具说服力可以把DATA_SEQ改成长一点的伪随机序列比如16位的0xB40D再在testbench里加一个自动比对模块实时统计恢复错误数。正常情况下的错误数应该是0。5. 常见问题与调试技巧实录5.1 为什么我的输出总是差一拍这是做CDR最容易遇到的“相位错位”问题。逻辑上没问题代码也没问题但仿真时恢复的bit串整体比发送序列晚了一个bit周期或者采样点恰好落在bit边界上导致恢复异常。排查的思路是先在波形里看edge_detected和cnt的关系。如果edge_detected出现那拍cnt没有被清零检查是不是边沿信号和计数器复位逻辑不在同一个时钟域或者复位优先级被占用。再看sample_point位置它应该在cnt等于4时出现如果等于0或者7那就是计数器初值或者触发条件写错了。还有一个容易忽略的点testbench里给din赋值用的是阻塞赋值而且紧跟在某个事件后容易与采样时钟沿产生竞争。仿真看不出问题、上板出问题时优先检查外部输入数据与采样时钟的相位关系而不是怀疑CDR核心逻辑。5.2 数据有频偏时如何处理前面容差分析里算了纯过采样CDR能容忍的频偏和连续相同bit数量成正比。实际项目中遇到频偏大的情况通常有三种应对思路。第一种提高过采样倍数。从8倍提到16倍相位误差和容差都会改善但代价是逻辑频率翻倍对FPGA器件等级要求更高。第二种在CDR后面接FIFO。发送端和接收端各自独立时钟数据恢复出来后先写入异步FIFO用本地系统时钟读出频率差异由FIFO深度吸收。这是很多低速串口协议的标准做法。第三种改用带PLL的数字CDR架构。用锁相环产生更高频的时钟再通过数字逻辑在环路里精细调整相位。这个方案复杂度有明显提升但也是高频SerDes门最基本的工作原理。当前这个简易CDR的意义在于把核心概念跑通真正上高速时再叠加PLL是自然的演进路线。5.3 过采样倍数选择8倍还是16倍选择过采样倍数本质是在相位精度和逻辑速度之间做权衡。4倍过采样每个bit只有4个采样点CDR能给出的相位分辨率只有25Mbps这个量级处理数据抖动很差不推荐。8倍是低速率串行接口的常见起点逻辑不复杂容差也可观。16倍适合数据率更高或信号完整性更差的场景代价是工作频率要求翻倍比如想恢复100Mbps数据采样时钟就要1600MHz这在大多数FPGA里已经不可能用普通IO实现了。在FPGA选型时还要考虑IO布局限制。如果采样时钟频率过高布局布线时序收敛会很困难初学者很容易在这一步卡住。我的建议是在10Mbps数据率下用8倍先把整个流程吃透再逐步提高数据率验证不同过采样倍数的影响。5.4 硬件调试心得板级调试时我习惯把din、dout_valid和dout用ILA或者逻辑分析仪抓下来一遍一遍和仿真波形对照。曾经遇到过一个情况仿真完全正确上板后采样点却不稳定最后抓波形发现是外部信号经过连接线后带了比较大的毛刺边沿检测逻辑一遇到毛刺就误触发计数器频繁复位造成采样点跳来跳去。解决办法是在输入管脚端加上拉或施密特整形或者在逻辑里延长边沿脉冲的消抖窗口来过滤毛刺。还有一点经验复位电路必须可靠。CDR的计数器依赖复位来确定初始相位如果复位信号本身有毛刺或释放时序不规范CDR会进入不可预测的状态。上板调试时优先确认rst_n释放时间是否符合采样时钟的复位时序要求否则CDR模块再正确也会输在起跑线上。关于采样到的数据如何与外部系统对接建议在dout_valid使能时直接把dout打入一个移位寄存器或异步FIFO。很多初学者只盯着“恢复出bit”就以为结束了实际上完整的串行接收链路还包括字节对齐、流量控制等CDR只是其中最核心的数据采集环节。先把这一节吃透后面的模块化开发就顺理成章了。