Modbus庫開發筆記之八:CRC循環冗余校驗的研究與實現


      談到Modbus通訊自然免不了循環冗余校驗(CRC),特別是在標准的串行RTU鏈路上是必不可少的。不僅如此在其他開發中,也經常要用到CRC 算法對各種數據進行校驗。這樣一來,我們就需要研究一下這個循環冗余校驗(CRC)算法。

1CRC簡述

      循環冗余檢查(CRC)是一種數據傳輸檢錯功能,對數據進行多項式計算,並將得到的結果附在幀的后面,接收設備也執行類似的算法,以保證數據傳輸的正確性和完整性。

CRC校驗的基本思想是利用線性編碼理論,在發送端根據要傳送的k位二進制碼序列,以一定的規則產生一個校驗用的監督碼(既CRC碼)r位,並附在信息后邊,構成一個新的二進制碼序列數共(k+r)位,最后發送出去。在接收端,則根據信息碼和CRC碼之間所遵循的規則進行檢驗,以確定傳送中是否出錯。

CRC的本質是模2除法的余數,采用的除數不同,CRC的類型也就不一樣。通常,CRC的除數用生成多項式來表示。最常用的CRC碼的生成多項式有下面幾種:

      不一樣的生成多項式,所以得到的結果自然也是不一樣的。事實上在Modbus通訊中采用的是CRC-16的方式。

2、算法分析

      CRC校驗碼的編碼方法是用待發送的二進制數據t(x)除以生成多項式g(x),將最后的余數作為CRC校驗碼。其實現步驟如下:

      設待發送的數據塊是m位的二進制多項式t(x),生成多項式為r階的g(x)。在數據塊的末尾添加r個0,數據塊的長度增加到m+r位,對應的二進制多項式為 。用生成多項式g(x)去除 ,求得余數為階數為r-1的二進制多項式y(x)。此二進制多項式y(x)就是t(x)經過生成多項式g(x)編碼的CRC校驗碼。用 以模2的方式減去y(x),得到二進制多項式 。 就是包含了CRC校驗碼的待發送字符串。

      從CRC的編碼規則可以看出,CRC編碼實際上是將代發送的m位二進制多項式t(x)轉換成了可以被g(x)除盡的m+r位二進制多項式,所以解碼時可以用接收到的數據去除g(x),如果余數位零,則表示傳輸過程沒有錯誤;如果余數不為零,則在傳輸過程中肯定存在錯誤。許多CRC的硬件解碼電路就是按這種方式進行檢錯的。同時可以看作是由t(x)和CRC校驗碼的組合,所以解碼時將接收到的二進制數據去掉尾部的r位數據,得到的就是原始數據。

      實際上,真正的CRC 計算通常與上面描述的還有些不同。這是因為這種最基本的CRC除法存在一個很明顯的缺陷,就是數據流的開頭添加一些0並不影響最后校驗的結果。為了彌補這一缺陷所以引入了兩個概念:一個是“余數初始值”,另一個是“結果異或值”。所謂 “余數初始值”就是在計算CRC值前,為存儲變量所賦的初值。對應的“結果異或值”就是在計算完成后,將變量值與這個值作最后的異或運算而得到校驗結果。

名稱

校驗和位寬

生成多項式

除數(多項式)

余數初始值

結果異或值

CRC-4

4

x4+x+1

3

 

 

CRC-8

8

x8+x5+x4+1

0x31

 

 

CRC-8

8

x8+x2+x1+1

0x07

 

 

CRC-8

8

x8+x6+x4+x3+x2+x1

0x5E

 

 

CRC-12

12

x12+x11+x3+x+1

80F

 

 

CRC-16 

16

x16+x15+x2+1

0x8005

0x0000

0x0000

CRC-CCITT

16

x16+x12+x5+1

0x1021

0xFFFF

0x0000

CRC-32

32

x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+1

0x04C11DB7

0xFFFFFFFF

0xFFFFFFFF

CRC-32c

32

x32+x28+x27+...+x8+x6+1

1EDC6F41

 

 

 

      說到這里我們已經可以描述一下這個算法的實現過程:

第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值。

3、代碼實現

      有了前面的准備實際上我們要實現CRC校驗的代碼已經很簡單了,實現這一過程有各種方法我們說常用的2種:一是直接計算法,就是按照前面的步驟計算出來;二是驅動表法,就是將一些數據儲存起來直接獲取計算。因為在Modbus中使用的是CRC-16,所以我們一次為例來實現它。

(1)直接計算法

直接計算法簡單直接,便寫程序也比較簡單,我們以CRC-16為例,其多項式記為0x8005,因為其記過異或值為0x0000,所以可以不添加。具體代碼如下:

#define Initial_Value    0x0000

#define EOR 0x0000

#define POLY16 0x8005

uint16_t CRC16(uint8_t *buf,uint16_t length)

{

  uint16_t crc16,data,val;

 

  crc16 = Initial_Value;

 

  for(int i=0;i<length;i++)

  {

    if((i % 8) == 0)

    {

      data = (*buf++)<<8;

     }

    val = crc16 ^ data;

    crc16 = crc16<<1;

    data = data <<1;

    if(val&0x8000)

    {

      crc16 = crc16 ^ POLY16;

    }

  }

  return crc16;

}

(2)驅動表法

     對於直接計算法,雖然簡單直接,但有時候效率卻是個問題,所以在Modbus通訊中我們通常采用驅動表法來實現:

//CRC_16高8位數據區

const uint8_t auchCRCHi[] = {

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

};

//CRC低位字節值表

const uint8_t auchCRCLo[] = {//CRC_16低8位數據區

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

};

 

/*函數功能:CRC校驗碼生成

  輸入參數:puchMsgg是要進行CRC校驗的消息,usDataLen是消息中字節數

  函數輸出:計算出來的CRC校驗碼

  GenerateCRC16CheckCode查表計算函數*/

static uint16_t GenerateCRC16CheckCode(uint8_t *puckMsg,uint8_t usDataLen)

{

  uint8_t uchCRCHi = 0xFF ; //高CRC字節初始化

  uint8_t uchCRCLo = 0xFF ; //低CRC 字節初始化

  uint32_t  uIndex ; //CRC循環中的索引

  //傳輸消息緩沖區

  while (usDataLen--)

  {

    //計算CRC

    uIndex = uchCRCLo ^ *puckMsg++ ; 

    uchCRCLo = uchCRCHi ^ auchCRCHi[uIndex] ;

    uchCRCHi = auchCRCLo[uIndex] ;

  }

  //返回結果,高位在前

  return (uchCRCLo << 8 |uchCRCHi) ;

}

4、結束語

      CRC的應用非常廣泛,特別是在做通訊時更是經常見到,所以掌握它是非常有必要的,至少會使用它。我們在開發Modbus庫函數的過程中,對它也不過是有了一些比較粗淺的理解,在此記述以求共進。

 

若對本文檔有興趣,可已添加如下公眾號有更多精彩內容:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM