Replay initial nRF52 USB READY after application handoff

This commit is contained in:
mikecarper
2026-09-25 17:44:15 -07:00
parent 2724bab5da
commit ce46e60a86
3 changed files with 90 additions and 3 deletions
+6 -1
View File
@@ -24,6 +24,10 @@ framework implementation stops the build for review. The backport:
- rechecks attachment while waiting, including completion by another callback;
- bounds both waits with one 100,000-poll budget, independent of OS ticks;
- ignores stale READY on a disabled peripheral and exits if it is removed.
- replays an already asserted regulator READY state when the application USB
task starts with VBUS present, as the bootloader does. The framework's
original startup replayed DETECTED only, so a missing later power edge could
leave the new application's USB transport unattached after a UF2 handoff.
The poll budget is an iteration limit, not a promised elapsed-time timeout.
If the clock/peripheral never becomes ready, the caller returns instead of
@@ -36,6 +40,7 @@ patched handler from the pinned-framework fixture, model W1C event semantics,
and cover duplicate/nested callbacks, delayed/missing clocks and READY, removal,
retry, and detached USB. Restoring the inherited READY prefix must reproduce
the infinite wait under a subprocess deadline. Additional tests check SDK
isolation, idempotence, fail-closed patching, and all nRF52 environment hooks.
isolation, idempotence, fail-closed patching, the startup DETECTED/READY order,
and all nRF52 environment hooks.
The harness is shared with OTAFIX's TinyUSB fork at
`test/otafix/nrf5x_power_test.py`; keep the two copies in sync when extending it.
+36 -2
View File
@@ -1,4 +1,4 @@
"""Compile a build-local fix for the framework's inherited nRF52 USB READY hang.
"""Compile build-local fixes for the framework's nRF52 USB power startup.
Never modify PlatformIO's shared framework package. Fail closed if a framework
update changes the code this narrow backport expects. The same handler policy
@@ -60,6 +60,22 @@ ATTACH = """ // Enable pull up
case USB_EVT_REMOVED:"""
OLD_PORT_INIT = """ if (usb_reg & POWER_USBREGSTATUS_VBUSDETECT_Msk) {
tusb_hal_nrf_power_event(NRFX_POWER_USB_EVT_DETECTED);
}
}
"""
PORT_INIT = """ if (usb_reg & POWER_USBREGSTATUS_VBUSDETECT_Msk) {
tusb_hal_nrf_power_event(NRFX_POWER_USB_EVT_DETECTED);
}
// On an application handoff, VBUS and the regulator may already be ready.
// The power driver only reports future edges, so replay both initial states.
if (usb_reg & POWER_USBREGSTATUS_OUTPUTRDY_Msk) {
tusb_hal_nrf_power_event(NRFX_POWER_USB_EVT_READY);
}
}
"""
def patched_source(source):
source = source.replace("\r\n", "\n")
@@ -73,10 +89,24 @@ def patched_source(source):
return source.replace(OLD_READY, READY).replace(OLD_ATTACH, ATTACH)
def patched_port_source(source):
source = source.replace("\r\n", "\n")
if PORT_INIT in source and OLD_PORT_INIT not in source:
return source
if source.count(OLD_PORT_INIT) != 1:
raise RuntimeError(
"nRF52 USB power fix: unrecognized USB startup; review the "
"framework update before building (shared SDK was not modified)"
)
return source.replace(OLD_PORT_INIT, PORT_INIT)
def replace_driver(build_env, node):
# SCons passes a not-yet-created VariantDir node, not the SDK source path.
source = Path(node.srcnode().get_abspath())
patched = patched_source(source.read_text(encoding="utf-8"))
patcher = (patched_port_source if source.name == "Adafruit_TinyUSB_nrf.cpp"
else patched_source)
patched = patcher(source.read_text(encoding="utf-8"))
destination = Path(build_env.subst("$BUILD_DIR")) / "patched-nrf52-usb" / source.name
destination.parent.mkdir(parents=True, exist_ok=True)
if not destination.exists() or destination.read_text(encoding="utf-8") != patched:
@@ -90,6 +120,10 @@ def install(build_env):
replace_driver,
"*Adafruit_TinyUSB_Arduino*src*portable*nordic*nrf5x*dcd_nrf5x.c",
)
build_env.AddBuildMiddleware(
replace_driver,
"*Adafruit_TinyUSB_Arduino*src*arduino*ports*nrf*Adafruit_TinyUSB_nrf.cpp",
)
if "Import" in globals():
+48
View File
@@ -4,6 +4,8 @@
import configparser
import importlib.util
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
@@ -15,6 +17,32 @@ SPEC = importlib.util.spec_from_file_location("usb_fix", ROOT / "scripts/nrf52_u
FIX = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(FIX)
ORIGINAL = (ROOT / "test/fixtures/nrf52_usb_power_original.c").read_text()
PORT_SOURCE = """
typedef struct { unsigned USBREGSTATUS; } PowerRegisters;
static PowerRegisters registers;
#define NRF_POWER (&registers)
#define POWER_USBREGSTATUS_VBUSDETECT_Msk 1u
#define POWER_USBREGSTATUS_OUTPUTRDY_Msk 2u
enum { NRFX_POWER_USB_EVT_DETECTED = 0, NRFX_POWER_USB_EVT_READY = 2 };
static unsigned events[2], event_count;
static void tusb_hal_nrf_power_event(unsigned event) { events[event_count++] = event; }
static void usb_hardware_init(void) {
unsigned usb_reg = NRF_POWER->USBREGSTATUS;
if (usb_reg & POWER_USBREGSTATUS_VBUSDETECT_Msk) {
tusb_hal_nrf_power_event(NRFX_POWER_USB_EVT_DETECTED);
}
}
int main() {
for (unsigned status = 0; status < 4; ++status) {
registers.USBREGSTATUS = status;
event_count = 0;
usb_hardware_init();
if (event_count != ((status & 1u) != 0u) + ((status & 2u) != 0u)) return 1;
if (status & 1u && events[0] != NRFX_POWER_USB_EVT_DETECTED) return 2;
if (status & 2u && events[event_count - 1] != NRFX_POWER_USB_EVT_READY) return 3;
}
}
"""
class UsbPowerTests(unittest.TestCase):
@@ -26,6 +54,26 @@ class UsbPowerTests(unittest.TestCase):
self.assertEqual(fixed, FIX.patched_source(ORIGINAL.replace("\n", "\r\n")))
self.assertEqual(fixed, FIX.patched_source(fixed))
def test_initial_ready_is_replayed_after_detected(self):
compiler = shutil.which("c++")
if compiler is None:
self.skipTest("C++ compiler unavailable")
patched = FIX.patched_port_source(PORT_SOURCE)
self.assertEqual(patched, FIX.patched_port_source(patched))
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / "usb_port.cpp"
program = Path(directory) / "usb_port"
source.write_text(patched)
subprocess.run([compiler, "-std=c++11", "-o", str(program), str(source)],
check=True, capture_output=True)
subprocess.run([str(program)], check=True, capture_output=True)
def test_unrecognized_usb_port_fails_closed(self):
for source in ("", PORT_SOURCE.replace("USBREGSTATUS", "STATUS"),
PORT_SOURCE + PORT_SOURCE):
with self.subTest(source=source[:40]), self.assertRaises(RuntimeError):
FIX.patched_port_source(source)
def test_unrecognized_or_partly_fixed_driver_fails_closed(self):
for source in ("", ORIGINAL.replace("hfclk_running()", "clock_running()"),
ORIGINAL + ORIGINAL, ORIGINAL.replace(FIX.OLD_READY, FIX.READY)):