Skip to content

24G Input Interface

Receives a single 8BitDo SF30 2.4G wireless controller directly over an nRF24L01+ radio, impersonating the OEM USB dongle closely enough that the controller pairs and links without it.

Protocol

An interrupt-driven nRF24L01+ receiver following a 64-channel frequency-hopping sequence, decoding 13-byte frames at up to ~99 packets/second per linked controller.

See 24G Protocol for the full wire format: radio configuration, the hop table, frame layout, and the pairing rendezvous.

Location: src/native/host/rf24g/

Scheduling

The radio runs off interrupts, not the main polling loop:

  • IRQ (falling edge) -- RX_DR. The ISR drains the RX FIFO, decodes the newest valid frame, waits an ACK guard window, retunes to the next hop index, and re-arms the dwell alarm.
  • Hardware alarm -- fires if a dwell expires with nothing received; advances the hop index and retunes anyway, counting a miss.

This keeps packet capture immune to jitter from other core-0 work (LED updates, flash writes, USB polling) -- a main-loop-polled design would drop packets under load.

PARK -> SEARCH -> LINKED, plus PAIRING:

  • PARK -- sits on the controller's only unlinked-probe channel (2447 MHz), rewriting RF_CH every ~2ms with CE held high, matching the OEM dongle's idle behavior.
  • SEARCH -- a 52-hop sweep, giving acquisition a second, independent path in alongside a direct probe contact.
  • LINKED -- every frame's hop-index byte re-syncs both index and phase, so the link self-heals after any stall.
  • PAIRING -- see Pairing below.

Supported Controllers

Device Detection
8BitDo SF30 2.4G wireless controller Links via the SF30 2.4G RF protocol; no other controller in this family has been tested

Button Mapping

SF30 2.4G JP_BUTTON_* Notes
B B1
A B2
Y B3
X B4
L L1
R R1
Select S1
Start S2 Also the controller's power button
D-pad Up DU Set by either byte2:0x80 or byte3:0x40 -- both fire together on hardware but are reported as a single button, never surfaced separately
D-pad Down DD
D-pad Left DL
D-pad Right DR

Combinations (including diagonals) are a plain bitwise OR at the protocol level -- no SOCD handling needed.

Layout reported as LAYOUT_NINTENDO_4FACE (SNES-style face buttons).

Analog Axes

None -- the SF30 2.4G has no sticks or analog triggers. analog[] stays at the 128 center init_input_event() sets.

Connection Detection

  • A frame is only decoded for buttons once the controller is LINKED -- an unlinked (probing) controller sets byte3:0x20 (Start) on most probe frames, which would otherwise appear as a phantom Start press.
  • event.transport = INPUT_TRANSPORT_NATIVE, so the router auto-registers the player on the first frame without waiting for a button press.
  • On link loss, the player at that device address is removed rather than left registered with stale state.

Pairing

Hold the BOOTSEL button ~1.5s to begin pairing (see 24g2usb app). The pairing exchange:

  1. Listens on the SF30 2.4G rendezvous address at 2447 MHz for the controller's request burst (35 04 00 <counter>).
  2. Waits for the burst to go quiet (~250ms), then replies ~346ms after the last request with the offered address and its complement.
  3. Runs 5 reply/listen cycles (25 replies at 20ms, then 500ms listening) -- the controller stops re-requesting once it accepts, only in the final cycle.
  4. The offered address is live the moment the exchange ends -- its pipe was already open (see Addressing below) -- but the controller ceasing to ask isn't proof it actually took the address, so a follow-up watch checks whether a real frame arrives on it within the next several seconds and logs either confirmed -- frames arriving or no frames yet.

That confirmation is diagnostic only: there is nothing to roll back either way, unlike an earlier design where an unconfirmed claim reverted. no frames yet does not mean pairing failed. A controller that already held this address, or one that won't transmit until power-cycled, stays silent through the watch and still ends up paired -- it links on its next power-on. Re-pairing an already-paired controller trips this every time, because a controller sitting in pairing mode sends no data frames at all. If you see it, power-cycle the controller before assuming anything went wrong.

ACK counts are not a reliable success signal during pairing -- the controller ceasing to request is.

Addressing

The receiver's own identity (the 4-byte body of its address -- see 24G Protocol for the address layout) is derived once at boot from the board's factory-programmed unique ID, not chosen, negotiated, or stored anywhere. Hashing the whole ID rather than a prefix matters because boards from the same production reel commonly share the leading bytes of that ID.

Because the derivation is a pure function of the board, it comes out identical on every boot: there is nothing to persist to flash and nothing to lose on a power cycle. A controller paired to a given board stays paired across a reboot and across a reflash of the same board. Moving a controller to a different board changes the receiver ID -- a different unique ID hashes differently -- so it requires re-pairing, exactly as swapping in a different physical OEM dongle would.

Only pipe 0 is open from boot (EN_RXADDR = 0x01); there is no "claimed" state gating reception, only whether the controller has actually been heard from yet (used for the seen/diagnostics reporting).

The receiver supports exactly one paired controller at a time (see 24g2usb app for why). Pairing always re-offers the same address, so pairing a second controller replaces whichever controller was previously paired to it -- there is no way to hold two controllers' addresses simultaneously.

Configuration

  • Pin defines (RF24G_PIN_* in rf24g_host.h, #ifndef-guarded so an app can override): RF24G_PIN_MISO, RF24G_PIN_CE, RF24G_PIN_CSN, RF24G_PIN_SCK, RF24G_PIN_MOSI, RF24G_PIN_IRQ
  • Device address: RF24G_DEV_ADDR (0xB0)
  • Transport type: INPUT_TRANSPORT_NATIVE
  • Input source: INPUT_SOURCE_NATIVE_24G
  • SPI: hardware spi0 at 8 MHz (the nRF24L01+ tolerates up to 10 MHz)

Apps Using This Input

  • 24g2usb -- SF30 2.4G controller to USB HID gamepad