概述:本文詳細介紹了CRC循環(huán)冗余計算的數(shù)學原理,算法中使用的參數(shù)說明,并以Modbus協(xié)議中的CRC-16算法為例,進行手算驗證,同時提供LabVIEW和C語言的直接計算CRC-16 值的代碼以及C的查表計算CRC-16代碼和代碼原理的說明。
一、筆者個人經(jīng)歷
初次接觸CRC校驗是因為項目需要上位機軟件來記錄PLC寄存器中的數(shù)據(jù),實現(xiàn)PLC控制全過程中關鍵數(shù)據(jù)的記錄和查詢。上位機軟件使用LV進行編寫,數(shù)據(jù)的獲取通過Modbus TCP實現(xiàn),因為當時對Modbus和CRC都不是很熟悉,就采用了最成熟簡單的辦法,直接調(diào)用了第三方的Modbus工具包,項目功能也是順利實現(xiàn)。之后又遇到一個項目,需要上位機作為從機,回應主控的Modbus RTU指令,這次沒有選擇直接使用Modbus工具包,而是使用《LabVIEW寶典》中Modbus CRC-16校驗算法,根據(jù)Modbus RTU協(xié)議自主編程完成了項目要求。后來因為做嵌入式單片機,又了解使用了CRC-8和CRC-16的查表算法,也是沒有詳細了解過CRC循環(huán)冗余校驗的原理,僅僅是可以熟練實現(xiàn)功能。直到后來遇到一個項目,需要用示波器捕捉并分析出未知的通信幀的通信協(xié)議,幀頭,幀尾很快通過對比分析了出來,通信內(nèi)容也是反復實驗分析出具體數(shù)據(jù)字節(jié)和位的意義,就是校驗方式分析不出,但是可以肯定數(shù)據(jù)幀是一定包含校驗字節(jié)的,那時才認真開始考慮CRC循環(huán)冗余算法,試圖找出數(shù)據(jù)幀的校驗規(guī)則,很慚愧沒有分析出來,后來是通過第三方的幫助才解決了這個問題,但是當時腦中留下了很多問號和片段式的思考及部分無序的筆記,現(xiàn)在重新進行了整理、思考和驗證,形成此文,希望可以解答對CRC循環(huán)冗余校驗算法有疑問的同學的困惑。
二、CRC循環(huán)冗余校驗原理
百度CRC可以很容易的獲取CRC的定義:CRC(Cyclic Redundancy Check),即循環(huán)冗余校核,是一種根據(jù)網(wǎng)絡數(shù)據(jù)包或電腦文件等數(shù)據(jù)產(chǎn)生簡短固定位數(shù)校核碼的快速算法,主要用來檢測或校核數(shù)據(jù)傳輸或者保存后可能出現(xiàn)的錯誤。CRC利用除法及余數(shù)的原理,實現(xiàn)錯誤偵測的功能,具有原理清晰、實現(xiàn)簡單等優(yōu)點。
首先明確CRC是一種數(shù)據(jù)校驗方法,與和校驗、異或校驗功能相同,常用于通信的雙方判斷通信幀在通信過程中數(shù)據(jù)傳輸是否正確,如校驗不通過則需要考慮舍棄此通信幀,同時需根據(jù)需要判斷是否需要向發(fā)送方反饋通信異常的情況。
1、模2運算
從百度到的CRC定義中可以看到CRC是利用除法和余數(shù)的原理,這里所說的除法和余數(shù)的原理就是模2算法了,下面是模2算法的百度定義。
模2運算是一種二進制算法,CRC校驗技術中的核心部分。與四則運算相同,模2運算也包括模2加法、模2減法、模2乘法、模2除法四種二進制運算。與四則運算不同的是模2運算不考慮進位和借位,模2算術是編碼理論中多項式運算的基礎。模2算術在其他數(shù)字領域中的應用也是很廣泛的。
這里可以得出模2算法是不考慮進位和借位的,也就可以理解為每位都是獨立的,不會影響其他位也不會被其他位影響,這點在后文中的計算驗證部分有體現(xiàn)。
下面是模2運算的四則運算法則:
0+0=0;0+1=1;1+0=1;1+1=0;
0-0 =0;0-1=1;1-0=1;1-1=0;
0 *0=0;0 *1=0;1*0=0;1*1=1;
0/1=0; 1/1=1;
CRC算法中主要使用到的就是模2減法和模2除法。通過上述模2減法法則可以發(fā)現(xiàn),模2減法實際和異或運算在結(jié)果上是完全相同的,也就不再過多描述。這里最關鍵的是多位二進制的除法,這是CRC校驗的核心部分。模2除法具有以下性質(zhì):
(1)每一位除的結(jié)果不影響其他位,即不向上一位借位;
(2)當被除數(shù)的位數(shù)小于除數(shù)位數(shù)時,則商為0,被除數(shù)就是余數(shù),也可以理解為被除數(shù)首位為0時,商為0。
(3)只要被除數(shù)的位數(shù)和除數(shù)一樣多,且最高位為1,不管其他位是什么數(shù),皆可商1,也可以理解為被除數(shù)首位為1,商為1,余數(shù)為被除數(shù)與除數(shù)的模2減的結(jié)果;
(4)要保證每次除完首位都為0,才能進行右移;
(5)當最后余數(shù)的位數(shù)小于除數(shù)位數(shù)時,除法停止。
通過對上述多位二進制的模2除法性質(zhì)的思考,我們可以感覺到模2除與循環(huán)異或的本質(zhì)是相同的,這個可以通過下文中具體的計算過程體現(xiàn)。
2、CRC算法參數(shù)
這里給大家推薦一個很好用的CRC計算工具:
,這個計算工具包含了很多種CRC算法,并且標示出了具體的關鍵參數(shù),從我個人的使用經(jīng)歷上來說,需要注意的就僅僅是CRC-16 Modbus的計算結(jié)果是高字節(jié)在前,低字節(jié)在后的,這個與實際使用中通常低字節(jié)在前高字節(jié)在后不同,需要注意一下。下面我們就以CRC-16 Modbus為例對CRC算法進行說明。
(1)標準CRC生成多項式
每種CRC算法的標準生成多項式可能是不同的,這個需要進行注意。從上面截圖的左下方我們可以得知,CRC-16 Modbus的標準生成多項式是X16+X15+X2+1,其中1可以換成X0,這樣就很容易可以看出,16、15、2、0這些數(shù)字代表的是多位二進制數(shù)的數(shù)位,則標準生成多項式可寫為1 1000 0000 0000 0101,對應十六進制就是0x18005,也就是對應上圖右側(cè)的Poly:0x8005。這里的0x8005實際是標準生成多項式的簡記式,因為標準生成多項式的最高位固定為1,故在簡記式中就忽略了最高位1了,同時在程序編程中實際使用的也是簡記式,這個在下文的程序部分有所體現(xiàn)。
(2)CRC初始值
初始值,這個也是根據(jù)具體哪種CRC標準來確定的,不同的CRC標準對應不同的計算初始值。還是以CRC-16 Modbus為例,計算初始值就是0xFFFF,對應上圖Init:0xFFFF。計算初始值先與需要校驗的數(shù)據(jù)的首字節(jié)數(shù)據(jù)進行異或,異或后結(jié)果進行模2除法運算,這個后續(xù)程序部分會體現(xiàn)。
(3)正序/反序
就像串口通信需要考慮低位先傳還是高位先傳一樣,循環(huán)冗余計算時也需要考慮從高位開始還是低位開始,即編程時需要考慮數(shù)據(jù)進行左移還是右移。需要說明的一點是,數(shù)據(jù)進行正序或者反序,最后的結(jié)果是不相同的,這個下文也會進行驗證說明。上圖計算工具中是通過RefIn和RefOut來進行體現(xiàn)的。
RefIn:true或false表示在進行計算之前,原始數(shù)據(jù)是否翻轉(zhuǎn),如原始數(shù)據(jù):0x34 = 0011 0100,如果REFIN為true,進行翻轉(zhuǎn)之后為0010 1100 = 0x2C。
REFOUT:true或false表示運算完成之后,得到的CRC值是否進行翻轉(zhuǎn),如計算得到的CRC值:0x97 = 1001 0111,如果REFOUT為true,進行翻轉(zhuǎn)之后為1110 1001 = 0xE9。
以CRC-16 Modbus為例,都是True,則表示反序循環(huán)冗余校驗。這個結(jié)合下文程序會更好理解,下文也會進行相應的說明。
(4)CRC結(jié)果異或值
CRC結(jié)果異或值就是CRC循環(huán)冗余計算的結(jié)果與CRC結(jié)果異或值進行異或處理,結(jié)果為CRC計算的最終值,對應上圖中的XorOut:0x0000。
3、手算CRC算法及驗證
前面已經(jīng)介紹了模2算法以及CRC算法的參數(shù),下面就來驗證一下上面的理論是否正確。還是以CRC-16 Modbus為例,對單字節(jié)0x12數(shù)據(jù)計算校驗值。
首先需要確定CRC-16 Modbus算法的參數(shù):
(1)標準生成多項式為X16+X15+X2+1,轉(zhuǎn)換成二進制則為1 1000 0000 0000 0101;
(2)初始值為0xFFFF;
(3)采用反序的計算順序;
(4)CRC結(jié)果異或值為0x0000;
然后就按照CRC-16 Modbus算法來進行計算,
0x12轉(zhuǎn)為二進制為0001 0010;
與0xFFFF即1111 1111 1111 1111異或,結(jié)果為1111 1111 1110 1101;
反序為1011 0111 1111 1111;
下面進行模2除,被除數(shù)為1011 0111 1111 1111 0000 0000,除數(shù)為1 1000 0000 0000 0101,因為通信幀的基本單位是字節(jié),所以被除數(shù)為1011 0111 1111 1111后面加8個0。
模2除余數(shù)為1111 1100 1011 0010;(這里需要注意一下,CRC循環(huán)冗余算法中關注的是模2除的余數(shù),而不是商)
反序為0100 1101 0011 1111,即0x4D3F;
結(jié)果再與0x0000進行異或,最終結(jié)果為0x4D3F。
利用上面介紹的CRC計算小工具進行驗證,結(jié)果如下圖所示。
同理,筆者針對不同標準生成多項式進行手算實驗,選擇了CRC-32 對0x12進行CRC校驗,手算結(jié)果與CRC計算工具結(jié)果相同。
同時針對數(shù)據(jù)的正序反序問題,選擇CRC-8對0x12進行CRC校驗,手算結(jié)果與CRC計算工具結(jié)果相同。
對于多字節(jié)數(shù)據(jù)的校驗過程是:首先首字節(jié)與初始默認值進行異或校驗,結(jié)果作為被除數(shù)進行模2除法;下一個字節(jié)與上一個字節(jié)的模2除法的結(jié)果進行異或,作為下一次模2除法的被除數(shù);以此類推,這個在下文代碼部分體現(xiàn)。
筆者針對多字節(jié)也進行了手算實驗,選擇了CRC-16 Modbus對0x12、0x34兩個字節(jié)進行了CRC校驗,手算結(jié)果與CRC計算工具結(jié)果相同。
三、CRC 編程實現(xiàn)方法
1、直接計算法
CRC算法這里還是以CRC-16 Modbus為例,其直接計算編程實現(xiàn)過程為:
1)設置CRC寄存器,并給其賦值0xFFFF。
2)將數(shù)據(jù)的第一個8-bit字符(將此8位高位補0為16位)與16位CRC寄存器的值進行異或,并把結(jié)果存入CRC寄存器。
3)CRC寄存器向右移(即最低位方向)一位,MSB補零,移出并檢查LSB。
4)如果LSB為0,重復第三步;若LSB為1,CRC寄存器與多項式碼(0xA001)相異或。此時的0xA001即為0x8005的反序。
注意:該步檢查LSB應該是右移前的LSB,即第3步前的LSB。
5)重復第3與第4步直到8次移位全部完成。此時一個8-bit數(shù)據(jù)處理完畢。
6)重復第2至第5步直到所有數(shù)據(jù)全部處理完成。
7)最終CRC寄存器的內(nèi)容即為CRC值。
這里對上文中提及的標準多項式和簡記式的區(qū)別再進行一下說明:
在上文中手算部分可以看到實際標準多項式最高位對應被除數(shù)的那一位必定是1,與標準多項式最高位異或的結(jié)果或者說模2減的結(jié)果必定是0,因此,在步驟4)就是僅判斷LSB是1還是0,進而確定是先進行異或再移位還是直接進行移位,而不參與異或運算,同時也可以理解上述步驟中僅涉及簡記式進行異或或者說模2減。
讀者可以結(jié)合上文中的手算部分的運算步驟來理解這里的處理步驟,關鍵是理解模2除和數(shù)據(jù)右移的關系,筆者這里不再進行過多說明。
LabVIEW直接計算 CRC-16 Modbus:
程序中while循環(huán)實際就是模2除法的體現(xiàn),以下程序同理。
C語言直接計算CRC-16 Modbus:
unsigned short do_crc(unsigned char *ptr, int len)
{
unsigned char i;
unsigned int crc16 = 0xFFFF;
while(len--)
{
crc16 ^= *ptr++;
for (i = 0; i < 8; ++i)
{
if (crc16 &0x0001)
crc16 = (crc16 >> 0x01) ^ 0xA001;
else
crc16 = (crc16 >> 0x01);
}
}
return crc16;
}
這里有一點需要注意的是,返回的CRC16為16位數(shù),分為兩個字節(jié),高低字節(jié)需轉(zhuǎn)換(僅針對Modbus,因為modbus一般要求校驗值低位在前高位在后)。
2、查表法
對于查表法,其實就是利用空間換時間,通過直接查詢CRC運算結(jié)果表格來減少計算時間,這個在嵌入式單片機方面使用較多,這邊還是以CRC-16 Modbus為例。
首先針對表格進行說明:
因為標準生成多項式為X16+X15+X2+1,則可以確定校驗結(jié)果為2字節(jié),因此表格分為高字節(jié)和低字節(jié),高低位CRC數(shù)組中(即下面的 Table_CRCL[256] 和 Table_CRCH[256]中 )同下標的兩個單字節(jié)數(shù)組合成一個雙字節(jié)校驗值。
關于下面表格中數(shù)值,是使用CRC-16,(標準生成多項式為X16+X15+X2+1;初始值0x0000;RefIn:True,RefOut:True,即反序;XorOut:0x0000)計算的0-255的CRC值。例如0x01按照上述CRC算法,結(jié)果是0xC0C1,即對應Table_CRCH[1]=0xC0和Table_CRCL[1]=0xC1。這里讀者可能有疑問,為什么不是使用CRC-16 Modbus的算法?對比CRC-16 Modbus和上述算法的參數(shù),可以看到僅僅是初始值不同,CRC-16 Modbus 初始值為0xFFFF。這里先明確,表的意義是取代模2除法的計算過程。
再回到之前的手算部分,即如下所示。
從上式可以發(fā)現(xiàn),被除數(shù)從左邊起,8位及之后的7位,即1111 1111,這些數(shù)位僅參與異或且全程參與異或,或者說模2減,而異或是符合交換律的,即A^B^C=A^C^B,則1111 1111也可最后再參與異或,而0x00與任何單字節(jié)數(shù)異或均為單字節(jié)數(shù)本身,則上式被除數(shù)可以改成1011 0111 0000 0000 0000 0000 ,最后的余數(shù)低字節(jié)再與原被除數(shù)高字節(jié)異或,得到的數(shù)就是符合CRC-16 Modbus算法的最終的低字節(jié)值,也就是說原被除數(shù)高字節(jié)的值僅影響模2除余數(shù)的低字節(jié)值,這也就是下文代碼部分的解釋。
C語言查表計算CRC-16 Modbus:
const uint8_t Table_CRCL[256] = // CRC 高位字節(jié)值表
{
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
0x80, 0x41, 0x00, 0xC1, 0x81, 0x40
};
const uint8_t Table_CRCH[256] = // CRC高位位字節(jié)值表
{
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
0x43, 0x83, 0x41, 0x81, 0x80, 0x40
};
uint16_t CRC16(uint8_t *puchMsg, uint8_t DataLen)
{
uint8_t CRCH= 0xFF ; // 高CRC字節(jié)初始化
uint8_t CRCL = 0xFF ; // 低CRC 字節(jié)初始化
uint8_t Index ; // CRC表中的索引
while (DataLen--)
{
Index = CRCL ^ *puchMsg++ ;
CRCL = CRCH ^ CRCL[Index];
CRCH = CRCH[Index];
}
return ((uint16_t)CRCL<< 8 | CRCH); // Modbus校驗值一般低字節(jié)在前,高字節(jié)在后
}
四、總結(jié)
本文選擇以CRC-16 Modbus算法標準進行了詳細的舉例及手算驗證,同時筆者也對比其他算法,如CRC-8,CRC-32進行了針對性的驗證,結(jié)果均證明正確。之所以選擇CRC-16 Modbus,感覺這個可能是大家平時使用中較為常用的,尤其是工控領域,同時也相信讀者舉一反三的能力,可以按照本文介紹方法掌握其他CRC算法。本文編寫期間也是對文字,計算及代碼反復考量驗證,力求正確性和邏輯性,轉(zhuǎn)發(fā)及引用請注明出處。
——文章來自宋雨的個人分享
審核編輯:湯梓紅
-
LabVIEW
+關注
關注
1971文章
3654瀏覽量
323598 -
crc
+關注
關注
0文章
199瀏覽量
29465 -
C語言
+關注
關注
180文章
7604瀏覽量
136824 -
代碼
+關注
關注
30文章
4788瀏覽量
68612 -
循環(huán)冗余校驗
+關注
關注
0文章
7瀏覽量
6541
發(fā)布評論請先 登錄
相關推薦
評論