On nRF52832 with Espruino's SDK12 DFU bootloader, DFU-pushing a sub-page-sized application (≲4 KB) twice in succession produces a corrupted bank_1 staging region. The first push works; the second push completes successfully according to the Web IDE, and the device reboots but immediately re-enters DfuTarg and stays there — the new app's reset vector lands at a corrupted address and HardFaults on boot.
The corrupted bytes at the bank_1 staging address (0x20000 when bank_0 is a 1-page app) are a strict bit-subset of the expected .bin bytes — NVMC's 1→0-only programming applied to two writes at the same flash address.
To Reproduce:
- Flash an MDBT42Q with stock Espruino: SoftDevice S132 v3.1.0 + Espruino SDK12 bootloader + Espruino app firmware (~276 KB).
- Build a ~1.9 KB application that fits in one flash page. Package as a signed DFU zip:
nrfutil pkg generate --application-version 0xff --hw-version 52 --sd-req 0x8C,0x91 ...
- Enter DFU mode (BTN1-hold + power-cycle on the MDBT42Q breakout). Push the zip via Web IDE. Succeeds.
- Re-enter DFU the same way, push the same zip again. Tool reports success.
- Device is stuck in
DfuTarg. LED stuck on (corrupted reset vector faults immediately).
SWD-read at 0x20000 to confirm:
expected (.bin first 32 bytes):
20010000 0001F629 0001F651 0001F653 0001F655 0001F657 0001F659 00000000
observed after failed push #2:
00000000 0001F628 0000B000 0000B400 0000F654 00002000 00004610 00000000
For every word, observed & expected == observed — the NVMC AND-of-two-writes signature.
One possible mechanism:
dfu_req_handling.c receives DFU data into a 4-slot cyclic buffer (m_data_buf[4][256]) and queues each filled slot to fstorage via nrf_dfu_flash_store(), passing a pointer to the slot rather than a copy. On a small transfer that fits in fewer chunks than fstorage can drain in time, the producer wraps the buffer index and overwrites a slot whose earlier store is still in flight. This is one plausible path to two writes landing at the same flash address with different intended payloads.
A targeted fix would copy each chunk into a dedicated single-owner buffer before calling nrf_dfu_flash_store(), or otherwise gate buffer reuse on m_flash_operations_pending draining. SDK15's nrf_dfu_req_handler.c rewrite addresses this class architecturally.
Related / adjacent reports
Workaround
Pad the application past ~70 KB (not sure the exact size necessary) before packaging the DFU zip. Larger transfers don't exhibit the bug. Use a forced .rodata array referenced via a runtime-indexed read so the compiler/linker can't strip it:
const uint8_t pad[80 * 1024] = { 0xAA }; // first byte non-zero forces .rodata, not .bss
// in main():
pad_keepalive = pad[NRF_FICR->DEVICEID[0] % sizeof(pad)]; // runtime index defeats constant folding
Three back-to-back DFU pushes of an >80 KB app empirically work; the same code without padding fails 100% on the second push.
Environment
- Chip: nRF52832 (MDBT42Q module)
- SoftDevice: S132 v3.1.0 (FWID
0x8C, also 0x91)
- SDK: nRF5 SDK 12
- Bootloader source:
Espruino/targets/nrf5x_dfu/sdk12/
- Tools:
nrfutil ≥ 8.x, Espruino Web IDE for DFU uploads
On nRF52832 with Espruino's SDK12 DFU bootloader, DFU-pushing a sub-page-sized application (≲4 KB) twice in succession produces a corrupted bank_1 staging region. The first push works; the second push completes successfully according to the Web IDE, and the device reboots but immediately re-enters
DfuTargand stays there — the new app's reset vector lands at a corrupted address and HardFaults on boot.The corrupted bytes at the bank_1 staging address (
0x20000when bank_0 is a 1-page app) are a strict bit-subset of the expected.binbytes — NVMC's 1→0-only programming applied to two writes at the same flash address.To Reproduce:
DfuTarg. LED stuck on (corrupted reset vector faults immediately).SWD-read at
0x20000to confirm:For every word,
observed & expected == observed— the NVMC AND-of-two-writes signature.One possible mechanism:
dfu_req_handling.creceives DFU data into a 4-slot cyclic buffer (m_data_buf[4][256]) and queues each filled slot to fstorage vianrf_dfu_flash_store(), passing a pointer to the slot rather than a copy. On a small transfer that fits in fewer chunks than fstorage can drain in time, the producer wraps the buffer index and overwrites a slot whose earlier store is still in flight. This is one plausible path to two writes landing at the same flash address with different intended payloads.A targeted fix would copy each chunk into a dedicated single-owner buffer before calling
nrf_dfu_flash_store(), or otherwise gate buffer reuse onm_flash_operations_pendingdraining. SDK15'snrf_dfu_req_handler.crewrite addresses this class architecturally.Related / adjacent reports
dfu_req_handling.c's buffer scheme on SDK13.nrf_fstorage_is_busy()polling as the mitigation pattern.app_sched_executein DFU — SDK13→14 DFU rework context.dfu_req_handling.c→nrf_dfu_req_handler.crewrite (request handling moved toapp_scheduler, explicit drain barriers).Workaround
Pad the application past ~70 KB (not sure the exact size necessary) before packaging the DFU zip. Larger transfers don't exhibit the bug. Use a forced
.rodataarray referenced via a runtime-indexed read so the compiler/linker can't strip it:Three back-to-back DFU pushes of an >80 KB app empirically work; the same code without padding fails 100% on the second push.
Environment
0x8C, also0x91)Espruino/targets/nrf5x_dfu/sdk12/nrfutil≥ 8.x, Espruino Web IDE for DFU uploads