Skip to content

nrf52: fix loop-task stack overflow causing reset on BLE pairing/first-sync - #10944

Merged
thebentern merged 3 commits into
developfrom
fix/nrf52-looptask-stack-overflow
Jul 8, 2026
Merged

nrf52: fix loop-task stack overflow causing reset on BLE pairing/first-sync#10944
thebentern merged 3 commits into
developfrom
fix/nrf52-looptask-stack-overflow

Conversation

@thebentern

@thebentern thebentern commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Fixes the 2.8 nRF52 crash where devices silently reset seconds after a phone connects/pairs (tracked in field reports as a T1000-E "bootloop on BLE connect"; also reproduced deterministically with set_time_only on an empty-DB node).

Root cause — SWD-proven

The Arduino loop task runs Meshtastic's entire cooperative OSThread scheduler on the Adafruit core's stock 4 KB (1024-word) stack (LOOP_STACK_SZ, previously not overridable). The 2.8 first-sync window stacks the deepest excursion:

AdminModule::handleSetConfig      (phone's routine config push on connect)
 └ saveChanges → configChanged → RadioInterface::reloadConfig
   └ LR11x0Interface::reconfigure → setStandby → SPIwriteStream
     └ LockingArduinoHal::spiBeginTransaction → concurrency::Lock::lock()
       └ xQueueSemaphoreTake(<corrupted handle>) → BusFault

…with _vsnprintf_r / USB-CDC logging frames interleaved on the same stack.

A hardware fault capture (RAKDAP + pyocd vector_catch=h, T1000-E, 100 % repro) showed:

  • loop task SP 40 bytes below its own pxStack base; stack paint (0xa5a5a5a5) consumed to the floor
  • the fatal dereference was a FreeRTOS semaphore handle overwritten by adjacent-heap corruption with log-line text (0x3f3f207c = "| ??")
  • CFSR=0x8200 (precise BusFault), BFAR = handle+0x38

The core's HardFault handler is a bare NVIC_SystemReset, so in the field this is a silent instant reboot (log severed mid-line); when the corruption lands on task TCBs instead, it's a 30 s zombie hang ending in a supervision-timeout 0x8 disconnect. 2.7.26 is unaffected because the deep frames (XEdDSA signing, satellite-map conversion, replay engine) didn't exist yet.

Fix

  • -DLOOP_STACK_SZ=2048 (words = 8 KB) for all nrf52840 targets. Validated on hardware: pairing + full sync completes; stack paint shows healthy margin under live BLE load.
  • Depends on Allow build-flag override of loop/callback task stack sizes Adafruit_nRF52_Arduino#7 (adds the #ifndef guard; merge that first — until then this flag is a harmless macro-redefinition warning with no effect).
  • Drive-by fix: the boot Reset reason: log has always printed 0x0 because the core's init() caches-and-clears RESETREAS before setup() runs — switched to readResetReason(). (preFSBegin()'s RESETREAS==0 gate is deliberately untouched; its degenerate GPREGRET-only behavior is what makes lfs recovery work today.)

RAM cost: 4 KB from the heap arena per nrf52840 device — more than covered by the +8 KB reclaimed in #10903.

Verification

  • tracker-t1000-e and rak4631 build green; mov.w r2, #2048 confirmed in both binaries
  • T1000-E hardware: previously 100 %-reproducible pairing crash gone; full app sync completes and stays connected; fault trap (vector catch) stayed silent through the whole session

Summary by CodeRabbit

  • Bug Fixes
    • Improved reset logging so device restarts now report a more accurate reset reason.
    • Increased stability during Bluetooth pairing on the nRF52840 by raising the loop-task stack size to prevent silent crashes and unexpected resets.

…t-sync

The Arduino loop task runs Meshtastic's entire cooperative OSThread
scheduler on the Adafruit core's stock 4 KB (1024-word) stack. The 2.8
first-sync path - AdminModule::handleSetConfig -> saveChanges ->
configChanged -> RadioInterface::reloadConfig -> LR11x0 reconfigure ->
SPI Lock::lock, with vsnprintf/USB-CDC logging frames stacked on top -
overflows it.

SWD fault capture on a T1000-E (pyocd vector_catch=h) proved it: the
loop task's SP was driven 40 bytes BELOW its own pxStack base, the
stack paint was consumed to the floor, and the crash was a BusFault in
xQueueSemaphoreTake dereferencing a semaphore handle that adjacent-heap
corruption had overwritten with log text (0x3f3f207c = "| ??"). The
core's HardFault handler is a bare NVIC_SystemReset, so in the field
this presents as a silent reboot ~seconds after a phone pairs (or a 30 s
zombie hang -> supervision timeout 0x8 when the corruption lands on task
TCBs instead). Also explains the set_time_only -> immediate-NodeInfo
crash variant reported on empty-DB nodes: same task, different deep
chain. 2.7.26 is unaffected because the deep frames (XEdDSA, satellite
map conversion, replay engine) did not exist.

Fix: set -DLOOP_STACK_SZ=2048 (words = 8 KB) for all nrf52840 targets.
Requires the #ifndef guard from meshtastic/Adafruit_nRF52_Arduino#7;
until that merges the flag is a harmless redefinition warning. Validated
on hardware: pairing + full sync completes, stack paint shows healthy
margin under load.

Also fix the boot-time reset-reason log: the core's init() caches
RESETREAS and W1C-clears the register before setup() runs, so the raw
read here has always printed 0. Use readResetReason() instead.
(preFSBegin's RESETREAS==0 gate is intentionally left as-is - its
degenerate GPREGRET-only behavior is what makes lfs recovery work.)
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Note

Building this pull request… the flash button, badges and supported-board
list will appear here automatically once CI finishes.

@github-actions github-actions Bot added the bugfix Pull request that fixes bugs label Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 15d9bcbe-71ea-4d5e-9f03-94f042d76c1f

📥 Commits

Reviewing files that changed from the base of the PR and between 3937a26 and 51f6c77.

📒 Files selected for processing (1)
  • variants/nrf52840/nrf52.ini
🚧 Files skipped from review as they are similar to previous changes (1)
  • variants/nrf52840/nrf52.ini

📝 Walkthrough

Walkthrough

This PR changes nRF52 reset-reason logging to use readResetReason() and adds LOOP_STACK_SZ=2048 to the nrf52840 build flags.

Changes

NRF52 platform adjustments

Layer / File(s) Summary
Reset reason logging source change
src/platform/nrf52/main-nrf52.cpp
nrf52Setup() now sets why from readResetReason() instead of NRF_POWER->RESETREAS, with comments describing the cached and cleared register behavior.
Loop stack size build flag
variants/nrf52840/nrf52.ini
variants/nrf52840/nrf52.ini adds -DLOOP_STACK_SZ=2048 and comments about the stack size value and upstream redefinition warning.

Estimated code review effort: 1 (Trivial) | ~5 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: increasing nRF52 loop-task stack size to fix BLE pairing resets.
Description check ✅ Passed The description is substantive and covers the issue, root cause, fix, and verification, though it does not use the template's checkbox attestations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/nrf52-looptask-stack-overflow

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/platform/nrf52/main-nrf52.cpp (1)

381-385: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim the comment to comply with the repo's comment-length guideline.

The functional change (using readResetReason()) is correct and well-justified, but the accompanying comment spans 4 lines, exceeding the repo's comment-length limit.

✏️ Suggested trim
-    // The Adafruit core's init() (cores/nRF5/wiring.c) caches RESETREAS into a static and then
-    // W1C-clears the hardware register before setup() ever runs, so a raw NRF_POWER->RESETREAS
-    // read here is ALWAYS 0. Use the core's cached copy so this log line is actually meaningful
-    // (0x1 pin reset, 0x2 watchdog, 0x4 soft reset/SREQ, 0x8 CPU lockup, 0x10000 System OFF wake).
+    // Adafruit core's init() caches+clears RESETREAS before setup() runs, so a raw
+    // register read here is always 0; use the core's cached copy instead.
     uint32_t why = readResetReason();

As per coding guidelines, "Keep code comments minimal: one or two lines მაქsimum, only when the reason is not obvious, and do not restate the next line."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/nrf52/main-nrf52.cpp` around lines 381 - 385, Trim the
explanatory comment above readResetReason() in main-nrf52.cpp so it fits the
repo’s one- or two-line comment guideline. Keep only the essential reason for
using the cached reset reason instead of raw NRF_POWER->RESETREAS, and remove
the extra detail/list of reset codes while preserving the functional context
around the why variable assignment.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@variants/nrf52840/nrf52.ini`:
- Around line 26-32: Pin the nRF52 Arduino platform source to an immutable
commit or tag instead of relying on the moving default branch, and keep the
LOOP_STACK_SZ override in place. Update the platform_packages entry in the same
config area that defines the nrf52840 variant so it references a fixed revision
rather than the current master-style target, using the existing nrf52.ini
package configuration to locate it.

---

Nitpick comments:
In `@src/platform/nrf52/main-nrf52.cpp`:
- Around line 381-385: Trim the explanatory comment above readResetReason() in
main-nrf52.cpp so it fits the repo’s one- or two-line comment guideline. Keep
only the essential reason for using the cached reset reason instead of raw
NRF_POWER->RESETREAS, and remove the extra detail/list of reset codes while
preserving the functional context around the why variable assignment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 563a6986-e9de-43d0-a723-e7e9476c4018

📥 Commits

Reviewing files that changed from the base of the PR and between fcea931 and 3937a26.

📒 Files selected for processing (2)
  • src/platform/nrf52/main-nrf52.cpp
  • variants/nrf52840/nrf52.ini

Comment thread variants/nrf52840/nrf52.ini
@thebentern
thebentern merged commit b4b1ece into develop Jul 8, 2026
82 of 83 checks passed
@thebentern
thebentern deleted the fix/nrf52-looptask-stack-overflow branch July 8, 2026 23:51
thebentern added a commit that referenced this pull request Jul 24, 2026
…FS (#11190)

Since #10967 made Router::sendLocal handle self-addressed packets
synchronously, the entire phone-API chain for a BLE client runs inline in
the Bluefruit characteristic write callback: toRadioWriteCb ->
PhoneAPI::handleToRadio -> admin set-config -> radio reconfigure ->
NodeDB::saveToDisk. That callback executes on the Bluefruit BLE FreeRTOS
task, whose stock stack is 5 KB (CFG_BLE_TASK_STACKSIZE = 256*5 words) -
not the Arduino loop task that #10944 already raised to 8 KB. The loop-task
fix therefore protects the wrong task for BLE-originated writes.

On a Seeed Wio Tracker L1 the 5 KB stack overflows during pairing
first-sync, resetting the device mid-LittleFS-write, every single time.
Repeated mid-write resets tear the LittleFS metadata, lfs_assert fires on
the next boot, and the corruption handler formats the whole filesystem:
region, channels, module config, and the node's keypair are all lost
(critical fault #13, new node identity on next region set). Reproduced
end-to-end tonight on stock develop 6908d27; with this change the same
device pairs, serves config screens, and survives back-to-back
config.proto saves over BLE.

Raise the BLE task to the same 2048 words (8 KB) as LOOP_STACK_SZ, for the
same reason. bluefruit.cpp's #ifndef guard makes the -D take effect with no
framework patch. Costs 3 KB of RAM on nrf52840 targets only.

Credit where due: Ixitxachitl independently established in #11155 testing
that the save-path crash persists after #11185 and that re-queueing
sendLocal (moving the pipeline back to the Router thread) makes it go away
- which corroborates this diagnosis from the other direction. This commit
is the minimal capacity-side fix; #11155's relocation of the pipeline off
the BLE task remains the right architectural follow-up, and this guard
stays correct even after it lands.

Likely also explains #10905 (L1 display-thread crash when a client
requests full configuration) and the 2.8 field reports of idle nodes
losing region and keys after a BLE session.
Itzdavid01 pushed a commit to Itzdavid01/firmware that referenced this pull request Sep 5, 2026
…t-sync (meshtastic#10944)

The Arduino loop task runs Meshtastic's entire cooperative OSThread
scheduler on the Adafruit core's stock 4 KB (1024-word) stack. The 2.8
first-sync path - AdminModule::handleSetConfig -> saveChanges ->
configChanged -> RadioInterface::reloadConfig -> LR11x0 reconfigure ->
SPI Lock::lock, with vsnprintf/USB-CDC logging frames stacked on top -
overflows it.

SWD fault capture on a T1000-E (pyocd vector_catch=h) proved it: the
loop task's SP was driven 40 bytes BELOW its own pxStack base, the
stack paint was consumed to the floor, and the crash was a BusFault in
xQueueSemaphoreTake dereferencing a semaphore handle that adjacent-heap
corruption had overwritten with log text (0x3f3f207c = "| ??"). The
core's HardFault handler is a bare NVIC_SystemReset, so in the field
this presents as a silent reboot ~seconds after a phone pairs (or a 30 s
zombie hang -> supervision timeout 0x8 when the corruption lands on task
TCBs instead). Also explains the set_time_only -> immediate-NodeInfo
crash variant reported on empty-DB nodes: same task, different deep
chain. 2.7.26 is unaffected because the deep frames (XEdDSA, satellite
map conversion, replay engine) did not exist.

Fix: set -DLOOP_STACK_SZ=2048 (words = 8 KB) for all nrf52840 targets.
Requires the #ifndef guard from meshtastic/Adafruit_nRF52_Arduino#7;
until that merges the flag is a harmless redefinition warning. Validated
on hardware: pairing + full sync completes, stack paint shows healthy
margin under load.

Also fix the boot-time reset-reason log: the core's init() caches
RESETREAS and W1C-clears the register before setup() runs, so the raw
read here has always printed 0. Use readResetReason() instead.
(preFSBegin's RESETREAS==0 gate is intentionally left as-is - its
degenerate GPREGRET-only behavior is what makes lfs recovery work.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant