原文地址:循環冗餘校驗(CRC)算法入門引導html
參考地址:https://en.wikipedia.org/wiki/Computation_of_cyclic_redundancy_checks#References 給出了CRC 計算的詳細信息。想要深刻學習,能夠從這個網址開始。尤爲是最後給出的 Reference 各個是精品程序員
http://www.zorc.breitbandkatze.de/crc.html 是個很是有用的網站,文中給出的代碼都與那裏的結果進行了對比算法
寫給嵌入式程序員的循環冗餘校驗(CRC)算法入門引導編程
CRC校驗(循環冗餘校驗)是數據通信中最常採用的校驗方式。在嵌入式軟件開發中,常常要用到CRC 算法對各類數據進行校驗。所以,掌握基本的CRC算法應是嵌入式程序員的基本技能。但是,我認識的嵌入式程序員中能真正掌握CRC算法的人卻不多,日常在項目中見到的CRC的代碼多數都是那種效率很是低下的實現方式。ide
其實,在網上有一篇介紹CRC 算法的很是好的文章,做者是Ross Williams,題目叫:「A PAINLESS GUIDE TO CRC ERROR DETECTION ALGORITHMS」。我常將這篇文章推薦給向我詢問CRC算法的朋友,但很多朋友向我抱怨原文太長了,並且是英文的。但願我能寫篇短點的文章,所以就有了本文。不過,個人水平比不了Ross Williams,個人文章確定也沒Ross Williams的寫的好。所以,閱讀英文沒有障礙的朋友仍是去讀Ross Williams的原文吧。函數
本文的讀者羣設定爲軟件開發人員,尤爲是從事嵌入式軟件開發的程序員,而不是專業從事數學或通信領域研究的學者(我也沒有這個水平寫的這麼高深)。所以,本文的目標是介紹CRC算法的基本原理和實現方式,用到的數學儘可能控制在高中生能夠理解的深度。oop
另外,鑑於大多數嵌入式程序員都是半路出家轉行過來的,很多人只會C語言。所以,文中的示例代碼所有采用C語言來實現。做爲一篇入門短文,文中給出的代碼更注重於示範性,儘量的保持易讀性。所以,文中的代碼並不追求最高效的實現,但對於通常的應用卻也足夠快速了。學習
所謂通信過程的校驗是指在通信數據後加上一些附加信息,經過這些附加信息來判斷接收到的數據是否和發送出的數據相同。好比說RS232串行通信能夠設置奇偶校驗位,所謂奇偶校驗就是在發送的每個字節後都加上一位,使得每一個字節中1的個數爲奇數個或偶數個。好比咱們要發送的字節是0x1a,二進制表示爲0001 1010。大數據
採用奇校驗,則在數據後補上個0,數據變爲0001 1010 0,數據中1的個數爲奇數個(3個)網站
採用奇校驗,則在數據後補上個1,數據變爲0001 1010 1,數據中1的個數爲偶數個(4個)
接收方經過計算數據中1個數是否知足奇偶性來肯定數據是否有錯。
奇偶校驗的缺點也很明顯,首先,它對錯誤的檢測機率大約只有50%。也就是隻有一半的錯誤它可以檢測出來。另外,每傳輸一個字節都要附加一位校驗位,對傳輸效率的影響很大。所以,在高速數據通信中不多采用奇偶校驗。奇偶校驗優勢也很明顯,它很簡單,所以能夠用硬件來實現,這樣能夠減小軟件的負擔。所以,奇偶校驗也被普遍的應用着。
奇偶校驗就先介紹到這來,之因此從奇偶校驗提及,是由於這種校驗方式最簡單,並且後面將會知道奇偶校驗其實就是CRC 校驗的一種(CRC-1)。
另外一種常見的校驗方式是累加和校驗。所謂累加和校驗實現方式有不少種,最經常使用的一種是在一次通信數據包的最後加入一個字節的校驗數據。這個字節內容爲前面數據包中所有數據的忽略進位的按字節累加和。好比下面的例子:
咱們要傳輸的信息爲: 六、2三、2
加上校驗和後的數據包:六、2三、二、31
這裏 31 爲前三個字節的校驗和。接收方收到所有數據後對前三個數據進行一樣的累加計算,若是累加和與最後一個字節相同的話就認爲傳輸的數據沒有錯誤。
累加和校驗因爲實現起來很是簡單,也被普遍的採用。可是這種校驗方式的檢錯能力也比較通常,對於單字節的校驗和大概有1/256 的機率將本來是錯誤的通信數據誤判爲正確數據。之因此這裏介紹這種校驗,是由於CRC校驗在傳輸數據的形式上與累加和校驗是相同的,均可以表示爲:通信數據 校驗字節(也多是多個字節)
CRC 算法的基本思想是將傳輸的數據當作一個位數很長的數。將這個數除以另外一個數。獲得的餘數做爲校驗數據附加到原數據後面。還以上面例子中的數據爲例:
六、2三、2 能夠看作一個2進制數: 0000011000010111 00000010
假如被除數選9,二進制表示爲:1001
則除法運算能夠表示爲:
能夠看到,最後的餘數爲1。若是咱們將這個餘數做爲校驗和的話,傳輸的數據則是:六、2三、二、1
CRC 算法和這個過程有點相似,不過採用的不是上面例子中的一般的這種除法。在CRC算法中,將二進制數據流做爲多項式的係數,而後進行的是多項式的乘除法。仍是舉個例子吧。
好比說咱們有兩個二進制數,分別爲:1101 和1011。
1101 與以下的多項式相聯繫:1x3+1x2+0x1+1x0=x3+x2+x0
1011與以下的多項式相聯繫:1x3+0x2+1x1+1x0=x3+x1+x0
兩個多項式的乘法:(x3+x2+x0)(x3+x1+x0)=x6+x5+x4+x3+x3+x3+x2+x1+x0
獲得結果後,合併同類項時採用模2運算。也就是說乘除法採用正常的多項式乘除法,而加減法都採用模2運算。所謂模2運算就是結果除以2後取餘數。好比3 mod 2 = 1。所以,上面最終獲得的多項式爲:x6+x5+x4+x3+x2+x1+x0,對應的二進制數:111111
加減法採用模2運算後其實就成了一種運算了,就是咱們一般所說的異或運算:
0+0=0 0+1=1 1+0=1 1+1=0 |
0-0=0 1-0=1 0-1=1 1-1=0 |
上面說了半天多項式,其實就算是不引入多項式乘除法的概念也能夠說明這些運算的特殊之處。只不過幾乎全部講解 CRC 算法的文獻中都會提到多項式,所以這裏也簡單的寫了一點基本的概念。不過總用這種多項式表示也很羅嗦,下面的講解中將盡可能採用更簡潔的寫法。
除法運算與上面給出的乘法概念相似,仍是遇到加減的地方都用異或運算來代替。下面是一個例子:
要傳輸的數據爲:1101011011
被除數設爲:10011
在計算前先將原始數據後面填上4個0:11010110110000,之因此要補0,後面再作解釋。
從這個例子能夠看出,採用了模2的加減法後,不須要考慮借位的問題,因此除法變簡單了。最後獲得的餘數就是CRC 校驗字。爲了進行CRC運算,也就是這種特殊的除法運算,必需要指定個被除數,在CRC算法中,這個被除數有一個專有名稱叫作「生成多項式」。生成多項式的選取是個頗有難度的問題,若是選的很差,那麼檢出錯誤的機率就會低不少。好在這個問題已經被專家們研究了很長一段時間了,對於咱們這些使用者來講,只要把現成的成果拿來用就好了。
最經常使用的幾種生成多項式以下:
CRC8=X8+X5+X4+X0
CRC-CCITT=X16+X12+X5+X0
CRC16=X16+X15+X2+X0
CRC12=X12+X11+X3+X2+X0
CRC32=X32+X26+X23+X22+X16+X12+X11+X10+X8+X7+X5+X4+X2+X1+X0
有一點要特別注意,文獻中提到的生成多項式常常會說到多項式的位寬(Width,簡記爲W),這個位寬不是多項式對應的二進制數的位數,而是位數減1。好比CRC8中用到的位寬爲8的生成多項式,其實對應得二進制數有九位:100110001。另一點,多項式表示和二進制表示都很繁瑣,交流起來不方便,所以,文獻中多用16進制簡寫法來表示,由於生成多項式的最高位確定爲1,最高位的位置由位寬可知,故在簡記式中,將最高的1統一去掉了,如CRC32的生成多項式簡記爲04C11DB7實際上表示的是104C11DB7。固然,這樣簡記除了方便外,在編程計算時也有它的用處。
對於上面的例子,位寬爲4(W=4),按照CRC算法的要求,計算前要在原始數據後填上W個0,也就是4個0。
位寬W=1的生成多項式(CRC1)有兩種,分別是X1和X1+X0,讀者能夠本身證實10 對應的就是奇偶校驗中的奇校驗,而11對應則是偶校驗。所以,寫到這裏咱們知道了奇偶校驗其實就是CRC校驗的一種特例,這也是我要以奇偶校驗做爲開篇介紹的緣由了。
說了這麼多總算到了核心部分了。從前面的介紹咱們知道CRC校驗覈心就是實現無借位的除法運算。下面仍是經過一個例子來講明如何實現CRC校驗。
假設咱們的生成多項式爲:100110001(簡記爲0x31),也就是CRC-8
則計算步驟以下:
(1) 將CRC寄存器(8-bits,比生成多項式少1bit)賦初值0
(2) 在待傳輸信息流後面加入8個0
(3) While (數據未處理完)
(4) Begin
(5) If (CRC寄存器首位是1)
(6) reg = reg XOR 0x31
(7) CRC寄存器左移一位,讀入一個新的數據於CRC寄存器的0 bit的位置。
(8) End
(9) CRC寄存器就是咱們所要求的餘數。
實際上,真正的CRC 計算一般與上面描述的還有些出入。這是由於這種最基本的CRC除法有個很明顯的缺陷,就是數據流的開頭添加一些0並不影響最後校驗字的結果。這個問題很讓人惱火啊,所以真正應用的CRC 算法基本都在原始的CRC算法的基礎上作了些小的改動。
所謂的改動,也就是增長了兩個概念,第一個是「餘數初始值」,第二個是「結果異或值」。
所謂的「餘數初始值」就是在計算CRC值的開始,給CRC寄存器一個初始值。「結果異或值」是在其他計算完成後將CRC寄存器的值在與這個值進行一下異或操做做爲最後的校驗值。
常見的三種CRC 標準用到個各個參數以下表。
|
CCITT |
CRC16 |
CRC32 |
校驗和位寬W |
16 |
16 |
32 |
生成多項式 |
x16+x12+x5+X0 |
x16+x15+x2+X0 |
x32+x26+x23+x22+x16+ x12+x11+x10+x8+x7+x5+ x4+x2+x1+X0 |
除數(多項式) |
0x1021 |
0x8005 |
0x04C11DB7 |
餘數初始值 |
0xFFFF |
0x0000 |
0xFFFFFFFF |
結果異或值 |
0x0000 |
0x0000 |
0xFFFFFFFF |
加入這些變形後,常見的算法描述形式就成了這個樣子了:
(1) 設置CRC寄存器,並給其賦值爲「餘數初始值」。
(2) 將數據的第一個8-bit字符與CRC寄存器進行異或,並把結果存入CRC寄存器。
(3) CRC寄存器向右移一位,MSB補零,移出並檢查LSB。
(4) 若是LSB爲0,重複第三步;若LSB爲1,CRC寄存器與0x31相異或。
(5) 重複第3與第4步直到8次移位所有完成。此時一個8-bit數據處理完畢。
(6) 重複第2至第5步直到全部數據所有處理完成。
(7) 最終CRC寄存器的內容與「結果異或值」進行或非操做後即爲CRC值。
示例性的C代碼以下所示,由於效率很低,項目中如對計算時間有要求應該避免採用這樣的代碼。不過這個代碼已經比網上常見的計算代碼要好了,由於這個代碼有一個crc的參數,能夠將上次計算的crc結果傳入函數中做爲此次計算的初始值,這對大數據塊的CRC計算是頗有用的,不須要一次將全部數據讀入內存,而是讀一部分算一次,全讀完後就計算完了。這對內存受限系統仍是頗有用的。
1 #define POLY 0x1021 2 /** 3 * Calculating CRC-16 in 'C' 4 * @para addr, start of data 5 * @para num, length of data 6 * @para crc, incoming CRC 7 */ 8 uint16_t crc16(unsigned char *addr, int num, uint16_t crc) 9 { 10 int i; 11 for (; num > 0; num--) /* Step through bytes in memory */ 12 { 13 crc = crc ^ (*addr++ << 8); /* Fetch byte from memory, XOR into CRC top byte*/ 14 for (i = 0; i < 8; i++) /* Prepare to rotate 8 bits */ 15 { 16 if (crc & 0x8000) /* b15 is set... */ 17 crc = (crc << 1) ^ POLY; /* rotate and XOR with polynomic */ 18 else /* b15 is clear... */ 19 crc <<= 1; /* just rotate */ 20 } /* Loop for 8 bits */ 21 crc &= 0xFFFF; /* Ensure CRC remains 16-bit value */ 22 } /* Loop until num=0 */ 23 return(crc); /* Return updated CRC */ 24 }
上面的代碼是我從http://mdfs.net/Info/Comp/Comms/CRC16.htm找到的,不過原始代碼有錯誤,我作了些小的修改。
下面對這個函數給出個例子片斷代碼:
1 unsigned char data1[] = {'1', '2', '3', '4', '5', '6', '7', '8', '9'}; 2 unsigned char data2[] = {'5', '6', '7', '8', '9'}; 3 unsigned short c1, c2; 4 c1 = crc16(data1, 9, 0xffff); 5 c2 = crc16(data1, 4, 0xffff); 6 c2 = crc16(data2, 5, c2); 7 printf("%04x\n", c1); 8 printf("%04x\n", c2);
讀者能夠驗算,c一、c2 的結果都爲 29b1。上面代碼中crc 的初始值之因此爲0xffff,是由於CCITT標準要求的除數初始值就是0xffff。
上面的算法對數據流逐位進行計算,效率很低。實際上仔細分析CRC計算的數學性質後咱們能夠多位多位計算,最經常使用的是一種按字節查表的快速算法。該算法基於這樣一個事實:計算本字節後的CRC碼,等於上一字節餘式CRC碼的低8位左移8位,加上上一字節CRC右移 8位和本字節之和後所求得的CRC碼。若是咱們把8位二進制序列數的CRC(共256個)所有計算出來,放在一個表裏,編碼時只要從表中查找對應的值進行處理便可。
按照這個方法,能夠有以下的代碼(這個代碼也不是我寫的,是我在Micbael Barr的書「Programming Embedded Systems in C and C++」 中找到的,一樣,我作了點小小的改動。):
1 /* 2 crc.h 3 */ 4 5 #ifndef CRC_H_INCLUDED 6 #define CRC_H_INCLUDED 7 8 /* 9 * The CRC parameters. Currently configured for CCITT. 10 * Simply modify these to switch to another CRC Standard. 11 */ 12 /* 13 #define POLYNOMIAL 0x8005 14 #define INITIAL_REMAINDER 0x0000 15 #define FINAL_XOR_VALUE 0x0000 16 */ 17 #define POLYNOMIAL 0x1021 18 #define INITIAL_REMAINDER 0xFFFF 19 #define FINAL_XOR_VALUE 0x0000 20 21 /* 22 #define POLYNOMIAL 0x1021 23 #define POLYNOMIAL 0xA001 24 #define INITIAL_REMAINDER 0xFFFF 25 #define FINAL_XOR_VALUE 0x0000 26 */ 27 28 /* 29 * The width of the CRC calculation and result. 30 * Modify the typedef for an 8 or 32-bit CRC standard. 31 */ 32 typedef unsigned short width_t; 33 #define WIDTH (8 * sizeof(width_t)) 34 #define TOPBIT (1 << (WIDTH - 1)) 35 36 /** 37 * Initialize the CRC lookup table. 38 * This table is used by crcCompute() to make CRC computation faster. 39 */ 40 void crcInit(void); 41 42 /** 43 * Compute the CRC checksum of a binary message block. 44 * @para message, 用來計算的數據 45 * @para nBytes, 數據的長度 46 * @note This function expects that crcInit() has been called 47 * first to initialize the CRC lookup table. 48 */ 49 width_t crcCompute(unsigned char * message, unsigned int nBytes); 50 51 #endif // CRC_H_INCLUDED
/* *crc.c */ #include "crc.h" /* * An array containing the pre-computed intermediate result for each * possible byte of input. This is used to speed up the computation. */ static width_t crcTable[256]; /** * Initialize the CRC lookup table. * This table is used by crcCompute() to make CRC computation faster. */ void crcInit(void) { width_t remainder; width_t dividend; int bit; /* Perform binary long division, a bit at a time. */ for(dividend = 0; dividend < 256; dividend++) { /* Initialize the remainder. */ remainder = dividend << (WIDTH - 8); /* Shift and XOR with the polynomial. */ for(bit = 0; bit < 8; bit++) { /* Try to divide the current data bit. */ if(remainder & TOPBIT) { remainder = (remainder << 1) ^ POLYNOMIAL; } else { remainder = remainder << 1; } } /* Save the result in the table. */ crcTable[dividend] = remainder; } } /* crcInit() */ /** * Compute the CRC checksum of a binary message block. * @para message, 用來計算的數據 * @para nBytes, 數據的長度 * @note This function expects that crcInit() has been called * first to initialize the CRC lookup table. */ width_t crcCompute(unsigned char * message, unsigned int nBytes) { unsigned int offset; unsigned char byte; width_t remainder = INITIAL_REMAINDER; /* Divide the message by the polynomial, a byte at a time. */ for( offset = 0; offset < nBytes; offset++) { byte = (remainder >> (WIDTH - 8)) ^ message[offset]; remainder = crcTable[byte] ^ (remainder << 8); } /* The final remainder is the CRC result. */ return (remainder ^ FINAL_XOR_VALUE); } /* crcCompute() */
上面代碼中crcInit() 函數用來計算crcTable,所以在調用 crcCompute 前必須先調用 crcInit()。不過,對於嵌入式系統,RAM是很緊張的,最好將 crcTable 提早算好,做爲常量數據存到程序存儲區而不佔用RAM空間。CRC 計算實際上還有不少內容能夠介紹,不過對於通常的程序員來講,知道這些也就差很少了。餘下的部分之後有時間了我再寫篇文章來介紹吧。