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=16 poly=0x8005 init=0xffff refin=true refout=true xorout=0x0000 check=0x4b37 residue=0x0000 name="CRC-16/MODBUS"
パラメータ 意味
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
{id:'crc16modbus',name:'CRC-16/MODBUS',alias:'Modbus RTU',w:16,poly:0x8005,init:0xFFFF,refin:true,refout:true,xorout:0x0000,check:'4B37'},

計算本体は、表を使わずに1ビットずつ割り算する素直な形である。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function crc(bytes,a){
var top=(a.w===32)?0x80000000:(1<<(a.w-1));
var mask=(a.w===32)?0xFFFFFFFF:((1<<a.w)-1);
var reg=a.init>>>0;
for(var k=0;k<bytes.length;k++){
var byte=a.refin?reflect(bytes[k],8):bytes[k];
reg=((reg ^ ((byte & 0xFF) << (a.w-8)))>>>0) & mask;
for(var i=0;i<8;i++){
if((reg & top)!==0){reg=(((reg<<1)>>>0) ^ a.poly)>>>0;}else{reg=(reg<<1)>>>0;}
reg=reg & mask;
}
}
if(a.refout){reg=reflect(reg,a.w);}
return (((reg ^ a.xorout)>>>0) & mask)>>>0;
}

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
2
3
4
5
CRC-16/ARC       0xd6c5
CRC-16/MODBUS 0xcdc5 一致(下位バイトが先)
CRC-16/IBM-3740 0x0428
CRC-16/XMODEM 0x0a38
CRC-16/KERMIT 0xb6bd

候補が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行をそのままオブジェクトにして渡す。ビット演算の符号の扱いを避けるため、シフトを掛け算と剰余で書いている。

crc.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// パラメータ5つ(+ 幅)で決まる CRC。reveng カタログの書き方に合わせる
export const CATALOG = [
{ name: 'CRC-32/ISO-HDLC', width: 32, poly: 0x04c11db7, init: 0xffffffff, refin: true, refout: true, xorout: 0xffffffff, check: 0xcbf43926 },
{ name: 'CRC-32/ISCSI', width: 32, poly: 0x1edc6f41, init: 0xffffffff, refin: true, refout: true, xorout: 0xffffffff, check: 0xe3069283 },
{ name: 'CRC-32/BZIP2', width: 32, poly: 0x04c11db7, init: 0xffffffff, refin: false, refout: false, xorout: 0xffffffff, check: 0xfc891918 },
{ name: 'CRC-16/ARC', width: 16, poly: 0x8005, init: 0x0000, refin: true, refout: true, xorout: 0x0000, check: 0xbb3d },
{ name: 'CRC-16/MODBUS', width: 16, poly: 0x8005, init: 0xffff, refin: true, refout: true, xorout: 0x0000, check: 0x4b37 },
{ name: 'CRC-16/IBM-3740', width: 16, poly: 0x1021, init: 0xffff, refin: false, refout: false, xorout: 0x0000, check: 0x29b1 },
{ name: 'CRC-16/XMODEM', width: 16, poly: 0x1021, init: 0x0000, refin: false, refout: false, xorout: 0x0000, check: 0x31c3 },
{ name: 'CRC-16/KERMIT', width: 16, poly: 0x1021, init: 0x0000, refin: true, refout: true, xorout: 0x0000, check: 0x2189 },
{ name: 'CRC-8/SMBUS', width: 8, poly: 0x07, init: 0x00, refin: false, refout: false, xorout: 0x00, check: 0xf4 },
{ name: 'CRC-8/MAXIM-DOW', width: 8, poly: 0x31, init: 0x00, refin: true, refout: true, xorout: 0x00, check: 0xa1 },
];

// 下位 w ビットの並びを逆にする
export function reflect(v, w) {
let r = 0;
for (let i = 0; i < w; i++) if ((v >>> i) & 1) r |= 1 << (w - 1 - i);
return r >>> 0;
}

// 1ビットずつ割る素直な実装(表は使わない)
export function crc(bytes, p) {
const top = 2 ** (p.width - 1);
const mask = 2 ** p.width - 1;
let reg = p.init;
for (const b of bytes) {
const x = p.refin ? reflect(b, 8) : b;
reg = (reg ^ (x * 2 ** (p.width - 8))) >>> 0;
for (let i = 0; i < 8; i++) {
reg = reg >= top ? ((reg - top) * 2) ^ p.poly : reg * 2;
reg = (reg >>> 0) % (mask + 1);
}
}
if (p.refout) reg = reflect(reg, p.width);
return ((reg ^ p.xorout) >>> 0) % (mask + 1);
}

export const hex = (v, w) => '0x' + v.toString(16).padStart(w / 4, '0');

2. 検査値と zlib.crc32 で確かめる

check.mjs
1
2
3
4
5
6
7
8
9
import zlib from 'node:zlib';
import { CATALOG, crc, hex } from './crc.mjs';

const data = new TextEncoder().encode('123456789');
for (const p of CATALOG) {
const got = crc(data, p);
console.log(p.name.padEnd(16), hex(got, p.width), got === p.check ? 'OK' : 'NG (catalog ' + hex(p.check, p.width) + ')');
}
console.log('zlib.crc32 ', hex(zlib.crc32('123456789'), 32));
1
node check.mjs

期待される出力:

1
2
3
4
5
6
7
8
9
10
11
CRC-32/ISO-HDLC  0xcbf43926 OK
CRC-32/ISCSI 0xe3069283 OK
CRC-32/BZIP2 0xfc891918 OK
CRC-16/ARC 0xbb3d OK
CRC-16/MODBUS 0x4b37 OK
CRC-16/IBM-3740 0x29b1 OK
CRC-16/XMODEM 0x31c3 OK
CRC-16/KERMIT 0x2189 OK
CRC-8/SMBUS 0xf4 OK
CRC-8/MAXIM-DOW 0xa1 OK
zlib.crc32 0xcbf43926

10件すべてがカタログの check と一致し、Node.js の zlib.crc32() は CRC-32/ISO-HDLC と同じ値になる。反転した多項式も1行で確かめられる。

1
node -e "import('./crc.mjs').then(m => console.log(m.hex(m.reflect(0x8005, 16), 16), m.hex(m.reflect(0x04c11db7, 32), 32)))"
1
0xa001 0xedb88320

3. 受信フレームから変種とバイト順を当てる

identify.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
import { CATALOG, crc, hex } from './crc.mjs';

// 末尾2バイトが CRC-16 のフレームから、どの変種・どのバイト順かを当てる
const frame = Buffer.from(process.argv[2].replace(/\s+/g, ''), 'hex');
const body = frame.subarray(0, -2);
const tail = frame.subarray(-2);
const be = tail.readUInt16BE(0), le = tail.readUInt16LE(0);

for (const p of CATALOG.filter((p) => p.width === 16)) {
const v = crc(body, p);
const hit = v === be ? '一致(上位バイトが先)' : v === le ? '一致(下位バイトが先)' : '';
console.log(p.name.padEnd(16), hex(v, 16), hit);
}
1
node identify.mjs "01 03 00 00 00 0A C5 CD"

出力は前の節に載せたとおりで、MODBUS の行だけが「下位バイトが先」で一致する。手元の機器が送ってきたフレームを16進で貼れば、同じように候補を絞れる。

4. PNG のチャンク CRC を検算する

長さフィールドを除いた「チャンク型+データ」に zlib.crc32() を掛け、格納されている値と比べる。

png-crc.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { readFileSync } from 'node:fs';
import zlib from 'node:zlib';

// PNG のチャンクごとに、格納されている CRC と再計算した CRC を比べる
const buf = readFileSync(process.argv[2]);
let off = 8; // 8バイトのシグネチャを飛ばす
while (off < buf.length) {
const len = buf.readUInt32BE(off);
const typeAndData = buf.subarray(off + 4, off + 8 + len); // 長さフィールドは含めない
const stored = buf.readUInt32BE(off + 8 + len);
const calc = zlib.crc32(typeAndData);
console.log(typeAndData.subarray(0, 4).toString('latin1'), String(len).padStart(7),
stored.toString(16).padStart(8, '0'), calc === stored ? 'OK' : 'NG ' + calc.toString(16));
off += 12 + len;
}
1
node png-crc.mjs some.png

筆者が手元の PNG(OGP 画像)で実行した出力:

1
2
3
IHDR      13 c022ec0b OK
IDAT 34866 c70da0d9 OK
IEND 0 ae426082 OK

チャンクの数や IDAT の長さはファイルによって変わるが、データが空の IEND の CRC はどの PNG でも ae426082 になる。zlib.crc32('IEND') の値と同じである。

5. Python で確かめる

1
python -c "import binascii,zlib; d=b'123456789'; print(hex(binascii.crc_hqx(d,0)), hex(binascii.crc_hqx(d,0xffff)), hex(binascii.crc32(d)), hex(zlib.crc32(d)))"
1
0x31c3 0x29b1 0xcbf43926 0xcbf43926

ブラウザだけで確かめたい場合は、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 は長さフィールドを含めない)を疑う

参照した一次情報