CRC算法與實現

 

摘要: 本文首先討論了CRC的代數學算法,然後以常見的CRC-ITU爲例,通過硬件電路的實現,引出了比特型算法,最後重點介紹了字節型快速查表算法,給出了相應的C語言實現。

關鍵詞: CRC, FCS, 生成多項式, 檢錯重傳

引言

CRC的全稱爲Cyclic Redundancy Check,中文名稱爲循環冗餘校驗。它是一類重要的線性分組碼,編碼和解碼方法簡單,檢錯和糾錯能力強,在通信領域廣泛地用於實現差錯控制。實際上,除數據通信外,CRC在其它很多領域也是大有用武之地的。例如我們讀軟盤上的文件,以及解壓一個ZIP文件時,偶爾會碰到“Bad CRC”錯誤,由此它在數據存儲方面的應用可略見一斑。

差錯控制理論是在代數理論基礎上建立起來的。這裏我們着眼於介紹CRC的算法與實現,對原理只能捎帶說明一下。若需要進一步瞭解線性碼、分組碼、循環碼、糾錯編碼等方面的原理,可以閱讀有關資料。

利用CRC進行檢錯的過程可簡單描述爲:在發送端根據要傳送的k位二進制碼序列,以一定的規則產生一個校驗用的r位監督碼(CRC碼),附在原始信息後邊,構成一個新的二進制碼序列數共k+r位,然後發送出去。在接收端,根據信息碼和CRC碼之間所遵循的規則進行檢驗,以確定傳送中是否出錯。這個規則,在差錯控制理論中稱爲“生成多項式”。


 

1 代數學的一般性算法

在代數編碼理論中,將一個碼組表示爲一個多項式,碼組中各碼元當作多項式的係數。例如 1100101 表示爲
1·x6+1·x5+0·x4+0·x3+1·x2+0·x+1,即 x6+x5+x2+1。

設編碼前的原始信息多項式爲P(x),P(x)的最高冪次加1等於k;生成多項式爲G(x),G(x)的最高冪次等於r;CRC多項式爲R(x);編碼後的帶CRC的信息多項式爲T(x)。

發送方編碼方法:將P(x)乘以xr(即對應的二進制碼序列左移r位),再除以G(x),所得餘式即爲R(x)。用公式表示爲
T(x)=xrP(x)+R(x)

接收方解碼方法:將T(x)除以G(x),如果餘數爲0,則說明傳輸中無錯誤發生,否則說明傳輸有誤。

舉例來說,設信息碼爲1100,生成多項式爲1011,即P(x)=x3+x2,G(x)=x3+x+1,計算CRC的過程爲

      xrP(x)     x3(x3+x2)     x6+x5                    x
     -------- = ---------- = -------- = (x3+x2+x) + --------
       G(x)       x3+x+1      x3+x+1                 x3+x+1

即 R(x)=x。注意到G(x)最高冪次r=3,得出CRC爲010。

如果用豎式除法,計算過程爲

               1110
            -------   
      1011 /1100000     (1100左移3位)
            1011
            ----
             1110
             1011
             -----
              1010
              1011
              -----
               0010
               0000
               ----
                010

因此,T(x)=(x6+x5)+(x)=x6+x5+x, 即 1100000+010=1100010

如果傳輸無誤,

       T(x)     x6+x5+x
      ------ = --------- = x3+x2+x,
       G(x)     x3+x+1

無餘式。回頭看一下上面的豎式除法,如果被除數是1100010,顯然在商第三個1時,就能除盡。

上述推算過程,有助於我們理解CRC的概念。但直接編程來實現上面的算法,不僅繁瑣,效率也不高。實際上在工程中不會直接這樣去計算和驗證CRC。

下表中列出了一些見於標準的CRC資料:

 名稱 

 生成多項式 

 簡記式* 

 應用舉例 

 CRC-4 

 x4+x+1 

  

 ITU G.704 

 CRC-12 

 x12+x11+x3+x+1 

  

  

 CRC-16 

 x16+x12+x2+1 

 1005 

 IBM SDLC 

 CRC-ITU** 

 x16+x12+x5+1 

 1021 

 ISO HDLC, ITU X.25, V.34/V.41/V.42, PPP-FCS 

 CRC-32 

 x32+x26+x23+...+x2+x+1 

 04C11DB7 

 ZIP, RAR, IEEE 802 LAN/FDDI, IEEE 1394, PPP-FCS 

 CRC-32c 

 x32+x28+x27+...+x8+x6+1 

 1EDC6F41 

 SCTP 

    *  生成多項式的最高冪次項係數是固定的1,故在簡記式中,將最高的1統一去掉了,如04C11DB7實際上是104C11DB7。
    ** 前稱CRC-CCITT。ITU的前身是CCITT。


2 硬件電路的實現方法

多項式除法,可用除法電路來實現。除法電路的主體由一組移位寄存器和模2加法器(異或單元)組成。以CRC-ITU爲例,它由16級移位寄存器和3個加法器組成,見下圖(編碼/解碼共用)。編碼、解碼前將各寄存器初始化爲"1",信息位隨着時鐘移入。當信息位全部輸入後,從寄存器組輸出CRC結果。

CRC-ITU


3 比特型算法

上面的CRC-ITU除法電路,完全可以用軟件來模擬。定義一個寄存器組,初始化爲全"1"。依照電路圖,每輸入一個信息位,相當於一個時鐘脈衝到來,從高到低依次移位。移位前信息位與bit0相加產生臨時位,其中bit15移入臨時位,bit10、bit3還要加上臨時位。當全部信息位輸入完成後,從寄存器組取出它們的值,這就是CRC碼。

typedef unsigned char bit;
typedef unsigned char byte;
typedef unsigned short u16;
    
typedef union {
    u16 val;
    struct {
        u16 bit0 : 1;
        u16 bit1 : 1;
        u16 bit2 : 1;
        u16 bit3 : 1;
        u16 bit4 : 1;
        u16 bit5 : 1;
        u16 bit6 : 1;
        u16 bit7 : 1;
        u16 bit8 : 1;
        u16 bit9 : 1;
        u16 bit10 : 1;
        u16 bit11 : 1;
        u16 bit12 : 1;
        u16 bit13 : 1;
        u16 bit14 : 1;
        u16 bit15 : 1;
    } bits;
} CRCREGS;
    
// 寄存器組
CRCREGS regs;
    
// 初始化CRC寄存器組:移位寄存器置爲全"1"
void crcInitRegisters()
{
    regs.val = 0xffff;
}
    
// CRC輸入一個bit
void crcInputBit(bit in)
{
    bit a;
    
    a = regs.bits.bit0 ^ in;
    
    regs.bits.bit0 = regs.bits.bit1;
    regs.bits.bit1 = regs.bits.bit2;
    regs.bits.bit2 = regs.bits.bit3;
    regs.bits.bit3 = regs.bits.bit4 ^ a;
    regs.bits.bit4 = regs.bits.bit5;
    regs.bits.bit5 = regs.bits.bit6;
    regs.bits.bit6 = regs.bits.bit7;
    regs.bits.bit7 = regs.bits.bit8;
    regs.bits.bit8 = regs.bits.bit9;
    regs.bits.bit9 = regs.bits.bit10;
    regs.bits.bit10 = regs.bits.bit11 ^ a;
    regs.bits.bit11 = regs.bits.bit12;
    regs.bits.bit12 = regs.bits.bit13;
    regs.bits.bit13 = regs.bits.bit14;
    regs.bits.bit14 = regs.bits.bit15;
    regs.bits.bit15 = a;
}
    
// 輸出CRC碼(寄存器組的值)
u16 crcGetRegisters()
{
    return regs.val;
}

crcInputBit中一步一步的移位/異或操作,可以進行簡化:

void crcInputBit(bit in)
{
    bit a;
    a = regs.bits.bit0 ^ in;
    regs.val >>= 1;
    if(a) regs.val ^= 0x8408;
}

細心的話,可以發現0x8408和0x1021(CRC-ITU的簡記式)之間的關係。由於我們是從低到高輸出比特流的,將0x1021左右反轉就得到0x8408。將生成多項式寫成 G(x)=1+x5+x12+x16,是不是更好看一點?

下面是一個典型的PPP幀。最後兩個字節稱爲FCS(Frame Check Sequence),是前面11個字節的CRC。

FF 03 C0 21 04 03 00 07 0D 03 06 D0 3A

我們來計算這個PPP幀的CRC,並驗證它。

    byte ppp[13] = {0xFF, 0x03, 0xC0, 0x21, 0x04, 0x03, 0x00, 0x07, 0x0D, 0x03, 0x06, 0x00, 0x00};
    int i,j;
    u16 result;
    
    /////////// 以下計算FCS
    
    // 初始化
    crcInitRegisters();
    
    // 逐位輸入,每個字節低位在先,不包括兩個FCS字節
    for(i = 0; i < 11; i++)
    {
        for(j = 0; j < 8; j++)
        {
            crcInputBit((ppp[i] >> j) & 1);
        }
    }
    
    // 得到CRC:將寄存器組的值求反
    result = ~crcGetRegisters();
    
    // 填寫FCS,先低後高
    ppp[11] = result & 0xff;
    ppp[12] = (result >> 8) & 0xff;
    
    /////////// 以下驗證FCS
    
    // 初始化
    crcInitRegisters();
    
    // 逐位輸入,每個字節低位在先,包括兩個FCS字節
    for(i = 0; i < 13; i++)
    {
        for(j = 0; j < 8; j++)
        {
            crcInputBit((ppp[i] >> j) & 1);
        }
    }
    
    // 得到驗證結果
    result = crcGetRegisters();

可以看到,計算出的CRC等於0x3AD0,與原來的FCS相同。驗證結果等於0。初始化爲全"1",以及將寄存器組的值求反得到CRC,都是CRC-ITU的要求。事實上,不管初始化爲全"1"還是全"0",計算CRC取反還是不取反,得到的驗證結果都是0。


4 字節型算法

比特型算法逐位進行運算,效率比較低,不適用於高速通信的場合。數字通信系統(各種通信標準)一般是對一幀數據進行CRC校驗,而字節是幀的基本單位。最常用的是一種按字節查表的快速算法。該算法基於這樣一個事實:計算本字節後的CRC碼,等於上一字節餘式CRC碼的低8位左移8位,加上上一字節CRC右移8位和本字節之和後所求得的CRC碼。如果我們把8位二進制序列數的CRC(共256個)全部計算出來,放在一個表裏 ,編碼時只要從表中查找對應的值進行處理即可。

CRC-ITU的計算算法如下:
a.寄存器組初始化爲全"1"(0xFFFF)。
b.寄存器組向右移動一個字節。
c.剛移出的那個字節與數據字節進行異或運算,得出一個指向值表的索引。
d.索引所指的表值與寄存器組做異或運算。
f.數據指針加1,如果數據沒有全部處理完,則重複步驟b。
g.寄存器組取反,得到CRC,附加在數據之後。
         
CRC-ITU的驗證算法如下:
a.寄存器組初始化爲全"1"(0xFFFF)。
b.寄存器組向右移動一個字節。
c.剛移出的那個字節與數據字節進行異或運算,得出一個指向值表的索引。
d.索引所指的表值與寄存器組做異或運算。
e.數據指針加1,如果數據沒有全部處理完,則重複步驟b (數據包括CRC的兩個字節)。
f.寄存器組的值是否等於“Magic Value”(0xF0B8),若相等則通過,否則失敗。

下面是通用的CRC-ITU查找表以及計算和驗證CRC的C語言程序:

// CRC-ITU查找表
const u16 crctab16[] = 
{
    0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
    0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
    0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
    0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
    0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
    0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
    0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
    0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
    0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
    0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
    0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
    0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
    0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
    0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
    0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
    0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
    0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
    0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
    0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
    0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
    0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
    0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
    0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
    0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
    0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
    0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
    0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
    0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
    0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
    0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
    0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
    0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78,
};
    
// 計算給定長度數據的16位CRC。
u16 GetCrc16(const byte* pData, int nLength)
{
    u16 fcs = 0xffff;    // 初始化
    
    while(nLength>0)
    {
        fcs = (fcs >> 8) ^ crctab16[(fcs ^ *pData) & 0xff];
        nLength--;
        pData++;
    }
    
    return ~fcs;    // 取反
}
    
// 檢查給定長度數據的16位CRC是否正確。
bool IsCrc16Good(const byte* pData, int nLength)
{
    u16 fcs = 0xffff;    // 初始化
    
    while(nLength>0)
    {
        fcs = (fcs >> 8) ^ crctab16[(fcs ^ *pData) & 0xff];
        nLength--;
        pData++;
    }
    
    return (fcs == 0xf0b8);  // 0xf0b8是CRC-ITU的"Magic Value"
}

使用字節型算法,前面出現的PPP幀FCS計算和驗證過程,可用下面的程序片斷實現:

    byte ppp[13] = {0xFF, 0x03, 0xC0, 0x21, 0x04, 0x03, 0x00, 0x07, 0x0D, 0x03, 0x06, 0x00, 0x00};
    u16 result;
    
    // 計算CRC
    result = GetCrc16(ppp, 11);
    
    // 填寫FCS,先低後高
    ppp[11] = result & 0xff;
    ppp[12] = (result >> 8) & 0xff;
    
    // 驗證FCS
    if(IsCrc16Good(ppp, 13))
    {
        ... ...
    }

該例中數據長度爲11,說明CRC計算並不要求數據2字節或4字節對齊。

至於查找表的生成算法,以及CRC-32等其它CRC的算法,可參考RFC 1661, RFC 3309等文檔。需要注意的是,雖然CRC算法的本質是一樣的,但不同的協議、標準所規定的初始化、移位次序、驗證方法等可能有所差別。


結語

CRC是現代通信領域的重要技術之一。掌握CRC的算法與實現方法,在通信系統的設計、通信協議的分析以及軟件保護等諸多方面,能發揮很大的作用。如在作者曾經設計的一個多串口數據傳輸系統中,每串口速率爲460kbps,不加校驗時誤碼率大於10-6,加上簡單的奇偶校驗後性能改善不很明顯,利用CRC進行檢錯重傳,誤碼率降低至10-15以下,滿足了實際應用的要求。


參考文獻

1. Simpson, W., Editor, "The Point-to-Point Protocol (PPP)", STD 51, RFC 1661, 1994
2. J. Stone, "Stream Control Transmission Protocol (SCTP) Checksum Change", RFC 3309, 2002
3. J. Satran, "Internet Protocol Small Computer System Interface (iSCSI) Cyclic Redundancy Check (CRC)/Checksum Considerations", RFC 3385, 2002
4. International Standardization,"High-level data link control (HDLC) procedures", ISO/IEC 3309, 1992
5. ITU-T V.41, "Code-independent error-control system", 1989
6. 郭梯雲等,《數據傳輸(修訂本)》, 人民郵電出版社, 1998  

 

發佈了45 篇原創文章 · 獲贊 2 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章