概念
CRC(Cyclic redundancy check),循環冗余校驗
CRC校驗是用於檢測一幀數據發送是否正確,只有確認對錯的作用,並沒有糾錯的能力。
還有一點就是CRC校驗通過了,並不代表這個數據肯定就是正確的,只能說盡可能減少出錯的概率,當然
CRC錯了那么這個數據肯定是不正確的。
而這個概率是跟CRC的位數相關,也跟選擇的多項式有關,大致可以理解為CRC8,就是1/(28),CRC16則是1/(216)以此類推。
對於檢驗一幀數據是否正確有很多算法,CRC只是其中的一種,SUM的形式也可以的,只是算法不同對於校驗結果的效果也是不一樣的,最好的效果是,每一位的變化都可以引起最終checksum的值發生較大的改變。引入除法計算是一種很好的方法,每一位發生改變對於最后的余數都會引起較大的變化。
多項式(Polynomical)
多項式即CRC除法的除數,而且多項式是總於高於CRCN中N的一位,這樣可以保證余數的位數與N相同。同時多項式也有好壞之分,區別就是在於出錯的概率,至於哪種多項式好一些,這個一般來說是數學家的事情,我們工程上拿過來用就好,而且一般的協議中也已經規定了這個CRC的多項式。
其實多項式只是一種表現方式,當然也可以直接用16進制表示
以CRC-CCITT為例
也可以表示為0x1021
計算例子
引用別人文檔中的例子來說明CRC機制,如下是一個CRC4計算的例子
1100001010 = Quotient (nobody cares about the quotient)
_______________
10011 ) 11010110110000 = Augmented message (1101011011 + 0000)
=Poly 10011,,.,,....
-----,,.,,....
10011,.,,....
10011,.,,....
-----,.,,....
00001.,,....
00000.,,....
-----.,,....
00010,,....
00000,,....
-----,,....
00101,....
00000,....
-----,....
01011....
00000....
-----....
10110...
10011...
-----...
01010..
00000..
-----..
10100.
10011.
-----.
01110
00000
-----
1110 = Remainder = THE CHECKSUM!!!!
先將要計算的后方填充相應位數的0(CRC4,4位),再對POLY進行求余操作,這個余數就是我們要的checksum
這個操作就是一個除法操作,只是在減的時候用XOR來代替減法,這樣就不要考慮進位借位的問題,而且XOR來代替減法也不會使CRC的效果變差,因為每一位的改變還是會引起checksum較大的變化。
計算方法
了解了CRC的原理,接下來就對CRC校驗進行計算,接下來的討論都以CRC16為模板。
一、直接計算法
這個方法就是根據CRC的定義,進行按位操作,先將數據移位,若高位是1的話,就將當前的CRC與poly進行XOR操作來更新當的CRC值,直至所有的數據被更新后,再在數據的結尾添加相應的0位來得到最終的CRC結果,對於CRC16來說這個零位就是2個字節。總體來說,這種實現方式是根據定義來的,比較好理解,不過運行速度有待提高。
示例代碼如下:
unsigned int poly = 0x11021;
unsigned char testData[10] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa};
unsigned short crcUpdate(unsigned short crc, unsigned char data)
{
int i;
unsigned short result;
unsigned int tmp;
tmp = crc;
for(i = 0; i < 8; i++)
{
tmp <<= 1;
if(data & 0x80)
{
tmp += 1;
}
data <<= 1;
if(tmp & 0x10000)
{
tmp ^= poly;
}
}
result = tmp &0xffff;
return result;
}
unsigned short crcCheck(unsigned char *pData, unsigned char size)
{
unsigned short result = 0;
int i;
for(i = 0; i < size; i++)
{
result = crcUpdate(result, *(pData+i));
}
//補零
for(i = 0; i < 2; i++)
{
result = crcUpdate(result, 0);
}
return result;
}
void demo(void)
{
printf("Result %04x\n",crcCheck(testData,10)); //print 0xd877
}
二、表驅動法(Table-Driven Implementation)
在直接計算的情況下,為了提高運行的速度別人又提出了用表驅動的方法(根據當前的值來查找相應的CRC的結果,再代入公式進行計算最終的結果)。換句話,直接計算是通過一次移一位的操作來進行,而表驅動法剛是采用一次移多位的形式來進行CRC計算,來提高運行速度。
原理XOR也是滿足交換律如下
根據這個交換律,我們可以先將POLY進行移位XOR,再將結果同最初的值來進行XOR,來得來相應的移位。
下面以CRC8來說明
//以1位為單位為進行XOR
1 0 1 1 1 0 0 0
________________________________
1 0 0 0 1 1 1 0 0/ 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0
1 0 0 0 1 1 1 0 0
0 0 0 0 0 0 0 0 0
1 0 0 0 1 1 1 0 0
1 0 0 0 1 1 1 0 0
1 0 0 0 1 1 1 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
-------------------------------
1 0 1 0 0 0 0 0
//先將POLY的多次移位進行XOR
1 0 0 0 1 1 1 0 0
0 0 0 0 0 0 0 0 0
1 0 0 0 1 1 1 0 0
1 0 0 0 1 1 1 0 0
1 0 0 0 1 1 1 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0
-------------------------------
(1 0 1 1 0 1 0 0)1 0 1 0 0 0 0 0
相當於是這樣次一次性移位8位,8位的值為(10110100)其對於應是這樣的操作,后8位直接XOR(10100000)
這個其實跟直接除xor是一樣的,只是這樣一次移位得多,可以加快計算結果。
表的生成
表的長度
是由一次移動的位數決定,如一次性移4bit,那么表的長度就是24(16),如果一次性移8bit(1byte),那么表的長度就是28(256),一般都是256為長度。
表的一個單元
這個就是由CRCN,這個N來決定,CRC8為8bit,CRC16為16bit
表的生成代碼
unsigned short crc16Table[256] = {0};
unsigned short poly = 0x8005;
void crcTableCreate(void)
{
int i = 0;
for(i = 0; i < 256; i++)
{
unsigned short crc;
crc = i << 8;
for(int j = 0; j < 8; j++)
{
if(crc & 0x8000)
{
crc = (crc << 1) ^ poly;
}
else
{
crc <<= 1;
}
}
crc16Table[i] = crc;
}
}
//crc16Table
0000,8005,800f,000a,801b,001e,0014,8011,8033,0036,003c,8039,0028,802d,8027,0022,
8063,0066,006c,8069,0078,807d,8077,0072,0050,8055,805f,005a,804b,004e,0044,8041,
80c3,00c6,00cc,80c9,00d8,80dd,80d7,00d2,00f0,80f5,80ff,00fa,80eb,00ee,00e4,80e1,
00a0,80a5,80af,00aa,80bb,00be,00b4,80b1,8093,0096,009c,8099,0088,808d,8087,0082,
8183,0186,018c,8189,0198,819d,8197,0192,01b0,81b5,81bf,01ba,81ab,01ae,01a4,81a1,
01e0,81e5,81ef,01ea,81fb,01fe,01f4,81f1,81d3,01d6,01dc,81d9,01c8,81cd,81c7,01c2,
0140,8145,814f,014a,815b,015e,0154,8151,8173,0176,017c,8179,0168,816d,8167,0162,
8123,0126,012c,8129,0138,813d,8137,0132,0110,8115,811f,011a,810b,010e,0104,8101,
8303,0306,030c,8309,0318,831d,8317,0312,0330,8335,833f,033a,832b,032e,0324,8321,
0360,8365,836f,036a,837b,037e,0374,8371,8353,0356,035c,8359,0348,834d,8347,0342,
03c0,83c5,83cf,03ca,83db,03de,03d4,83d1,83f3,03f6,03fc,83f9,03e8,83ed,83e7,03e2,
83a3,03a6,03ac,83a9,03b8,83bd,83b7,03b2,0390,8395,839f,039a,838b,038e,0384,8381,
0280,8285,828f,028a,829b,029e,0294,8291,82b3,02b6,02bc,82b9,02a8,82ad,82a7,02a2,
82e3,02e6,02ec,82e9,02f8,82fd,82f7,02f2,02d0,82d5,82df,02da,82cb,02ce,02c4,82c1,
8243,0246,024c,8249,0258,825d,8257,0252,0270,8275,827f,027a,826b,026e,0264,8261,
0220,8225,822f,022a,823b,023e,0234,8231,8213,0216,021c,8219,0208,820d,8207,0202,
unsigned short crcUpdate2(unsigned short crcIn, unsigned char data)
{
unsigned short result = 0;
result = (crcIn << 8 | data) ^ crc16Table[(crcIn >> 8) & 0xff];
return result;
}
unsigned short crcCheck2(unsigned char *pData, unsigned char size)
{
unsigned short crcResult = 0;//Initial Value
for(int i = 0; i < size; i++)
{
crcResult = crcUpdate2(crcResult, *(pData+i));
}
// add zero to the tail
for(int i = 0; i < 2; i++)
{
crcResult = crcUpdate2(crcResult, 0);
}
return crcResult;
}
void demo(void)
{
crcTableCreate();
printf("Result %04x\n",crcCheck2(testData,10)); //print 0x2a62
}
三、直驅表法(Slightly Mangled Table-Driven Implementation)
直驅表法這種翻譯說法,我也不知道是否合理,照我個理解來說,這個應該叫做一種表驅動法的變種
這個變種作用是在於在進行CRC計算之后不需要進行填充相應位數的0。
驅動表還是一樣的,只是計算的公式不一樣
unsigned short crcUpdate3(unsigned short crcIn, unsigned char data)
{
unsigned short result = 0;
result = (crcIn << 8) ^ crc16Table[(crcIn >> 8) ^ data];
return result;
}
unsigned short crcCheck3(unsigned char *pData, unsigned char size)
{
unsigned short crcResult = 0;//Initial Value
for(int i = 0; i < size; i++)
{
crcResult = crcUpdate3(crcResult, *(pData+i));
}
return crcResult;
}
void demo(void)
{
crcTableCreate();
printf("Result %04x\n",crcCheck3(testData,10)); //print 0x2a62
}
這個做法是這樣的,至於這個公式是如何推導出來的,看的資料也沒有解釋清楚的,本人暫時也還不太理解,不過實際工程上基本都用的是這個方法。
而實際表驅動法與直驅表法,其實就是兩種算法,雖然兩者可以用的是相同的表,只有在驅動表法的初始值為0與填充值為0才與驅動表法初始值為0的情況下結果是一樣的,其他情況下值應該都是不一樣的。
對於CRC的真正用途來說,算法沒有具體的意義,只是有在數據發生改變的時候,CHECKSUM就可以發生較大的改變,且重復的概率比較小,那么這種算法就是一種比較好的算法。工程上用這種算法,就可以省掉補零的操作。
CRC的其他述語
上面說的是CRC的基本概念以及實現的方式,而在實際的用途中這個CRC還是會一些細微的參數。
如下是一些CRC的表述方式
Name : "CRC-16/CITT"
Width : 16
Poly : 1021
Init : FFFF
RefIn : False
RefOut : False
XorOut : 0000
Check : ?
Name : "XMODEM"
Width : 16
Poly : 8408
Init : 0000
RefIn : True
RefOut : True
XorOut : 0000
Check : ?
Name : "ARC"
Width : 16
Poly : 8005
Init : 0000
RefIn : True
RefOut : True
XorOut : 0000
Check : ?
標識 | 含義 |
---|---|
Name | 名字標識 |
Width | CRC長度 |
Poly | 多項式 |
Init | 寄存器初始值,針對直驅表法的寄存器初始值 |
RefIn | 輸入是否是低位在前 |
RefOut | 輸出是否低位在前 |
XorOut | 輸出前與這個值進行XOR再輸出 |
Check | 表示的一個參考輸出,以STRING(123456789)為例 |
這里的參數無論哪一個修改了,最終的值都會發生變化,相當於就生成一個新的CRC校驗方式
網上寫CRC的文章很多,記錄自己的理解前也參考了很多的文章