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

2024-04-29 18:36

本文主要是介绍GNU Radio之OFDM Channel Estimation底层C++实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 一、 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,实现初始化和配置信道估计的各种参数

// 构造函数,初始化和配置信道估计的各种参数
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 evenif (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)来产生预期的输出项。这一机制确保在执行处理函数时,块有足够的数据来进行处理,从而避免处理函数中出现缓冲区下溢的情况。

// 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、计算载波偏移量

// 用于计算并返回载波偏移量
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// 从最大负载波偏移量到最大正载波偏移量遍历,步长为2for (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 {// Correlatestd::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、提取信道响应

// 用于从同步符号中提取信道响应,即“信道抽头”(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、核心的数据处理任务

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;
}

我的qq:2442391036,欢迎交流!


这篇关于GNU Radio之OFDM Channel Estimation底层C++实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/946846

相关文章

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

C#使用yield关键字实现提升迭代性能与效率

《C#使用yield关键字实现提升迭代性能与效率》yield关键字在C#中简化了数据迭代的方式,实现了按需生成数据,自动维护迭代状态,本文主要来聊聊如何使用yield关键字实现提升迭代性能与效率,感兴... 目录前言传统迭代和yield迭代方式对比yield延迟加载按需获取数据yield break显式示迭

Python实现高效地读写大型文件

《Python实现高效地读写大型文件》Python如何读写的是大型文件,有没有什么方法来提高效率呢,这篇文章就来和大家聊聊如何在Python中高效地读写大型文件,需要的可以了解下... 目录一、逐行读取大型文件二、分块读取大型文件三、使用 mmap 模块进行内存映射文件操作(适用于大文件)四、使用 pand

python实现pdf转word和excel的示例代码

《python实现pdf转word和excel的示例代码》本文主要介绍了python实现pdf转word和excel的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、引言二、python编程1,PDF转Word2,PDF转Excel三、前端页面效果展示总结一

Python xmltodict实现简化XML数据处理

《Pythonxmltodict实现简化XML数据处理》Python社区为提供了xmltodict库,它专为简化XML与Python数据结构的转换而设计,本文主要来为大家介绍一下如何使用xmltod... 目录一、引言二、XMLtodict介绍设计理念适用场景三、功能参数与属性1、parse函数2、unpa

C#实现获得某个枚举的所有名称

《C#实现获得某个枚举的所有名称》这篇文章主要为大家详细介绍了C#如何实现获得某个枚举的所有名称,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以参考一下... C#中获得某个枚举的所有名称using System;using System.Collections.Generic;usi

Go语言实现将中文转化为拼音功能

《Go语言实现将中文转化为拼音功能》这篇文章主要为大家详细介绍了Go语言中如何实现将中文转化为拼音功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 有这么一个需求:新用户入职 创建一系列账号比较麻烦,打算通过接口传入姓名进行初始化。想把姓名转化成拼音。因为有些账号即需要中文也需要英

C# 读写ini文件操作实现

《C#读写ini文件操作实现》本文主要介绍了C#读写ini文件操作实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录一、INI文件结构二、读取INI文件中的数据在C#应用程序中,常将INI文件作为配置文件,用于存储应用程序的

C#实现获取电脑中的端口号和硬件信息

《C#实现获取电脑中的端口号和硬件信息》这篇文章主要为大家详细介绍了C#实现获取电脑中的端口号和硬件信息的相关方法,文中的示例代码讲解详细,有需要的小伙伴可以参考一下... 我们经常在使用一个串口软件的时候,发现软件中的端口号并不是普通的COM1,而是带有硬件信息的。那么如果我们使用C#编写软件时候,如