Functional Safety over EtherCAT (FSoE) використовує власний 16-бітний CRC для захисту кожного Safety PDU (Protocol Data Unit). На перший погляд алгоритм виглядає як стандартний CRC-16 з таблицею пошуку, але два дизайнерські рішення роблять його незвичайним:
- Кожен байт даних множиться на додатковий множник $x^{24}$ порівняно зі стандартним побайтовим оновленням CRC.
- 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 встановлено:
for (n = 0; n < 8 + 8*k; n++)
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x39B7) : (crc << 1);Крок побайтового оновлення
Кожен байт згортається в CRC за цим шаблоном (повторюється для кожного байта в обчисленні):
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 на початку кожної ітерації, потім байти обробляються в такому порядку:
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 кожного кадру залежить від CRC усіх попередніх кадрів. Зловмисник не може відтворити старий кадр, оскільки ланцюг 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 байтів, вбудовується кілька CRC (CRC₀, CRC₁, CRC₂, …) — кожен захищає 2-байтовий сегмент даних. Ключовий механізм — crc_common.
Крок 1 — Обчислення спільної бази (обробляється 7 байтів):
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):
/* continue from crc_common: process Data[0], Data[1] */
/* result = CRC₀ (stored/verified at pCrc) */Крок 3 — CRCᵢ для i ≥ 1 (наступні сегменти, кожен починається заново з crc_common):
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) */Структура циклу:
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.Hi | oldCRC (oC) — CRC попереднього кадру, переданий як startCrc; розбито на молодший/старший байт |
CID.Lo, CID.Hi | Connection ID (ConnID) — молодший/старший байт, зчитується з останніх 2 байт PDU |
Sq.Lo, Sq.Hi | Sequence 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ᵢ = 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, якщо не зазначено інше.
| Test | 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 навіть при однаковому корисному навантаженні.
Підсумок
Механізм FSoE CRC має два рівні успадкування:
Міжкадрове: CRC₀ попереднього кадру згортається в CRC наступного кадру як перші два байти, створюючи ланцюг, що запобігає атакам відтворення. Цикл
do…whileгарантує, що жодні два послідовні кадри не мають однакового CRC.Внутрішньокадрове: Для мульти-сегментних 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.
Пов’язані статті
Огляд та основи
- Що насправді означає абревіатура 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 — байтові структури для всіх довжин safety-data
CRC
- FSoE CRC: Який поліном він використовує? — 17-бітний поліном CRC FSoE
- FSoE: Як устроєні таблиці CRC? — як генеруються таблиці CRC
- Як обчислити контрольну суму CRC для FSoE PDU? — алгоритм обчислення CRC побайтово
Коди помилок та формат даних
- FSoE: Перелік усіх кодів помилок зв’язку — усі коди помилок зв’язку FSoE
- FSoE: дані передаються у little-endian чи big-endian? — порядок байтів у багатобайтових полях