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

news2024/11/25 12:31:11

文章目录

  • 前言
  • 一、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,欢迎交流!


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

游戏引擎之高级动画技术

一、动画混合 当我们拥有各类动画素材&#xff08;clips&#xff09;时&#xff0c;要将它们融合起来成为一套完整的动画。 最经典的例子就是从走的动画自然的过渡到跑的动画。 1.1 线性插值 不同于上节课的LERP&#xff08;同一个clip内不同pose之间&#xff09;&#xff…

学习使用echats因xAxis值过多,可以滚动的柱状图解决方案

学习使用echats因xAxis值过多&#xff0c;可以滚动的柱状图解决方案 效果图柱状图代码关键代码 效果图 柱状图代码 function echarts() {// 基于准备好的dom&#xff0c;初始化echarts实例var myChart echarts.init(document.getElementById(echart4));let xaxisData [1, 2,…

HTML常用标签-最基础的标签

从本篇开始&#xff0c;我们围绕HTML原生标签开始&#xff0c;围绕整个前端三剑客进行&#xff0c;将进行一个大致的介绍和案例展示&#xff0c;没有啥技术含量&#xff0c;只是把学习前端的时候&#xff0c;案例全部展示出来&#xff0c;作为一个实时记录&#xff0c;或者说回…

Redis高可用(持久化)

目录 一、Redis高可用 1. Redis高可用概述 2. Redis高可用策略 二、Redis持久化 1. Redis持久化的功能 2. Redis持久化的两种方式 3. RDB持久化 3.1 触发条件 3.1.1 手动触发 3.1.2 自动触发 ① 配置方式 ②其他自动触发机制 3.2 执行流程 3.3 启动时加载 4. AOF…

jnpf3.6私有化部署

文件内容 project web > 特别说明&#xff1a;源码、JDK、MySQL、Redis等安装或存放路径禁止包含中文、空格、特殊字符等## 一 技术栈- 主框架&#xff1a;Spring Boot Spring Framework - 持久层框架&#xff1a;MyBatis-Plus - 数据库连接池&#xff1a;Alibaba Druid -…

商场促销--策略模式

1.1 商场收银软件 package com.lhx.design.pattern.test;import java.util.Scanner;public class Test {public static void main(String[] args){System.out.println("**********************************************"); System.out.println("《大话设计模式…

坦白局:PMP真的是智商税吗?

近些年报考PMP认证的学员越来越多&#xff0c;PMP全球持证人数已经突破百万了&#xff0c;据PMI统计&#xff0c;IT行业近50%人士都持有PMP证书&#xff0c;因此也有很多学员在思考&#xff0c;PMP持证人员这么多&#xff0c;PMP是不是都已经烂大街了&#xff1f;证书还有含金量…

看完不会来揍我 | R包的下载与安装 | 再也没有一个包可以逃出你的手掌心啦

好久不见&#xff01;非常抱歉有一段时间没有更新正经内容啦&#xff01;主要是最近接了一个项目和一个一对一指导&#xff0c;实在是精力有限&#xff0c;又不想随便写几篇应付大家。毕竟&#xff0c;咱们主打高质量嘛&#xff01;来&#xff01;大声喊出来&#xff01; 「要知…

英伟达智算先锋训练,冲刺智算时代实战

随着数字经济的深入发展&#xff0c;智能算力作为关键生产力&#xff0c;其规模在2022年已达到268.0 EFLOPS&#xff0c;并预计到2028年将增长至2769 EFLOPS&#xff0c;显示出强劲的发展势头。在2024年政府工作报告中&#xff0c;也首次提出了“人工智能”行动&#xff0c;强调…

Golang 内存管理和垃圾回收底层原理(二)

一、这篇文章我们来聊聊Golang内存管理和垃圾回收&#xff0c;主要注重基本底层原理讲解&#xff0c;进一步实战待后续文章 垃圾回收&#xff0c;无论是Java 还是 Golang&#xff0c;基本的逻辑都是基于 标记-清理 的&#xff0c; 标记是指标记可能需要回收的对象&#xff0c…

Java设计之道:色即是空,空即是色

0.引子 我们的这个世界上&#xff0c;存在这么一种东西&#xff1a; 第一&#xff1a;它不占据任何3D之体积&#xff0c;即它没有Volume第二&#xff1a;它也不占据任何2D之面积&#xff0c;即它没有Area第三&#xff1a;它也不占据任何1D之长度&#xff0c;即它没有Length 总…

【容易不简单】love 2d Lua 俄罗斯方块超详细教程

源码已经更新在CSDN的码库里&#xff1a; git clone https://gitcode.com/funsion/love2d-game.git 一直在找Lua 能快速便捷实现图形界面的软件&#xff0c;找了一堆&#xff0c;终于发现love2d是小而美的原生lua图形界面实现的方式。 并参考相关教程做了一个更详细的&#x…

【深耕 Python】Data Science with Python 数据科学(7)书352页练习题

写在前面 关于数据科学环境的建立&#xff0c;可以参考我的博客&#xff1a; 【深耕 Python】Data Science with Python 数据科学&#xff08;1&#xff09;环境搭建 往期数据科学博文&#xff1a; 【深耕 Python】Data Science with Python 数据科学&#xff08;2&#xf…

苹果开发者账号注册步骤中的常见疑问解答与技巧分享

转载&#xff1a;注册苹果开发者账号的方法 在2020年以前&#xff0c;注册苹果开发者账号后&#xff0c;就可以生成证书。 但2020年后&#xff0c;因为注册苹果开发者账号需要使用Apple Developer app注册开发者账号&#xff0c;所以需要缴费才能创建ios证书了。 所以新政策出…

算法系列--递归,回溯,剪枝的综合应用(3)

&#x1f495;"对相爱的人来说&#xff0c;对方的心意&#xff0c;才是最好的房子。"&#x1f495; 作者&#xff1a;Lvzi 文章主要内容&#xff1a;算法系列–递归,回溯,剪枝的综合应用(3) 大家好,今天为大家带来的是算法系列--递归,回溯,剪枝的综合应用(3),带来几…

深入浅出:大模型产业链的全景解码

大模型产业仿佛如同一场盛宴&#xff0c;虽然AGI的菜肴还没有上桌&#xff0c;但掩盖不住的香味已经让所有人垂涎&#xff0c;都希望自己将来在餐桌上能够“吃饱饱”。这也是一场没有邀请函的宴席&#xff0c;从尚在学校的学生到公司的决策层&#xff0c;都能在生成式AI的相关研…

简单而复杂的Python

Python是一种简单&复杂的编程语言。简单的时候可以到极致&#xff1a; print(hello world!)另一方面&#xff0c;Python 也具有许多复杂的语法特性&#xff0c;例如面向对象编程、装饰器、迭代器、生成器等等。这些特性使得 Python 适用于各种不同的编程任务和项目。 当我…

【JavaWeb】Day31.SpringBootWeb请求响应——分层解耦(二)

3.IOC&DI 3.1 IOC&DI入门 完成Controller层、Service层、Dao层的代码解耦 思路&#xff1a; 1. 删除Controller层、Service层中new对象的代码 2. Service层及Dao层的实现类&#xff0c;交给IOC容器管理 3. 为Controller及Service注入运行时依赖的对象 Controller程序…

HarmonyOS 应用开发之同步任务开发指导 (TaskPool和Worker)

同步任务是指在多个线程之间协调执行的任务&#xff0c;其目的是确保多个任务按照一定的顺序和规则执行&#xff0c;例如使用锁来防止数据竞争。 同步任务的实现需要考虑多个线程之间的协作和同步&#xff0c;以确保数据的正确性和程序的正确执行。 由于TaskPool偏向于单个独…

【Java代码审计】SSTI模板注入篇

【Java代码审计】SSTI模板注入篇 1.概述2.Velocity 模板引擎3.Thymeleaf 模板注入复现普通url作为视图 4.SSTI 漏洞修复白名单控制跳转模版设置response参数 1.概述 模板引擎支持使用静态模板文件&#xff0c;在运行时用 HTML 页面中的实际值替换变量/占位符&#xff0c;从而让…