FSoE PDU の CRC チェックサムを計算する方法

この記事では、FSoE(Functional Safety over EtherCAT)CRC-16 チェックサムのすぐに使える2つの C++ 実装を提供します。どちらも同一の結果を生成し、元の FSoE 参照コードで検証済みです。

アルゴリズムの動作の背景については以下を参照:

アルゴリズム概要

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 にリセットされ、その後バイトがこの順序で処理されます:

example.txt
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 を共有:

example.cpp
// 低レベル
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

使用例

example.cpp
#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;
}
output.txt
CRC0 = 0xDD27

バリアント A:テーブルベース実装

ライセンス: CC0 1.0 Universal — パブリックドメイン献呈

このバリアントは Tables.c のルックアップテーブル(FSoE CRC テーブルはどう構築されるか? 参照)と対応する Tables.h ヘッダが必要です:

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
/*
 * 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
/*
 * 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;
}

ビルド(テーブルベース):

build-table-based.sh
# ルックアップテーブルを生成(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
/*
 * 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
/*
 * 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;
}

ビルド(オンザフライ):

build-on-the-fly.sh
# テーブル生成不要——完全に自己完結
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 を使用:

sizestartCrcConnIDCmdDataCRC₀CRC₁CRC₂CRC₃
60x00000x12340x01AB0x2C82———
70x00000x12340x01AB CD0xDD27———
110x00000x12340x01AB CD EF 120xDD270x05A5——
150x00000x12340x01AB CD EF 12 34 560xDD270x05A50x93AA—
70xDD270x12340x01AB CD0x41F5———
190x00000xCAFE0x0210 20 30 40 50 60 70 800xE6BD0x687A0xB2650x657C

5行目(startCrc = 0xDD27)は CRC 継承を示します:2行目の CRC₀ を startCrc として使い、同じデータに対して異なる CRC を生成します。

関連記事

概要と基礎

状態別 PDU 構造

CRC

エラーコードとデータ形式


同じカテゴリの類似記事をチェック: 機能安全 EtherCAT C++ CRC