Jak vypočítat kontrolní součet CRC pro FSoE PDU?

Tento příspěvek poskytuje dvě hotové k použití C++ implementace kontrolního součtu FSoE (Functional Safety over EtherCAT) CRC-16. Obě dávají identické výsledky a byly ověřeny proti původnímu referenčnímu kódu FSoE.

Pro pozadí toho, jak algoritmus funguje, viz:

Algoritmus ve stručnosti

FSoE CRC zpracovává každý bajt pomocí:

$$ \text{crc}_{\text{new}} = (\text{crc}_{\text{lo}} \ll 8) \;\oplus\; T_0[\text{crc}_{\text{hi}}] \;\oplus\; T_3[\text{input}] $$

kde $T_0[i] = (i \cdot x^{16}) \bmod P$ a $T_3[i] = (i \cdot x^{40}) \bmod P$. CRC je resetováno na 0 na začátku, pak jsou bajty zpracovávány v tomto pořadí:

example.txt
oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi, SeqNo-Lo, SeqNo-Hi, Command, Data[0], [Data[1], ...]

Pro PDU větší než 10 bajtů se počítá více CRC — každé CRC segmentu restartuje ze sdíleného základu crc_common (stav po zpracování prvních 7 hlavičkových bajtů) a přidává pár bajtů indexu a 2 datové bajty.

Dvě implementační varianty

Varianta A: Založená na tabulce (FsoeCrcTable)

Používá dvě předpočítané vyhledávací tabulky o 256 položkách (CRC16_TABLE a CRC16_TABLE2). To je rychlejší (dvě vyhledávání v poli na bajt), ale vyžaduje, aby tabulky byly vygenerovány a přilinkovány. Viz Jak jsou konstruovány FSoE CRC tabulky? pro způsob generování.

Varianta B: On-the-fly (FsoeCrcOnTheFly)

Počítá každou položku tabulky na vyžádání pomocí bit-po-bitu polynomiálního dělení dlouhým dělením. Nejsou potřeba žádné vyhledávací tabulky — pouze konstanta polynomu 0x39B7. Pomalejší (8 nebo 32 shift-XOR cyklů na bajt), ale samostatná a vhodná pro prostředí s omezenou pamětí nebo když chcete vyhnout ukládání 1 KB dat tabulky.

Obě varianty sdílejí stejné API:

example.cpp
// Nízké úrovně
void reset();                    // CRC = 0
void update(uint8_t byte);      // FSoE krok aktualizace bajtu
uint16_t value() const;         // Získat aktuální CRC

// Vysoké úrovně (specifické pro FSoE)
uint16_t computeCrc0(...);      // CRC0: oldCRC + ConnID + SeqNo + Cmd + Data[0..1]
uint16_t computeCrcCommon(...); // Sdílený základ: oldCRC + ConnID + SeqNo + Cmd
uint16_t computeCrcI(...);      // CRCi: crc_common + Index + Data[2i,2i+1]
std::vector<uint16_t> computeAllCrcs(...); // Všechna CRC pro PDU

Příklad použití

example.cpp
#include "FsoeCrcTable.hpp"  // nebo "FsoeCrcOnTheFly.hpp"
#include <cstdio>

int main() {
    FsoeCrcTable crc;  // nebo FsoeCrcOnTheFly

    uint8_t data[] = {0xAB, 0xCD};
    uint16_t startCrc = 0x0000;  // první rámec: startCrc = 0
    uint16_t connId   = 0x1234;
    uint16_t seqNo    = 0x0001;
    uint8_t  command  = 0x01;
    int      pduSize  = 7;       // 2 datové bajty

    // Vypočítat všechna CRC pro toto PDU
    auto crcs = crc.computeAllCrcs(startCrc, connId, seqNo, command,
                                    data, 2, pduSize);

    printf("CRC0 = 0x%04X\n", crcs[0]);
    // Pro další rámec použijte crcs[0] jako startCrc (dědičnost CRC)
    return 0;
}
output.txt
CRC0 = 0xDD27

Varianta A: Implementace založená na tabulce

Licence: CC0 1.0 Universal — věnování public domain

Tato varianta vyžaduje vyhledávací tabulky z Tables.c (viz Jak jsou konstruovány FSoE CRC tabulky?) a odpovídající hlavičku Tables.h:

Tables.h
/*
 * Tables.h
 * ========
 *
 * Hlavička pro dvě FSoE CRC-16 vyhledávací tabulky definované v Tables.c
 * (automaticky generováno 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
 * =================
 *
 * Výpočet FSoE CRC-16 založený na tabulce.
 *
 * Používá předpočítané vyhledávací tabulky z Tables.c:
 *   CRC16_TABLE  (k=0): T0[i] = (i * x^16) mod P
 *   CRC16_TABLE2 (k=3): T3[i] = (i * x^40) mod P
 *
 * Krok aktualizace pro každý bajt je:
 *   new_crc = (crc_lo << 8) ^ T0[crc_hi] ^ T3[input]
 *
 * Build: link FsoeCrcTable.cpp s 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();

    /* ---- Operace CRC nízké úrovně ---- */

    /* Reset stavu CRC na 0 */
    void reset();

    /* Zpracovat jeden bajt (FSoE krok aktualizace) */
    void update(uint8_t byte);

    /* Získat aktuální hodnotu CRC */
    uint16_t value() const;

    /* ---- Operace FSoE vysoké úrovně ---- */

    /* Vypočítat CRC0 pro dané parametry PDU.
       Zpracovává: startCrc(2), ConnID(2), SeqNo(2), Command(1), Data[0..dataLen-1]
       dataLen je 1 pro size<=6, 2 pro size>6. */
    uint16_t computeCrc0(uint16_t startCrc, uint16_t connId,
                         uint16_t seqNo, uint8_t command,
                         const uint8_t* data, int dataLen);

    /* Vypočítat crc_common (sdílený základ pro multi-CRC segmenty).
       Zpracovává: startCrc(2), ConnID(2), SeqNo(2), Command(1) */
    uint16_t computeCrcCommon(uint16_t startCrc, uint16_t connId,
                               uint16_t seqNo, uint8_t command);

    /* Vypočítat CRCi (i >= 1) z crc_common.
       Zpracovává: Index(2), Data[2i], Data[2i+1] */
    uint16_t computeCrcI(uint16_t crcCommon, uint16_t index,
                          uint8_t data0, uint8_t data1);

    /* Vypočítat všechna CRC pro PDU a vrátit je jako vektor.
       První prvek je CRC0, následující prvky jsou CRC1, CRC2, ...
       pduSize je celková velikost PDU v bajtech.
       numData je počet datových bajtů v 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_;

    /* Pomocná funkce: zpracovat 16-bitovou hodnotu v little-endian pořadí bajtů */
    void update16(uint16_t word);
};

#endif /* FSOE_CRC_TABLE_HPP */
FsoeCrcTable.cpp
/*
 * FsoeCrcTable.cpp
 * =================
 *
 * Implementace FSoE CRC-16 založená na tabulce.
 *
 * Build: g++ -std=c++17 -c FsoeCrcTable.cpp -o FsoeCrcTable.o
 *        (link s 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 krok aktualizace:
     *   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 bajt */
    update(static_cast<uint8_t>((word >> 8) & 0xFF)); /* Hi bajt */
}

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]);   /* Datové bajty */
    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;      /* restart ze sdíleného základu */
    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;

    /* První 1-2 datové bajty (před CRC0) */
    int firstData = (pduSize > 6) ? 2 : 1;

    /* CRC0 */
    crcs.push_back(computeCrc0(startCrc, connId, seqNo, command,
                                data, firstData));

    /* Další CRC segmentů (pokud 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 (založený na tabulce):

build-table-based.sh
# Vygenerovat vyhledávací tabulky (viz odkazovaný příspěvek pro GenTables.c)
cc -std=c99 -O2 -o GenTables GenTables.c
./GenTables > Tables.c

# Zkompilovat variantu založenou na tabulce
g++ -std=c++17 -O2 -c FsoeCrcTable.cpp -o FsoeCrcTable.o
gcc -std=c99 -c Tables.c -o Tables.o
# Link FsoeCrcTable.o + Tables.o do vašeho projektu

Varianta B: On-the-fly implementace (bez tabulek)

Licence: CC0 1.0 Universal — věnování public domain

Tato varianta je plně samostatná — nevyžaduje žádné externí soubory tabulek. Počítá stejné CRC vyhodnocením polynomiálního dělení při každé aktualizaci bajtu.

FsoeCrcOnTheFly.hpp
/*
 * FsoeCrcOnTheFly.hpp
 * ====================
 *
 * On-the-fly výpočet FSoE CRC-16 (bez vyhledávacích tabulek).
 *
 * Počítá stejné CRC jako verze založená na tabulce, ale počítá
 * každou položku tabulky na vyžádání pomocí bit-po-bitu polynomiálního
 * dělení dlouhým dělením s polynomem P(x) = 0x39B7.
 *
 * Krok aktualizace pro každý bajt je:
 *   new_crc = (crc_lo << 8) ^ T0[crc_hi] ^ T3[input]
 *
 * kde T0 a T3 jsou počítány on-the-fly:
 *   T0[byte] = polyMod(byte, 8)   = (byte * x^16) mod P
 *   T3[byte] = polyMod(byte, 32)  = (byte * x^40) mod P
 *
 * Build: g++ -std=c++17 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
 *        (žádná externí závislost na tabulce)
 *
 * SPDX-License-Identifier: CC0-1.0
 */

#ifndef FSOE_CRC_ONTHEFLY_HPP
#define FSOE_CRC_ONTHEFLY_HPP

#include <cstdint>
#include <vector>

class FsoeCrcOnTheFly {
public:
    FsoeCrcOnTheFly();

    /* ---- Operace CRC nízké úrovně ---- */

    void reset();
    void update(uint8_t byte);
    uint16_t value() const;

    /* ---- Operace FSoE vysoké úrovně (stejné API jako FsoeCrcTable) ---- */

    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 polynom: x^16 + x^13 + x^12 + x^11 + x^8 + x^7
     *                       + x^5 + x^4 + x^2 + x + 1
     * Spodních 16 bitů (x^16 člen je implicitní): 0x39B7 */
    static constexpr uint16_t POLY = 0x39B7;

    /* Vypočítat (byte * x^(8+shifts)) mod P bit-po-bitu polynomiálním dělením.
     * Načte byte do horního bajtu 16-bitového registru a posune vlevo
     * 'shifts'-krát, XORuje POLY vždy, když je bit 15 nastaven. */
    static uint16_t polyMod(uint8_t byte, int shifts);

    /* Pomocná funkce: zpracovat 16-bitovou hodnotu v little-endian pořadí bajtů */
    void update16(uint16_t word);
};

#endif /* FSOE_CRC_ONTHEFLY_HPP */
FsoeCrcOnTheFly.cpp
/*
 * FsoeCrcOnTheFly.cpp
 * ====================
 *
 * On-the-fly implementace FSoE CRC-16 (bez vyhledávacích tabulek).
 *
 * Build: g++ -std=c++17 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
 *        (žádná externí závislost na tabulce)
 *
 * 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)
{
    /* Načíst byte do horního bajtu 16-bitového registru, pak posunout vlevo
     * 'shifts'-krát, XORovat polynom vždy, když je bit 15 nastaven.
     * Toto počítá (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 krok aktualizace (stejný jako u verze založené na tabulce, ale T0/T3 počítány on-the-fly):
     *   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):

build-on-the-fly.sh
# Není potřeba generování tabulek — plně samostatné
g++ -std=c++17 -O2 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
# Link FsoeCrcOnTheFly.o do vašeho projektu

Kterou variantu mám použít?

KritériumZaložená na tabulceOn-the-fly
RychlostRychlá (2 vyhledávání/bajt)Pomalejší (8+32 posuny/bajt)
Paměť~1 KB pro tabulky~0 bajtů (jen konstanta polynomu)
ZávislostiVyžaduje Tables.cSamostatná
Případ použitíKritické na výkon, tabulky již k dispoziciOmezená paměť, rychlé prototypování nebo vyhnutí se generování tabulek

Obě varianty produkují identické hodnoty CRC — byly ověřeny proti původní referenční implementaci FSoE CalcCrc v 10 testovacích případech pokrývajících PDU s jedním CRC, PDU s více CRC (až 4 CRC), dědičnost CRC mezi rámci a okrajové případy (všechny nuly, všechny 0xFF, nenulový startCrc).

Referenční hodnoty CRC

Následující hodnoty lze použít k ověření vaší implementace. Všechny používají startCrc = 0x0000, seqNo = 0x0001 pokud není uvedeno jinak:

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
  1. řádek (startCrc = 0xDD27) demonstruje dědičnost CRC: používá CRC₀ z 2. řádku jako svůj startCrc, produkující odlišné CRC pro stejná data.

Související příspěvky

Přehled a základy

CRC

Chybové kódy a formát dat


Podívejte se na podobné články podle kategorie: Functional Safety EtherCAT C++ CRC