Vivado中实现Costas正交混合数字锁相环的关键路径
简介基于Vivado 2019.2的正交混合数字锁相环工程面向FPGA载波同步应用使用Costas环完成同步信号搜索数据编码采用8B/10B适合通信类课程设计、毕业设计以及数字接收机相关项目。资源集成了完整工程目录、仿真脚本和操作录像压缩包共79个文件约5.91MB其中14个Verilog源文件构成核心代码另有工程配置、综合运行记录、仿真波形数据、批处理脚本与HTML报告等辅助类型能覆盖从工程创建到仿真验证的典型路径。已有795人学习下载也是博主用于演示Vivado下Costas环实现的完整样例。相比零散代码片段这套资源提供可复现的工程环境可打开工程直接仿真通过录像逐步查看仿真操作并明确路径必须设置为英文这一常见限制同时提供内存初始化文件、调试输出等便于深入分析环路锁定过程与8B/10B编码细节。目录按源码、仿真、IP用户文件和运行结果划分层次清楚适合需要一边看操作、一边对照工程学习成本环设计的读者。1. 为什么要在 Vivado 里开发 Costas 正交混合数字锁相环通信接收机里的载波同步如果还没撞过 Costas 环不能说入过门。模拟环路靠鉴相器和 RC 低通滤波到了 FPGA 里全都变成采样率、环路增益和位宽之间的拉锯。这个标题的核心不在“是不是用了 Costas”而在“正交混合 数字锁相环”这一整套在 Vivado 里怎么落地I/Q 两路同时下变频NCO 跑在系统时钟下参数都可以在仿真里调出来还附带一段能回放的操作录像。实际做通信链路时最怕的不是环路不锁而是仿真时序图里一片红色 X 态以及数据从有符号变无符号导致的 180 度跳变。下面按理论、RTL、仿真、录像的顺序把绕开这些坑的关键路径讲透。这套流程适合正在把 QPSK/BPSK 接收机搬上 FPGA 的工程师也适合被环路发散问题卡住的人拿来对照排错。2. Costas 环与正交混合接收的理论先定环路参数再写 RTL2.1 为什么载波同步要拆成 I/Q 两路正交混合大多数教科书从 BPSK 信号出发s(t) A * d(t) * cos(2π f_c t φ0)。直接做平方环可以消掉d(t)的极性但平方会把噪声频谱抬高一倍还要额外做二分频在 FPGA 里载波恢复后总有相位模糊难以清理。正交混合的思路是让输入信号同时与cos(2π f_c t φ̂)和-sin(2π f_c t φ̂)相乘得到 I/Q 两路基带信号。对 BPSK 而言I 路基带约等于A*d(t)*cos(Δφ)Q 支路约等于A*d(t)*sin(Δφ)。用乘积Ip*Qp去驱动 NCO可以让相位差Δφ收敛到 0同时避开直接平方和二分频的问题。这个结构对 QPSK 同样适用只是鉴相器要改成sign(I)*Q - sign(Q)*I或四象限反正切。所谓“正交混合”并不是简单地把信号分成两路而是要求两条支路的本地载波相位差严格保持 90°。数字实现里直接使用 DDS 的 SIN、COS 两个输出就能天然满足省去了模拟 I/Q 不平衡的烦恼。因此在 FPGA 中正交混合数字锁相环的骨架就是一个 DDS 或相位累加器加查找表产生 I/Q 本振两路乘法器完成混频再进入数字低通和鉴相器。2.2 数字环路三要素鉴相器、环路滤波器和 NCOCostas 环本质上是一个二阶数字锁相环闭环里有三个关键块。鉴相器把相位误差转换为误差电压环路滤波器决定带宽和阻尼是抑制噪声与跟踪频偏的主要场所NCO 把数字频控字变成实际相位和 I/Q 本振。数字 NCO 的相位来自累加器输出频率控制字FREQ_WORD f_offset * 2^phase_width / Fs。这个公式写进代码会让环路在不同时钟频率下都可以复用而不是重新调参。对 QPSK 信号我一般不用I*Q直接做鉴相因为它在 45° 与 225° 相位附近存在多个未定义点收敛区间只有 ±45°。更稳的是极性判决式pd sign(I) * Q - sign(Q) * I。它只取符号信息对幅度不敏感在 FPGA 里只需要两个比较器和减法器代价是低信噪比下会有抖动。如果需要更高精度可以改用atan2(Q,I)配合 CORDIC IP但 LUT 和时钟代价更高多数同步场景用极性判决式已经足够。2.3 用需求倒推环路系数符号率、归一化带宽和无阻尼振荡频率写 Verilog 前先把系数算出来否则仿真里调参等于盲调。设采样率Fs、符号率Rs、接收机需要捕获的最大频偏Δf_max。环路自然角频率ωn与捕获范围和噪声抑制有关工程上通常取ωn ≈ 2π*Δf_max/5到2π*Δf_max/10阻尼系数ζ取 0.707 附近。之后用双线性变换或直接差分方程映射到数字域参数列表如下参数连续域公式离散化到 Fs 后的建议比例支路增益 Kp2ζωnKp 2ζωn / Fs积分支路增益 Kiωn²Ki ωn² / Fs²环路等效带宽 BLωn(ζ 1/(4ζ))/2以 Hz 为单位记录到注释里相位累加器位宽NN log2(Fs / 频率分辨率) 2把Fs 100 MHz、Rs 5 MHz、Δf_max 1 MHz代入ωn ≈ (2π*1e6)/7 ≈ 0.897e6 rad/s于是Kp ≈ 2*0.707*0.897e6/100e6 ≈ 0.0127Ki ≈ (0.897e6/100e6)^2 ≈ 8.04e-5。这个数量级直接告诉我们在 16 位定点下 Kp 只需要约 5 位小数量级而 Ki 需要 14 位以上的小数。仿真时出现发散多半是 Ki 的量化步长过大或极性写反。把这些值先放进表格再看 RTL 实现时会很容易检查位宽是否选得合理。3. Vivado 工程与 RTL 实现从 NCO 到环路滤波器的位宽取舍3.1 创建 Vivado 工程时先改掉的两个默认习惯用 Vivado 建工程我一般不用默认的top名字而是建好目录后通过 Tcl 一次性建工程避免 GUI 中途改路径导致仿真库重建。下面的脚本适合 2023.2 之后的版本source ./settings64.sh 2/dev/null vivado -mode batch -source build_project.tclbuild_project.tcl里至少指定-part xc7a35ticsg324-1L、-top dp_costas_top并把sim_1设置为仿真目标。默认综合策略会选Flow_PerfOptimized_high但这对纯逻辑的调制器没有意义我会选Flow_RuntimeOptimized把时间留给仿真验证。另一个容易踩的坑是xsim.dir会和工程.Xil目录混在一起仿真前先跑一次reset_project能避免很多老版本行为。3.2 NCO 与正交混合用 DDS Compiler 还是直接查表工程里最稳妥的做法是调用 Xilinx DDS Compiler选中 “Hardware” 和 “SIN/COS LUT only”。相位宽度设 24 位输出宽度 16 位系统时钟接clk。DDS Compiler 的 latency 是固定的仿真时不需要额外加#1。它输出sine和cosine两个 16 位总线声明成signed后后续全部使用$signed()运算。如果没有 DDS IP也可以自己写一个累加器加两个 ROM 查找表但不要用cos函数去综合那会生成巨大的选择器网络。正交混合部分就两个乘法器wire signed [15:0] i_mix $signed(adc_data) * $signed(cos_q); wire signed [15:0] q_mix $signed(adc_data) * $signed(sin_i);这里adc_data建议在顶层就声明为signed [15:0]避免在乘法器里多一次位宽扩展。Xilinx 的乘法器会自动推断出DSP48E1如果综合报告里发现乘法器用 LUT 实现通常是因为乘数是常数或位宽小于 18。仿真时若adc_data为高阻波形显示为红色先查 testbench 里的驱动有没有把总线设成U或Z。3.3 鉴相器的实现与极性检查极性型 QPSK 鉴相器的 Verilog 只有几行reg signed [31:0] phase_err; always (posedge clk) begin if (!rst_n) phase_err 32d0; else phase_err (i_mix[15] ? -$signed(q_mix) : $signed(q_mix)) - (q_mix[15] ? -$signed(i_mix) : $signed(i_mix)); end注意三目运算符会生成一组 MUX但减法器本身是组合逻辑。$signed要放在减法表达式两侧避免有符号数被自动扩展成无符号。仿真后先看phase_err的平均值对 BPSK 信号它应围绕 0 摆动如果均值接近正负满量程说明环路极性反了把 NCO 的正弦余弦输出对调即可。对 BPSKI*Q的均值也成立但容易在符号翻转瞬间产生 2 倍频纹波测试时可以用phase_err同时观察两种选择。3.4 数字环路滤波器的定点量化与溢出保护环路滤波器采用比例积分结构。先把Kp、Ki换成定点增益再全部用移位近似。以Kp ≈ 0.0127、Ki ≈ 8e-5为例定点选择是比例支路除以 128积分支路除以 131072。下面的模块用移位代替乘法reg signed [31:0] int_acc, prop, loop_out; wire signed [31:0] p_shift $signed(phase_err 7); wire signed [31:0] i_shift $signed(phase_err 17); always (posedge clk) begin if (!rst_n) begin int_acc 32d0; loop_out 32d0; end else begin int_acc $signed(int_acc) i_shift; prop p_shift; // 比例支路 loop_out $signed(prop) $signed(int_acc); end end必须用于有符号数右移普通会把它当逻辑右移让负误差变成很大的正数。int_acc要做饱和保护否则环路长时间跟踪大频偏时会溢出并反向摆动饱和判断可以只检查int_acc[31]与int_acc[30]是否一致。最后loop_out截取高 12 位送给 NCO 的频率字。调带宽时优先改移位位数而不是改硬件设计注意Ki和Kp都是除以 2 的幂会限制环路参数的可选集所以仿真阶段允许用定点常数但不建议引入乘法器。4. 仿真中验证环路锁定Testbench、收敛判据与发散排错4.1 在 Testbench 里生成带频偏的 BPSK/QPSK 信号源不用等完整基带链路仿真需要的是一个冲击式信号源。常见做法是在 testbench 里用$cos生成带已知载波频偏的调制信号再用$rtoi量化为 16 位有符号数。下面给出一段实用的 BPSK 载波源parameter Fs 100_000_000; parameter Fc 10_000_000; parameter DELTAF 1_000_000; parameter SYM_LEN 8; // 8 个采样点翻转一次数据位 reg signed [15:0] sig_src; real phase_nco 0.0; real delta_phase; reg [3:0] sym_cnt 0; reg data_bit 1b1; initial delta_phase 2.0 * 3.1415926 * (Fc DELTAF) / Fs; always (posedge clk) begin if (!rst_n) begin sym_cnt 0; data_bit 1b1; end else if (sym_cnt SYM_LEN - 1) begin sym_cnt 0; data_bit ~data_bit; end else begin sym_cnt sym_cnt 1; end end always (posedge clk) begin phase_nco phase_nco delta_phase; sig_src $signed( $rtoi( 16000.0 * (data_bit ? 1.0 : -1.0) * $cos(phase_nco) ) ); enddelta_phase是每个采样周期的相位增量包含Fc DELTAF所以测试信号从仿真的第一拍就带着确定频偏SYM_LEN控制数据位翻转周期8 个采样点一个符号等价于 12.5 MSps 的符号率。$rtoi把实数截断成整数$signed保证乘出来的数按补码解释。实际调试时把DELTAF先设成0.001*Fs左右也就是 100 kHz让环路在几百微秒内锁定否则仿真时间太长记录视频完整曲线会很拖沓。4.2 锁定判据误差信号、频率字和星座图的配合锁定的表面现象是phase_err趋近于 0但光看这一个信号不够。phase_err在数据跃变瞬间会有毛刺因此更可靠的判据是频率字freq_word稳定在预测值附近。已知DELTAF为 1 MHz、相位累加器 24 位、Fs为 100 MHz 时预期频率控制字为(1e6 * 2^24) / 1e8 ≈ 167772。锁定后freq_word的高低振荡不超过 1~2 个 LSB若频率字一直下降或周期性扫过很大范围说明环路滤波器的Ki太小或起始相位差太大。星座图是更直观的验证手段。在 testbench 里每过一个符号周期把i_mix、q_mix写入文本再用 Python 画出来import numpy as np import matplotlib.pyplot as plt data np.loadtxt(iq_samples.txt, delimiter,) plt.scatter(data[:,0], data[:,1], s2) plt.axis(equal) plt.grid(True) plt.xlabel(I) plt.ylabel(Q) plt.savefig(constellation.png, dpi150)这里iq_samples.txt的生成要在$fwrite里同时输出采样指针避免在符号跳变边缘取样。QPSK 的四团点应分别落在 ±A±Ai 附近如果只剩两团说明 I/Q 有一路极性反了或鉴相器没按 QPSK 公式配置。这个脚本也可以直接用作录像中的画面素材比干看波形更容易确认环路真的收敛。4.3 仿真发散的总线索红色波形、X 态和 NCO 极性回环我见到最多的“仿真发散”其实不是数值发散而是信号在仿真器里直接出现X波形图上显示为红线。红线不一定代表驱动冲突也可能是寄存器没有复位。比如 NCO 的freq_word在initial里为 0但没有rst_n置为有效值仿真前 100 ns 里它一直处于Z或U导致后面的乘法器全部输出 X。排查顺序是先查sig_src是否在 0 时刻就有有效数据再查i_mix/q_mix的 X 源是不是来自乘法器最后在sim_1下用force命令把freq_word置成一个常量看闭环能否工作。如果所有信号都是正常波形但环路不收敛那就是真实发散。典型特征是loop_out迅速冲到满幅并保持或者phase_err在两个极值之间来回摆动。此时检查三个地方环路滤波器是否用了逻辑右移NCO 的 DDS 输出是否被截断成无符号数adc_data与 DDS 输出是否在同一个符号位宽下工作。把这三个都排除后再回头算一遍Kp/Ki确认没有把ωn/Fs当成ωn*T而丢掉采样率归一化。5. 录制 Vivado 仿真操作录像从波形窗口到视频交付5.1 把关键信号整理成可辨识的波形分组录制前先保存一份.wcfg让观看的人能一眼认出“这个值是环路的输入还是输出”。我通常把adc_data单独放一组i_mix/q_mix放一组phase_err/freq_word/loop_out放一组。每个总线都改为Signed Decimal或Analog其中phase_err用模拟波形最方便freq_word用十六进制就可以因为它变化幅度小。Vivado 仿真窗口里右键信号名在 “Radix” 里选 “Signed Decimal”再把总线高度拉高录制时就不会出现细得像头发丝一样的波形。这里有一个容易忽略的细节run all的时长要提前设好让第一屏就出现从捕获到锁定的全过程。比如锁定耗时 200 us系统时钟 100 MHz就要跑 2 万个时钟周期。提前用run 200 us等波形走到锁定后再开始录屏比一边录一边拖动进度条更专业。录制者应该把phase_err的值以文本方式显示在波形光标旁屏幕上同时出现参数和光标读数才算是“可操作”的录像。5.2 用 OBS 或 ffmpeg 录制仿真窗口并与脚本同步在 Linux 下录 Vivado 窗口我推荐直接用 ffmpeg 抓屏避免 OBS 带来的额外开销ffmpeg -f x11grab -framerate 30 -video_size 1920x1080 -i :0.0 \ -c:v libx264 -preset fast -crf 23 vivado_costas_sim.mp4-f x11grab是 Linux 下的抓屏接口-video_size必须和实际分辨率一致-c:v libx264用 CPU 编码-crf 23是质量参数不需要无损23 左右足够看清波形。Windows 下可以直接用 OBS 的“窗口采集”锁定 Vivado 窗口再把鼠标光标录制进去。注意把 Vivado 的 Tcl Console 也录进去因为run 100 us之后的返回消息里有仿真结束提示有时还能看到断言错误。为了让录像对同事和搜索引擎都有用建议开头 10 秒做一个“参数字幕”在命令行里输出echo Fs100MHz Rs5MHz deltaF1MHz Kp1/128 Ki1/131072让它显示在屏幕上方同时在视频描述里也写一遍。这样别人从录像里就能知道这是哪组配置后续对比参数不用反复暂停放大波形。录制时最好开启鼠标光标高亮并固定 Vivado 窗口大小避免观众被分辨率变化打断注意力。5.3 录像里比波形更重要的一条“失败—修改—复跑”主线真正好的仿真操作录像不是顺利锁定的一遍而是保留一次失败和一次修改。比如先跑一个loop_out饱和的状态暂停后说明“频率字溢出说明 Ki 的 17 位小数太大”然后修改移位位数再跑一遍锁定过程。观看者需要看到phase_err从大的摆动到收敛的过程中途每改一个参数就用鼠标指向对应行。整体时长控制在 4~6 分钟太长不如分别导出成“参数计算”“工程搭建”“仿真锁定”三个短视频。录制结束后可以用ffmpeg -ss 00:00:10 -to 00:00:20 -i input.mp4 -c copy clip.mp4快速剪辑或用-vf showinfo检查关键帧位置。剪辑后把.wcfg和测试代码放到压缩包一起交付才算是完整可复现的录像包。只有 MP4 没有波形配置别人还是不知道你看的是哪根线。6. 锁定不是终点量化比特、环路带宽与 ILA 在板验证6.1 用一张表量化位宽对环路行为的影响在仿真阶段把位宽逐一变化可以得到下面这张实验记录表。输出位宽并非越大越好。例如i_mix/q_mix从 16 位变到 18 位phase_err的均值改善有限但 DSP 资源增加 40%。而 NCO 相位宽度从 24 位变到 20 位后频率分辨率从 6 Hz 变成 95 Hz环路锁定后的剩余频偏直接抬高了一个量级。模块位宽对锁定的影响资源增量i_mix/q_mix16 位星座图收敛噪声约 0.5 LSB基准i_mix/q_mix18 位噪声变化小于 0.1 LSB多约 1 个 DSP48NCO 相位累加器24 位频率分辨率 6 Hz0NCO 相位累加器20 位分辨率 95 Hz剩余相位抖动明显减少 4 个 LUT仿真关注的核心不是绝对位宽而是环路内部是否存在“噪声源被放大”的路径。比如i_mix的截断在进入phase_err前可以被忽略但如果截断发生在loop_out则会导致 NCO 的调制噪声混入频控字。因此我一般将截断点放在int_acc累加之前而不是放到最终freq_word上。6.2 从仿真到板卡用 ILA 抓实际信号并对照仿真波形最后一步是在板上验证。加入 ILA 核之后把phase_err、freq_word和锁定后的i_mix/q_mix接到ila_0的 probe 上综合实现后打开 Hardware Manager触发条件设为freq_word的稳定值范围后运行。此时看到的数据会与仿真波形有偏差常见的是频谱泄漏和参考时钟抖动导致phase_err不再围绕 0 摆动而是在 8~12 LSB 的范围内缓慢游走。处理方法是把 ILA 触发深度调深到 16384先抓一段线性区再缩小到 256 采样点看局部纹波。如果看到周期性的尖峰而仿真中不存在那多半是板上电源耦合或 DDS 时钟缓冲没设对而不是环路参数本身的问题。由于freq_word在锁定后数值已经稳定ILA 触发可以设成窄窗口例如低 16 位在16h19A8附近再开始抓数据否则捕获窗口里全是未锁定的过渡态。抓完波形后先看freq_word是否落在预取的 1~3 LSB 抖动范围内再决定要不要把环路滤波器换成近似积分器或增加低通前级。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →