CRC-16の値が合わない — 5つのパラメータで変種を特定する
機器の仕様書に「CRC-16 を付ける」とだけ書いてあり、手元で計算した値が受信データと合わない。CRC ではよくある詰まり方である。原因はたいてい計算の誤りではなく、「CRC-16」という名前が1つのアルゴリズムを指していないことにある。
この記事では、ハシトシステムのCRC計算ツールが実装している「パラメータで CRC を定義する」書き方を読みながら、
- CRC の変種を決める5つのパラメータと、それぞれが値をどう変えるか
- 入力
123456789に対する検査値(check)で、自分の実装が正しいかを確かめる方法 - 受信したフレームから、相手がどの変種・どのバイト順を使っているかを当てる方法
を、手元で動かしたコードで確かめる。確認した環境は Windows 11 / Node.js v24.11.1 / Python 3.11.9 である。
「CRC-16」は30種類ある
CRC のパラメータを集めた資料として広く参照されているのが、Greg Cook 氏の Catalogue of parametrised CRC algorithms(CRC RevEng のカタログ)である。16ビットの CRC だけで、CRC-16 のページには30種類が載っている。
カタログは各アルゴリズムを次の形の1行で定義している(CRC-16/MODBUS の例)。
1 | |
| パラメータ | 意味 |
|---|---|
width |
CRC のビット数(8 / 16 / 32 など) |
poly |
生成多項式(最上位ビットを省いた16進表記) |
init |
計算を始める前のレジスタの値 |
refin |
入力の各バイトをビット反転して(LSB から)処理するか |
refout |
最後にレジスタ全体をビット反転するか |
xorout |
最後にレジスタと XOR する値 |
check |
ASCII 文字列 123456789(9バイト)を入れたときの結果 |
width を除く5つ(poly / init / refin / refout / xorout)のどれか1つでも違えば、同じデータから別の値が出る。カタログから、よく出会う CRC-16 を並べるとこうなる。
| カタログ名 | poly | init | refin/refout | xorout | check | カタログ上の別名(抜粋) |
|---|---|---|---|---|---|---|
| CRC-16/ARC | 0x8005 | 0x0000 | true | 0x0000 | 0xbb3d | CRC-16、CRC-IBM |
| CRC-16/MODBUS | 0x8005 | 0xffff | true | 0x0000 | 0x4b37 | MODBUS |
| CRC-16/IBM-3740 | 0x1021 | 0xffff | false | 0x0000 | 0x29b1 | CRC-16/CCITT-FALSE |
| CRC-16/XMODEM | 0x1021 | 0x0000 | false | 0x0000 | 0x31c3 | XMODEM、ZMODEM |
| CRC-16/KERMIT | 0x1021 | 0x0000 | true | 0x0000 | 0x2189 | CRC-16/CCITT、CRC-CCITT |
ARC と MODBUS は多項式もビット反転も同じで、初期値だけが違う。IBM-3740 と XMODEM も初期値だけが違う。そして名前の混乱がいちばん大きいのが「CCITT」である。カタログでは CRC-CCITT / CRC-16/CCITT は KERMIT の別名で、init=0xffff の非反転版は CRC-16/CCITT-FALSE(正式名 IBM-3740)になっている。仕様書の「CRC-CCITT」がどちらを指しているかは、名前からは決められない。
実装:5つのパラメータをそのまま使う
CRC計算ツールは、9種類の CRC を1つの関数で計算している。アルゴリズムの違いはすべてパラメータ表に寄せてあり、たとえば MODBUS の行は次のとおりである(ツールのソースから引用)。
1 | |
計算本体は、表を使わずに1ビットずつ割り算する素直な形である。
1 | |
5つのパラメータは、この関数の次の場所に効いている。
- init:
reg=a.initでレジスタの初期値になる - refin:各バイトを
reflect(bytes[k],8)で反転してから取り込む - poly:最上位ビットが1のとき、左シフトしたレジスタに XOR する
- refout:最後にレジスタ全体を
reflect(reg,a.w)で反転する - xorout:返す直前にレジスタと XOR する
JavaScript のビット演算は32ビット符号付き整数で行われるので、>>>0 を挟んで符号なしに戻している。CRC-32 のように最上位ビットが立つ値を扱うときに、これがないと負の数になる。
「右シフトで 0xA001」と書く仕様との関係
Modbus の仕様書(MODBUS over Serial Line Specification and Implementation Guide V1.02、6.2.2 CRC Generation)は、計算手順を「レジスタに FFFF hex を入れる」「LSB 側へ右シフトし、取り出した LSB が1なら 0xA001 と XOR する」と書いている。上のパラメータ表の poly=0x8005 とは数字が違うが、別物ではない。0x8005 を16ビットでビット反転すると 0xA001 になる(下の「実際に試す」で確かめる)。refin=true の CRC は、ビットを反転して左シフトする代わりに、反転した多項式で右シフトしても同じ結果になる。CRC-32 で 0xEDB88320 という定数をよく見るのも同じ理由で、0x04C11DB7 を32ビットで反転した値である。
検査値 123456789 で実装を確かめる
カタログの check は、ASCII の 123456789 を入れたときの CRC である。自分で CRC を実装したり、ライブラリの設定を変えたりしたときは、まずこの1つの入力で値を比べれば、パラメータの取り違えはほぼ見つかる。
ツールもページを開くたびに9件の検査値を計算し、カタログの値と一致するかを「自己検査」として表示している。
標準ライブラリの関数も、この方法で正体が分かる。
| 関数 | 123456789 の結果 |
対応する変種 |
|---|---|---|
Node.js zlib.crc32() |
0xcbf43926 | CRC-32/ISO-HDLC |
Python binascii.crc32() / zlib.crc32() |
0xcbf43926 | CRC-32/ISO-HDLC |
Python binascii.crc_hqx(data, 0) |
0x31c3 | CRC-16/XMODEM |
Python binascii.crc_hqx(data, 0xffff) |
0x29b1 | CRC-16/IBM-3740(CCITT-FALSE) |
Python のドキュメントは crc_hqx について、多項式 0x1021 を使い、引数の値を初期値にすると説明している。初期値を呼び出し側が渡すので、0 を渡すか 0xFFFF を渡すかで XMODEM にも CCITT-FALSE にもなる。KERMIT(反転あり)は crc_hqx では作れない。
Node.js の zlib.crc32() は Node.js のドキュメントによると v22.2.0 と v20.15.0 で追加された。それより前のバージョンでは自前で実装するか、パッケージを使う必要がある。
受信フレームから変種とバイト順を当てる
CRC の値が合わないとき、もう1つの原因がバイト順である。Modbus の仕様書(2.5.1.2 CRC Checking)は、CRC を付けるときに下位バイトを先に、上位バイトを後に送ると定めている。CRC の値が 0xCDC5 なら、フレームの末尾は C5 CD の順に並ぶ。末尾2バイトを「普通に」上位バイトから読むと 0xC5CD になり、計算値と一致しない。
そこで、受信したフレームが1本あれば、候補の変種をすべて計算し、末尾2バイトを両方の順で読んで比べればよい。Modbus RTU の読み出し要求 01 03 00 00 00 0A C5 CD(スレーブ1・機能コード3・アドレス0から10レジスタ)で試すと、MODBUS の行だけが「下位バイトが先」で一致した(コードは次の節)。
1 | |
候補が1つに絞れなければ、別のフレームでもう一度試す。それでも当たらなければ、カタログの30種類から候補を足すか、CRC の対象範囲(ヘッダを含むか、長さフィールドを含むか)を疑う。
対象範囲の例として、PNG がある。PNG の仕様(W3C、第3版)の 5.5 CRC algorithm によると、各チャンクの CRC は ISO 3309 / ITU-T V.42 の CRC-32 で、チャンク型とデータには掛かるが、長さフィールドには掛からない。長さから計算に含めると、変種が正しくても値は合わない。
実際に試す
前提:Node.js v22.2.0 以降(または v20.15.0 以降)。筆者は v24.11.1 で確認した。外部パッケージは使わない。Python の確認は 3.11.9 で行った。
1. パラメータで動く CRC 関数
ツールと同じ考え方で、カタログの1行をそのままオブジェクトにして渡す。ビット演算の符号の扱いを避けるため、シフトを掛け算と剰余で書いている。
1 | |
2. 検査値と zlib.crc32 で確かめる
1 | |
1 | |
期待される出力:
1 | |
10件すべてがカタログの check と一致し、Node.js の zlib.crc32() は CRC-32/ISO-HDLC と同じ値になる。反転した多項式も1行で確かめられる。
1 | |
1 | |
3. 受信フレームから変種とバイト順を当てる
1 | |
1 | |
出力は前の節に載せたとおりで、MODBUS の行だけが「下位バイトが先」で一致する。手元の機器が送ってきたフレームを16進で貼れば、同じように候補を絞れる。
4. PNG のチャンク CRC を検算する
長さフィールドを除いた「チャンク型+データ」に zlib.crc32() を掛け、格納されている値と比べる。
1 | |
1 | |
筆者が手元の PNG(OGP 画像)で実行した出力:
1 | |
チャンクの数や IDAT の長さはファイルによって変わるが、データが空の IEND の CRC はどの PNG でも ae426082 になる。zlib.crc32('IEND') の値と同じである。
5. Python で確かめる
1 | |
1 | |
ブラウザだけで確かめたい場合は、CRC計算ツールで入力の種類を「16進バイト列」にして 01 03 00 00 00 0A を入れると、9種類の CRC が16進と10進で並び、各行にパラメータも表示される。CRC-16/MODBUS の行が 0xCDC5 になり、フレーム末尾の C5 CD と下位バイト・上位バイトが入れ替わった関係になっているのが見える。計算はブラウザ内で完結し、入力は送信されない。
CRC と同じく「末尾の検査用の値を、重みや規則の取り違えで合わせ損ねる」話は、バーコードのチェックディジットでも起きる。EAN-13 と UPC-A で重み 1/3 が入れ替わる理由で扱っている。なお CRC は偶発的な誤りを見つけるための符号で、改ざんの検出には使えない。Node.js のドキュメントも、zlib.crc32() は暗号学的な認証に向かないと注記している。
まとめ
- 「CRC-16」は1つのアルゴリズムではない。カタログには16ビットだけで30種類あり、poly / init / refin / refout / xorout の5つで区別される
- 名前は当てにならない。
CRC-CCITTはカタログでは KERMIT(反転あり・初期値0)の別名で、init=0xffffの非反転版は CCITT-FALSE(IBM-3740)である - 実装を確かめるときは、
123456789の検査値をカタログと比べる。Node.js のzlib.crc32()と Python のbinascii.crc32()は CRC-32/ISO-HDLC、crc_hqxは初期値しだいで XMODEM か CCITT-FALSE になる - 値が合わないときは、変種のほかにバイト順(Modbus は下位バイトが先)と計算の対象範囲(PNG は長さフィールドを含めない)を疑う
参照した一次情報
- Catalogue of parametrised CRC algorithms(CRC RevEng)(16ビット・17ビット以上の各パラメータと別名)
- MODBUS over Serial Line Specification and Implementation Guide V1.02(2.5.1.2 CRC Checking、6.2.2 CRC Generation)
- Portable Network Graphics (PNG) Specification (Third Edition)(5.5 CRC algorithm)
- Node.js zlib ドキュメント(
zlib.crc32(data[, value])) - Python binascii ドキュメント(
crc_hqx、crc32)