CRC校驗詳解(附代碼示例)

CRC校驗即循環冗餘校驗(Cyclic Redundancy Check),是基於數據計算一組效驗碼,用於覈對數據傳輸過程中是否被更改或傳輸錯誤。首先看兩個概念,後續會用到。

  • 模2除法:也叫模2運算,就是結果除以2後取餘數。模2除法每一位除的結果不影響其它位,即不向上一位借位,所以實際上就是異或。在CRC計算中有應用到模2除法。
  • 多項式與二進制:二進制可表示成多項式的形式,比如二進制1101表示爲: x3+x2+x0;1011表示爲:x3+x1+x0。

1.CRC校驗原理

CRC校驗本質上是選取一個合適的除數,要進行校驗的數據是被除數,然後做模2除法,得到的餘數就是CRC校驗值。

下面用具體的例子做講解:給定一組數據A:10110011(二進制),選取除數B:11001。

  1. 首先需要在被除數A後加4個比特位0(具體加幾個0是根據除數B的位數決定的,比如這裏B是5位,那麼A後面加4個0;如果B選6位,則A後面加5個0,總之加的0的個數比除數B的個數少1位。後面還會提到怎麼添加)。
  2. 進行模2除法運算。注意每次都是模2運算,即異或。
  3. 最後得到餘數C就是CRC校驗值。注意餘數位數必須比除數少1位,如果不夠前面加0補齊。運算如下圖所示

                   

2.生成多項式

第1章講解了CRC校驗的基本原理,通常我們把選取的除數稱之爲“生成多項式”。事實上,生成多項式的選取是由一定標準的,如果選的不好,那麼檢出錯誤的概率就會低很多。好在這個問題已經被專家們研究了很長一段時間了,對於我們這些使用者來說,只要把現成的成果拿來用就行了。下表是一些標準的CRC生成多項式,可以直接使用。

標準CRC生成多項式
名稱 生成多項式 簡記式
CRC-4 x4+x+1 0x03
CRC-8 x8+x5+x4+1 0x31
CRC-8 x8+x2+x1+1  0x07
CRC-8 x8+x6+x4+x3+x2+x1 0x5E
CRC-12 x12+x11+x3+x+1 0x080F
CRC-16 x16+x15+x2+1  0x8005
CRC16-CCITT x16+x12+x5+1 0x1021
CRC-32 x32+x26+x23+...+x2+x+1 0x04C11DB7

更多標準CRC生成式請參考https://en.wikipedia.org/wiki/Cyclic_redundancy_check

有一點要特別注意,文獻中提到的生成多項式經常會說到多項式的位寬(Width,簡記爲W),這個位寬不是多項式對應的二進制數的位數,而是位數減1。比如CRC8中用到的位寬爲8的生成多項式,其實對應得二進制數有九位:100110001。另外一點,多項式表示和二進制表示都很繁瑣,交流起來不方便,因此,文獻中多用16進制簡寫法來表示,因爲生成多項式的最高位肯定爲1,最高位的位置由位寬可知,故在簡記式中,將最高的1統一去掉了,如CRC32的生成多項式簡記爲04C11DB7實際上表示的是104C11DB7。當然,這樣簡記除了方便外,在編程計算時也有它的用處。所以在第一章中提到的在被除數後增加0的位數就是位寬,計算出的CRC校驗值長度也是位寬。

3.以CRC-16校驗爲例講解編程實現

3.3.1 完全按照CRC原理實現校驗

實際工程中多使用CRC-16校驗,即選取生成多項式爲0x8005。按照前面提到的CRC校驗原理,編程實現步驟如下:(注意實際編程時並不用這種直接的方法,如不想看可直接跳到3.3.2

  1. 預置1個16位的變量爲CRC,此值存放CRC校驗值,賦初值爲0;
  2. 將需要校驗的字符串str後面添加16個0;
  3. 如果變量CRC最高位爲1,變量CRC與0x8005異或,然後將變量CRC左移1位,最低位補入1比特新的數據(來自需要校驗的字符串str);
  4. 如果變量CRC最高位爲0,直接將變量CRC左移1位,最低位補入1比特新的數據(來自需要校驗的字符串str);
  5. 重複2-3步,直到字符串str最後1位補入變量CRC中;
  6. 此時得到的餘數就是CRC校驗值。

這種直接的方法有一個弊端,那就是在字符串前面加0,並不影響校驗值,這就不符合我們的預期了。比如,我們想校驗的1字節1001 1100,現在在前面補1字節0,變成2字節0000 0000 1001 1100,結果兩個得到的校驗值是一樣的。所以在實際應用中,CRC校驗過程做了一些改變:增加了“餘數初始值”、“結果異或值”、“輸入數據反轉”和“輸出數據反轉”四個概念。

3.3.2 工程中常用CRC校驗過程

  • 餘數初始值:即在計算開始前,先給變量CRC賦的初值。
  • 結果異或值:即在計算結束後,得到的變量CRC與這個值進行異或操作,就得到了最終的校驗值。
  • 輸入數據反轉:即在計算開始前,將需要校驗的數據反轉,如數據位1011,反轉後爲1101。
  • 輸出數據反轉:即在計算結束後,與結果異或值異或之前,計算值反轉,如計算結果爲1011,反轉後爲1101。

實際應用中,生成多項式、餘數初始值、結果異或值、輸入數據反轉和輸出數據反轉是有對應關係的,這些對應關係是大家都遵守的標準,如下表所示:

表3-1 常見CRC參數模型
CRC算法名稱 多項式公式 寬度 多項式(16進制) 初始值(16進制) 結果異或值(16進制) 輸入值反轉 輸出值反轉
CRC-4/ITU x4 + x + 1 4 03 00 00 true true
CRC-5/EPC x4 + x3 + 1 5 09 09 00 false false
CRC-5/ITU x5 + x4 + x2 + 1 5 15 00 00 true true
CRC-5/USB x5 + x2 + 1 5 05 1F 1F true true
CRC-6/ITU x6 + x + 1 6 03 00 00 true true
CRC-7/MMC x7 + x3 + 1 7 09 00 00 false false
CRC-8 x8 + x2 + x + 1 8 07 00 00 false false
CRC-8/ITU x8 + x2 + x + 1 8 07 00 55 false false
CRC-8/ROHC x8 + x2 + x + 1 8 07 FF 00 true true
CRC-8/MAXIM x8 + x5 + x4 + 1 8 31 00 00 true true
CRC-16/IBM x16 + x15 + x2 + 1 16 8005 0000 0000 true true
CRC-16/MAXIM x16 + x15 + x2 + 1 16 8005 0000 FFFF true true
CRC-16/USB x16 + x15 + x2 + 1 16 8005 FFFF FFFF true true
CRC-16/MODBUS x16 + x15 + x2 + 1 16 8005 FFFF 0000 true true
CRC-16/CCITT x16 + x12 + x5 + 1 16 1021 0000 0000 true true
CRC-16/CCITT-FALSE x16 + x12 + x5 + 1 16 1021 FFFF 0000 false false
CRC-16/x5 x16 + x12 + x5 + 1 16 1021 FFFF FFFF true true
CRC-16/XMODEM x16 + x12 + x5 + 1 16 1021 0000 0000 false false
CRC-16/DNP x16 + x13 + x12 + x11 + x10 + x8 + x6 + x5 + x2 + 1 16 3D65 0000 FFFF true true
CRC-32 x32 + x26 + x23 + x22 + x16 + x12 + x11 + x10 + x8 + x7 + x5 + x4 + x2 + x + 1 32 04C11DB7 FFFFFFFF FFFFFFFF true true
CRC-32/MPEG-2 x32 + x26 + x23 + x22 + x16 + x12 + x11 + x10 + x8 + x7 + x5 + x4 + x2 + x + 1 32 04C11DB7 FFFFFFFF 00000000 false false

接下來以CRC-16/IBM校驗爲例,講解工程中使用的CRC校驗編程實現。具體實現時,以字節爲單位進行計算。

  1. 預置1個16位的變量CRC,存放校驗值,首先根據表3-1賦初值0x0000;
  2. 將第1個字節按照表3-1看是否需要反轉,若需要,則按反轉,若不需要,直接進入第3步。這裏需要反轉;
  3. 把第1個字節按照步驟2處理後,與16位的變量CRC的低高8位相異或,把結果放於變量CRC,低8位數據不變;
  4. 把變量CRC的內容左移1位(朝高位)用0填補最低位,並檢查左移後的移出位;
  5. 如果移出位爲0:重複第3步(再次左移一位);如果移出位爲1,變量CRC與多項式8005(1000 0000 0000 0101)進行異或;
  6. 重複步驟4和5,直到左移8次,這樣整個8位數據全部進行了處理;
  7. 重複步驟2到步驟6,進行通訊信息幀下一個字節的處理;
  8. 將該通訊信息幀所有字節按上述步驟計算完成後,將得到的16位變量CRC按照表3-1看是否需要反轉,這裏需要反轉;
  9. 最後,與結果異或值異或,得到的變量CRC即爲CRC校驗值;

我在這裏按照如上方法整理了一個通用代碼,包含CRC-8,CRC-16和CRC-32。代碼如下:

type.h

#ifndef __TYPE_H__
#define __TYPE_H__
#include <stdio.h>

typedef unsigned char        u8;
typedef unsigned short       u16;
typedef unsigned int         u32;
typedef unsigned long long   u64;

typedef unsigned char        BOOL;
#define FALSE                0
#define TRUE                 1

#endif

crc.h文件

#ifndef __CRC_H__
#define __CRC_H__
#include "type.h"

typedef struct
{
	u8 poly;//多項式
	u8 InitValue;//初始值
	u8 xor;//結果異或值
	BOOL InputReverse;
	BOOL OutputReverse;
}CRC_8;

typedef struct
{
	u16 poly;//多項式
	u16 InitValue;//初始值
	u16 xor;//結果異或值
	BOOL InputReverse;
	BOOL OutputReverse;
}CRC_16;

typedef struct
{
	u32 poly;//多項式
	u32 InitValue;//初始值
	u32 xor;//結果異或值
	BOOL InputReverse;
	BOOL OutputReverse;
}CRC_32;

const CRC_8 crc_8;
const CRC_8 crc_8_ITU;
const CRC_8 crc_8_ROHC;
const CRC_8 crc_8_MAXIM;

const CRC_16 crc_16_IBM;
const CRC_16 crc_16_MAXIM;
const CRC_16 crc_16_USB;
const CRC_16 crc_16_MODBUS;
const CRC_16 crc_16_CCITT;
const CRC_16 crc_16_CCITT_FALSE;
const CRC_16 crc_16_X5;
const CRC_16 crc_16_XMODEM;
const CRC_16 crc_16_DNP;

const CRC_32 crc_32;
const CRC_32 crc_32_MPEG2;

u8 crc8(u8 *addr, int num,CRC_8 type) ;
u16 crc16(u8 *addr, int num,CRC_16 type) ;
u32 crc32(u8 *addr, int num,CRC_32 type) ;

#endif

crc.c文件

#include <stdio.h>
#include "type.h"
#include "CRC.h"

const CRC_8 crc_8 = {0x07,0x00,0x00,FALSE,FALSE};
const CRC_8 crc_8_ITU = {0x07,0x00,0x55,FALSE,FALSE};
const CRC_8 crc_8_ROHC = {0x07,0xff,0x00,TRUE,TRUE};
const CRC_8 crc_8_MAXIM = {0x31,0x00,0x00,TRUE,TRUE};

const CRC_16 crc_16_IBM = {0x8005,0x0000,0x0000,TRUE,TRUE};
const CRC_16 crc_16_MAXIM = {0x8005,0x0000,0xffff,TRUE,TRUE};
const CRC_16 crc_16_USB = {0x8005,0xffff,0xffff,TRUE,TRUE};
const CRC_16 crc_16_MODBUS = {0x8005,0xffff,0x0000,TRUE,TRUE};
const CRC_16 crc_16_CCITT = {0x1021,0x0000,0x0000,TRUE,TRUE};
const CRC_16 crc_16_CCITT_FALSE = {0x1021,0xffff,0x0000,FALSE,FALSE};
const CRC_16 crc_16_X5 = {0x1021,0xffff,0xffff,TRUE,TRUE};
const CRC_16 crc_16_XMODEM = {0x1021,0x0000,0x0000,FALSE,FALSE};
const CRC_16 crc_16_DNP = {0x3d65,0x0000,0xffff,TRUE,TRUE};

const CRC_32 crc_32 = {0x04c11db7,0xffffffff,0xffffffff,TRUE,TRUE};
const CRC_32 crc_32_MPEG2 = {0x04c11db7,0xffffffff,0x00000000,FALSE,FALSE};

/*****************************************************************************
*function name:reverse8
*function: 字節反轉,如1100 0101 反轉後爲1010 0011
*input:1字節
*output:反轉後字節
******************************************************************************/
u8 reverse8(u8 data)
{
    u8 i;
    u8 temp=0;
    for(i=0;i<8;i++)	//字節反轉
        temp |= ((data>>i) & 0x01)<<(7-i);
    return temp;
}
/*****************************************************************************
*function name:reverse16
*function: 雙字節反轉,如1100 0101 1110 0101反轉後爲1010 0111 1010 0011
*input:雙字節
*output:反轉後雙字節
******************************************************************************/
u16 reverse16(u16 data)
{
    u8 i;
    u16 temp=0;
    for(i=0;i<16;i++)		//反轉
        temp |= ((data>>i) & 0x0001)<<(15-i);
    return temp;
}
/*****************************************************************************
*function name:reverse32
*function: 32bit字反轉
*input:32bit字
*output:反轉後32bit字
******************************************************************************/
u32 reverse32(u32 data)
{
    u8 i;
    u32 temp=0;
    for(i=0;i<32;i++)		//反轉
        temp |= ((data>>i) & 0x01)<<(31-i);
    return temp;
}

/*****************************************************************************
*function name:crc8
*function: CRC校驗,校驗值爲8位
*input:addr-數據首地址;num-數據長度(字節);type-CRC8的算法類型
*output:8位校驗值
******************************************************************************/
u8 crc8(u8 *addr, int num,CRC_8 type)  
{  
    u8 data;
    u8 crc = type.InitValue;                   //初始值
    int i;  
    for (; num > 0; num--)               
    {  
        data = *addr++;
        if(type.InputReverse == TRUE)
        data = reverse8(data);                 //字節反轉
        crc = crc ^ data ;                     //與crc初始值異或 
        for (i = 0; i < 8; i++)                //循環8位 
        {  
            if (crc & 0x80)                    //左移移出的位爲1,左移後與多項式異或
                crc = (crc << 1) ^ type.poly;    
            else                               //否則直接左移
                crc <<= 1;                  
        }
    }
    if(type.OutputReverse == TRUE)             //滿足條件,反轉
        crc = reverse8(crc);
    crc = crc^type.xor;                        //最後返與結果異或值異或
    return(crc);                               //返回最終校驗值
}

/*****************************************************************************
*function name:crc16
*function: CRC校驗,校驗值爲16位
*input:addr-數據首地址;num-數據長度(字節);type-CRC16的算法類型
*output:16位校驗值
******************************************************************************/
u16 crc16(u8 *addr, int num,CRC_16 type)  
{  
    u8 data;
    u16 crc = type.InitValue;					//初始值
    int i;  
    for (; num > 0; num--)               
    {  
        data = *addr++;
        if(type.InputReverse == TRUE)
            data = reverse8(data);				//字節反轉
        crc = crc ^ (data<<8) ;					//與crc初始值高8位異或 
        for (i = 0; i < 8; i++)					//循環8位 
        {  
            if (crc & 0x8000)					//左移移出的位爲1,左移後與多項式異或
                crc = (crc << 1) ^ type.poly;    
            else		                        //否則直接左移
                crc <<= 1;                  
        }
    }
    if(type.OutputReverse == TRUE)              //滿足條件,反轉
        crc = reverse16(crc);
    crc = crc^type.xor;	                        //最後返與結果異或值異或
    return(crc);                                //返回最終校驗值
}
/*****************************************************************************
*function name:crc32
*function: CRC校驗,校驗值爲32位
*input:addr-數據首地址;num-數據長度(字節);type-CRC32的算法類型
*output:32位校驗值
******************************************************************************/
u32 crc32(u8 *addr, int num,CRC_32 type)  
{  
    u8 data;
    u32 crc = type.InitValue;					//初始值
    int i;  
    for (; num > 0; num--)               
    {  
        data = *addr++;
        if(type.InputReverse == TRUE)
            data = reverse8(data);				//字節反轉
        crc = crc ^ (data<<24) ;				//與crc初始值高8位異或 
        for (i = 0; i < 8; i++)					//循環8位 
        {  
            if (crc & 0x80000000)				//左移移出的位爲1,左移後與多項式異或
                crc = (crc << 1) ^ type.poly;    
            else                                //否則直接左移
                crc <<= 1;                  
        }
    }
    if(type.OutputReverse == TRUE)              //滿足條件,反轉
        crc = reverse32(crc);
    crc = crc^type.xor;	                        //最後返與結果異或值異或
    return(crc);                                //返回最終校驗值
}

調用時,只需傳入相應的參數即可。經驗證全部正確。如有疑問請評論留言。

3.3.3 改進的CRC校驗過程

3.3.2中的代碼具有通用性,但是可以看到效率不高。以crc16函數爲例,需要判斷字節是否需要反轉,結束時,也需要判斷是否需要反轉,這都會耗費時間,如果需要反轉,那麼反轉函數要花費更多時間。如何能提高效率呢?實際中我們常用某一種具體的校驗方法,所以可以寫單獨的代碼而非通用的,這樣就可以省去兩次判斷反轉的時間。以crc16/MAXIM爲例,開始和結束都需要反轉,改進後可以省略,具體操作如下:

/*****************************************************************************
*function name:crc16_MAXIM
*function: CRC校驗,校驗值爲16位
*input:addr-數據首地址;num-數據長度(字節)
*output:16位校驗值
******************************************************************************/
u16 crc16_MAXIM(u8 *addr, int num)  
{  
    u8 data;
    u16 crc = 0x0000;//初始值
    int i;  
    for (; num > 0; num--)             
    {  
        crc = crc ^ (*addr++) ;     //低8位異或
        for (i = 0; i < 8; i++)             
        {  
            if (crc & 0x0001)       //由於前面和後面省去了反轉,所以這裏是左移,且異或的值爲多項式的反轉值
                crc = (crc >> 1) ^ 0xA001;//右移後與多項式反轉後異或
            else                   //否則直接右移
                crc >>= 1;                    
        }                               
    }
    return(crc^0xffff);            //返回校驗值 
}  

讀者可對比通用代碼中crc16函數和crc16_MAXIM函數的區別。

 

以上是計算法計算校驗值,最後還有一小節,講解查表法計算校驗值,查表法更快,但是需要佔用一定內存空間。計算法和查表法各有利弊,使用時根據實際情況選擇。最後一節後續在更。

如有疑問,歡迎大家在評論區留言討論。

參考文獻:

[1]https://www.cnblogs.com/sinferwu/p/7904279.html

[2]https://en.wikipedia.org/wiki/Cyclic_redundancy_check

[3]https://www.cnblogs.com/94cool/p/3559585.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章