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:
- FSoE CRC: Which polynomial does it use? — the polynomial $P(x) = x^{16} + x^{13} + x^{12} + x^{11} + x^{8} + x^{7} + x^{5} + x^{4} + x^{2} + x + 1$ (0x39B7)
- How are the FSoE CRC tables constructed? — how the two lookup tables are derived from the polynomial
- FSoE: How does CRC inheritance work? — detailed explanation of the per-byte update step, cross-frame inheritance, and intra-frame
crc_commonmechanism
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:
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:
// 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
#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;
}CRC0 = 0xDD27Variant 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
* ========
*
* 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
* =================
*
* 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
* =================
*
* 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):
# 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 projectVariant 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
* ====================
*
* 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
* ====================
*
* 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):
# No table generation needed — fully self-contained
g++ -std=c++17 -O2 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
# Link FsoeCrcOnTheFly.o into your projectWhich variant should I use?
| Criterion | Table-based | On-the-fly |
|---|---|---|
| Speed | Fast (2 lookups/byte) | Slower (8+32 shifts/byte) |
| Memory | ~1 KB for tables | ~0 bytes (just the polynomial constant) |
| Dependencies | Requires Tables.c | Self-contained |
| Use case | Performance-critical, tables already available | Memory-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:
| 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 |
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.
Related posts
Overview & basics
- What does the FSoE abbreviation actually mean? — what the FSoE acronym stands for
- FSoE frame structure explained by examples — general Safety PDU layout
- All the states of the FSoE state machine — the five FSoE states and their transitions
- FSoE: Safety PDU command table — the full list of FSoE commands
PDU structures by state
- FSoE Reset State PDU: Master and Slave Structure — PDU layout and error codes for the Reset state
- FSoE Session PDU: Master and Slave Structure — PDU layout for the Session state
- FSoE Connection PDU: Master and Slave Structure — PDU layout for the Connection state
- FSoE Parameter PDU: Master and Slave Structure — PDU layout for the Parameter state
- FSoE Data PDU: Master and Slave Structure — PDU layout for the Data state
- FSoE Reset PDU: Master and Slave Structure — byte layouts for all safety-data lengths
CRC
- FSoE CRC: Which polynomial does it use? — the 17-bit polynomial behind the FSoE CRC
- How are the FSoE CRC tables constructed? — how the CRC lookup tables are generated
- FSoE: How does CRC inheritance work? — how the CRC chain links consecutive PDUs
Error codes & data format
- FSoE: List of all communication error codes — all FSoE communication error codes
- FSoE: Is data transmitted little-endian or big-endian? — byte order of multi-byte fields