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:
- FSoE CRC: Jaký polynom používá? — polynom $P(x) = x^{16} + x^{13} + x^{12} + x^{11} + x^{8} + x^{7} + x^{5} + x^{4} + x^{2} + x + 1$ (0x39B7)
- Jak jsou konstruovány FSoE CRC tabulky? — jak jsou dvě vyhledávací tabulky odvozeny z polynomu
- FSoE: Jak funguje dědičnost CRC? — podrobné vysvětlení kroku aktualizace pro každý bajt, dědičnosti mezi rámci a intra-frame mechanismu
crc_common
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í:
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:
// 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í
#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;
}CRC0 = 0xDD27Varianta 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
* ========
*
* 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
* =================
*
* 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
* =================
*
* 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):
# 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 projektuVarianta 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
* ====================
*
* 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
* ====================
*
* 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):
# 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 projektuKterou variantu mám použít?
| Kritérium | Založená na tabulce | On-the-fly |
|---|---|---|
| Rychlost | Rychlá (2 vyhledávání/bajt) | Pomalejší (8+32 posuny/bajt) |
| Paměť | ~1 KB pro tabulky | ~0 bajtů (jen konstanta polynomu) |
| Závislosti | Vyžaduje Tables.c | Samostatná |
| Případ použití | Kritické na výkon, tabulky již k dispozici | Omezená 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:
| 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 |
- řádek (
startCrc = 0xDD27) demonstruje dědičnost CRC: používá CRC₀ z 2. řádku jako svůjstartCrc, produkující odlišné CRC pro stejná data.
Související příspěvky
Přehled a základy
- Co vlastně znamená zkratka FSoE? — co znamená zkratka FSoE
- FSoE: Struktura rámce vysvětlená na příkladech — obecné rozvržení Safety PDU
- FSoE: Tabulka příkazů bezpečnostní PDU — úplný seznam příkazů FSoE
CRC
- FSoE CRC: Jaký polynom používá? — 17bitový polynom CRC FSoE
- FSoE: Jak jsou konstruovány tabulky CRC? — jak se generují vyhledávací tabulky CRC
- FSoE: Jak funguje dědičnost CRC? — jak řetězec CRC propojuje po sobě jdoucí PDU
Chybové kódy a formát dat
- FSoE: Seznam všech chybových kódů komunikace — všechny chybové kódy komunikace FSoE
- FSoE: Jsou data přenášena v little-endian nebo big-endian? — pořadí bajtů vícebajtových polí