From 015f9f7c69f08e58c239922206ef1b5a54ceae60 Mon Sep 17 00:00:00 2001 From: Comma Device Date: Sun, 30 Aug 2026 14:15:55 +0800 Subject: [PATCH] pandad: protect app firmware from recover, keep blank-board flashing The SPI panda boot loop called recover_internal_panda() whenever the panda did not appear within 20s after a reset. recover() erases both the bootstub and app sectors (dfu.py program_bootstub erases sectors 0+1) before reflashing, so a working panda that booted slowly lost its app firmware. Fix: - pandad.py: wait up to 60s for the panda, retry the normal reset 3x, and only enter the ROM bootloader (recover) when the panda is confirmed in DFU or all normal resets failed - preserving provisioning of blank boards. - hardware.py: align recover_internal_panda() timing with the working manual flash scripts (0.2s reset hold, 1s settle, keep BOOT0 high). - spi.py: raise the SPI bootloader ack timeout from 0.01s to 0.1s so the DFU flash reliably succeeds instead of erasing then failing. --- openpilot/common/hardware/comma/hardware.py | 7 ++++--- openpilot/selfdrive/pandad/pandad.py | 21 ++++++++++++++------- panda/python/spi.py | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpilot/common/hardware/comma/hardware.py b/openpilot/common/hardware/comma/hardware.py index effb4d9db..1b3680338 100644 --- a/openpilot/common/hardware/comma/hardware.py +++ b/openpilot/common/hardware/comma/hardware.py @@ -410,10 +410,11 @@ class HardwareComma(HardwareBase): gpio_set(GPIO.STM_RST_N, True) gpio_set(GPIO.STM_BOOT0, True) - time.sleep(0.01) + time.sleep(0.2) gpio_set(GPIO.STM_RST_N, False) - time.sleep(0.01) - gpio_set(GPIO.STM_BOOT0, False) + time.sleep(1) + # Keep BOOT0 high while the SPI ROM bootloader session runs; the custom + # SPI panda needs it held (matches the manual flash scripts). def booted(self): # this normally boots within 8s, but on rare occasions takes 30+s diff --git a/openpilot/selfdrive/pandad/pandad.py b/openpilot/selfdrive/pandad/pandad.py index 856be9f5c..b4eff4253 100755 --- a/openpilot/selfdrive/pandad/pandad.py +++ b/openpilot/selfdrive/pandad/pandad.py @@ -101,17 +101,16 @@ def main() -> None: cloudlog.exception("pandad.uncaught_exception") count = 0 + recover_attempts = 0 while not do_exit: try: cloudlog.event("pandad.flash_and_connect", count=count) HARDWARE.reset_internal_panda() count += 1 - # The internal SPI panda takes ~10s to boot its app after a reset. - # Wait for it to come back in normal (non-bootstub) mode before - # deciding whether to flash. Only fall back to the bootloader - # (recover) path if it never appears. + # The internal SPI panda can take a while (up to ~60s on occasion) to + # boot its app after a reset, so wait generously. panda_serials: list[str] = [] - for _ in range(40): + for _ in range(120): panda_serials = Panda.list() if panda_serials: try: @@ -122,9 +121,17 @@ def main() -> None: pass time.sleep(0.5) if not panda_serials: - cloudlog.info("Panda did not appear after reset, trying recover...") + # Never force a board that already has firmware into the ROM bootloader + # right away: recover erases the app sector before reflashing. Only a + # truly blank board needs it, and that is detected after several normal + # resets fail to produce a panda at all. + if not PandaDFU.list() and recover_attempts < 3: + recover_attempts += 1 + cloudlog.warning(f"Panda did not appear after reset ({recover_attempts}/3), retrying normal reset...") + continue + cloudlog.info("Panda missing after normal resets, entering ROM bootloader (recover)...") HARDWARE.recover_internal_panda() - time.sleep(5) + time.sleep(2) # Flash all Pandas in DFU mode for serial in PandaDFU.list(): diff --git a/panda/python/spi.py b/panda/python/spi.py index 0e31ce1d7..8d4c18cee 100644 --- a/panda/python/spi.py +++ b/panda/python/spi.py @@ -328,7 +328,7 @@ class STBootloaderSPIHandle(BaseSTBootloaderHandle): # sync + command spi.xfer([self.SYNC, ]) spi.xfer([cmd, cmd ^ 0xFF]) - self._get_ack(spi, timeout=0.01) + self._get_ack(spi, timeout=0.1) # "predata" - for commands that send the first data without a checksum if predata is not None: