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。
- 首先需要在被除數A後加4個比特位0(具體加幾個0是根據除數B的位數決定的,比如這裏B是5位,那麼A後面加4個0;如果B選6位,則A後面加5個0,總之加的0的個數比除數B的個數少1位。後面還會提到怎麼添加)。
- 進行模2除法運算。注意每次都是模2運算,即異或。
- 最後得到餘數C就是CRC校驗值。注意餘數位數必須比除數少1位,如果不夠前面加0補齊。運算如下圖所示
2.生成多項式
第1章講解了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個16位的變量爲CRC,此值存放CRC校驗值,賦初值爲0;
- 將需要校驗的字符串str後面添加16個0;
- 如果變量CRC最高位爲1,變量CRC與0x8005異或,然後將變量CRC左移1位,最低位補入1比特新的數據(來自需要校驗的字符串str);
- 如果變量CRC最高位爲0,直接將變量CRC左移1位,最低位補入1比特新的數據(來自需要校驗的字符串str);
- 重複2-3步,直到字符串str最後1位補入變量CRC中;
- 此時得到的餘數就是CRC校驗值。
這種直接的方法有一個弊端,那就是在字符串前面加0,並不影響校驗值,這就不符合我們的預期了。比如,我們想校驗的1字節1001 1100,現在在前面補1字節0,變成2字節0000 0000 1001 1100,結果兩個得到的校驗值是一樣的。所以在實際應用中,CRC校驗過程做了一些改變:增加了“餘數初始值”、“結果異或值”、“輸入數據反轉”和“輸出數據反轉”四個概念。
3.3.2 工程中常用CRC校驗過程
- 餘數初始值:即在計算開始前,先給變量CRC賦的初值。
- 結果異或值:即在計算結束後,得到的變量CRC與這個值進行異或操作,就得到了最終的校驗值。
- 輸入數據反轉:即在計算開始前,將需要校驗的數據反轉,如數據位1011,反轉後爲1101。
- 輸出數據反轉:即在計算結束後,與結果異或值異或之前,計算值反轉,如計算結果爲1011,反轉後爲1101。
實際應用中,生成多項式、餘數初始值、結果異或值、輸入數據反轉和輸出數據反轉是有對應關係的,這些對應關係是大家都遵守的標準,如下表所示:
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個16位的變量CRC,存放校驗值,首先根據表3-1賦初值0x0000;
- 將第1個字節按照表3-1看是否需要反轉,若需要,則按反轉,若不需要,直接進入第3步。這裏需要反轉;
- 把第1個字節按照步驟2處理後,與16位的變量CRC的低高8位相異或,把結果放於變量CRC,低8位數據不變;
- 把變量CRC的內容左移1位(朝高位)用0填補最低位,並檢查左移後的移出位;
- 如果移出位爲0:重複第3步(再次左移一位);如果移出位爲1,變量CRC與多項式8005(1000 0000 0000 0101)進行異或;
- 重複步驟4和5,直到左移8次,這樣整個8位數據全部進行了處理;
- 重複步驟2到步驟6,進行通訊信息幀下一個字節的處理;
- 將該通訊信息幀所有字節按上述步驟計算完成後,將得到的16位變量CRC按照表3-1看是否需要反轉,這裏需要反轉;
- 最後,與結果異或值異或,得到的變量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