Fix bootloader hardware bring-up and add key-based boot entry

Hardware-tested the UF2 bootloader end to end on a real VersaPad v2
board. Found and fixed a real bug: the bootloader's jump into the app
(__set_MSP -> SCB->VTOR -> bx) hard-faulted on every standalone boot,
even with the debugger fully disconnected; identical register/VTOR
values injected directly by a halted debugger ran fine, which pointed
at the missing __DSB()/__ISB() barriers ARM's own guidance requires
for this exact pattern. Also fixed a USB PID collision (0x0011 is
Adafruit's own Gemma M0 bootloader PID, misidentified by Windows as a
Circuit Playground COM port instead of exposing VERSABOOT).

This board has no dedicated reset/boot button, so add a hardware boot
entry that doesn't need one: holding the bottom-right Cherry MX key
(key_id 24) during reset/power-on drives its matrix row and reads its
column directly in the bootloader, before the app is even validated.

Also corrected the app-side flash_with_bootloader.ld (was missing the
NVM carve-out flash_without_bootloader.ld already has) and
boards/versapad.json (wrong flash/RAM size, wrong MCU macro, stale
PID), and enabled the previously-commented-out env:versapad_usb.

Documented findings in bootloader/README.md, bootloader/TESTING.md,
and doc/09_known_limitations.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Julian Appel 2026-08-05 21:28:15 +02:00
parent 4749adcd1d
commit f60a29137c
8 changed files with 234 additions and 82 deletions

View file

@ -87,6 +87,38 @@ extern int8_t led_tick_step;
#define RESET_CONTROLLER RSTC
#endif
#if defined(BOOT_KEY_ROW_PIN) && defined(BOOT_KEY_COL_PIN)
/**
* \brief Check whether the boot-hold key (bottom-right Cherry MX button) is
* held down. Drives its matrix row low and reads its matrix column back,
* the same polarity the app firmware's own matrix scan uses.
*/
static bool boot_key_pressed(void) {
PORT_PINCFG_Type col_cfg = {0};
col_cfg.bit.PMUXEN = false;
col_cfg.bit.INEN = true; // external 10k pull-up already on the board
PORT_PINCFG_Type row_cfg = {0};
row_cfg.bit.PMUXEN = false;
row_cfg.bit.DRVSTR = true;
PINCFG(BOOT_KEY_COL_PIN) = col_cfg.reg;
PINOP(BOOT_KEY_COL_PIN, DIRCLR); // column stays an input
PINOP(BOOT_KEY_ROW_PIN, OUTCLR); // pre-set drive level before enabling output
PINCFG(BOOT_KEY_ROW_PIN) = row_cfg.reg;
PINOP(BOOT_KEY_ROW_PIN, DIRSET); // row -> output, driving low
for (volatile int i = 0; i < 200; i++) {
} // let the row settle through the diode/pull-up RC
bool pressed = (PINIP(BOOT_KEY_COL_PIN) == 0);
PINOP(BOOT_KEY_ROW_PIN, DIRCLR); // release row back to high-Z
return pressed;
}
#endif
/**
* \brief Check the application startup condition
*
@ -94,6 +126,13 @@ extern int8_t led_tick_step;
static void check_start_application(void) {
uint32_t app_start_address;
#if defined(BOOT_KEY_ROW_PIN) && defined(BOOT_KEY_COL_PIN)
if (boot_key_pressed()) {
/* Stay in bootloader */
return;
}
#endif
// Check if there is an IO which will hold us inside the bootloader.
#if defined(HOLD_PIN) && defined(HOLD_STATE)
PORT_PINCFG_Type pincfg = {0};
@ -167,6 +206,14 @@ static void check_start_application(void) {
/* Rebase the vector table base address */
SCB->VTOR = ((uint32_t)APP_START_ADDRESS & SCB_VTOR_TBLOFF_Msk);
/* Ensure the MSP/VTOR writes are visible before jumping; without these
* barriers the app's first fetch can race the pipeline (observed on
* real hardware: identical MSP/VTOR/PC values injected by a halted
* debugger boot fine, but the bootloader's own running jump hard-faults
* every time). */
__DSB();
__ISB();
/* Jump to application Reset Handler in the application */
asm("bx %0" ::"r"(app_start_address));
}