前言:
最近因為工作的關係,需要熟悉 RS485 的通訊協定。這個協定基本上是硬體層 RS232 的差動電壓版本,叫做 RS422,再加上一個傳輸層的協定叫做 Modbus 的 protocol,並不難。
但假如要實作的話,就需要搞懂所謂 CRC (Cyclic redundancy check) 循環冗餘校驗的計算演算法,所有資料在送出前需要透過這個CRC計算後附加上去。主從的雙方收到資料後都依賴這個 CRC 的計算來彼此查驗是否資料的傳輸的過程中發生任何資料丟包的情況,最後來進行後續的處置。
特別是像 RS485 的這種號稱可以傳送長達一公里距離的 bus, 這種訊號因為長距離的傳輸而扭曲丟包的情況是非常容易發生的,所以採用 CRC 的技術來進行檢查變得非常的必要。
上網找了一些資料,因為工作上要使用,當然最後這個 CRC 計算用其他的電腦語言實作出來了。
但是這裡是我的 FORTH 語言推廣的小小部落格,所以順便也寫了一個 FORTH 語言的版本囉,所以不管怎樣,要來簡單的分享 FORTH 語言 CRC16 計算的程式碼囉。
關於CRC的理論:
理論方面,維基百科其實做了蠻不錯的整理,按我進連結
理論講得相當複雜,但筆者對 CRC計算有個很簡單的看法,就是把它當作是一群資料的雜湊函數,CRC碼代表這群資料的雜湊結果。當算出來的 CRC 雜湊碼不太一樣時,自然代表這群資料已經不太一樣了,有資料已經掉或失真囉。
Modbus CRC16 計算程序:
CRC 的計算有非常多種,有 CRC8, CRC16, CRC32 的各種版本。同樣的,即使是 CRC16 ,也會因為採用的 generation function key 值的不同而算出來的結果不同。
先來條列一下 Modbus CRC16 計算的演算法吧:
(1) 將 crc 結果變數設為 0xFFFF 初始值 (2bytes)
(2) 取出預計計算的 1 byte 的資料 跟 crc 結果變數的低位元 byte 進行 xor 運算
(3) DO-LOOP 迴圈,對 crc 結果變數的LSB 做檢查,總共8次
- 當最右的 LSB 為 1 時,將crc 結果變數右移一位,並和 0xA001 做 xor 運算
- 當最右的 LSB 為 0 時,將crc 結果變數右移一位
(4) 完成此 byte 的 crc 運算,回到 (2) 取出下個資料 byte 繼續進行 crc 運算,直到完成所有資料 byte
(5) 對調crc 結果變數的高低 byte,此即為 CRC16 檢查碼!
FORTH 程式解說:
這裡因為要用到大量的位元運算,所以對 FORTH 的資料結構要搞清楚,謹記在心的
8位元 CPU 的FORTH系統
FORTH 裡一個字元為 1 byte ,一個 word 的單整數是 2 bytes ,兩個 words 的雙整數是 4 bytes
16位元 CPU 的FORTH系統
FORTH 裡一個字元為 1 byte ,一個 word 的單整數是 2 bytes ,兩個 words 的雙整數是 4 bytes
32位元 CPU 的FORTH系統
FORTH 裡一個字元為 1 byte ,一個 word 的單整數是 4 bytes ,兩個 words 的雙整數是 8 bytes
現代的不管是 Mac 或是 PC 都是 32位元以上的系統,自然,所謂一個單整數是指的是 4 bytes 的長度。所以 gFORTH 在這些電腦下的參數堆疊,每個單整數都是 4 bytes 的長度。但是 CRC16 只用到 2bytes 的長度,所以任何位元運算前,都得使用 0xFFFF 的遮罩,以 AND 運算進行截取 32 bits 的 Low Bytes,否則 High Bytes 結果跑進來的話,就會導致後續計算的錯誤囉。
當然,假如你是8位元的系統,剛好參數堆疊的單整數長度是 16 bits ,就不需要這麼麻煩囉!
ByteSwap
這個指令顧名思義,將 High Low byte 交換 (Little Endian) 成 Modbus RTU 所規定的順序。
0xFF AND 取出 low byte 左移8位到 high byte
0xFF00 AND 取出 high byte 右移8位到 low byte,再利用 OR 合併後就完成交換
: ByteSwap ( n -- n')
dup 00FF and 8 LSHIFT
swap FF00 and 8 RSHIFT
or
;
CRC16
這是主要計算 CRC16 的指令,需要透過堆疊告訴它兩個參數:資料的起始位址,跟長度
: CRC16 ( addr n -- crc )
FFFF ( addr n crc )
rot rot over + swap ( crc addr+n addr)
do ( crc)
FFFF and ( if you are using 8 bits system, remove this line.)
i c@ xor
8 0
do dup 1 and
if 1 RSHIFT
A001 XOR
else 1 RSHIFT then
loop
loop
ByteSwap
;
首先將 0xFFFF 的 crc16 起始值堆上堆疊
FFFF ( addr n crc )
再來根據資料的起始位址,跟長度計算設定一下主要的 DO-LOOP 迴圈,要以1 byte 的方式逐步掃描取出所有資料進行 CRC 累積計算
rot rot over + swap ( crc addr+n addr)
do ( crc)
...
loop
然後 Byte 計算前,如前所述,如果是 32 bits 的 FORTH 系統,需用 0xFFFF AND 一下,把無用的 high bytes 截掉
FFFF and ( if you are using 8 bits system, remove this line.)
將位址的 1 byte 資料,用 c@ 取出,跟堆疊上的 crc16 進行 XOR 運算
i c@ xor
接下來用個 DO-LOOP 掃過 crc16 Low Byte 的 8 bits
0x01 AND 取出最右 bit
假如最右 bit 為 1 , crc16 右移一位 ,然後跟 0xA001 這個 generation key XOR 後放回
假如最右 bit 為 0 , crc16 右移一位
8 0
do dup 1 and
if 1 RSHIFT
A001 XOR
else 1 RSHIFT then
loop
這樣就完成了單一 byte 的 CRC16 計算
逐次完成所有計算後, 將堆疊上的 crc16 透過 ByteSwap 指令交換一下高低位,這樣就完成所有 CRC16 的計算
結果驗證:
用兩筆資料來進行驗證
第一筆
0x2D 0x00 0x03 0x00 0x07
這樣的資料, CRC16 算出來應該是 0x39C4
第二筆
0x01 0x03 0x00 0x00 0x00 0x01
這樣的資料, CRC16 算出來應該是 0x840A
經過執行後結果如下,完全正確無誤
原始程式碼列表
\
\ Modbus CRC16 calculation
\ 2020.9.19 Frank Lin
\
hex
: ByteSwap ( n -- n')
dup 00FF and 8 LSHIFT
swap FF00 and 8 RSHIFT
or
;
: CRC16 ( addr n -- crc )
FFFF ( addr n crc )
rot rot over + swap ( crc addr+n addr)
do ( crc)
FFFF and ( if you are using 8 bits system, remove this line.)
i c@ xor
8 0
do dup 1 and
if 1 RSHIFT
A001 XOR
else 1 RSHIFT then
loop
loop
ByteSwap
;
\
\ Verification
\
\
\ 0x2D 0x00 0x03 0x00 0x07
\ The CRC should be 0x39C4
\
create test1 2D c, 00 c, 03 c, 00 c, 07 c,
test1 5 CRC16
cr cr
.( == Verify #1 ==) cr
test1 5 dump .( CRC should be: 39C4) cr
.( The calculated value is ) . cr
cr
\
\ 0x01 0x03 0x00 0x00 0x00 0x01
\ The CRC should be 0x840A
\
create test2 01 c, 03 c, 00 c, 00 c, 00 c, 01 c,
test2 6 CRC16
cr cr
.( == Verify #2 ==) cr
test2 6 dump .( CRC should be: 840A) cr
.( The calculated value is ) . cr
cr