相干解调需要接收端产生一个与发送载波同频同相的本地振荡。实际中,收发两端晶振存在频差和相差,多普勒效应也会引入频偏。载波同步的任务就是估计并补偿这些偏差。
设接收信号:r(t) = s(t)·exp(j(2πΔf·t + Δφ))
Costas环是最广泛使用的载波同步方案之一,适用于BPSK/QPSK信号:
接收信号 r(t) = d(t)·cos(2πf_ct + θ),d(t) = ±1
当Δθ→0时,I路输出=±d(t),Q路→0,锁定!
对于QPSK,鉴相器特性不同:
这个鉴相器在±π/4范围内具有线性特性,适用于QPSK的4重相位模糊。
// costas_loop.v - Costas环载波同步器
// 第06课:载波同步
module costas_loop #(
parameter SAMPLE_W = 12, // ADC采样位宽
parameter PHASE_W = 16, // NCO相位位宽
parameter FREQ_W = 16, // 频率控制字位宽
parameter FILTER_W = 24, // 环路滤波器位宽
parameter LOOP_BW = 16'sd128 // 环路带宽参数
)(
input wire clk,
input wire rst_n,
// 接收采样输入
input wire signed [SAMPLE_W-1:0] adc_i, // I路ADC采样
input wire signed [SAMPLE_W-1:0] adc_q, // Q路ADC采样
input wire sample_valid,
// 解调输出
output wire signed [SAMPLE_W-1:0] demod_i, // 解调I路
output wire signed [SAMPLE_W-1:0] demod_q, // 解调Q路
output wire demod_valid,
// 载波同步状态
output wire carrier_lock,
output wire signed [PHASE_W-1:0] phase_error, // 相位误差
output wire signed [FREQ_W-1:0] freq_offset // 频偏估计
);
// ============================================================
// NCO (本地振荡器)
// ============================================================
reg signed [PHASE_W-1:0] nco_phase;
reg signed [FREQ_W-1:0] nco_freq; // 频率控制字 = 标称值 + 频偏修正
// NCO输出
wire signed [SAMPLE_W-1:0] nco_cos;
wire signed [SAMPLE_W-1:0] nco_sin;
// 简化余弦/正弦查找表
reg signed [SAMPLE_W-1:0] cos_table [0:2**PHASE_W-1];
reg signed [SAMPLE_W-1:0] sin_table [0:2**PHASE_W-1];
initial begin
integer i;
for (i = 0; i < 2**PHASE_W; i = i + 1) begin
cos_table[i] = $rtoi((2**(SAMPLE_W-1) - 1) *
$cos(2.0 * 3.14159265 * i / 2**PHASE_W));
sin_table[i] = $rtoi((2**(SAMPLE_W-1) - 1) *
$sin(2.0 * 3.14159265 * i / 2**PHASE_W));
end
end
// NCO相位累加
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
nco_phase <= 0;
nco_freq <= 16'h1000; // 标称载波频率控制字
end else if (sample_valid) begin
nco_phase <= nco_phase + nco_freq;
end
end
assign nco_cos = cos_table[nco_phase];
assign nco_sin = sin_table[nco_phase];
// ============================================================
// 下变频 (混频器)
// ============================================================
reg signed [2*SAMPLE_W:0] mix_i, mix_q;
always @(posedge clk) begin
if (sample_valid) begin
mix_i <= adc_i * nco_cos + adc_q * nco_sin; // I路
mix_q <= -adc_i * nco_sin + adc_q * nco_cos; // Q路
end
end
// ============================================================
// 低通滤波器 (简化:累加平均)
// ============================================================
localparam LPF_LEN = 8;
reg signed [2*SAMPLE_W+$clog2(LPF_LEN):0] lpf_i_acc, lpf_q_acc;
reg [$clog2(LPF_LEN)-1:0] lpf_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
lpf_i_acc <= 0;
lpf_q_acc <= 0;
lpf_cnt <= 0;
end else if (sample_valid) begin
if (lpf_cnt == 0) begin
lpf_i_acc <= mix_i >>> SAMPLE_W;
lpf_q_acc <= mix_q >>> SAMPLE_W;
end else begin
lpf_i_acc <= lpf_i_acc + (mix_i >>> SAMPLE_W);
lpf_q_acc <= lpf_q_acc + (mix_q >>> SAMPLE_W);
end
lpf_cnt <= lpf_cnt + 1'b1;
end
end
assign demod_i = lpf_i_acc[SAMPLE_W+$clog2(LPF_LEN)-1:$clog2(LPF_LEN)];
assign demod_q = lpf_q_acc[SAMPLE_W+$clog2(LPF_LEN)-1:$clog2(LPF_LEN)];
assign demod_valid = sample_valid && (lpf_cnt == LPF_LEN - 1);
// ============================================================
// 鉴相器 (QPSK四相鉴相器)
// e = sign(I)*Q - sign(Q)*I
// ============================================================
reg signed [SAMPLE_W:0] phase_err;
always @(*) begin
phase_err = (demod_i[SAMPLE_W-1] ? -demod_q : demod_q) -
(demod_q[SAMPLE_W-1] ? -demod_i : demod_i);
end
assign phase_error = phase_err[SAMPLE_W-1:0];
// ============================================================
// 环路滤波器 (二阶)
// y[n] = y[n-1] + K2*e[n] + K1*(e[n]-e[n-1])
// ============================================================
reg signed [FILTER_W-1:0] lf_integrator; // 积分项
reg signed [FILTER_W-1:0] lf_prev_error; // 上一次误差
reg signed [FILTER_W-1:0] lf_output; // 滤波器输出
// 环路增益参数
localparam signed [FILTER_W-1:0] K1 = 24'sd256; // 比例增益
localparam signed [FILTER_W-1:0] K2 = 24'sd8; // 积分增益
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
lf_integrator <= 0;
lf_prev_error <= 0;
lf_output <= 0;
end else if (demod_valid) begin
lf_integrator <= lf_integrator + (K2 * phase_err) >>> (FILTER_W/2);
lf_output <= lf_integrator +
((K1 * (phase_err - lf_prev_error)) >>> (FILTER_W/2));
lf_prev_error <= phase_err;
end
end
assign freq_offset = lf_output[FREQ_W-1:0];
// 更新NCO频率
always @(posedge clk) begin
if (demod_valid)
nco_freq <= 16'h1000 + lf_output[FREQ_W-1:0];
end
// ============================================================
// 锁定检测
// ============================================================
reg [7:0] lock_counter;
reg is_locked;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
lock_counter <= 0;
is_locked <= 1'b0;
end else if (demod_valid) begin
if (phase_err < 24'sd128 && phase_err > -24'sd128) begin
lock_counter <= (lock_counter < 255) ? lock_counter + 1'b1 : 255;
end else begin
lock_counter <= (lock_counter > 0) ? lock_counter - 1'b1 : 0;
end
is_locked <= (lock_counter > 200);
end
end
assign carrier_lock = is_locked;
endmodule
// ============================================================
// 数字锁相环 (DPLL) 通用模块
// ============================================================
module dpll #(
parameter PHASE_W = 16,
parameter FREQ_W = 16
)(
input wire clk,
input wire rst_n,
input wire ref_clk, // 参考时钟
output wire locked,
output wire [PHASE_W-1:0] phase_out,
output wire [FREQ_W-1:0] freq_out
);
reg [PHASE_W-1:0] phase_accum;
reg [FREQ_W-1:0] freq_word;
reg ref_prev;
reg ref_edge;
// 检测参考时钟上升沿
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
ref_prev <= 1'b0;
ref_edge <= 1'b0;
end else begin
ref_prev <= ref_clk;
ref_edge <= ref_clk && !ref_prev;
end
end
// 相位累加
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
phase_accum <= 0;
else
phase_accum <= phase_accum + freq_word;
end
// 简化锁相:参考边沿处检测相位差
reg signed [PHASE_W:0] phase_diff;
always @(posedge clk) begin
if (ref_edge)
phase_diff = PHASE_W'(phase_accum) - PHASE_W'(1 << (PHASE_W-1));
end
assign phase_out = phase_accum;
assign freq_out = freq_word;
assign locked = (phase_diff < 16'sd100) && (phase_diff > -16'sd100);
endmodule
#!/usr/bin/env python3
"""carrier_sync.py - 载波同步仿真
第06课:载波同步
演示Costas环捕获和跟踪过程
"""
import numpy as np
import matplotlib.pyplot as plt
class CostasLoop:
"""数字Costas环仿真"""
def __init__(self, fs, fc, loop_bw=0.01, damping=0.707):
self.fs = fs
self.fc = fc
self.nco_phase = 0.0
self.nco_freq = fc # 初始频率估计
# 环路滤波器参数
self.damping = damping
self.loop_bw = loop_bw
# 二阶环路参数 (从自然频率和阻尼系数推导)
wn = loop_bw * fs
self.k1 = 2 * damping * wn / fs
self.k2 = wn**2 / fs**2
# 状态
self.lf_integrator = 0.0
self.lf_prev_error = 0.0
self.phase_errors = []
self.freq_offsets = []
self.locked = False
self.lock_count = 0
def process_sample(self, rx_i, rx_q):
"""处理一个采样点"""
# NCO输出
nco_cos = np.cos(2 * np.pi * self.nco_freq / self.fs + self.nco_phase)
nco_sin = np.sin(2 * np.pi * self.nco_freq / self.fs + self.nco_phase)
# 下变频
demod_i = rx_i * nco_cos + rx_q * nco_sin
demod_q = -rx_i * nco_sin + rx_q * nco_cos
# 鉴相器 (QPSK)
phase_err = np.sign(demod_i) * demod_q - np.sign(demod_q) * demod_i
# 环路滤波器
self.lf_integrator += self.k2 * phase_err
lf_out = self.lf_integrator + self.k1 * (phase_err - self.lf_prev_error)
self.lf_prev_error = phase_err
# 更新NCO
self.nco_freq += lf_out * self.fs
self.nco_phase += 2 * np.pi * self.nco_freq / self.fs
# 锁定检测
if abs(phase_err) < 0.3:
self.lock_count = min(self.lock_count + 1, 255)
else:
self.lock_count = max(self.lock_count - 1, 0)
self.locked = self.lock_count > 200
self.phase_errors.append(phase_err)
self.freq_offsets.append(self.nco_freq - self.fc)
return demod_i, demod_q
def simulate_costas_acquisition():
"""仿真Costas环捕获过程"""
fs = 48000
fc = 8000
symbol_rate = 1000
samples_per_sym = fs // symbol_rate
# 生成QPSK信号,带频偏和相偏
np.random.seed(42)
freq_offset = 200 # 200Hz频偏
phase_offset = np.pi / 6 # 30度相偏
num_symbols = 500
bits = np.random.randint(0, 2, num_symbols * 2)
t = np.arange(num_symbols * samples_per_sym) / fs
# QPSK调制
symbols_i = 1 - 2 * bits[0::2]
symbols_q = 1 - 2 * bits[1::2]
# 上采样
tx_i = np.repeat(symbols_i, samples_per_sym) / np.sqrt(2)
tx_q = np.repeat(symbols_q, samples_per_sym) / np.sqrt(2)
# 加入频偏和相偏
carrier = np.exp(1j * (2 * np.pi * freq_offset * t + phase_offset))
rx_signal_i = tx_i * np.cos(2 * np.pi * freq_offset * t + phase_offset) - \
tx_q * np.sin(2 * np.pi * freq_offset * t + phase_offset)
rx_signal_q = tx_i * np.sin(2 * np.pi * freq_offset * t + phase_offset) + \
tx_q * np.cos(2 * np.pi * freq_offset * t + phase_offset)
# 加入AWGN
snr_db = 15
noise_std = 1.0 / np.sqrt(2 * 10**(snr_db/10))
rx_signal_i += noise_std * np.random.randn(len(rx_signal_i))
rx_signal_q += noise_std * np.random.randn(len(rx_signal_q))
# Costas环
loop = CostasLoop(fs, fc, loop_bw=0.005, damping=0.707)
demod_i = np.zeros(len(rx_signal_i))
demod_q = np.zeros(len(rx_signal_q))
for n in range(len(rx_signal_i)):
di, dq = loop.process_sample(rx_signal_i[n], rx_signal_q[n])
demod_i[n] = di
demod_q[n] = dq
# 绘制捕获过程
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
t_ms = np.arange(len(loop.phase_errors)) / fs * 1000
# 相位误差
axes[0].plot(t_ms, loop.phase_errors, 'c-', linewidth=0.5, alpha=0.7)
axes[0].axhline(0, color='yellow', alpha=0.3)
axes[0].set_title('Costas环相位误差收敛过程', fontsize=13)
axes[0].set_ylabel('相位误差')
axes[0].grid(True, alpha=0.3)
axes[0].set_xlim(0, t_ms[-1])
# 频偏估计
axes[1].plot(t_ms, np.array(loop.freq_offsets), '#10b981', linewidth=0.8)
axes[1].axhline(freq_offset, color='red', linestyle='--', alpha=0.5,
label=f'真实频偏 {freq_offset}Hz')
axes[1].set_title('频偏估计收敛', fontsize=13)
axes[1].set_ylabel('频偏估计 (Hz)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
axes[1].set_xlim(0, t_ms[-1])
# 星座图:锁定前后对比
lock_idx = min(5000, len(demod_i)) # 跳过捕获期
axes[2].scatter(demod_i[lock_idx::4], demod_q[lock_idx::4],
c='cyan', s=2, alpha=0.3)
axes[2].set_title('锁定后星座图', fontsize=13)
axes[2].set_xlabel('I'); axes[2].set_ylabel('Q')
axes[2].grid(True, alpha=0.3)
axes[2].axis('equal')
plt.tight_layout()
plt.savefig('/var/www/ttl/digital-comm/costas_acquisition.png', dpi=100,
facecolor='#0f172a', edgecolor='none')
print("Costas环捕获过程图已保存")
def simulate_phase_ambiguity():
"""仿真QPSK的相位模糊问题"""
np.random.seed(42)
num_symbols = 1000
bits = np.random.randint(0, 2, num_symbols * 2)
symbols_i = (1 - 2 * bits[0::2]) / np.sqrt(2)
symbols_q = (1 - 2 * bits[1::2]) / np.sqrt(2)
fig, axes = plt.subplots(2, 2, figsize=(12, 12))
for idx, phase_shift in enumerate([0, np.pi/4, np.pi/2, np.pi]):
rotated_i = symbols_i * np.cos(phase_shift) - symbols_q * np.sin(phase_shift)
rotated_q = symbols_i * np.sin(phase_shift) + symbols_q * np.cos(phase_shift)
noise = 0.05 * np.random.randn(len(rotated_i))
ax = axes[idx // 2, idx % 2]
ax.scatter(rotated_i + noise, rotated_q + noise, c='cyan', s=3, alpha=0.5)
ax.set_title(f'相位偏移 {np.degrees(phase_shift):.0f}°', fontsize=13)
ax.set_xlabel('I'); ax.set_ylabel('Q')
ax.grid(True, alpha=0.3)
ax.axis('equal')
ax.set_xlim(-1.2, 1.2); ax.set_ylim(-1.2, 1.2)
plt.suptitle('QPSK相位模糊问题', fontsize=15, y=1.02)
plt.tight_layout()
plt.savefig('/var/www/ttl/digital-comm/phase_ambiguity.png', dpi=100,
facecolor='#0f172a', edgecolor='none')
print("相位模糊图已保存")
if __name__ == '__main__':
print("=" * 60)
print("载波同步仿真")
print("=" * 60)
simulate_costas_acquisition()
simulate_phase_ambiguity()
print("\n✅ 所有仿真完成!")
练习1:修改Costas环参数(K1, K2),观察捕获时间和稳态抖动的变化。
练习2:实现BPSK的Costas环(鉴相器为I×Q),与QPSK版本对比。
练习3:仿真大多普勒频移场景(如卫星通信,频偏10kHz),调整环路参数使其能跟踪。
练习4:用差分编码解决QPSK的相位模糊问题,仿真验证。
练习5:实现基于导频的载波同步(插入已知导频符号),与Costas环对比性能。
你攻克了相干解调最关键的一环!从Costas环到锁定检测,从频偏估计到相位模糊,你已经能让接收端精确锁定发送端的载波了。调制解调阶段全部完成!
下一课预告:第07课开始信道编码阶段——理解信道模型,为可靠通信打下基础。