GNU Radio之OFDM Channel Estimation底层C++实现

2024-04-30 08:09:57 浏览数 (2)

文章目录

  • 前言
  • 一、 OFDM Channel Estimation 模块简介
  • 二、C 具体实现
    • 1、初始化和配置参数
    • 2、forecast 函数
    • 3、计算载波偏移量
    • 4、提取信道响应
    • 5、核心的数据处理任务

前言

OFDM Channel Estimation 模块的功能是根据前导码(同步字)估计 OFDM 的信道和粗略频率偏移,本文对 OFDM Channel Estimation 模块的底层 C 源码进行剖析。


一、 OFDM Channel Estimation 模块简介

在这里插入图片描述在这里插入图片描述

OFDM Channel Estimation模块的主要目的是从接收的OFDM符号中恢复出发送时的信道条件。主要包括以下功能:

  • 信道估计:
    • 这个模块核心的功能是估计 OFDM 系统中的信道特性。这包括计算信道的频率响应,以便可以对接收到的信号进行适当的校正,以恢复原始发送的数据。信道估计通常利用已知的同步或导频符号来测量信道对这些已知符号的影响。
  • 载波频率偏移估计:
    • 在 OFDM 系统中,载波频率偏移是接收机和发射机之间存在的频率误差。ofdm_chanest_vcvc_impl 类通过分析接收到的 OFDM 符号来估计这一偏移,这对确保数据正确解调是至关重要的。
  • 生成信道抽头(Channel Taps):
    • 信道抽头是描述信道频率响应的复数值,这些复数值可以直接应用于信号解调和均衡过程中。在 OFDM 系统中,每个子载波的信道响应可以被视为一个抽头。
  • 处理和传递元数据:
    • 这个类还负责在GNU Radio的流图中处理和传递相关的元数据,如信道估计结果和载波偏移信息。这些信息通常通过标签(tags)的形式添加到数据流中,供后续的处理块使用。

注意:这个模块只是做估计,未进行均衡,均衡由 OFDM Frame Equalizer 模块实现

二、C 具体实现

ofdm_chanest_vcvc_impl 实现了以下关键方法:

  • forecast()
    • 该方法为调度器提供了关于块如何根据输入生成输出的信息。具体来说,它告诉调度器在执行处理之前需要多少输入数据。
  • general_work()
    • 这是块的主要处理函数,它处理输入数据,执行信道估计和载波偏移估计,并生成输出数据。此函数还负责将计算出的信道信息和其他相关元数据标签插入到输出流中。
  • get_carr_offset()get_chan_taps()
    • 这些辅助函数用于计算载波偏移和提取信道抽头,是信道估计过程的核心部分。

1、初始化和配置参数

构造函数 ofdm_chanest_vcvc_impl,实现初始化和配置信道估计的各种参数

代码语言:javascript复制
// 构造函数,初始化和配置信道估计的各种参数
ofdm_chanest_vcvc_impl::ofdm_chanest_vcvc_impl(	
    const std::vector<gr_complex>& sync_symbol1,	// 同步符号, 用于信道估计
    const std::vector<gr_complex>& sync_symbol2,	// 同步符号, 用于信道估计
    int n_data_symbols,								// 数据符号的数量,表示每次处理的数据符号数
    int eq_noise_red_len,							// 均衡噪声减少的长度,用于设置信道估计中的一些内部处理
    int max_carr_offset,							// 最大载波偏移,用于粗略频率估计
    bool force_one_sync_symbol)						// 用于控制是否强制只使用一个同步符号进行信道估计
    : block("ofdm_chanest_vcvc",
            io_signature::make(1, 1, sizeof(gr_complex) * sync_symbol1.size()),		// 示这个模块有一个输入端口,每个输入项是一个复数向量,向量的长度等于 sync_symbol1.size()。
            io_signature::make(1, 2, sizeof(gr_complex) * sync_symbol1.size())),	// 表示这个模块有一个或两个输出端口,输出数据格式与输入相同。
      d_fft_len(sync_symbol1.size()),	// FFT的长度
      d_n_data_syms(n_data_symbols),
      d_n_sync_syms(1),
      d_eq_noise_red_len(eq_noise_red_len),
      d_ref_sym((!sync_symbol2.empty() && !force_one_sync_symbol) ? sync_symbol2	// 参考同步符号为 sync_symbol2
                                                                  : sync_symbol1),
      d_corr_v(sync_symbol2),	// 用于信道估计的向量,用于存储相关性向量
	  // 用于存储已知和新的符号差异。
	  d_known_symbol_diffs(0, 0),	
      d_new_symbol_diffs(0, 0),	
      d_first_active_carrier(0),	// 第一个活跃子载波的索引  
      d_last_active_carrier(sync_symbol2.size() - 1),	// 最后一个活跃子载波的索引
      d_interpolate(false)			// 不需要插值
{
    // Set index of first and last active carrier
	// ******************************寻找活跃载波**********************
	/*
		这两个循环用于确定第二个同步字中第一个和最后一个非零(即活跃)载波的索引。
		这是为了确定数据中的有效范围。
	*/
    for (int i = 0; i < d_fft_len; i  ) {
        if (d_ref_sym[i] != gr_complex(0, 0)) {
            d_first_active_carrier = i;
            break;
        }
    }
    for (int i = d_fft_len - 1; i >= 0; i--) {
        if (d_ref_sym[i] != gr_complex(0, 0)) {
            d_last_active_carrier = i;
            break;
        }
    }

    // Sanity checks
    // ******************************合理性检查**********************
	/*
		这部分代码首先检查两个同步符号的长度是否相等,如果不等则抛出异常。
		接着,根据是否强制使用一个同步符号来调整同步符号的数量。
		如果只有一个同步符号且下一个载波是零,则开启插值模式。
	*/
	if (!sync_symbol2.empty()) {
        if (sync_symbol1.size() != sync_symbol2.size()) {
            throw std::invalid_argument("sync symbols must have equal length.");
        }
        if (!force_one_sync_symbol) {
            d_n_sync_syms = 2;
        }
    } else {
        if (sync_symbol1[d_first_active_carrier   1] == gr_complex(0, 0)) {
            d_last_active_carrier  ;
            d_interpolate = true;
        }
    }

    // Set up coarse freq estimation info
    // Allow all possible values:
    // ******************************设置频率估计参数**********************
	/*
		这部分设置最大负载和正载波偏移量,并确保这些偏移量为偶数,这是因为同步算法要求。
	*/
	d_max_neg_carr_offset = -d_first_active_carrier;	// 系统可以容忍的最大向下(或向负方向)的频率偏移量,表示为载波数量。负载波偏移意味着接收频率低于发射频率。
    d_max_pos_carr_offset = d_fft_len - d_last_active_carrier - 1;	// 系统可以容忍的最大向上(或向正方向)的频率偏移量,同样表示为载波数量。正载波偏移意味着接收频率高于发射频率。
    if (max_carr_offset != -1) {
        d_max_neg_carr_offset = std::max(-max_carr_offset, d_max_neg_carr_offset);
        d_max_pos_carr_offset = std::min(max_carr_offset, d_max_pos_carr_offset);
    }
    // Carrier offsets must be even
    if (d_max_neg_carr_offset % 2)
        d_max_neg_carr_offset  ;
    if (d_max_pos_carr_offset % 2)
        d_max_pos_carr_offset--;

	// ******************************处理相关性向量**********************
	/*
		如果使用两个同步符号,计算每个载波的相关性。如果只使用一个,
		重新设置相关向量并计算已知符号之间的差异。
	*/
	if (d_n_sync_syms == 2) {
        for (int i = 0; i < d_fft_len; i  ) {
            if (sync_symbol1[i] == gr_complex(0, 0)) {
                d_corr_v[i] = gr_complex(0, 0);
            } else {
                d_corr_v[i] /= sync_symbol1[i];		// 同步字2 ÷ 同步字1
            }
        }
    } else {
        d_corr_v.resize(0, 0);
        d_known_symbol_diffs.resize(d_fft_len, 0);
        d_new_symbol_diffs.resize(d_fft_len, 0);
        for (int i = d_first_active_carrier;
             i < d_last_active_carrier - 2 && i < d_fft_len - 2;
             i  = 2) {
            d_known_symbol_diffs[i] = std::norm(sync_symbol1[i] - sync_symbol1[i   2]);
        }
    }

	// ******************************设置输出和速率**********************
    set_output_multiple(d_n_data_syms);		// 设置输出的数量
    set_relative_rate((uint64_t)d_n_data_syms, (uint64_t)(d_n_data_syms   d_n_sync_syms)); // 设置输出的相对速率
    set_tag_propagation_policy(TPP_DONT);	// 设置输出的标签传播策略
}

2、forecast 函数

forecast 函数是由框架在调度块(block)执行之前调用的。这个函数的主要作用是告诉调度器(scheduler),在实际调用处理函数(如 general_work 或 work 函数)之前,块(block)需要多少输入项(samples)来产生预期的输出项。这一机制确保在执行处理函数时,块有足够的数据来进行处理,从而避免处理函数中出现缓冲区下溢的情况。

代码语言:javascript复制
// forecast 方法在 GNU Radio 中的用途是为调度器提供关于数据依赖关系的信息,
// 即它告诉系统在产生一定数量的输出之前,需要多少输入。这个方法对于确保块在
// 有足够的输入数据处理之前不被调用是非常重要的。
void ofdm_chanest_vcvc_impl::forecast(int noutput_items,		// 预期的输出项数。在这个上下文中,它指的是调度器计划产生的输出数据块的数量
                                      gr_vector_int& ninput_items_required)	// 用于存储每个输入流所需的输入项数
{	
	// ************************逻辑解释************************
	// 这个 forecast 方法实现的基本思想是:为了产生 noutput_items 个输出,每个输出都需要 d_n_data_syms 个数据符号,但每组输入还包括一定数量的同步符号 (d_n_sync_syms)。
	// 因此,我们需要从输入流中获取足够的数据来覆盖这两部分的需求。
	// 这种计算方式确保了无论何时调度器决定调用这个块处理数据时,块都能有足够的输入数据来满足其输出产量的需求,从而避免在数据不足时处理数据,这是确保数据流正确性的关键一环。


	// 计算并设置第一个输入流(索引为0)所需的输入项数
	// (noutput_items / d_n_data_syms): 将预期的输出项数除以每组数据符号的数量,这个操作基本上在计算为了生成所需的输出数量,需要处理多少组数据。
	// (d_n_data_syms   d_n_sync_syms): 计算得到的每组数据的数量乘以每组中数据符号和同步符号的总和
	ninput_items_required[0] =
        (noutput_items / d_n_data_syms) * (d_n_data_syms   d_n_sync_syms);	
}

3、计算载波偏移量

代码语言:javascript复制
// 用于计算并返回载波偏移量
int ofdm_chanest_vcvc_impl::get_carr_offset(const gr_complex* sync_sym1,	// 同步符号,用于计算载波偏移
                                            const gr_complex* sync_sym2)
{
    int carr_offset = 0;
    if (!d_corr_v.empty()) {
        // Use Schmidl & Cox method
        // 相关性的估计方法,如Schmidl & Cox方法
        float Bg_max = 0;	// 初始化最大相关性度量为0
        // g here is 2g in the paper
        // 从最大负载波偏移量到最大正载波偏移量遍历,步长为2
        for (int g = d_max_neg_carr_offset; g <= d_max_pos_carr_offset; g  = 2) {
			// 初始化一个临时复数用于计算当前偏移量 g 下的相关性
			gr_complex tmp = gr_complex(0, 0);
			// 对每个FFT长度内的点,如果相关向量在该点不为零,则计算该点在两个同步符号上的相关性,并累加到 tmp。
            for (int k = 0; k < d_fft_len; k  ) {
                if (d_corr_v[k] != gr_complex(0, 0)) {
                    tmp  = std::conj(sync_sym1[k   g]) * std::conj(d_corr_v[k]) *
                           sync_sym2[k   g];
                }
            }
			// 如果当前的 tmp 的绝对值大于已知的最大值,则更新最大值和对应的载波偏移量。
            if (std::abs(tmp) > Bg_max) {
                Bg_max = std::abs(tmp);
                carr_offset = g;
            }
        }
    } else {
        // Correlate
        std::fill(d_new_symbol_diffs.begin(), d_new_symbol_diffs.end(), 0);
        for (int i = 0; i < d_fft_len - 2; i  ) {
            d_new_symbol_diffs[i] = std::norm(sync_sym1[i] - sync_sym1[i   2]);
        }

        float sum;
        float max = 0;
        for (int g = d_max_neg_carr_offset; g <= d_max_pos_carr_offset; g  = 2) {
            sum = 0;
            for (int j = 0; j < d_fft_len; j  ) {
                if (d_known_symbol_diffs[j]) {
                    sum  = (d_known_symbol_diffs[j] * d_new_symbol_diffs[j   g]);
                }
                if (sum > max) {
                    max = sum;
                    carr_offset = g;
                }
            }
        }
    }
    return carr_offset;
}

他这里的理论参考的是 Robust Frequency and Timing Synchronization for OFDM. Timothy M. Schmidl and Donald C. Cox, Fellow, IEEE 的论文内容。

4、提取信道响应

代码语言:javascript复制
// 用于从同步符号中提取信道响应,即“信道抽头”(channel taps)。这些信道抽头代表了在多径环境下,信道对每个频率的响应。
void ofdm_chanest_vcvc_impl::get_chan_taps(const gr_complex* sync_sym1,
                                           const gr_complex* sync_sym2,
                                           int carr_offset,					// 载波偏移
                                           std::vector<gr_complex>& taps)	// 用于存储计算出的信道抽头
{
	// ***************选择使用的同步符号****************
    const gr_complex* sym = ((d_n_sync_syms == 2) ? sync_sym2 : sync_sym1);	// 使用 sync_sym2 同步符号数组
	// ***************初始化信道抽头向量****************
	std::fill(taps.begin(), taps.end(), gr_complex(0, 0));
	// ***************设置循环边界****************
	/*
		初始化循环的起始和结束索引。根据载波偏移调整这些索引,
		以避免数组越界。载波偏移向正方向时,从偏移处开始;
		向负方向时,结束点前移。
	*/
    int loop_start = 0;
    int loop_end = d_fft_len;
    if (carr_offset > 0) {
        loop_start = carr_offset;
    } else if (carr_offset < 0) {
        loop_end = d_fft_len   carr_offset;
    }

	// ***************计算信道抽头****************
	/*
		遍历有效的FFT点范围。只有当参考符号在相应的位置不为零时,才计算信道抽头,避免除零错误。
		信道抽头是通过将当前同步符号(经过信道后)除以参考同步符号得到的。
	*/
    for (int i = loop_start; i < loop_end; i  ) {
        if ((d_ref_sym[i - carr_offset] != gr_complex(0, 0))) {
            taps[i - carr_offset] = sym[i] / d_ref_sym[i - carr_offset];
        }
    }

	// ***************插值处理****************
	/*
		如果启用了插值 (d_interpolate),对信道抽头进行插值处理,填补那些没有直接计算的点。
		这通常用于提高信道估计的平滑性和准确性。
	*/
    if (d_interpolate) {
        for (int i = d_first_active_carrier   1; i < d_last_active_carrier; i  = 2) {
            taps[i] = taps[i - 1];
        }
        taps[d_last_active_carrier] = taps[d_last_active_carrier - 1];
    }

	// ***************噪声降低处理(未实现)****************
    if (d_eq_noise_red_len) {
        // TODO
        // 1) IFFT
        // 2) Set all elements > d_eq_noise_red_len to zero
        // 3) FFT
    }
}

5、核心的数据处理任务

代码语言:javascript复制
int ofdm_chanest_vcvc_impl::general_work(int noutput_items,				// 函数打算产生的输出项数
                                         gr_vector_int& ninput_items,	// 每个输入流的输入项数
                                         gr_vector_const_void_star& input_items,
                                         gr_vector_void_star& output_items)
{
    const gr_complex* in = (const gr_complex*)input_items[0];
    gr_complex* out = (gr_complex*)output_items[0];
    const int framesize = d_n_sync_syms   d_n_data_syms;	// 定义处理的总帧大小,包括同步符号和数据符号的数量。

    // Channel info estimation
    // 信道信息估计
    int carr_offset = get_carr_offset(in, in   d_fft_len);		// 计算载波偏移量
    std::vector<gr_complex> chan_taps(d_fft_len, 0);			// 存储信道抽头
    get_chan_taps(in, in   d_fft_len, carr_offset, chan_taps);	// 填充chan_taps向量
	// 在输出流的特定位置添加标签,标识载波偏移和信道抽头的信息。
	add_item_tag(0,
                 nitems_written(0),
                 pmt::string_to_symbol("ofdm_sync_carr_offset"),
                 pmt::from_long(carr_offset));
    add_item_tag(0,
                 nitems_written(0),
                 pmt::string_to_symbol("ofdm_sync_chan_taps"),
                 pmt::init_c32vector(d_fft_len, chan_taps));

    // Copy data symbols
    // 复制数据符号到输出
    if (output_items.size() == 2) {	// 如果输出项数为2,则将chan_taps数据复制到第二个输出。
        gr_complex* out_chantaps = ((gr_complex*)output_items[1]);
        memcpy((void*)out_chantaps, (void*)&chan_taps[0], sizeof(gr_complex) * d_fft_len);
        produce(1, 1);
    }
	// 将输入中的数据符号部分复制到输出
    memcpy((void*)out,
           (void*)&in[d_n_sync_syms * d_fft_len],
           sizeof(gr_complex) * d_fft_len * d_n_data_syms);

    // Propagate tags
    // 传递标签
    /*
		从输入流获取所有标签并调整它们的位置,考虑到同步符号的存在。
	*/
    std::vector<gr::tag_t> tags;
    get_tags_in_range(tags, 0, nitems_read(0), nitems_read(0)   framesize);
    for (unsigned t = 0; t < tags.size(); t  ) {
        int offset = tags[t].offset - nitems_read(0);
        if (offset < d_n_sync_syms) {
            offset = 0;
        } else {
            offset -= d_n_sync_syms;
        }
        tags[t].offset = offset   nitems_written(0);
        add_item_tag(0, tags[t]);
    }

	// 生成和消耗数据
	// 指示产生的输出项数和消耗的输入项数。
    produce(0, d_n_data_syms);
    consume_each(framesize);
    return WORK_CALLED_PRODUCE;
}

0 人点赞