Selaa lähdekoodia

feat(rf): route the rf_link poll loop + E1 beacon through the ESB backend

Completes the nrf_esb adoption: under OPK_RADIO_ESB the connected poll/relay and
the discovery/keepalive beacon now run through nrf_esb instead of the raw RADIO
path, with the raw path #else'd out so the two never both own RADIO.

- Extract the connected-poll transport into rfTransact(): raw NRF_RADIO TX/RX in
  one branch, esbBackendPoll() in the other. The ESB reply is placed at rfrx[2]
  with a synthesized [LEN][S1] header so the large F1/F3/0x45 decoder is shared
  byte-for-byte across backends. Reply RSSI comes from RADIO->RSSISAMPLE (raw) or
  the ack-payload RSSI (ESB, via esbLastRssi()).
- rfHostFrameOnce(): E1 beacon TXes via esbBackendSendNoAck() under the flag
  (nrf_esb adds the LEN/S1/PID header + CRC, so it gets only the 18B payload).
- Channel hopping needs no change: rfTransact/the beacon set the channel each
  call from g_sessCh (which rfHopTo updates). PID dedup is now native to nrf_esb.

Both builds verified: raw 151324 B, ESB 154932 B, no warnings, no new lint.

Still hardware-gated: on-air A/B with a real controller, and rf_diag.cpp still
drives RADIO raw (would fight nrf_esb if a diag command runs with the flag on).
See docs/ESB_MIGRATION.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Jariullah Safi 4 kuukautta sitten
vanhempi
commit
47c77bcece
5 muutettua tiedostoa jossa 109 lisäystä ja 52 poistoa
  1. 3 4
      OpenPuck/OpenPuck.ino
  2. 6 0
      OpenPuck/esb_backend.cpp
  3. 3 0
      OpenPuck/esb_backend.h
  4. 77 32
      OpenPuck/rf_link.cpp
  5. 20 16
      docs/ESB_MIGRATION.md

+ 3 - 4
OpenPuck/OpenPuck.ino

@@ -230,10 +230,9 @@ void setup()
 		}
 	}
 #if OPK_RADIO_ESB
-	// Experimental: bring up the nrf_esb radio backend. This is the init entry point of the ESB migration;
-	// the rf_link poll loop is NOT yet routed through esbBackendPoll/SendNoAck, so with this flag on the link
-	// does not yet function (and nrf_esb's RADIO ownership conflicts with rf_link's raw rfConfig). Present so
-	// the library links into the image and gives the wiring a home. See docs/ESB_MIGRATION.md.
+	// Bring up the nrf_esb radio backend. With OPK_RADIO_ESB the rf_link poll loop + E1 beacon are routed
+	// through it (esbBackendPoll / esbBackendSendNoAck) instead of the raw RADIO path. Not yet validated on
+	// hardware, and rf_diag still drives RADIO raw -- see docs/ESB_MIGRATION.md.
 	esbBackendInit();
 #endif
 	// Hardware watchdog: if loop() ever stops feeding it (wedged radio busy-wait, HardFault spin, blocked CDC

+ 6 - 0
OpenPuck/esb_backend.cpp

@@ -96,6 +96,12 @@ uint8_t esbBackendPoll(const uint8_t *payload, uint8_t plen, uint8_t *out,
 	return n;
 }
 
+uint8_t esbLastRssi()
+{
+	// s_rx.rssi is the RSSI magnitude nrf_esb sampled on the received ACK payload.
+	return s_rxHave ? s_rx.rssi : 0;
+}
+
 void esbBackendSendNoAck(const uint8_t *payload, uint8_t plen)
 {
 	nrf_esb_payload_t tx;

+ 3 - 0
OpenPuck/esb_backend.h

@@ -31,4 +31,7 @@ uint8_t esbBackendPoll(const uint8_t *payload, uint8_t plen, uint8_t *out,
 
 // Send one no-ack packet (the E1 discovery/host frame); no reply is expected.
 void esbBackendSendNoAck(const uint8_t *payload, uint8_t plen);
+
+// |dBm| of the most recent ACK-payload reply (nrf_esb samples RSSI on the received ack). 0 = none yet.
+uint8_t esbLastRssi();
 #endif // OPK_RADIO_ESB

+ 77 - 32
OpenPuck/rf_link.cpp

@@ -6,6 +6,7 @@
 #include "haptics.h"
 #include "controllers.h"
 #include "status_led.h"
+#include "esb_backend.h"
 #include <Adafruit_TinyUSB.h>
 #include <Arduino.h>
 #include <string.h>
@@ -148,6 +149,14 @@ static void rfHostFrameOnce(int slot, bool discovery)
 	// identical either way -- so the discovery frame can also double as a re-advertisement if needed.
 	const uint8_t *txBase = discovery ? g_rfBase : g_sessBase[slot];
 	uint8_t txPfx = discovery ? g_rfPrefix : g_sessPrefix[slot];
+#if OPK_RADIO_ESB
+	// ESB adds the LENGTH/S1/PID header + CRC itself, so hand it only the 18-byte payload (0xE1...prefix) at
+	// rftx[2]. No-ack: the controller adopts the advertised session and then answers E3 polls; it does not ack
+	// the beacon, so there is no response window to capture (the raw path's RE/pairing print is dropped here).
+	esbBackendSetChannel(g_rfCh);
+	esbBackendSetAddr(txBase, txPfx);
+	esbBackendSendNoAck(&rftx[2], 0x12);
+#else
 	rfConfig(g_rfCh);
 	rfSetAddr(txBase, txPfx);
 	NRF_RADIO->PACKETPTR = (uint32_t)rftx;
@@ -189,6 +198,67 @@ static void rfHostFrameOnce(int slot, bool discovery)
 	NRF_RADIO->TASKS_DISABLE = 1;
 	RWAIT_DISABLED();
 	NRF_RADIO->EVENTS_DISABLED = 0;
+#endif
+}
+
+// Connected-poll transport: TX the [LEN][S1][payload] already staged in rftx on slot's session address +
+// channel ch, capture the reply into rfrx (with the [LEN][S1] header rebuilt so the decode is backend-
+// agnostic), and report whether a packet arrived, its CRC validity and its RSSI magnitude. Returns rxlen.
+static uint8_t rfTransact(uint8_t ch, int slot, uint16_t win, bool *gotPkt,
+			  bool *crcok, uint8_t *rssiMag)
+{
+#if OPK_RADIO_ESB
+	(void)win;
+	// ESB strips LEN/S1/PID; send the payload (rftx[2..]) of length rftx[0]=plen and place the reply payload
+	// at rfrx[2] with a synthesized [LEN][S1] header so the shared decode below reads it byte-identically.
+	esbBackendSetChannel(ch);
+	esbBackendSetAddr(g_sessBase[slot], g_sessPrefix[slot]);
+	uint8_t n = esbBackendPoll(&rftx[2], rftx[0], &rfrx[2],
+				   (uint8_t)(sizeof rfrx - 2));
+	rfrx[0] = n;
+	rfrx[1] = 0;
+	// nrf_esb delivers RX_RECEIVED only for CRC-good packets; a reply (ack payload) means CRC was good.
+	*gotPkt = (n != 0);
+	*crcok = (n != 0);
+	*rssiMag = esbLastRssi();
+	return n;
+#else
+	rfConfig(ch);
+	rfSetAddr(g_sessBase[slot], g_sessPrefix[slot]);
+	NRF_RADIO->PACKETPTR = (uint32_t)rftx;
+	NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk |
+			    RADIO_SHORTS_END_DISABLE_Msk;
+	NRF_RADIO->EVENTS_DISABLED = 0;
+	NRF_RADIO->TASKS_TXEN = 1;
+	RWAIT_DISABLED();
+	NRF_RADIO->EVENTS_DISABLED = 0;
+	NRF_RADIO->PACKETPTR = (uint32_t)rfrx;
+	rfrx[0] = 0;
+	// RXEN->READY->START; ADDRESS->RSSISTART samples the reply RSSI; DISABLED->RSSISTOP closes it.
+	NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk |
+			    RADIO_SHORTS_ADDRESS_RSSISTART_Msk |
+			    RADIO_SHORTS_DISABLED_RSSISTOP_Msk;
+	NRF_RADIO->EVENTS_END = 0;
+	NRF_RADIO->TASKS_RXEN = 1;
+	uint32_t t0 = micros();
+	while (!NRF_RADIO->EVENTS_END && (micros() - t0) < win) {
+	}
+	uint8_t rxlen = 0;
+	*gotPkt = false;
+	*crcok = false;
+	*rssiMag = 0;
+	if (NRF_RADIO->EVENTS_END) {
+		NRF_RADIO->EVENTS_END = 0;
+		*gotPkt = true;
+		*crcok = NRF_RADIO->CRCSTATUS & 1;
+		*rssiMag = (uint8_t)(NRF_RADIO->RSSISAMPLE & 0x7F);
+		rxlen = rfrx[0];
+	}
+	NRF_RADIO->TASKS_DISABLE = 1;
+	RWAIT_DISABLED();
+	NRF_RADIO->EVENTS_DISABLED = 0;
+	return rxlen;
+#endif
 }
 
 void rfHopTo(uint8_t newCh)
@@ -223,36 +293,14 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 	rftx[0] = plen; // LENGTH = payload byte count
 	rftx[1] = s1; // S1 (type-specific)
 	memcpy(rftx + 2, payload, plen); // payload[0]=type byte, then data/TLVs
-	rfConfig(ch);
 	// connected poll runs on this slot's UNIQUE session addr (per-bond). g_curSlot is set by rfConnStep
 	// before each call; fall back to slot 0 if not (e.g. rf_diag paths).
 	int slot = (g_curSlot >= 0 && g_curSlot < NSLOT) ? g_curSlot : 0;
-	rfSetAddr(g_sessBase[slot], g_sessPrefix[slot]);
-	NRF_RADIO->PACKETPTR = (uint32_t)rftx;
-	NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk |
-			    RADIO_SHORTS_END_DISABLE_Msk;
-	NRF_RADIO->EVENTS_DISABLED = 0;
-	NRF_RADIO->TASKS_TXEN = 1;
-	RWAIT_DISABLED();
-	NRF_RADIO->EVENTS_DISABLED = 0;
-	NRF_RADIO->PACKETPTR = (uint32_t)rfrx;
-	rfrx[0] = 0;
-	// RXEN->READY->START; catch the reply. ADDRESS->RSSISTART samples the reply's signal strength (read from
-	// RSSISAMPLE below, surfaced to Steam via report 0x7B); DISABLED->RSSISTOP closes the measurement.
-	NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk |
-			    RADIO_SHORTS_ADDRESS_RSSISTART_Msk |
-			    RADIO_SHORTS_DISABLED_RSSISTOP_Msk;
-	NRF_RADIO->EVENTS_END = 0;
-	NRF_RADIO->TASKS_RXEN = 1;
 	g_stPoll++;
-	uint32_t t0 = micros();
-	while (!NRF_RADIO->EVENTS_END && (micros() - t0) < win) {
-	} // RX window (tunable 'r'; or relay override)
-	uint8_t rxlen = 0;
-	if (NRF_RADIO->EVENTS_END) {
-		NRF_RADIO->EVENTS_END = 0;
-		bool crcok = NRF_RADIO->CRCSTATUS & 1;
-		rxlen = rfrx[0];
+	bool gotPkt = false, crcok = false;
+	uint8_t rssiMag = 0;
+	uint8_t rxlen = rfTransact(ch, slot, win, &gotPkt, &crcok, &rssiMag);
+	if (gotPkt) {
 		// reply arrived but CRC failed -> RF quality (channel/interference)
 		if (!crcok) {
 			g_stCrc++;
@@ -283,9 +331,8 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 				g_connRx++;
 				// link alive -> loop() suppresses the redundant E1 beacon
 				g_connReplyMs[s] = millis();
-				// |dBm| of this reply (started by the ADDRESS short)
-				uint8_t rs =
-					(uint8_t)(NRF_RADIO->RSSISAMPLE & 0x7F);
+				// |dBm| of this reply (raw: ADDRESS-short RSSISAMPLE; ESB: ack-payload RSSI)
+				uint8_t rs = rssiMag;
 				// EWMA, ~8-sample horizon, per-slot
 				if (rs)
 					g_linkRssi[s] =
@@ -644,9 +691,7 @@ uint8_t rfConnTx(uint8_t ch, uint8_t s1, const uint8_t *payload, uint8_t plen,
 		g_stNoRx++;
 		g_qosBad++;
 	}
-	NRF_RADIO->TASKS_DISABLE = 1;
-	RWAIT_DISABLED();
-	NRF_RADIO->EVENTS_DISABLED = 0;
+	// the radio is left disabled by rfTransact (raw) / managed by nrf_esb (ESB)
 	return rxlen;
 }
 

+ 20 - 16
docs/ESB_MIGRATION.md

@@ -50,23 +50,27 @@ runtime; reassign via `nrf_esb`'s `#define`s (overridable) if it does.
 
 ## Status
 
-Done and verified (compiles + links, no symbol conflicts; default build unaffected):
+Done and verified by compile/link (both backends build; no symbol conflicts; default build unchanged):
 - vendored `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.
+- `esb_backend` configures it for the puck's exact link;
+- **the data path is wired.** Under `OPK_RADIO_ESB` the connected poll/relay (`rfConnTx` via the new
+  `rfTransact()` transport) and the E1 beacon (`rfHostFrameOnce`) route through `esbBackendPoll()` /
+  `esbBackendSendNoAck()`; the raw `rfConfig`/`NRF_RADIO` path is `#else`'d out so the two never both own
+  RADIO. The F1/F3 decode is backend-agnostic — the ESB reply is rebuilt into `rfrx` with a synthesized
+  `[LEN][S1]` header so the exact same decoder runs. Channel hopping works as-is (`rfTransact`/the beacon set
+  the channel each call from `g_sessCh`, which `rfHopTo` updates). PID is handled natively by `nrf_esb`
+  (it auto-increments per distinct payload — the dedup the raw path hand-cycled).
 
-Remaining (hardware-gated — cannot be done without a puck + controller on the bench):
-1. **Wire the data path.** Route `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.
-2. **Channel hopping.** Re-point `rfHopTo`/QoS at `esbBackendSetChannel()`.
-3. **On-air A/B.** With a real controller: confirm it answers the PTX poll with F1 in the ACK payload on the
-   bonded address; tune `retransmit_count`/`retransmit_delay`; verify the E1 discovery beacon still lands.
-4. **Pacing.** `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`.
+Remaining (hardware-gated — needs a puck + controller on the bench):
+1. **On-air A/B.** Confirm the controller answers the PTX poll with F1 in the ACK payload on the bonded
+   address; confirm the E1 discovery beacon still lands and the controller adopts the session; tune
+   `retransmit_count` / `retransmit_delay`.
+2. **`rf_diag` coexistence.** `rf_diag.cpp` still drives `NRF_RADIO` raw; it will fight `nrf_esb` if a diag
+   command runs while the flag is on. Gate `rf_diag` under `OPK_RADIO_ESB` (or have it borrow the backend)
+   before relying on it in an ESB build.
+3. **Pacing.** `esbBackendPoll` busy-waits for the TX-done edge to mirror `rfConnTx`. Once stable, make 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 `nrf_esb`.
 
-Once 1–3 are validated, the buzz-relevant win is automatic: a lost poll is retransmitted in hardware instead
+Once (1) is 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.