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;
}
講完f和g運算,開始學習譯碼原理吧。假設左端(輸出端)比特從上至下數,第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。方法如何,我們後續也會講到,歡迎探討。
感謝耐心觀看,本人水平有限,歡迎批評指正。