Functional Safety over EtherCAT (FSoE) usa um CRC de 16 bits personalizado para proteger cada Safety PDU (Protocol Data Unit). À primeira vista, o algoritmo parece um CRC-16 padrão com tabela de consulta, mas duas escolhas de design o tornam incomum:
- Cada byte de dados é multiplicado por um fator extra de $x^{24}$ em comparação com uma atualização de CRC padrão byte a byte.
- O CRC do quadro anterior é dobrado no CRC do próximo quadro, criando uma cadeia entre quadros (“herança de CRC”).
Este artigo explica ambos os mecanismos em detalhe, com base no código-fonte original do FSoE e nas tabelas de consulta geradas automaticamente em Tables.c.
O polinômio e as duas tabelas
O CRC do FSoE usa o polinômio de 17 bits
$$ P(x) = x^{16} + x^{13} + x^{12} + x^{11} + x^{8} + x^{7} + x^{5} + x^{4} + x^{2} + x + 1 $$empacotado como 0x39B7 (o termo $x^{16}$ é implícito). O CRC é processado MSB-first (a direção “normal”, não refletida).
Duas tabelas de consulta de 256 entradas são pré-computadas a partir deste polinômio:
| Tabela | Fórmula | Deslocamentos | Significado |
|---|---|---|---|
CRC16_TABLE ($T_0$) | $T_0[i] = (i \cdot x^{16}) \bmod P$ | 8 | Tabela padrão byte a byte |
CRC16_TABLE2 ($T_3$) | $T_3[i] = (i \cdot x^{40}) \bmod P$ | 32 | Tabela slice-by-4 = $T_0$ aplicada 4× (byte $i$ + 3 bytes zero) |
Ambas as tabelas são geradas carregando $i$ no byte alto de um registrador de 16 bits e deslocando para a esquerda, fazendo XOR com o polinômio sempre que o bit 15 estiver definido:
for (n = 0; n < 8 + 8*k; n++)
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x39B7) : (crc << 1);A etapa de atualização por byte
Cada byte é dobrado no CRC com este padrão (repetido para cada byte no cálculo):
w1 = aCRCTab1[crc >> 8]; /* T0[crc_hi] -- deslocar registrador CRC 1 byte */
w2 = aCRCTab2[input_byte]; /* T3[input] -- incorporar o byte de entrada */
w = w1 ^ w2;
crc = (w & 0xFF) | ((w >> 8 ^ crc_lo) << 8);Simplificando (isso pode ser verificado expandindo as operações de byte):
$$ \text{crc}_{\text{new}} = (\text{crc}_{\text{lo}} \ll 8) \;\oplus\; T_0[\text{crc}_{\text{hi}}] \;\oplus\; T_3[\text{input}] $$O termo $(\text{crc}{\text{lo}} \ll 8) \oplus T_0[\text{crc}{\text{hi}}]$ é exatamente o deslocamento padrão do registrador CRC MSB-first por um byte (equivalente a processar um byte zero). Então $T_3[\text{input}]$ é adicionado via XOR.
Como isso difere de um CRC-16 padrão
Uma atualização de CRC-16 padrão byte a byte seria:
$$ \text{crc}_{\text{std}} = (\text{crc}_{\text{lo}} \ll 8) \;\oplus\; T_0[\text{crc}_{\text{hi}} \oplus \text{input}] $$que se expande para:
$$ \text{crc}_{\text{std}} = (\text{crc}_{\text{lo}} \ll 8) \;\oplus\; T_0[\text{crc}_{\text{hi}}] \;\oplus\; T_0[\text{input}] $$O código FSoE substitui $T_0[\text{input}]$ por $T_3[\text{input}]$. Como a aritmética de CRC é linear sobre GF(2):
- $T_0[\text{input}] = (\text{input} \cdot x^{16}) \bmod P$ — o byte de entrada em sua posição natural
- $T_3[\text{input}] = (\text{input} \cdot x^{40}) \bmod P$ — o byte de entrada deslocado 24 bits (3 bytes) para cima
Portanto, cada byte de entrada recebe um fator extra de $x^{24}$ em comparação com um CRC padrão. Após processar $n$ bytes $b_0 \dots b_{n-1}$ a partir do valor inicial $\text{startCrc}$ (que ele próprio é dobrado como os primeiros 2 bytes):
$$ \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 $$versus o padrão:
$$ \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 $$A única diferença é $x^{40}$ vs $x^{16}$ para cada byte de dados — um extra de $x^{24}$ por byte. Equivalentemente, o CRC do FSoE é o CRC da mensagem como se cada byte fosse seguido por 3 bytes zero, mas com o valor inicial deslocado apenas pela contagem real de bytes (não pela contagem preenchida). Esta é uma variante personalizada específica do FSoE, não equivalente a nenhuma convenção padrão de CRC preenchido.
Ordem de processamento de bytes
O CRC é redefinido para 0 no início de cada iteração, então os bytes são processados nesta ordem:
oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi, SeqNo-Lo, SeqNo-Hi, Command, [Data...]- oldCRC vem do parâmetro
startCrc(o CRC do quadro anterior — veja Herança abaixo) - ConnID é lido do final da PDU:
au8Data[size-2](Lo) eau8Data[size-1](Hi) - SeqNo vem do ponteiro
seqNo - Command está em
au8Data[OFFS_COMMAND] - Data começa em
au8Data[OFFS_DATA]
Herança de CRC — dois níveis
Nível 1: Herança entre quadros (a cadeia startCrc / oldCRC)
Os primeiros dois bytes dobrados em cada cálculo de CRC são o CRC do quadro anterior (startCrc). Isso cria uma cadeia entre quadros:
O CRC de cada quadro depende dos CRCs de todos os quadros anteriores. Um atacante não pode reproduzir um quadro antigo porque a cadeia de CRC seria quebrada.
O parâmetro oldCrc é usado para detecção de colisão no loop do…while:
} while (crc == oldCrc && (bRcvDir & NEW_CRC) != 0);Se o CRC recém-computado acontecer de ser igual ao CRC do quadro anterior, o número de sequência é incrementado (seqNo[0]++, pulando zero) e o CRC é recomputado. Isso continua até que o CRC difira de oldCrc, garantindo que dois quadros consecutivos nunca compartilhem o mesmo CRC — um requisito de segurança para tornar as colisões de CRC detectáveis.
Nível 2: Herança intra-quadro (a base crc_common)
Dentro de uma única PDU, quando o payload excede 10 bytes, múltiplos CRCs (CRC₀, CRC₁, CRC₂, …) são embutidos — cada um protegendo um segmento de dados de 2 bytes. O mecanismo chave é o crc_common.
Passo 1 — Computar a base comum (7 bytes processados):
crc = 0;
/* processar: oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi,
SeqNo-Lo, SeqNo-Hi, Command */
crc_common = crc; /* salvo aqui */Essa base encapsula todos os campos de “cabeçalho”: o CRC antigo herdado, o Connection ID, o Sequence Number e o byte Command.
Passo 2 — CRC₀ (primeiro segmento, continuando de crc_common):
/* continuar de crc_common: processar Data[0], Data[1] */
/* resultado = CRC₀ (armazenado/verificado em pCrc) */Passo 3 — CRCᵢ para i ≥ 1 (segmentos subsequentes, cada um reiniciando de crc_common):
crc = crc_common; /* REINICIAR da base compartilhada */
/* processar: Index-Lo (i & 0xFF), Index-Hi (i >> 8),
Data[2i], Data[2i+1] */
/* resultado = CRCᵢ (armazenado/verificado na próxima posição pCrc) */A estrutura do loop:
while (size >= 4) { /* cada iteração = 2 bytes de dados + 2 bytes CRC */
crc = crc_common; /* reiniciar da base compartilhada */
/* incorporar: i_lo, i_hi, data[2i], data[2i+1] */
/* verificar/inserir CRC em pCrc[0..1] */
size -= 4;
pSafeData += 4;
pCrc += 4;
i++;
}Por que o byte de índice importa
Cada CRC de segmento CRCᵢ inclui o índice $i$ (baseado em 1, como um valor little-endian de 2 bytes) entre o Command e o Data. Isso garante que mesmo se dois segmentos contiverem bytes de dados idênticos, seus CRCs serão diferentes porque o índice difere. Sem o índice, a troca de dois segmentos de dados passaria despercebida.
Visualização
O diagrama a seguir mostra como a herança de CRC funciona entre quadros e dentro de uma única PDU multi-CRC. Cada CRC de segmento reinicia de crc_common (a base compartilhada), e o CRC de cada quadro se torna o startCrc para o próximo quadro.
Abreviações usadas no diagrama
| Rótulo | Significado |
|---|---|
oC.Lo, oC.Hi | oldCRC (oC) — o CRC do quadro anterior, passado como startCrc; dividido em byte baixo/alto |
CID.Lo, CID.Hi | Connection ID (ConnID) — byte baixo/alto, lido dos últimos 2 bytes da PDU |
Sq.Lo, Sq.Hi | Sequence Number (SeqNo) — byte baixo/alto do número de sequência do quadro |
Cmd | byte de Command — o opcode de comando FSoE no offset OFFS_COMMAND |
D0, D1, D2, … | bytes de Data do payload (a partir de OFFS_DATA) |
i.Lo, i.Hi | índice de segmento i (base 1, little-endian) — byte baixo/alto, incluído em CRCᵢ para i ≥ 1 |
N.Lo, N.Hi | no quadro N+1: o byte baixo/alto do CRC₀ do quadro N (que se torna o oldCRC do quadro N+1) |
CRC₀, CRC₁, CRC₂ | o valor de CRC calculado para o segmento 0, 1, 2 — cada um é inserido (ou verificado) na PDU |
CRCᵢ = f( crc_common, Index=i, Data[2i], Data[2i+1] ) for i ≥ 1
crc_common = f( oldCRC, ConnID, SeqNo, Command ) /* salvo, não transmitido */
nextFrame.startCrc = thisFrame.CRC₀
Layout da PDU
O layout de bytes da PDU (reconstruído a partir dos acessos de campo em CalcCrc) depende do tamanho total:
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 está sempre nos últimos 2 bytes (
au8Data[size-2..size-1]) - CRC₀ está no offset 2 (size ≤ 6) ou offset 3 (size > 6)
- Cada segmento adicional tem 4 bytes: 2 dados + 2 CRC
size -= 7contabiliza a primeira parte (ConnID 2 + CRC₀ 2 + Command 1 + Data 2), entãosize -= 4por segmento adicional
Valores de referência de CRC
Os seguintes valores de referência foram gerados por uma transcrição fiel da função original CalcCrc e verificados independentemente usando divisão polinomial longa bit a bit (sem tabelas de consulta). Todas as entradas usam startCrc = 0x0000, seqNo = 0x0001, oldCrc = 0x0000 salvo indicação em contrário.
| 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 |
Teste 6b demonstra herança entre quadros: ele usa o CRC₀ do Teste 2 (0xDD27) como seu startCrc, produzindo um CRC diferente (0x41F5) para os mesmos dados — mostrando como a cadeia de herança muda o CRC mesmo quando o payload é idêntico.
Resumo
O mecanismo de CRC do FSoE tem duas camadas de herança:
Entre quadros: O CRC₀ do quadro anterior é dobrado no CRC do próximo quadro como os primeiros dois bytes, criando uma cadeia que previne ataques de repetição. Um loop
do…whilegarante que dois quadros consecutivos não compartilhem o mesmo CRC.Intra-quadro: Para PDUs multi-segmento, uma base compartilhada
crc_common(contendo oldCRC, ConnID, SeqNo, Command) é computada uma vez e reutilizada como ponto de partida para cada CRC de segmento. Cada segmento adiciona seu índice e 2 bytes de dados, produzindo um CRC independente que protege contra reordenação de segmentos.
A etapa de atualização em si é um deslocamento padrão de registrador CRC-16 MSB-first, mas com o byte de entrada dobrado via $T_3$ (a tabela $x^{40}$) em vez de $T_0$ (a tabela $x^{16}$), dando a cada byte de dados um fator extra de $x^{24}$. Isso torna o CRC do FSoE uma variante personalizada, não equivalente a nenhuma convenção padrão de CRC-16.
Artigos relacionados
Visão geral e noções básicas
- O que a abreviação FSoE realmente significa? — o que significa o acrônimo FSoE
- FSoE: estrutura de quadro explicada por exemplos — estrutura geral da Safety PDU
- FSoE: tabela de comandos da Safety PDU — a lista completa de comandos FSoE
CRC
- FSoE CRC: qual polinômio ele usa? — o polinômio de 17 bits do CRC do FSoE
- FSoE: Como as tabelas CRC são construídas? — como as tabelas de lookup CRC são geradas
Códigos de erro e formato de dados
- FSoE: lista de todos os códigos de erro de comunicação — todos os códigos de erro de comunicação FSoE
- FSoE: os dados são transmitidos em little-endian ou big-endian? — ordem de bytes dos campos multibyte