FSoE: كيف يعمل توريث CRC؟

يستخدم Functional Safety over EtherCAT (FSoE) CRC مخصصًا من 16 بت لحماية كل PDU للسلامة (Protocol Data Unit - وحدة بيانات البروتوكول). للوهلة الأولى، تبدو الخوارزمية وكأنها CRC-16 قياسي مع جدول بحث (lookup table)، لكن خيارين تصميميين يجعلانها غير اعتيادية:

  1. يُضرب كل بايت بيانات بعامل إضافي قدره $x^{24}$ مقارنةً بتحديث CRC قياسي بايتًا ببايت.
  2. يُدمج CRC للإطار السابق في CRC للإطار التالي، مما يخلق سلسلة عبر الإطارات (“توريث CRC”).

يشرح هذا المقال كلا الآليتين بالتفصيل، استنادًا إلى كود FSoE المصدري الأصلي وجداول البحث المُولَّدة تلقائيًا في Tables.c.

متعددة الحدود والجدولان

يستخدم CRC في FSoE متعددة حدود من 17 بت

$$ P(x) = x^{16} + x^{13} + x^{12} + x^{11} + x^{8} + x^{7} + x^{5} + x^{4} + x^{2} + x + 1 $$

المُعبَّأ عنها بـ 0x39B7 (حد $x^{16}$ ضمني). يُعالَج CRC بترتيب MSB-first (الاتجاه “الطبيعي” غير المنعكس).

يُحسب مسبقًا جدولا بحث من 256 مدخلاً لكل منهما من متعددة الحدود هذه:

الجدولالصيغةالإزاحاتالمعنى
CRC16_TABLE ($T_0$)$T_0[i] = (i \cdot x^{16}) \bmod P$8جدول قياسي بايتًا ببايت
CRC16_TABLE2 ($T_3$)$T_3[i] = (i \cdot x^{40}) \bmod P$32جدول Slice-by-4 = $T_0$ مطبَّق 4× (بايت $i$ + 3 بايتات صفرية)

يُولَّد كلا الجدولين بتحميل $i$ في البايت العالي لسجل 16 بت والإزاحة يسارًا، مع XOR لمتعددة الحدود كلما كان البت 15 مضبوطًا:

table-entry.c
for (n = 0; n < 8 + 8*k; n++)
    crc = (crc & 0x8000) ? ((crc << 1) ^ 0x39B7) : (crc << 1);

خطوة التحديث لكل بايت

يُدمج كل بايت في CRC وفق هذا النمط (يُكرَّر لكل بايت في الحساب):

fsoe-update.c
w1 = aCRCTab1[crc >> 8];       /* T0[crc_hi] -- إزاحة سجل CRC بمقدار بايت واحد */
w2 = aCRCTab2[input_byte];     /* T3[input] -- دمج بايت الإدخال */
w  = w1 ^ w2;
crc = (w & 0xFF) | ((w >> 8 ^ crc_lo) << 8);

وبالتبسيط (يمكن التحقق من ذلك بفرد عمليات البايت):

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

الحد $(\text{crc}{\text{lo}} \ll 8) \oplus T_0[\text{crc}{\text{hi}}]$ هو بالضبط إزاحة سجل CRC القياسية بترتيب MSB-first بمقدار بايت واحد (تكافئ معالجة بايت صفري). ثم يُ XOR عنصر $T_3[\text{input}]$.

كيف يختلف هذا عن CRC-16 قياسي

تحديث قياسي لـ CRC-16 بايتًا ببايت سيكون:

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

والذي يُفصَّل إلى:

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

يستبدل كود FSoE $T_0[\text{input}]$ بـ $T_3[\text{input}]$. وبما أن حساب CRC خطي على GF(2):

  • $T_0[\text{input}] = (\text{input} \cdot x^{16}) \bmod P$ — بايت الإدخال في موقعه الطبيعي
  • $T_3[\text{input}] = (\text{input} \cdot x^{40}) \bmod P$ — بايت الإدخال مُزاحًا 24 بت (3 بايتات) لأعلى

لذا يحصل كل بايت إدخال على عامل إضافي قدره $x^{24}$ مقارنةً بـ CRC قياسي. بعد معالجة $n$ بايتات $b_0 \dots b_{n-1}$ من قيمة ابتدائية $\text{startCrc}$ (التي تُدمج هي نفسها كأول 2 بايت):

$$ \text{FSoE CRC} = \left( \text{startCrc} \cdot x^{8n} + \sum_{k=0}^{n-1} b_k \cdot x^{40 + 8(n-1-k)} \right) \bmod P $$

مقابل القياسي:

$$ \text{Standard CRC} = \left( \text{startCrc} \cdot x^{8n} + \sum_{k=0}^{n-1} b_k \cdot x^{16 + 8(n-1-k)} \right) \bmod P $$

الفرق الوحيد هو $x^{40}$ مقابل $x^{16}$ لكل بايت بيانات — أي $x^{24}$ إضافية لكل بايت. وبتعبير مكافئ، فإن CRC في FSoE هو CRC للرسالة كما لو أن كل بايت يتبعه 3 بايتات صفرية، لكن مع إزاحة القيمة الابتدائية بعدد البايتات الفعلي فقط (وليس العدد المُحشَو). هذا متغير مخصص خاص بـ FSoE، ولا يكافئ أي اصطلاح CRC مُحشَو قياسي.

ترتيب معالجة البايتات

يُعاد ضبط CRC إلى 0 في بداية كل تكرار، ثم تُعالَج البايتات بهذا الترتيب:

example.txt
oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi, SeqNo-Lo, SeqNo-Hi, Command, [Data...]
  • oldCRC يأتي من المعامل startCrc (CRC للإطار السابق — انظر التوريث أدناه)
  • ConnID يُقرأ من نهاية الـ PDU: au8Data[size-2] (Lo) وau8Data[size-1] (Hi)
  • SeqNo يأتي من المؤشر seqNo
  • Command موجود في au8Data[OFFS_COMMAND]
  • Data يبدأ من au8Data[OFFS_DATA]

توريث CRC — مستويان

المستوى 1: التوريث عبر الإطارات (سلسلة startCrc / oldCRC)

أول بايتين يُدمجان في كل حساب CRC هما CRC للإطار السابق (startCrc). هذا يخلق سلسلة عبر الإطارات:

$$ \begin{aligned} \text{Frame } N\text{'s CRC} &= f(\text{Frame } N\text{'s data},\; \text{Frame } N{-}1\text{'s CRC}) \\ \text{Frame } N{+}1\text{'s CRC} &= f(\text{Frame } N{+}1\text{'s data},\; \text{Frame } N\text{'s CRC}) \end{aligned} $$

يعتمد CRC لكل إطار على جميع CRCs للإطارات السابقة. لا يستطيع المهاجم إعادة تشغيل إطار قديم لأن سلسلة CRC ستنكسر.

يُستخدم المعامل oldCrc لـ كشف التصادم في حلقة do…while:

collision-avoidance.c
} while (crc == oldCrc && (bRcvDir & NEW_CRC) != 0);

إذا صادف أن يساوي CRC المُحتسب حديثًا CRC للإطار السابق، يُزاد رقم التسلسل (seqNo[0]++، مع تخطي الصفر) ويُعاد حساب CRC. يستمر هذا حتى يختلف CRC عن oldCrc، مما يضمن ألا يتشارك إطاران متتاليان أبدًا في نفس CRC — وهو متطلب سلامة لجعل تصادمات CRC قابلة للكشف.

المستوى 2: التوريث داخل الإطار (الأساس crc_common)

ضمن PDU واحد، عندما تتجاوز الحمولة 10 بايتات، تُدمج عدة CRCs (CRC₀، CRC₁، CRC₂، …) — يحمي كل منها مقطع بيانات بحجم 2 بايت. الآلية الرئيسية هي crc_common.

الخطوة 1 — حساب الأساس المشترك (تُعالَج 7 بايتات):

crc-common.c
crc = 0;
/* معالجة: oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi,
            SeqNo-Lo, SeqNo-Hi, Command */
crc_common = crc;    /* يُحفظ هنا */

يُغلِّف هذا الأساس جميع حقول “الترويسة”: CRC القديم المُورَّث، ومعرف الاتصال (Connection ID)، ورقم التسلسل (Sequence Number)، وبايت الأمر (Command).

الخطوة 2 — CRC₀ (المقطع الأول، استكمالًا من crc_common):

crc0.c
/* استكمال من crc_common: معالجة Data[0], Data[1] */
/* النتيجة = CRC₀  (يُحفظ/يُتحقق منه عند pCrc) */

الخطوة 3 — CRCᵢ لـ i ≥ 1 (المقاطع اللاحقة، يبدأ كل منها من جديد من crc_common):

crci.c
crc = crc_common;           /* إعادة بداية من الأساس المشترك */
/* معالجة: Index-Lo (i & 0xFF), Index-Hi (i >> 8),
            Data[2i], Data[2i+1] */
/* النتيجة = CRCᵢ  (يُحفظ/يُتحقق منه عند موضع pCrc التالي) */

بنية الحلقة:

multi-crc-loop.c
while (size >= 4) {         /* كل تكرار = 2 بايت بيانات + 2 بايت CRC */
    crc = crc_common;       /* إعادة بداية من الأساس المشترك */
    /* دمج: i_lo, i_hi, data[2i], data[2i+1] */
    /* تحقق/إدراج CRC عند pCrc[0..1] */
    size -= 4;
    pSafeData += 4;
    pCrc += 4;
    i++;
}

لماذا يهم بايت الفهرس

يُدرج كل مقطع CRCᵢ الفهرس $i$ (مبدوءًا من 1، كقيمة 2-بايت بترتيب little-endian) بين الـ Command والـ Data. هذا يضمن أنه حتى لو احتوى مقطعان على بايتات بيانات متطابقة، فستختلف CRCs الخاصة بهما لأن الفهرس مختلف. بدون الفهرس، سيمر تبديل مقطعي بيانات دون اكتشاف.

التصور المرئي

يوضح المخطط التالي كيف يعمل توريث CRC عبر الإطارات وضمن PDU واحد متعدد الـ CRC. يبدأ كل مقطع CRC من جديد من crc_common (الأساس المشترك)، ويصبح CRC لكل إطار هو startCrc للإطار التالي.

الأساس المشترك (crc_common)
بايت بيانات
CRC (مُدرَج/مُتحقَّق منه)
الفهرس
ConnID
التوريث عبر الإطارات: CRC₀ لكل إطار يصبح startCrc للإطار التالي (oldCRC)
الإطار N−1
CRC₀: oC.Lo oC.Hi CID.Lo CID.Hi Sq.Lo Sq.Hi Cmd D0 D1
CRC₀ = 0xDD27 (مثال)
الإطار N (startCrc = 0xDD27)
crc_common (الأساس المشترك لجميع المقاطع)
0x27 0xDD CID.Lo CID.Hi Sq.Lo Sq.Hi Cmd
CRC₀: (من المشترك) D0 D1 CRC₀
CRC₁: (إعادة من المشترك) i.Lo i.Hi D2 D3 CRC₁
CRC₂: (إعادة من المشترك) i.Lo i.Hi D4 D5 CRC₂
الإطار N+1 (startCrc = CRC₀ للإطار N)
CRC₀: N.Lo N.Hi CID.Lo CID.Hi Sq.Lo Sq.Hi Cmd D0 D1
CRC₀ يُغذِّي الإطار N+2…
CRC₀ = f( oldCRC, ConnID, SeqNo, Command, Data[0], Data[1] )
CRCᵢ = f( crc_common, Index=i, Data[2i], Data[2i+1] ) for i ≥ 1
crc_common = f( oldCRC, ConnID, SeqNo, Command )  /* محفوظ، غير منقول */
nextFrame.startCrc = thisFrame.CRC₀
يسارًا: يُنتج الإطار N−1 القيمة CRC₀ = 0xDD27. وسط: يستخدم الإطار N القيمة 0xDD27 كـ startCrc له — وهي أول بايتين تُدمجان في كل حساب CRC. يُحسب الأساس المشترك (الأزرق) مرة واحدة ويُعاد استخدامه لكل مقطع. يبدأ كل مقطع CRC من هذا الأساس، ويضيف فهرسه وبايتات بياناته، ويُنتج CRC مستقلًا. يمينًا: يصبح CRC₀ للإطار N هو startCrc للإطار N+1، مما يستمر السلسلة.

تخطيط PDU

تخطيط بايتات PDU (المُعاد بناؤه من وصول الحقول في CalcCrc) يعتمد على الحجم الإجمالي:

example.txt
size = 6  (1 data byte, 1 CRC):
  [Command, Data0, CRC0Lo, CRC0Hi, ConnIDLo, ConnIDHi]

size = 7..10  (2 data bytes, 1 CRC):
  [Command, Data0, Data1, CRC0Lo, CRC0Hi, ConnIDLo, ConnIDHi]

size > 10  (multi-CRC, 2+2k data bytes):
  [Command, Data0, Data1, CRC0(2), Data2, Data3, CRC1(2),
   Data4, Data5, CRC2(2), ..., ConnIDLo, ConnIDHi]
  • ConnID موجود دائمًا في آخر 2 بايت (au8Data[size-2..size-1])
  • CRC₀ موجود عند الإزاحة 2 (size ≤ 6) أو الإزاحة 3 (size > 6)
  • كل مقطع إضافي بحجم 4 بايتات: 2 بيانات + 2 CRC
  • size -= 7 تُحسب للجزء الأول (ConnID 2 + CRC₀ 2 + Command 1 + Data 2)، ثم size -= 4 لكل مقطع إضافي

قيم CRC المرجعية

قيم المرجعية التالية وُلِّدت بنسخ أمين لدالة CalcCrc الأصلية وتُتحقق منها بشكل مستقل باستخدام قسمة متعددة الحدود الطويلة بتًا ببت (بدون جداول بحث). تستخدم جميع المدخلات startCrc = 0x0000، seqNo = 0x0001، oldCrc = 0x0000 ما لم يُذكر خلاف ذلك.

الاختبارsizestartCrcConnIDCmdDataCRC₀CRC₁CRC₂CRC₃
160x00000x12340x01AB0x2C82
270x00000x12340x01AB CD0xDD27
3110x00000x12340x01AB CD EF 120xDD270x05A5
4150x00000x12340x01AB CD EF 12 34 560xDD270x05A50x93AA
570x00000x56780x0501 020x82DC
6b70xDD270x12340x01AB CD0x41F5
770xBEEF0x42420x03DE AD0x945E
8110x00000x00000x0000 00 00 000xC8640x7BC5
9150x00000xFFFF0xFFFF FF FF FF FF FF0x26690xDB400xCD65
10190x00000xCAFE0x0210 20 30 40 50 60 70 800xE6BD0x687A0xB2650x657C

يُوضح الاختبار 6b التوريث عبر الإطارات: يستخدم CRC₀ من الاختبار 2 (0xDD27) كـ startCrc له، مُنتجًا CRC مختلفًا (0x41F5) لنفس البيانات — مما يُظهر كيف تغير سلسلة التوريث CRC حتى عندما تكون الحمولة متطابقة.

الخلاصة

آلية CRC في FSoE لها طبقتان من التوريث:

  1. عبر الإطارات: يُدمج CRC₀ للإطار السابق في CRC للإطار التالي كأول بايتين، مما يخلق سلسلة تمنع هجمات إعادة التشغيل. تضمن حلقة do…while ألا يتشارك إطاران متتاليان في نفس CRC.

  2. داخل الإطار: بالنسبة لـ PDUs متعددة المقاطع، يُحسب أساس crc_common مشترك (يحتوي على oldCRC، ConnID، SeqNo، Command) مرة واحدة ويُعاد استخدامه كنقطة بداية لكل CRC لمقطع. يضيف كل مقطع فهرسه و2 بايتات بيانات، مُنتجًا CRC مستقلًا يحمي من إعادة ترتيب المقاطع.

خطوة التحديث ذاتها هي إزاحة سجل CRC-16 قياسية بترتيب MSB-first، لكن مع دمج بايت الإدخال عبر $T_3$ (جدول $x^{40}$) بدلًا من $T_0$ (جدول $x^{16}$)، مما يمنح كل بايت بيانات عامل $x^{24}$ إضافي. هذا يجعل CRC في FSoE متغيرًا مخصصًا، لا يكافئ أي اصطلاح CRC-16 قياسي.

منشورات ذات صلة

نظرة عامة والأساسيات

هياكل PDU حسب الحالة

CRC

أكواد الأخطاء وتنسيق البيانات


Check out similar posts by category: Functional Safety EtherCAT CRC