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
This commit is contained in:
goebish
2026-10-25 16:57:07 +01:00
committed by GitHub
parent d3293afdab
commit 91199e146e
4 changed files with 587 additions and 108 deletions
+4 -2
View File
@@ -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;
+81 -106
View File
@@ -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<len; i++)
{
uint8_t b_in=buf[i];
if(xn297_scramble_enabled)
b_in ^= xn297_scramble[i+xn297_addr_len];
msg[i] = bit_reverse(b_in);
}
msg[i] = bit_reverse(XN297_unscramble_crc(buf[i]));
if (!xn297_crc)
return true; // 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 < len; ++i)
crc16_update( buf[i], 8);
//xorout
crc ^= XN297_crc_xorout(xn297_scramble_enabled, 8 * (xn297_addr_len + len));
crc = ~crc ^ (xn297_lfsr >> 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
+6
View File
@@ -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
+496
View File
@@ -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 <stdint.h>
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 <stdint.h>
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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
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. <https://github.com/pascallanger/DIY-Multiprotocol-TX-Module/pull/1199>
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. <https://github.com/pascallanger/DIY-Multiprotocol-TX-Module/pull/1201>
3. **XN297 emulation source at PR #1201 revision `0cfaad0d`:** <https://github.com/goebish/DIY-Multiprotocol-TX-Module/blob/0cfaad0d893ef9e111febbf9b4c2c4019201d83b/Multiprotocol/XN297_EMU.ino>
4. **XN297 interface definitions at the same revision:** <https://github.com/goebish/DIY-Multiprotocol-TX-Module/blob/0cfaad0d893ef9e111febbf9b4c2c4019201d83b/Multiprotocol/iface_xn297.h>
5. **Historical XN297 reverse-engineering materials and HackRF recordings:** <https://github.com/victzh/xn297_reveng>