[C原創]_產生ModBus RTU格式的CRC碼

今天我要來打一篇久違沒發的Code note
因為其實我的實力太嫩…大多數的Code problem都可以找到相關的中文文獻
所以一直沒有發太多的Code note…
今天要講的是ModBus RTU格式的CRC
若純寫軟體或者APP等,不需要跟低階外部設備交換的人,可能沒聽過這個協定。
其實這協定已經算是很古老但是到目前為止仍是自動控制領域的重要協定
主要用在雙方裝置的傳輸上用
在我進資策會接PROJECT做事情前,我也沒聽過也沒用過這套標準
努力K了一小段時間,並且花了2周左右的時間實作大部分的內容,
但讓我比較困惑的是最後CRC碼產生的部分。
大部分的CODE都是ASCII的SAMPLE…,甚至有些沒有特別註明到底是RTU還是ASCII,因此在這裡分享一下從ASCII轉變成RTU格式的轉換程式碼。
我發現我廢話再講下去就沒重點了
首先以下範例Modbus RTU Code採用
01 03 00 00 00 01 84 0A
為master送去slave 01做讀取動作,後面的address跟長度是專案定義的,我只是拿來作範例,為了好閱讀,以1Byte為單位進行分割(實際傳輸是靠攏出去的)
這一串算出來的CRC會是84 0A
但ModBus RTU的CRC計算規則是
1. 把整串ModBus(不含CRC),做CRC運算,
2. 得到的CRC,高低位元必須交換,才能跟原先不含CRC的ModBus合併傳送出去。
3. ModBus RTU是採用CRC16(IBM)的演算規則。
重點來了,網路有很多的範例…
寫法非常簡單,也能夠算出ASIIC版本的ModBus CRC…
例如下面這個:
http://www.kce.2u.com.tw/download/MODBUS%20CRC16.pdf
*/ // ============================ Unsigned int crc_chk(unsigned char* data, unsigned char length) { int j; unsigned int reg_crc=0xFFFF; while(length--) { reg_crc ^= *data++; for(j=0;j<8;j++) { if(reg_crc & 0x01) /* LSB(b0)=1 */ reg_crc=(reg_crc>>1) ^ 0xA001; else reg_crc=reg_crc >>1; } } return reg_crc; }
先姑且不論他的unsigned打成大寫U
如果直接拿它來當成RTU用
其中有個問題,也就是unsigned char直接拿去做邏輯運算時,其實Compiler會直接把char裏頭的字元不分青紅皂白的轉成ASCII碼
例如如果我上頭的sub function傳值塞了一個01 03 00 00 00 01 84 0A
此時,依照上方的程式流程,會逐一掃瞄該char指標的內容
第一個是0,所以在與reg_crc做xor運算時,其實是
0x0030 xor 0xffff
這對於RTU格式明顯是錯誤的,正確應該是0x0000 xor 0xffff才對
依類推,所以算出來的值若直接拿給RTU格式絕對不正確。
除了這個問題之外,要改良成RTU格式的話,
此ASCII格式程式碼還有個問題
那就是他輸出的CRC必須要高低位元交換
不過這個範例還是道出CRC運算的精隨
所以其實稍微修改一下,就能符合RTU的傳輸格式,不囉嗦
下午花了大約30分鐘找到幾個問題點後修正,
以下為ModBus RTU產生CRC的CODE:
//產生CRC unsigned int crcGenrator(char* data) { int length=strlen(data)/2; //計算長度 unsigned int reg_crc=0xFFFF; //CRC初始化 while(length--) { //把MODBUS DATA以兩個字元共8BITS為一組做切割 char hic = *data; //取得第一個字元(放在高位元) unsigned int hi; sscanf(&hic,"%X",&hi); //16進制字元轉16進制整數 *data++;//移動到下一個字元 char loc=*data;//取得第二個字元(放在低位元) unsigned int lo; sscanf(&loc,"%X",&lo); *data++; hi<<=4;//高位元向左移動 unsigned int twoByte=hi|lo;//高低位元串連 //以下做CRC運算,不解釋 reg_crc ^= twoByte ; int j; for(j=0;j<8;j++) { if(reg_crc & 0x01) reg_crc=(reg_crc>>1) ^ 0xA001; else reg_crc=reg_crc >>1; } } //modbus CRC高低位元交換 unsigned int outCRC=((reg_crc<<8)&0xFF00)|((reg_crc>>8)&0x00FF); return outCRC; } //驗證CRC int crcVertify(char* data) { char corigCRC[5]={}; unsigned int origCRC=0; char origModBusPrefix[200]={}; //先將原本資料尾巴的2BYTE CRC剪下來 int len = strlen(data); int trimLenth=strlen(data)-4;//ModBus前面有6碼+資料+結束CRC檢查4碼=>資料總長 printf("crc trimLenth:%d\n", trimLenth); strncpy(corigCRC,data+trimLenth,4); //再把CRC以前的資料擷取出來 strncpy(origModBusPrefix,data,trimLenth); //把字串CRC轉換成16進制整數 sscanf(corigCRC,"%04X",&origCRC); //把擷取出來的資料再送去自己產生CRC一次 unsigned int reCRC=crcGenrator(origModBusPrefix); //把原先資料的CRC跟自己算完的CRC做比對,若有錯誤則回傳-1,正確則回傳1 if(reCRC==origCRC) {return 1;} else {return -1;} }
其中分為三個FUNCTION,第一個就是單純產生CRC,用法也很簡單,直接塞一個char指標進去就可以了
傳值也簡化,比原始的版本還要精簡些,其詳細運作流程都在code的註解內。
第二個為驗證該資料的CRC是否正確,我這裡是把收到的資料後兩碼剪下,
然後再把前面的資料拿去自己在算一遍,最後比對自己算的跟原始傳來的
若為對的,就傳回1,否則傳回-1。
以下為使用範例(隨手打的…在家懶得開VS,明天去公司在檢查這段XD)
int void main(){ char *mbsend = "010300000001"; unsigned int crc=crcGenrator(mbsend); char *mbrecv = "0103000000018401"; //故意假裝收到錯誤的CRC int crcStatus=crcVertify(mbrecv); printf("You send ModBus's CRC is %04X, Recive Modbus status is %d", crc, crcStatus); }
大概是這樣~~
第一次發點部落文章,請各位前輩多多指教
我相信上面的程式碼還能夠做的更好!
若有更好的點子或者建議,煩請不吝指導 🙂
另外網路有找到兩個線上試算器,基本上應該是沒甚麼問題,算出來的值都是正確的
南樺電機提供的:
還有另外一個外國網站提供的,他也有提供完整的source code,包括轉換其他各種不同長度的CRC,不過要注意她的MODBUS部分,並沒有高低位元交換!
http://www.lammertbies.nl/comm/info/crc-calculation.html
thx for:
開頭modbus格式圖案- http://www.cni.co.th/download/cni_co_th/kb_dc_modbus%20ascii%20and%20modbus%20rtu.pdf
請問要如何把它改到 arduino 計算 CRC呢
你可以把它做成C Lib,透過Call libary的方式從arduino code內呼叫喔~