在 GNU Radio OFDM 系统中,一个非常重要的环节是在接收端准确地同步和检测发送端发出的信号。这就是 Schmidl & Cox 同步算法发挥作用的地方。Schmidl & Cox 算法是一种用于 OFDM 信号的时间同步的技术。本文对其底层 C++ 源码进行学习记录。
在 GNU Radio 中,Schmidl & Cox 同步模块如下图所示,其接受三个参数,分别是:FFT 长度、循环前缀长度、检测阈值。
Schmidl & Cox 算法利用特殊设计的训练序列或前导符号(preamble),这些前导符号包含两个连续的相同部分,通常记为 A 和 A。这种结构使得接收端能够通过比较这两部分来估计开始接收数据的最佳时刻。
这里的训练序列或前导符号指的是同步字,在 ofdm grc 例程中用的是两个符号的同步字
[0., 0., 0., 0., 0., 0., 0., 1.41421356, 0., -1.41421356, 0., 1.41421356, 0., -1.41421356, 0., -1.41421356, 0., -1.41421356, 0., 1.41421356, 0., -1.41421356, 0., 1.41421356, 0., -1.41421356, 0., -1.41421356, 0., -1.41421356, 0., -1.41421356, 0., 1.41421356, 0., -1.41421356, 0., 1.41421356, 0., 1.41421356, 0., 1.41421356, 0., -1.41421356, 0., 1.41421356, 0., 1.41421356, 0., 1.41421356, 0., -1.41421356, 0., 1.41421356, 0., 1.41421356, 0., 1.41421356, 0., 0., 0., 0., 0., 0.]
[0, 0, 0, 0, 0, 0, -1, -1, -1, -1, 1, 1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 0, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1, -1, 1, -1, 1, -1, -1, -1, -1, 0, 0, 0, 0, 0]
GNU Radio 的 Schmidl & Cox OFDM Sync 模块具有以下两个输出端:
output0
是模块输出的原始频率偏移估计值,fft_len
是 FFT 的长度,归一化的频率偏移是指偏移量除以 FFT 长度的一半,原因是 FFT 的输出范围通常解释为从负一半到正一半(即 -0.5 到 0.5)的频率范围。官方对其其输出也做了相应的解释:
其意思就是说:粗略频率偏移的评估不在该块中完成。此外,此处不计算初始均衡器抽头。 如果时序度量是
,那么归一化计算为
,(N = fft_len),这就意味着此模块估计两个半符号的能量,这可以避免在突发结束时能量水平突然下降时出现虚假检测。
他这里的理论参考的是 Robust Frequency and Timing Synchronization for OFDM. Timothy M. Schmidl and Donald C. Cox, Fellow, IEEE 的论文内容。
论文中指出:
其实也就是说论文中使用的是估计半个符号的能量,而在 Schmidl & Cox 同步模块中,使用的是估计两个半符号(即一个完整的 OFDM 符号)的能量。
ofdm_sync_sc_cfb_impl::ofdm_sync_sc_cfb_impl(int fft_len, // FFT(快速傅立叶变换)的长度
int cp_len, // 循环前缀的长度
bool use_even_carriers, // 一个布尔值,指示是否使用偶数承载波
float threshold) // 用于检测峰值的阈值
: hier_block2("ofdm_sync_sc_cfb",
io_signature::make(1, 1, sizeof(gr_complex)),
#ifndef SYNC_ADD_DEBUG_OUTPUT
io_signature::make2(2, 2, sizeof(float), sizeof(unsigned char)))
#else
io_signature::make3(
3, 3, sizeof(float), sizeof(unsigned char), sizeof(float)))
#endif
{
// 处理组件 ,代码创建了多个处理块,并且将这些块相互连接,形成数据处理的流程
std::vector<float> ma_taps(fft_len / 2, 1.0); // 被用作一个移动平均(Moving Average, MA)滤波器的系数
gr::blocks::delay::sptr delay( // 接收输入信号并将其延迟 fft_len / 2个样本。这用于创建与原始信号相比有一定时间偏移的信号
gr::blocks::delay::make(sizeof(gr_complex), fft_len / 2));
gr::blocks::conjugate_cc::sptr delay_conjugate(gr::blocks::conjugate_cc::make()); // 取延迟信号的共轭,这在信号处理中用于某些相关计算
gr::blocks::multiply_cc::sptr delay_corr(gr::blocks::multiply_cc::make()); // 将原始信号与延迟并共轭处理后的信号相乘,用于计算相关性,这是同步算法的一部分
gr::filter::fir_filter_ccf::sptr delay_ma(gr::filter::fir_filter_ccf::make( // 对上述乘积的结果应用滑动平均滤波器,滤波器的系数根据use_even_carriers确定为1或-1
1, std::vector<float>(fft_len / 2, use_even_carriers ? 1.0 : -1.0)));
gr::blocks::complex_to_mag_squared::sptr delay_magsquare( // 计算滤波后信号的幅度的平方,这有助于后续的峰值检测
gr::blocks::complex_to_mag_squared::make());
gr::blocks::divide_ff::sptr delay_normalize(gr::blocks::divide_ff::make()); // 将上述幅度平方与一个标准化信号相除,以调整不同条件下的信号级别
gr::blocks::complex_to_mag_squared::sptr normalizer_magsquare( // 用于计算复数输入信号的幅度的平方
gr::blocks::complex_to_mag_squared::make());
gr::filter::fir_filter_fff::sptr normalizer_ma( // 创建一个FIR滤波器实例,它的滤波器系数是一个大小为 fft_len 的向量,每个元素值为
gr::filter::fir_filter_fff::make(1, std::vector<float>(fft_len, 0.5)));
gr::blocks::multiply_ff::sptr normalizer_square(gr::blocks::multiply_ff::make()); // 该乘法块用于将两个浮点数输入信号逐点相乘
// 同步和频率估计
gr::blocks::complex_to_arg::sptr peak_to_angle(gr::blocks::complex_to_arg::make()); // 将复数信号转换为其相角,用于频率误差的估计
gr::blocks::sample_and_hold_ff::sptr sample_and_hold( // 在检测到峰值时,保持当前的频率估计值,直到下一个峰值被检测到
gr::blocks::sample_and_hold_ff::make());
// 峰值检测
gr::blocks::plateau_detector_fb::sptr plateau_detector( // 根据设置的阈值来检测峰值,这是确定信号同步点的关键步骤。
gr::blocks::plateau_detector_fb::make(cp_len, threshold));
// store plateau detector for use in callback setting threshold
d_plateau_detector = plateau_detector;
// 连接组件,将所有这些组件按照处理逻辑连接起来。这包括将信号从一个块传输到另一个块,确保信号流能够按照设计的路径正确流动。
// Delay Path
connect(self(), 0, delay, 0); // 连接源自身到延迟块
connect(delay, 0, delay_conjugate, 0); // 连接延迟块到共轭块
connect(delay_conjugate, 0, delay_corr, 1); // 连接共轭块到乘法块的第二个输入
connect(self(), 0, delay_corr, 0); // 连接源自身到乘法块的第一个输入
connect(delay_corr, 0, delay_ma, 0); // 连接乘法块到移动平均滤波器块
connect(delay_ma, 0, delay_magsquare, 0); // 连接移动平均滤波器块到幅度平方块
connect(delay_magsquare, 0, delay_normalize, 0); // 连接幅度平方块到归一化块
// Energy Path
connect(self(), 0, normalizer_magsquare, 0); // 从源自身到能量平方块
connect(normalizer_magsquare, 0, normalizer_ma, 0); // 从能量平方块到移动平均滤波器
connect(normalizer_ma, 0, normalizer_square, 0); // 从移动平均滤波器到乘法块(第一个输入)
connect(normalizer_ma, 0, normalizer_square, 1); // 从移动平均滤波器到乘法块(第二个输入)
connect(normalizer_square, 0, delay_normalize, 1); // 从乘法块到归一化块
// Fine frequency estimate (output 0)
connect(delay_ma, 0, peak_to_angle, 0); // 将delay_ma(移动平均滤波器)的输出连接到peak_to_angle块的输入
connect(peak_to_angle, 0, sample_and_hold, 0); // 从相位转换块到采样保持块
connect(sample_and_hold, 0, self(), 0); // 从采样保持块到层次块的输出
// Peak detect (output 1)
connect(delay_normalize, 0, plateau_detector, 0); // 从归一化块到平台检测器
connect(plateau_detector, 0, sample_and_hold, 1); // 从平台检测器到采样保持块(第二输入)
connect(plateau_detector, 0, self(), 1); // 从平台检测器到层次块的第二输出
#ifdef SYNC_ADD_DEBUG_OUTPUT
// Debugging: timing metric (output 2)
connect(delay_normalize, 0, self(), 2); // 将delay_normalize块的输出连接到层次块的第三个输出端口
#endif
}
std::vector<float> ma_taps(fft_len / 2, 1.0)
; gr::blocks::delay::sptr delay( gr::blocks::delay::make(sizeof(gr_complex), fft_len / 2));
gr::blocks::delay::sptr delay
:这部分定义了一个名为 delay 的智能指针(sptr,即shared pointer),指向 GNU Radio 中的一个延迟块。GNU Radio 通常使用智能指针来管理块的内存,这有助于自动处理块的生命周期和内存回收。gr::blocks::delay::make(sizeof(gr_complex), fft_len / 2)
:这是一个静态工厂方法,用于创建延迟块的实例。它接收两个参数: gr::blocks::divide_ff::sptr delay_normalize(gr::blocks::divide_ff::make());
connect(self(), 0, delay, 0);
// 连接源自身到延迟块
connect(delay, 0, delay_conjugate, 0);
// 连接延迟块到共轭块
connect(delay_conjugate, 0, delay_corr, 1);
// 连接共轭块到乘法块的第二个输入
connect(self(), 0, delay_corr, 0);
// 连接源自身到乘法块的第一个输入
connect(delay_corr, 0, delay_ma, 0);
// 连接乘法块到移动平均滤波器块
connect(delay_ma, 0, delay_magsquare, 0);
// 连接移动平均滤波器块到幅度平方块
connect(delay_magsquare, 0, delay_normalize, 0);
// 连接幅度平方块到归一化块 connect(self(), 0, normalizer_magsquare, 0);
// 从源自身到能量平方块
connect(normalizer_magsquare, 0, normalizer_ma, 0);
// 从能量平方块到移动平均滤波器
connect(normalizer_ma, 0, normalizer_square, 0);
// 从移动平均滤波器到乘法块(第一个输入)
connect(normalizer_ma, 0, normalizer_square, 1);
// 从移动平均滤波器到乘法块(第二个输入)
connect(normalizer_square, 0, delay_normalize, 1);
// 从乘法块到归一化块 connect(delay_ma, 0, peak_to_angle, 0);
// 将delay_ma(移动平均滤波器)的输出连接到peak_to_angle块的输入
connect(peak_to_angle, 0, sample_and_hold, 0);
// 从相位转换块到采样保持块
connect(sample_and_hold, 0, self(), 0);
// 从采样保持块到层次块的输出 connect(delay_normalize, 0, plateau_detector, 0);
// 从归一化块到平台检测器
connect(plateau_detector, 0, sample_and_hold, 1);
// 从平台检测器到采样保持块(第二输入)
connect(plateau_detector, 0, self(), 1);
// 从平台检测器到层次块的第二输出 流程图如下:
处理流程包括多个信号处理块的创建和连接,涉及信号的延迟处理、能量分析、频率估计和峰值检测。下面将详细解释各个部分的功能和它们之间的关系。
self()
到 delay
块: connect(self(), 0, delay, 0);
delay
到 delay_conjugate
块: connect(delay, 0, delay_conjugate, 0);
延迟后的信号进入 delay_conjugate 块,该块计算输入信号的复共轭。在信号处理中,复共轭常用于相关性计算和频谱分析。delay_conjugate
到 delay_corr
块: connect(delay_conjugate, 0, delay_corr, 1);
connect(self(), 0, delay_corr, 0);
delay_corr
到 delay_ma
块: connect(delay_corr, 0, delay_ma, 0);
delay_ma
到 delay_magsquare
块: connect(delay_ma, 0, delay_magsquare, 0);
delay_magsquare
到 delay_normalize
块: connect(delay_magsquare, 0, delay_normalize, 0);
self()
到 normalizer_magsquare
块: connect(self(), 0, normalizer_magsquare, 0);
normalizer_magsquare
到 normalizer_ma
块: connect(normalizer_magsquare, 0, normalizer_ma, 0);
normalizer_ma
到 normalizer_square
块: connect(normalizer_ma, 0, normalizer_square, 0);
connect(normalizer_ma, 0, normalizer_square, 1);
normalizer_square
到 delay_normalize
块: connect(normalizer_square, 0, delay_normalize, 1);
delay_ma
到 peak_to_angle
块: connect(delay_ma, 0, peak_to_angle, 0);
peak_to_angle
到 sample_and_hold
块: connect(peak_to_angle, 0, sample_and_hold, 0);
sample_and_hold
到 self()
: connect(sample_and_hold, 0, self(), 0);
delay_normalize
到 plateau_detector
块: connect(delay_normalize, 0, plateau_detector, 0);
plateau_detector
到 sample_and_hold
和 self()
: connect(plateau_detector, 0, sample_and_hold, 1);
connect(plateau_detector, 0, self(), 1);
delay_normalize
到 self()
(如果启用调试): connect(delay_normalize, 0, self(), 2);
这里将下图红框内的流程进行讲解梳理:
在这个GNU Radio流程图中,Schmidl & Cox OFDM synch 模块输出的频率偏移被送到了一个 Frequency Mod(频率调制)模块,然后此输出与延迟后的 OFDM 信号相乘。这是一种常见的频率校正方法,目的是要对接收到的 OFDM 信号进行补偿,以便修正由于接收机和发射机之间的频率偏差造成的效应。
流程说明:
为什么需要延迟?
Robust Frequency and Timing Synchronization for OFDM 文献部分阅读笔记