做数字接收机的人,几乎都被“残余频偏”折腾过。前面明明做了粗同步、做了数字下变频,结果星座图上信号还是一圈一圈慢慢转,误码率怎么都压不下去。这时候把 COSTAS 环搬出来,配合 MATLAB 仿真把参数摸清楚,再落到 FPGA 上实现一套真正的闭环补偿,是最常见也最稳妥的做法。这篇文章把我自己做这套流程的思路完整拆给你看:从残余频偏为什么会伤害解调,到 MATLAB 里怎么搭仿真、定系数,再到 FPGA 定点化、RTL 划分和实测调试时踩过的坑,一次说清楚。
1. 残余频偏是什么,COSTAS 环又能补到什么程度
1.1 先算一笔账:载波偏差到底影响多大
接收机里的载波同步通常分两级。第一级是粗同步,靠前导序列、FFT 估频或扫频方式,把接收信号的载波频率偏差拉到一个比较小的范围。比如符号率 1 Msps 的系统,粗同步可能只做到几百赫兹到几千赫兹的精度。别小看这几千赫兹,在星座图上它直接表现为相位持续旋转。
假设残余频偏是 Δf,符号周期是 Ts,那么相邻符号之间的相位旋转量就是 2πΔfTs。如果 Δf = 2 kHz,Ts = 1 μs,那么每个符号就转 0.0126 rad,看上去好像不大。但如果连续看 100 个符号,累积相位偏移已经接近 1.26 rad,这对 QPSK 来说已经是非常严重的相位误差了,解调器基本没法正常工作。
要保证解调性能,通常要求残余频偏导致的相位旋转远小于一个符号判决区间。拿 QPSK 来说,判决门限之间的宽度是 π/2,工程上一般希望稳态相位误差控制在几度以内,不然星座点就会往判决边界靠,噪声稍微一进来就误判。所以粗同步之后,必须再有一级闭环把残余频偏动态地压掉,这就是 COSTAS 环存在的意义。
1.2 抑制载波信号为什么离不开 COSTAS 环
通信系统为了节省发射功率,很多调制方式都会把载波分量抑制掉,比如 BPSK、QPSK、QAM 这类信号,频谱上在载频处没有明显的单音。你拿普通锁相环去做载波恢复,环路会很难锁定,因为锁相环需要输入信号中有一个稳定的载波分量,而抑制载波信号里根本没有这东西。COSTAS 环的精髓在于,它利用基带 I/Q 两路的乘积或判决信息,先把调制信息的影响消掉,再构造出一个和载波相位误差成比例的误差信号,用这个误差信号去驱动 NCO,本质上就是一个针对抑制载波信号设计的锁相环。
比如 BPSK 信号,在理想的载波相位条件下,I 路就是调制数据,Q 路理论上接近零。一旦本地载波和接收信号之间存在相位差θ,Q 路就会冒出和 θ 相关的分量。经典 COSTAS 环把 I 路和 Q 路乘起来,再经过环路滤波去控制本振,利用的就是这个特性。QPSK 和更高阶 QAM 则多用判决反馈式鉴相器,把硬判决结果反馈回来计算相位误差。不管哪种结构,COSTAS 环都不需要外部导频,这在点对点通信、卫星链路、突发接收机里非常实用。这也是为什么很多 FPGA 的调制解调参考设计里,COSTAS 环永远是标配模块。
1.3 接收链路里 COSTAS 环应该放在哪
这个问题在工程里特别容易搞混。COSTAS 环的工作时钟、数据速率要和整个接收链路匹配,通常放在匹配滤波之后、符号判决之前。做 MATLAB 仿真时不用太在意前后级延迟,但在 FPGA 里,匹配滤波、AGC、定时同步和 COSTAS 环的先后关系会直接影响环路的稳定性和收敛速度。
我的习惯是,如果系统是突发模式,先用前导做一次粗频偏估计,把 NCO 的频率字初始化到接近真实频偏的位置,然后 COSTAS 环只在残留频偏很小的情况下工作。这样 COSTAS 环只需要承担“小范围跟踪”的角色,环路带宽可以放窄一点,噪声性能也会更好。如果是连续模式,比如广播式链路,COSTAS 环就要从比较大的频偏范围开始牵引,那环路滤波器的比例项增益就得适当加大,否则锁定时间会非常长。
2. MATLAB 仿真:先把环路系数和收敛行为吃透
2.1 搭一个数字 COSTAS 环模型
我习惯先把整个链路做成符号率的离散模型,不考虑载波频率的高频细节,只关注“相位误差 -> 误差信号 -> 环路滤波 -> NCO 频率字 -> 相位更新”这个闭环。这样既能快速验证算法,又方便后面把每个模块一对一映射到 RTL。
一个典型的数字 COSTAS 环包含四个核心部分:数字混频器、环路滤波器、NCO(数字控制振荡器)、鉴相器。用 MATLAB 写出来,核心逻辑大致是这样:
% 参数设置 Rs = 1e6; % 符号速率 T = 1/Rs; % 符号周期 N = 20000; % 仿真符号数 freq_offset = 1500; % 残余频偏 1.5 kHz theta0 = 0.3; % 初始相位误差 % 环路系数 zeta = 0.707; wn = 2*pi*0.02*Rs; % 自然角频率,按符号率归一化 Kp = 2*zeta*wn*T; % 比例系数 Ki = (wn*T)^2; % 积分系数 % 生成 BPSK 基带信号并模拟频偏 data = 2*randi([0 1], N, 1) - 1; phase_per_sym = 2*pi*freq_offset*T; tx = data .* exp(1j*phase_per_sym*(0:N-1).'); tx = tx .* exp(1j*theta0); % 环路状态 nco_phase = 0; nco_freq = 0; % 频偏估计值,初始为 0 i_out = zeros(N,1); q_out = zeros(N,1); err_out = zeros(N,1); for n = 1:N % NCO 输出当前相位 i_out(n) = real(tx(n) * exp(-1j*nco_phase)); q_out(n) = imag(tx(n) * exp(-1j*nco_phase)); % BPSK 判决型鉴相器:误差正比于 sign(I) * Q err = sign(i_out(n)) * q_out(n); err_out(n) = err; % 环路滤波器(比例+积分) nco_freq = nco_freq + Ki*err; % 积分支路 loop_out = nco_freq + Kp*err; % 比例+积分 % NCO 相位更新,同时把固定频偏和修正量累加进去 nco_phase = nco_phase + 2*pi*freq_offset*T + loop_out; end这里我故意把 NCO 的初始频率设为 0,让环路自己去跟踪 1.5 kHz 频偏。实际系统里,粗同步已经把频偏估计值写进 NCO 频率字了,所以残余频偏比这个小得多。
2.2 环路滤波器的系数到底怎么算
环路滤波器的系数整定是整个仿真的核心。很多人直接在 MATLAB 里试 Kp、Ki,试出来波形好看就完事,但换一组频偏、换一个信噪比就锁不住,问题就出在系数没有理论支撑。
二阶环路最常用的参数化方式是通过自然角频率ωn 和阻尼系数ζ 来设计。数字环路滤波器的差分方程通常写成:
err_accum = err_accum + KiFilter * err; loop_output = err_accum + KpFilter * err;
这里的 KiFilter 和 KpFilter 与 ωn、ζ 的关系近似为:
KpFilter ≈ 2ζωnT KiFilter ≈ (ωnT)²
其中 T 是环路更新时间,也就是符号周期。为了让环路既有足够的捕获范围,又不会把太多噪声带进相位估计,ζ 我一般取 0.707,这个时候环路在锁定速度和超调量之间最均衡。ωn 的选取则要看环路噪声带宽 Bn,对于二阶环有近似关系:
Bn ≈ (ωn / 2) * (ζ + 1/(4ζ))
如果 ζ = 0.707,那么 Bn ≈ 0.53ωn。换句话说,想要更小的等效噪声带宽,就要把 ωn 压低,但 ωn 太低会导致锁定时间变长。具体选多少,取决于系统允许的前导长度和信噪比。
我在写 FPGA 之前,通常会在 MATLAB 里扫一组 Bn 从 0.001 Rs 到 0.1 Rs 的参数,观察相同信噪比下的收敛时间和稳态抖动。比如符号率 1 Msps,选择 ωn = 2π×20 kHz 时,环路能在几百个符号内拉到稳态;如果信道条件很差,我再把环路带宽降下来,宁可多花一点时间锁定,也不能让相位抖动把误码抬高。
2.3 频偏校正的仿真结果怎么判断
判断 COSTAS 环有没有生效,不要只看误码率。我习惯看三个指标:一是误差信号erro 的收敛趋势,二是 NCO 频率字是否收敛到真实频偏,三是恢复出来的星座图是否稳定不再旋转。
前面那个 MATLAB 模型里,err_out 初始阶段会有一个明显的脉冲式收敛过程,随后在零附近随机波动。把 err_out 滤波后求均值,你能直观看到环路在什么时候进入稳态。NCO 频率字收敛到 2π×1500×T 附近,说明环路已经“找到”了真实频偏,这也验证了整条反馈环路的极性没有问题。
星座图最容易看出问题。如果频偏没补偿干净,星座图上所有点会围着一个圆环逆时针或顺时针旋转;补偿到位后,BPSK 的 I 路会聚到 ±1,Q 路围绕零小幅波动。QPSK 的话则看到四个清晰的点簇。如果星座图上出现两个点簇各带一圈“拖尾”,通常说明残余频偏比较大,而且环路带宽不够宽,牵引速度跟不上。
2.4 浮点仿真里最容易被忽略的三件事
第一件事是鉴相器工作在符号率下的非线性。COSTAS 环的鉴相器不是真正线性的,特别是在相位误差比较大的时候,鉴相增益会明显下降,甚至出现符号翻转。很多人直接把小误差假设下的增益代入环路系数计算,导致实际捕获范围比设计值小很多。我建议仿真时人为加大初始频偏和相位误差,验证环路在最恶劣情况下能不能正常拉回。
第二件事是 NCO 的相位表达方式。MATLAB 浮点模型里的相位可以无限累加,但 FPGA 里的相位累加器是有位宽限制的,默认是自动回绕。仿真时如果用了 unwrap 或连续累加,后面做定点模型时很容易出现“MATLAB 能收敛、FPGA 不收敛”的诡异现象。
第三件事是不要把频偏固定成一个单一值。真实信道里频偏往往是缓慢变化的,仿真时除了加固定频偏,还应该加一个低带宽的随机游走频偏,或者用扫频的方式从正频偏扫到负频偏,这样才能验证环路的动态跟踪能力,而不只是静态收敛能力。
3. FPGA 定点实现:从浮点到 RTL,麻烦才刚刚开始
3.1 定点化要从数据通路和信号尺度两头考虑
MATLAB 里用 double 算得眉飞色舞,一落 FPGA 就得面对位宽、符号位、溢出、截断误差这些问题。定点化不是简单地把 double 换成 fixed point,而是要先把整个环路的信号尺度定下来。
数据通路的尺度通常由前端 AGC 或自动增益控制决定。为了充分利用 FPGA 里的乘法器和加法器位宽,匹配滤波器输出一般归一化到 ±1 附近,在定点里用 Q1.15 或 Q1.17 这类格式表示。I/Q 两路进入鉴相器之前,位宽不要随意截断,我一般至少保留 16 bit。如果后面还有均衡器,数据位宽要适当放宽到 18 bit 或 20 bit,否则均衡器带来的增益起伏很容易导致量化噪声成为环路的主导噪声。
NCO 的相位累加器位宽建议直接给到 32 bit。NCO 输出相位经过截断后查表,比如只取高 12 bit 或者 16 bit 作为 ROM 地址,相位截断带来的杂散会因为相位累加器位宽足够大而被分散到很低的电平。环路滤波器的输出在进入 NCO 之前,要注意和控制字的尺度匹配,否则一个很小的量化步进都可能让环路产生极限环振荡。
3.2 环路滤波器定点化:移位近似比乘法器更省资源
环路滤波器的 Kp 和 Ki 在浮点仿真里通常是非常小的小数,比如 Ki=1.6e-4,Kp=8.9e-3。FPGA 里直接做乘法当然可以,但如果你在意资源占用,更喜欢用移位加法和乘法相结合的方式去近似系数。
比如把 Kp=8.9e-3 近似为 1/64 + 1/256。1/64 是 0.015625,1/256 是 0.00390625,加起来是 0.01953125,误差不小。所以我会先用一个小数定点化工具把 Kp、Ki 转成定点整数,再观察化成几个移位项相加能不能满足性能。移位近似的好处是节省 DSP48,坏处是系数精度可能不够,特别在 Ki 非常小的时候,一旦量化后变成 0,就等于把环路积分支路废掉了,环路永远锁不到零稳态误差。这里一定要做定点闭环仿真验证,不能只看单个系数的量化误差。
环路滤波器的积分器还有一个非常致命的问题:积分饱和。当频偏很大、环路还没收敛时,积分器输出会不断累加,最后顶到寄存器的最大值。等真实频偏变小需要积分器反向回调时,它还在饱和区“嗡”半天,环路锁定时间被严重拉长。FPGA 里一定要给积分器和 NCO 频率字加饱和限幅,而不是让它自然溢出回绕。
3.3 NCO/DDS 实现:频率字、相位累加器和 ROM 表
FPGA 里做 NCO 的常规做法是相位累加器加查找表。累加器位宽设为 32 bit,每来一个时钟,累加器输出等于当前值加上频率控制字。频率控制字和输出频率的关系是:
f_out = fcw × Fs / 2^32
其中 Fs 是 NCO 的采样时钟频率,fcw 是频率控制字。如果 COSTAS 环工作在符号率上,Fs 就等于符号速率;如果环路工作在过采样数据上,Fs 就要按实际采样率算。
比如系统采样时钟是 100 MHz,符号率是 1 Msps,COSTAS 环若放在符号率后端,NCO 更新频率就是 1 MHz。设计时要把两个频率分开看:NCO 的相位累加器是被过采样时钟驱动,还是被符号率使能驱动。很多初学者把系统主时钟当 NCO 更新时钟,环路滤波器却按符号率计算,导致环路系数差 100 倍,当然锁不住。
相位到正余弦的映射,建议用两个 ROM 分别存 sin 和 cos。查表地址取 12 bit 够用,如果系统对杂散要求高,可以取 16 bit。ROM 深度和位宽的折中要结合具体 FPGA 芯片,Xilinx 的 Block RAM 和 LUTROM 都可以,但尽可能把整个查找表放进一个 BRAM 里,这样时序容易收敛。混合器乘法建议直接使用 DSP48,并把乘法结果寄存一拍,减少组合逻辑路径长度。
3.4 RTL 模块划分与时序处理
我习惯把 COSTAS 环拆成四个模块:mixer、phase_det、loop_filter、nco。顶层用状态机或者简单的 valid 信号驱动模块间数据流。如果整个接收链路是流水线结构,每个模块的输入输出都要有一组同步的 valid,不能让某个模块独自跑在自由时钟下。
mixer 模块里的复乘,两个乘法器加一个加减法,输出位宽要保留足够余量,否则 I/Q 路一旦幅度超过 ±1,符号信息就会损坏。phase_det 模块根据调制方式选择鉴相器,BPSK 用 sign(I)×Q,QPSK 常用硬判决反馈的方式。如果接收信号是 QPSK,比较经典的误差公式是:
err = sign(I) × Q – sign(Q) × I
注意符号极性可能因为 NCO 输出的 cos/sin 定义方向不同而变成加号。我在第一次写 RTL 时经常把极性搞反,最后看到环路输出频率往反方向跑,才意识到这个符号问题。为了避免反复试错,建议在 MATLAB 模型里就把 NCO 定义为和 RTL 完全一致的结构,然后让 MATLAB 浮点模型和定点模型跑同一组测试向量,输出对比一致后再开始写 RTL。
时序上要特别注意环路滤波器的反馈延迟。COSTAS 环是闭环系统,每多一级流水线寄存,就相当于在环路里多插入一个单位延迟,这会降低相位裕度。如果必须为了时序插入流水线,那么要在 MATLAB 模型里把同样的延迟补偿进去仿真,不能简单靠 RTL 实现后再慢慢调。
4. MATLAB 与 FPGA 联调过程中的常见问题
4.1 现象一:单片机测试时环路就是锁不住
这个问题我遇到很多次,最后排查下来,绝大部分原因不是 COSTAS 算法本身,而是 NCO 频率字的连接错了。顶层模块里粗同步模块和 COSTAS 环共用一个 NCO,或者初始频率字应该由上位机配置,结果 COSTAS 环的环路滤波器输出覆盖了配置值,导致 NCO 频率跳变超出环路捕获范围。
处理办法是先把 COSTAS 环从数据通路中断开,给 NCO 一个固定的频率字,人为加一个很接近该频率的测试信号,看混频后的 I/Q 输出是否平稳。这个测试能确认 NCO 和混合器的基本功能。再给一个固定的斜率信号模拟频偏变化,观察环路滤波器输出是否跟随而变化。
4.2 现象二:Lock 灯亮了,星座图还在缓慢旋转
Lock 检测通常只是判断误差信号的平均值变小,但它无法精确反映残余频偏已经压到多小。星座图还在缓慢旋转,说明稳态相位误差不为零。问题一般出在积分支路没有真正工作,或者 Ki 系数被量化成了 0。检查环路滤波器的定点仿真结果,看一下积分器的输出是不是一直顶在限幅值附近。还有一种可能是所有模块都正常,但 NCO 的频率控制字是在相位累加器打拍之后才被采样,导致环路更新时刻和相位推进之间有一个固定延迟。这个延迟在低信噪比下不会造成严重问题,但当你试图把残余频偏压到几十赫兹以内时,它的影响就变得不可忽略了。
4.3 现象三:环路自身发生振荡或失锁
环路自身振荡通常表现为误差信号不是收敛到零,而是等幅振荡,或者星座图在两个位置间反复跳动。原因可能有几个:Kp 太大导致相位裕度不足;环路滤波器积分器饱和限幅没有做,积分器溢出回绕;或者是符号率检测使能逻辑和自己仿真的时序不一致,每来一个时钟都执行积分,而不是每个符号只执行一次。后面这类问题在 MATLAB 仿真里根本发现不了,因为仿真模型天然是符号率更新的,FPGA 里如果 valid 信号拉错,积分器一直在累加,环路带宽实际被放大了好几倍。
4.4 问题排查时的检查表
下面这张表是我在做 COSTAS 环联调时必看的排查清单,每出现一个异常就直接按表定位,比自己瞎猜快得多。
| 检查项 | 正常情况 | 异常可能原因 |
|---|---|---|
| NCO 初始频率字 | 接近粗同步估计频偏 | 配置位序错误、寄存器未刷新 |
| NCO 相位累加器位宽 | 32 bit 稳定回绕 | 位宽被综合器优化,导致相位跳跃 |
| 鉴相器极性 | 正频偏时输出为正 | cos/sin 定义方向和误差公式不匹配 |
| 环路滤波器 Ki | 量化后不为 0 | 系数位宽不足或移位近似过度 |
| 积分器限幅 | 输出始终在限幅范围内 | 限幅未做或限幅门限过大 |
| 符号率 valid | 每个符号一个脉冲 | valid 被拉高多个周期,环路带宽异常 |
| 乘法器输出位宽 | 数据范围无溢出 | 信号尺度没对准,出现截断 |
| I/Q 两路延迟 | 两路对齐 | 一路多了一级寄存器,相位偏差始终存在 |
4.5 实际联调时我的调试顺序
我一般先在 MATLAB 里做定点模型和 RTL 的协同仿真。找一个固定的测试向量,分别从 MATLAB float 模型、MATLAB fixed 模型、RTL testbench 跑出结果,做波形对比。三者不一致的地方就是问题所在。最理想的情况是在 RTL 仿真阶段把所有问题解决掉,上板之后只做随机数据和误码率的验证,而不是拿板子去查环路问题。
上板调试时,优先看 NCO 的频率控制字和环路滤波器的中间信号。这两个信号能直接反映环路是在收紧还是在发散。用逻辑分析仪抓一段足够长的波形,不要只抓几百个时钟,COSTAS 环的收敛过程可能持续几千个符号周期。抓到之后和 MATLAB 定点模型的波形叠加对比,如果趋势一致,说明 RTL 实现正确,剩下就是信噪比和系数微调的问题。
最后再分享一个我自己的习惯:永远在 RTL 里把环路滤波器输出、NCO 输出相位高几位、鉴相器误差值这四五个信号通过调试接口引出,用固定帧格式做成观测寄存器。平时不用在意这点资源开销,一旦现场出了幺蛾子,你就能第一时间看到环路内部状态。很多时候问题出在 AGC 增益、匹配滤波器群延迟,甚至前一级的直流偏置上,COSTAS 环只是一个“受害者”。能直接看到环路内部状态,能帮你省下大量拿示波器猜来猜去的时间。