From 17899b08554425d2bc72d90c87f42a8bad03dbf4 Mon Sep 17 00:00:00 2001 From: Niklas Dusenlund Date: Tue, 23 Jun 2026 10:08:34 +0200 Subject: [PATCH] Add dev bootloader chip erase Add a development-only bootloader command that requests a chip erase after the USB response has been sent. Expose the command through the Python bootloader client and defer execution until outgoing transport writes are idle. --- py/bitbox02/bitbox02/bitbox02/bootloader.py | 6 +++ py/send_message.py | 9 ++++ scripts/template-bootloader.jlink | 14 +++++- src/bootloader/bootloader.c | 48 ++++++++++++++++++++- src/bootloader/bootloader.h | 5 +++ src/bootloader/startup.c | 6 +++ src/usb/class/hid/hww/hid_hww.c | 5 +++ src/usb/class/hid/hww/hid_hww.h | 5 +++ 8 files changed, 95 insertions(+), 3 deletions(-) diff --git a/py/bitbox02/bitbox02/bitbox02/bootloader.py b/py/bitbox02/bitbox02/bitbox02/bootloader.py index 5834bd360b..82748edd36 100644 --- a/py/bitbox02/bitbox02/bitbox02/bootloader.py +++ b/py/bitbox02/bitbox02/bitbox02/bootloader.py @@ -215,6 +215,12 @@ def erase(self) -> None: """ self._erase(0) + def chip_erase(self) -> None: + """ + Issues a chip erase. Only supported by development bootloaders. + """ + self._query(b"c") + def erased(self) -> bool: """ Returns True if the the device contains no firmware. diff --git a/py/send_message.py b/py/send_message.py index 642ac9db39..8785827dc0 100755 --- a/py/send_message.py +++ b/py/send_message.py @@ -1704,6 +1704,14 @@ def _get_hardware(self) -> None: def _erase(self) -> None: self._device.erase() + def _chip_erase(self) -> None: + print("You will need to backup the flash to avoid permanently bricking the device") + if input("Type CHIP ERASE to erase the chip: ") != "CHIP ERASE": + print("Chip erase aborted") + return + self._device.chip_erase() + self._stop = True + def _show_fw_hash(self) -> None: self._device.set_show_firmware_hash(True) @@ -1727,6 +1735,7 @@ def _menu(self) -> None: ("Print versions", self._get_versions), ("Print hardware variant", self._get_hardware), ("Erase firmware", self._erase), + ("Chip erase", self._chip_erase), ("Show firmware hash at startup", self._show_fw_hash), ("Don't show firmware hash at startup", self._dont_show_fw_hash), ("Get firmware & sigkey hashes", self._get_hashes), diff --git a/scripts/template-bootloader.jlink b/scripts/template-bootloader.jlink index 143b1b7e68..6cf6ca40c0 100644 --- a/scripts/template-bootloader.jlink +++ b/scripts/template-bootloader.jlink @@ -1,3 +1,15 @@ -loadfile $file +// Reset and halt the device r + +// Temporarily disable bootloader protection before flashing. +// NVMCTRL_CTRLB = NVMCTRL_CTRLB_CMDEX_KEY | NVMCTRL_CTRLB_CMD_SBPDIS. +w2 0x41004004 0xA51A + +// Flash +loadfile $file 0 noreset + +// Reset +r + +// Quit q diff --git a/src/bootloader/bootloader.c b/src/bootloader/bootloader.c index 569f647aee..4f9e7c0d6b 100644 --- a/src/bootloader/bootloader.c +++ b/src/bootloader/bootloader.c @@ -70,6 +70,8 @@ #define OP_SET_SHOW_FIRMWARE_HASH ((uint8_t)'H') /* 0x4A */ // OP_HARDWARE - Return the secure chip variant. #define OP_HARDWARE ((uint8_t)'W') /* 0x57 */ +// OP_CHIP_ERASE - Issue a chip erase. Only available in non-production dev builds. +#define OP_CHIP_ERASE ((uint8_t)'c') /* 0x63 */ // API return codes #define OP_STATUS_OK ((uint8_t)0) @@ -113,6 +115,9 @@ static_assert(sizeof(((boot_data_t*)0)->fields) <= FLASH_BOOTDATA_LEN, "boot_dat static bool _loading_ready = false; static uint8_t _firmware_num_chunks = 0; +#if defined(BOOTLOADER_DEVDEVICE) && !defined(BOOTLOADER_PRODUCTION) +static bool _chip_erase_requested = false; +#endif // Indicates whether the whole app flash contains only 0xFF. // This controls bootloader text messages on the screen. // The value is computed at bootloader enter. @@ -317,7 +322,9 @@ static void _render_message(const char* message, int duration) UG_ClearBuffer(); UG_PutString(0, 0, message); UG_SendBuffer(); - delay_ms(duration); + if (duration > 0) { + delay_ms(duration); + } } void bootloader_render_default_screen(void) @@ -854,6 +861,21 @@ static size_t _api_hardware(uint8_t* output) return _report_status(OP_STATUS_OK, output) + 1; } +#if defined(BOOTLOADER_DEVDEVICE) && !defined(BOOTLOADER_PRODUCTION) +static size_t _api_chip_erase(uint8_t* output) +{ + _loading_ready = false; + if (DSU->STATUSB.bit.CELCK) { + return _report_status(OP_STATUS_ERR_LOCK, output); + } + if (periph_unlock(DSU) != ERR_NONE) { + return _report_status(OP_STATUS_ERR_UNLOCK, output); + } + _chip_erase_requested = true; + return _report_status(OP_STATUS_OK, output); +} +#endif + static size_t _api_command(const uint8_t* input, uint8_t* output, const size_t max_out_len) { memset(output, 0, max_out_len); @@ -907,6 +929,11 @@ static size_t _api_command(const uint8_t* input, uint8_t* output, const size_t m case OP_HARDWARE: len = _api_hardware(output); break; +#if defined(BOOTLOADER_DEVDEVICE) && !defined(BOOTLOADER_PRODUCTION) + case OP_CHIP_ERASE: + len = _api_chip_erase(output); + break; +#endif default: len = _report_status(OP_STATUS_ERR_INVALID_CMD, output); _loading_ready = false; @@ -932,9 +959,25 @@ static void _api_setup(void) usb_processing_register_cmds(usb_processing_hww(), cmd_callbacks, 1); } +void bootloader_process_pending_action(void) +{ +#if defined(BOOTLOADER_DEVDEVICE) && !defined(BOOTLOADER_PRODUCTION) + if (!_chip_erase_requested) { + return; + } + _render_message("Chip erase", 0); + bootloader_close_interfaces(); + __disable_irq(); + DSU->STATUSA.reg = DSU_STATUSA_DONE | DSU_STATUSA_BERR | DSU_STATUSA_FAIL | DSU_STATUSA_PERR; + DSU->CTRL.reg = DSU_CTRL_CE; + while (DSU->STATUSA.bit.DONE == 0) { + } + _reset_mcu(); +#endif +} + static void _check_init(boot_data_t* data) { -#ifdef BOOTLOADER_PRODUCTION // Enable boot protection if not already enabled. The // BOOTPROT fuse is persistent and not erased on chip erase. while (NVMCTRL->STATUS.bit.READY == 0) { @@ -957,6 +1000,7 @@ static void _check_init(boot_data_t* data) _reset_mcu(); } +#ifdef BOOTLOADER_PRODUCTION // Hard lock the Device Service Unit, i.e., set the PAC write-protection bit, // which can only be cleared by a hardware reset. Because the DSU is soft // locked by default on reset, an unlock is required before a hard lock. diff --git a/src/bootloader/bootloader.h b/src/bootloader/bootloader.h index d633ca9aa8..e9cc71303a 100644 --- a/src/bootloader/bootloader.h +++ b/src/bootloader/bootloader.h @@ -13,6 +13,11 @@ void bootloader_jump(void); */ void bootloader_render_default_screen(void); +/** + * Runs pending bootloader actions that must happen after a USB response was sent. + */ +void bootloader_process_pending_action(void); + #if PLATFORM_BITBOX02PLUS /** * Renders a BLE pairing confirmations screen. Use the `confirmed` argument to display the diff --git a/src/bootloader/startup.c b/src/bootloader/startup.c index 7b306d43eb..d2b36bcfc3 100644 --- a/src/bootloader/startup.c +++ b/src/bootloader/startup.c @@ -156,6 +156,9 @@ int main(void) // screen_sprintf_debug(1000, "got frame"); da14531_handler(frame, uart_write_queue); } + if (!hww_data && rust_bytequeue_num(uart_write_queue) == 0) { + bootloader_process_pending_action(); + } } #endif @@ -167,6 +170,9 @@ int main(void) hww_data = NULL; } } + if (!hww_data && !hid_hww_write_busy()) { + bootloader_process_pending_action(); + } #if PLATFORM_BITBOX02PLUS == 1 } #endif diff --git a/src/usb/class/hid/hww/hid_hww.c b/src/usb/class/hid/hww/hid_hww.c index f1c5ca74d7..7ad2e17b84 100644 --- a/src/usb/class/hid/hww/hid_hww.c +++ b/src/usb/class/hid/hww/hid_hww.c @@ -81,6 +81,11 @@ bool hid_hww_write_poll(const uint8_t* data) return false; } +bool hid_hww_write_busy(void) +{ + return _send_busy; +} + /** * The callback function is called after usb data has been received (endpoint = OUT). */ diff --git a/src/usb/class/hid/hww/hid_hww.h b/src/usb/class/hid/hww/hid_hww.h index a48d986b74..136f9845af 100644 --- a/src/usb/class/hid/hww/hid_hww.h +++ b/src/usb/class/hid/hww/hid_hww.h @@ -54,6 +54,11 @@ void hid_hww_setup(void); */ bool hid_hww_write_poll(const uint8_t* data); +/** + * Returns true while an outgoing HID report transfer is in flight. + */ +bool hid_hww_write_busy(void); + /** * Read data *