يقدم هذا المقال تنفيذين جاهزين للاستخدام في C++ لبصمة CRC-16 في FSoE (Functional Safety over EtherCAT). يُنتج كلاهما نتائج متطابقة وقد تم التحقق منهما مقابل كود FSoE المرجعي الأصلي.
للخلفية حول كيفية عمل الخوارزمية، انظر:
- FSoE CRC: أي متعددة حدود تستخدم؟ — متعددة الحدود $P(x) = x^{16} + x^{13} + x^{12} + x^{11} + x^{8} + x^{7} + x^{5} + x^{4} + x^{2} + x + 1$ (0x39B7)
- كيف تُبنى جداول CRC في FSoE؟ — كيف تُشتق جدولا البحث من متعددة الحدود
- FSoE: كيف يعمل توريث CRC؟ — شرح مفصل لخطوة التحديث لكل بايت، والتوريث عبر الإطارات، وآلية
crc_commonداخل الإطار
الخوارزمية باختصار
يعالج CRC في FSoE كل بايت بـ:
$$ \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 في البداية، ثم تُعالَج البايتات بهذا الترتيب:
oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi, SeqNo-Lo, SeqNo-Hi, Command, Data[0], [Data[1], ...]بالنسبة لـ PDUs أكبر من 10 بايتات، تُحسب عدة CRCs — يبدأ كل CRC لمقطع من جديد من أساس crc_common مشترك (الحالة بعد معالجة أول 7 بايتات ترويسة) ويضيف زوج بايتات فهرس و2 بايتات بيانات.
متغيرا التنفيذ
المتغير A: قائم على الجداول (FsoeCrcTable)
يستخدم جدولي البحث المُحسوبين مسبقًا من 256 مدخلًا (CRC16_TABLE وCRC16_TABLE2). هذا أسرع (بحثان في المصفوفة لكل بايت) لكنه يتطلب توليد الجداول وربطها. انظر كيف تُبنى جداول CRC في FSoE؟ لكيفية توليدها.
المتغير B: فوري (FsoeCrcOnTheFly)
يحسب كل مدخل جدول عند الطلب باستخدام قسمة متعددة الحدود الطويلة بتًا ببت. لا حاجة لجداول بحث — فقط ثابت متعددة الحدود 0x39B7. أبطأ (8 أو 32 دورة إزاحة-XOR لكل بايت) لكنه مكتفٍ ذاتيًا ومناسب للبيئات محدودة الذاكرة أو عندما تريد تجنب تخزين 1 كيلوبايت من بيانات الجدول.
يتشارك كلا المتغيرين في نفس 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
مثال الاستخدام
#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 = 0xDD27المتغير A: التنفيذ القائم على الجداول
الترخيص: CC0 1.0 Universal — إهداء للملكية العامة
يتطلب هذا المتغير جداول البحث من Tables.c (انظر كيف تُبنى جداول CRC في FSoE؟) وترويسة Tables.h المُقابلة:
/*
* Tables.h
* ========
*
* ترويسة لجدولي بحث CRC-16 في FSoE المُعرَّفين في Tables.c
* (مُولَّد تلقائيًا بواسطة 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
* =================
*
* حساب CRC-16 في FSoE القائم على الجداول.
*
* يستخدم جداول البحث المُحسوبة مسبقًا من 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();
/* معالجة بايت واحد (خطوة تحديث FSoE) */
void update(uint8_t byte);
/* الحصول على قيمة CRC الحالية */
uint16_t value() const;
/* ---- عمليات FSoE عالية المستوى ---- */
/* حساب CRC0 لمعاملات PDU المُعطاة.
يُعالج: startCrc(2), ConnID(2), SeqNo(2), Command(1), Data[0..dataLen-1]
dataLen هي 1 لـ size<=6، 2 لـ size>6. */
uint16_t computeCrc0(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command,
const uint8_t* data, int dataLen);
/* حساب crc_common (الأساس المشترك لمقاطع multi-CRC).
يُعالج: startCrc(2), ConnID(2), SeqNo(2), Command(1) */
uint16_t computeCrcCommon(uint16_t startCrc, uint16_t connId,
uint16_t seqNo, uint8_t command);
/* حساب CRCi (i >= 1) من crc_common.
يُعالج: Index(2), Data[2i], Data[2i+1] */
uint16_t computeCrcI(uint16_t crcCommon, uint16_t index,
uint8_t data0, uint8_t data1);
/* حساب جميع CRCs لـ PDU وإرجاعها كـ vector.
العنصر الأول هو 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 بت بترتيب بايتات little-endian */
void update16(uint16_t word);
};
#endif /* FSOE_CRC_TABLE_HPP *//*
* FsoeCrcTable.cpp
* =================
*
* تنفيذ CRC-16 في FSoE القائم على الجداول.
*
* البناء: 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));
/* CRCs مقاطع إضافية (إذا كان 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;
}البناء (قائم على الجداول):
# 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المتغير B: التنفيذ الفوري (بدون جداول)
الترخيص: CC0 1.0 Universal — إهداء للملكية العامة
هذا المتغير مكتفٍ ذاتيًا بالكامل — لا حاجة لملفات جداول خارجية. يحسب نفس CRC بتقييم قسمة متعددة الحدود عند كل تحديث بايت.
/*
* FsoeCrcOnTheFly.hpp
* ====================
*
* حساب CRC-16 في FSoE الفوري (بدون جداول بحث).
*
* يحسب نفس 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 عالية المستوى (نفس API مثل 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_;
/* متعددة حدود CRC في FSoE: 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 بت ويُزاح يسارًا
* 'shifts' مرة، مع XOR لـ POLY كلما كان البت 15 مضبوطًا. */
static uint16_t polyMod(uint8_t byte, int shifts);
/* مساعد: معالجة قيمة 16 بت بترتيب بايتات little-endian */
void update16(uint16_t word);
};
#endif /* FSOE_CRC_ONTHEFLY_HPP *//*
* FsoeCrcOnTheFly.cpp
* ====================
*
* تنفيذ CRC-16 في FSoE الفوري (بدون جداول بحث).
*
* البناء: 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 بت، ثم يُزاح يسارًا
* 'shifts' مرة، مع XOR لمتعددة الحدود كلما كان البت 15 مضبوطًا.
* هذا يحسب (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;
}البناء (فوري):
# No table generation needed — fully self-contained
g++ -std=c++17 -O2 -c FsoeCrcOnTheFly.cpp -o FsoeCrcOnTheFly.o
# Link FsoeCrcOnTheFly.o into your projectأي متغير يجب أن أستخدم؟
| المعيار | قائم على الجداول | فوري |
|---|---|---|
| السرعة | سريع (بحثان/بايت) | أبطأ (8+32 إزاحة/بايت) |
| الذاكرة | ~1 كيلوبايت للجداول | ~0 بايت (فقط ثابت متعددة الحدود) |
| التبعيات | يتطلب Tables.c | مكتفٍ ذاتيًا |
| حالة الاستخدام | حرج للأداء، الجداول متاحة بالفعل | محدود الذاكرة، نمذجة سريعة، أو تجنب توليد الجداول |
يُنتج كلا المتغيرين قيم CRC متطابقة — تم التحقق منهما مقابل تنفيذ CalcCrc المرجعي الأصلي لـ FSoE عبر 10 حالات اختبار تغطي PDUs ذات CRC واحد، PDUs متعددة CRC (حتى 4 CRCs)، توريث CRC عبر الإطارات، وحالات حدية (أصفار كاملة، 0xFF كامل، startCrc غير صفري).
قيم CRC المرجعية
يمكن استخدام القيم التالية للتحقق من تنفيذك. تستخدم جميعها startCrc = 0x0000، seqNo = 0x0001 ما لم يُذكر خلاف ذلك:
| 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 |
يُوضح الصف الخامس (startCrc = 0xDD27) توريث CRC: يستخدم CRC₀ من الصف الثاني كـ startCrc له، مُنتجًا CRC مختلفًا لنفس البيانات.