Polar SC的C语言实现之译码篇

Part1.SC编码与比特传输过程回顾

经过编码和调制后,信息序列才得以传输,传输过程中会受到各种各样的噪声(在此指电磁波)干扰,每个比特会发生不同程度的变化,在传送到用户端前,要把比特序列还原成原码序列,这一过程简单理解,就是译码纠错。极化码具有良好的纠错性能,SC作为极化码家族中译码算法最简单的一员,在5G通信中得到了最广泛的应用。本篇博客文章仍将使用通俗易懂的文字解析SC的译码部分。

 

Part2.译码原理

这位码农,快!上图!——还是原来的配方,还是熟悉的味道,这咋跟那个编码图那么像,是不是搞错了?!

本码农:没错~就这张,只不过这次我们要反过来译码。

译码之前,我们先来了解两种运算方式:f运算与g运算。

先讲f运算,sign是指右端两个逻辑相关单元的乘积,如果是正数,则sign就表示正号“+”,如果是负数,则sign表示负号“-”;在确定好f运算左上单元的正负性后,再来确定它的大小吧,比较右端两单元的绝对值大小,取最小的单元值。例如,Lin1=-1.4,Lin2=0.1,则sign(Lin1,Lin2)为负,而绝对值最小的为0.1,因此,左端单元的运算结果为Lout=-0.1。f运算函数程序如下:

float f(float Lin1,float Lin2)
{float Lout,s,min;
int sign;
s=Lin1*Lin2;
if(s>0)sign=1;
else sign=-1;
min=fabs(Lin1)<=fabs(Lin2)?fabs(Lin1):fabs(Lin2);
Lout=sign*min;
return Lout;
}

再讲g运算,b值由左上单元确定,大于0,b=0;小于0,b=1。左下单元Lout=[(-1)^b]*右上单元值+右下单元值。如图所示:

g运算函数程序如下:

float g(float Lin1,float Lin2,int b)
{float Lout;
Lout=pow((float)-1.0,b)*Lin1+Lin2;
return Lout;
}

讲完fg运算,开始学习译码原理吧。假设左端(输出端)比特从上至下数,第1、2位是信息比特,第3、4位是固定比特,那么1、2位遵循“大于0,译成0;小于0,译成1”的规则,而3、4位直接译成0。注意一点,最左端的译码值其实也是它们的b值。

第一步:从右至左进行f运算。

第二步:前两位信息位,则大于0,译成0;小于0,译成1。同时,译码值也是b值,进行g运算求出左下方值,然后按照”大于0,译成0;小于0,译成1“原则译码。

 

接着,又回到了我们编码学过的异或运算,得出右端一层单元的b值。

得到b值后,按照红色箭头的指示进行g运算。

g运算得出新值后,再从右往左进行f运算。

其实,到了这一步,我们无需再计算了,因为3、4位是固定比特,我们可直接得到0的译码结果。

之后的计算以此类推。译码原理讲解到此结束,下面,我们开始动手写代码。

Part3.代码实战

程序要求:写码长为16的SC译码,输入模拟信噪比(建议范围:1.5~3.5dB),程序按照比特的排列规则,随机生成一次码长为16的比特序列,模拟其经过编码、调制、受白噪声干扰,被接收后,将比特序列进行SC译码纠错,还原信息序列,返回给用户端。

输出显示:1.原码序列;2.译码序列; 3.比较原码与译码序列是否完全相同,完全相同,输出right;不完全相同,输出wrong。

我们之前的编码与调制程序已经写好,我们将在注释的译码位置补全译码部分,程序如下:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<math.h>//噪声部分会用到数学函数
const int N=16; 
const int n=5;
int A[N][n],b[N][n];
float a[N][n];//增加一个float型二维数组存储加噪声后的比特
int CBR[N]={0,0,0,0,0,0,0,1,0,1,1,1,1,1,1,1};
float add_gassrand(float EbNo);//加噪声函数
float gaussrand();//生成噪声函数
//译码函数声明************************************

//************************************************ 
int main(){
    srand((unsigned)time(NULL));
	//设定种子随机数,使随机数随时间改变 
    float EbNo;//信噪比
    printf("EbNo(dB):");
    scanf("%f",&EbNo);//信噪比输入端
    printf("\n");
	int Vi,e,sum=0,s=0;
	for(Vi=0;Vi<N;Vi++)
	{if(CBR[Vi])A[Vi][0]=rand()%2; 
	//CBR数组中非0的元素是信息比特位,在对应行产生0或1的随机数 
	else A[Vi][0]=0;
	//固定比特位仍然为0 
	}
//编码 
int h=N,y1,o;
for(y1=0;y1<n-1;y1++)
{
	for(o=0;o<N;o=o+(2*N)/h)
	{for(e=o;e<o+N/h;e++)
	{A[e][y1+1]=A[e][y1]^A[e+N/h][y1]?1:0;
	//^即为异或运算符号 
	 A[e+N/h][y1+1]=A[e+N/h][y1];
	}
	}
	h/=2;
}
//调制 
for(y1=0;y1<N;y1++)
{a[y1][n-1]=A[y1][n-1]?-1.0:1.0;}
//加噪声
add_gassrand(EbNo);
//译码部分**************************************** 

//************************************************
//输出端
printf("原码序列:");
for(y1=0;y1<N;y1++)printf("%d ",A[y1][0]);
printf("\n译码序列:");
for(y1=0;y1<N;y1++)printf("%d ",b[y1][0]);
//判断正误 
int w=0; 
for(int i=0;i<N;i++)if(A[i][0]^b[i][0]){w++;}
if(w)printf("\n结果:wrong\n");
else printf("\n结果:right\n");
return 0;
}
//译码函数**************************************** 

//************************************************
float gaussrand(){
	static float V1,V2,S;
	static int phase=0;
	float X;
	if (!phase){ do{float U1=(float)rand()/RAND_MAX;
			float U2=(float)rand()/RAND_MAX;
			V1=2*U1-1;
			V2=2*U2-1;
			S=V1*V1+V2*V2;
		} while(S>=1||!S);
		X=V1*sqrt(-2*log(S)/S);
	}
	else X=V2*sqrt(-2*log(S)/S);
	phase=1-phase;
	return X;
}
float add_gassrand(float EbNo){
	int i;
	float Sigma2;//噪声方差
	float Sigma;//噪声标准差
	float Rate=(N/2)/(float)N;//数据的传输速率
	Sigma2=(float)1/(2*Rate*pow(10,(EbNo / 10.0)));//白噪声的方差
	Sigma=sqrtf(Sigma2);//白噪声的标准差
	for(i=0;i<N;i++)
	{	
		a[i][n-1]=2*(a[i][n-1]+gaussrand()*Sigma)/Sigma2;
	}
	return 0;
}

开始第一步工作——算法设计。16码长属于短码,而我们真正用于传输的往往是512、1024的码长,甚至要更长。因此,如果硬写,把一行一行、一列一列的运算一个个写出,就像千年危机那样,满足了当下,却不得不在出现bug后被动的改进。我们的方法是:以下图都覆盖的译码范围为一个单元,基于switch架构,依次调用该单元函数,并在对应行/列进行b值运算、g运算,接着使用f运算反推回去,然后继续调用单元函数……从而覆盖全部比特译码。

第二步,方案确定好后,写出我们需要的译码函数:f运算函数、g运算函数、单元函数、反推函数。

//译码函数**************************************** 
float f(float Lin1,float Lin2)//f运算函数
{float Lout,s,min;
int sign;
s=Lin1*Lin2;
if(s>0)sign=1;
else sign=-1;
min=fabs(Lin1)<=fabs(Lin2)?fabs(Lin1):fabs(Lin2);
Lout=sign*min;
return Lout;
}
float g(float Lin1,float Lin2,bool b)//g运算函数
{float Lout;
Lout=pow((float)-1.0,b)*Lin1+Lin2;
return Lout;
} 
inline void first_4(int i)//单元函数
{if(!CBR[i])b[i][0]=0;
else b[i][0]=a[i][0]>0.0?0:1;
a[i+1][0]=g(a[i][1],a[i+1][1],b[i][0]);
if(!CBR[i+1])b[i+1][0]=0;
else b[i+1][0]=a[i+1][0]>0.0?0:1;
b[i][1]=b[i][0]^b[i+1][0]?1:0;
b[i+1][1]=b[i+1][0];
a[i+2][1]=g(a[i][2],a[i+2][2],b[i][1]);
a[i+3][1]=g(a[i+1][2],a[i+3][2],b[i+1][1]);
a[i+2][0]=f(a[i+2][1],a[i+3][1]);
if(!CBR[i+2])b[i+2][0]=0;
else b[i+2][0]=a[i+2][0]>0.0?0:1;
a[i+3][0]=g(a[i+2][1],a[i+3][1],b[i+2][0]);
if(!CBR[i+3])b[i+3][0]=0;
else b[i+3][0]=a[i+3][0]>0.0?0:1;
b[i+2][1]=b[i+2][0]^b[i+3][0]?1:0;
b[i+3][1]=b[i+3][0]; 
}
inline void function_back(int i,int v)//反推函数
{
int vi,t,r,u=(int)pow(2.0,v),p=u;
for(vi=i+u-4;vi<i+u-2;vi++)
{b[vi][2]=b[vi][1]^b[vi+2][1]?1:0;
b[vi+2][2]=b[vi+2][1];
}
if(v>2)
{for(vi=i+u-8;vi<i+u-4;vi++)
{b[vi][3]=b[vi][2]^b[vi+4][2]?1:0;
b[vi+4][3]=b[vi+4][2];
}
}
if(v>3)
{for(vi=i+u-16;vi<i+u-8;vi++)
{b[vi][4]=b[vi][3]^b[vi+8][3]?1:0;
b[vi+8][4]=b[vi+8][3];
}
}
for(vi=i;vi<i+u;vi++)
{a[vi+u][v]=g(a[vi][v+1],a[vi+u][v+1],b[vi][v]);}
for(r=v;r>0;r--)
{u/=2;
for(t=i+p;t<i+u+p;t++)
{a[t][r-1]=f(a[t][r],a[t+u][r]);}
}
}
//************************************************

写完函数后别忘了在程序开头声明一下:

//译码函数声明************************************
float f(float Lin1,float Lin2);
float g(float Lin1,float Lin2,bool b);
inline void first_4(int i);
inline void function_back(int i,int v);
//************************************************

第三步,在main函数中开始第一次的f运算,得到第一位译码后,可以开始调用单元函数啦。先把switch架构搭好,我们可以将函数调用的顺序以数组的形式写出,switch只要循环读取数组,就能执行对应操作。这真的像是个密码,不如就把数组命名为password吧。

//译码部分**************************************** 
int j,i,u=N;
for(j=n-1;j>0;j--)
{u/=2;
for(i=0;i<u;i++)
{a[i][j-1]=f(a[i][j],a[i+u][j]);}
}
int password[7]={0,1,0,2,0,1,0};
int clue=0,clue0=0;
for(Vi=0;Vi<7;Vi++)
{
switch(password[Vi])
{
	case 0:first_4(clue);clue+=4;break;
	case 1:function_back(clue0,2);clue0+=8;break;
	case 2:function_back(0,3);break;
}
}
//************************************************

Part 4.检验程序

到了最精彩的地方——验货。

这看样子是要水到渠成了,可是,也有失误的时候:

其实,这并没有问题。任何纠错码都有一定的纠错限度,相对而言,信噪比高,说明信号能量比噪声干扰能量强;信噪比低,说明信号能量比噪声干扰能量弱。所以,在一定范围内,信噪比越低,误码率会越高。下图展示了几种通信译码的性能:

对比来看,SC使用的贪心算法机制虽然使程序相对简洁、高效,但性能仍然还不如极化码家族的其他成员以及部分其他种类的译码方法。

Polar SC的C语言实现在此已经讲完,使用的是二维数组的方法,在C语言实现极化码家族中的另一个成员——SCL时,我们需要将SC程序改写成一维数组形式,才能继续将其改进成SCL。方法如何,我们后续也会讲到,欢迎探讨。

感谢耐心观看,本人水平有限,欢迎批评指正。

 

发布了9 篇原创文章 · 获赞 14 · 访问量 4701
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章