nrf_esb)The default radio path (radio.cpp + the poll loop in rf_link.cpp) is a hand-rolled, synchronous,
busy-wait half-duplex RADIO driver. It works, but it has no automatic retransmission and runs entirely
inside loop(), so a single lost poll/reply surfaces to the app as a reply gap. Those gaps are what trip the
reconnect/connect-edge paths that cause the connect-time buzz. A real ESB stack hides single-packet loss in
hardware (auto-ack + auto-retransmit) and runs off RADIO IRQ + PPI + a dedicated timer, independent of
loop().
This change vendors Nordic's nrf_esb (nRF5 SDK 17.1.0) and adds an nrf_esb-based backend behind a
build flag. It is foundational, not finished — see Status.
make compile # default raw backend (unchanged, 151260 B)
make compile EXTRA_FLAGS="-DNRF52840_XXAA {build.flags.usb} -DCFG_TUD_HID=4 -DOPK_RADIO_ESB=1"
OPK_RADIO_ESB=0 (default) leaves the production build byte-for-byte unchanged. =1 builds and links the
ESB backend (+~1.5 KB flash, +2 KB RAM) and calls esbBackendInit() at boot.
| file | role |
|---|---|
OpenPuck/nrf_esb.{c,h}, nrf_esb_error_codes.h, nrf_esb_resources.h |
vendored verbatim from nRF5 SDK 17.1.0 components/proprietary_rf/esb. The only edits: nrf_esb.c is wrapped in #if OPK_RADIO_ESB and its SDK includes (nrf_error.h/sdk_common.h/sdk_macros.h/app_util.h/nrf_gpio.h/nrf_delay.h) point at the shim. Excluded from make format (Nordic style). |
OpenPuck/nrf_esb_shim.h |
tiny shim providing the only SDK symbols nrf_esb needs: nrf_error.h (re-included from the BSP), the VERIFY_* macros, STATIC_ASSERT, ret_code_t, and NRF_ESB_MAX_PAYLOAD_LENGTH=96. Avoids vendoring the whole sdk_common chain. |
OpenPuck/esb_backend.{h,cpp} |
the puck-side glue: configures nrf_esb as a PTX poll master and bridges its event-driven model to the synchronous poll → reply shape rf_link expects. |
radio.cpp / docs/PROTOCOL.md)| puck link | nrf_esb config |
note |
|---|---|---|
PHY = Ble_2Mbit |
bitrate = NRF_ESB_BITRATE_2MBPS_BLE |
BLE 2M = 16-bit preamble. Not NRF_ESB_BITRATE_2MBPS (that is Nrf_2Mbit, 8-bit preamble — would not sync.) |
| CRC16 poly 0x11021 init 0xFFFF, addr included | crc = NRF_ESB_CRC_16BIT |
nrf_esb's 16-bit CRC is exactly this. |
| dynamic length, S1LEN=3 (PID+noack) | protocol = NRF_ESB_PROTOCOL_ESB_DPL |
ESB DPL framing. |
| 5-byte address (BALEN=4) | set_base_address_0 + set_prefixes |
nrf_esb bit-reverses to the wire itself — pass the stored bytes (as in g_rfBase/the bond), not the reversed form. |
| reply in the ACK window | PTX + selective_auto_ack, poll TX with noack=0 |
controller is the PRX; its F1/F3 reply rides back as the ACK payload. The E1 beacon uses noack=1. |
| F1 up to ~66 B | NRF_ESB_MAX_PAYLOAD_LENGTH = 96 |
default 32 would truncate the 0x43-augmented F1. |
nrf_esb takes exclusive ownership of RADIO, TIMER2 (sys timer) and TIMER3 (nRF52 anomaly-132
workaround), SWI0_EGU0 (event IRQ), and PPI channels 7–13. There is no SoftDevice in this build
(BLE is never started), so RADIO/PPI are free. The link test confirms nrf_esb's RADIO_IRQHandler,
TIMER2/3_IRQHandler and SWI0_IRQHandler do not collide with the Adafruit core (the image links
clean). Before runtime bring-up, confirm the core/TinyUSB does not also drive TIMER2/TIMER3/PPI 7–13 at
runtime; reassign via nrf_esb's #defines (overridable) if it does.
Done and verified (compiles + links, no symbol conflicts; default build unaffected):
nrf_esb builds against the Adafruit BSP via the shim;esb_backend configures it for the puck's exact link and is linked into the OPK_RADIO_ESB=1 image;esbBackendInit() is the boot entry point.Remaining (hardware-gated — cannot be done without a puck + controller on the bench):
rfConnStep/rfConnTx (poll) through esbBackendPoll() and
rfHostFrameOnce (E1 beacon) through esbBackendSendNoAck(), and gate off the raw rfConfig()/
NRF_RADIO path in rf_link.cpp/radio.cpp under OPK_RADIO_ESB (the two cannot both own RADIO). Today
with the flag on, esbBackendInit() and the raw poller both touch RADIO, so the link does not function —
this is why the flag is experimental/off.rfHopTo/QoS at esbBackendSetChannel().retransmit_count/retransmit_delay; verify the E1 discovery beacon still lands.esbBackendPoll busy-waits for the TX-done edge to mirror rfConnTx; once stable, consider
making the poll loop fully event-driven (TX-done callback drives the next poll) so it no longer blocks
loop() at all — the real payoff of moving to nrf_esb.Once 1–3 are validated, the buzz-relevant win is automatic: a lost poll is retransmitted in hardware instead of becoming the reply gap that the (separately fixed) reconnect/connect-edge logic reacts to.