解决GNU Radio+USRP实现OFDM收发在接收端存在误码问题

文章目录

  • 前言
  • 一、OFDM 收发流程
    • 1、OFDM 收端流程
    • 2、OFDM 收端流程
  • 二、问题所在
    • 1、find_trigger_signal 函数解读
    • 2、general_work 函数
    • 3、问题所在
  • 三、修改源码
  • 四、运行结果
    • 1、频谱
    • 2、传输数据测试
  • 五、调试小技巧
  • 六、资源自取


前言

在使用 GNU Radio 时使用官方例程搭建 GNU Radio + USRP 实现 OFDM 收发测试时,发现误码情况很严重,明明都是理想信道的情况下,即时在仿真情况下不接 USRP 硬件设备进行收发也会出现误码,如下图所示,这就不得不怀疑是官方的底层 C++ 源码存在的问题了。
在这里插入图片描述

当然,之前我也用了一些方法在不修改底层 C++ 源码时解决了这个问题:GNURadio+USRP+OFDM实现文件传输,但是还是想从根本上解决这个误码问题。

首先声明一下我的环境:(Ubuntu20.04LTS + GNURadio 3.8 + UHD 3.15),一台电脑 + 一台 USRP 自收自发。


一、OFDM 收发流程

当使用官方的例程(一次发送 10 帧即 960 个字节的数据)进行测试时即使是在仿真中将信道条件改为理想信道时在接收端也会出现丢帧的现象。

1、OFDM 收端流程

有关 OFDM 发送端流程图如下图所示:
在这里插入图片描述

发端没有什么问题,问题存在于收端的处理

2、OFDM 收端流程

有关 OFDM 接收端流程图如下图所示:
在这里插入图片描述
其中问题所在是 Header/Payload Demux 模块的底层处理,下面一起看看其内部实现

二、问题所在

下图红框内的模块即 Header/Payload Demux 模块。
在这里插入图片描述
Header/Payload Demux:该模块的作用是根据定时信息和帧头信息,将复合在一起的帧头和数据进行分离。该模块的工作原理是:首先,将三个输入端口从上到下编号为 0,1,2,输出端编号类似。0 号端口连续输入去除载波频偏的数据流,当 1 号端口(定时信息)输入 1 时,也就是功能被触发,则输出端口 0 输出帧头,而数据(Payload)则保持不动。直到输入端口 2 接收到解码后的帧头信息,输出端口才有数据输出,输出数据为帧头和数据 payload 的分离数据。

我们首先看一下官方源码的原理,以下为官方有关核心程序讲解:

1、find_trigger_signal 函数解读

/*
	函数功能:在信号处理或数据流处理程序中寻找触发信号的函数
*/
int header_payload_demux_impl::find_trigger_signal(int skip_items,
                                                   int max_rel_offset,
                                                   uint64_t base_offset,
                                                   const unsigned char* in_trigger)
{
/*
	参数说明:
	skip_items:开始搜索之前要跳过的项目数量
	max_rel_offset:最大的相对偏移量,即在这个范围内寻找触发信号
	base_offset:基准偏移量,是搜索的起始点
	in_trigger:指向触发信号数据的指针
*/
    int rel_offset = max_rel_offset;	// 初始化为最大相对偏移量,用来存储找到的触发信号的相对位置

	/*如果最大相对偏移量小于要跳过的项目数,直接返回rel_offset。
    这意味着没有足够的数据来进行搜索,所以函数提前结束。*/
    if (max_rel_offset < skip_items) {
        return rel_offset;
    }
	
    if (in_trigger) {	// 如果 in_trigger 不是空指针,即有触发信号数据提供进行搜索。
    	/*
    		这里使用了一个for循环从skip_items开始,一直到max_rel_offset,遍历触发信号数据。
    	*/
        for (int i = skip_items; i < max_rel_offset; i++) {
			/*
				如果在某个位置i找到触发信号(if (in_trigger[i])),则更新rel_offset为这个位置,
				并跳出循环。这表示找到了触发信号的第一个实例。
			*/
            if (in_trigger[i]) {
                rel_offset = i;
                break;
            }
        }
    }
    if (d_uses_trigger_tag) {	// 如果类的成员变量d_uses_trigger_tag为真,表示使用了触发标签进行搜索
        std::vector<tag_t> tags;	// 用来存储找到的标签
        get_tags_in_range(tags,	 // 从输入数据端口(PORT_INPUTDATA)中获取一个范围内的标签,并把这些标签存储到tags中
                          PORT_INPUTDATA,
                          base_offset + skip_items,
                          base_offset + max_rel_offset,
                          d_trigger_tag_key);
		/*
			如果找到了标签,则按照偏移量对它们进行排序。
			取排序后的第一个标签的相对偏移量(相对于base_offset),并与当前的rel_offset比较。如果找到的标签偏移量更小,则更新rel_offset为该标签偏移量。
		*/
        if (!tags.empty()) {
            std::sort(tags.begin(), tags.end(), tag_t::offset_compare);
            const int tag_rel_offset = tags[0].offset - base_offset;
            if (tag_rel_offset < rel_offset) {
                rel_offset = tag_rel_offset;
            }
        }
    }
    return rel_offset;	// 即找到的触发信号的相对位置(如果找到的话),或者是最大相对偏移量(如果没有找到触发信号)
} /* find_trigger_signal() */

2、general_work 函数

我们重点看 general_work 函数中的有效载荷(payload)数据的处理实现:

int header_payload_demux_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 unsigned char* in = (const unsigned char*)input_items[PORT_INPUTDATA];
    unsigned char* out_header = (unsigned char*)output_items[PORT_HEADER];
    unsigned char* out_payload = (unsigned char*)output_items[PORT_PAYLOAD];

    const int n_input_items = (ninput_items.size() == 2)
                                  ? std::min(ninput_items[0], ninput_items[1])
                                  : ninput_items[0];
    // Items read going into general_work()
    const uint64_t n_items_read_base = nitems_read(PORT_INPUTDATA);
    // Items read during this call to general_work()
    int n_items_read = 0;

#define CONSUME_ITEMS(items_to_consume)                                         \
    update_special_tags(n_items_read_base + n_items_read,                       \
                        n_items_read_base + n_items_read + (items_to_consume)); \
    consume_each(items_to_consume);                                             \
    n_items_read += (items_to_consume);                                         \
    in += (items_to_consume)*d_itemsize;
    switch (d_state) {
    	...
    	/*
			当解复用器的状态变为 STATE_PAYLOAD 时,意味着它已经成功接收到了头部(header)信息,
			并准备处理接下来的有效载荷数据。这个状态下的主要任务是从输入数据流中读取有效载荷数据,
			然后将这些数据发送到输出端口。
		*/
    	case STATE_PAYLOAD:	// 有效载荷(payload)数据
        // Assumptions:
        // - Input buffer is in the right spot to just start copying
        /*
        	检查缓冲区是否准备好
			首先,通过调用 check_buffers_ready 函数来检查是否有足够的输入和输出缓冲区空间来
			处理当前的有效载荷长度。这个检查确保了在开始复制数据之前,输入和输出都已经准备妥当。

			这些参数用来判断是否满足处理当前有效载荷的条件:
			d_curr_payload_len是当前有效载荷的长度。
			noutput_items, ninput_items, 和 n_items_read分别表示输出项数、输入项数和已读项数,
			
		*/ 
        if (check_buffers_ready(d_curr_payload_len, // 当前有效载荷的长度
                                0,
                                noutput_items,	// 输出项数
                                d_curr_payload_len * (d_items_per_symbol + d_gi),
                                ninput_items,	// 输入项数
                                n_items_read)) {	// 已读项数
            // Write payload
            /*
            	写入有效载荷:
            	如果缓冲区检查通过,copy_n_symbols 函数会被调用来从输入缓冲区(in)复制有效载荷数据
            	到输出缓冲区(out_payload)。复制的数据量基于当前的有效载荷长度(d_curr_payload_len)
            	和每个符号的项目数(d_items_per_symbol加上d_gi,d_gi是一个保护间隔)。
			*/
            copy_n_symbols(in,
                           out_payload,
                           PORT_PAYLOAD,
                           n_items_read_base + n_items_read,
                           d_curr_payload_len);
            // Consume payload
            // We can't consume the full payload, because we need to hold off
            // at least the padding value. We'll use a minimum padding of 1
            // item here.

			/*
				消耗输入项:
				完成数据复制后,需要更新已处理的输入项计数。不过,这里有一个微妙之处:
				我们不能简单地消耗掉所有的有效载荷数据,因为需要保留一定的“填充”数据以
				确保数据的完整性。因此,计算items_to_consume时会减去一个最小的填充项数,
				通常至少为1。这确保了在当前处理周期结束时,输入缓冲区中还留有一些数据,
				以便后续的处理。
			*/
            const int items_padding = std::max(d_header_padding_total_items, 1);
            const int items_to_consume =
                d_curr_payload_len * (d_items_per_symbol + d_gi) - items_padding;
            CONSUME_ITEMS(items_to_consume);
            set_min_noutput_items(d_output_symbols ? 1 : (d_items_per_symbol + d_gi));

			/*
				更新状态:
				最后,解复用器的状态被设置回STATE_FIND_TRIGGER,这意味着在处理完当前有效载荷后,
				解复用器将重新开始寻找下一个触发信号,以准备接收下一个数据包的头部。
			*/
            d_state = STATE_FIND_TRIGGER;
        }
        break;
    	...
    }

3、问题所在

总的来说,丢帧的原因就是相邻两个定时信号的间隔过短时,导致当前帧提取数据时将后一个帧数据的定时信号作为当前帧的数据一并读入,这样就丢失了下一帧数据的定时信号,因此就造成了丢帧的现象。这种现象是源码中固有的问题。具体分析如下:

下图中数据与触发信号是严格执行对应位置的并行传输关系,Header/Payload Demux 模块先读取 trigger 信号,当读到值为 1 时就被认为是一帧数据的开始,这时就从数据信号的相应位置开始往后提取 959 个数据作为当前帧的数据进行输出。
在这里插入图片描述
根据源码的数据处理过程,源码中每次接收到定时信号后,都会提取紧跟着该定时信号后面的 959 个数据作为当前帧进行输出,因此这对定时信号的精确型提出了很高的要求,如果相邻两个定时信号的间隔出现了小于正常数据帧长度的偏差,比如正常间隔为 960,如果此时出现了间隔为 958 的间隔,如下图,则在提取后续 959 个数据的时候就会正好把下一帧的定时信号当作当前帧的数据一起读入,这样就丢失了下一帧数据的定时信号,因此就造成了丢帧的现象。
在这里插入图片描述

三、修改源码

解决这个问题的方法就是在源码中进行修改,在保证相邻定时信号不想相互干扰的基础上再重新进行源码的编译安装。需要修改的源码部分为 gr-digital/lib/header_payload_demux_impl.cc 以及 gr-digital/lib/header_payload_demux_impl.h

相关修改以及详解以放到文末,有需要的通信爱好者可自取。

find_trigger_signal() 部分代码
在这里插入图片描述

general_work() 部分代码
在这里插入图片描述

四、运行结果

1、频谱

使用 USRP 自收自发 OFDM 收发端频谱如下图:
在这里插入图片描述

2、传输数据测试

使用 USRP 自收自发 OFDM 随机数传输测试:
在这里插入图片描述
可以看到,误码率为 0

五、调试小技巧

如何在 GNU Radio 中添加调试打印信息方便分析程序执行流程?

#include <iostream>
std::cout << "Debug: The value of variable is " << variable << std::endl;

例如下面我加了一些打印信息用于打印相关变量
在这里插入图片描述
在这里插入图片描述

更改后编译出现下面信息:

/usr/include/uhd/types/sensors.hpp:133: Warning 362: operator= ignored
/usr/include/uhd/types/dict.hpp:144: Warning 503: Can’t wrap ‘operator std::mapstd::string,std::string’ unless renamed to a valid identifier.

这些编译警告信息来自于 SWIG(Simplified Wrapper and Interface Generator)在处理 C++ 代码时遇到的特定情况。SWIG 是一个通常用于将 C 或 C++ 代码包装成其他编程语言可调用的库的工具,例如在 GNU Radio 项目中将 C++ 代码包装成 Python 模块。这些警告具体涉及到如何处理 C++ 中的运算符重载和特定类型的转换。这些警告通常不会阻止你的程序编译或运行,不用理会即可。

六、资源自取

链接:解决GNU Radio+USRP实现OFDM收发在接收端存在误码问题
在这里插入图片描述


我的qq:2442391036,欢迎交流!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/511527.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

游戏引擎中的物理系统

一、物理对象与形状 1.1 对象 Actor 一般来说&#xff0c;游戏中的对象&#xff08;Actor&#xff09;分为以下四类&#xff1a; 静态对象 Static Actor动态对象 Dynamic Actor ---- 可能受到力/扭矩/冲量的影响检测器 TriggerKinematic Actor 运动学对象 ---- 忽略物理法则…

华为审核被拒提示: 您的应用存在(最近任务列表隐藏风险活动)的行为,不符合华为应用市场审核标准

应用审核意见&#xff1a; 您的应用存在&#xff08;最近任务列表隐藏风险活动&#xff09;的行为&#xff0c;不符合华为应用市场审核标准。 修改建议&#xff1a;请参考测试结果进行修改。 请参考《审核指南》第2.19相关审核要求&#xff1a;https://developer.huawei.com/c…

【opencv】教程代码 —videoio(2)将两个视频的每一帧逐一读取并计算其PSNR 和MSSIM...

本教程开始介绍的源代码将对每一帧执行PSNR测量&#xff0c;并且只对PSNR低于输入值的帧进行SSIM测量。为了可视化的目的&#xff0c;我们在OpenCV窗口中展示两幅图像&#xff0c;并将PSNR和MSSIM值打印到控制台。期望看到如下内容&#xff1a; video-input-psnr-ssim.cpp 将两…

JeeSite Vue3:前端开发控制实现基于身份角色的权限验证

随着技术的飞速发展&#xff0c;前端开发技术日新月异。在这个背景下&#xff0c;JeeSite Vue3 作为一个基于 Vue3、Vite、Ant-Design-Vue、TypeScript 和 Vue Vben Admin 的前端框架&#xff0c;引起了广泛关注。它凭借其先进的技术栈和丰富的功能模块&#xff0c;为初学者和团…

IP代理检测:判断IP质量优劣要注意什么?

做跨境电商的用户们往往对IP代理这个词都不会感到陌生&#xff0c;那么如何去评判IP的优劣势以及再选择IP时需要注意什么呢&#xff1f; 首先要知道的是IP代理检测是确保网络安全、提高网络访问效率以及满足特定需求的重要步骤。在判断IP代理质量优劣时&#xff0c;有几个关键…

使用阿里云试用Elasticsearch学习:1.1 基础入门——入门实践

阿里云试用一个月&#xff1a;https://help.aliyun.com/search/?kelastic&sceneall&page1 官网试用十五天&#xff1a;https://www.elastic.co/cn/cloud/cloud-trial-overview Elasticsearch中文文档&#xff1a;https://www.elastic.co/guide/cn/elasticsearch/guide…

剑指Offer题目笔记24(集合的组合、排序)

面试题79&#xff1a; 问题&#xff1a; ​ 输入一个不含重复数字的数据集合&#xff0c;找出它的所有子集。 解决方案&#xff1a; ​ 使用回溯法。子集就是从一个集合中选出若干元素。如果集合中包含n个元素&#xff0c;那么生成子集可以分为n步&#xff0c;每一步从集合中…

数据可视化:智慧农业发展的催化剂

数据可视化在智慧农业中发挥着不可替代的作用。随着科技的不断进步&#xff0c;农业领域也在不断探索创新&#xff0c;以提高生产效率、优化资源利用&#xff0c;从而实现可持续发展。而数据可视化技术的应用&#xff0c;则成为了实现智慧农业目标的重要途径。下面我就从可视化…

ABAP OOALV标题设置

ABAP OOALV标题设置 OOALV默认标题是SAP&#xff0c;需要我们自己创建GUI 标题 创建GUI 标题&#xff0c;写好要展示的描述 添加截图中的代码即可。 下面的ALV 报表标题修改的位置在以下代码区域。 这时候通过查询layout&#xff08;wa_layout TYPE lvc_s_layo&#xff0…

mini2440移植lvgl(v8.2)

目录 概述 1 下载源码 1.1 登录LVGL git地址 1.2 LVGL linux平台上的库文件介绍 1.3 下载代码 1.3.1 下载lvgl 1.3.2 下载lv_drivers 1.3.3 下载lv_port_linux_frame_buffer 2 配置编译环境 2.1 创建工程目录 2.2 完善工程目录下的文件 2.2.1 构建工程文件 2.2.2 匹…

Oracle常用sql命令(新手)

1、备份单张表 创建复制表结构 create table employeesbak as select * from cims.employees 如果只复制表结构&#xff0c;只需要在结尾加上 where 10 插入数据 insert into employeesbak select * from cims.employees 删除一条数据 delete from…

水泥设备如何实现物联网远程监控?

水泥设备如何实现物联网远程监控&#xff1f; 在当今的工业4.0时代&#xff0c;水泥行业正在经历一场深度的技术革新&#xff0c;其中构建智慧工厂并采用物联网远程监控解决方案成为了提升生产效率、保障产品质量、实现节能减排的关键路径。该方案通过集成先进的信息技术、物联…

list使用与模拟实现

目录 list使用 reverse sort unique splice list模拟实现 类与成员函数声明 节点类型的定义 非const迭代器的实现 list成员函数 构造函数 尾插 头插 头删 尾删 任意位置插入 任意位置删除 清空数据 析构函数 拷贝构造函数 赋值重载函数 const迭代器的设计 …

【PostgreSQL】用pgAdmin轻松管理PostgreSQL

pgAdmin 是一个功能强大的开源Web界面工具&#xff0c;专为管理和维护PostgreSQL数据库而设计。它提供了一个直观的图形界面&#xff0c;使得用户能够轻松地执行复杂的数据库操作&#xff0c;如查询、更新、导入/导出数据以及管理数据库对象等。pgAdmin 支持几乎所有的PostgreS…

EasyExcel 模板导出excel、合并单元格及单元格样式设置。 Freemarker导出word 合并单元格

xls文件&#xff1a; 后端代码&#xff1a; InputStream filePath this.getClass().getClassLoader().getResourceAsStream(templateFile);// 根据模板文件生成目标文件ExcelWriter excelWriter EasyExcel.write(orgInfo.getFilename()).excelType(ExcelTypeEnum.XLS).withTe…

redis 数据库的安装及使用方法

目录 一 关系数据库与非关系型数据库 &#xff08;一&#xff09;关系型数据库 1&#xff0c;关系型数据库是什么 2&#xff0c;主流的关系型数据库有哪些 3&#xff0c;关系型数据库注意事项 &#xff08;二&#xff09;非关系型数据库 1&#xff0c;非关系型数据库是…

37.HarmonyOS鸿蒙系统 App(ArkUI) 创建第一个应用程序hello world

HarmonyOS App(ArkUI) 创建第一个应用程序helloworld 线性布局 1.鸿蒙应用程序开发app_hap开发环境搭建 3.DevEco Studio安装鸿蒙手机app本地模拟器 打开DevEco Studio,点击文件-》新建 双击打开index.ets 复制如下代码&#xff1a; import FaultLogger from ohos.faultL…

鸿蒙OS元服务开发说明:【WebGL网页图形库开发接口】

一、场景介绍 WebGL主要帮助开发者在前端开发中完成图形图像的相关处理&#xff0c;比如绘制彩色图形等。目前该功能仅支持使用兼容JS的类Web开发范式开发。 二、接口说明 表1 WebGL主要接口列表 鸿蒙OS开发更多内容↓点击HarmonyOS与OpenHarmony技术鸿蒙技术文档开发知识更…

elment UI el-date-picker 月份组件选定后提交后台页面显示正常,提交后台字段变成时区格式

需求&#xff1a;要实现一个日期的月份选择<el-date-picker :typeformData.dateType :value-formatdateFormat v-modelformData.leaveFactoryDateplaceholder选择月份></el-date-picker>错误示例&#xff1a;将日期显示类型(type)dateType或将日期绑定值的格式(val…

Java SpringBoot中优雅地判断一个对象是否为空

在Java中&#xff0c;可以使用以下方法优雅地判断一个对象是否为空&#xff1a; 使用Objects.isNull()方法判断对象是否为空&#xff1a; import java.util.Objects;if (Objects.isNull(obj)) {// obj为空的处理逻辑 }使用Optional类优雅地处理可能为空的对象&#xff1a; impo…