Fix nRF52 loop stack allocation under LTO
This commit is contained in:
1 parent
63f4642252
commit
8586ef5f51
4 files changed
+157
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user