Files
DIY-Multiprotocol-TX-Module/docs/XN297_Whitening_and_CRC.md
T
goebish 91199e146e 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
2026-10-25 16:57:07 +01:00

17 KiB

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:

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:

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:

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:

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:

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:

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

#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:

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:

s[i] = s[i-7] ^ s[i-3]       for i >= 7

Over GF(2), raising the connection polynomial to the fourth power gives:

(x^7 + x^3 + 1)^4 = x^28 + x^12 + 1

Therefore the same sequence also satisfies:

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:

0xE3B14BE0
#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:

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:

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:

feedback = ((c >> 15) ^ b) & 1
c = (c << 1) & 0xFFFF
if feedback: c ^= 0x1021

A reference function is:

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:

C_raw   = CRC(0xFFFF, M)
C_final = C_raw ^ 0xFFFF

With whitening enabled, the transmitted CRC is:

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:

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:

C_old = CRC(0xB5D2, M ^ S[0:n])

The transmitted CRC defined in section 4 is:

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:

XOROUT(n, scrambled)
    = CRC(0x4A2D, S[0:n]) ^ 0xFFFF ^ S[n:n+16]

The old calculation is therefore equivalent to:

C_tx = C_old ^ XOROUT(n, scrambled)

When whitening is disabled, substitute zero for every whitening bit:

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

#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:

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:

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:

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:

reversed-order address bytes
+ 10-bit packet-control region
+ bit-reversed payload bytes

With A address bytes and P payload bytes:

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:

out0 = (uint8_t)(tail | (C >> 10));
out1 = (uint8_t)(C >> 2);
out2 = (uint8_t)(C << 6);

The resulting CRC bit layout is:

[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

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

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:

CRC(B5D2, transmitted data): E0B6
XOROUT(80, scrambled):       18E6
E0B6 ^ 18E6:                 F850

Complete data-and-CRC sequence:

E6 B5 48 E8 84 3C A5 A6 2D 0E F8 50

7.3 Enhanced scrambled packet

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:

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:

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