简介:面向FPGA开发者的基于Costas环的正交混合数字锁相环实现资料,基于Vivado2019.2开发,附带完整仿真操作录像,适合学习数字载波同步及PLL工程落地的工程师。压缩包共79个文件,主体为Verilog源码、Vivado工程文件、Tcl脚本、仿真波形数据与avi操作录像,另有少量配置和日志文件,整体约5.91MB,目录结构遵循Vivado工程标准,便于定位源码、仿真与录像。设计采用正交混合结构,通过Costas环搜索同步信号,数据编码为8/10,覆盖从工程建立、仿真验证到录像演示的完整链路,重点展示Costas环关键参数与波形分析。资源提示使用前需将Vivado工程路径设为英文,以免工具异常。已有795人学习下载,借助录屏与源码可快速复现仿真流程,对理解FPGA中Costas环的同步机制和调试方法有实际帮助。
1. 为什么要自己用 Vivado 搭 Costas 环?
接收端载波同步是通信基带处理里最容易被低估的一环。直接拿 PLL 锁 BPSK 信号,调制信息引起的 180° 反相会让环路误锁到相反相位,解调输出一串错码。Costas 环利用正交两路相乘把调制项去掉,只保留载波相位差,所以对抑制载波的 BPSK/QPSK 天然免疫相位模糊。Vivado 2019.2 下的这套正交混合数字锁相环工程,完整实现了通过 Costas 环搜索同步信号,数据编码采用 8B/10B,并附带仿真操作录像,省去了对着文档猜参数的过程。
这个工程的价值在于“能对照”。RTL 源码、sim 工程、xpr 工程文件和操作录像 0043.avi 放在一起,路径怎么设、仿真怎么跑、波形在哪看,全部有据可依。适合刚入手 FPGA 数字通信的人,也适合需要快速评估数字 Costas 环有效性的系统工程师。下面从原理、实现、仿真到排错,讲一遍我拆这个工程的思路。
2. 正交混合数字 Costas 环的环路模型与参数换算
2.1 从模拟 Costas 环到数字正交混合结构
先看模拟域。Costas 环在 BPSK 下就是两路正交乘法器加环路滤波器的锁相结构,输入信号分别乘本地载波的 cos 和 sin,得到 I 路和 Q 路。在没有调制信息时,平均相位误差来自两路低通乘积;在 BPSK 调制下,调制符号落在 I 路上,Q 路只包含相位误差信息,用 I 路判决值乘 Q 路,就把 180° 相位模糊抵消掉了。QPSK 则是 I、Q 相互判决交叉相乘。
数字域把这一结构映射到采样时钟域,主要变化有三处。一是 VCO 换成 NCO,用频率字累加产生相位,再查表或 Cordic 生成正余弦。二是模拟乘法器换成有符号定点乘法器,位宽要覆盖输入动态范围。三是环路滤波器变成数字 PI,通常用一个寄存器累加积分项。正交混合这四个字指的就是 NCO 输出两路正交本振与输入信号做数字混频,而不是模拟混频。
在 FPGA 里更常用的误差检测是判决辅助形式:BPSK 用I * sign(Q),QPSK 用sign(I)*Q - sign(Q)*I。这个误差量直接进 PI 环路滤波器,调整 NCO 频率字,让本地载波频率和相位向输入信号逼近。工程中也可以先用经典I*Q,锁定后再切到判决辅助,降低初始误锁概率。
2.2 环路滤波器系数的计算起点
数字 PI 滤波器的 z 域结构是F(z) = Kp + Ki/(1 - z^{-1}),Kp 决定跟踪速度,Ki 决定稳态误差。系数不是拍脑袋定的,一般先按符号率和采样率算浮点值,再转成定点。一个常见做法是给定阻尼系数 ζ 和环路带宽 Bn。对理想二阶环有Bn = ωn * (ζ + 1/(4ζ)) / 2,所以ωn = Bn * 2 / (ζ + 1/(4ζ))。然后采样周期 T=1/fs,比例系数Kp = 2ζωnT / (Kd*K0),积分系数Ki = (ωnT)^2 / (Kd*K0)。Kd 是鉴相器增益,K0 是 NCO 增益,当环路滤波器输出量直接作为频率字附加项时,K0 约为 1。
我一般用下面这段 Python 算初始系数:
import math def calc_loop_coeffs(symbol_rate, fs, zeta=0.707, bl_ratio=0.01): bn = symbol_rate * bl_ratio wn = bn * 2.0 / (zeta + 1.0 / (4.0 * zeta)) t = 1.0 / fs kp = 2.0 * zeta * wn * t ki = (wn * t) ** 2 return kp, ki kp, ki = calc_loop_coeffs(1e6, 25e6, 0.707, 0.01) print(f"Kp={kp:.6f}, Ki={ki:.6f}")这个函数里,symbol_rate是调制符号率,fs是 FPGA 采样时钟,bl_ratio是环路带宽与符号率的比值,一般取 0.005 到 0.02。跑出来 Kp 和 Ki 是浮点值,在 RTL 里要先按定点化标尺转换成整数。注意这段代码假设 Kd 和 K0 都归一化为 1,如果鉴相器输出带增益,或者 NCO 频率字位宽对应的是相位步进,那么 Kp、Ki 要等比例缩放。真实工程里,我会把计算出来的系数放大 2^Q 倍再截位,Q 由 FPGA 定点精度决定。
2.3 关键位宽怎么定
位宽直接决定环路是否能收敛。NCO 相位累加器位宽 N 越大,频率分辨率 fs/2^N 越高。比如 fs=25MHz,N=24,分辨率约 1.49Hz,足够跟踪 kHz 级别的频偏。如果 N 只有 20,分辨率约 23.8Hz,环路锁定后会有固定相位抖动。乘法器输出位宽建议比输入信号位宽多出 3 到 4 位,避免混频后增益过大截断。环路滤波器内部积分器则在乘法器位宽基础上再增加 4 到 6 位余量,防止频率字累积时溢出。
可以按这个表快速确定位宽:
| 模块 | 位宽设计要点 | 参考范围 |
|---|---|---|
| NCO 相位累加器 | 频率分辨率决定最小可跟踪频偏 | 20~32 bit |
| 乘法器输出 | 保留输入位宽+混频增益 | 输入位宽+3~6 bit |
| 环路滤波器积分项 | 防止频率字长期累积溢出 | 乘法器位宽+4~8 bit |
| 鉴相器误差 | 需要符号位,避免直流偏置 | 与乘法器一致或宽 1~2 bit |
位宽设定后,还要留意定点化截位带来的自激。如果环路滤波器系数太小,截位后积分项可能在一段时间内不更新,表现为锁定缓慢;如果系数太大,误差被放大,环路会震荡发散。这个平衡只能靠仿真扫参数确认。
3. Vivado 工程搭建与 RTL 关键模块实现
3.1 工程目录结构先看清楚
解开压缩包后,第一眼看到一堆目录,很容易乱。project_1.xpr 是 Vivado 工程文件,project_1.srcs 里放着 sources_1 和 sim_1,sources_1 是 RTL 和约束,sim_1 是仿真源文件。project_1.runs 是综合、实现、仿真运行结果,里面 synth_1、impl_1、sim_1 这些子目录都是每次运行生成的。project_1.ip_user_files 放 IP 核定制文件,compile_simlib 则是编译仿真库时生成的。
在 Vivado 里打开工程时要注意,整个路径必须是英文,不能带中文。否则仿真库编译会报路径错误,很多第一次用的人卡在这里。README.txt 里应该写了版本,项目是 Vivado 2019.2,低版本打开高版本工程会失败,所以环境版本要匹配。
提示:Vivado 工程路径包含中文时,
compile_simlib和仿真运行都会出现找不到路径的问题,新建工程后第一件事先确认路径。
3.2 NCO 与正交混频器的 RTL 实现
NCO 最省事的做法是直接调 Xilinx DDS Compiler IP,配置正弦和余弦输出,phase width 按 2.3 节算出来的位宽填。但为了看清环路反馈结构,我习惯先在 RTL 里写一个简化累加器模型,仿真通过后再替换成 DDS IP。下面是一个简化的 NCO 片段:
module nco_simplified #( parameter PHASE_W = 24, parameter LUT_W = 12 )( input wire clk, input wire rst_n, input wire signed [PHASE_W-1:0] freq_word, output reg signed [LUT_W-1:0] sin_o, output reg signed [LUT_W-1:0] cos_o ); reg [PHASE_W-1:0] phase_acc; always @(posedge clk or negedge rst_n) begin if (!rst_n) phase_acc <= 0; else phase_acc <= phase_acc + freq_word; end // 查找表或 CORDIC 生成 sin/cos // 这里用函数占位,实际用 ROM 或 Xilinx DDS IP always @(posedge clk or negedge rst_n) begin if (!rst_n) begin sin_o <= 0; cos_o <= 0; end else begin cos_o <= cos_lut(phase_acc[PHASE_W-1:PHASE_W-LUT_W]); sin_o <= sin_lut(phase_acc[PHASE_W-1:PHASE_W-LUT_W]); end end // 函数 cos_lut、sin_lut 为查找表示意 endmodule这里freq_word是环路滤波器输出附加到固定载波频率字上的值。相位累加器位宽PHASE_W决定频率分辨率,高 12 位作为查找表地址。实际综合时要替换成 ROM 初始化的查找表,或者用 DDS IP 核,否则cos_lut这种函数需要实现成 case 语句。乘法部分则是:
wire signed [DATA_W+OUT_W-1:0] i_mix = adc_data * cos_o; wire signed [DATA_W+OUT_W-1:0] q_mix = adc_data * sin_o;混频后通常要接低通滤波器,去掉二倍频分量。在上述代码中,adc_data是输入信号,cos_o和sin_o是 NCO 两路输出,相乘后位宽扩展,后续再抽取滤波。注意adc_data必须是 signed,Verilog 里乘法结果位宽是两操作数位宽之和,所以提前声明比实际所需多几位无损。
3.3 相位误差检测与 PI 环路滤波器
误差检测在 BPSK 模式下用I*Q或I*sign(Q)。I*sign(Q)判决辅助意味着先取 Q 路符号,再乘 I 路,实际实现就是条件取反:
wire signed [DATA_W-1:0] err = q_mix[DATA_W-1] ? -i_mix : i_mix;这句在 Q 路最高位为 1(负)时取 I 的反码,否则取原值,等效于I * sign(Q)。这里要求 I、Q 已经经过低通滤波并完成符号同步,误差信号才干净。PI 环路滤波器用两个累加器:
reg signed [LOOP_W-1:0] freq_word; reg signed [LOOP_W-1:0] integrator; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin freq_word <= 0; integrator <= 0; end else begin integrator <= integrator + signed'(err * KI_SCALE); // 积分项 freq_word <= signed'(err * KP_SCALE) + integrator; // 比例+积分 end endKP_SCALE和KI_SCALE是浮点系数定点化后的整数标尺,需要在前面用 localparam 定义。注意integrator会无限累加,必须有上下限保护,否则溢出后频率字会突然跳到另一极值,环路直接失锁。这个保护在 Xilinx FPGA 里可以写成饱和运算,或者用计数器复位。环路滤波器输出的freq_word叠加到中心载波频率字上,作为 NCO 的输入。
3.4 8B/10B 编码与同步搜索状态机
数据编码采用 8B/10B,是为了保证 DC 平衡和足够跳变密度。Costas 环锁定后,解调数据还要经过 8B/10B 解码器才能恢复原始字节。工程里同步搜索不只是找载波,还要找 K 码对齐字节边界。状态机大概分三态:搜索、预锁定、锁定。在搜索态,每隔 N 个符号做一次相关检测,关联到 K28.5 这类逗号码后进入预锁定,再连续多次确认后进入锁定态。
这个状态机建议放在project_1.srcs里的独立文件,不要和环路滤波器混在一起。调试时把状态输出到 ILA,能直观看到锁定过程。注意 8B/10B 编码在高位宽并行处理时要处理字节边界,和串行位流下的 Costas 环联调是常见坑,后面仿真部分再讲。
4. 仿真测试台设计与锁定现象分析
4.1 测试台怎么生成带频偏的调制信号
仿真不能直接把 ADC 数据喂进去。为了验证 Costas 环锁定能力,我会用 testbench 在行为级生成 BPSK 信号,并故意加入 10kHz 载波偏移。符号率取 1MHz,采样时钟 25MHz。生成方法是先造一个本振相位累加器:
module tb_costas_top; reg clk = 0; reg rst_n = 0; // 仿真参数 localparam real CARRIER_HZ = 25_000_000 + 10_000; // 故意偏移10kHz localparam integer SAMPLE_RATE = 25_000_000; localparam integer SYMBOL_RATE = 1_000_000; real phase = 0.0; reg [7:0] data_in = 0; wire signed [15:0] adc_data; wire signed [15:0] cos_o; // ... endmodule这段代码用real类型变量模拟连续时间相位,每个时钟沿按phase += 2*$pi*CARRIER_HZ/SAMPLE_RATE累加,然后与一个随机或计数型调制符号相乘,输出adc_data。注意行为级模型里精度要足够,否则载波偏移会被量化噪声吞掉。为了省仿真时间,符号率可以提高到 5MHz,偏移也相应放大,只要环路归一化参数不变,锁定行为一致。
testbench 里还可以把环路滤波器的输入数据导出到文本文件,方便用脚本分析。Vivado 的 xsim 支持系统任务$fwrite,在仿真目录下自动生成 log 文件。
4.2 锁定波形的判断方法
启动仿真后,重点看三个信号:NCO 频率字freq_word、误差信号err、解调输出 I 路。频率字开始会以一个方向爬升,当接近实际载波频率时,误差信号的正负交替明显变慢,最终围绕零附近摆动。锁定后freq_word基本平稳,err在零附近抖动,抖动幅度取决于环路带宽和输入信噪比。
如果看到误差信号恒定在一个非零值,或者频率字一直单调递增到边界,那大概率是失锁或极性接反。可以先反过来接 I/Q,或者把 Kp、Ki 调小一半再仿真。在 xsim 的波形窗口里,我习惯把freq_word设成模拟格式,用数值表示实数,直观看出是否收敛。
4.3 对照操作录像复现仿真
压缩包里的操作录像 0043.avi 用 Windows Media Player 播放。录像录制了从打开project_1.xpr到运行仿真、查看波形的全过程。第一次做建议按暂停键逐步对齐路径设置。有几个细节录像里可能一笔带过:一是仿真前要在 Vivado 的 Settings 里选择 xsim,并正确设置compile_simlib,确保仿真库已经编译;二是如果机器上装了多个 Vivado 版本,需要确认 2019.2 版本的vivado.bat在 PATH 最前面;三是工程路径含中文时,仿真库生成会失败,在任务管理器里清除残留进程后重试。
仿真时间不要设太长。Costas 环锁定时间通常在几千个符号周期内,对 25MHz 时钟、1M 符号率,仿真 5ms 大概需要跑几分钟。如果超过预期仍没锁定,优先检查freq_word初始化和极性,而不是无限延长仿真时间。
5. 排除仿真发散与失锁的实用技巧
5.1 仿真发散先看这三个指标
发散是 Costas 环仿真最常见的失败形式。我每次遇到发散,先看 Kp、Ki 是否有数量级错误,再看积分器是否饱和,最后看输入幅度。环路滤波器系数如果比正确值大一两个数量级,误差信号会推动 NCO 频率字来回摆,波形看起来像正弦振荡而非收敛。用 2.2 节的浮点脚本算一次基准,再对比 RTL 里的定标值,基本能定位。
| 现象 | 可能原因 | 优先排查项 |
|---|---|---|
| 频率字来回大幅摆动 | Kp 过大 | 将 Kp 缩小 10 倍重仿真 |
| 锁定后误差有直流 | Ki 过小或积分器截位 | 增大 Ki,检查积分器位宽 |
| 频率字跑到边界卡住 | 积分器溢出或 NCO 位宽不足 | 加饱和保护,增加位宽 |
| 无任何收敛趋势 | 极性接反或时钟不同源 | 交换 I/Q,检查复位释放时序 |
5.2 加一个锁定检测计数器
与其肉眼盯着波形,不如直接在 RTL 里加一个锁定检测逻辑。它统计误差信号绝对值连续低于阈值的时钟数,超过设定值就置lock=1:
reg [15:0] cnt_lock; wire err_within = (err < LOCK_TH) && (err > -LOCK_TH); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_lock <= 0; lock <= 0; end else if (err_within) begin cnt_lock <= cnt_lock + 1; if (cnt_lock >= LOCK_TIME) lock <= 1; end else begin cnt_lock <= 0; lock <= 0; end endLOCK_TH是误差阈值,按误差信号满量程的 5% 到 10% 设置;LOCK_TIME是连续通过的周期数,一般取几百个符号周期。Lock 信号拉高后,再开始 8B/10B 解码和字节对齐,能避免解码器被未锁定数据的误码污染。同样,Lock 信号也可以接到 ILA 上,上板时直接观察。
5.3 上板验证前先用 ILA
Vivado ILA 调试时,把freq_word、err、lock、解调数据总线加进去,采样深度设成与仿真一致的数量级。注意 ILA 会占用 BRAM,如果采样深度太大,综合时序可能收紧,导致生成比特流失败。如果遇到生成比特流失败,先检查时序约束里有没有为 ILA 添加异步时钟域,或者把 ILA 采样深度调低到 4096,一般能解决。
以上是实际调试中最常用的三个手段。在 Vivado 2019.2 下跑这套 Costas 环,只要先把仿真跑稳,上板调试通常只是重复仿真过程而已。
本文还有配套的精品资源,点击获取