How to compute the CRC checksum for FSoE PDUs?

This post provides two ready-to-use C++ implementations of the FSoE (Functional Safety over EtherCAT) CRC-16 checksum. Both produce identical results and have been verified against the original FSoE reference code.

For background on how the algorithm works, see:

The algorithm in brief

The FSoE CRC processes each byte with:

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

where $T_0[i] = (i \cdot x^{16}) \bmod P$ and $T_3[i] = (i \cdot x^{40}) \bmod P$. The CRC is reset to 0 at the start, then bytes are processed in this order:

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

For PDUs larger than 10 bytes, multiple CRCs are computed — each segment CRC restarts from a shared crc_common base (the state after processing the first 7 header bytes) and adds an index byte pair and 2 data bytes.

Two implementation variants

Variant A: Table-based (FsoeCrcTable)

Uses the two precomputed 256-entry lookup tables (CRC16_TABLE and CRC16_TABLE2). This is faster (two array lookups per byte) but requires the tables to be generated and linked. See How are the FSoE CRC tables constructed? for how to generate them.

Variant B: On-the-fly (FsoeCrcOnTheFly)

Computes each table entry on demand using bit-by-bit polynomial long division. No lookup tables needed — just the polynomial constant 0x39B7. Slower (8 or 32 shift-XOR cycles per byte) but self-contained and suitable for memory-constrained environments or when you want to avoid storing 1 KB of table data.

Both variants share the same API:

example.cpp
// Low-level
void reset();                    // CRC = 0
void update(uint8_t byte);      // FSoE byte update step
uint16_t value() const;         // Get current CRC

// High-level (FSoE-specific)
uint16_t computeCrc0(...);      // CRC0: oldCRC + ConnID + SeqNo + Cmd + Data[0..1]
uint16_t computeCrcCommon(...); // Shared base: oldCRC + ConnID + SeqNo + Cmd
uint16_t computeCrcI(...);      // CRCi: crc_common + Index + Data[2i,2i+1]
std::vector<uint16_t> computeAllCrcs(...); // All CRCs for a PDU

Usage example

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

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

    uint8_t data[] = {0xAB, 0xCD};
    uint16_t startCrc = 0x0000;  // first frame: startCrc = 0
    uint16_t connId   = 0x1234;
    uint16_t seqNo    = 0x0001;
    uint8_t  command  = 0x01;
    int      pduSize  = 7;       // 2 data bytes

    // Compute all CRCs for this PDU
    auto crcs = crc.computeAllCrcs(startCrc, connId, seqNo, command,
                                    data, 2, pduSize);

    printf("CRC0 = 0x%04X\n", crcs[0]);
    // For the next frame, use crcs[0] as startCrc (CRC inheritance)
    return 0;
}
output.txt
CRC0 = 0xDD27

Variant A: Table-based implementation

License: CC0 1.0 Universal — public domain dedication

This variant requires the lookup tables from Tables.c (see How are the FSoE CRC tables constructed?) and the corresponding Tables.h header:

Tables.h
/*
 * Tables.h
 * ========
 *
 * Header for the two FSoE CRC-16 lookup tables defined in Tables.c
 * (auto-generated by 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
 * =================
 *
 * Table-based FSoE CRC-16 computation.
 *
 * Uses the precomputed lookup tables from Tables.c:
 *   CRC16_TABLE  (k=0): T0[i] = (i * x^16) mod P
 *   CRC16_TABLE2 (k=3): T3[i] = (i * x^40) mod P
 *
 * The per-byte update step is:
 *   new_crc = (crc_lo << 8) ^ T0[crc_hi] ^ T3[input]
 *
 * Build: link FsoeCrcTable.cpp with 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();

    /* ---- Low-level CRC operations ---- */

    /* Reset CRC state to 0 */
    void reset();

    /* Process one byte (FSoE update step) */
    void update(uint8_t byte);

    /* Get current CRC value */
    uint16_t value() const;

    /* ---- High-level FSoE operations ---- */

    /* Compute CRC0 for the given PDU parameters.
       Processes: startCrc(2), ConnID(2), SeqNo(2), Command(1), Data[0..dataLen-1]
       dataLen is 1 for size<=6, 2 for size>6. */
    uint16_t computeCrc0(uint16_t startCrc, uint16_t connId,
                         uint16_t seqNo, uint8_t command,
                         const uint8_t* data, int dataLen);

    /* Compute crc_common (shared base for multi-CRC segments).
       Processes: startCrc(2), ConnID(2), SeqNo(2), Command(1) */
    uint16_t computeCrcCommon(uint16_t startCrc, uint16_t connId,
                               uint16_t seqNo, uint8_t command);

    /* Compute CRCi (i >= 1) from crc_common.
       Processes: Index(2), Data[2i], Data[2i+1] */
    uint16_t computeCrcI(uint16_t crcCommon, uint16_t index,
                          uint8_t data0, uint8_t data1);

    /* Compute all CRCs for a PDU and return them as a vector.
       The first element is CRC0, subsequent elements are CRC1, CRC2, ...
       pduSize is the total PDU size in bytes.
       numData is the number of data bytes in the 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_;

    /* Helper: process a 16-bit value in little-endian byte order */
    void update16(uint16_t word);
};

#endif /* FSOE_CRC_TABLE_HPP */
FsoeCrcTable.cpp
/*
 * FsoeCrcTable.cpp
 * =================
 *
 * Table-based FSoE CRC-16 implementation.
 *
 * Build: g++ -std=c++17 -c FsoeCrcTable.cpp -o FsoeCrcTable.o
 *        (link with 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 update step:
     *   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 byte */
    update(static_cast<uint8_t>((word >> 8) & 0xFF)); /* Hi byte */
}

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]);   /* Data bytes */
    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 from shared base */
    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;

    /* First 1-2 data bytes (before CRC0) */
    int firstData = (pduSize > 6) ? 2 : 1;

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

    /* Additional segment CRCs (if 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):

build-table-based.sh
# Generate the lookup tables (see the linked post for GenTables.c)
cc -std=c99 -O2 -o GenTables GenTables.c
./GenTables > Tables.c

# Compile the table-based variant
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 into your project

Variant B: On-the-fly implementation (no tables)

License: CC0 1.0 Universal — public domain dedication

This variant is fully self-contained — no external table files needed. It computes the same CRC by evaluating the polynomial division on each byte update.

FsoeCrcOnTheFly.hpp
/*
 * FsoeCrcOnTheFly.hpp
 * ====================
 *
 * On-the-fly FSoE CRC-16 computation (no lookup tables).
 *
 * Computes the same CRC as the table-based version, but calculates
 * each table entry on demand using bit-by-bit polynomial long
 * division with the polynomial P(x) = 0x39B7.
 *
 * The per-byte update step is:
 *   new_crc = (crc_lo << 8) ^ T0[crc_hi] ^ T3[input]
 *
 * where T0 and T3 are computed 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
 *        (no external table dependency)
 *
 * SPDX-License-Identifier: CC0-1.0
 */

#ifndef FSOE_CRC_ONTHEFLY_HPP
#define FSOE_CRC_ONTHEFLY_HPP

#include <cstdint>
#include <vector>

class FsoeCrcOnTheFly {
public:
    FsoeCrcOnTheFly();

    /* ---- Low-level CRC operations ---- */

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

    /* ---- High-level FSoE operations (same API as 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 polynomial: x^16 + x^13 + x^12 + x^11 + x^8 + x^7
     *                       + x^5 + x^4 + x^2 + x + 1
     * Low 16 bits (x^16 term is implicit): 0x39B7 */
    static constexpr uint16_t POLY = 0x39B7;

    /* Compute (byte * x^(8+shifts)) mod P by bit-by-bit polynomial division.
     * Loads byte into the high byte of a 16-bit register and shifts left
     * 'shifts' times, XORing POLY in whenever bit 15 is set. */
    static uint16_t polyMod(uint8_t byte, int shifts);

    /* Helper: process a 16-bit value in little-endian byte order */
    void update16(uint16_t word);
};

#endif /* FSOE_CRC_ONTHEFLY_HPP */
FsoeCrcOnTheFly.cpp
/*
 * FsoeCrcOnTheFly.cpp
 * ====================
 *
 * On-the-fly FSoE CRC-16 implementation (no lookup tables).
 *
 * Build: g++ -std=c++17 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
 *        (no external table dependency)
 *
 * 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)
{
    /* Load byte into high byte of 16-bit register, then shift left
     * 'shifts' times, XORing the polynomial in whenever bit 15 is set.
     * This computes (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 update step (same as table-based, but T0/T3 computed 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
# No table generation needed — fully self-contained
g++ -std=c++17 -O2 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
# Link FsoeCrcOnTheFly.o into your project

Which variant should I use?

CriterionTable-basedOn-the-fly
SpeedFast (2 lookups/byte)Slower (8+32 shifts/byte)
Memory~1 KB for tables~0 bytes (just the polynomial constant)
DependenciesRequires Tables.cSelf-contained
Use casePerformance-critical, tables already availableMemory-constrained, quick prototyping, or avoiding table generation

Both variants produce identical CRC values — they have been verified against the original FSoE CalcCrc reference implementation across 10 test cases covering single-CRC PDUs, multi-CRC PDUs (up to 4 CRCs), cross-frame CRC inheritance, and edge cases (all-zeros, all-0xFF, non-zero startCrc).

Reference CRC values

The following values can be used to verify your implementation. All use startCrc = 0x0000, seqNo = 0x0001 unless noted:

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

The 5th row (startCrc = 0xDD27) demonstrates CRC inheritance: it uses the CRC₀ from the 2nd row as its startCrc, producing a different CRC for the same data.

Overview & basics

PDU structures by state

CRC

Error codes & data format


Check out similar posts by category: Functional Safety EtherCAT C++ CRC