يستخدم Functional Safety over EtherCAT (FSoE) CRC مخصصًا من 16 بت لحماية كل PDU للسلامة (Protocol Data Unit - وحدة بيانات البروتوكول). للوهلة الأولى، تبدو الخوارزمية وكأنها CRC-16 قياسي مع جدول بحث (lookup table)، لكن خيارين تصميميين يجعلانها غير اعتيادية:
- يُضرب كل بايت بيانات بعامل إضافي قدره $x^{24}$ مقارنةً بتحديث CRC قياسي بايتًا ببايت.
- يُدمج 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 مضبوطًا:
for (n = 0; n < 8 + 8*k; n++)
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x39B7) : (crc << 1);خطوة التحديث لكل بايت
يُدمج كل بايت في CRC وفق هذا النمط (يُكرَّر لكل بايت في الحساب):
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 في بداية كل تكرار، ثم تُعالَج البايتات بهذا الترتيب:
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). هذا يخلق سلسلة عبر الإطارات:
يعتمد CRC لكل إطار على جميع CRCs للإطارات السابقة. لا يستطيع المهاجم إعادة تشغيل إطار قديم لأن سلسلة CRC ستنكسر.
يُستخدم المعامل oldCrc لـ كشف التصادم في حلقة do…while:
} 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 = 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):
/* استكمال من crc_common: معالجة Data[0], Data[1] */
/* النتيجة = CRC₀ (يُحفظ/يُتحقق منه عند pCrc) */الخطوة 3 — CRCᵢ لـ i ≥ 1 (المقاطع اللاحقة، يبدأ كل منها من جديد من crc_common):
crc = crc_common; /* إعادة بداية من الأساس المشترك */
/* معالجة: Index-Lo (i & 0xFF), Index-Hi (i >> 8),
Data[2i], Data[2i+1] */
/* النتيجة = CRCᵢ (يُحفظ/يُتحقق منه عند موضع pCrc التالي) */بنية الحلقة:
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ᵢ = f( crc_common, Index=i, Data[2i], Data[2i+1] ) for i ≥ 1
crc_common = f( oldCRC, ConnID, SeqNo, Command ) /* محفوظ، غير منقول */
nextFrame.startCrc = thisFrame.CRC₀
تخطيط PDU
تخطيط بايتات PDU (المُعاد بناؤه من وصول الحقول في CalcCrc) يعتمد على الحجم الإجمالي:
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 ما لم يُذكر خلاف ذلك.
| الاختبار | size | startCrc | ConnID | Cmd | Data | CRC₀ | CRC₁ | CRC₂ | CRC₃ |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 6 | 0x0000 | 0x1234 | 0x01 | AB | 0x2C82 | — | — | — |
| 2 | 7 | 0x0000 | 0x1234 | 0x01 | AB CD | 0xDD27 | — | — | — |
| 3 | 11 | 0x0000 | 0x1234 | 0x01 | AB CD EF 12 | 0xDD27 | 0x05A5 | — | — |
| 4 | 15 | 0x0000 | 0x1234 | 0x01 | AB CD EF 12 34 56 | 0xDD27 | 0x05A5 | 0x93AA | — |
| 5 | 7 | 0x0000 | 0x5678 | 0x05 | 01 02 | 0x82DC | — | — | — |
| 6b | 7 | 0xDD27 | 0x1234 | 0x01 | AB CD | 0x41F5 | — | — | — |
| 7 | 7 | 0xBEEF | 0x4242 | 0x03 | DE AD | 0x945E | — | — | — |
| 8 | 11 | 0x0000 | 0x0000 | 0x00 | 00 00 00 00 | 0xC864 | 0x7BC5 | — | — |
| 9 | 15 | 0x0000 | 0xFFFF | 0xFF | FF FF FF FF FF FF | 0x2669 | 0xDB40 | 0xCD65 | — |
| 10 | 19 | 0x0000 | 0xCAFE | 0x02 | 10 20 30 40 50 60 70 80 | 0xE6BD | 0x687A | 0xB265 | 0x657C |
يُوضح الاختبار 6b التوريث عبر الإطارات: يستخدم CRC₀ من الاختبار 2 (0xDD27) كـ startCrc له، مُنتجًا CRC مختلفًا (0x41F5) لنفس البيانات — مما يُظهر كيف تغير سلسلة التوريث CRC حتى عندما تكون الحمولة متطابقة.
الخلاصة
آلية CRC في FSoE لها طبقتان من التوريث:
عبر الإطارات: يُدمج CRC₀ للإطار السابق في CRC للإطار التالي كأول بايتين، مما يخلق سلسلة تمنع هجمات إعادة التشغيل. تضمن حلقة
do…whileألا يتشارك إطاران متتاليان في نفس CRC.داخل الإطار: بالنسبة لـ 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 قياسي.
منشورات ذات صلة
نظرة عامة والأساسيات
- ماذا يعني اختصار FSoE فعليًا؟ — ماذا يعنيه اختصار FSoE
- FSoE: هيكل الإطار مشروح بالأمثلة — التخطيط العام لـ Safety PDU
- جميع حالات آلة حالة FSoE — الحالات الخمس لـ FSoE وانتقالاتها
- FSoE: جدول أوامر PDU للسلامة — القائمة الكاملة لأوامر FSoE
هياكل PDU حسب الحالة
- FSoE Reset State PDU: بنية Master و Slave — تخطيط PDU وأكواد الأخطاء لحالة Reset
- FSoE Session PDU: بنية Master و Slave — تخطيط PDU لحالة Session
- FSoE Connection PDU: بنية Master و Slave — تخطيط PDU لحالة Connection
- FSoE Parameter PDU: بنية Master و Slave — تخطيط PDU لحالة Parameter
- FSoE Data PDU: بنية Master و Slave — تخطيط PDU لحالة Data
- FSoE Reset PDU: بنية Master و Slave — تخطيطات البايت لجميع أطوال بيانات السلامة
CRC
- FSoE CRC: أي متعدد حدود يستخدم؟ — متعدد الحدود 17-bit خلف CRC لـ FSoE
- كيف تُبنى جداول CRC في FSoE؟ — كيف تُولّد جداول lookup لـ CRC
- كيف تُحسب بصمة CRC لـ PDUs في FSoE؟ — خوارزمية حساب CRC بايتًا بايتًا
أكواد الأخطاء وتنسيق البيانات
- FSoE: قائمة بجميع رموز أخطاء الاتصال — جميع رموز أخطاء اتصال FSoE
- FSoE: هل تُنقل البيانات بصيغة little-endian أم big-endian؟ — ترتيب البايتات للحقول متعددة البايتات