Functional Safety over EtherCAT (FSoE) использует специальный 16-битный CRC для защиты каждой Safety PDU (Protocol Data Unit). На первый взгляд алгоритм выглядит как стандартный CRC-16 с таблицей поиска, но два архитектурных решения делают его необычным:
- Каждый байт данных умножается на дополнительный множитель $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 на 1 байт */
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 на один байт (эквивалент обработки нулевого байта). Затем $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}$ на каждый байт. Эквивалентно, 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 каждого кадра зависит от 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;
/* обработка: oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi,
SeqNo-Lo, SeqNo-Hi, Command */
crc_common = crc; /* сохраняется здесь */Эта база инкапсулирует все «заголовочные» поля: унаследованный старый CRC, Connection ID, порядковый номер и байт команды.
Шаг 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. Это гарантирует, что даже если два сегмента содержат одинаковые байты данных, их 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 | Порядковый номер (SeqNo) — младший/старший байт порядкового номера кадра |
Cmd | Байт команды — код команды FSoE по адресу OFFS_COMMAND |
D0, D1, D2, … | Байты данных полезной нагрузки (начиная с 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, если не указано иное.
| Тест | 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.Внутрикадровое: Для многосегментных PDU общая база
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: таблица команд safety 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
- How to compute the CRC checksum for FSoE PDUs? — алгоритм побайтового вычисления CRC
Коды ошибок и формат данных
- FSoE: список всех кодов ошибок связи — все коды ошибок связи FSoE
- FSoE: данные передаются в little-endian или big-endian? — порядок байтов многобайтовых полей