この記事では、FSoE(Functional Safety over EtherCAT)CRC-16 チェックサムのすぐに使える2つの C++ 実装を提供します。どちらも同一の結果を生成し、元の FSoE 参照コードで検証済みです。
アルゴリズムの動作の背景については以下を参照:
- FSoE CRC: どの多項式を使うか? — 多項式 $P(x) = x^{16} + x^{13} + x^{12} + x^{11} + x^{8} + x^{7} + x^{5} + x^{4} + x^{2} + x + 1$ (0x39B7)
- FSoE CRC テーブルはどう構築されるか? — 2つのルックアップテーブルが多項式からどう導出されるか
- FSoE: CRC 継承はどう機能するか? — バイトごとの更新ステップ、フレーム間継承、フレーム内
crc_common機構の詳細な説明
アルゴリズム概要
FSoE CRC は各バイトを以下で処理します:
$$ \text{crc}_{\text{new}} = (\text{crc}_{\text{lo}} \ll 8) \;\oplus\; T_0[\text{crc}_{\text{hi}}] \;\oplus\; T_3[\text{input}] $$ここで $T_0[i] = (i \cdot x^{16}) \bmod P$、$T_3[i] = (i \cdot x^{40}) \bmod P$。CRC は開始時に 0 にリセットされ、その後バイトがこの順序で処理されます:
oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi, SeqNo-Lo, SeqNo-Hi, Command, Data[0], [Data[1], ...]10バイトを超える PDU では複数の CRC が計算されます——各セグメント CRC は共有 crc_common ベース(最初の7ヘッダバイト処理後の状態)から再開し、インデックスバイトペアとデータバイト2つを追加します。
2つの実装バリアント
バリアント A:テーブルベース(FsoeCrcTable)
2つの事前計算された256エントリルックアップテーブル(CRC16_TABLE と CRC16_TABLE2)を使用。より高速(バイトごとに2回の配列参照)ですが、テーブルを生成してリンクする必要があります。生成方法は FSoE CRC テーブルはどう構築されるか? を参照。
バリアント B:オンザフライ(FsoeCrcOnTheFly)
ビットごとの多項式長除算で各テーブルエントリをオンデマンド計算。ルックアップテーブル不要——多項式定数 0x39B7 だけ。低速(バイトごとに8または32回のシフト XOR サイクル)ですが自己完結しており、メモリ制約のある環境や 1KB のテーブルデータを避けたい場合に適しています。
両バリアントは同じ API を共有:
// 低レベル
void reset(); // CRC = 0
void update(uint8_t byte); // FSoE バイト更新ステップ
uint16_t value() const; // 現在の CRC を取得
// 高レベル(FSoE 固有)
uint16_t computeCrc0(...); // CRC0: oldCRC + ConnID + SeqNo + Cmd + Data[0..1]
uint16_t computeCrcCommon(...); // 共有ベース: oldCRC + ConnID + SeqNo + Cmd
uint16_t computeCrcI(...); // CRCi: crc_common + Index + Data[2i,2i+1]
std::vector<uint16_t> computeAllCrcs(...); // PDU の全 CRC
使用例
#include "FsoeCrcTable.hpp" // または "FsoeCrcOnTheFly.hpp"
#include <cstdio>
int main() {
FsoeCrcTable crc; // または FsoeCrcOnTheFly
uint8_t data[] = {0xAB, 0xCD};
uint16_t startCrc = 0x0000; // 最初のフレーム: startCrc = 0
uint16_t connId = 0x1234;
uint16_t seqNo = 0x0001;
uint8_t command = 0x01;
int pduSize = 7; // データ2バイト
// この PDU の全 CRC を計算
auto crcs = crc.computeAllCrcs(startCrc, connId, seqNo, command,
data, 2, pduSize);
printf("CRC0 = 0x%04X\n", crcs[0]);
// 次のフレームでは crcs[0] を startCrc として使う(CRC 継承)
return 0;
}CRC0 = 0xDD27バリアント A:テーブルベース実装
ライセンス: CC0 1.0 Universal — パブリックドメイン献呈
このバリアントは Tables.c のルックアップテーブル(FSoE CRC テーブルはどう構築されるか? 参照)と対応する Tables.h ヘッダが必要です:
/*
* Tables.h
* ========
*
* Tables.c で定義される2つの FSoE CRC-16 ルックアップテーブルの
* ヘッダ(GenTables.c で自動生成)。
*
* SPDX-License-Identifier: CC0-1.0
*/
#ifndef TABLES_H
#define TABLES_H
#include <stdint.h>
extern const uint16_t CRC16_TABLE[256];
extern const uint16_t CRC16_TABLE2[256];
#endif /* TABLES_H *//*
* FsoeCrcTable.hpp
* =================
*
* テーブルベース FSoE CRC-16 計算。
*
* Tables.c の事前計算済みルックアップテーブルを使用:
* CRC16_TABLE (k=0): T0[i] = (i * x^16) mod P
* CRC16_TABLE2 (k=3): T3[i] = (i * x^40) mod P
*
* バイトごとの更新ステップ:
* new_crc = (crc_lo << 8) ^ T0[crc_hi] ^ T3[input]
*
* ビルド:FsoeCrcTable.cpp を Tables.c とリンク
*
* SPDX-License-Identifier: CC0-1.0
*/
#ifndef FSOE_CRC_TABLE_HPP
#define FSOE_CRC_TABLE_HPP
#include <cstdint>
#include <vector>
class FsoeCrcTable {
public:
FsoeCrcTable();
/* ---- 低レベル CRC 操作 ---- */
/* CRC 状態を 0 にリセット */
void reset();
/* 1バイトを処理(FSoE 更新ステップ) */
void update(uint8_t byte);
/* 現在の CRC 値を取得 */
uint16_t value() const;
/* ---- 高レベル FSoE 操作 ---- */
/* 与えられた PDU パラメータの CRC0 を計算。
処理:startCrc(2), ConnID(2), SeqNo(2), Command(1), Data[0..dataLen-1]
dataLen は size<=6 で 1、size>6 で 2。 */
uint16_t computeCrc0(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command,
const uint8_t* data, int dataLen);
/* crc_common を計算(複数 CRC セグメントの共有ベース)。
処理:startCrc(2), ConnID(2), SeqNo(2), Command(1) */
uint16_t computeCrcCommon(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command);
/* crc_common から CRCi (i >= 1) を計算。
処理:Index(2), Data[2i], Data[2i+1] */
uint16_t computeCrcI(uint16_t crcCommon, uint16_t index,
uint8_t data0, uint8_t data1);
/* PDU の全 CRC を計算してベクタで返す。
最初の要素は CRC0、以降は CRC1, CRC2, ...
pduSize はバイト単位の PDU 総サイズ。
numData は PDU のデータバイト数。 */
std::vector<uint16_t> computeAllCrcs(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command,
const uint8_t* data, int numData,
int pduSize);
private:
uint16_t crc_;
/* ヘルパー:16ビット値をリトルエンディアンバイト順で処理 */
void update16(uint16_t word);
};
#endif /* FSOE_CRC_TABLE_HPP *//*
* FsoeCrcTable.cpp
* =================
*
* テーブルベース FSoE CRC-16 実装。
*
* ビルド:g++ -std=c++17 -c FsoeCrcTable.cpp -o FsoeCrcTable.o
* (Tables.c とリンク)
*
* SPDX-License-Identifier: CC0-1.0
*/
#include "FsoeCrcTable.hpp"
#include "Tables.h"
FsoeCrcTable::FsoeCrcTable() : crc_(0) {}
void FsoeCrcTable::reset() { crc_ = 0; }
void FsoeCrcTable::update(uint8_t byte)
{
/* FSoE 更新ステップ:
* new_crc = (crc_lo << 8) ^ T0[crc_hi] ^ T3[input]
*
* T0 = CRC16_TABLE (i * x^16 mod P)
* T3 = CRC16_TABLE2 (i * x^40 mod P)
*/
uint16_t t0 = CRC16_TABLE[crc_ >> 8];
uint16_t t3 = CRC16_TABLE2[byte];
crc_ = ((crc_ & 0xFF) << 8) ^ t0 ^ t3;
}
uint16_t FsoeCrcTable::value() const { return crc_; }
void FsoeCrcTable::update16(uint16_t word)
{
update(static_cast<uint8_t>(word & 0xFF)); /* Lo バイト */
update(static_cast<uint8_t>((word >> 8) & 0xFF)); /* Hi バイト */
}
uint16_t FsoeCrcTable::computeCrc0(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command,
const uint8_t* data, int dataLen)
{
reset();
update16(startCrc); /* oldCRC-Lo, oldCRC-Hi */
update16(connId); /* ConnID-Lo, ConnID-Hi */
update16(seqNo); /* SeqNo-Lo, SeqNo-Hi */
update(command); /* Command */
for (int i = 0; i < dataLen; i++)
update(data[i]); /* データバイト */
return crc_;
}
uint16_t FsoeCrcTable::computeCrcCommon(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command)
{
reset();
update16(startCrc);
update16(connId);
update16(seqNo);
update(command);
return crc_;
}
uint16_t FsoeCrcTable::computeCrcI(uint16_t crcCommon, uint16_t index,
uint8_t data0, uint8_t data1)
{
crc_ = crcCommon; /* 共有ベースから再開 */
update16(index); /* Index-Lo, Index-Hi */
update(data0); /* Data[2i] */
update(data1); /* Data[2i+1] */
return crc_;
}
std::vector<uint16_t> FsoeCrcTable::computeAllCrcs(uint16_t startCrc,
uint16_t connId,
uint16_t seqNo,
uint8_t command,
const uint8_t* data,
int /*numData*/,
int pduSize)
{
std::vector<uint16_t> crcs;
/* 最初の1-2データバイト(CRC0 の前) */
int firstData = (pduSize > 6) ? 2 : 1;
/* CRC0 */
crcs.push_back(computeCrc0(startCrc, connId, seqNo, command,
data, firstData));
/* 追加のセグメント CRC(size > 10 の場合) */
if (pduSize > 10) {
uint16_t crcCommon = computeCrcCommon(startCrc, connId, seqNo, command);
int numExtra = (pduSize - 7 + 3) / 4;
int dataPos = firstData;
for (int i = 1; i <= numExtra; i++) {
crcs.push_back(computeCrcI(crcCommon, static_cast<uint16_t>(i),
data[dataPos], data[dataPos + 1]));
dataPos += 2;
}
}
return crcs;
}ビルド(テーブルベース):
# ルックアップテーブルを生成(GenTables.c はリンク先の記事を参照)
cc -std=c99 -O2 -o GenTables GenTables.c
./GenTables > Tables.c
# テーブルベースバリアントをコンパイル
g++ -std=c++17 -O2 -c FsoeCrcTable.cpp -o FsoeCrcTable.o
gcc -std=c99 -c Tables.c -o Tables.o
# FsoeCrcTable.o + Tables.o をプロジェクトにリンクバリアント B:オンザフライ実装(テーブルなし)
ライセンス: CC0 1.0 Universal — パブリックドメイン献呈
このバリアントは完全に自己完結——外部テーブルファイル不要。各バイト更新で多項式除算を評価して同じ CRC を計算します。
/*
* FsoeCrcOnTheFly.hpp
* ====================
*
* オンザフライ FSoE CRC-16 計算(ルックアップテーブルなし)。
*
* テーブルベース版と同じ CRC を計算するが、各テーブルエントリを
* 多項式 P(x) = 0x39B7 でビットごとの多項式長除算を使い
* オンデマンドで算出する。
*
* バイトごとの更新ステップ:
* new_crc = (crc_lo << 8) ^ T0[crc_hi] ^ T3[input]
*
* T0 と T3 はオンザフライで計算:
* T0[byte] = polyMod(byte, 8) = (byte * x^16) mod P
* T3[byte] = polyMod(byte, 32) = (byte * x^40) mod P
*
* ビルド:g++ -std=c++17 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
* (外部テーブル依存なし)
*
* SPDX-License-Identifier: CC0-1.0
*/
#ifndef FSOE_CRC_ONTHEFLY_HPP
#define FSOE_CRC_ONTHEFLY_HPP
#include <cstdint>
#include <vector>
class FsoeCrcOnTheFly {
public:
FsoeCrcOnTheFly();
/* ---- 低レベル CRC 操作 ---- */
void reset();
void update(uint8_t byte);
uint16_t value() const;
/* ---- 高レベル FSoE 操作(FsoeCrcTable と同じ API) ---- */
uint16_t computeCrc0(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command,
const uint8_t* data, int dataLen);
uint16_t computeCrcCommon(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command);
uint16_t computeCrcI(uint16_t crcCommon, uint16_t index,
uint8_t data0, uint8_t data1);
std::vector<uint16_t> computeAllCrcs(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command,
const uint8_t* data, int numData,
int pduSize);
private:
uint16_t crc_;
/* FSoE CRC 多項式:x^16 + x^13 + x^12 + x^11 + x^8 + x^7
* + x^5 + x^4 + x^2 + x + 1
* 下位16ビット(x^16 項は暗黙):0x39B7 */
static constexpr uint16_t POLY = 0x39B7;
/* ビットごとの多項式除算で (byte * x^(8+shifts)) mod P を計算。
* byte を16ビットレジスタの上位バイトにロードし、bit 15 が
* 立つたびに POLY を XOR しながら 'shifts' 回左シフト。 */
static uint16_t polyMod(uint8_t byte, int shifts);
/* ヘルパー:16ビット値をリトルエンディアンバイト順で処理 */
void update16(uint16_t word);
};
#endif /* FSOE_CRC_ONTHEFLY_HPP *//*
* FsoeCrcOnTheFly.cpp
* ====================
*
* オンザフライ FSoE CRC-16 実装(ルックアップテーブルなし)。
*
* ビルド:g++ -std=c++17 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
* (外部テーブル依存なし)
*
* SPDX-License-Identifier: CC0-1.0
*/
#include "FsoeCrcOnTheFly.hpp"
constexpr uint16_t FsoeCrcOnTheFly::POLY;
FsoeCrcOnTheFly::FsoeCrcOnTheFly() : crc_(0) {}
void FsoeCrcOnTheFly::reset() { crc_ = 0; }
uint16_t FsoeCrcOnTheFly::polyMod(uint8_t byte, int shifts)
{
/* byte を16ビットレジスタの上位バイトにロードし、bit 15 が
* 立つたびに多項式を XOR しながら 'shifts' 回左シフト。
* これで (byte * x^(8+shifts)) mod P を計算する。 */
uint16_t r = static_cast<uint16_t>(byte) << 8;
for (int i = 0; i < shifts; i++)
r = (r & 0x8000U) ? static_cast<uint16_t>((r << 1) ^ POLY)
: static_cast<uint16_t>(r << 1);
return r;
}
void FsoeCrcOnTheFly::update(uint8_t byte)
{
/* FSoE 更新ステップ(テーブルベースと同じだが T0/T3 はオンザフライ計算):
* new_crc = (crc_lo << 8) ^ T0[crc_hi] ^ T3[input]
*
* T0[crc_hi] = polyMod(crc_hi, 8) = (crc_hi * x^16) mod P
* T3[input] = polyMod(input, 32) = (input * x^40) mod P
*/
uint16_t t0 = polyMod(static_cast<uint8_t>(crc_ >> 8), 8);
uint16_t t3 = polyMod(byte, 32);
crc_ = ((crc_ & 0xFF) << 8) ^ t0 ^ t3;
}
uint16_t FsoeCrcOnTheFly::value() const { return crc_; }
void FsoeCrcOnTheFly::update16(uint16_t word)
{
update(static_cast<uint8_t>(word & 0xFF));
update(static_cast<uint8_t>((word >> 8) & 0xFF));
}
uint16_t FsoeCrcOnTheFly::computeCrc0(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command,
const uint8_t* data, int dataLen)
{
reset();
update16(startCrc);
update16(connId);
update16(seqNo);
update(command);
for (int i = 0; i < dataLen; i++)
update(data[i]);
return crc_;
}
uint16_t FsoeCrcOnTheFly::computeCrcCommon(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command)
{
reset();
update16(startCrc);
update16(connId);
update16(seqNo);
update(command);
return crc_;
}
uint16_t FsoeCrcOnTheFly::computeCrcI(uint16_t crcCommon, uint16_t index,
uint8_t data0, uint8_t data1)
{
crc_ = crcCommon;
update16(index);
update(data0);
update(data1);
return crc_;
}
std::vector<uint16_t> FsoeCrcOnTheFly::computeAllCrcs(uint16_t startCrc,
uint16_t connId,
uint16_t seqNo,
uint8_t command,
const uint8_t* data,
int /*numData*/,
int pduSize)
{
std::vector<uint16_t> crcs;
int firstData = (pduSize > 6) ? 2 : 1;
crcs.push_back(computeCrc0(startCrc, connId, seqNo, command,
data, firstData));
if (pduSize > 10) {
uint16_t crcCommon = computeCrcCommon(startCrc, connId, seqNo, command);
int numExtra = (pduSize - 7 + 3) / 4;
int dataPos = firstData;
for (int i = 1; i <= numExtra; i++) {
crcs.push_back(computeCrcI(crcCommon, static_cast<uint16_t>(i),
data[dataPos], data[dataPos + 1]));
dataPos += 2;
}
}
return crcs;
}ビルド(オンザフライ):
# テーブル生成不要——完全に自己完結
g++ -std=c++17 -O2 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
# FsoeCrcOnTheFly.o をプロジェクトにリンクどちらのバリアントを使うべきか?
| 基準 | テーブルベース | オンザフライ |
|---|---|---|
| 速度 | 高速(バイトごと2回参照) | 低速(バイトごと8+32回シフト) |
| メモリ | テーブル用に約1KB | 約0バイト(多項式定数のみ) |
| 依存関係 | Tables.c が必要 | 自己完結 |
| 用途 | 性能重視、テーブルがすでに利用可能 | メモリ制約、迅速なプロトタイピング、テーブル生成の回避 |
両バリアントは同一の CRC 値を生成します——単一 CRC PDU、複数 CRC PDU(最大4 CRC)、フレーム間 CRC 継承、エッジケース(全ゼロ、全 0xFF、非ゼロ startCrc)をカバーする10のテストケースで、元の FSoE CalcCrc 参照実装に対して検証済みです。
参照 CRC 値
以下の値で実装を検証できます。特記なき限り startCrc = 0x0000、seqNo = 0x0001 を使用:
| size | startCrc | ConnID | Cmd | Data | CRC₀ | CRC₁ | CRC₂ | CRC₃ |
|---|---|---|---|---|---|---|---|---|
| 6 | 0x0000 | 0x1234 | 0x01 | AB | 0x2C82 | — | — | — |
| 7 | 0x0000 | 0x1234 | 0x01 | AB CD | 0xDD27 | — | — | — |
| 11 | 0x0000 | 0x1234 | 0x01 | AB CD EF 12 | 0xDD27 | 0x05A5 | — | — |
| 15 | 0x0000 | 0x1234 | 0x01 | AB CD EF 12 34 56 | 0xDD27 | 0x05A5 | 0x93AA | — |
| 7 | 0xDD27 | 0x1234 | 0x01 | AB CD | 0x41F5 | — | — | — |
| 19 | 0x0000 | 0xCAFE | 0x02 | 10 20 30 40 50 60 70 80 | 0xE6BD | 0x687A | 0xB265 | 0x657C |
5行目(startCrc = 0xDD27)は CRC 継承を示します:2行目の CRC₀ を startCrc として使い、同じデータに対して異なる CRC を生成します。
関連記事
概要と基礎
- FSoE という略語は実際何を意味するのか? — FSoE 頭字語が表すもの
- FSoE フレーム構造を例で解説 — 一般的な Safety PDU レイアウト
- FSoE ステートマシンの全状態 — 5つの FSoE 状態とその遷移
- FSoE: Safety PDU コマンドテーブル — FSoE コマンドの全リスト
状態別 PDU 構造
- FSoE Reset State PDU: マスターとスレーブの構造 — Reset 状態の PDU レイアウトとエラーコード
- FSoE Session PDU: マスターとスレーブの構造 — Session 状態の PDU レイアウト
- FSoE Connection PDU: マスターとスレーブの構造 — Connection 状態の PDU レイアウト
- FSoE Parameter PDU: マスターとスレーブの構造 — Parameter 状態の PDU レイアウト
- FSoE Data PDU: マスターとスレーブの構造 — Data 状態の PDU レイアウト
- FSoE Reset PDU: マスターとスレーブの構造 — 全 safety-data 長のバイトレイアウト
CRC
- FSoE CRC: どの多項式を使うか? — FSoE CRC の背後にある17ビット多項式
- FSoE CRC テーブルはどう構築されるか? — CRC ルックアップテーブルの生成方法
- FSoE: CRC 継承はどう機能するか? — CRC チェーンが連続 PDU をどうリンクするか
エラーコードとデータ形式
- FSoE: 全通信エラーコード一覧 — すべての FSoE 通信エラーコード
- FSoE: データはリトルエンディアンかビッグエンディアンか? — 複数バイトフィールドのバイト順