
简介面向光通信、卫星导航及数字信号处理的研究者和工程师这份资源聚焦CAP调制系统中的载波相位估计难题提供基于Viterbi-Viterbi算法的MATLAB实现CPE.m。Viterbi-Viterbi算法将动态规划思想从离散码字空间扩展到连续相位空间通过建立载波相位状态转移模型对每个时刻计算接收信号与无相位误差理想信号之间的距离代价再经状态路径跟踪、回溯得到最可能的相位序列最终实现相位补偿并提升解调质量相比传统方法在抗噪声和相位跟踪鲁棒性上更有优势。包内仅含1个m脚本文件压缩包大小约899B代码体量精简但覆盖观察模型、距离计算、动态规划、回溯与相位补偿等完整步骤适合逐行研读和二次开发。已有2143人学习下载特别适合通信算法工程师、光通信方向研究生以及数字信号处理学习者作为原理验证与教学参考借助该脚本可快速掌握载波相位估计的实现链路也可为课程设计或工程预研提供可运行的算法基底。1. 从一堆星座点的旋转说起Viterbi-Viterbi到底在解决什么问题干过突发通信接收的人都有这种经历符号同步做完了定时恢复也正常星座图搓出来却是“一锅粥”——明明该落在 QPSK 四个标准点上的符号变成一个旋转了任意角度的散点云而且这个角度每几百个符号还会飘一下。这就是载波相位不确定。Viterbi-Viterbi 算法一般简写成 VV是解决这个问题的经典非数据辅助方法接收端不需要知道发送的比特只利用符号本身的高阶统计特性把未知相位从调制信息里剥出来。它尤其适合突发帧、符号数不多、希望一次估出整段相位的场景。这个算法的核心思想一句话就能说清对每个接收符号做 m 次方把调制相位“打回原形”然后在块内累加取平均最后从累加值的角度里除以 m得到载波相位估计。听起来简单实际工程里牵涉到块长怎么选、相位模糊怎么消、频偏低信噪比怎么防跳变每一步都能翻车。下面先讲清原理再给可复现的代码和避坑清单。适合正在做 QPSK/8PSK 相干解调、卫星或光纤接收机、或者用软件无线电做突发协议的同学。2. 抛开公式看原理m次方去调制与相位旋转的算术关系2.1 为什么是m次方而不是直接算角度假设发送端在 QPSK 里选了四个相位之一比如 φ ∈ {π/4, 3π/4, 5π/4, 7π/4}。接收到的符号带一个未知载波相位 θ 和加性噪声 n可以写成 r_k a_k · e^{jθ} n_k。如果直接对 r_k 求平均角度四个不同调制相位会在圆周上互相抵消平均结果和 θ 没有任何稳定关系。常见做法是“去掉调制”。怎么去利用 PSK 星座的旋转对称性任意一个合法信号点 a_k 的 m 次方 a_k^m 都等于同一个常量 C对于恒模信号就是常量。于是 r_k^m a_k^m · e^{jmθ} 加上噪声交叉项再对块内累加载波相位信息就集中到 e^{jmθ} 这一项上。为什么不直接对幅度做归一化再取角度归一化只能解决幅度起伏去不掉调制相位的多值性。m 次方是一种非线性映射等价于把圆周上的 m 个星座点“折到一起”。QPSK 的 m 就是 48PSK 的 m 就是 8。这个 m 必须和 PSK 的星座对称数一致否则折不到同一角度累加时会打散。2.2 Viterbi-Viterbi的相位提取流程四次方、累加、取相位、除以4先看数学形式。对块内 N 个符号计算 Z (1/N) ∑ r_k^m然后载波相位估计值 θ̂ (1/m) · arg(Z)。这里 arg(Z) 取主值区间 [-π, π)除法后 θ̂ 只落在 [-π/m, π/m) 范围内所以天然有 2π/m 的模糊周期。QPSK 下 m4估计范围是 [-π/4, π/4)真实相位可能差 π/2 的整数倍。流程拆开是四步第一步对每个符号做复数 m 次方第二步同一段块内累加块长选多少后面会讲第三步对累加结果求反正切角 arg(Z)第四步把角度除以 m得到相位估计。整个过程是开环的不依赖判决。这一点让它的并行度高、适合突发帧不像判决引导算法要等符号判决收敛。2.3 和判决引导算法的差别为什么V-V适合突发判决引导相位估计是先对符号做粗判决再用判决误差去纠正相位类似锁相环。这种算法在连续长流里性能好但突发通信前导很短锁相环来不及收敛。VV 算法最大的好处是一次块内同步估计不需要反馈迭代。假设一个时隙只有 100 个符号VV 可以把 100 个符号作为一个块一次性估出相位再接判决。另一个差别是判决引导算法在低信噪比下容易错误判决错误判决会带着相位估计跑偏形成误码传播。VV 不依赖判决噪声的影响通过累加平均被压低了所以低信噪比下反而稳。当然 VV 也有代价需要对信号做非线性 m 次方这个操作会放大噪声尤其高阶调制的时候。所以它更适合恒包络 PSKQAM 上要用往往得先做幅度归一化或改成 QPSK 分裂处理。下一章直接给最简复现。3. 用Python实现Viterbi-Viterbi从符号序列到相位估计3.1 最小复现代码对QPSK符号流做载波相位估计先写一个最小可跑的函数。假设你已经有定时恢复后的复数符号序列符号率为 1不考虑频偏只估一个常数相位。这里用 NumPy 操作复数数组避免手写循环。import numpy as np def vv_phase_estimate(rx, m4): 对整段符号序列估计一个载波相位。 参数 ---- rx : np.ndarray 复数符号序列长度大于0。 m : int PSK星座阶数对应的幂次QPSK为48PSK为8。 返回 ---- phase : float 估计的载波相位范围在(-pi/m, pi/m]之间。 # 对每个符号取m次方调制相位折叠到一处 z np.sum(rx ** m) # 累加后取角度再除以m得到载波相位 phase np.angle(z) / m return phase代码逻辑rx ** m在 NumPy 里对复数广播求幂等价于每个元素做复数乘方。累加用np.sum。np.angle返回主值区间 (-π, π]除以 m 后相位区间被压缩。参数 m 必须匹配调制阶数QPSK 用 48PSK 用 8BPSK 用 2 也可以。实际使用时常会遇到相位跳变下面会讲原因。3.2 关键参数怎么设块长N、m次幂、相位归一化上面是整段估计。突发场景往往需要分块因为相位随时间漂移。分块估计需要指定块长 N。常见经验值QPSK 下 N 取 64256 之间。信噪比越低N 要越大否则累加平均压不住噪声但 N 太大块内相位变化可能不能忽略估计出来是平均值反而劣化。表里给了参考参数QPSK 常用值说明m4与调制阶数一致块长 N64512受信噪比和相位漂移双重约束处理方式重叠滑窗每步前进 N/2 避免边缘突变相位归一化也是个坑。np.angle/ m 得到的是 (-π/m, π/m)这个值本身不足以还原真实相位需要后续解模糊。另外rx ** m的算法在浮点精度上没问题但要注意输入符号若有直流偏置会被 m 次方放大。所以输入序列应该预先做直流消除和幅度归一化。3.3 从相位估计到相位校正旋转信号点估计出相位后校正就是复数旋转。注意旋转方向接收信号 r s·e^{jθ}要乘 e^{-jθ̂} 才能转回。def vv_correct(rx, phase): 用估计相位校正接收符号。 return rx * np.exp(-1j * phase)这一句背后有工程含义如果 phase 是一个数组每块一个估计值则校正时要把 phase 插值到每个符号上否则块边界会出现相位台阶。示例代码如下def vv_block_est(rx, m, block_len): 分块估计相位每块一个估计值。 n len(rx) n_blocks n // block_len phases np.zeros(n_blocks) for i in range(n_blocks): block rx[i*block_len:(i1)*block_len] z np.sum(block ** m) phases[i] np.angle(z) / m return phases实际接收时块与块之间相位不连续直接分段校正会让星座图在边界产生调制痕迹。所以要么用重合滑窗要么对 phase 数组做平滑插值。下一章讲相位模糊那块解决不好前面所有工作等于白做。4. 相位模糊与解模糊Viterbi-Viterbi的最后一公里4.1 相位模糊怎么来的除以m产生的周期性因为 arg(Z) 的主值范围是 (-π, π]除以 m 以后被估出的载波相位只有模 2π/m 的区间。真实载波相位 θ 其实可以是 θ̂ 2πl/ml 是整数。QPSK 下四个候选相位相差 π/2解错的话星座图虽然看起来“整齐”了但每个符号可能整体旋转 π/2、π、3π/2映射到比特就全是错的。我见过不少同学仿真星座图转正了误码率却接近 0.5就是这个原因。这个模糊是算法本性不是实现 bug。任何基于 m 次方的非数据辅助算法都带这类周期性模糊。解决办法要么用导频把模敲定要么发送端做差分编码接收端做差分判决。4.2 基于导频或差分编码解模糊导频方案好理解在每个突发帧开头插入一段已知符号接收端在相位校正后和已知导频做相关看旋转了哪个 m 分之一把整个帧的估计相位补上。导频开销通常几个到十几个符号。差分编码方案不需要额外占开销但误码性能会损失约 0.5 dB。工程上更常见的是差分尤其在突发短帧里为省开销宁愿接受一点性能损失。实现的时候有一个关键点解模糊要在相位校正之后、判决之前做。你先用 VV 估计一个“模糊相位” θ̂把星座粗转正再用导频符号判断剩余旋转量 δ 2πl/m然后对全帧最终做补偿。顺序不要颠倒否则导频相关会被剩余的较大相位误差干扰。def resolve_ambiguity(pilot_rx, pilot_tx, phase_rough, m): pilot_rx: 接收端经过粗校正的导频符号 pilot_tx: 本地已知导频 phase_rough: VV粗估相位已用于将pilot_rx粗校正 # 粗校正后再对导频求误差 z np.sum(pilot_rx * np.conj(pilot_tx)) # 误差角度 delta np.angle(z) # 量化到最近的2*pi/m倍数 l round(delta * m / (2 * np.pi)) refine l * (2 * np.pi / m) return phase_rough refine这段代码演示了量化倍数的思路先粗校正再用导频算残差残差除以步进的四舍五入作为模糊阶数 l最后把真实相位定为 phase_rough l·(2π/m)。注意 delta 也要先除以 m实际上这里 delta 已经包含的是未除以 m 的残余因为相位估计和校正已经做过了。如果直接用符号乘共轭相位误差就是残余旋转直接比较步进。逻辑需要自行确认但思路如此。参数 m 不变。4.3 连续信号的滑窗与重叠处理如果信号不是一个一个突发而是连续的长包VV 分块估计出来的相位序列会有块间台阶。常见做法是滑窗重叠每步前进 block_len/2用重叠区把相邻块的相位差值限制在小范围内。也可以对每块估计值做一阶滤波比如 phase_smooth[i] α·phase_est[i] (1-α)·phase_smooth[i-1]α 取 0.3~0.5。但要注意相位本身存在 2π 跨越滤波前要先做相位展开。工程上我会先把每块估计值按相邻差不超过 π/m 的规则展开防止跳变。还有一种做法是把 VV 估计值当锁相环的“引导”粗估值纠正整数旋转然后锁相环只负责细小相位跟踪。这样既继承了 VV 的快速捕获又利用锁相环消除块间抖动。下一章的避坑列表里有一项和这种场景直接相关。5. Viterbi-Viterbi避坑从翻车现场提炼的5个排查点5.1 现象星座图转正了但误码率不降反升现象是星座图看起来四簇分明和标准 QPSK 的位置完全重合但解出来的比特全部不对误码率约等于 0.5。这就是相位模糊没处理。原因在于 VV 估计出的 θ̂ 有四个可能候选星座图无论选哪个候选都能转正但比特映射差个 π/2 的整数倍。解决方法是第四章说的导频或差分编码。我一般习惯先做一个开源仿真验证用同一段数据对四个候选相位分别解调看误码率是否只有一个候选接近 0。如果四个都差那是信道条件问题不是模糊。5.2 现象块长越大估计越不准有人想当然认为VV 是累加平均块长越大噪声越小于是把 N 从 64 调到 1024结果误码率反而上升。原因很简单VV 算法隐含假设块内载波相位是常数如果发送端和接收端存在远远大于 1% 符号速率的频偏相位在块内一直在转对 r_k^m 求和时e^{jmθ} 里 θ 随时间线性变化累加项互相抵消估计值会被拉扁。解决办法是先粗略做频偏估计把残余频偏压到很小再用 VV或者缩短块长。应急排错办法是输出每块估计相位的差分序列看相邻差值是否稳定在一个常数如果稳定但常数较大就是频偏。5.3 现象低信噪比下估计跳变另一个常见现象信噪比低于一定门限时星座图时而正时而差 45 度误码率突变。原因是 m 次方把噪声放大当累加结果 Z 的模长无法保证其角度可信时噪声会直接把 arg(Z) 推偏甚至越过相位判定的边界。解决方向有三个增大块长、在取相位前对累加结果做模长加权、或者用多个重叠块的估计做中值滤波。我自己的习惯是先用仿真画出“估计相位误差与信噪比关系”的曲线观察误差超过 10 度的拐点在哪再决定要不要加反馈式处理。5.4 现象算法在QAM16上失效如果换到 QAM16仍按 m4 直接四次方求和估计结果会严重偏差。原因是 QAM16 的星座不是恒模符号幅度有 3 个等级四次方后的幅度被调制信息干扰直接累加会把幅度小的符号拖累。常见做法是先对每个符号做幅度归一化即 r_k/|r_k|把它当作 PSK 用 VV 估一个粗相位再用估计结果校正后判决最后用判决符号做精细相位调整。或者把这部分符号拆分只利用幅度接近最大的一圈符号做 VV抗噪会好点但效率低。记住标题算法的主战场是 PSKQAM 场景要改造。5.5 现象估计相位偏一个固定角度有同学发现仿真里估计值和真实相位始终差一个固定角度比如总是差 0.3 弧度而且与噪声无关。这种情况往往不是算法而是坐标系方向定义问题。有人用 arctan 函数没注意象限复数的实部虚部反了有人用np.angle返回值正确但旋转校正时本应乘 e^{-jθ}乘成了 e^{jθ}。排查方法很简单发送端生成一个已知相位 θ_true把接收相位校正好后打印 θ̂ - θ_true 对信噪比的变化曲线。如果差值是固定偏置先检查旋转方向和复数运算的符号约定再检查符号是否经过了匹配滤波引入固定时延。固定偏置一般可以直接用导频估计补偿掉。6. 让Viterbi-Viterbi用得更顺联合频偏补偿与计算技巧6.1 频偏预消除或滑动窗有频偏时与其缩短块长对抗不如先估频偏。工程做法是在 VV 之外用两次分块相位估计做差分频率估计第一次对前半段块估相位 θ1第二次对后半段块估相位 θ2频偏近似为 (θ2 − θ1) / (块间隔时间)。把这个频偏从接收信号里去掉后再对整帧做一次 VV相位估计精度会明显提升。若是低信噪比可以用 3~5 个重叠块的相位拟合一条直线再用相位斜率补偿频偏。这个方法开销不大适合突发帧。6.2 用查找表避免atan2的循环依赖在很多实时处理平台里atan2是流水线延迟较大的函数。如果每个块都调用一次短帧场景下可以接受但连续流高频工作时查找表更快。做法是把复数 Z 的实部和虚部分别缩放成索引查一个预先生成的相位表。比如把实部、虚部按 3 位量化成 8×8 的网格取 256 项表角度量化误差在 2π/64 以内足够给 VV 用。注意查找表要处理虚部为零和象限翻转否则相位看起来是连续的实际跳 180 度还察觉不到。6.3 验证估计质量的仿真方法最后给一个验证习惯。我会在仿真里把 VV 估计值和真实相位做差画误差直方图。误差标准差应该大体随块长 N 的平方根下降。如果没下降先看是否有相位模糊未解再看是否有频偏残留。这个习惯能帮我快速判断算法实现的对不对。验证代码里还可以故意加上相位模糊故意去掉导频确认解模糊模块是真的在工作。整个 VV 链路调通后测一下不同信噪比的误码率曲线和理论曲线做对比。如果 1 dB 内能对上就说明这套实现基本没有性能损失。这是我每次搭载波同步都会走一遍的流程。从最开始的“符号四次方累加取角度”到后面处理频偏、解模糊每一步都有对应的坑。尤其是相位模糊不撞一次墙很难记得住。后来我把解模糊和相位展开写成了固定的工具函数每次新项目直接调用省了很多返工。希望这篇笔记能帮你把 VV 算法一次调通少走我当初绕过的弯路。本文还有配套的精品资源点击获取