Files
DIY-Multiprotocol-TX-Module/docs/XBM-37_Analysis.md
2026-07-16 18:25:58 +02:00

671 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# XBM-37 Quad SPI Capture Analysis
**Manufacturer:** T-Smart
**Model:** XBM-37 (toy quadcopter)
**TX/RX RF Chip:** SV7241A (QFN-20, NRF24L01+ clone / BK2425 derivative)
**Protocol Basis:** Closely related to FQ777 (same RF chip family, same bind address, same ssv ESB air encoding)
**Capture Tool:** Logic analyzer digital (02a) and SPI-decoded (all "b" files)
---
## Table of Contents
1. [Capture File Inventory](#1-capture-file-inventory)
2. [Key Protocol Parameters](#2-key-protocol-parameters)
3. [SPI Initialization Sequence](#3-spi-initialization-sequence)
4. [Bind Sequence Deep Analysis (02a + 02b)](#4-bind-sequence--deep-analysis-02a--02b)
5. [Bind vs No-RX Comparison (01b vs 02b)](#5-bind-vs-no-rx-comparison-01b-vs-02b)
6. [Normal Data Packet Format](#6-normal-data-packet-format)
7. [Per-Channel Control Analysis (03b 24b)](#7-per-channel-control-analysis-03b--24b)
8. [RF Timing Summary](#8-rf-timing-summary)
9. [MPM Implementation Notes](#9-mpm-implementation-notes)
---
## 1. Capture File Inventory
All captures are located in `Captures_XBM-37/`. The **"b" files** are SPI-decoded exports
(columns: `Time [s], Packet ID, MOSI, MISO`). The **"a" file** is the raw digital export
(columns: `Time[s], MOSI, MISO, SCK, CSN, CE, IRQ`).
| File | Type | Description | SPI Transactions | TX Payloads | Duration |
|------|------|-------------|-----------------|-------------|----------|
| `01b-XBM-37_Quad_TX-PowerOn-NoRX.csv` | SPI | TX power-on, **no RX present** | 5,437 | 1,352 | 2.963 s |
| `02a-XBM-37_Quad_TX-PowerOn-withRX-Bind.csv` | **Digital** | TX power-on + bind **with RX** (all 6 lines) | 405,271 samples | 1,345 payloads | 4.380 s |
| `02b-XBM-37_Quad_TX-PowerOn-withRX-Bind.csv` | SPI | TX power-on + bind **with RX** | 5,409 | 1,345 | 2.949 s |
| `03b-XBM-37_Quad_Aileron-Center-Left-Center-Right-Center.csv` | SPI | Aileron stick: center → left → center → right → center | 6,039 | 1,509 | 3.127 s |
| `04b-XBM-37_Quad_Elevator-Center-Back-Center-Forward-Center.csv` | SPI | Elevator stick: center → back → center → fwd → center | 6,035 | 1,509 | 3.123 s |
| `05b-XBM-37_Quad_Throttle-Low-High-Low.csv` | SPI | Throttle: low → high → low | 6,034 | 1,509 | 3.122 s |
| `06b-XBM-37_Quad_Rudder-Center-Left-Center-Right-Center.csv` | SPI | Rudder (yaw): center → left → center → right → center | 6,034 | 1,509 | 3.122 s |
| `07b-XBM-37_Quad_RateModeSwitch-1-2-3.csv` | SPI | Rate/speed mode switch: 1 → 2 → 3 | 6,034 | 1,509 | 3.122 s |
| `08b-XBM-37_Quad_FlipSwitch-PushButton_Off-On.csv` | SPI | Flip trick button: off → on | 6,034 | 1,509 | 3.122 s |
| `09b-XBM-37_Quad_VideoSwitch-Off-On-Off-On.csv` | SPI | Video record toggle: off → on → off → on | 6,023 | 1,506 | 3.116 s |
| `10b-XBM-37_Quad_PictureSwitch-PushButton-3X.csv` | SPI | Photo/picture button: pressed 3 times | 6,019 | 1,505 | 3.113 s |
| `11b-XBM-37_Quad_HeadlessSwitch-PushButton-Off-On.csv` | SPI | Headless mode button: off → on | 6,022 | 1,506 | 3.116 s |
| `12b-XBM-37_Quad_ReturnToHomeSwitch-PushButton-Off-On.csv` | SPI | RTH button: off → on | 6,034 | 1,508 | 3.127 s |
| `13b-XBM-37_Quad_LED-LightsSwitch-PushButton-On-Off.csv` | SPI | LED lights toggle: on → off | 6,019 | 1,505 | 3.113 s |
| `14b-XBM-37_Quad_OK-Switch-PushButton-Off-On.csv` | SPI | OK button: off → on | 6,027 | 1,507 | 3.120 s |
| `20b-XBM-37_Quad_Elevator-Trim-Center-Forward-Max_32-Clicks.csv` | SPI | Elevator trim: center → forward max (32 clicks) | 38,683 | 9,671 | 20.004 s |
| `21b-XBM-37_Quad_Elevator-Trim-Center-Back-Max_31-Clicks.csv` | SPI | Elevator trim: center → back max (31 clicks) | 33,215 | 8,304 | 17.191 s |
| `22b-XBM-37_Quad_Aileron-Trim-Center-Left-Max_32-Clicks.csv` | SPI | Aileron trim: center → left max (32 clicks) | 32,315 | 8,079 | 16.730 s |
| `23b-XBM-37_Quad_Aileron-Trim-Center-Right-Max_31-Clicks.csv` | SPI | Aileron trim: center → right max (31 clicks) | 28,495 | 7,124 | 14.756 s |
| `24b-XBM-37_Quad_Ail-Trim-Forward-Max_Ele-Trim-Right-Max_Push-OK-Button-2X.csv` | SPI | Elevator trim at forward max, aileron trim at right max, OK button pressed 2× | 29,275 | 7,319 | 15.176 s |
### Notes
- The "b" SPI files each contain one SPI byte per row. Multiple rows sharing the same `Packet ID` belong to one CSN-low SPI transaction.
- Files 03b24b were all captured in **normal (post-bind) flight mode**. The TX had previously
completed a successful bind.
- File 01b and 02b are functionally **identical** captures (confirmed by direct payload comparison)
except for a tiny time offset (~7 µs between corresponding packets). This confirms the TX
transmits identically whether or not an RX is present; see Section 5.
---
## 2. Key Protocol Parameters
| Parameter | Value |
|-----------|-------|
| RF transceiver chip | SV7241A (QFN-20, NRF24L01+ / BK2425 derivative) |
| RF frequency band | 2.4 GHz ISM |
| Air data rate (SV7241A) | 2 Mbps (RF_SETUP = 0x26, bit5 = RF_DR = 1) |
| Air data rate (nRF24L01+ equivalent) | **250 kbps** via `ssv_pack_dpl()` method |
| Packet size (raw, pre-pack) | 8 bytes |
| Packet size (after ssv_pack_dpl) | 12 bytes |
| Enhanced ShockBurst | Yes (dynamic payload length, no auto-ACK) |
| SPI clock frequency | ~143 kHz (7 µs period) |
| Bind packet count | **400** |
| Bind address (broadcast) | `E7 E7 E7 E7 67` |
| Data address format | `[TX_ID₀ TX_ID₁ TX_ID₂ E7 67]` |
| Bind channel (first packet) | 0x00 (universal) |
| Hop channels (bind + data) | **0x49, 0x34, 0x26, 0x07** (4 channels) |
| Packet period (steady state) | **~2,070 µs** (~2.07 ms) |
| CE high pulse width | ~1,172 µs |
| CE low gap (inter-packet) | ~896 µs |
| TX-only mode | Yes — EN_RXADDR = 0x00; TX never listens |
---
## 3. SPI Initialization Sequence
The sequence below is from `02b` (PIDs 024), occurring over the first ~5 ms after the SPI
bus becomes active (~150335 ms after TX power-on due to startup delays).
### 3.1 SV7241A Private Bank Registers
The SV7241A (like BK2425) has bank-switched extended registers at addresses 0x180x1B.
Register 0x1F selects the bank. These registers hold RF/analog calibration constants specific
to the chip and **do not exist on nRF24L01+** — they are ignored when implementing with MPM.
| PID | Register | Data Written | Purpose |
|-----|----------|-------------|---------|
| 0 | 0x1F | `00` | Select bank 0 |
| 1 | 0x1B | `10 E1 D3 3D` | Bank 0 RF calibration reg 1 |
| 2 | 0x19 | `06 AA A2 DB` | Bank 0 RF calibration reg 2 |
| 3 | 0x1F | `01` | Select bank 1 |
| 4 | 0x19 | `77 48 9A E8` | Bank 1 internal reg |
| 5 | 0x1B | `76 87 CA 01` | Bank 1 internal reg |
| 6 | 0x1F | `02` | Select bank 2 |
| 7 | 0x1B | `A0 00 18 A0` | Bank 2 internal reg |
| 8 | 0x1F | `04` | Select bank 4 |
| 9 | 0x18 | `01 00 F0 00` | Bank 4 internal reg |
| 10 | 0x1F | `05` | Select bank 5 |
| 11 | 0x18 | `84 03 2A 03` | Bank 5 internal reg |
| 12 | 0x19 | `90 BF 00 00` | Bank 5 internal reg |
| 13 | 0x1A | `A0 0F 00 00` | Bank 5 internal reg |
### 3.2 Standard NRF24L01-Compatible Registers
Written immediately after the private registers (PIDs 1424):
| PID | Register | Written Value | Decoded Meaning |
|-----|----------|--------------|-----------------|
| 14 | CONFIG (0x00) | `0x0C` | EN_CRC=1, CRCO=1 (2-byte CRC), PWR_UP=0, PRIM_RX=0 (TX) |
| 15 | CONFIG (0x00) | *read back* `0x0C` | Verify CONFIG |
| 16 | TX_ADDR (0x10) | `E7 E7 E7 E7 67` | Bind broadcast address |
| 17 | RX_ADDR_P0 (0x0A) | `E7 E7 E7 E7 67` | Matches TX_ADDR for bind |
| 18 | EN_AA (0x01) | `0x00` | Auto-ACK **disabled** on all pipes |
| 19 | EN_RXADDR (0x02) | `0x00` | All RX pipes **disabled** TX only |
| 20 | RF_CH (0x05) | `0x49` | Initial RF channel (73 MHz offset) |
| 21 | FEATURE (0x1D) | `0x04` | EN_DPL = 1 (dynamic payload length) |
| 22 | DYNPD (0x1C) | `0x01` | DPL enabled on pipe 0 |
| 23 | RF_SETUP (0x06) | `0x26` | SV7241A: 2 Mbps, max power |
| 24 | CONFIG (0x00) | `0x0E` | PWR_UP=1 → TX powered up |
**RF_SETUP = 0x26 mapping:**
| Chip | Bit 5 meaning | Value 0x26 decodes as |
|------|--------------|----------------------|
| SV7241A | RF_DR (0=1Mbps, 1=2Mbps) | **2 Mbps**, max power |
| nRF24L01+ | RF_DR_LOW (0=normal, 1=250kbps) | **250 kbps**, max power |
→ On nRF24L01+, writing 0x26 would set **250 kbps** (not 2 Mbps). For MPM emulation the
correct approach (same as FQ777) is `NRF24L01_SetBitrate(NRF24L01_BR_250K)` combined with
`ssv_pack_dpl()` to produce a compatible air packet.
**Startup timing:**
The TX power-on to first SPI transaction takes ~150 ms (CE is held high from
t = 72 ms until t = 0, then the ~150 ms wait elapses before SPI activity at t ≈ +181 ms in the
02b file). The entire init register sequence completes in ~6 ms.
---
## 4. Bind Sequence Deep Analysis (02a + 02b)
### 4.1 Overview
After the ~150 ms startup wait, the TX sends exactly **400 bind packets** before switching to
normal data mode.
| Property | Value |
|----------|-------|
| Total bind packets | **400** |
| Bind address | `E7 E7 E7 E7 67` (fixed broadcast) |
| First bind packet channel | `0x00` (universal channel) |
| Remaining 399 bind packet channels | Cycling: `0x49 → 0x34 → 0x26 → 0x07 → 0x49 → …` |
| Bind packet payload | `20 14 07 03 TX_ID₀ TX_ID₁ TX_ID₂ CKSUM` |
| Bind packet content | Identical across all 400 transmissions |
The first bind packet is sent on RF channel **0x00**, ensuring a newly powered-on RX (sitting
on its default channel) can hear the bind announcement regardless of any prior state. Subsequent
bind packets cycle through the four data-hopping channels.
### 4.2 Bind Packet Structure
```
Byte 0 1 2 3 4 5 6 7
[20 14 07 03 TX_ID₀ TX_ID₁ TX_ID₂ CKSUM]
```
| Byte | Value (this TX) | Description |
|------|----------------|-------------|
| B0 | `0x20` | Bind identifier byte (constant — marks this as a bind packet) |
| B1 | `0x14` | Protocol variant constant (XBM-37 specific; FQ777 uses `0x15`) |
| B2 | `0x07` | Protocol variant constant (XBM-37 specific; FQ777 uses `0x05`) |
| B3 | `0x03` | Protocol variant constant (XBM-37 specific; FQ777 uses `0x06`) |
| B4 | `0x91` | TX_ID byte 0 — unique per TX unit |
| B5 | `0x05` | TX_ID byte 1 — unique per TX unit |
| B6 | `0x05` | TX_ID byte 2 — unique per TX unit |
| B7 | `0x9B` | Checksum = (TX_ID₀ + TX_ID₁ + TX_ID₂) & 0xFF = (0x91+0x05+0x05) & 0xFF |
**Checksum formula for bind packet:** `B7 = (B4 + B5 + B6) & 0xFF`
(This differs from the data packet checksum which sums B0B6.)
### 4.3 TX ID
TX ID is **3 bytes** embedded in B4B6 of the bind packet. This TX unit has:
```
TX_ID = [0x91, 0x05, 0x05]
```
After bind, the TX_ADDR is set to: `[TX_ID₀ TX_ID₁ TX_ID₂ 0xE7 0x67]`
= `[91 05 05 E7 67]`
The RX extracts TX_ID from the bind packet and uses it to construct the matching address for
normal data reception.
### 4.4 Bind Channel Sequence (from 02b SPI decoded)
```
Packet #1: ch=0x00 [20 14 07 03 91 05 05 9B] ← universal announce
Packet #2: ch=0x49 [20 14 07 03 91 05 05 9B]
Packet #3: ch=0x34 [20 14 07 03 91 05 05 9B]
Packet #4: ch=0x26 [20 14 07 03 91 05 05 9B]
Packet #5: ch=0x07 [20 14 07 03 91 05 05 9B]
Packet #6: ch=0x49 [20 14 07 03 91 05 05 9B]
...continues cycling 0x49→0x34→0x26→0x07...
Packet #400: ch=0x26 [20 14 07 03 91 05 05 9B] ← last bind
```
Channel usage across all 400 bind packets:
| Channel | Count | Notes |
|---------|-------|-------|
| 0x00 | 1 | First packet only |
| 0x49 (73 MHz) | 100 | Regular cycle |
| 0x34 (52 MHz) | 100 | Regular cycle |
| 0x26 (38 MHz) | 100 | Regular cycle (includes last bind packet) |
| 0x07 (7 MHz) | 99 | Regular cycle |
### 4.5 Bind-to-Normal Transition
After the 400th bind packet the TX:
1. Completes the 400th transmission (CE pulse ~1,172 µs, then CE low).
2. Waits **~16.6 ms** (no SPI activity; firmware processing time).
3. Writes the new TX_ADDR: `W_TX_ADDR [91 05 05 E7 67]` (SPI pid=1625 at t=1.175882 s).
4. Immediately clears STATUS and starts normal data mode.
5. First normal packet: ch=**0x07** (continuation of the hop sequence from where bind ended).
```
Last bind (400th): t=1.159258 s, ch=0x26
TX_ADDR change: t=1.175882 s (+16.6 ms gap)
First data packet: t=1.176619 s, ch=0x07 [E1 70 70 70 20 20 00 71]
```
### 4.6 IRQ / STATUS Analysis (from 02a Digital Capture)
The IRQ line (active-low) is asserted by the SV7241A for every successfully transmitted packet
(TX_DS interrupt):
| Observation | Value |
|-------------|-------|
| Total IRQ assertions | 1,340 (matching 1,340 normal CE pulses) |
| IRQ pulse duration (typical) | ~600 µs |
| IRQ source | TX_DS only (TX complete) |
| RX_DR interrupts | **Zero** — no data ever received |
The SPI confirms there are **no `R_RX_PAYLOAD` (0x61) commands** in either 01b or 02b.
Combined with `EN_RXADDR = 0x00` (all RX pipes disabled), it is impossible for the TX to
receive data from the RX. This is confirmed by the STATUS register never showing bit6
(RX_DR) = 1.
One extended IRQ assertion of **15.9 ms** occurs at t=1.160476 s (immediately after the 400th
bind packet). This is not an RX event — it is simply the TX_DS interrupt remaining uncleared
while the firmware processes the bind-completion event and updates TX_ADDR. The IRQ is
cleared when `W_STATUS [70]` is written at t=1.176254 s (pid=1626).
### 4.7 CE Pulse Timing (from 02a)
| Measurement | Value |
|-------------|-------|
| Normal CE pulse duration | min 1,165 µs, max 1,178 µs, **avg 1,172 µs** |
| CE inter-pulse gap (end to start) | **~896 µs** |
| Total cycle (CE high + gap) | **~2,068 µs** |
| Pulse-to-pulse interval (high to high) | min 2.063 ms, max 20.686 ms, **avg 2.084 ms** |
The 20.686 ms outlier corresponds to the bind-to-normal transition pause (~16.6 ms).
---
## 5. Bind vs No-RX Comparison (01b vs 02b)
**Finding: The TX transmits identically whether or not an RX is present.**
| Metric | 01b (No RX) | 02b (With RX) |
|--------|------------|----------------|
| Total TX payloads | 1,352 | 1,345 |
| Bind packets | 400 | 400 |
| Bind packet content | `20 14 07 03 91 05 05 9B` | `20 14 07 03 91 05 05 9B` (identical) |
| TX_ADDR change time | t=1.175899 s | t=1.175882 s |
| TX_ADDR after bind | `[91 05 05 E7 67]` | `[91 05 05 E7 67]` (identical) |
| Normal data payload (idle) | `E1 70 70 70 20 20 00 71` | `E1 70 70 70 20 20 00 71` (identical) |
| Time offset between captures | — | ~7 µs per packet |
The TX performs an automatic timed bind sequence (400 packets) and then switches to data mode
unconditionally, regardless of RX acknowledgment. The TX **never knows** whether the RX
accepted the bind. Bind is one-sided: the RX passively listens for the bind packet on ch=0x00
(or the cycling channels), extracts the TX_ID and hop channels, and then follows the TX's
normal data transmissions.
---
## 6. Normal Data Packet Format
### 6.1 Packet Structure (8 bytes)
```
Byte 0 1 2 3 4 5 6 7
[Throttle Rudder Aileron Elevator Flags1 Flags2 Flags3 CKSUM]
```
**Checksum (B7):** `B7 = (B0 + B1 + B2 + B3 + B4 + B5 + B6) & 0xFF`
All 8-byte checksum values have been verified against this formula across all capture files.
### 6.2 Stick Channels (B0B3)
| Byte | Channel | Min | Center | Max | Notes |
|------|---------|-----|--------|-----|-------|
| B0 | Throttle | `0xE1` | `0x70` | `0x00` | Non-return throttle |
| B1 | Rudder (Yaw) | `0x00` | `0x70` | `0xE1` | |
| B2 | Aileron (Roll) | `0xE1` | `0x70` | `0x00` | |
| B3 | Elevator (Pitch) | `0xE1` | `0x70` | `0x00` | |
All analog channels use the same range: **0x00 0xE1** (0 225 decimal) with center at
**0x70** (112 decimal).
- Throttle, Aileron, and Elevator use reversed state (0xE1...0x70...0x00).
### 6.3 Flags Byte 1 (B4)
| Value | Meaning | Description |
|-------|---------|-------------|
| `0x20` | **Trim Elevator Center** | Resets at TX start |
| `0x21 up to 0x40` | Trim elevator **forward** | 32 Clicks to max |
| `0x1F downto 0x01` | Trim elevator **back** | 31 Clicks to max |
### 6.4 Flags Byte 2 (B5)
| Value | Meaning | Description |
|-----|------|-------------|
| `0x20` | **Trim Aileron Center** | Resets at TX start |
| `0x21 up to 0x40` | Trim aileron **left** | 32 Clicks to max |
| `0x1F down to 0x01` | Trim aileron **right** | 31 Clicks to max |
| `0x80` | OK button | OK pressed (`0x20``0xA0`) |
`B5 bit7` is an OR mask on top of trim value (`B5_with_OK = B5_trim | 0x80`), e.g.
center `0x20 -> 0xA0`, right-max `0x01 -> 0x81`.
### 6.5 Flags Byte 3 (B6)
| Bits | Mask | Name | Description |
|------|------|------|-------------|
| [1:0] | `0x03` | Rate mode | `0x00`=rate 1 (slow), `0x01`=rate 2, `0x02`=rate 3 (fast) |
| 2 | `0x04` | LED off | `0`=LED lights ON, `1`=LED lights OFF |
| 3 | `0x08` | RTH | `1`=return-to-home active |
| 4 | `0x10` | Headless | `1`=headless mode active |
| 5 | `0x20` | Video | `1`=video recording active |
| 6 | `0x40` | Picture | `1`=photo capture triggered |
| 7 | `0x80` | Flip | `1`=3D flip command |
Normal state: `B6 = 0x00` (rate 1, LED on, no special modes)
### 6.6 Return-to-Home (RTH)
Updated analysis of **file 12b** (RTH OFF in first half, ON in second half):
- RTH-OFF payload is steady at `7E 70 70 70 20 20 00 0E`.
- After the OFF→ON transition, the payload changes to `7E 70 70 70 20 20 08 16`.
- The persistent functional change in this capture is `B6: 0x00 -> 0x08`
(bit3 asserted when RTH is ON).
In this export, RTH is represented in the steady 8-byte payload by `B6 bit3`.
### 6.7 Example Payloads
| Payload | Meaning |
|---------|---------|
| `E1 70 70 70 20 20 00 71` | Throttle low `0xE1`, all sticks center `0x70`, LED on, no special modes `0x00` |
| `82 70 70 70 20 20 00 12` | Throttle ~mid `0x82`, all sticks center, LED on, no special modes |
| `00 70 70 70 20 20 00 90` | Throttle max `0x00`, all sticks center, LED on, no special modes |
| `E1 70 00 70 20 20 00 01` | Throttle low, aileron full right `0x00`, LED on, no special modes |
| `E1 70 E1 70 20 20 00 E2` | Throttle low, aileron full left `0xE1`, LED on, no special modes |
| `E1 70 70 00 20 20 00 01` | Throttle low, elevator full back `0x00`,LED on, no special modes |
| `82 70 70 70 20 20 80 92` | Throttle ~mid, all sticks center, flip command active `0x80` |
| `82 70 70 70 20 20 10 22` | Throttle ~mid, all sticks center, headless mode active `0x10` |
| `82 70 70 70 20 20 01 13` | Throttle ~mid, all sticks center, rate mode 2 `0x01` |
| `82 70 70 70 20 20 02 14` | Throttle ~mid, all sticks center, rate mode 3 `0x02` |
| `00 70 70 70 20 A0 00 F1` | Throttle max, all sticks center, OK button pressed `0xA0` |
| `00 70 70 70 20 20 04 75` | Throttle max, all sticks center, LED lights OFF `0x04` |
---
## 7. Per-Channel Control Analysis (03b 24b)
All control captures were taken in post-bind normal mode. Hopping channels confirmed active:
`0x07, 0x49, 0x34, 0x26` (cyclic). Average packet interval: **~2.07 ms** across all files.
### 03b Aileron (Roll)
- **Affected byte:** B2
- Range observed: `0xE1` (full left) → `0x70` (center) → `0x00` (full right)
### 04b Elevator (Pitch)
- **Affected byte:** B3
- Range observed: `0x00` (full back) → `0x70` (center) → `0xE1` (full forward)
### 05b Throttle
- **Affected byte:** B0
- Range observed: `0xE1` (low minimum) ↔ `0x70` (center) ↔ `0x00` (full maximum)
- Non-return throttle (stick does not spring back to center)
### 06b Rudder (Yaw)
- **Affected byte:** B1
- Range observed: `0x00` (full left) → `0x70` (center) → `0xE1` (full right)
- Note: B0 (throttle) also varies in this capture because left stick controls both
throttle (up/down) and rudder (left/right) Mode 2 transmitter
### 07b Rate Mode Switch
- **Affected byte:** B6 bits[1:0]
- `B6 = 0x00`: Rate 1 (slowest/beginner)
- `B6 = 0x01`: Rate 2 (intermediate)
- `B6 = 0x02`: Rate 3 (fastest/expert)
### 08b Flip Switch
- **Affected byte:** B6 bit7
- `B6 = 0x00`: No flip; `B6 = 0x80`: 3D flip command active
- While flip is held: throttle (B0) increments slightly (`0x82``0x83`)
### 09b Video Switch
- **Affected byte:** B6 bit5
- `B6 = 0x00`: Video off; `B6 = 0x20`: Video recording active
- Toggle on/off: transitions between these two states
### 10b Picture Switch (3×)
- **Affected byte:** B6 bit6
- `B6 = 0x00`: Idle; `B6 = 0x40`: Photo capture triggered (momentary)
### 11b Headless Switch
- **Affected byte:** B6 bit4
- `B6 = 0x00`: Headless off; `B6 = 0x10`: Headless mode active
### 12b Return to Home Switch
- **Capture split tested:** first half RTH OFF, second half RTH ON.
- **RTH OFF payload:** `7E 70 70 70 20 20 00 0E`
- **RTH ON payload:** `7E 70 70 70 20 20 08 16`
- **Primary byte change:** `B6: 0x00 -> 0x08`
- `B6 bit3` is asserted when RTH is active in this capture
- **Minor secondary variation while ON:** `B0` occasionally increments `0x7E -> 0x7F`,
producing checksum `0x16 -> 0x17`
- All other payload bytes remain unchanged across the OFF→ON transition in this file.
### 13b LED Lights Switch
- **Affected byte:** B6 bit2
- `B6 = 0x00`: LED lights **ON** (default)
- `B6 = 0x04`: LED lights **OFF**
- Note: Inverted logic — bit2=1 means OFF
### 14b OK Switch
- **Affected byte:** B5 bit7
- `B5 = 0x20`: OK button not pressed
- `B5 = 0xA0`: OK button pressed (0x20 | 0x80)
### 20b Elevator Trim Center → Forward Max (32 clicks)
- **Affected byte:** B4 (elevator trim)
- Observed progression: `0x20 -> ... -> 0x40`
- Endpoint in this capture: `B4 = 0x40` (forward max)
- `B5` remains `0x20`; `B6` remains `0x00`
### 21b Elevator Trim Center → Back Max (31 clicks)
- **Affected byte:** B4 (elevator trim)
- Observed progression: `0x20 -> ... -> 0x01`
- Endpoint in this capture: `B4 = 0x01` (back max)
- `B5` remains `0x20`; `B6` remains `0x00`
### 22b Aileron Trim Center → Left Max (32 clicks)
- **Affected byte:** B5 (aileron trim)
- Observed progression: `0x20 -> ... -> 0x40`
- Endpoint in this capture: `B5 = 0x40` (left max)
- `B4` remains `0x20`; `B6` remains `0x00`
### 23b Aileron Trim Center → Right Max (31 clicks)
- **Affected byte:** B5 (aileron trim)
- Observed progression: `0x20 -> ... -> 0x01`
- Endpoint in this capture: `B5 = 0x01` (right max)
- `B4` remains `0x20`; `B6` remains `0x00`
### 24b Trim Endpoints + OK Button (2 presses)
- Fixed trim baseline in this capture:
- `B4 = 0x40` (elevator forward max)
- `B5 trim = 0x01` (aileron right max)
- **OK effect:** `B5 bit7` toggles over trim baseline:
- not pressed: `B5 = 0x01`
- pressed: `B5 = 0x81` (`0x01 | 0x80`)
- `B6` remains `0x00` throughout.
---
## 8. RF Timing Summary
### 8.1 Per-Packet SPI Cycle (02b decoded)
Each packet transmission consists of 4 SPI transactions:
```
1. W_STATUS [27] = 0x70 → Clear TX_DS / MAX_RT / RX_DR interrupt flags
2. FLUSH_TX [E1] → Empty TX FIFO
3. W_RF_CH [25] = <ch> → Set hop channel
4. W_TX_PAYLOAD [A0] = 8B → Write 8-byte payload to TX FIFO
[CE pulse ~1,172 µs to transmit]
[~896 µs gap before next cycle]
```
### 8.2 Bind Phase Timing
| Event | Timestamp (02b) |
|-------|----------------|
| Init register writes complete | t = 0.186 s |
| Startup wait completes | t = 0.335 s (~150 ms) |
| First bind packet (ch=0x00) | t = 0.335838 s |
| 400th (last) bind packet (ch=0x26) | t = 1.159258 s |
| TX_ADDR change to data address | t = 1.175882 s (+16.6 ms) |
| First normal data packet | t = 1.176619 s |
Total bind phase duration: **~0.824 s** (150 ms wait + ~674 ms for 400 packets at 2.07 ms each)
### 8.3 Normal Data Phase Timing
| Event | Interval |
|-------|---------|
| Packet 1 → 2 (startup) | 0.918 ms |
| Packet 2 → 3 (startup) | 0.916 ms |
| Packet 3 → 4 (startup) | 1.491 ms |
| Steady state (4+) | **~2.070 ms** per packet |
The first 3 packets after bind-to-data transition have shorter intervals, then settle into the
steady 2.07 ms period.
### 8.4 STATUS Byte Values Observed
| STATUS | Meaning | When seen |
|--------|---------|-----------|
| `0x0E` | TX FIFO not full, RX FIFO empty, all interrupts clear | Normal/idle |
| `0x2E` | TX_DS = 1 (TX complete interrupt), TX FIFO not full | After each TX packet |
RX_DR (bit6) is **never set** in any STATUS byte — confirming no data is ever received.
---
## 9. MPM Implementation Notes
### 9.1 Comparison with FQ777
The XBM-37 protocol is closely related to FQ777 but has the following differences:
| Property | FQ777 | XBM-37 | Comment|
|----------|-------|---------|---------|
| Bind count | 1,000 | **400** | works with either |
| Bind packet B1 | `0x15` | `0x14` | different |
| Bind packet B2 | `0x05` | `0x07` | different |
| Bind packet B3 | `0x06` | `0x03` | different |
| Hop channels | `4D 43 27 07` | **`49 34 26 07`** | different |
| Data range | 0x000x64 | **0x000xE1** | different |
| Bind address | `E7 E7 E7 E7 67` | `E7 E7 E7 E7 67` | (same) |
| Checksum method | Sum B0B6 | Sum B0B6 | (same) |
| Bind checksum | Sum B4B6 | Sum B4B6 | (same) |
| ssv_pack_dpl encoding | Yes | Yes | (same) |
| Air bitrate (nRF24L01+) | 250 kbps | 250 kbps | (same) |
### 9.2 Required Implementation Constants
```c
static const uint8_t XBM37_bind_addr[] = {0xE7, 0xE7, 0xE7, 0xE7, 0x67};
static const uint8_t XBM37_hop_channels[] = {0x49, 0x34, 0x26, 0x07};
```
### 9.3 Bind Packet Builder
```c
// Bind packet (bytes 46 = TX ID; B7 = checksum of B4+B5+B6)
packet[0] = 0x20;
packet[1] = 0x14;
packet[2] = 0x07;
packet[3] = 0x03;
packet[4] = rx_tx_addr[0]; // TX_ID byte 0
packet[5] = rx_tx_addr[1]; // TX_ID byte 1
packet[6] = rx_tx_addr[2]; // TX_ID byte 2
packet[7] = packet[4] + packet[5] + packet[6];
```
### 9.4 Data Packet Builder
```c
packet[0] = convert_channel_16b_limit(THROTTLE, 0xE1, 0x00);
packet[1] = convert_channel_16b_limit(RUDDER, 0x00, 0xE1);
packet[2] = convert_channel_16b_limit(AILERON, 0xE1, 0x00);
packet[3] = convert_channel_16b_limit(ELEVATOR, 0xE1, 0x00);
packet[4] = ((convert_channel_8b(CH13) * 63) / 255) + 1; // ele trim (01..20..40)
packet[5] = 64 - (((uint32_t)convert_channel_8b(CH14) * 63 + 127) / 255); // ail trim (40..20..01)
packet[5] |= GET_FLAG(OK_SW, 0x80); // OK button
packet[6] = (rate_mode & 0x03) // bits[1:0] = rate (0/1/2)
| GET_FLAG(LED_SW, 0x04) // bit2=1 = LED off
| GET_FLAG(RTH_SW, 0x08) // bit3 = RTH
| GET_FLAG(HEADLESS_SW, 0x10) // bit4 = headless
| GET_FLAG(VIDEO_SW, 0x20) // bit5 = video
| GET_FLAG(PHOTO_SW, 0x40) // bit6 = picture
| GET_FLAG(FLIP_SW, 0x80); // bit7 = flip
packet[7] = 0;
for (uint8_t i = 0; i < 7; i++) packet[7] += packet[i]; // checksum
```
### 9.5 Address Management
```c
// TX init:
rx_tx_addr[2] = 0x00; // original hardcoded value now changes for model match capability (see below)
rx_tx_addr[3] = 0xE7;
rx_tx_addr[4] = 0x67;
// rx_tx_addr[0] and [1] are random/unique to the TX
// rx_tx_addr[2] now varies by changing receiver number (0-63) for model match
// Bind address (used during bind):
NRF24L01_WriteRegisterMulti(NRF24L01_10_TX_ADDR, XBM37_bind_addr, 5);
// After 400th bind packet, switch to data address:
NRF24L01_WriteRegisterMulti(NRF24L01_10_TX_ADDR, rx_tx_addr, 5);
```
### 9.6 RF Init
```c
void XBM37_RF_init() {
NRF24L01_Initialize();
NRF24L01_WriteRegisterMulti(NRF24L01_10_TX_ADDR, XBM37_bind_addr, 5);
NRF24L01_WriteReg(NRF24L01_01_EN_AA, 0x00); // no auto-ACK
NRF24L01_WriteReg(NRF24L01_02_EN_RXADDR, 0x00); // no RX pipes
NRF24L01_WriteReg(NRF24L01_1D_FEATURE, 0x04); // EN_DPL
NRF24L01_WriteReg(NRF24L01_1C_DYNPD, 0x01); // DPL pipe 0
NRF24L01_SetBitrate(NRF24L01_BR_250K); // 250 kbps on nRF24L01+
}
```
### 9.7 Air Encoding
Because both the XBM-37 TX and RX use SV7241A (a BK2425 derivative), the air packet format is
the SV7241A Enhanced ShockBurst encoding. When implementing with nRF24L01+, the same
`ssv_pack_dpl()` function used in the FQ777 protocol must be applied to convert the raw 8-byte
payload into the 12-byte packed representation that nRF24L01+ transmits at 250 kbps to produce
a compatible on-air signal.
---
*Analysis completed using Python scripts against the raw CSV captures. All packet checksums,
channel sequences, bind counts, and register values verified programmatically.*