世界的本质是旋转(7) PSK 接收机上层同步技巧以及8PSK解调的额外处理

上一篇文章里,我们以BPSK为例子,介绍了nPSK(n=2,4,8)波形的接收、解调中的同步技术。

前文阐述的同步技术所工作的对象是复平面的坐标,X轴是实部、Y轴是虚部。当完成时钟、频率同步后,就获得了一串整数,也就是解调的结果了。但还有很多其他的工作有待完成。调制与解调只是协议栈最底层的部分。本节,会继续介绍码流层面的同步技术。同时,在文章的最后,会给出这种野路子协议栈的缺陷,以及学习通信原理时需要具备的认知:书本的流程和现实实验之间存在大量的技巧知识空隙,需要仔细琢磨和学习思考。

1. 选取较好的符号映射方案

对BPSK来说,符号取值0,1;QPSK为0,1,2,3;8PSK为0~7的整数。

在调制阶段,如何把这些整数填写到复平面,是有一定讲究的。主要的原则有两个:

  1. 0直流分量。各个符号如果是均匀出现的,则其重心应位于原点。含有直流分量的方案在经过各级数字、模拟处理后,直流分量会丢失或者导致畸变,从电气角度也是不好的。
  2. 相邻位置01跳变最小。在复平面上相邻的位置是比较容易出错的。因此要把二进制长相类似的符号作为邻居,而不只是算术相邻的。比如 1和2算术相邻,但是二进制相差2比特,01,10,如果相邻判错符号,一下就引入2bit错误。因此1最好与0、3相邻。

采用上述原则的映射,就是最常见的Gray映射

	static const int sbm_qpsk[4][2] = {{1,1},{1,-1},{-1,1},{-1,-1}};
	static const double sbm_8psk[8][2] = {
		{cos(c_pi * 0 /4),sin(c_pi * 0 /4)},//000,0
		{cos(c_pi * 1 /4),sin(c_pi * 1 /4)},//001,1
		{cos(c_pi * 3 /4),sin(c_pi * 3 /4)},//010,2
		{cos(c_pi * 2 /4),sin(c_pi * 2 /4)},//011,3
		{cos(c_pi * 7 /4),sin(c_pi * 7 /4)},//100,4
		{cos(c_pi * 6 /4),sin(c_pi * 6 /4)},//101,5
		{cos(c_pi * 4 /4),sin(c_pi * 4 /4)},//110,6
		{cos(c_pi * 5 /4),sin(c_pi * 5 /4)} //111,7
	};

行Q列I-11
120
-131
clc;
clear;
theta = 0:pi/4:pi*2-pi/4;
r = ones(1,8);
polar(theta,r,'r*');

sbcode = [
		cos(pi * 0 /4),sin(pi * 0 /4);
		cos(pi * 1 /4),sin(pi * 1 /4);
		cos(pi * 3 /4),sin(pi * 3 /4);
		cos(pi * 2 /4),sin(pi * 2 /4);
		cos(pi * 7 /4),sin(pi * 7 /4);
		cos(pi * 6 /4),sin(pi * 6 /4);
		cos(pi * 4 /4),sin(pi * 4 /4);
		cos(pi * 5 /4),sin(pi * 5 /4)];

textitmBIN = ['000';'001';'010';'011';'100';'101';'110';'111'];
textitmDEC = ['0';'1';'2';'3';'4';'5';'6';'7'];
text(sbcode(:,1)*0.9,sbcode(:,2)*0.9,textitmBIN,'color','blue');
text(sbcode(:,1)*0.8,sbcode(:,2)*0.8,textitmDEC,'color','red');

8psk可以看到,上图的复平面位置(红色星号)相邻位置上仅反转1个比特。

2 接收相位模糊处理

由于我们的锁相环路锁定的是相对的位置,即随便选取一个初始相位为参考0,进行同步,故而接收到的复平面坐标极有可能是整体旋转一个角度的。旋转的方案总共就n种。对BPSK,只有2种,QP4种,8P8种。

如果在解调时,直接按照没有旋转的情况已经做了判决,则可以通过代换表的方法来方便获取旋转后的方案。代换的思路是“旋转后,目前认为是m的位置,实际应该是n”。生成代换表的8PSK Octave程序:

code = [0 1 3 2 6 7 5 4];
rota = [0 1 3 2 6 7 5 4];
mapt = [0 0 0 0 0 0 0 0];
for turn_idx = 1:8
  mapt(code+1) = rota;
  disp(mapt);
  tmp = rota(1);
  rota(1:7) = rota(2:8);
  rota(8) = tmp;
end

输出:

   0   1   2   3   4   5   6   7
   1   3   6   2   0   4   7   5
   3   2   7   6   1   0   5   4
   2   6   5   7   3   1   4   0
   6   7   4   5   2   3   0   1
   7   5   0   4   6   2   1   3
   5   4   1   0   7   6   3   2
   4   0   3   1   5   7   2   6

整理:

	static const int turnningMap[8][8] = {
		{0,1,2,3,4,5,6,7},
		{1,3,6,2,0,4,7,5},
		{3,2,7,6,1,0,5,4},
		{2,6,5,7,3,1,4,0},
		{6,7,4,5,2,3,0,1},
		{7,5,0,4,6,2,1,3},
		{5,4,1,0,7,6,3,2},
		{4,0,3,1,5,7,2,6}
	};

在使用上表时,在模糊方案mis下,按照方案0判定的符号为p,则恢复后的符号为 turnningMap[mis][p];

怎么判断哪一种旋转方案是正确的呢?

一种方法是加入一种固定的头部,如果旋转后的序列里能够找到这个头部,就对了。但加入固定头部会带来更多的开销,降低有效比特率。另一种方法是利用纠错序列的校验进行同步。本例子就是利用校验关系进行同步。

由于采用的是标准的无限长卷积纠错码,因而使用校验序列即可完成验证。校验方法参考:http://staff.ustc.edu.cn/~wyzhou/ct_chapter4.pdf,这篇中科大的论文里,通过一种局部校验矩阵实现判定。如果恢复对了,则使用校验矩阵滑动一个窗口,滑动生成的所有校验值都是0。

对于构造校验矩阵,即可以使用论文的方法,也可以使用穷尽法。满足校验关系的方案其实很多。通过下面的小程序,找一个参与校验位数最多的方案即可:

int main()
{
	srand(time(0));
	unsigned char reg[6] = { 0,0,0,0,0,0 };
	std::vector<unsigned char> vec_info;
	const int testL = 1000;
	//产生长度1000的随机编码,1/3系统卷积码,为了和8psk符号长度3对应,取1/3利于对齐
	//100,133,171
	for (int i = 0; i < testL; ++i)
	{
			const int c = rand()%2;
			int c1 = c ^ reg[1] ^ reg[2] ^ reg[4] ^ reg[5];
			int c2 = c ^ reg[0] ^ reg[1] ^ reg[2] ^ reg[5];
			vec_info.push_back(c*4 + c1 * 2 + c2);
			reg[5] = reg[4];
			reg[4] = reg[3];
			reg[3] = reg[2];
			reg[2] = reg[1];
			reg[1] = reg[0];
			reg[0] = c;
		
	}
	//称重查表,下标 0-7的二进制含有的1的个数
	const int spopcnt[] = { 0,1,1,2,1,2,2,3 };
	int maxWeight = 0;
	//穷尽校验式,2^21种
	for (int v = 0; v < (1 << (3 * 7)); ++v)
	{
		int val[7] = {(v&7),((v>>3) & 7),((v >> 6) & 7),((v >> 9) & 7),
		((v >> 12) & 7),((v >> 15) & 7),((v >> 18) & 7) };
			int ch = 0;
			for (int i = 0; i < testL - 7; i += 1)
			{
				for (int j = 0; j < 7; ++j)
					ch += spopcnt[(vec_info[i + j] & val[j])];
				if (ch % 2)
					break;
			}
			if (ch % 2 == 0)
			{
				if (maxWeight <= ch)
				{
					maxWeight = ch;
					printf("重量%d: ", ch);
					for (int j = 0; j < 7; ++j)
						printf("%d ", val[j]);
					printf("\n");
				}
				ch = 0;
			}

	}
	return 0;
}

输出:

重量0: 0 0 0 0 0 0 0
重量4002: 7 5 4 3 0 0 0
重量4002: 0 7 5 4 3 0 0
重量4940: 7 2 1 7 3 0 0
重量4960: 3 6 6 5 6 0 0
重量4962: 7 1 3 1 1 3 0
重量5938: 7 6 6 5 2 3 0
重量5970: 7 5 3 6 4 3 0
重量6010: 4 7 5 4 7 3 0
重量6012: 7 2 5 7 4 5 0
重量6990: 3 5 7 6 7 5 0
重量6990: 7 6 5 7 4 1 3
重量7906: 7 6 6 2 7 7 3
重量7928: 3 6 1 7 7 7 3
重量8018: 7 5 7 5 6 5 6

如此一来,只要在代码中不断检查这个校验关系,则可以找到正确的旋转方案了。

主要代码参考:

std::vector<std::vector<unsigned char> > alg_decap_8psk(const std::vector<unsigned char> & packages,void * codec)
{
	//0. Append to cache
	const  unsigned char * pSyms = (const  unsigned char *)packages.data();
	const  int sym_total = packages.size();
	for (int i=0;i<sym_total;++i)
	{
		cache_symbols[w_clk % RingBufSize]=pSyms[i];
		++w_clk;
	}
	//1. 测试相位旋转,使用http://staff.ustc.edu.cn/~wyzhou/ct_chapter4.pdf 的H校验方法
	const unsigned char test_Hq[7]{7, 5,7,5,6,5,6};
	if (r_clk + 128 > w_clk)
		return res;
	const int symbol_len = w_clk - r_clk - 2;
	static int oppo01 = 0;
	int good_times[8]{0,0,0,0,0,0,0,0};
	static const int turnningMap[8][8] = {
		{0,1,2,3,4,5,6,7},
		{1,3,6,2,0,4,7,5},
		{3,2,7,6,1,0,5,4},
		{2,6,5,7,3,1,4,0},
		{6,7,4,5,2,3,0,1},
		{7,5,0,4,6,2,1,3},
		{5,4,1,0,7,6,3,2},
		{4,0,3,1,5,7,2,6}
	};
	static int bestMis = 0,bestGood = 0;
	bestGood = 0;
	int spopcnt[8]={0,1,1,2,1,2,2,3};
	for (int pos = 0; pos < 128-7; ++pos)
	{
		for (int mis = 0;mis<8;++mis)
		{
			int chk =
				spopcnt[(turnningMap[mis][cache_symbols[(r_clk + pos + 0) % RingBufSize ]] & test_Hq[0] )%8 ]+
				spopcnt[(turnningMap[mis][cache_symbols[(r_clk + pos + 1) % RingBufSize ]] & test_Hq[1] )%8 ] +
				spopcnt[(turnningMap[mis][cache_symbols[(r_clk + pos + 2) % RingBufSize ]] & test_Hq[2] )%8 ] +
				spopcnt[(turnningMap[mis][cache_symbols[(r_clk + pos + 3) % RingBufSize ]] & test_Hq[3] )%8 ] +
				spopcnt[(turnningMap[mis][cache_symbols[(r_clk + pos + 4) % RingBufSize ]] & test_Hq[4] )%8 ] +
				spopcnt[(turnningMap[mis][cache_symbols[(r_clk + pos + 5) % RingBufSize ]] & test_Hq[5] )%8 ] +
				spopcnt[(turnningMap[mis][cache_symbols[(r_clk + pos + 6) % RingBufSize ]] & test_Hq[6] )%8 ] ;
			good_times[mis] += (chk%2) ? 0:1;
		}
	}
	//mis
	for (int i=0;i<8;++i)
	{
		if (bestGood < good_times[i])
		{
			bestGood = good_times[i];
			bestMis = i;
		}
	}
	//...
}

3 信息流的封装与处理

如何把具有不同长度的字节流,比如我们的以太网包,封装为一个连续的流呢?可以参考古老的HDLC封装方法。对于一个流,遇到连续的5个1就插入一个0.这样,采用对称序列01111110即可分割各个序列了。

在恢复时,

  1. 遇到01111110就处理缓存
  2. 当前缓存,累加到5个1,判断后面如果是0,则踢出一个。
  3. 如果有连续7个1出现,那就是错误了。提示反码或者质量太差。

相关封装代码:(解封装相反操作即可)

	//Checksum
	std::vector<unsigned char> input;
	std::copy(inputData.begin(),inputData.end(),std::back_inserter(input));
	unsigned int vsum = 0;
	unsigned char * psum = (unsigned char *) &vsum;
	for (int i=0;i<input.size();++i)
	{
		unsigned int vl = (vsum >>24) &0xff;
		unsigned int vr = input[i] ^ vl;
		vsum <<=8;
		vsum ^=vr;
	}
	input.push_back(psum[0]);
	input.push_back(psum[1]);
	input.push_back(psum[2]);
	input.push_back(psum[3]);

	unsigned long long len = input.size();
	//Bit encap using fake HDLC,校验是野路子的。不是标准HDLC
	std::vector<unsigned char> bitspan{0,1,1,1,1,1,1,0};
	int cnt = 0;
	for (int i=0;i<len;++i)
	{
		for (int j=0;j<8;++j)
		{
			const int c = (input[i]>>j)&0x01;
			bitspan.push_back(c);
			if (c)
			{
				++cnt;
				if (cnt==5)
				{
					bitspan.push_back(0);
					cnt = 0;
				}
			}
			else
				cnt = 0;
		}
	}

4 对8PSK在更高的倍率下同步

在室内载噪比较高的情况下, 8PSK能够满足传输的需要,但4倍采样速率锁相环在8PSK下还是有一定的误码。要收敛星座,需要在 4倍采样率下,上采样进行最佳位置拟合,用非最佳状态的四个点,拟合出最佳的位置。拟合对于BPSK、QPSK这种相位差达到90度、180度的方式不是很重要(信号强),但8PSK还是很重要的。因为4倍速率采样,得到的位置和真实的最佳符号时钟可能依旧误差较大,请看下图:

filter上图中,红色的四倍采样位置与黑色的理论位置没有重合,无论如何选取4路中的一路,得到的向量点都是不聚焦的,会影响质量。

要更好地计算实际的黑色位置,需要额外的计算量和算法。我们的例子采用了二次上采样,导致计算量增加,这是一种野路子的缺陷。

代码参考:

https://gitcode.net/coloreaglestdio/taskbus_course/-/tree/master/src

5 效果与不足

我们使用第二组上采样x4滤波器进行插值,从而一个周期内的位置从4选一增加到16选1,可以很好的锁定最佳采样位置:

8psl

更为完善的解调还要包含多径衰落处理,这些工作只有结合具体的场景和波形的特点来做了。8PSK的二次上采样解调代码参考 :我的代码库

软件实现整个协议栈的一个重要问题是实时性与最大带宽。例子里都是1MHz采样率处理250KBd的波形,计算机是可以应对的。但如果带宽成倍增加,就需要很多技巧了。

  1. 整形定点处理
  2. SIMD加速
  3. 三角函数查表
  4. 并行计算

其中,2、4如果操作不好,会带来大延迟。1、3操作不好,会造成溢出。这个实验并不打算尽善尽美地实现上述功能,那样会使整个逻辑结构极为拗口,不利于理解算法本身,陷入技巧的漩涡。本课程实验的意义在于让我们意识到,从课本上的原理图,到实际能够工作的完整协议栈之间,存在巨大的知识间隙。一旦脱离了仿真,变成SDR真刀真枪的干上了,需要解决的技术细节很多很多。

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
以下是一个简单的MATLAB代码示例,用于实现2PSK、QPSK和8PSK调制、解调以及蒙特卡罗仿真: ```matlab % 2PSK调制解调示例 M = 2; % 调制阶数 data = randi([0 M-1],1000,1);% 随机数据生成 tx = pskmod(data,M); % 2PSK调制 rx = awgn(tx,10); % 加入高斯白噪声 rx_data = pskdemod(rx,M); % 2PSK解调 SER_2psk = sum(abs(rx_data-data))/length(data) % 符号误码率 % QPSK调制解调示例 M = 4; % 调制阶数 data = randi([0 M-1],1000,1);% 随机数据生成 tx = pskmod(data,M); % QPSK调制 rx = awgn(tx,10); % 加入高斯白噪声 rx_data = pskdemod(rx,M); % QPSK解调 SER_qpsk = sum(abs(rx_data-data))/length(data) % 符号误码率 % 8PSK调制解调示例 M = 8; % 调制阶数 data = randi([0 M-1],1000,1);% 随机数据生成 tx = pskmod(data,M); % 8PSK调制 rx = awgn(tx,10); % 加入高斯白噪声 rx_data = pskdemod(rx,M); % 8PSK解调 SER_8psk = sum(abs(rx_data-data))/length(data) % 符号误码率 % 蒙特卡罗仿真 SNR = 0:2:20; % 信噪比范围 M = 2; % 调制阶数 for i = 1:length(SNR) data = randi([0 M-1],1000,1); % 随机数据生成 tx = pskmod(data,M); % 2PSK调制 rx = awgn(tx,SNR(i)); % 加入高斯白噪声 rx_data = pskdemod(rx,M); % 2PSK解调 SER_2psk(i) = sum(abs(rx_data-data))/length(data); % 符号误码率 end semilogy(SNR,SER_2psk,'b-*'); % 绘制2PSK误码率曲线 xlabel('SNR (dB)'); ylabel('Symbol Error Rate'); grid on; ``` 这里只给出了一个简单的示例,你可以根据实际需求进行修改和扩展。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁劲犇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值