在上一篇文章中,已经让数据通过声卡或者SDR播放/发射出去了。经过传输,在接收方可以获取到发送的波形。作为接收方,要做的第一步就是波形的接收与检测,并对检测的波形进行解调获得01二进制数据流。
室内接收波形,考虑饱和比考虑噪声要重要。这一点和实际的无线通信正好相反。在实验室的环境下,控制好发射增益和接收放大器,使得接收到的波形的振幅处于A/D量化振幅的20%-80%左右,一般效果叫好。
通过调节增益,让波形看起来平稳就可以,比如下图:

当然,这是实验室里得到的理想波形,实际波形因为干扰的存在,噪声不可能这样小。因此,先调节发射增益,确保载噪比较高;再调节接收增益,确保绝对电平大小范围合适。
由于接收与发射双方的时钟往往存在误差,并且是不同步的(也有无线协议实现了预同步。使得同步后的收发时钟误差在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);
}
}
效率警告: 真正的SDR通信系统中,是不用动态内存的。一般用一个环状缓存代替动态内存。这里为了编程方便直观,还是使用动态内存的方法。
在没有通信的时刻,收到的只有噪声。需要用前文定义的头部符号,来检测波形的起点.下面这个程式使用最简单的大小比较来去差分得到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);
使用 Audacity可以较为直观的查看导出的RX波形。

一旦捕获了首部,即可提取经过扩频保护的长度字段。提取的方法,就是比较每个扩频符号与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里
数据字段没有扩频保护,使用的是卷积码进行纠错。直接使用前后时刻幅度大小进行判决,完成解调。
//接上文
//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);
}
//后续就只要调用纠错算法
//……
至此,已经恢复了可能含错的原始数据,存储在code里。检测解调部分,主要的关键注意点: