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

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount