Fix nRF52 loop stack allocation under LTO

This commit is contained in:
mikecarper committed 2026-10-04 01:54:01 -07:00
1 parent 63f4642252
commit 8586ef5f51
4 files changed
+157 -1

No files matched your search

+1
View File
@@ -160,6 +160,7 @@ extra_scripts = create-uf2.py
pre:scripts/nrf52_internal_flash_fix.py
pre:scripts/nrf52_ble_cccd_fix.py
pre:scripts/nrf52_ble_dfu_fix.py
pre:scripts/nrf52_loop_stack_fix.py
pre:scripts/nrf52_internal_bootloader_link.py
; Nordic's builder otherwise overwrites the LTO-aware archive tools.
pre:scripts/nrf52_lto_toolchain.py
+56
View File
@@ -0,0 +1,56 @@
"""Make the nRF52 core's loop task use the configured stack even under LTO.
GNU --wrap only redirects undefined references. The pinned core and FreeRTOS
share an LTO graph, so its xTaskCreate call can bind directly to the real
function and silently retain the framework's 4 KiB stack. Explicitly route
only the core loop-task call through MeshCore's existing stack policy in a
private build copy. Never edit the shared framework package.
"""
from pathlib import Path
OLD_LOOP_CREATE = ' xTaskCreate(loop_task, "loop", LOOP_STACK_SZ, NULL, TASK_PRIO_LOW, &_loopHandle);'
LOOP_CREATE = OLD_LOOP_CREATE.replace("xTaskCreate(", "__wrap_xTaskCreate(", 1)
DECLARATION = """extern "C" BaseType_t __wrap_xTaskCreate(
TaskFunction_t task_code, const char* const task_name,
const configSTACK_DEPTH_TYPE stack_depth, void* const parameters,
UBaseType_t priority, TaskHandle_t* const created_task);
"""
LOOP_STACK = "#define LOOP_STACK_SZ (256*4)"
def patched_source(source):
source = source.replace("\r\n", "\n")
if (source.count(LOOP_CREATE) == 1 and source.count(DECLARATION) == 1
and OLD_LOOP_CREATE not in source and source.count(LOOP_STACK) == 1):
return source
if (source.count(OLD_LOOP_CREATE) != 1 or source.count(LOOP_STACK) != 1
or LOOP_CREATE in source or DECLARATION in source):
raise RuntimeError(
"nRF52 loop stack fix: unrecognized core task creation; review the "
"framework update before building (shared SDK not modified)"
)
return source.replace(LOOP_STACK, DECLARATION + LOOP_STACK).replace(
OLD_LOOP_CREATE, LOOP_CREATE)
def replace_core(build_env, node):
source = Path(node.srcnode().get_abspath())
patched = patched_source(source.read_text(encoding="utf-8"))
destination = Path(build_env.subst("$BUILD_DIR")) / "patched-nrf52-core" / "main.cpp"
destination.parent.mkdir(parents=True, exist_ok=True)
if not destination.exists() or destination.read_text(encoding="utf-8") != patched:
destination.write_text(patched, encoding="utf-8")
print("nRF52 loop stack: explicit LTO-safe core task policy enabled")
return build_env.File(str(destination))
def install(build_env):
build_env.AddBuildMiddleware(replace_core, "*cores*nRF5*main.cpp")
if "Import" in globals():
Import("env")
install(env)
+3 -1
View File
@@ -19,7 +19,9 @@ extern "C" BaseType_t __real_xTaskCreate(
// The Adafruit nRF52 core hard-codes the Arduino loop task to 1024 32-bit
// words (4 KiB). Mesh packet verification and authenticated remote commands
// can legitimately enter Ed25519 routines whose stack peaks do not fit there.
// Wrap task creation so only the framework's "loop" task receives 8 KiB; all
// nrf52_loop_stack_fix.py explicitly directs that core call here: GNU --wrap
// alone is bypassed when the caller and FreeRTOS share the LTO graph.
// Only the framework's "loop" task receives the configured 8 KiB; all
// BLE, timer, callback, and application-created tasks retain their requested
// sizes.
extern "C" BaseType_t __wrap_xTaskCreate(
+97
View File
@@ -0,0 +1,97 @@
"""Reproduce the LTO wrapper bypass and execute the patched core task policy."""
import importlib.util
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"nrf52_loop_stack_fix", ROOT / "scripts/nrf52_loop_stack_fix.py")
PATCH = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PATCH)
RTOS = """#pragma once
#include <cstdint>
using BaseType_t = int;
using UBaseType_t = unsigned;
using TaskHandle_t = void*;
using TaskFunction_t = void (*)(void*);
using configSTACK_DEPTH_TYPE = uint16_t;
constexpr BaseType_t pdPASS = 1;
extern "C" BaseType_t xTaskCreate(TaskFunction_t, const char*,
configSTACK_DEPTH_TYPE, void*, UBaseType_t, TaskHandle_t*);
"""
CORE = """#include "Arduino.h"
#include <cassert>
#include <cstddef>
#define LOOP_STACK_SZ (256*4)
static TaskHandle_t _loopHandle;
static void loop_task(void*) {}
static unsigned last_depth;
constexpr unsigned TASK_PRIO_LOW = 1;
extern "C" BaseType_t xTaskCreate(TaskFunction_t, const char*,
configSTACK_DEPTH_TYPE depth, void*, UBaseType_t, TaskHandle_t*) {
last_depth = depth;
return pdPASS;
}
int main() {
xTaskCreate(loop_task, "loop", LOOP_STACK_SZ, NULL, TASK_PRIO_LOW, &_loopHandle);
assert(last_depth == EXPECTED_LOOP_WORDS);
xTaskCreate(nullptr, "usbd", 200, nullptr, 3, nullptr);
assert(last_depth == 200);
xTaskCreate(nullptr, "callback", 768, nullptr, 1, nullptr);
assert(last_depth == 768);
}
"""
class Nrf52LoopStackFixTest(unittest.TestCase):
def test_patch_is_idempotent_and_rejects_changed_or_partial_core(self):
patched = PATCH.patched_source(CORE.replace("\n", "\r\n"))
self.assertEqual(PATCH.patched_source(patched), patched)
for source in (
CORE.replace("LOOP_STACK_SZ (256*4)", "LOOP_STACK_SZ 4096"),
CORE.replace(PATCH.OLD_LOOP_CREATE, PATCH.LOOP_CREATE),
CORE.replace(PATCH.LOOP_STACK, PATCH.DECLARATION + PATCH.LOOP_STACK),
CORE + "\n" + PATCH.OLD_LOOP_CREATE,
):
with self.subTest(source=source), self.assertRaises(RuntimeError):
PATCH.patched_source(source)
@unittest.skipUnless(shutil.which("c++"), "native C++ compiler unavailable")
def test_lto_bypass_is_reproduced_then_fixed_without_resizing_other_tasks(self):
with tempfile.TemporaryDirectory(prefix="meshcore-loop-stack-") as temp:
temp = Path(temp)
(temp / "Arduino.h").write_text(RTOS)
for name in ("FreeRTOS.h", "task.h"):
(temp / name).write_text('#include "Arduino.h"\n')
core, binary = temp / "main.cpp", temp / "core"
for patched, configured_words in ((False, 2048), (True, 2048), (True, 3072)):
with self.subTest(patched=patched, configured_words=configured_words):
core.write_text(PATCH.patched_source(CORE) if patched else CORE)
expected = configured_words if patched else 1024
built = subprocess.run([
"c++", "-std=c++17", "-O2", "-flto", "-flto-partition=one",
"-fno-semantic-interposition", "-DNRF52_PLATFORM=1",
f"-DMESH_NRF52_LOOP_STACK_WORDS={configured_words}",
f"-DEXPECTED_LOOP_WORDS={expected}", "-I" + str(temp),
str(core), str(ROOT / "src/Nrf52LoopStack.cpp"),
"-Wl,--wrap=xTaskCreate", "-o", str(binary),
], text=True, capture_output=True)
self.assertEqual(built.returncode, 0, built.stderr)
result = subprocess.run([str(binary)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_every_nrf52_build_uses_private_core_patch(self):
config = (ROOT / "platformio.ini").read_text()
nrf = config.split("[nrf52_base]", 1)[1].split("\n[", 1)[0]
self.assertIn("pre:scripts/nrf52_loop_stack_fix.py", nrf)
self.assertEqual(config.count("pre:scripts/nrf52_loop_stack_fix.py"), 1)
if __name__ == "__main__":
unittest.main()