FSoE: Як працює успадкування CRC?

Functional Safety over EtherCAT (FSoE) використовує власний 16-бітний CRC для захисту кожного Safety PDU (Protocol Data Unit). На перший погляд алгоритм виглядає як стандартний CRC-16 з таблицею пошуку, але два дизайнерські рішення роблять його незвичайним:

  1. Кожен байт даних множиться на додатковий множник $x^{24}$ порівняно зі стандартним побайтовим оновленням CRC.
  2. CRC попереднього кадру згортається в CRC наступного кадру, створюючи ланцюг між кадрами (“успадкування CRC”).

Ця стаття детально пояснює обидва механізми на основі оригінального вихідного коду FSoE та автоматично згенерованих таблиць пошуку у Tables.c.

Поліном і дві таблиці

FSoE CRC використовує 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]  -- shift CRC register 1 byte */
w2 = aCRCTab2[input_byte];     /* T3[input]   -- fold in the input byte  */
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 на один байт (еквівалентно обробці нульового байта). Потім $T_3[\text{input}]$ XOR-ується в результат.

Чим це відрізняється від стандартного 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}$ на байт. Еквівалентно, FSoE CRC — це CRC повідомлення ніби кожен байт супроводжувався 3 нульовими байтами, але з початковим значенням, зсунутим лише на фактичну кількість байтів (не на доповнену кількість). Це власний варіант, специфічний для FSoE, не еквівалентний жодній стандартній конвенції padded-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 кожного кадру залежить від CRC усіх попередніх кадрів. Зловмисник не може відтворити старий кадр, оскільки ланцюг 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 байтів, вбудовується кілька CRC (CRC₀, CRC₁, CRC₂, …) — кожен захищає 2-байтовий сегмент даних. Ключовий механізм — crc_common.

Крок 1 — Обчислення спільної бази (обробляється 7 байтів):

crc-common.c
crc = 0;
/* process: oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi,
            SeqNo-Lo, SeqNo-Hi, Command */
crc_common = crc;    /* saved here */

Ця база інкапсулює всі поля “заголовка”: успадкований старий CRC, Connection ID, Sequence Number та байт Command.

Крок 2 — CRC₀ (перший сегмент, продовження з crc_common):

crc0.c
/* continue from crc_common: process Data[0], Data[1] */
/* result = CRC₀  (stored/verified at pCrc) */

Крок 3 — CRCᵢ для i ≥ 1 (наступні сегменти, кожен починається заново з crc_common):

crci.c
crc = crc_common;           /* RESTART from the shared base */
/* process: Index-Lo (i & 0xFF), Index-Hi (i >> 8),
            Data[2i], Data[2i+1] */
/* result = CRCᵢ  (stored/verified at next pCrc position) */

Структура циклу:

multi-crc-loop.c
while (size >= 4) {         /* each iteration = 2 data bytes + 2 CRC bytes */
    crc = crc_common;       /* restart from shared base */
    /* fold in: i_lo, i_hi, data[2i], data[2i+1] */
    /* verify/insert CRC at pCrc[0..1] */
    size -= 4;
    pSafeData += 4;
    pCrc += 4;
    i++;
}

Чому байт індексу важливий

Кожен сегмент CRCᵢ включає індекс $i$ (з 1, як 2-байтове little-endian значення) між Command та Data. Це гарантує, що навіть якщо два сегменти містять однакові байти даних, їхні CRC відрізнятимуться, оскільки індекс різний. Без індексу обмін двох сегментів даних залишився б невиявленим.

Візуалізація

Наступна діаграма показує, як успадкування CRC працює між кадрами та в межах одного мульти-CRC PDU. Кожен сегмент CRC починається заново з crc_common (спільної бази), а CRC кожного кадру стає startCrc для наступного кадру.

Абревіатури на діаграмі

ПозначенняЗначення
oC.Lo, oC.HioldCRC (oC) — CRC попереднього кадру, переданий як startCrc; розбито на молодший/старший байт
CID.Lo, CID.HiConnection ID (ConnID) — молодший/старший байт, зчитується з останніх 2 байт PDU
Sq.Lo, Sq.HiSequence Number (SeqNo) — молодший/старший байт номера послідовності кадру
Cmdбайт Command — код команди FSoE за зміщенням OFFS_COMMAND
D0, D1, D2, …байти Data корисного навантаження (починаючи з OFFS_DATA)
i.Lo, i.Hiіндекс сегмента i (з 1, little-endian) — молодший/старший байт, включається у CRCᵢ для i ≥ 1
N.Lo, N.Hiу кадрі N+1: молодший/старший байт CRC₀ кадру N (який стає oldCRC для кадру N+1)
CRC₀, CRC₁, CRC₂значення CRC, обчислене для сегмента 0, 1, 2 — кожне вставляється (або перевіряється) у PDU
Спільна база (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, якщо не зазначено інше.

TestsizestartCrcConnIDCmdDataCRC₀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 навіть при однаковому корисному навантаженні.

Підсумок

Механізм FSoE CRC має два рівні успадкування:

  1. Міжкадрове: CRC₀ попереднього кадру згортається в CRC наступного кадру як перші два байти, створюючи ланцюг, що запобігає атакам відтворення. Цикл do…while гарантує, що жодні два послідовні кадри не мають однакового CRC.

  2. Внутрішньокадрове: Для мульти-сегментних PDU спільна база crc_common (що містить oldCRC, ConnID, SeqNo, Command) обчислюється один раз і перевикористовується як початкова точка для кожного сегментного CRC. Кожен сегмент додає свій індекс та 2 байти даних, виробляючи незалежний CRC, що захищає від переставляння сегментів.

Сам крок оновлення — це стандартний зсув регістра CRC-16 MSB-first, але з вхідним байтом, згорнутим через $T_3$ (таблиця $x^{40}$) замість $T_0$ (таблиця $x^{16}$), надаючи кожному байту даних додатковий множник $x^{24}$. Це робить FSoE CRC власним варіантом, не еквівалентним жодній стандартній конвенції CRC-16.

Пов’язані статті

Огляд та основи

Структури PDU за станами

CRC

Коди помилок та формат даних


Дивіться схожі статті за категоріями: Functional Safety EtherCAT CRC