From 91199e146e70a05cfc536ca3331d9627da473215 Mon Sep 17 00:00:00 2001 From: goebish Date: Sun, 25 Oct 2026 16:57:07 +0100 Subject: [PATCH] Compute xn297 whitening instead of using an hardcoded array (#1201) * Compute xn297 whitening instead of using an hardcoded array * Add function to scramble byte for XN297 interface * Refactor XN297 scrambling logic to use a global LFSR and improve byte generation * Add technical specification and implementation details for XN297 whitening and CRC --- Multiprotocol/XN297Dump_nrf24l01.ino | 6 +- Multiprotocol/XN297_EMU.ino | 187 +++++----- Multiprotocol/iface_xn297.h | 6 + docs/XN297_Whitening_and_CRC.md | 496 +++++++++++++++++++++++++++ 4 files changed, 587 insertions(+), 108 deletions(-) create mode 100644 docs/XN297_Whitening_and_CRC.md diff --git a/Multiprotocol/XN297Dump_nrf24l01.ino b/Multiprotocol/XN297Dump_nrf24l01.ino index 4fa32a5d..b092c40e 100644 --- a/Multiprotocol/XN297Dump_nrf24l01.ino +++ b/Multiprotocol/XN297Dump_nrf24l01.ino @@ -70,12 +70,14 @@ static void __attribute__((unused)) XN297Dump_RF_init() } } -extern const uint8_t xn297_scramble[]; - static boolean __attribute__((unused)) XN297Dump_process_packet(void) { uint16_t crcxored; uint8_t packet_sc[XN297DUMP_MAX_PACKET_LEN], packet_un[XN297DUMP_MAX_PACKET_LEN]; + uint8_t xn297_scramble[XN297DUMP_MAX_PACKET_LEN]; + xn297_lfsr = XN297_SCRAMBLE_SEED; + for (uint8_t i = 0; i < XN297DUMP_MAX_PACKET_LEN; i++) + xn297_scramble[i] = XN297_scramble_byte(); enhanced=false; // init crc crc = 0xb5d2; diff --git a/Multiprotocol/XN297_EMU.ino b/Multiprotocol/XN297_EMU.ino index 56a7085a..7afa0499 100644 --- a/Multiprotocol/XN297_EMU.ino +++ b/Multiprotocol/XN297_EMU.ino @@ -6,22 +6,49 @@ bool xn297_scramble_enabled, xn297_crc, xn297_bitrate, xn297_rf; uint8_t xn297_addr_len, xn297_rx_packet_len; uint8_t xn297_tx_addr[5], xn297_rx_addr[5]; -// xn297 address / pcf / payload scramble table -const uint8_t xn297_scramble[] = { - 0xE3, 0xB1, 0x4B, 0xEA, 0x85, 0xBC, 0xE5, 0x66, - 0x0D, 0xAE, 0x8C, 0x88, 0x12, 0x69, 0xEE, 0x1F, - 0xC7, 0x62, 0x97, 0xD5, 0x0B, 0x79, 0xCA, 0xCC, - 0x1B, 0x5D, 0x19, 0x10, 0x24, 0xD3, 0xDC, 0x3F, - 0x8E, 0xC5, 0x2F, 0xAA, 0x16, 0xF3, 0x95 }; +// XN297 whitening: x^7 + x^3 + 1, seed 0x71, MSB first (E3 B1 4B EA 85 ...). +// Generated a byte at a time with (x^7 + x^3 + 1)^4 = x^28 + x^12 + 1: s[i] = s[i-28] ^ s[i-12]. +// xn297_lfsr holds the next 28 bits in bits 31..4, 0 when scrambling is off. +uint32_t xn297_lfsr; + +static uint8_t __attribute__((unused)) XN297_scramble_byte() +{ + uint8_t s = xn297_lfsr >> 24; + xn297_lfsr = (xn297_lfsr << 8) | (uint8_t)(s ^ (xn297_lfsr >> 8)) << 4; + return s; +} + +static void __attribute__((unused)) XN297_scramble_init() +{ + xn297_lfsr = xn297_scramble_enabled ? XN297_SCRAMBLE_SEED : 0; +} // XN297 CRC: CRC-16/CCITT (init 0xFFFF, xorout 0xFFFF) over the unscrambled -// address, pcf and payload, sent scrambled. The emulation computes it from 0xB5D2 -// over the bits as sent: the xorout is the difference for a CRC over 'bits' bits, -// the CRC of the scramble bits plus the scramble of the CRC bits. +// address, pcf and payload, sent scrambled: XORed with the scramble bits that follow, +// (xn297_lfsr >> 16) once the last byte has been scrambled. +// Returns the scrambled byte, crc updated with the unscrambled one. +static uint8_t __attribute__((unused)) XN297_crc_scramble(uint8_t b) +{ + crc16_update(b, 8); + return b ^ XN297_scramble_byte(); +} + +// Returns the unscrambled byte, crc updated with it. +static uint8_t __attribute__((unused)) XN297_unscramble_crc(uint8_t b) +{ + b ^= XN297_scramble_byte(); + crc16_update(b, 8); + return b; +} + +#ifdef XN297DUMP_NRF24L01_INO +// XN297Dump computes the CRC from 0xB5D2 over the bits as sent: the xorout is the +// difference for a CRC over 'bits' bits, the CRC of the scramble bits plus the +// scramble of the CRC bits. static uint16_t __attribute__((unused)) XN297_crc_xorout(bool scramble, uint16_t bits) { uint16_t xorout = 0xFFFF ^ 0xB5D2; - uint8_t lfsr = 0x71, s; // scramble bits (xn297_scramble[]): 1110001, then s[i] = s[i-7] ^ s[i-3] + uint8_t lfsr = 0x71, s; // scramble bits: 1110001, then s[i] = s[i-7] ^ s[i-3] for(uint16_t i = 0; i < bits + 16; i++) { s = scramble ? lfsr >> 6 : 0; @@ -38,6 +65,7 @@ static uint16_t __attribute__((unused)) XN297_crc_xorout(bool scramble, uint16_t } return ~xorout; } +#endif static bool __attribute__((unused)) XN297_Configure(bool crc_en, bool scramble_en, bool bitrate, bool force_nrf) { @@ -103,13 +131,10 @@ static void __attribute__((unused)) XN297_SetTXAddr(const uint8_t* addr, uint8_t static void __attribute__((unused)) XN297_SetRXAddr(const uint8_t* addr, uint8_t rx_packet_len) { - //Scramble address - for (uint8_t i = 0; i < xn297_addr_len; ++i) - { - xn297_rx_addr[i] = addr[i]; - if(xn297_scramble_enabled) - xn297_rx_addr[i] ^= xn297_scramble[xn297_addr_len-i-1]; - } + //Scramble address in transmission order + XN297_scramble_init(); + for (uint8_t i = xn297_addr_len; i--; ) + xn297_rx_addr[i] = addr[i] ^ XN297_scramble_byte(); if(xn297_crc) rx_packet_len += 2; // Include CRC @@ -257,32 +282,19 @@ static void __attribute__((unused)) XN297_WritePayload(uint8_t* msg, uint8_t len } // address + crc = 0xFFFF; + XN297_scramble_init(); for (uint8_t i = 0; i < xn297_addr_len; ++i) - { - buf[last] = xn297_tx_addr[xn297_addr_len-i-1]; - if(xn297_scramble_enabled) - buf[last] ^= xn297_scramble[i]; - last++; - } + buf[last++] = XN297_crc_scramble(xn297_tx_addr[xn297_addr_len-i-1]); // payload for (uint8_t i = 0; i < len; ++i) - { - // bit-reverse bytes in packet - buf[last] = bit_reverse(msg[i]); - if(xn297_scramble_enabled) - buf[last] ^= xn297_scramble[xn297_addr_len+i]; - last++; - } + buf[last++] = XN297_crc_scramble(bit_reverse(msg[i])); // bit-reverse bytes in packet // crc if (xn297_crc) { - uint8_t offset = (xn297_addr_len < 4 && xn297_rf == XN297_NRF) ? 1 : 0; - crc = 0xb5d2; - for (uint8_t i = offset; i < last; ++i) - crc16_update( buf[i], 8); - crc ^= XN297_crc_xorout(xn297_scramble_enabled, 8 * (xn297_addr_len + len)); + crc = ~crc ^ (xn297_lfsr >> 16); buf[last++] = crc >> 8; buf[last++] = crc & 0xff; } @@ -294,7 +306,6 @@ static void __attribute__((unused)) XN297_WritePayload(uint8_t* msg, uint8_t len static void __attribute__((unused)) XN297_WriteEnhancedPayload(uint8_t* msg, uint8_t len, uint8_t noack) { uint8_t buf[32]; - uint8_t scramble_index=0; uint8_t last = 0; static uint8_t pid=0; @@ -304,54 +315,37 @@ static void __attribute__((unused)) XN297_WriteEnhancedPayload(uint8_t* msg, uin } // address + crc = xn297_scramble_enabled ? 0xFFFF : 0xB5D2; // unscrambled: no xorout + XN297_scramble_init(); for (uint8_t i = 0; i < xn297_addr_len; ++i) - { - buf[last] = xn297_tx_addr[xn297_addr_len-i-1]; - if(xn297_scramble_enabled) - buf[last] ^= xn297_scramble[scramble_index++]; - last++; - } + buf[last++] = XN297_crc_scramble(xn297_tx_addr[xn297_addr_len-i-1]); // pcf - buf[last] = (len << 1) | (pid>>1); - if(xn297_scramble_enabled) - buf[last] ^= xn297_scramble[scramble_index++]; - last++; + buf[last++] = XN297_crc_scramble((len << 1) | (pid>>1)); buf[last] = (pid << 7) | (noack << 6); // payload if(len) { - buf[last]|= bit_reverse(msg[0]) >> 2; // first 6 bit of payload - if(xn297_scramble_enabled) - buf[last] ^= xn297_scramble[scramble_index++]; + buf[last] = XN297_crc_scramble(buf[last] | (bit_reverse(msg[0]) >> 2)); // first 6 bit of payload for (uint8_t i = 0; i < len-1; ++i) { last++; - buf[last] = (bit_reverse(msg[i]) << 6) | (bit_reverse(msg[i+1]) >> 2); - if(xn297_scramble_enabled) - buf[last] ^= xn297_scramble[scramble_index++]; + buf[last] = XN297_crc_scramble((bit_reverse(msg[i]) << 6) | (bit_reverse(msg[i+1]) >> 2)); } last++; buf[last] = bit_reverse(msg[len-1]) << 6; // last 2 bit of payload } - if(xn297_scramble_enabled) - buf[last] ^= xn297_scramble[scramble_index++] & 0xc0; + crc16_update( buf[last], 2); + buf[last] ^= (xn297_lfsr >> 24) & 0xc0; // crc if (xn297_crc) { - uint8_t offset = (xn297_addr_len < 4 && xn297_rf == XN297_NRF) ? 1 : 0; - crc = 0xb5d2; - for (uint8_t i = offset; i < last; ++i) - crc16_update( buf[i], 8); - crc16_update( buf[last] & 0xc0, 2); if (xn297_scramble_enabled) - crc ^= XN297_crc_xorout(true, 8 * (xn297_addr_len + len) + 10); - //else - // crc ^= XN297_crc_xorout(false, 8 * (xn297_addr_len + len) + 10); + crc = ~crc ^ (xn297_lfsr >> 14); buf[last++] |= (crc >> 8) >> 2; buf[last++] = ((crc >> 8) << 6) | ((crc & 0xff) >> 2); @@ -408,28 +402,18 @@ static bool __attribute__((unused)) XN297_ReadPayload(uint8_t* msg, uint8_t len) // Read payload XN297_ReceivePayload(buf, len); - // Decode payload + // Decode payload, calculate CRC + crc = 0xFFFF; + XN297_scramble_init(); + for (uint8_t i = 0; i < xn297_addr_len; ++i) + XN297_unscramble_crc(xn297_rx_addr[xn297_addr_len-i-1]); for(uint8_t i=0; i> 16); //test if( (crc >> 8) == buf[len] && (crc & 0xff) == buf[len+1]) return true; // CRC OK @@ -443,40 +427,31 @@ static uint8_t __attribute__((unused)) XN297_ReadEnhancedPayload(uint8_t* msg, u // Read payload XN297_ReceivePayload(buffer, len+2); // Read pcf + payload + CRC - // Decode payload - pcf_size = buffer[0]; - if(xn297_scramble_enabled) - pcf_size ^= xn297_scramble[xn297_addr_len]; - pcf_size = pcf_size >> 1; + // Decode payload, calculate CRC +#ifdef XN297DUMP_NRF24L01_INO + bool xorout = true; +#else + bool xorout = xn297_scramble_enabled; // unscrambled: no xorout +#endif + crc = xorout ? 0xFFFF : 0xB5D2; + XN297_scramble_init(); + for (uint8_t i = 0; i < xn297_addr_len; ++i) + XN297_unscramble_crc(xn297_rx_addr[xn297_addr_len-i-1]); + pcf_size = XN297_unscramble_crc(buffer[0]) >> 1; if(pcf_size>32) return 255; // Error for(uint8_t i=0; i< pcf_size; i++) - { - msg[i] = bit_reverse((buffer[i+1] << 2) | (buffer[i+2] >> 6)); - if(xn297_scramble_enabled) - msg[i] ^= bit_reverse((xn297_scramble[xn297_addr_len+i+1] << 2) | - (xn297_scramble[xn297_addr_len+i+2] >> 6)); + { // payload is shifted by 2 bits: unscramble with bits 29..22 of the next scramble bits + msg[i] = bit_reverse(((buffer[i+1] << 2) | (buffer[i+2] >> 6)) ^ (xn297_lfsr >> 22)); + XN297_unscramble_crc(buffer[i+1]); } + crc16_update( (buffer[pcf_size+1] ^ (xn297_lfsr >> 24)) & 0xc0, 2); if (!xn297_crc) return pcf_size; // No CRC so OK by default... - // Calculate CRC - crc = 0xb5d2; - //process address - for (uint8_t i = 0; i < xn297_addr_len; ++i) - crc16_update( xn297_rx_addr[xn297_addr_len-i-1], 8); - //process payload - for (uint8_t i = 0; i < pcf_size+1; ++i) - crc16_update( buffer[i], 8); - crc16_update( buffer[pcf_size+1] & 0xc0, 2); - //xorout - if (xn297_scramble_enabled) - crc ^= XN297_crc_xorout(true, 8 * (xn297_addr_len + pcf_size) + 10); -#ifdef XN297DUMP_NRF24L01_INO - else - crc ^= XN297_crc_xorout(false, 8 * (xn297_addr_len + pcf_size) + 10); -#endif + if (xorout) + crc = ~crc ^ (xn297_lfsr >> 14); uint16_t crcxored=(buffer[pcf_size+1]<<10)|(buffer[pcf_size+2]<<2)|(buffer[pcf_size+3]>>6) ; if( crc == crcxored) return pcf_size; // CRC OK diff --git a/Multiprotocol/iface_xn297.h b/Multiprotocol/iface_xn297.h index 653252bd..afcf4e37 100644 --- a/Multiprotocol/iface_xn297.h +++ b/Multiprotocol/iface_xn297.h @@ -20,6 +20,9 @@ #define XN297_250K true #define XN297_NRF false #define XN297_CC2500 true +#define XN297_SCRAMBLE_SEED 0xE3B14BE0 // whitening x^7 + x^3 + 1, seed 0x71: first 28 bits + +extern uint32_t xn297_lfsr; ////////////// // Functions @@ -41,7 +44,10 @@ static void __attribute__((unused)) XN297_RFChannel(uint8_t); static void __attribute__((unused)) XN297_SetPower(); static void __attribute__((unused)) XN297_SetFreqOffset(); static bool __attribute__((unused)) XN297_IsPacketSent(); +static uint8_t __attribute__((unused)) XN297_scramble_byte(); +#ifdef XN297DUMP_NRF24L01_INO static uint16_t __attribute__((unused)) XN297_crc_xorout(bool, uint16_t); +#endif #endif diff --git a/docs/XN297_Whitening_and_CRC.md b/docs/XN297_Whitening_and_CRC.md new file mode 100644 index 00000000..7b03ff94 --- /dev/null +++ b/docs/XN297_Whitening_and_CRC.md @@ -0,0 +1,496 @@ +# XN297 Whitening and CRC: Technical Specification and Implementation + +**Scope:** XN297 packet whitening and 16-bit CRC as implemented and reconstructed in the DIY Multiprotocol TX Module's XN297 emulation, including standard and enhanced packet formats, historical CRC correction tables, and reference test vectors. + +## 1. Conventions + +- Bits within each transmitted byte are processed **most significant bit first**. +- `^` or `⊕` denotes bitwise XOR; `~` denotes bitwise complement. +- `R8(x)` denotes reversal of the eight bits in byte `x`. +- The address length `A` is 3, 4, or 5 bytes; payload length `P` is in bytes. +- `M` denotes the **unwhitened sequence of bits in transmission order** that is covered by the CRC. +- `S[i]` denotes bit `i` of the whitening sequence, starting at the first transmitted address bit (`i = 0`). +- `S[a:b]` denotes the bits at positions `a` through `b-1`, interpreted MSB first when converted to an integer. +- `CRC(I, M)` denotes the uncomplemented CRC-16 register after processing bitstream `M` from initial register value `I`. + +The synchronization/preamble sequence precedes the address. It is separate from `M` and from the whitening sequence indexed here. For the XN297 emulation, the 28-bit synchronization pattern is `0xC710F55`; its representation in the underlying radio interface depends on the transceiver used. + +## 2. Packet-field ordering + +### 2.1 Standard packets + +The CRC-covered, unwhitened bitstream is: + +```text +address bytes in reverse API order | payload bytes with bits reversed +``` + +For API address `01 02 03 04 05` and payload `01 02 03 04 05`, the CRC input bytes are: + +```text +05 04 03 02 01 80 40 C0 20 A0 +``` + +The address bytes are reversed in **byte order**. The payload bytes are individually reversed in **bit order**. The address bytes are not individually bit-reversed in this representation. + +With whitening enabled, the address and payload bits are XORed with consecutive whitening bits. The CRC is then XORed with the **next 16 whitening bits**. Whitening is continuous across the address, packet-control field when present, payload, and CRC; it is not restarted at field boundaries. + +The standard CRC input length is: + +```text +n = 8 * (A + P) +``` + +### 2.2 Enhanced packets + +Enhanced packets include a packet-control region between the address and payload. The implementation serializes this region as 10 bits: + +```text +0 | payload_length[5:0] | PID[1:0] | NO_ACK +``` + +The payload then follows as a contiguous stream of bit-reversed payload bytes, without byte alignment after the 10-bit packet-control region. + +The enhanced CRC input length is: + +```text +n = 8 * (A + P) + 10 +``` + +Consequently, the CRC begins **two bits into a byte**. Its 16 bits span parts of three transmitted bytes. The final packed byte has six unused low-order bits, set to zero by the implementation. + +## 3. Whitening generator + +### 3.1 Parameters + +| Property | Value | +|---|---| +| Generator | 7-bit linear-feedback shift register (LFSR) | +| Connection polynomial | `x^7 + x^3 + 1` | +| Initial register value | `0x71` | +| Output bit | Register bit 6 (MSB) | +| Feedback bit | Register bit 6 XOR register bit 2 | +| Register shift | Left by one; feedback enters bit 0 | +| Output-byte assembly | MSB first, eight consecutive output bits | + +For register state `r`, one whitening bit and the next state are: + +```text +output = (r >> 6) & 1 +r_next = ((r << 1) | (((r >> 6) ^ (r >> 2)) & 1)) & 0x7F +``` + +The initial seven output bits are `1110001`, corresponding to the seed `0x71`. + +### 3.2 Bit-level reference implementation + +```c +#include + +static uint8_t xn297_whitening_bit(uint8_t *lfsr) +{ + uint8_t output = (*lfsr >> 6) & 1u; + uint8_t feedback = ((*lfsr >> 6) ^ (*lfsr >> 2)) & 1u; + *lfsr = (uint8_t)(((*lfsr << 1) | feedback) & 0x7Fu); + return output; +} + +static uint8_t xn297_whitening_byte(uint8_t *lfsr) +{ + uint8_t value = 0; + for (unsigned i = 0; i < 8; ++i) + value = (uint8_t)((value << 1) | xn297_whitening_bit(lfsr)); + return value; +} +``` + +Initialize `uint8_t lfsr = 0x71;` at the start of the packet's address field. + +### 3.3 Whitening reference bytes + +The first 39 output bytes are: + +```text +E3 B1 4B EA 85 BC E5 66 +0D AE 8C 88 12 69 EE 1F +C7 62 97 D5 0B 79 CA CC +1B 5D 19 10 24 D3 DC 3F +8E C5 2F AA 16 F3 95 +``` + +These reproduce the historical `xn297_scramble[]` lookup table. The LFSR generates additional whitening bits beyond the end of that table, including the bits needed for enhanced-packet CRCs. + +### 3.4 Eight-bit-at-a-time generator + +Let `s[i]` denote the individual whitening bits. The seven-bit recurrence is: + +```text +s[i] = s[i-7] ^ s[i-3] for i >= 7 +``` + +Over GF(2), raising the connection polynomial to the fourth power gives: + +```text +(x^7 + x^3 + 1)^4 = x^28 + x^12 + 1 +``` + +Therefore the same sequence also satisfies: + +```text +s[i] = s[i-28] ^ s[i-12] for i >= 28 +``` + +This permits an eight-bit-at-a-time generator using a 32-bit word containing the **next 28 whitening bits in bits 31..4**. Its initial value is the first 28 stream bits followed by four zero bits: + +```text +0xE3B14BE0 +``` + +```c +#include + +static uint8_t xn297_next_byte_optimized(uint32_t *state) +{ + uint8_t output = (uint8_t)(*state >> 24); + *state = (*state << 8) | + ((uint32_t)(uint8_t)(output ^ (*state >> 8)) << 4); + return output; +} +``` + +Initialize `uint32_t state = 0xE3B14BE0u;` when whitening is enabled, or `state = 0;` when whitening is disabled. The zero state makes the same XOR-based packing operations emit unmodified data. + +At an aligned byte boundary, after all CRC-input bytes have been processed, the next 16 whitening bits are: + +```c +uint16_t crc_whitening = (uint16_t)(state >> 16); +``` + +For an enhanced packet with two additional CRC-input bits after the last full byte, the CRC whitening mask is: + +```c +uint16_t crc_whitening = (uint16_t)(state >> 14); +``` + +The second expression skips the two whitening bits consumed by the final partial data byte. + +## 4. CRC algorithm + +### 4.1 CRC parameters + +The packet-level CRC model for standard packets and scrambled enhanced packets is: + +| Property | Value | +|---|---| +| Width | 16 bits | +| Polynomial | `0x1021` (`x^16 + x^12 + x^5 + 1`) | +| Initial register | `0xFFFF` | +| Input bit order | MSB first | +| Reflected input | No | +| Reflected output | No | +| Final XOR | `0xFFFF` | +| Transmitted CRC bit order | MSB first | +| CRC whitening | XOR with the next 16 whitening bits when enabled | + +These parameters correspond to the CRC-16/GENIBUS parameterization of the CCITT polynomial. They are distinct from CRC-16/CCITT-FALSE, which uses a final XOR of zero. + +For one input bit `b` and current register `c`: + +```text +feedback = ((c >> 15) ^ b) & 1 +c = (c << 1) & 0xFFFF +if feedback: c ^= 0x1021 +``` + +A reference function is: + +```c +static uint16_t crc1021_bit(uint16_t crc, uint8_t bit) +{ + uint8_t feedback = (uint8_t)(((crc >> 15) ^ bit) & 1u); + crc = (uint16_t)(crc << 1); + if (feedback) + crc ^= 0x1021u; + return crc; +} +``` + +### 4.2 CRC over unwhitened data + +For a CRC input bitstream `M` of length `n`, define: + +```text +C_raw = CRC(0xFFFF, M) +C_final = C_raw ^ 0xFFFF +``` + +With whitening enabled, the transmitted CRC is: + +```text +C_tx = C_final ^ S[n:n+16] +``` + +With whitening disabled, the standard-packet transmitted CRC is simply `C_final`. + +The whitening applied to the CRC uses whitening bits at **bit offsets `n` through `n+15`**, not the first 16 bits of the whitening sequence. In standard packets those bits are byte-aligned; in enhanced packets they begin two bits into a byte. + +### 4.3 Processing order + +For standard packets, the encoder performs the following operations in order: + +1. Initialize the CRC register to `0xFFFF` and the whitening generator at the beginning of the address. +2. Feed each unwhitened address byte into the CRC in transmitted byte order; XOR the same byte with its whitening byte for transmission. +3. Feed each bit-reversed payload byte into the CRC; XOR it with the next whitening byte for transmission. +4. Complement the CRC register and XOR it with the next 16 whitening bits. +5. Append the resulting CRC most-significant byte first. + +The corresponding decoder XORs each received data byte with the same whitening stream **before** feeding it into the CRC. It then compares the received CRC with the complemented calculated CRC XORed with the 16 CRC whitening bits. + +For enhanced packets, the same process applies to the extra packet-control bits and the non-byte-aligned end of the payload. + +## 5. Historical `0xB5D2` CRC model and XOROUT correction + +Earlier XN297 emulation computed the CRC directly over the **already whitened, on-air data bits**, using initial register `0xB5D2`. It then XORed the CRC result with a correction value that depended on the packet length and mode. + +Let: + +```text +I_old = 0xB5D2 +I_new = 0xFFFF +D = I_old ^ I_new = 0x4A2D +``` + +With whitening enabled, the on-air data bitstream is `M ^ S[0:n]`. The old intermediate value is: + +```text +C_old = CRC(0xB5D2, M ^ S[0:n]) +``` + +The transmitted CRC defined in section 4 is: + +```text +C_tx = CRC(0xFFFF, M) ^ 0xFFFF ^ S[n:n+16] +``` + +Because an uncomplemented CRC register transition is linear over GF(2), their difference is independent of the contents of `M`: + +```text +XOROUT(n, scrambled) + = CRC(0x4A2D, S[0:n]) ^ 0xFFFF ^ S[n:n+16] +``` + +The old calculation is therefore equivalent to: + +```text +C_tx = C_old ^ XOROUT(n, scrambled) +``` + +When whitening is disabled, substitute zero for every whitening bit: + +```text +XOROUT(n, unscrambled) + = CRC(0x4A2D, n zero bits) ^ 0xFFFF +``` + +This explains why a correction indexed by packet length can compensate for both the different CRC initialization and the use of whitened rather than unwhitened CRC input. + +### 5.1 Reference correction function + +```c +#include +#include +#include + +static uint16_t xn297_crc_xorout(bool scrambled, size_t nbits) +{ + uint16_t correction = 0xFFFFu ^ 0xB5D2u; + uint8_t lfsr = 0x71u; + + for (size_t i = 0; i < nbits; ++i) { + uint8_t mask_bit = xn297_whitening_bit(&lfsr); + correction = crc1021_bit(correction, + scrambled ? mask_bit : 0u); + } + + uint16_t crc_mask = 0; + for (unsigned i = 0; i < 16; ++i) { + uint8_t mask_bit = xn297_whitening_bit(&lfsr); + crc_mask = (uint16_t)((crc_mask << 1) | + (scrambled ? mask_bit : 0u)); + } + + return (uint16_t)(correction ^ 0xFFFFu ^ crc_mask); +} +``` + +This function uses `xn297_whitening_bit()` and `crc1021_bit()` from sections 3 and 4. + +### 5.2 Old lookup-table indexing + +Both standard and enhanced correction tables used: + +```text +index = A - 3 + P +``` + +The bit count supplied to the computed replacement is: + +| Packet type | CRC-covered bits | +|---|---| +| Standard | `8 * (A + P)` | +| Enhanced | `8 * (A + P) + 10` | + +The original source contained four correction tables: standard scrambled, standard unscrambled, enhanced scrambled, and enhanced unscrambled. Entry zero in each table was a placeholder. + +The computed formula matches **135 of the 136 non-placeholder historical entries**. The sole discrepancy is the standard scrambled entry for `A = 3`, `P = 1`: + +```text +Historical table: 0x3448 +Computed value: 0xE348 +``` + +The new correction implementation uses `0xE348`. The remaining historical entries follow the same formula. + +### 5.3 Enhanced unscrambled compatibility behavior + +The multiprotocol firmware's enhanced-packet **transmit** path retains a distinct legacy convention when scrambling is disabled: + +```text +CRC init = 0xB5D2 +Final XOR = 0x0000 +CRC whitening = disabled +``` + +That path therefore transmits `CRC(0xB5D2, M)` without applying the general `0xFFFF`-initialized, `0xFFFF`-complemented model. This is an implementation-specific distinction and must be preserved when reproducing that firmware behavior. + +The XN297Dump analyzer also contains a computed correction path for enhanced unscrambled packets using the general `XOROUT(n, unscrambled)` formula. The analyzer and transmit-path conventions should not be conflated. + +## 6. Enhanced-packet CRC alignment + +An enhanced packet's unwhitened CRC input consists of: + +```text +reversed-order address bytes ++ 10-bit packet-control region ++ bit-reversed payload bytes +``` + +With `A` address bytes and `P` payload bytes: + +```text +n = 8 * (A + P) + 10 +n mod 8 = 2 +``` + +Thus the last two data bits occupy the most significant two bits of a byte. The first six bits of the 16-bit CRC occupy the remaining six bits of that byte; the next eight CRC bits occupy another byte; and the last two CRC bits occupy the two most significant bits of a third byte. + +If `C` is the 16-bit transmitted CRC and `tail` is the encoded final two-bit data field already stored in bits 7..6: + +```c +out0 = (uint8_t)(tail | (C >> 10)); +out1 = (uint8_t)(C >> 2); +out2 = (uint8_t)(C << 6); +``` + +The resulting CRC bit layout is: + +```text +[data:2][CRC:6] [CRC:8] [CRC:2][zero:6] +``` + +The CRC-input calculation covers the final **two data bits** but does not feed CRC bits or the six trailing padding bits back into the CRC register. + +## 7. Reference test vectors + +All hexadecimal bytes below use the transmission-order representation defined in section 2, excluding the radio-specific synchronization/preamble sequence. + +### 7.1 Whitening sequence + +```text +LFSR seed: 71 +First eight bytes:E3 B1 4B EA 85 BC E5 66 +28-bit seed word: E3B14BE0 +``` + +Both whitening implementations in section 3 produce the same stream. + +### 7.2 Standard scrambled packet + +```text +Address (API order): 01 02 03 04 05 +Payload (API order): 01 02 03 04 05 +CRC input, unwhitened: 05 04 03 02 01 80 40 C0 20 A0 +Whitening, first 10 bytes: E3 B1 4B EA 85 BC E5 66 0D AE +Transmitted data: E6 B5 48 E8 84 3C A5 A6 2D 0E +CRC input bit length: 80 +CRC(FFFF, input): 8B27 +CRC after final XOR: 74D8 +CRC whitening mask: 8C88 +Transmitted CRC: F850 +``` + +Cross-check against the historical CRC procedure: + +```text +CRC(B5D2, transmitted data): E0B6 +XOROUT(80, scrambled): 18E6 +E0B6 ^ 18E6: F850 +``` + +Complete data-and-CRC sequence: + +```text +E6 B5 48 E8 84 3C A5 A6 2D 0E F8 50 +``` + +### 7.3 Enhanced scrambled packet + +```text +Address (API order): 01 02 03 04 05 +Payload (API order): 01 02 +PID: 0 +NO_ACK: 0 +Packet-control bits: 0 | 000010 | 00 | 0 +CRC input bit length: 66 +CRC(FFFF, input): 125A +CRC after final XOR: EDA5 +CRC whitening mask: 36BA +Transmitted CRC: DB1F +XOROUT(66, scrambled): CA52 +``` + +Complete encoded bytes, including the final six zero padding bits: + +```text +E6 B5 48 E8 84 B8 C5 76 36 C7 C0 +``` + +The enhanced CRC begins two bits into the ninth byte of this encoded sequence. + +### 7.4 Short-length historical-table correction + +For a three-byte address and one-byte payload in standard scrambled mode: + +```text +A = 3 +P = 1 +n = 32 bits +XOROUT(32, scrambled) = E348 +``` + +## 8. Implementation integration + +The optimized firmware implementation uses a shared 32-bit whitening state to perform whitening, unwhitening, and CRC-mask extraction without the historical `xn297_scramble[]` table. The standard packet encoder/decoder computes the CRC over **unwhitened** bytes and applies the CRC whitening mask directly. The enhanced implementation uses the same model with the additional two-bit alignment. + +`XN297Dump` continues to calculate CRC values over received encoded bytes and uses the computed historical correction function to evaluate candidate lengths and scrambling modes. Its local whitening-byte buffer is generated at runtime rather than stored as a global constant table. + +The original XOROUT tables are therefore not necessary for the described emulation. The calculated correction depends only on scrambling mode and the number of CRC-covered bits; it does not depend on packet contents. + +## 9. Sources and implementation references + +1. **DIY Multiprotocol TX Module, PR #1199:** *HoTT, XN297 emu: compute the hop and CRC xorout tables* — derivation of the CRC correction function and comparison with historical tables. +2. **DIY Multiprotocol TX Module, PR #1201:** *Compute xn297 whitening instead of using an hardcoded array* — 28-bit recurrence, optimized byte generator, direct CRC handling, and enhanced-packet implementation. +3. **XN297 emulation source at PR #1201 revision `0cfaad0d`:** +4. **XN297 interface definitions at the same revision:** +5. **Historical XN297 reverse-engineering materials and HackRF recordings:** +