CRC, Cyclic Redundancy Check, 迴圈冗餘校驗
1. 基本原理
CRC的本質是除法,把待檢驗的資料當作一個很大(很長)的被除數,兩邊選定一個除數(有的文獻叫poly),最後得到的餘數就是CRC的校驗值。
判定方法:
-
將訊息和校驗和分開。計算訊息的校驗和(在附加W個零後),並比較兩個校驗和。
-
把校驗值放到資料的結尾,對整批進行校驗和(不附加零),看看結果是否為零!
1.1. 為什麼用CRC
比較常見的是累加和校驗,但是有以下缺點:
-
80
與80 00 .. 00
的計算結果一致,即如果資料裡參雜了00
是檢測不出來的,對於不定長的檢測不友好 -
因為是累加和,所以
80 00
有非常多的組合是校驗值相等的,比如70 10
,79 06
等等
那麼什麼情況下會導致CRC失敗呢?
2. 推導前準備
2.1. 無進位加法及減法
CRC算術中的兩個數字相加與普通二進位制算術中的數字相加相同,除了沒有進位。
無進位的加法及減法其實是異或運算(異或,不一樣就或在一起:不一樣為1,相同為0)
這意味著每對對應的位元確定對應的輸出位元,而不參考任何其他位元位置。例如
10011011
+11001010
--------
01010001
--------
加法的4中情況:
0+0=0
0+1=1
1+0=1
1+1=0 (no carry)
減法也是類似的:
10011011
-11001010
--------
01010001
--------
with
0-0=0
0-1=1 (wraparound)
1-0=1
1-1=0
這麼一來,我們相當於把加法和減法合併成為了一種演算法,或者可以理解為加法和減法這裡稱為了一種互逆運算,比如我們可以透過加減相同的數量,可以從1010到1001:
1001 = 1010 + 0011
1001 = 1010 - 0011
所以在無進位的加減法裡,1010不再可以被視為大於1001;
這麼做有什麼好處?
你會發現無論多長的資料bit在運算時都不再依賴於前一位或者後一位的狀態,這和帶進位的加法及帶借位的減法不同,你可以理解為執行平行計算:
-
帶進位的加法,高位的計算結果需要累加低位結果產生的進位,這就導致了必須要先計算低位,之後才能計算高位;比如下面的例子,如果帶進位的話就必須從最右邊開始計算,依次算到最左邊得到結果。但是如果我們把進位取消,就會發現我從那邊開始算都可以,當然也可以多位同時一起算(平行計算)
1011 1011 + 1101 + 1101 ---- ---- 1 1000 (with carry) 0110 (no carry)
-
減法也是如此,不再贅述。
2.2. 無進位乘法
定義了加法後,我們可以進行乘法和除法。乘法是簡單的,只不過在加法運算的時候使用XOR就行了
1101
x 1011
----
1101
1101.
0000..
1101...
-------
1111111 Note: The sum uses CRC addition
-------
2.3. 無進位除法
除法也是類似的,只不過有兩點需要注意:
-
當除數和被除數的最高位都是1的時候,就當作是
對齊
了,就可以開始XOR運算,不要比較資料大小,比如1001
可以被1011
除,至於商的結果是1或者0,沒有人去關注,自己開心就好,因為這個演算法壓根就不用; -
被除數和除數做減法時,需要使用無進位的減法,即XOR運算;
1 = 商 (nobody cares about the quotient) ______ 1011 ) 1001 除數 =Poly 1011 ---- 0010
3. 演算法推導
即使我們知道CRC的演算法是基於除法,我們也不能直接使用除法運算,一個是待校驗的資料很長,我們沒有這麼大的暫存器;再則,你知道除法在MCU中是怎麼實現的嗎?
3.1. 仿人算方法
現在我們假設一個訊息資料為1101011011
,選取除數為10011
,使用CRC演算法將訊息除以poly:
1100001010 = Quotient (nobody cares about the quotient)
_______________
10011 ) 11010110110000 = Augmented message (1101011011 + 0000)
=Poly 10011,,.,,..|.
-----,,.,,|...
10011,.,,|.|.
10011,.,,|...
-----,.,,|.|.
10110...
10011.|.
-----...
010100.
10011.
-----.
01110
00000
-----
1110 = Remainder = THE CHECKSUM!!!!
除數poly的左邊的高位的作用其實是給人看的(實際上參與運算的是0011),目的是幹掉當前最高位的被除數,本質上是讓poly和被除數對齊
,然後開始XOR運算。
那麼什麼情況算是對齊
呢? 從例子上看,當被除數和除數的最高位都是1時,就算是對齊了。
轉換成演算法的思路就是,你也可以理解成一長串的資料不斷的從右邊移位到暫存器中,當暫存器最左邊溢位的數值是1的時候,那麼當前暫存器的資料就可以和poly異或運算了,用演算法表示,大概是這樣:
3 2 1 0 Bits
+---+---+---+---+
Pop! <-- | | | | | <----- Augmented message
+---+---+---+---+
1 0 0 1 1 = The Poly
用演算法語言描述就是:
暫存器清零
資料最右邊補齊W位0 // W是CRC校驗值的位數
when(還有資料){
左移暫存器1位,讀取資料的下一位到暫存器的bit 0
if (左移暫存器時出現溢位){
暫存器 ^= poly; // 這裡的poly=0011,按照上面的例子
}
}
暫存器的值就是校驗值了
用C語言:
// CRC8生成多項式
#define POLYNOMIAL 0x07
// 計算CRC8校驗值
uint8_t crc8_data(const uint8_t dat8) {
uint8_t crc = dat8;
for (j = 8; j; j--) {
if (crc & 0x80)
crc = (crc << 1) ^ POLYNOMIAL;
else
crc <<= 1;
}
return crc;
}
但是這個方法太笨了,按位進行計算,效率有待提升。
3.2. 使用Table驅動計算CRC4
3.2.1. 4-bit 資料計算
為了方便描述,我們舉例W=4
且poly=3
的情況,比如我們計算一個3
的CRC值為5
,我們寫成XOR的計算過程:
0011 0000 // 補W=4個零 (值1)
,,10 011 // poly對齊 (值2)
---------
0001 0110
,,,1 0011 // poly對齊 (值3)
---------
0000 0101 // CRC值 (值4)
上面的計算經過了N次迭代運算(其實多少次迭代我們並不關心),等價於
CRC值 = 值1 ^ 值2 ^ 值3
= 值1 ^ (值2 ^ 值3)
= 值1 ^ 查表值 // 令 `查表值` = 值2 ^ 值3
需要注意的是,在CRC計算時,末尾補了4個0,但是我們是清楚的,任何數和0的XOR運算都是其本身,所以補0不會影響最後CRC的值,只不過相當於把CRC的值提取出來。 CRC計算等價於一系列的移位和XOR運算,所以上面的表示式實際上為:
CRC值 = (值1 ^ 0) ^ 值2 ^ 值3
= (值1 ^ 0) ^ (值2 ^ 值3)
= (值1 ^ 0) ^ 查表值
= 值1 ^ 查表值 // 令 `查表值` = 值2 ^ 值3
也就是說,我們可以實現把0~15
的CRC的值先預先算一遍,然後存起來,這樣下次再計算就可以直接查表計算,這很好理解。
3.2.2. 8-bit 資料計算
想象一下,一個8-bit的位元組是可以拆分成兩個4-bit資料的,如果我們可以利用查表的方法,是不是透過兩次計算就可以得到一個8-bit的CRC值了?具體要怎麼做呢,我們舉例W=4
且poly=3
的情況,比如我們計算一個33h
的CRC值,我們寫成XOR的計算過程:
0011 0011 0000 // 補W=4個零 (值1)
,,10 011 // poly對齊 (值2)
--------------
0001 0101 0000
,,,1 0011 // poly對齊 (值3)
--------------
0000 0110 0000 // 變回4-bit CRC計算 (值4)
下面的計算我們就熟悉了,回到 計算4-bit資料為 6
的CRC值:
0110 0000 // 補W=4個零
,100 11 // poly對齊
---------
0010 1100
,,10 011 // poly對齊
---------
0000 1010 // CRC值
我們發現一個有意思的事情,原來4-bit資料3
的CRC值是5
,但是當33h
先進行計算高4-bit的CRC值卻是6
,和之前的不一樣(也幸虧不一樣,如果後面無論跟什麼資料都一樣還有校驗幹嘛用),這是什麼原因?
首先,我們看一下8-bit計算和原來4-bit計算的區別在於末尾補數:
-
4-bit CRC計算時,末尾補的是0,是不影響計算結果的;
-
8-bit CRC計算時,末尾補的是後面跟的低4-bit資料,是會影響原來計算結果的:
為了方便描述,我們把8-bit的
值1
的高4-bit資料記為H4,低4-bit資料記為L4`值4` = `值1` ^ 值2 ^ 值3 = `值1` ^ `查表值` // 令 `查表值` = 值2 ^ 值3 = `(H4<<4 ^ L4)` ^ `查表值` = `(H4<<4)` ^ `查表值` ^ L4 // (H4<<4)其實就是計算H4的CRC值且末尾補0的情況
所以,我們可以得2段4-bit的計算流程:
- 去掉位元組的高4-bit值為H4
- 將H4值進行查表計算,得到值TMP1
- 把TMP1的值
異或上
低4位的值L4,得到值TMP2 - 然後用TMP2的值進行查表計算,得到值CRC
4. 演算法改進
4.1. CRC8計算
現在我們可以根據CRC4的計算過程類比到CRC8計算,其實主要的區別就是暫存器的位數從4位提升到了8位,一個典型的CRC8計算模型如下,現在你應該可以讀懂了。
#include <stdio.h>
#include <stdint.h>
// CRC8生成多項式
#define POLYNOMIAL 0x07
// 初始化CRC8查詢表
void init_crc8_table(void) {
uint8_t i, j;
for (i = 0; i < 256; i++) {
uint8_t crc = i;
for (j = 8; j; j--) {
if (crc & 0x80)
crc = (crc << 1) ^ POLYNOMIAL;
else
crc <<= 1;
}
crc8_table[i] = crc;
}
}
// 計算CRC8校驗值
uint8_t crc8(const void *data, size_t len) {
const uint8_t *byte = data;
uint8_t crc = 0x00;
for (; len > 0; len--) {
crc = crc8_table[(crc ^ *byte++) & 0xFF];
}
return crc;
}
int main(int argc, char *argv[]) {
int fd;
uint8_t buffer;
size_t bytes_read;
uint8_t crc;
if (argc != 2) {
fprintf(stderr, "Usage: %s filename\n", argv);
exit(1);
}
fd = open(argv, O_RDONLY);
bytes_read = read(fd, buffer, sizeof(buffer));
crc = crc8(buffer, bytes_read);
printf("CRC: 0x%02X\n", crc);<q refer="1"></q><span class="_q_s_"></span>
close(fd);
return 0;
}
4.2. CRC8計算-改進型
上面的演算法還是不夠好,因為Table的表太大了佔用256位元組,對於FLASH空間緊張的MCU來說不怎麼友好,能不能把一個8-bit資料拆分成兩次4-bit資料的計算,這樣是不是就可以搞成16
位元組的表了?我們來試一下!
實際上使用了32位元組
idx | L4 | H4 | H4說明 |
---|---|---|---|
0 | 0 | 0 | (L4<<4) ^ 07*0 |
1 | 07 | 70 | (L4<<4) ^ 07*0 |
2 | 0E | E0 | (L4<<4) ^ 07*0 |
3 | 09 | 90 | (L4<<4) ^ 07*0 |
4 | 1C | C7 | (L4<<4) ^ 07 |
5 | 1B | B7 | (L4<<4) ^ 07 |
6 | 12 | 27 | (L4<<4) ^ 07 |
7 | 15 | 57 | (L4<<4) ^ 07 |
8 | 38 | 89 | (L4<<4) ^ ((07<<1) ^ 07) |
9 | 3F | F9 | (L4<<4) ^ ((07<<1) ^ 07) |
A | 36 | 69 | (L4<<4) ^ ((07<<1) ^ 07) |
B | 31 | 19 | (L4<<4) ^ ((07<<1) ^ 07) |
C | 24 | 4E | (L4<<4) ^ (07<<1) |
D | 23 | 3E | (L4<<4) ^ (07<<1) |
E | 2A | AE | (L4<<4) ^ (07<<1) |
F | 2D | DE | (L4<<4) ^ (07<<1) |
// 計算CRC8校驗值
uint8_t crc8(const void *data, size_t len) {
const uint8_t *byte = data;
uint8_t crc = 0x00;
for (; len > 0; len--) {
crc = crc8_table_h4[(crc ^ *byte)>>4] ^
crc8_table_l4[(crc ^ *byte) & 0xF] ;
byte++;
}
return crc;
}
驗證:
-
CRC8計算單位元組
crc8(88) = 38 ^ 89 = B1
-
CRC8計算多位元組
crc8(8888) = crc8(B1 ^ 88) = crc8(39) = 90^3F=AF crc8(1234) = crc8(crc8(12) ^ 34) = crc8(7E ^ 34 = 4A) = C7^36=F1
4.3. CRC16計算-改進型
進一步地,我們可不可以使用相同的原理實現CRC16演算法?W=16, poly=8005
idx | X[3:0] | X[7:4] | X[7:4]說明 | X[11:8] | X[11:8]說明 | X[15:12] | X[15:12]說明 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | |||||
1 | 8005 | 8063 | X[3:0]<<4 ^ 8033 | 8603 | X[3:0]<<8 ^ 8303 | E003 | X[3:0]<<12 ^ B003 |
2 | 800F | 80C3 | X[3:0]<<4 ^ 8033 | 8C03 | X[3:0]<<8 ^ 8303 | 4003 | X[3:0]<<12 ^ B003 |
3 | 0A | 00A0 | X[3:0]<<4 | A00 | X[3:0]<<8 | A000 | X[3:0]<<12 |
4 | 801B | 8183 | X[3:0]<<4 ^ 8033 | 9803 | X[3:0]<<8 ^ 8303 | 8006 | X[3:0]<<12 ^ B003 ^ 8005 |
5 | 1E | 1E0 | X[3:0]<<4 | 1E00 | X[3:0]<<8 | 6005 | X[3:0]<<12 ^ 8005 |
6 | 14 | 140 | X[3:0]<<4 | 1400 | X[3:0]<<8 | C005 | X[3:0]<<12 ^ 8005 |
7 | 8011 | 8123 | X[3:0]<<4 ^ 8033 | 9203 | X[3:0]<<8 ^ 8303 | 2006 | X[3:0]<<12 ^ B003 ^ 8005 |
8 | 8033 | 8303 | X[3:0]<<4 ^ 8033 | B003 | X[3:0]<<8 ^ 8303 | 8009 | X[3:0]<<12 ^ B003 ^ A |
9 | 36 | 360 | X[3:0]<<4 | 3600 | X[3:0]<<8 | 600A | X[3:0]<<12 ^ A |
A | 3C | 3C0 | X[3:0]<<4 | 3C00 | X[3:0]<<8 | C00A | X[3:0]<<12 ^ A |
B | 8039 | 83A3 | X[3:0]<<4 ^ 8033 | BA03 | X[3:0]<<8 ^ 8303 | 2009 | X[3:0]<<12 ^ B003 ^ A |
C | 28 | 280 | X[3:0]<<4 | 2800 | X[3:0]<<8 | 000F | X[3:0]<<12 ^ 800F |
D | 802D | 82E3 | X[3:0]<<4 ^ 8033 | AE03 | X[3:0]<<8 ^ 8303 | E00C | X[3:0]<<12 ^ B003 ^ 800F |
E | 8027 | 8243 | X[3:0]<<4 ^ 8033 | A403 | X[3:0]<<8 ^ 8303 | 400C | X[3:0]<<12 ^ B003 ^ 800F |
F | 22 | 220 | X[3:0]<<4 | 2200 | X[3:0]<<8 | A00F | X[3:0]<<12 ^ 800F |
驗證:
-
CRC16計算雙位元組
比如計算
ABCD
的CRC16:crc16(A000) = C00A crc16(0B00) = BA03 crc16(00C0) = 0280 crc16(000D) = 802D 故crc16(ABCD) = C00A ^ BA03 ^ 0280 ^ 802D = F8A4
-
CRC16計算四位元組
如果資料是連貫的呢,比如
ABCD
crc(ABCD) = F8A4 crc(ABCD1234) = crc(F8A4 ^ 1234) = crc(EA90) = 400C ^ 3C00 ^ 360 ^ 0 = 7F6C