Reset into BLE OTA DFU instead of jumping from the DFU callback (#12220)

The Adafruit BLEDfu handler switches to the main stack and jumps to the
bootloader. Since the nRF52 toolchain moved to GCC 12.3.1 (#12027) that
call is compiled as a tail call whose epilogue pops the task frame from the
main stack, past the end of RAM, and the CPU locks up; the bootloader then
starts with GPREGRET 0xB1 and no SoftDevice, faults, and resets into the
app. Handle START_DFU here instead: write the same peer data and leave
through enterOTADfu(), a reset with the 0xA8 magic that every bootloader
handles.
This commit is contained in:
James Rich authored and GitHub committed 2026-10-23 15:34:52 +00:00
1 parent 0d3da2dcda
commit e039e34171
1 file changed
+64 -5
+64 -5
View File
@@ -25,8 +25,8 @@ static int lastBatteryLevel = -1; // last value written to BAS, to skip redundan
#ifndef BLE_DFU_SECURE
namespace
{
// The library's DFU handler jumps to the bootloader from the callback task with no flash quiesce, so
// wrap it: wait out any write in flight first, and unlock again if it comes back without jumping.
// START_DFU is handled here rather than by the library: wait out any flash write in flight, then reset into the
// bootloader with the OTA magic instead of the library's jump, and unlock again if it returns without resetting.
class QuiescingBLEDfu : public BLEDfu
{
struct Peek : BLECharacteristic {
@@ -34,6 +34,65 @@ class QuiescingBLEDfu : public BLEDfu
};
static BLECharacteristic::write_authorize_cb_t libraryCb;
// The bootloader reads this from .noinit to reconnect to the same peer; layout and CRC match the library's.
struct PeerData {
ble_gap_addr_t addr;
ble_gap_irk_t irk;
ble_gap_enc_key_t enc_key;
uint8_t sys_attr[8];
uint16_t crc16;
};
static_assert(offsetof(PeerData, crc16) == 60, "bootloader peer data layout");
static uint16_t crc16(const uint8_t *data, uint8_t length)
{
uint16_t crc = 0xFFFF;
while (length--) {
uint8_t x = crc >> 8 ^ *data++;
x ^= x >> 4;
crc = (crc << 8) ^ (uint16_t)(x << 12) ^ (uint16_t)(x << 5) ^ (uint16_t)x;
}
return crc;
}
// Leaves through a reset, never the library's switch to the main stack and jump: the compiler may still pop the
// task's frame after the switch, from the wrong stack.
static void startDfu(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_write_t *request)
{
if (request->handle != chr->handles().value_handle || request->op == BLE_GATTS_OP_PREP_WRITE_REQ ||
request->op == BLE_GATTS_OP_EXEC_WRITE_REQ_NOW || request->op == BLE_GATTS_OP_EXEC_WRITE_REQ_CANCEL)
return;
BLEConnection *conn = Bluefruit.Connection(conn_hdl);
ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_WRITE};
if (!chr->notifyEnabled(conn_hdl)) {
reply.params.write.gatt_status = BLE_GATT_STATUS_ATTERR_CPS_CCCD_CONFIG_ERROR;
sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply);
return;
}
reply.params.write.gatt_status = BLE_GATT_STATUS_SUCCESS;
sd_ble_gatts_rw_authorize_reply(conn_hdl, &reply);
PeerData *peer = reinterpret_cast<PeerData *>(0x20007F80UL);
memset(peer, 0, sizeof(*peer));
uint16_t sysAttrLen = sizeof(peer->sys_attr);
sd_ble_gatts_sys_attr_get(conn_hdl, peer->sys_attr, &sysAttrLen, BLE_GATTS_SYS_ATTR_FLAG_SYS_SRVCS);
peer->addr = conn->getPeerAddr();
if (conn->secured()) {
bond_keys_t keys;
if (conn->loadBondKey(&keys)) {
peer->addr = keys.peer_id.id_addr_info;
peer->irk = keys.peer_id.id_info;
peer->enc_key = keys.own_enc;
}
}
peer->crc16 = crc16(reinterpret_cast<const uint8_t *>(peer), offsetof(PeerData, crc16));
Bluefruit.Advertising.restartOnDisconnect(false);
conn->disconnect();
enterOTADfu();
}
static void onControlWrite(uint16_t conn_hdl, BLECharacteristic *chr, ble_gatts_evt_write_t *request)
{
// Only START_DFU resets, and the library reads this byte without checking len, so match it exactly.
@@ -42,10 +101,10 @@ class QuiescingBLEDfu : public BLEDfu
return;
}
nrf52FlashQuiesce();
// The handler reloads the bond keys through LittleFS, so it cannot run under the FS mutex; spiLock still
// fences every other writer until the jump.
// Loading the bond keys goes through LittleFS, so it cannot run under the FS mutex; spiLock still fences
// every other writer until the reset.
InternalFS._unlockFS();
libraryCb(conn_hdl, chr, request);
startDfu(conn_hdl, chr, request);
spiLock->unlock();
}