• 使用C语言+USRP B210从零开始实现无线通信(4) 接收检测与解调


    上一篇文章中,已经让数据通过声卡或者SDR播放/发射出去了。经过传输,在接收方可以获取到发送的波形。作为接收方,要做的第一步就是波形的接收与检测,并对检测的波形进行解调获得01二进制数据流。

    1. 接收波形

    室内接收波形,考虑饱和比考虑噪声要重要。这一点和实际的无线通信正好相反。在实验室的环境下,控制好发射增益和接收放大器,使得接收到的波形的振幅处于A/D量化振幅的20%-80%左右,一般效果叫好。

    • 过小的振幅,会增大量化噪声,浪费A/D芯片的比特位宽。
    • 过大的振幅,会发生截断。不过,对于DASK波形,只要不是很离谱,基本影响不大。

    通过调节增益,让波形看起来平稳就可以,比如下图:
    恰到好处的幅度
    当然,这是实验室里得到的理想波形,实际波形因为干扰的存在,噪声不可能这样小。因此,先调节发射增益,确保载噪比较高;再调节接收增益,确保绝对电平大小范围合适。

    2. 锁定最佳采样时刻

    由于接收与发射双方的时钟往往存在误差,并且是不同步的(也有无线协议实现了预同步。使得同步后的收发时钟误差在1/N个符号/比特之内,但我们没有实现这样的功能),故而必须在接收到波形后,决定从哪个采样点进行提取,能够恰好提取到峰值附近。比如下图:

    采样在N倍采样率下进行符号提取,同样是每N个位置取1个值,在黑色位置取值,要比红色位置好得多。实现这种最佳位置的选取,一般可以直接用大小比较法,比较以各组位置为起点的情况下,哪个起点抽取的波形的电平绝对值最高。注意,由于SDR每次返回的样点数不一定是N的倍数,所以要进行缓存、取整、留尾巴。

    	QVector<unsigned char> cache;
    		while (false==bfinished)
    		{
    			subject_package_header header;
    			vector<unsigned char> packagedta = pull_subject(&header);
    			//...
    			if (is_wav(packagedta ))
    			{
    				std::copy(packagedta.begin(),packagedta.end(),std::back_inserter(cache));
    				short (*pIQ)[2] = (short (*)[2]) cache.data();
    				//xN = x15
    				std::vector<unsigned short> Msg[15];
    				long long sum4[15] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0};
    				const int pts = cache.size()/4;
    				const int groups = pts/15;
    				for (int i=0;i<groups;++i)
    				{
    					for (int s = 0;s<15;++s)
    					{
    						long long i1 = pIQ[i*15+s][0];
    						long long q1 = pIQ[i*15+s][1];
    						long long amp = i1 * i1 + q1 * q1;
    						unsigned short va = sqrt(amp*1.0/4) + .5;
    						Msg[s].push_back(va);
    						sum4[s]+=va;
    					}
    				}
    				long long max_i = 0, max_v = 0;
    				for (int i=0;i<15;++i)
    				{
    					if (max_v < sum4[i])
    					{
    						max_v = sum4[i];
    						max_i = i;
    					}
    				}
    				//..
    				//Dealing Msg[max_i];
    				cache.remove(0,groups*4*15);
    			}
    		}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    效率警告: 真正的SDR通信系统中,是不用动态内存的。一般用一个环状缓存代替动态内存。这里为了编程方便直观,还是使用动态内存的方法。

    3. 检测

    在没有通信的时刻,收到的只有噪声。需要用前文定义的头部符号,来检测波形的起点.下面这个程式使用最简单的大小比较来去差分得到0或者1:

    std::vector<std::vector<unsigned char> > alg_decap(const std::vector<unsigned char> & packages,void * codec)
    {
    	std::vector<std::vector<unsigned char> > res;
    	//头部: {0,1,1,1,1,1,1,0, 0,0,1,1,0,1,0,1, 0,0,1,0,1,0,0,1, 1,1,0,1,0,1,0,1};
    	unsigned char header[32] = {0,1,1,1,1,1,1,0, 0,0,1,1,0,1,0,1, 0,0,1,0,1,0,0,1, 1,1,0,1,0,1,0,1};
    	//0. Append to cache,因为不一定能够攒齐完整的数据,需要进行缓存。在实际系统中,请使用环状缓存。
    	const  unsigned short * pSyms = (const  unsigned short *)packages.data();
    	const  int sym_total = packages.size()/2;
    	std::copy(pSyms,pSyms+sym_total,std::back_inserter(cache_bits));
    
    	//检测循环
    	while (cache_bits.size())
    	{
    		//1.Find header
    		int nPos = 0;
    		int nStart = -1;
    		int Oppo = 0;
    		int errb = 0;
    		while (nPos + 64 <cache_bits.size() && nStart<0)
    		{
    			errb = 0;
    			//头部检测
    			for (int i=0;i<32;++i)
    			{
    				int bt = cache_bits[nPos+i*2] > cache_bits[nPos+i*2+1]?1:0;
    				if (bt!=header[i])
    					++errb;
    			}
    			//注意,检测到取反的结果,也认为是正确的。这在某些调制中会出现。在这个例子里,我们保留这个操作,因为未来可能会替换调制解调方式。
    			if (errb==0||errb==32)
    			{
    				nStart = nPos;
    				Oppo = errb<3?0:1;
    			}
    			++nPos;
    		}
    		if (nStart>=0)
    		//找到了
    			cache_bits.remove(0,nStart);
    	
    	
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    使用 Audacity可以较为直观的查看导出的RX波形。
    Audacity

    4. 解调长度字段

    一旦捕获了首部,即可提取经过扩频保护的长度字段。提取的方法,就是比较每个扩频符号与0,1符号的相似度。这样做,容错性很高。

        //接上一块代码
    		//2. Length 16 bits
    		//Length, protected by coding spread 1:8,16bits:128bits
    		if (cache_bits.size()<(16*8+32)*2)
    			return res;
    		unsigned char sprheader[2][8] = {
    			{1,1,0,1,0,0,0,1},
    			{0,1,0,1,1,0,1,1}
    		};
    		unsigned int len = 0;
    		for (int i=0;i<16;++i)
    		{
    			int p0 = 0, p1 = 0;
    			for (int j=0;j<8;++j)
    			{
    				int b0 = cache_bits[64+i*8*2+j*2] < cache_bits[64+i*8*2+j*2+1]?0:1;
    				p0 += (b0==((sprheader[0][j]+Oppo)%2))?1:0;
    				p1 += (b0==((sprheader[1][j]+Oppo)%2))?1:0;
    			}
    			unsigned int b = p0>=p1?0:1;
    			len ^= (b<<i);
    		}
    		//长度存储在len里
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    5. 解调数据段

    数据字段没有扩频保护,使用的是卷积码进行纠错。直接使用前后时刻幅度大小进行判决,完成解调。

       //接上文
       		//3. decode
    		std::vector<int> code;
    		const int valid_syms = len*8 + 7;
    		for (int i=0;i<valid_syms+VIT_WINLEN;++i)
    		{
    			int v = 0;
    			if (i<valid_syms)
    			{
    				int b1 = ((cache_bits[(32+128)*2+(i*3+0)*2]<cache_bits[(32+128)*2+(i*3+0)*2+1])?0:1) ^ Oppo;
    				int b2 = ((cache_bits[(32+128)*2+(i*3+1)*2]<cache_bits[(32+128)*2+(i*3+1)*2+1])?0:1) ^ Oppo;
    				int b3 = ((cache_bits[(32+128)*2+(i*3+2)*2]<cache_bits[(32+128)*2+(i*3+2)*2+1])?0:1) ^ Oppo;
    				v = (b1<<2) ^ (b2<<1) ^ b3;
    			}
    			code.push_back(v);
    		}
    		//后续就只要调用纠错算法
    		//……
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    6.小结

    至此,已经恢复了可能含错的原始数据,存储在code里。检测解调部分,主要的关键注意点:

    • 选择与调制阶段相匹配的接收速率。
    • 跟踪最佳的符号判决位置(最大的幅度)。
    • 数据的凑整和完整等待,例子中使用线性向量,真正工程中建议使用预分配的环状缓存。
  • 相关阅读:
    实现Element Select选择器滚动加载
    Windows + Msys 下编译 TensorFlow 2.14
    iApp祁天社区UI成品源码 功能齐全的社区应用
    Flullter学习第一天:什么是Flullter与Flullter安装
    《FFmpeg Basics》中文版-03-比特率/帧率/文件大小
    [附源码]计算机毕业设计springboot求职招聘网站
    动态规划01 背包问题(算法)
    IDEA配置tomcat运行环境
    C++ 指针的算术运算
    Android WiFi ip显示
  • 原文地址:https://blog.csdn.net/goldenhawking/article/details/126780503