上一篇文章里,我们以BPSK为例子,介绍了nPSK(n=2,4,8)波形的接收、解调中的同步技术。
前文阐述的同步技术所工作的对象是复平面的坐标,X轴是实部、Y轴是虚部。当完成时钟、频率同步后,就获得了一串整数,也就是解调的结果了。但还有很多其他的工作有待完成。调制与解调只是协议栈最底层的部分。本节,会继续介绍码流层面的同步技术。同时,在文章的最后,会给出这种野路子协议栈的缺陷,以及学习通信原理时需要具备的认知:书本的流程和现实实验之间存在大量的技巧知识空隙,需要仔细琢磨和学习思考。
1. 选取较好的符号映射方案
对BPSK来说,符号取值0,1;QPSK为0,1,2,3;8PSK为0~7的整数。
在调制阶段,如何把这些整数填写到复平面,是有一定讲究的。主要的原则有两个:
- 0直流分量。各个符号如果是均匀出现的,则其重心应位于原点。含有直流分量的方案在经过各级数字、模拟处理后,直流分量会丢失或者导致畸变,从电气角度也是不好的。
- 相邻位置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 | -1 | 1 |
---|---|---|
1 | 2 | 0 |
-1 | 3 | 1 |
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');
可以看到,上图的复平面位置(红色星号)相邻位置上仅反转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<4;++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即可分割各个序列了。
在恢复时,
- 遇到01111110就处理缓存
- 当前缓存,累加到5个1,判断后面如果是0,则踢出一个。
- 如果有连续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倍采样率下,进行最佳位置拟合,用非最佳状态的四个点,拟合出最佳的位置。拟合对于BPSK、QPSK这种相位差达到90度、180度的方式不是很重要(信号强),但8PSK还是很重要的。
上图中,红色的四倍采样位置与黑色的理论位置没有重合,无论如何选取4路中的一路,得到的向量点都是不聚焦的,会影响质量。要更好地计算实际的黑色位置,需要额外的计算量和算法。
代码参考:
https://gitcode.net/coloreaglestdio/taskbus_course/-/tree/master/src
这个实验的意义在于让我们意识到,从课本上的原理图,到实际能够工作的完整协议栈之间,存在巨大的知识间隙。一旦脱离了仿真,变成SDR真刀真枪的干上了,需要解决的技术细节很多很多。
更为完善的解调应该同步准确的星座位置,并避免多径导致的衰落。这些工作只有结合具体的场景和波形的特点来做了。