FSoE: CRC kalıtımı nasıl çalışır?

Functional Safety over EtherCAT (FSoE), her Safety PDU’sunu (Protocol Data Unit) korumak için özel bir 16-bit CRC kullanır. İlk bakışta algoritma bir arama tablosuyla standart bir CRC-16 gibi görünür, ancak iki tasarım tercihi onu olağan dışı yapar:

  1. Her veri baytı, standart bayt bazlı CRC güncellemesine kıyasla ek bir $x^{24}$ çarpanıyla çarpılır.
  2. Önceki çerçevenin CRC’si bir sonraki çerçevenin CRC’sine katılır, çerçeveler arasında bir zincir oluşturur (“CRC kalıtımı”).

Bu makale, her iki mekanizmayı da orijinal FSoE kaynak koduna ve Tables.c içindeki otomatik olarak üretilen arama tablolarına dayanarak ayrıntılı olarak açıklar.

Polinom ve iki tablo

FSoE CRC, 17-bit polinomu kullanır

$$ P(x) = x^{16} + x^{13} + x^{12} + x^{11} + x^{8} + x^{7} + x^{5} + x^{4} + x^{2} + x + 1 $$

0x39B7 olarak paketlenmiş ($x^{16}$ terimi örtüktür). CRC MSB-first (“normal”, yansıtılmamış yön) olarak işlenir.

Bu polinomdan iki adet 256 girişli arama tablosu önceden hesaplanır:

TabloFormülKaydırmalarAnlamı
CRC16_TABLE ($T_0$)$T_0[i] = (i \cdot x^{16}) \bmod P$8Standart bayt bazlı tablo
CRC16_TABLE2 ($T_3$)$T_3[i] = (i \cdot x^{40}) \bmod P$32Slice-by-4 tablosu = $T_0$ 4× uygulanmış (bayt $i$ + 3 sıfır bayt)

Her iki tablo da $i$‘yi 16-bit bir yazmacın yüksek baytına yükleyerek ve sola kaydırarak, 15. bit her set olduğinde polinomu XOR’layarak üretilir:

table-entry.c
for (n = 0; n < 8 + 8*k; n++)
    crc = (crc & 0x8000) ? ((crc << 1) ^ 0x39B7) : (crc << 1);

Bayt başına güncelleme adımı

Her bayt bu örüntüyle CRC’ye katılır (hesaplama içindeki her bayt için tekrarlanır):

fsoe-update.c
w1 = aCRCTab1[crc >> 8];       /* T0[crc_hi]  -- CRC yazmacını 1 bayt kaydır */
w2 = aCRCTab2[input_byte];     /* T3[input]   -- giriş baytını kat           */
w  = w1 ^ w2;
crc = (w & 0xFF) | ((w >> 8 ^ crc_lo) << 8);

Basitleştirilirse (bu, bayt işlemleri açılarak doğrulanabilir):

$$ \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}}]$ terimi tam olarak standart MSB-first CRC yazmaç kaydırması bir bayt için (bir sıfır bayt işlemeye eşdeğer). Ardından $T_3[\text{input}]$ XOR’lanır.

Bu, standart CRC-16’dan nasıl farklıdır

Standart bir bayt bazlı CRC-16 güncellemesi şöyle olurdu:

$$ \text{crc}_{\text{std}} = (\text{crc}_{\text{lo}} \ll 8) \;\oplus\; T_0[\text{crc}_{\text{hi}} \oplus \text{input}] $$

bu da açılır:

$$ \text{crc}_{\text{std}} = (\text{crc}_{\text{lo}} \ll 8) \;\oplus\; T_0[\text{crc}_{\text{hi}}] \;\oplus\; T_0[\text{input}] $$

FSoE kodu $T_0[\text{input}]$‘u $T_3[\text{input}]$ ile değiştirir. CRC aritmetiği GF(2) üzerinde doğrusal olduğundan:

  • $T_0[\text{input}] = (\text{input} \cdot x^{16}) \bmod P$ — giriş baytı doğal konumunda
  • $T_3[\text{input}] = (\text{input} \cdot x^{40}) \bmod P$ — giriş baytı 24 bit (3 bayt) daha yüksek kaydırılmış

Böylece her giriş baytı, standart CRC’ye kıyasla ek bir $x^{24}$ çarpanı alır. $\text{startCrc}$ başlangıç değerinden (ki bu kendisi ilk 2 bayt olarak katılır) $n$ bayt $b_0 \dots b_{n-1}$ işledikten sonra:

$$ \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 $$

standart olan ise:

$$ \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 $$

Tek fark her veri baytı için $x^{40}$ ile $x^{16}$ arasındadır — bayt başına ek bir $x^{24}$. Eşdeğer olarak, FSoE CRC’si, mesajın her baytı 3 sıfır baytla takip ediliyormuş gibi CRC’sidir, ancak başlangıç değeri yalnızca gerçek bayt sayısı kadar (doldurulmuş sayı kadar değil) kaydırılır. Bu, FSoE’ye özgü özel bir varyanttır ve hiçbir standart doldurulmuş-CRC kuralına eşdeğer değildir.

Bayt işleme sırası

CRC her yinelemenin başında 0’a sıfırlanır, ardından baytlar bu sırayla işlenir:

example.txt
oldCRC-Lo, oldCRC-Hi, ConnID-Lo, ConnID-Hi, SeqNo-Lo, SeqNo-Hi, Command, [Data...]
  • oldCRC, startCrc parametresinden gelir (önceki çerçevenin CRC’si — aşağıda Kalıtım’a bakın)
  • ConnID, PDU’nun sonundan okunur: au8Data[size-2] (Lo) ve au8Data[size-1] (Hi)
  • SeqNo, seqNo işaretçisinden gelir
  • Command, au8Data[OFFS_COMMAND] konumundadır
  • Data, au8Data[OFFS_DATA] konumundan başlar

CRC kalıtımı — iki seviye

Seviye 1: Çerçeveler arası kalıtım (startCrc / oldCRC zinciri)

Her CRC hesaplamasına katılan ilk iki bayt önceki çerçevenin CRC’sidir (startCrc). Bu, çerçeveler arasında bir zincir oluşturur:

$$ \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} $$

Her çerçevenin CRC’si tüm önceki çerçevelerin CRC’lerine bağlıdır. Bir saldırgan eski bir çerçeveyi tekrar oynatamaz çünkü CRC zinciri kırılır.

oldCrc parametresi do…while döngüsünde çakışma tespiti için kullanılır:

collision-avoidance.c
} while (crc == oldCrc && (bRcvDir & NEW_CRC) != 0);

Yeni hesaplanan CRC tesadüfen önceki çerçevenin CRC’sine eşitse, sıra numarası artırılır (seqNo[0]++, sıfır atlanır) ve CRC yeniden hesaplanır. Bu, CRC oldCrc‘den farklı olana kadar devam eder ve ardışık iki çerçevenin asla aynı CRC’yi paylaşmamasını sağlar — bu, CRC çakışmalarını tespit edilebilir kılan bir güvenlik gereksinimidir.

Seviye 2: Çerçeve içi kalıtım (crc_common tabanı)

Tek bir PDU içinde, yük 10 baytı aştığında, birden fazla CRC (CRC₀, CRC₁, CRC₂, …) gömülür — her biri 2 baytlık bir veri segmentini korur. Anahtar mekanizma crc_common‘dur.

Adım 1 — Ortak tabanı hesapla (7 bayt işlenir):

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

Bu taban tüm “başlık” alanlarını kapsar: kalıtılan eski CRC, Bağlantı Kimliği, Sıra Numarası ve Komut baytı.

Adım 2 — CRC₀ (ilk segment, crc_common‘dan devam ederek):

crc0.c
/* crc_common'dan devam: Data[0], Data[1] işle */
/* sonuç = CRC₀  (pCrc'de saklanır/doğrulanır) */

Adım 3 — i ≥ 1 için CRCᵢ (sonraki segmentler, her biri crc_common‘dan yeniden başlayarak):

crci.c
crc = crc_common;           /* paylaşılan tabandan YENİDEN BAŞLAT */
/* process: Index-Lo (i & 0xFF), Index-Hi (i >> 8),
            Data[2i], Data[2i+1] */
/* sonuç = CRCᵢ  (sonraki pCrc konumunda saklanır/doğrulanır) */

Döngü yapısı:

multi-crc-loop.c
while (size >= 4) {         /* her yineleme = 2 veri baytı + 2 CRC baytı */
    crc = crc_common;       /* paylaşılan tabandan yeniden başlat */
    /* kat: i_lo, i_hi, data[2i], data[2i+1] */
    /* pCrc[0..1] konumunda CRC doğrula/ekle */
    size -= 4;
    pSafeData += 4;
    pCrc += 4;
    i++;
}

İndeks baytı neden önemlidir

Her segment CRCᵢ, Command ile Data arasına indeks $i$‘yi (1-tabanlı, 2 baytlık little-endian değer olarak) dahil eder. Bu, iki segment özdeş veri baytları içerse bile, indeks farklı olduğundan CRC’lerinin farklı olmasını sağlar. İndeks olmadan, iki veri segmentinin yer değiştirmesi tespit edilmeden geçerdi.

Görselleştirme

Aşağıdaki diyagram, CRC kalıtımının çerçeveler arasında ve tek bir multi-CRC PDU içinde nasıl çalıştığını gösterir. Her segment CRC’si crc_common‘dan (paylaşılan taban) yeniden başlar ve her çerçevenin CRC’si bir sonraki çerçeve için startCrc olur.

Ortak taban (crc_common)
Veri baytı
CRC (eklenen/doğrulanan)
İndeks
ConnID
Çerçeveler arası kalıtım: her çerçevenin CRC₀'ı bir sonraki çerçevenin startCrc'si (oldCRC) olur
Çerçeve N−1
CRC₀: oC.Lo oC.Hi CID.Lo CID.Hi Sq.Lo Sq.Hi Cmd D0 D1
CRC₀ = 0xDD27 (örnek)
Çerçeve N (startCrc = 0xDD27)
crc_common (tüm segmentler için paylaşılan taban)
0x27 0xDD CID.Lo CID.Hi Sq.Lo Sq.Hi Cmd
CRC₀: (ortaktan) D0 D1 CRC₀
CRC₁: (ortaktan yeniden) i.Lo i.Hi D2 D3 CRC₁
CRC₂: (ortaktan yeniden) i.Lo i.Hi D4 D5 CRC₂
Çerçeve N+1 (startCrc = N'nin CRC₀'ı)
CRC₀: N.Lo N.Hi CID.Lo CID.Hi Sq.Lo Sq.Hi Cmd D0 D1
CRC₀ Çerçeve N+2'yi besler…
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 )  /* kaydedildi, iletilmedi */
nextFrame.startCrc = thisFrame.CRC₀
Sol: Çerçeve N−1, CRC₀ = 0xDD27 üretir. Orta: Çerçeve N, 0xDD27'yi startCrc olarak kullanır — her CRC hesaplamasına katılan ilk iki bayt. Ortak taban (mavi) bir kez hesaplanır ve her segment için yeniden kullanılır. Her segment CRC'si bu tabandan yeniden başlar, indeksini ve veri baytlarını ekler ve bağımsız bir CRC üretir. Sağ: Çerçeve N'nin CRC₀'ı Çerçeve N+1 için startCrc olur ve zincir devam eder.

PDU düzeni

PDU bayt düzeni (CalcCrc‘deki alan erişimlerinden yeniden yapılandırılmıştır) toplam boyuta bağlıdır:

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 her zaman son 2 bayttadır (au8Data[size-2..size-1])
  • CRC₀ ofset 2’de (size ≤ 6) veya ofset 3’te (size > 6)
  • Her ek segment 4 bayttır: 2 veri + 2 CRC
  • size -= 7 ilk parçayı hesaba katar (ConnID 2 + CRC₀ 2 + Command 1 + Data 2), ardından her ek segment için size -= 4

Referans CRC değerleri

Aşağıdaki referans değerler, orijinal CalcCrc fonksiyonunun sadık bir transkripsiyonu tarafından üretilmiş ve bit-by-bit polinom uzun bölme kullanılarak bağımsız olarak doğrulanmıştır (arama tablosu yok). Aksi belirtilmedikçe tüm girişler startCrc = 0x0000, seqNo = 0x0001, oldCrc = 0x0000 kullanır.

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

Test 6b, çerçeveler arası kalıtımı gösterir: Test 2’den CRC₀’ı (0xDD27) startCrc olarak kullanır ve aynı veri için farklı bir CRC (0x41F5) üretir — bu, yük özdeş olsa bile kalıtım zincirinin CRC’yi nasıl değiştirdiğini gösterir.

Özet

FSoE CRC mekanizmasının iki kalıtım katmanı vardır:

  1. Çerçeveler arası: Önceki çerçevenin CRC₀’ı bir sonraki çerçevenin CRC’sine ilk iki bayt olarak katılır, tekrar oynatma saldırılarını önleyen bir zincir oluşturur. Bir do…while döngüsü, ardışık iki çerçevenin aynı CRC’yi paylaşmamasını sağlar.

  2. Çerçeve içi: Multi-segment PDU’lar için, paylaşılan bir crc_common tabanı (oldCRC, ConnID, SeqNo, Command içeren) bir kez hesaplanır ve her segment CRC’si için başlangıç noktası olarak yeniden kullanılır. Her segment indeksini ve 2 veri baytını ekler, segment yeniden sıralanmasına karşı koruyan bağımsız bir CRC üretir.

Güncelleme adımının kendisi standart bir MSB-first CRC-16 yazmaç kaydırmasıdır, ancak giriş baytı $T_0$ ($x^{16}$ tablosu) yerine $T_3$ ($x^{40}$ tablosu) üzerinden katılır ve her veri baytına ek bir $x^{24}$ çarpanı verir. Bu, FSoE CRC’sini özel bir varyant yapar ve hiçbir standart CRC-16 kuralına eşdeğer değildir.

İlgili yazılar

Genel bakış ve temeller

Duruma göre PDU yapıları

CRC

Hata kodları ve veri formatı


Check out similar posts by category: Functional Safety EtherCAT CRC