天天看點

Modbus庫開發筆記之八:CRC循環備援校驗的研究與實作

      談到Modbus通訊自然免不了循環備援校驗(CRC),特别是在标準的串行RTU鍊路上是必不可少的。不僅如此在其他開發中,也經常要用到CRC 算法對各種資料進行校驗。這樣一來,我們就需要研究一下這個循環備援校驗(CRC)算法。

1、CRC簡述

      循環備援檢查(CRC)是一種資料傳輸檢錯功能,對資料進行多項式計算,并将得到的結果附在幀的後面,接收裝置也執行類似的算法,以保證資料傳輸的正确性和完整性。

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

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

Modbus庫開發筆記之八: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
x8+x2+x1+1 0x07
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
CRC-CCITT x16+x12+x5+1 0x1021 0xFFFF
CRC-32 32 x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x1+1 0x04C11DB7 0xFFFFFFFF
CRC-32c 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,

0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,

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,

0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 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庫函數的過程中,對它也不過是有了一些比較粗淺的了解,在此記述以求共進。

若對本文檔有興趣,可已添加如下公衆号有更多精彩内容:

如果閱讀這篇文章讓您略有所得,還請點選下方的【好文要頂】按鈕。

當然,如果您想及時了解我的部落格更新,不妨點選下方的【關注我】按鈕。

如果您希望更友善且及時的閱讀相關文章,也可以掃描上方二維碼關注我的微信公衆号【木南創智】

繼續閱讀