From ea7d4aa4101f2b90568c2a5d30449e8900a83abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 15 Sep 2026 07:16:33 +0000 Subject: [PATCH] Port nRF54L15 to the s145 SoftDevice Arduino core (#11842) * Remove the Zephyr based nRF54L15 port * Add nRF54L15 port on the s145 Arduino core: nrf54l15dk and xiao_nrf54l15 variants * nRF54L: errno-style nrfx results, flush console before assert reset * nRF54L: log the SoftDevice status on Bluefruit failure, ignore the seed request event * Support the Wio-LR2021 LoRa Plus expansion board with OLED and K1 on the XIAO nRF54L15 variant * Consume the nRF54L15 platform, core and bootloader from their repositories * Pin the nRF54L15 platform to v0.2.0 * nRF52: forward SoftDevice flash events taken by the main loop to the flash driver, log the pairing failure status * Pin the nRF54L15 platform to v0.2.1 * Pin the nRF54L15 platform to v0.3.0 * Split the XIAO nRF54L15 variant into SX1262 and LoRa Plus environments, seed the SoftDevice on request, add the nrf54l15 CI build script * Pin the nRF54L15 platform to meshtastic/platform-nordicnrf54 v0.3.1 * NRF54: Fix mtjson generation --------- Co-authored-by: vidplace7 --- bin/build-nrf54l15.sh | 36 + bin/platformio-custom.py | 3 + boards/nrf54l15dk.json | 26 - extra_scripts/nrf54l15_linker.py | 140 --- platformio.ini | 1 - src/FSCommon.cpp | 4 + src/FSCommon.h | 8 - src/RedirectablePrint.cpp | 4 - src/freertosinc.h | 2 +- src/main.cpp | 12 - src/main.h | 4 - src/mesh/HardwareRNG.cpp | 11 +- src/modules/AdminModule.cpp | 7 - src/platform/nrf52/BLEDfuSecure.cpp | 5 + src/platform/nrf52/NRF52Bluetooth.cpp | 14 +- src/platform/nrf52/architecture.h | 4 +- src/platform/nrf52/hardfault.cpp | 2 +- src/platform/nrf52/main-nrf52.cpp | 75 +- src/platform/nrf54l15/Arduino.h | 835 ------------------ src/platform/nrf54l15/IPAddress.h | 34 - src/platform/nrf54l15/InternalFileSystem.cpp | 274 ------ src/platform/nrf54l15/InternalFileSystem.h | 212 ----- src/platform/nrf54l15/NRF52Bluetooth.h | 18 - src/platform/nrf54l15/NRF54L15Bluetooth.cpp | 806 ----------------- src/platform/nrf54l15/NRF54L15Bluetooth.h | 29 - src/platform/nrf54l15/Nrf52SaadcLock.h | 17 - src/platform/nrf54l15/Print.h | 4 - src/platform/nrf54l15/SPI.h | 62 -- src/platform/nrf54l15/Stream.h | 5 - src/platform/nrf54l15/Tone.h | 4 - src/platform/nrf54l15/WProgram.h | 5 - src/platform/nrf54l15/Wire.cpp | 219 ----- src/platform/nrf54l15/Wire.h | 83 -- src/platform/nrf54l15/architecture.h | 79 -- src/platform/nrf54l15/bluefruit.h | 19 - src/platform/nrf54l15/main-nrf54l15.cpp | 222 ----- src/platform/nrf54l15/nrf54l15_arduino.cpp | 557 ------------ src/platform/nrf54l15/nrf54l15_main.cpp | 121 --- src/platform/nrf54l15/utility/bonding.h | 11 - variants/nrf54l15/cpp_overrides/lfs_assert.h | 18 + variants/nrf54l15/nrf54l15.ini | 99 +-- variants/nrf54l15/nrf54l15dk/README.md | 109 --- variants/nrf54l15/nrf54l15dk/platformio.ini | 8 +- variants/nrf54l15/nrf54l15dk/variant.cpp | 18 +- variants/nrf54l15/nrf54l15dk/variant.h | 144 +-- .../nrf54l15/xiao_nrf54l15/platformio.ini | 34 + variants/nrf54l15/xiao_nrf54l15/variant.cpp | 22 + variants/nrf54l15/xiao_nrf54l15/variant.h | 140 +++ .../boards/nrf54l15dk_nrf54l15_cpuapp.overlay | 117 --- zephyr/prj.conf | 299 ------- 50 files changed, 475 insertions(+), 4507 deletions(-) create mode 100755 bin/build-nrf54l15.sh delete mode 100644 boards/nrf54l15dk.json delete mode 100644 extra_scripts/nrf54l15_linker.py delete mode 100644 src/platform/nrf54l15/Arduino.h delete mode 100644 src/platform/nrf54l15/IPAddress.h delete mode 100644 src/platform/nrf54l15/InternalFileSystem.cpp delete mode 100644 src/platform/nrf54l15/InternalFileSystem.h delete mode 100644 src/platform/nrf54l15/NRF52Bluetooth.h delete mode 100644 src/platform/nrf54l15/NRF54L15Bluetooth.cpp delete mode 100644 src/platform/nrf54l15/NRF54L15Bluetooth.h delete mode 100644 src/platform/nrf54l15/Nrf52SaadcLock.h delete mode 100644 src/platform/nrf54l15/Print.h delete mode 100644 src/platform/nrf54l15/SPI.h delete mode 100644 src/platform/nrf54l15/Stream.h delete mode 100644 src/platform/nrf54l15/Tone.h delete mode 100644 src/platform/nrf54l15/WProgram.h delete mode 100644 src/platform/nrf54l15/Wire.cpp delete mode 100644 src/platform/nrf54l15/Wire.h delete mode 100644 src/platform/nrf54l15/architecture.h delete mode 100644 src/platform/nrf54l15/bluefruit.h delete mode 100644 src/platform/nrf54l15/main-nrf54l15.cpp delete mode 100644 src/platform/nrf54l15/nrf54l15_arduino.cpp delete mode 100644 src/platform/nrf54l15/nrf54l15_main.cpp delete mode 100644 src/platform/nrf54l15/utility/bonding.h create mode 100644 variants/nrf54l15/cpp_overrides/lfs_assert.h delete mode 100644 variants/nrf54l15/nrf54l15dk/README.md create mode 100644 variants/nrf54l15/xiao_nrf54l15/platformio.ini create mode 100644 variants/nrf54l15/xiao_nrf54l15/variant.cpp create mode 100644 variants/nrf54l15/xiao_nrf54l15/variant.h delete mode 100644 zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay delete mode 100644 zephyr/prj.conf diff --git a/bin/build-nrf54l15.sh b/bin/build-nrf54l15.sh new file mode 100755 index 0000000000..9ff90384f7 --- /dev/null +++ b/bin/build-nrf54l15.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -e + +VERSION=$(bin/buildinfo.py long) + +BUILDDIR=.pio/build/$1 +OUTDIR=release + +rm -f $OUTDIR/firmware* +rm -r $OUTDIR/* || true + +# Important to pull latest version of libs into all device flavors, otherwise some devices might be stale +platformio pkg install -e $1 + +echo "Building for $1 with $PLATFORMIO_BUILD_FLAGS" +rm -f $BUILDDIR/firmware* + +# The shell vars the build tool expects to find +export APP_VERSION=$VERSION + +basename=firmware-$1-$VERSION +ota_basename=${basename}-ota + +pio run --environment $1 -t dfu -t mtjson # -v + +cp $BUILDDIR/$basename.elf $OUTDIR/$basename.elf + +echo "Copying merged hex (bootloader + SoftDevice + application)" +cp $BUILDDIR/$basename.hex $OUTDIR/$basename.hex + +echo "Copying nRF54L dfu (OTA) file" +cp $BUILDDIR/$basename.zip $OUTDIR/$ota_basename.zip + +echo "Copying manifest" +cp $BUILDDIR/$basename.mt.json $OUTDIR/$basename.mt.json || true diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index bd2d3fc82e..7ded02b121 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -41,8 +41,11 @@ def infer_architecture(board_cfg): return "rp2350" if "nrf52" in mcu_l or "nrf52840" in mcu_l: return "nrf52840" + if "nrf54l15" in mcu_l: + return "nrf54l15" if "stm32" in mcu_l: return "stm32" + print(f"mtjson: could not infer architecture from MCU '{mcu_l}'") return None def run_size_tool(env, flag, purpose): diff --git a/boards/nrf54l15dk.json b/boards/nrf54l15dk.json deleted file mode 100644 index 863ad290b4..0000000000 --- a/boards/nrf54l15dk.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "build": { - "cpu": "cortex-m33", - "f_cpu": "128000000L", - "mcu": "nrf54l15", - "zephyr": { - "variant": "nrf54l15dk/nrf54l15/cpuapp" - } - }, - "connectivity": ["bluetooth"], - "debug": { - "default_tools": ["jlink"], - "jlink_device": "nRF54L15_M33", - "svd_path": "nrf54l15.svd" - }, - "frameworks": ["zephyr"], - "name": "Nordic nRF54L15-DK (PCA10156)", - "upload": { - "maximum_ram_size": 262144, - "maximum_size": 1572864, - "protocol": "jlink", - "protocols": ["jlink"] - }, - "url": "https://www.nordicsemi.com/Products/nRF54L15", - "vendor": "Nordic Semiconductor" -} diff --git a/extra_scripts/nrf54l15_linker.py b/extra_scripts/nrf54l15_linker.py deleted file mode 100644 index 824aae7ce3..0000000000 --- a/extra_scripts/nrf54l15_linker.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -# trunk-ignore-all(ruff/F821) -# trunk-ignore-all(flake8/F821): For SConstruct imports -# -# post:extra_scripts/nrf54l15_linker.py -# -# Fix for Zephyr two-pass link on nRF54L15: -# platformio-build.py registers env.Depends("$PROG_PATH", final_ld_script) but -# the SCons dependency chain is broken (final_ld_script Command never runs). -# This script adds a PreAction on the final firmware binary that runs the gcc -# preprocessing command directly (extracted from build.ninja) to generate -# zephyr/linker.cmd before the link step. -# -# PlatformIO bundles an old Ninja that can't handle multi-output depslog rules, -# so we parse the COMMAND line from build.ninja and run just the gcc -E part, -# skipping the cmake_transform_depfile step (only needed for Ninja deps tracking). - -import os -import re -import subprocess - -Import("env") - -if env.get("PIOENV") != "nrf54l15dk": - pass # Only for the nrf54l15dk environment -else: - - def _extract_gcc_command(ninja_build): - """Parse build.ninja to find the gcc -E command that generates linker.cmd. - - The rule format depends on the host: - Windows (CMake's RunCMake wraps every command): - COMMAND = cmd.exe /C "cd /D DIR && arm-none-eabi-gcc.exe ... -o linker.cmd && cmake.exe -E cmake_transform_depfile ..." - POSIX (Linux/macOS - no wrapper): - COMMAND = cd DIR && arm-none-eabi-gcc ... -o linker.cmd && cmake -E cmake_transform_depfile ... - - Returns (gcc_cmd_string, cwd_path) or raises RuntimeError. - """ - in_rule = False - with open(ninja_build, "r", encoding="utf-8", errors="replace") as f: - for line in f: - # Detect start of the linker.cmd custom command rule - if not in_rule: - if "build zephyr/linker.cmd" in line and "CUSTOM_COMMAND" in line: - in_rule = True - continue - - stripped = line.strip() - if not stripped.startswith("COMMAND = "): - continue - - command_val = stripped[len("COMMAND = ") :] - - # On Windows the value is wrapped in `cmd.exe /C "..."` - strip - # the wrapper. On POSIX hosts the inner sequence is the value - # itself (no quoting layer). - m = re.search(r'/C\s+"(.*)"\s*$', command_val) - inner = m.group(1) if m else command_val - parts = inner.split(" && ") - - cwd = None - gcc_cmd = None - for part in parts: - part = part.strip() - if part.startswith("cd /D "): # Windows form - cwd = part[len("cd /D ") :] - elif part.startswith("cd "): # POSIX form - cwd = part[len("cd ") :] - elif "arm-none-eabi-gcc" in part: - gcc_cmd = part - - if not gcc_cmd: - raise RuntimeError( - "nRF54L15 linker fix: arm-none-eabi-gcc command not found in:\n%s" - % inner[:400] - ) - - return gcc_cmd, cwd - - raise RuntimeError( - "nRF54L15 linker fix: 'build zephyr/linker.cmd' rule not found in build.ninja" - ) - - def _generate_linker_cmd(target, source, env): - """Generate zephyr/linker.cmd via direct gcc invocation before the final link.""" - build_dir = env.subst("$BUILD_DIR") - zephyr_dir = os.path.join(build_dir, "zephyr") - linker_cmd = os.path.join(zephyr_dir, "linker.cmd") - - if os.path.exists(linker_cmd): - return # Already present - nothing to do - - ninja_build = os.path.join(build_dir, "build.ninja") - if not os.path.exists(ninja_build): - raise RuntimeError( - "nRF54L15 linker fix: build.ninja not found at %s\n" - "Run a full build first so CMake generates the Ninja files." - % ninja_build - ) - - gcc_cmd, cwd = _extract_gcc_command(ninja_build) - run_cwd = cwd if cwd else zephyr_dir - - print( - "==> nRF54L15: Generating zephyr/linker.cmd (LINKER_ZEPHYR_FINAL) via GCC" - ) - # gcc_cmd comes verbatim from our own build.ninja (never user input) and - # contains Windows-style paths with spaces that cannot be safely argv-split - # with shlex, so we run it via the platform shell. nosec/nosemgrep below - # acknowledge this deliberate, scoped use of shell=True. - result = subprocess.run( # nosec B602 - gcc_cmd, - shell=True, # nosemgrep: python.lang.security.audit.subprocess-shell-true.subprocess-shell-true - cwd=run_cwd, - capture_output=True, - text=True, - ) - if result.returncode != 0: - print("GCC stdout:", result.stdout[:2000]) - print("GCC stderr:", result.stderr[:2000]) - raise RuntimeError( - "nRF54L15 linker fix: GCC failed to generate linker.cmd (rc=%d)" - % result.returncode - ) - if not os.path.exists(linker_cmd): - raise RuntimeError( - "nRF54L15 linker fix: GCC returned 0 but linker.cmd was not created at %s" - % linker_cmd - ) - print("==> linker.cmd generated successfully") - - # Use PIOMAINPROG (set by ZephyrBuildProgram) to get the exact SCons node - prog = env.get("PIOMAINPROG") - if prog: - env.AddPreAction(prog, _generate_linker_cmd) - else: - print( - "[nrf54l15_linker] WARNING: PIOMAINPROG not set, falling back to $PROG_PATH" - ) - env.AddPreAction(env.subst("$PROG_PATH"), _generate_linker_cmd) diff --git a/platformio.ini b/platformio.ini index f061da4e91..aa3e8f9ed6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,7 +27,6 @@ extra_scripts = pre:bin/platformio-pre.py pre:bin/optional-modules.py bin/platformio-custom.py - post:extra_scripts/nrf54l15_linker.py ; note: we add src to our include search path so that lmic_project_config can override ; note: TINYGPS_OPTION_NO_CUSTOM_FIELDS is VERY important. We don't use custom fields and somewhere in that pile ; of code is a heap corruption bug! diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 0bba740116..b6269141b6 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -40,7 +40,11 @@ size_t fsUsedBytes() return 0; size_t blocks = 0; FSCom._lockFS(); +#if LFS_VERSION_MAJOR >= 2 + int err = lfs_fs_traverse(fs, fsCountBlockCb, &blocks); +#else int err = lfs_traverse(fs, fsCountBlockCb, &blocks); +#endif FSCom._unlockFS(); if (err < 0) return fsTotalBytes(); // report "full" so capacity checks fail safe diff --git a/src/FSCommon.h b/src/FSCommon.h index 7daa57ad90..de4322579e 100644 --- a/src/FSCommon.h +++ b/src/FSCommon.h @@ -48,14 +48,6 @@ using namespace STM32_LittleFS_Namespace; using namespace Adafruit_LittleFS_Namespace; #endif -#if defined(ARCH_NRF54L15) -// nRF54L15 - Zephyr LittleFS on 36 KB storage_partition (internal RRAM) -#include "InternalFileSystem.h" -#define FSCom InternalFS -#define FSBegin() FSCom.begin() -using namespace Adafruit_LittleFS_Namespace; -#endif - // Filesystem capacity, in bytes. Only ESP32's LittleFS and the nRF54L15 wrapper expose totalBytes()/usedBytes() // directly; the other backends need per-platform work (littlefs v1 traversal, FSInfo, statvfs), so callers must // use these helpers rather than reaching into FSCom. diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 66a266d960..e4a56bb4af 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -235,8 +235,6 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_ isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected(); #elif defined(ARCH_NRF52) isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected(); -#elif defined(ARCH_NRF54L15) - isBleConnected = nrf54l15Bluetooth != nullptr && nrf54l15Bluetooth->isConnected(); #endif if (isBleConnected) { auto thread = concurrency::OSThread::currentThread; @@ -253,8 +251,6 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_ nimbleBluetooth->sendLog(buffer.get(), size); #elif defined(ARCH_NRF52) nrf52Bluetooth->sendLog(buffer.get(), size); -#elif defined(ARCH_NRF54L15) - nrf54l15Bluetooth->sendLog(buffer.get(), size); #endif } } diff --git a/src/freertosinc.h b/src/freertosinc.h index e9e6cd53a0..db49fbab11 100644 --- a/src/freertosinc.h +++ b/src/freertosinc.h @@ -12,7 +12,7 @@ #include #endif -#if defined(ARDUINO_NRF52_ADAFRUIT) || defined(ARDUINO_ARCH_RP2040) +#if defined(ARDUINO_NRF52_ADAFRUIT) || defined(ARDUINO_ARCH_NRF54) || defined(ARDUINO_ARCH_RP2040) #define HAS_FREE_RTOS #include diff --git a/src/main.cpp b/src/main.cpp index 7e9bd39d40..3f7baa2fdb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -76,12 +76,6 @@ NimbleBluetooth *nimbleBluetooth = nullptr; NRF52Bluetooth *nrf52Bluetooth = nullptr; #endif -#ifdef ARCH_NRF54L15 -void nrf54l15Setup(); -void nrf54l15Loop(); -NRF54L15Bluetooth *nrf54l15Bluetooth = nullptr; -#endif - #ifdef MESHTASTIC_ENABLE_APPROTECT #include "security/APProtect.h" #endif @@ -852,9 +846,6 @@ void setup() #ifdef ARCH_NRF52 nrf52Setup(); #endif -#ifdef ARCH_NRF54L15 - nrf54l15Setup(); -#endif #ifdef ARCH_RP2040 rp2040Setup(); @@ -1483,9 +1474,6 @@ void loop() #ifdef ARCH_NRF52 nrf52Loop(); #endif -#ifdef ARCH_NRF54L15 - nrf54l15Loop(); -#endif #ifdef ARCH_RP2040 rp2040Loop(); #endif diff --git a/src/main.h b/src/main.h index 49a63365bc..29a76aa2f6 100644 --- a/src/main.h +++ b/src/main.h @@ -20,10 +20,6 @@ extern NimbleBluetooth *nimbleBluetooth; #include "NRF52Bluetooth.h" extern NRF52Bluetooth *nrf52Bluetooth; #endif -#ifdef ARCH_NRF54L15 -#include "NRF54L15Bluetooth.h" -extern NRF54L15Bluetooth *nrf54l15Bluetooth; -#endif #if !MESHTASTIC_EXCLUDE_I2C #include "detect/ScanI2CTwoWire.h" #endif diff --git a/src/mesh/HardwareRNG.cpp b/src/mesh/HardwareRNG.cpp index 43a3e03854..451b8d8b74 100644 --- a/src/mesh/HardwareRNG.cpp +++ b/src/mesh/HardwareRNG.cpp @@ -10,7 +10,9 @@ #include "RadioLibInterface.h" #endif -#if defined(ARCH_NRF52) +#if defined(ARCH_NRF54L) +#include +#elif defined(ARCH_NRF52) #include extern Adafruit_nRFCrypto nRFCrypto; #elif defined(ARCH_ESP32) @@ -107,7 +109,12 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy) bool filled = false; -#if defined(ARCH_NRF52) +#if defined(ARCH_NRF54L) + // CRACEN TRNG + nRF54Crypto.begin(); + filled = nRF54Crypto.random(buffer, length); + nRF54Crypto.end(); +#elif defined(ARCH_NRF52) // The Nordic SDK RNG provides cryptographic-quality randomness backed by hardware. nRFCrypto.begin(); auto result = nRFCrypto.Random.generate(buffer, length); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 6dfed40c3d..9be681b42f 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -1819,10 +1819,6 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r if (config.bluetooth.enabled && nrf52Bluetooth) { conn.bluetooth.is_connected = nrf52Bluetooth->isConnected(); } -#elif defined(ARCH_NRF54L15) - if (config.bluetooth.enabled && nrf54l15Bluetooth) { - conn.bluetooth.is_connected = nrf54l15Bluetooth->isConnected(); - } #endif #endif conn.has_serial = true; // No serial-less devices @@ -2512,9 +2508,6 @@ void disableBluetooth() #elif defined(ARCH_NRF52) if (nrf52Bluetooth) nrf52Bluetooth->shutdown(); -#elif defined(ARCH_NRF54L15) - if (nrf54l15Bluetooth) - nrf54l15Bluetooth->shutdown(); #endif #endif } diff --git a/src/platform/nrf52/BLEDfuSecure.cpp b/src/platform/nrf52/BLEDfuSecure.cpp index 040df8bdff..c198e86388 100644 --- a/src/platform/nrf52/BLEDfuSecure.cpp +++ b/src/platform/nrf52/BLEDfuSecure.cpp @@ -121,7 +121,12 @@ static void bledfu_control_wr_authorize_cb(uint16_t conn_hdl, BLECharacteristic Bluefruit.Advertising.restartOnDisconnect(false); conn->disconnect(); +#ifdef ARCH_NRF54L + sd_power_gpregret_clr(0, 0xFF); + sd_power_gpregret_set(0, 0xB1); +#else NRF_POWER->GPREGRET = 0xB1; +#endif NVIC_SystemReset(); } } diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 608fb1b7bb..c0c5a2bb2f 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -11,6 +11,11 @@ #include "mesh/mesh-pb-constants.h" #include #include + +#ifdef ARCH_NRF54L +extern uint32_t sd_app_ram_start_required; // Bluefruit54Lib +extern "C" uint32_t verify_last_err, verify_last_line; // core verify.h +#endif static BLEService meshBleService = BLEService(BLEUuid(MESH_SERVICE_UUID_16)); static BLECharacteristic fromNum = BLECharacteristic(BLEUuid(FROMNUM_UUID_16)); static BLECharacteristic fromRadio = BLECharacteristic(BLEUuid(FROMRADIO_UUID_16)); @@ -23,7 +28,7 @@ static int lastBatteryLevel = -1; // last value written to BAS, to skip redundan #ifndef BLE_DFU_SECURE static BLEDfu bledfu; // DFU software update helper service #else -static BLEDfuSecure bledfusecure; // DFU software update helper service +static BLEDfuSecure bledfusecure; // DFU software update helper service #endif // This scratch buffer is used for various bluetooth reads/writes - but it is safe because only one bt operation can be in @@ -287,7 +292,12 @@ void NRF52Bluetooth::setup() // current Bluefruit config. Without this check the node would silently run without BLE. // Rebuild with -DCFG_DEBUG=1 to get "SoftDevice's RAM requires: 0x..." in the log, then // raise the ORIGIN accordingly. +#ifdef ARCH_NRF54L + LOG_ERROR("Bluefruit.begin failed: status 0x%lx at line %lu, app RAM base wanted 0x%08lx", verify_last_err, + verify_last_line, sd_app_ram_start_required); +#else LOG_ERROR("Bluefruit.begin failed: SoftDevice RAM too small"); +#endif RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_UNSPECIFIED); return; } @@ -498,7 +508,7 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu meshtastic::BluetoothStatus newConnectedStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); bluetoothStatus->updateStatus(&newConnectedStatus); } else { - LOG_INFO("BLE pair failed"); + LOG_INFO("BLE pair failed, status 0x%02x", auth_status); // Notify UI (or any other interested firmware components) meshtastic::BluetoothStatus newDisconnectedStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newDisconnectedStatus); diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 279ad75757..c3b62320da 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -51,7 +51,7 @@ #ifndef HAS_CPU_SHUTDOWN #define HAS_CPU_SHUTDOWN 1 #endif -#ifndef HAS_CUSTOM_CRYPTO_ENGINE +#if !defined(HAS_CUSTOM_CRYPTO_ENGINE) && !defined(ARCH_NRF54L) #define HAS_CUSTOM_CRYPTO_ENGINE 1 #endif @@ -198,7 +198,7 @@ // If we are not on a NRF52840 (which has built in USB-ACM serial support) and we don't have serial pins hooked up, then we MUST // use SEGGER for debug output -#if !defined(PIN_SERIAL_RX) && !defined(NRF52840_XXAA) +#if !defined(PIN_SERIAL_RX) && !defined(NRF52840_XXAA) && !defined(ARCH_NRF54L) // No serial ports on this board - ONLY use segger in memory console #define USE_SEGGER #endif diff --git a/src/platform/nrf52/hardfault.cpp b/src/platform/nrf52/hardfault.cpp index 260a0a6a6f..1f54b4486d 100644 --- a/src/platform/nrf52/hardfault.cpp +++ b/src/platform/nrf52/hardfault.cpp @@ -1,5 +1,5 @@ #include "configuration.h" -#include +#include #ifdef MESHTASTIC_ENCRYPTED_STORAGE #include "security/EncryptedStorage.h" diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 784d29e283..6114975513 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -1,22 +1,37 @@ #include "UptimeClock.h" #include "configuration.h" #include "mesh/Throttle.h" +#ifndef ARCH_NRF54L #include #include +#endif #include #include #include #define APP_WATCHDOG_SECS 90 +#ifdef ARCH_NRF54L +// The nRF54L core compiles the nrfx drivers itself (nrfx 3: errno-style returns, 0 is success); +// POWER/RESET registers are split differently. +#include +#include +#define NRFX_OK 0 +#define GPREGRET_REG NRF_POWER->GPREGRET[0] +#define RESETREAS_REG NRF_RESET->RESETREAS +#else #define NRFX_WDT_ENABLED 1 #define NRFX_WDT0_ENABLED 1 #define NRFX_WDT_CONFIG_NO_IRQ 1 #include "nrfx_power.h" +#include +#include +#define GPREGRET_REG NRF_POWER->GPREGRET +#define RESETREAS_REG NRF_POWER->RESETREAS +#define NRFX_OK NRFX_SUCCESS +#endif #include #include #include -#include -#include #include // #include #include "HardwareRNG.h" @@ -71,7 +86,11 @@ __attribute__((noinline)) bool variant_enableBatteryLpcompWake() return true; } +#ifdef ARCH_NRF54L +static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(NRF_WDT31); +#else static nrfx_wdt_t nrfx_wdt = NRFX_WDT_INSTANCE(0); +#endif static nrfx_wdt_channel_id nrfx_wdt_channel_id_nrf52_main; // This is a public global so that the debugger can set it to false automatically from our gdbinit @@ -89,7 +108,11 @@ static inline void debugger_break(void) // PowerHAL NRF52 specific function implementations bool powerHAL_isVBUSConnected() { +#ifdef ARCH_NRF54L + return false; // no USB peripheral +#else return NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk; +#endif } bool powerHAL_isPowerLevelSafe() @@ -138,8 +161,10 @@ void powerHAL_platformInit() // I did experiments with bench power supply and no matter what is set to POFCON, it always triggers right below // 2.8V. I compared raw registry values with datasheet. +#ifndef ARCH_NRF54L NRF_POWER->POFCON = ((POWER_POFCON_THRESHOLD_V22 << POWER_POFCON_THRESHOLD_Pos) | (POWER_POFCON_POF_Enabled << POWER_POFCON_POF_Pos)); +#endif // remember to always match VBAT_AR_INTERNAL with AREF_VALUE in variant definition file #ifdef VBAT_AR_INTERNAL @@ -183,7 +208,8 @@ bool loopCanSleep() void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr) { LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr); - // debugger_break(); FIXME doesn't work, possibly not for segger + Serial.flush(); // the reset below would cut the message short + // debugger_break(); FIXME doesn't work, possibly for segger // Reboot cpu NVIC_SystemReset(); } @@ -203,7 +229,11 @@ bool getDeviceId(uint8_t *deviceId) { // Nordic burns a FIPS-compliant random id into each chip at the factory. We concatenate // the device address to that random id to form the 16-byte hardware identifier. +#ifdef ARCH_NRF54L + uint64_t device_id_start = ((uint64_t)NRF_FICR->INFO.DEVICEID[1] << 32) | NRF_FICR->INFO.DEVICEID[0]; +#else uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0]; +#endif uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0]; memcpy(deviceId, &device_id_start, sizeof(device_id_start)); memcpy(deviceId + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); @@ -294,9 +324,9 @@ void preFSBegin() { // The GPREGRET register keeps its value across warm boots. Check that this is a warm boot and, if GPREGRET // is set to NRF52_MAGIC_LFS_IS_CORRUPT, format LittleFS. - if (!(NRF_POWER->RESETREAS == 0 && NRF_POWER->GPREGRET == NRF52_MAGIC_LFS_IS_CORRUPT)) + if (!(RESETREAS_REG == 0 && GPREGRET_REG == NRF52_MAGIC_LFS_IS_CORRUPT)) return; - NRF_POWER->GPREGRET = 0; + GPREGRET_REG = 0; // unset-sentinel-ok: formatted_this_boot carries the armed state, so 0 is a legal stamp last_format_ms = Time::getMillis(); formatted_this_boot = true; @@ -334,7 +364,7 @@ extern "C" void lfs_assert(const char *reason) if (!NRF_POWER->EVENTS_POFWARN) { if (!(sd_power_gpregret_clr(0, 0xFF) == NRF_SUCCESS && sd_power_gpregret_set(0, NRF52_MAGIC_LFS_IS_CORRUPT) == NRF_SUCCESS)) { - NRF_POWER->GPREGRET = NRF52_MAGIC_LFS_IS_CORRUPT; + GPREGRET_REG = NRF52_MAGIC_LFS_IS_CORRUPT; } } @@ -344,6 +374,9 @@ extern "C" void lfs_assert(const char *reason) NVIC_SystemReset(); } +// Defined by the core's InternalFileSystem, completes a pending sd_flash_write() +extern "C" void flash_nrf5x_event_cb(uint32_t event); + void checkSDEvents() { if (useSoftDevice) { @@ -353,6 +386,21 @@ void checkSDEvents() case NRF_EVT_POWER_FAILURE_WARNING: RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_BROWNOUT); break; + // Bluefruit's SoC task polls the same queue; an event taken here must still reach the flash driver + case NRF_EVT_FLASH_OPERATION_SUCCESS: + case NRF_EVT_FLASH_OPERATION_ERROR: + flash_nrf5x_event_cb(evt); + break; +#ifdef ARCH_NRF54L + case NRF_EVT_RAND_SEED_REQUEST: { + uint8_t seed[SD_RAND_SEED_SIZE]; + nRF54Crypto.begin(); + if (nRF54Crypto.random(seed, sizeof(seed))) + sd_rand_seed_set(seed); + nRF54Crypto.end(); + break; + } +#endif default: LOG_DEBUG("Unexpected SDevt %d", evt); @@ -452,6 +500,11 @@ void nrf52Setup() // Set up nrfx watchdog. Do not enable the watchdog yet (we do that // the first time through the main loop), so that other threads can // allocate their own wdt channel to protect themselves from hangs. +#ifdef ARCH_NRF54L + // nrfx 3: behaviour is a RUN_* mask (0 = pause in sleep and halt), init takes a context argument + nrfx_wdt_config_t wdt0_config = {.behaviour = 0, .reload_value = APP_WATCHDOG_SECS * 1000}; + int r = nrfx_wdt_init(&nrfx_wdt, &wdt0_config, nullptr, nullptr); +#else nrfx_wdt_config_t wdt0_config = { .behaviour = NRF_WDT_BEHAVIOUR_PAUSE_SLEEP_HALT, .reload_value = APP_WATCHDOG_SECS * 1000, // Note: Not using wdt interrupts. @@ -460,10 +513,11 @@ void nrf52Setup() nrfx_err_t r = nrfx_wdt_init(&nrfx_wdt, &wdt0_config, nullptr // Watchdog event handler, not used, we just reset. ); - assert(r == NRFX_SUCCESS); +#endif + assert(r == NRFX_OK); r = nrfx_wdt_channel_alloc(&nrfx_wdt, &nrfx_wdt_channel_id_nrf52_main); - assert(r == NRFX_SUCCESS); + assert(r == NRFX_OK); } void cpuDeepSleep(uint32_t msecToWake) @@ -541,11 +595,16 @@ void cpuDeepSleep(uint32_t msecToWake) } #endif +#ifdef ARCH_NRF54L + // s145 has no sd_power_system_off(); REGULATORS is not SoftDevice-restricted + NRF_REGULATORS->SYSTEMOFF = 1; +#else auto ok = sd_power_system_off(); if (ok != NRF_SUCCESS) { LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off"); NRF_POWER->SYSTEMOFF = 1; } +#endif } // The following code should not be run, because we are off diff --git a/src/platform/nrf54l15/Arduino.h b/src/platform/nrf54l15/Arduino.h deleted file mode 100644 index 0d7449e891..0000000000 --- a/src/platform/nrf54l15/Arduino.h +++ /dev/null @@ -1,835 +0,0 @@ -/** - * Arduino.h - Zephyr compatibility shim for nRF54L15 - * - * Provides the Arduino API surface expected by Meshtastic, backed by - * Zephyr primitives. Only the subset actually used by Meshtastic is - * implemented; the rest compiles as no-ops / stubs for now. - * - * Phase 2: compile only. Real GPIO / SPI / Wire implementations follow - * in Phase 3 once the build is clean. - */ - -#pragma once -#ifndef Arduino_h -#define Arduino_h - -// ── C standard headers ─────────────────────────────────────────────────────── -#include -#include -#include -#include -#include -#include -#include -#include /* strcasecmp, strncasecmp */ - -// ── Zephyr kernel ──────────────────────────────────────────────────────────── -#include -#include - -// ── Basic Arduino types ────────────────────────────────────────────────────── -typedef bool boolean; -typedef uint8_t byte; -typedef uint16_t word; - -// ── Pin / digital constants ────────────────────────────────────────────────── -#define INPUT 0u -#define OUTPUT 1u -#define INPUT_PULLUP 2u -#define INPUT_PULLDOWN 3u -#define OUTPUT_OPENDRAIN 4u - -#define HIGH 1u -#define LOW 0u - -#define CHANGE 1 -#define FALLING 2 -#define RISING 3 - -#ifndef LED_BUILTIN -#define LED_BUILTIN -1 -#endif - -// ── Math / trig constants ──────────────────────────────────────────────────── -#ifndef PI -#define PI 3.14159265358979323846 -#endif -#define HALF_PI 1.57079632679489661923 -#define TWO_PI 6.28318530717958647693 -#define DEG_TO_RAD 0.01745329251994329576 -#define RAD_TO_DEG 57.2957795130823208767 -#define EULER 2.71828182845904523536 - -// ── Bit utilities ──────────────────────────────────────────────────────────── -#define bitRead(v, b) (((v) >> (b)) & 1) -#define bitSet(v, b) ((v) |= (1UL << (b))) -#define bitClear(v, b) ((v) &= ~(1UL << (b))) -#define bitToggle(v, b) ((v) ^= (1UL << (b))) -#define bitWrite(v, b, x) ((x) ? bitSet(v, b) : bitClear(v, b)) -#define bit(b) (1UL << (b)) -#define lowByte(w) ((uint8_t)((w)&0xff)) -#define highByte(w) ((uint8_t)((w) >> 8)) -// word(h,l) - only define if not already defined (conflicts with typedef above) -#undef word -#define word(h, l) ((uint16_t)(((h) << 8) | (l))) - -// ── UART config constants ───────────────────────────────────────────────────── -#define SERIAL_8N1 0x800001cu -#define SERIAL_8N2 0x8000001eu -#define SERIAL_8E1 0x8000001eu -#define SERIAL_7E1 0x8000001cu - -// ── Integer order ──────────────────────────────────────────────────────────── -// Adafruit BusIO's SPIDevice.h has `typedef BitOrder BusIOBitOrder;` which -// requires BitOrder to be a *type*, not a macro. Mirror the ArduinoCore-API -// enum definition rather than #defines. -enum BitOrder : uint8_t { - LSBFIRST = 0, - MSBFIRST = 1, -}; - -// ── pgmspace compatibility (no-ops on Cortex-M) ────────────────────────────── -#define PROGMEM -#define PSTR(s) (s) -#define F(s) (s) -#define pgm_read_byte(addr) (*((const uint8_t *)(addr))) -#define pgm_read_word(addr) (*((const uint16_t *)(addr))) -#define pgm_read_dword(addr) (*((const uint32_t *)(addr))) -#define pgm_read_float(addr) (*((const float *)(addr))) -#define pgm_read_ptr(addr) (*((const void **)(addr))) -#define strlen_P(s) strlen(s) -#define strcpy_P(d, s) strcpy(d, s) -#define strncpy_P(d, s, n) strncpy(d, s, n) -#define strcmp_P(a, b) strcmp(a, b) -#define memcpy_P(d, s, n) memcpy(d, s, n) -#define sprintf_P sprintf -typedef const char *PGM_P; -typedef const char *PGM_VOID_P; - -// ── Arduino numeric base constants (used by Print, RadioLib, etc.) ─────────── -#define DEC 10 -#define HEX 16 -#define OCT 8 -#define BIN 2 - -// ── ulong / uint typedef (used by RadioLibInterface, etc.) ─────────────────── -typedef unsigned long ulong; -typedef unsigned int uint; - -// ── Interrupt stubs ────────────────────────────────────────────────────────── -static inline void interrupts() {} -static inline void noInterrupts() {} -#define digitalPinToInterrupt(p) (p) - -// ── portMAX_DELAY - freertosinc.h also defines this; let it win ────────────── -// We intentionally do NOT define portMAX_DELAY here. freertosinc.h defines -// it for the FreeRTOS / Meshtastic threading layer and must not be overridden. - -// ── Timing & system functions - declared with C linkage ────────────────────── -// buzz.cpp and others forward-declare delay() as extern "C"; keep linkage -// consistent by wrapping in extern "C" here. -#ifdef __cplusplus -extern "C" { -#endif -void NVIC_SystemReset(void); -uint32_t millis(void); -uint32_t micros(void); -void delay(uint32_t ms); -void delayMicroseconds(uint32_t us); -void yield(void); -#ifdef __cplusplus -} -#endif - -#ifdef __cplusplus - -#include -#include - -// ── C++ STL - include BEFORE defining any min/max helpers ─────────────────── -// Include algorithm first so its min/max templates are in scope. -// We must NOT define min/max as function-like macros: the C++ STL uses -// 3-argument versions (min(a,b,comp)) that the preprocessor would treat as -// calling a 2-arg macro with 3 args. -#include -// Bring 2-arg std::min / std::max into the global namespace as unqualified -// names so that Arduino code calling min(a,b) continues to compile. -// (Arduino convention; kept minimal to avoid surprises.) -#undef min -#undef max -using std::max; -using std::min; - -// ── Arduino math helpers (macros safe for mixed-type / C calls) ────────────── -#ifndef abs -#define abs(x) ((x) >= 0 ? (x) : -(x)) -#endif -#define constrain(x, l, h) ((x) < (l) ? (l) : ((x) > (h) ? (h) : (x))) -#define round(x) ((x) >= 0 ? (long)((x) + 0.5) : (long)((x)-0.5)) -#define radians(d) ((d)*DEG_TO_RAD) -#define degrees(r) ((r)*RAD_TO_DEG) -#define sq(x) ((x) * (x)) - -// ── Random ─────────────────────────────────────────────────────────────────── -static inline void randomSeed(unsigned long seed) -{ - srand((unsigned int)seed); -} -static inline long random(void) -{ - return (long)rand(); -} -static inline long random(long bound) -{ - return bound > 0 ? (rand() % bound) : 0; -} -static inline long random(long lo, long hi) -{ - return hi > lo ? lo + rand() % (hi - lo) : lo; -} - -// ── GPIO - real Zephyr implementation (Phase 3) ────────────────────────────── -// Implemented in nrf54l15_arduino.cpp using Zephyr GPIO/SPI APIs. -// Pin numbering: P0.n = n, P1.n = 16+n, P2.n = 32+n -void pinMode(uint32_t pin, uint32_t mode); -void digitalWrite(uint32_t pin, uint32_t value); -int digitalRead(uint32_t pin); -static inline void digitalToggle(uint32_t pin) -{ - digitalWrite(pin, !digitalRead(pin)); -} -static inline uint32_t analogRead(uint32_t) -{ - return 0; -} -static inline void analogWrite(uint32_t, uint32_t) {} -static inline void analogReadResolution(int) {} -static inline void analogWriteResolution(int) {} - -// ── __WFI - provided by CMSIS core_cm33.h; do NOT redefine here ───────────── - -// ── __FlashStringHelper - Arduino PROGMEM string class (no-op on Cortex-M) ── -class __FlashStringHelper; - -// ── attachInterrupt / detachInterrupt - real Zephyr GPIO interrupt impl ────── -typedef void (*voidFuncPtr)(void); -void attachInterrupt(uint32_t pin, voidFuncPtr cb, int mode); -void detachInterrupt(uint32_t pin); - -// ── Forward declaration of String (needed by Print / Stream) ───────────────── -class String; - -// ── Print base class ───────────────────────────────────────────────────────── -class Print -{ - public: - virtual size_t write(uint8_t c) = 0; - virtual size_t write(const uint8_t *buf, size_t n) - { - size_t written = 0; - while (n--) - written += write(*buf++); - return written; - } - size_t write(const char *s) { return s ? write((const uint8_t *)s, strlen(s)) : 0; } - size_t write(const char *s, size_t n) { return write((const uint8_t *)s, n); } - - size_t print(const char *s) { return s ? write((const uint8_t *)s, strlen(s)) : 0; } - int printf(const char *fmt, ...) __attribute__((format(printf, 2, 3))); - - size_t print(char c) { return write((uint8_t)c); } - size_t print(const String &s); - size_t print(unsigned char n, int base = 10); - size_t print(int n, int base = 10); - size_t print(long n, int base = 10); - size_t print(unsigned int n, int base = 10); - size_t print(unsigned long n, int base = 10); - size_t print(float n, int digits = 2); - size_t print(double n, int digits = 2); - size_t print(bool b) { return print(b ? "true" : "false"); } - - size_t println() { return write((uint8_t)'\n'); } - size_t println(const char *s) - { - size_t r = print(s); - return r + println(); - } - size_t println(char c) - { - size_t r = print(c); - return r + println(); - } - size_t println(const String &s); - size_t println(int n, int base = 10) - { - size_t r = print(n, base); - return r + println(); - } - size_t println(long n, int base = 10) - { - size_t r = print(n, base); - return r + println(); - } - size_t println(unsigned long n, int base = 10) - { - size_t r = print(n, base); - return r + println(); - } - size_t println(unsigned int n, int base = 10) - { - size_t r = print(n, base); - return r + println(); - } - size_t println(float n, int d = 2) - { - size_t r = print(n, d); - return r + println(); - } - size_t println(double n, int d = 2) - { - size_t r = print(n, d); - return r + println(); - } - size_t println(bool b) - { - size_t r = print(b); - return r + println(); - } - - virtual void flush() {} - virtual int availableForWrite() { return 0; } -}; - -// ── Stream base class ──────────────────────────────────────────────────────── -class Stream : public Print -{ - public: - virtual int available() = 0; - virtual int read() = 0; - virtual int peek() = 0; - virtual void setTimeout(unsigned long) {} - virtual bool find(const char *) { return false; } - String readString(); - String readStringUntil(char terminator); -}; - -// ── Minimal Arduino String class (backed by a char buffer) ─────────────────── -class String -{ - public: - String() : _buf(nullptr), _len(0), _cap(0) {} - // Implicit conversion is part of the Arduino String contract, used pervasively across the codebase. - // cppcheck-suppress noExplicitConstructor - String(const char *cstr) : _buf(nullptr), _len(0), _cap(0) - { - if (cstr) - assign(cstr, strlen(cstr)); - } - // cppcheck-suppress noExplicitConstructor - String(const String &s) : _buf(nullptr), _len(0), _cap(0) { assign(s._buf ? s._buf : "", s._len); } - // cppcheck-suppress noExplicitConstructor - String(char c) : _buf(nullptr), _len(0), _cap(0) - { - const char tmp[2] = {c, 0}; - assign(tmp, 1); - } - // cppcheck-suppress noExplicitConstructor - String(int n) : _buf(nullptr), _len(0), _cap(0) - { - char tmp[16]; - snprintf(tmp, 16, "%d", n); - assign(tmp, strlen(tmp)); - } - // cppcheck-suppress noExplicitConstructor - String(unsigned int n) : _buf(nullptr), _len(0), _cap(0) - { - char tmp[16]; - snprintf(tmp, 16, "%u", n); - assign(tmp, strlen(tmp)); - } - // cppcheck-suppress noExplicitConstructor - String(long n) : _buf(nullptr), _len(0), _cap(0) - { - char tmp[24]; - snprintf(tmp, 24, "%ld", n); - assign(tmp, strlen(tmp)); - } - // cppcheck-suppress noExplicitConstructor - String(unsigned long n) : _buf(nullptr), _len(0), _cap(0) - { - char tmp[24]; - snprintf(tmp, 24, "%lu", n); - assign(tmp, strlen(tmp)); - } - // cppcheck-suppress noExplicitConstructor - String(float n, int d = 2) : _buf(nullptr), _len(0), _cap(0) - { - char tmp[32]; - snprintf(tmp, 32, "%.*f", d, n); - assign(tmp, strlen(tmp)); - } - // cppcheck-suppress noExplicitConstructor - String(double n, int d = 2) : _buf(nullptr), _len(0), _cap(0) - { - char tmp[32]; - snprintf(tmp, 32, "%.*f", d, (double)n); - assign(tmp, strlen(tmp)); - } - ~String() { free(_buf); } - - String &operator=(const String &s) - { - assign(s._buf ? s._buf : "", s._len); - return *this; - } - String &operator=(const char *s) - { - assign(s ? s : "", s ? strlen(s) : 0); - return *this; - } - String &operator=(char c) - { - const char tmp[2] = {c, 0}; - assign(tmp, 1); - return *this; - } - - String &operator+=(const String &s) - { - concat(s._buf ? s._buf : "", s._len); - return *this; - } - String &operator+=(const char *s) - { - if (s) - concat(s, strlen(s)); - return *this; - } - String &operator+=(char c) - { - concat(&c, 1); - return *this; - } - String &operator+=(int n) { return *this += String(n); } - String &operator+=(unsigned long n) { return *this += String(n); } - - String operator+(const String &rhs) const - { - String r(*this); - r += rhs; - return r; - } - String operator+(const char *rhs) const - { - String r(*this); - r += rhs; - return r; - } - String operator+(char rhs) const - { - String r(*this); - r += rhs; - return r; - } - - bool operator==(const String &s) const { return _len == s._len && (_len == 0 || strcmp(_buf, s._buf) == 0); } - bool operator==(const char *s) const { return s && strcmp(c_str(), s) == 0; } - bool operator!=(const String &s) const { return !(*this == s); } - bool operator!=(const char *s) const { return !(*this == s); } - bool operator<(const String &s) const { return strcmp(c_str(), s.c_str()) < 0; } - bool operator>(const String &s) const { return strcmp(c_str(), s.c_str()) > 0; } - - char operator[](unsigned int i) const { return (_buf && i < _len) ? _buf[i] : 0; } - char &operator[](unsigned int i) - { - static char dummy = 0; - return (_buf && i < _len) ? _buf[i] : dummy; - } - - const char *c_str() const { return _buf ? _buf : ""; } - unsigned int length() const { return _len; } - bool isEmpty() const { return _len == 0; } - bool equals(const String &s) const { return *this == s; } - bool equals(const char *s) const { return *this == s; } - bool equalsIgnoreCase(const String &s) const - { - if (_len != s._len) - return false; - for (unsigned i = 0; i < _len; i++) - if (std::tolower(_buf[i]) != std::tolower(s._buf[i])) - return false; - return true; - } - bool startsWith(const String &pfx) const - { - if (pfx._len > _len) - return false; - return strncmp(c_str(), pfx.c_str(), pfx._len) == 0; - } - bool startsWith(const char *pfx) const - { - if (!pfx) - return false; - size_t pl = strlen(pfx); - return pl <= _len && strncmp(c_str(), pfx, pl) == 0; - } - bool endsWith(const String &sfx) const - { - if (sfx._len > _len) - return false; - return strcmp(c_str() + _len - sfx._len, sfx.c_str()) == 0; - } - int indexOf(char c, unsigned from = 0) const - { - if (!_buf) - return -1; - const char *p = strchr(_buf + from, c); - return p ? (int)(p - _buf) : -1; - } - int indexOf(const String &s, unsigned from = 0) const - { - if (!_buf) - return -1; - const char *p = strstr(_buf + from, s.c_str()); - return p ? (int)(p - _buf) : -1; - } - int lastIndexOf(char c) const - { - if (!_buf) - return -1; - const char *p = strrchr(_buf, c); - return p ? (int)(p - _buf) : -1; - } - String substring(unsigned beginIndex) const - { - if (!_buf || beginIndex >= _len) - return String(); - return String(_buf + beginIndex); - } - String substring(unsigned beginIndex, unsigned endIndex) const - { - if (!_buf || beginIndex >= _len) - return String(); - if (endIndex > _len) - endIndex = _len; - if (endIndex <= beginIndex) - return String(); - String r; - r.assign(_buf + beginIndex, endIndex - beginIndex); - return r; - } - void toUpperCase() - { - if (_buf) - for (unsigned i = 0; i < _len; i++) - _buf[i] = (char)std::toupper(_buf[i]); - } - void toLowerCase() - { - if (_buf) - for (unsigned i = 0; i < _len; i++) - _buf[i] = (char)std::tolower(_buf[i]); - } - void trim() - { - if (!_buf || _len == 0) - return; - unsigned s = 0; - while (s < _len && std::isspace(_buf[s])) - s++; - unsigned e = _len; - while (e > s && std::isspace(_buf[e - 1])) - e--; - if (s > 0 || e < _len) { - memmove(_buf, _buf + s, e - s); - _len = e - s; - _buf[_len] = 0; - } - } - void replace(char from, char to) - { - if (_buf) - for (unsigned i = 0; i < _len; i++) - if (_buf[i] == from) - _buf[i] = to; - } - void replace(const String &from, const String &to); - bool remove(unsigned index, unsigned count = 1) - { - if (!_buf || index >= _len) - return false; - if (index + count > _len) - count = _len - index; - memmove(_buf + index, _buf + index + count, _len - index - count + 1); - _len -= count; - return true; - } - void clear() - { - _len = 0; - if (_buf) - _buf[0] = 0; - } - char charAt(unsigned i) const { return (*this)[i]; } - void setCharAt(unsigned i, char c) - { - if (_buf && i < _len) - _buf[i] = c; - } - void toCharArray(char *buf, unsigned int bufsize, unsigned int index = 0) const - { - if (!buf || bufsize == 0) - return; - unsigned int avail = (_buf && _len > index) ? (_len - index) : 0; - unsigned int copy = avail < bufsize - 1 ? avail : bufsize - 1; - if (copy > 0) - memcpy(buf, _buf + index, copy); - buf[copy] = '\0'; - } - void concat(const String &s) { *this += s; } - void concat(const char *s) { *this += s; } - long toInt() const { return _buf ? atol(_buf) : 0; } - float toFloat() const { return _buf ? (float)atof(_buf) : 0.0f; } - double toDouble() const { return _buf ? atof(_buf) : 0.0; } - - private: - char *_buf; - unsigned int _len; - unsigned int _cap; - - void assign(const char *s, unsigned int n) - { - // reserve() keeps the old (smaller) buffer on OOM, so a failed grow must abort the - // write: memcpy'ing n >= _cap bytes would overflow into adjacent heap. - if (n + 1 == 0) - return; // n + 1 would wrap - if (n >= _cap && !reserve(n + 1)) - return; - if (_buf) { - memcpy(_buf, s, n); - _buf[n] = 0; - _len = n; - } - } - void concat(const char *s, unsigned int n) - { - if (!s || n == 0) - return; - unsigned newlen = _len + n; - if (newlen < _len || newlen + 1 == 0) - return; // length arithmetic wrapped - if (newlen >= _cap && !reserve(newlen + 1)) - return; // OOM: keep the existing content intact instead of writing past the buffer - if (_buf) { - memcpy(_buf + _len, s, n); - _len = newlen; - _buf[_len] = 0; - } - } - bool reserve(unsigned int n) - { - if (n == 0) - return false; - char *b = (char *)realloc(_buf, n); - if (b) { - _buf = b; - _cap = n; - return true; - } - return false; - } -}; - -inline String operator+(const char *lhs, const String &rhs) -{ - return String(lhs) + rhs; -} -inline String operator+(char lhs, const String &rhs) -{ - return String(lhs) + rhs; -} - -// ── Print inline definitions that need String ──────────────────────────────── -inline size_t Print::print(const String &s) -{ - return write((const uint8_t *)s.c_str(), s.length()); -} -inline size_t Print::println(const String &s) -{ - size_t r = print(s); - return r + println(); -} - -// ── Stream inline definitions that need String ─────────────────────────────── -inline String Stream::readString() -{ - return String(); -} -inline String Stream::readStringUntil(char) -{ - return String(); -} - -// ── HardwareSerial ─────────────────────────────────────────────────────────── -class HardwareSerial : public Stream -{ - public: - void begin(unsigned long) {} - void begin(unsigned long, uint16_t) {} - void end() {} - void setPins(int rx, int tx) {} - void setPinout(int tx, int rx) {} - void setFIFOSize(size_t) {} - void setRxBufferSize(size_t) {} - void begin(unsigned long baud, uint32_t config, int8_t rx = -1, int8_t tx = -1, bool invert = false) {} - int available() override { return 0; } - int read() override { return -1; } - int peek() override { return -1; } - size_t write(uint8_t c) override; - size_t write(const uint8_t *buf, size_t n) override; - using Print::write; // un-hide base class write(const char*) - size_t readBytes(uint8_t *buf, size_t len) { return 0; } - size_t readBytes(char *buf, size_t len) { return 0; } - operator bool() const { return true; } - void flush() override {} - String readString() { return String(); } - String readStringUntil(char) { return String(); } -}; - -// Uart - nRF52 BSP alias for HardwareSerial (used by GPS.h when ARCH_NRF52) -typedef HardwareSerial Uart; - -extern HardwareSerial Serial; -extern HardwareSerial Serial1; -extern HardwareSerial Serial2; - -// ── map() utility ──────────────────────────────────────────────────────────── -static inline long map(long x, long in_min, long in_max, long out_min, long out_max) -{ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - -// ── shiftIn / shiftOut stubs ───────────────────────────────────────────────── -static inline uint8_t shiftIn(uint8_t, uint8_t, uint8_t) -{ - return 0; -} -static inline void shiftOut(uint8_t, uint8_t, uint8_t, uint8_t) {} - -// ── tone / noTone stubs ────────────────────────────────────────────────────── -static inline void tone(uint8_t, unsigned int, unsigned long = 0) {} -static inline void noTone(uint8_t) {} - -// ── pulseIn stub ───────────────────────────────────────────────────────────── -static inline unsigned long pulseIn(uint8_t, uint8_t, unsigned long = 1000000UL) -{ - return 0; -} - -// ── strdup / stpcpy - POSIX extensions not in Zephyr newlib ───────────────── -#ifndef strdup -static inline char *strdup(const char *s) -{ - size_t n = strlen(s) + 1; - char *d = (char *)malloc(n); - if (d) - memcpy(d, s, n); - return d; -} -#endif -#ifndef stpcpy -static inline char *stpcpy(char *dst, const char *src) -{ - while ((*dst++ = *src++) != '\0') { - } - return dst - 1; -} -#endif - -// ── strnstr - BSD extension not in Zephyr libc; defined in meshUtils.cpp ───── -// Declare here so callers (GPS.cpp etc.) don't need ARCH_PORTDUINO. -#ifndef STRNSTR -#define STRNSTR -char *strnstr(const char *s, const char *find, size_t slen); -#endif - -// ── strlcpy - BSD extension; implementation in nrf54l15_arduino.cpp ────────── -#ifndef HAVE_STRLCPY -#define HAVE_STRLCPY -#ifdef __cplusplus -extern "C" { -#endif -size_t strlcpy(char *dst, const char *src, size_t size); -#ifdef __cplusplus -} -#endif -#endif - -// ── setenv / getenv / tzset - Zephyr stubs for timezone support ────────────── -#include -static inline int setenv(const char *, const char *, int) -{ - return 0; -} -static inline void tzset(void) {} - -// ── dbgHeapFree / dbgHeapTotal - nRF52 BSP heap diagnostics ───────────────── -// Used by memGet.cpp when ARCH_NRF52 is defined. Return 0 for Phase 2. -static inline uint32_t dbgHeapFree(void) -{ - return 0; -} -static inline uint32_t dbgHeapTotal(void) -{ - return 0; -} - -// ── WCharacter helpers ─────────────────────────────────────────────────────── -static inline bool isAlpha(char c) -{ - return std::isalpha((unsigned char)c) != 0; -} -static inline bool isAlphaNumeric(char c) -{ - return std::isalnum((unsigned char)c) != 0; -} -static inline bool isDigit(char c) -{ - return std::isdigit((unsigned char)c) != 0; -} -static inline bool isSpace(char c) -{ - return std::isspace((unsigned char)c) != 0; -} -static inline bool isUpperCase(char c) -{ - return std::isupper((unsigned char)c) != 0; -} -static inline bool isLowerCase(char c) -{ - return std::islower((unsigned char)c) != 0; -} -static inline char toUpperCase(char c) -{ - return (char)std::toupper((unsigned char)c); -} -static inline char toLowerCase(char c) -{ - return (char)std::tolower((unsigned char)c); -} - -#else /* C only */ -#ifndef min -#define min(a, b) ((a) < (b) ? (a) : (b)) -#endif -#ifndef max -#define max(a, b) ((a) > (b) ? (a) : (b)) -#endif -#ifndef abs -#define abs(x) ((x) >= 0 ? (x) : -(x)) -#endif -#define constrain(x, l, h) ((x) < (l) ? (l) : ((x) > (h) ? (h) : (x))) -#define round(x) ((x) >= 0 ? (long)((x) + 0.5) : (long)((x)-0.5)) -#endif /* __cplusplus */ - -#endif /* Arduino_h */ diff --git a/src/platform/nrf54l15/IPAddress.h b/src/platform/nrf54l15/IPAddress.h deleted file mode 100644 index 549d11d157..0000000000 --- a/src/platform/nrf54l15/IPAddress.h +++ /dev/null @@ -1,34 +0,0 @@ -// IPAddress.h - stub for nRF54L15/Zephyr -// MQTT.cpp includes this for IPv4 address representation. -// Phase 2: compile-only stub. -#pragma once -#include - -class IPAddress -{ - public: - IPAddress() : _addr(0) {} - explicit IPAddress(uint32_t addr) : _addr(addr) {} - IPAddress(uint8_t a, uint8_t b, uint8_t c, uint8_t d) - : _addr(((uint32_t)a) | ((uint32_t)b << 8) | ((uint32_t)c << 16) | ((uint32_t)d << 24)) - { - } - - uint8_t operator[](int i) const { return reinterpret_cast(&_addr)[i]; } - operator uint32_t() const { return _addr; } - bool operator==(const IPAddress &o) const { return _addr == o._addr; } - bool operator!=(const IPAddress &o) const { return _addr != o._addr; } - - bool fromString(const char *addr) - { - unsigned a, b, c, d; - if (sscanf(addr, "%u.%u.%u.%u", &a, &b, &c, &d) == 4 && a <= 255 && b <= 255 && c <= 255 && d <= 255) { - _addr = a | (b << 8) | (c << 16) | (d << 24); - return true; - } - return false; - } - - private: - uint32_t _addr; -}; diff --git a/src/platform/nrf54l15/InternalFileSystem.cpp b/src/platform/nrf54l15/InternalFileSystem.cpp deleted file mode 100644 index 15fdec6df6..0000000000 --- a/src/platform/nrf54l15/InternalFileSystem.cpp +++ /dev/null @@ -1,274 +0,0 @@ -// InternalFileSystem.cpp - Zephyr LittleFS backend for nRF54L15 -// -// Implements Adafruit_LittleFS_Namespace used by FSCommon.h/cpp. -// Storage: 36 KB storage_partition in nRF54L15 internal RRAM (defined in -// zephyr/dts/nordic/nrf54l15_partition.dtsi, included by the board DTS). - -#include "InternalFileSystem.h" -#include "configuration.h" - -#include -#include -#include - -using namespace Adafruit_LittleFS_Namespace; - -// ── LittleFS mount ──────────────────────────────────────────────────────── - -FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(nrf54l15_lfs_data); - -static struct fs_mount_t _lfs_mnt = { - .type = FS_LITTLEFS, - .mnt_point = NRF54L15_FS_MOUNT, - .fs_data = &nrf54l15_lfs_data, - .storage_dev = (void *)(uintptr_t)FIXED_PARTITION_ID(storage_partition), - .flags = 0, -}; - -// ── Global singleton ────────────────────────────────────────────────────── - -Adafruit_LittleFS_Namespace::InternalFileSystem InternalFS; - -// ── Path helpers ────────────────────────────────────────────────────────── - -void InternalFileSystem::toabs(const char *rel, char *abs, size_t abssz) -{ - // Root "/" maps to the mount point itself (no trailing slash) - if (rel[0] == '/' && rel[1] == '\0') { - strncpy(abs, NRF54L15_FS_MOUNT, abssz - 1); - abs[abssz - 1] = '\0'; - } else if (rel[0] == '/') { - snprintf(abs, abssz, "%s%s", NRF54L15_FS_MOUNT, rel); - } else { - snprintf(abs, abssz, "%s/%s", NRF54L15_FS_MOUNT, rel); - } -} - -// Strip mount-point prefix to get the FS-root-relative path ("/prefs/..."). -static void torel(const char *abs, char *rel, size_t relsz) -{ - const char *mp = NRF54L15_FS_MOUNT; - size_t mplen = strlen(mp); - if (strncmp(abs, mp, mplen) == 0) { - const char *suffix = abs + mplen; - if (suffix[0] == '\0') { - strncpy(rel, "/", relsz); - } else { - strncpy(rel, suffix, relsz - 1); - rel[relsz - 1] = '\0'; - } - } else { - strncpy(rel, abs, relsz - 1); - rel[relsz - 1] = '\0'; - } -} - -// ── InternalFileSystem methods ──────────────────────────────────────────── - -bool InternalFileSystem::begin() -{ - if (_mounted) - return true; - - int rc = fs_mount(&_lfs_mnt); - if (rc == 0) { - _mounted = true; - return true; - } - - // Mount failed: attempt to format (creates a fresh LittleFS) - LOG_WARN("LittleFS mount failed (%d), formatting storage partition", rc); - int fmt_rc = fs_mkfs(FS_LITTLEFS, (uintptr_t)FIXED_PARTITION_ID(storage_partition), NULL, 0); - if (fmt_rc != 0) { - LOG_ERROR("LittleFS format failed (%d)", fmt_rc); - return false; - } - - rc = fs_mount(&_lfs_mnt); - if (rc == 0) { - _mounted = true; - return true; - } - - LOG_ERROR("LittleFS mount failed after format (%d)", rc); - return false; -} - -File InternalFileSystem::open(const char *path, const char *mode) -{ - if (!_mounted) - return File(); - - char abs[NRF54L15_FS_PATHLEN]; - toabs(path, abs, sizeof(abs)); - - auto s = std::make_shared(); - if (!s) - return File(); - - strncpy(s->fullpath, abs, sizeof(s->fullpath) - 1); - torel(abs, s->relpath, sizeof(s->relpath)); - - // Check whether the path is a directory - struct fs_dirent entry; - int stat_rc = fs_stat(abs, &entry); - if (stat_rc == 0 && entry.type == FS_DIR_ENTRY_DIR) { - s->is_dir = true; - if (fs_opendir(&s->dir, abs) == 0) { - s->valid = true; - return File(s); - } - return File(); - } - - // Open as a regular file - fs_mode_t flags; - if (strcmp(mode, FILE_O_WRITE) == 0) { - // Truncate on write - unlink first to ensure a clean start - fs_unlink(abs); - flags = FS_O_WRITE | FS_O_CREATE; - } else { - flags = FS_O_READ; - } - - if (fs_open(&s->file, abs, flags) == 0) { - s->is_dir = false; - s->valid = true; - return File(s); - } - - return File(); -} - -bool InternalFileSystem::exists(const char *path) -{ - if (!_mounted) - return false; - char abs[NRF54L15_FS_PATHLEN]; - toabs(path, abs, sizeof(abs)); - struct fs_dirent entry; - return fs_stat(abs, &entry) == 0; -} - -bool InternalFileSystem::remove(const char *path) -{ - if (!_mounted) - return false; - char abs[NRF54L15_FS_PATHLEN]; - toabs(path, abs, sizeof(abs)); - return fs_unlink(abs) == 0; -} - -bool InternalFileSystem::rename(const char *from, const char *to) -{ - if (!_mounted) - return false; - char absfrom[NRF54L15_FS_PATHLEN], absto[NRF54L15_FS_PATHLEN]; - toabs(from, absfrom, sizeof(absfrom)); - toabs(to, absto, sizeof(absto)); - return fs_rename(absfrom, absto) == 0; -} - -bool InternalFileSystem::mkdir(const char *path) -{ - if (!_mounted) - return false; - char abs[NRF54L15_FS_PATHLEN]; - toabs(path, abs, sizeof(abs)); - int rc = fs_mkdir(abs); - return rc == 0 || rc == -EEXIST; -} - -bool InternalFileSystem::rmdir(const char *path) -{ - if (!_mounted) - return false; - char abs[NRF54L15_FS_PATHLEN]; - toabs(path, abs, sizeof(abs)); - return fs_unlink(abs) == 0; -} - -bool InternalFileSystem::rmdir_r(const char *path) -{ - if (!_mounted) - return false; - char abs[NRF54L15_FS_PATHLEN]; - toabs(path, abs, sizeof(abs)); - - struct fs_dir_t dir; - fs_dir_t_init(&dir); - if (fs_opendir(&dir, abs) != 0) { - // Not a directory - try to delete as file - return fs_unlink(abs) == 0; - } - - struct fs_dirent entry; - char child[NRF54L15_FS_PATHLEN]; - while (fs_readdir(&dir, &entry) == 0 && entry.name[0] != '\0') { - snprintf(child, sizeof(child), "%s/%s", abs, entry.name); - if (entry.type == FS_DIR_ENTRY_DIR) { - // Recurse: pass the absolute path stripped of mount prefix - char childrel[NRF54L15_FS_PATHLEN]; - torel(child, childrel, sizeof(childrel)); - rmdir_r(childrel); - } else { - fs_unlink(child); - } - } - fs_closedir(&dir); - return fs_unlink(abs) == 0; -} - -bool InternalFileSystem::format() -{ - if (_mounted) { - fs_unmount(&_lfs_mnt); - _mounted = false; - } - int rc = fs_mkfs(FS_LITTLEFS, (uintptr_t)FIXED_PARTITION_ID(storage_partition), NULL, 0); - if (rc != 0) { - LOG_ERROR("LittleFS format failed (%d)", rc); - return false; - } - return begin(); -} - -// ── File::openNextFile ──────────────────────────────────────────────────── -// Defined here because it accesses Zephyr fs_readdir/fs_open APIs. - -File File::openNextFile() -{ - if (!_s || !_s->valid || !_s->is_dir) - return File(); - - struct fs_dirent entry; - if (fs_readdir(&_s->dir, &entry) != 0) - return File(); - if (entry.name[0] == '\0') - return File(); // end of directory - - char childabs[NRF54L15_FS_PATHLEN]; - snprintf(childabs, sizeof(childabs), "%s/%s", _s->fullpath, entry.name); - - auto s = std::make_shared(); - if (!s) - return File(); - - strncpy(s->fullpath, childabs, sizeof(s->fullpath) - 1); - torel(childabs, s->relpath, sizeof(s->relpath)); - - if (entry.type == FS_DIR_ENTRY_DIR) { - s->is_dir = true; - if (fs_opendir(&s->dir, childabs) == 0) { - s->valid = true; - return File(s); - } - } else { - s->is_dir = false; - if (fs_open(&s->file, childabs, FS_O_READ) == 0) { - s->valid = true; - return File(s); - } - } - return File(); -} diff --git a/src/platform/nrf54l15/InternalFileSystem.h b/src/platform/nrf54l15/InternalFileSystem.h deleted file mode 100644 index 8eea7c1cb3..0000000000 --- a/src/platform/nrf54l15/InternalFileSystem.h +++ /dev/null @@ -1,212 +0,0 @@ -// InternalFileSystem.h - Zephyr LittleFS backend for nRF54L15 -// -// Implements the Adafruit InternalFileSystem API subset used by Meshtastic, -// backed by Zephyr's fs/littlefs on the storage_partition of the nRF54L15's -// internal RRAM. Partition size is taken from the DTS at compile time via -// FIXED_PARTITION_SIZE(storage_partition) - the DK overlay currently maps -// ~700 KB into slot1 (see zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay). -// -// Mount point: /lfs -// All paths passed to open/exists/mkdir etc. are relative to the FS root -// (e.g. "/prefs/config.proto") and are prepended with "/lfs" internally. -// -// File objects are copyable via std::shared_ptr. -// The underlying Zephyr handle is closed when the last copy is destroyed. - -#pragma once - -#include -#include -#include -#include - -#include -#include - -#ifndef FILE_O_READ -#define FILE_O_READ "r" -#define FILE_O_WRITE "w" -#endif - -#define NRF54L15_FS_MOUNT "/lfs" -#define NRF54L15_FS_PATHLEN 256 - -namespace Adafruit_LittleFS_Namespace -{ - -class InternalFileSystem; // forward - -// ── Internal file/dir state ─────────────────────────────────────────────── - -struct NRF54L15FileState { - bool valid = false; - bool is_dir = false; - - // Absolute Zephyr path, e.g. "/lfs/prefs/config.proto" - char fullpath[NRF54L15_FS_PATHLEN] = {0}; - // Path from FS root, e.g. "/prefs/config.proto" (returned by name()) - char relpath[NRF54L15_FS_PATHLEN] = {0}; - - struct fs_file_t file; - struct fs_dir_t dir; - - NRF54L15FileState() - { - fs_file_t_init(&file); - fs_dir_t_init(&dir); - } - - ~NRF54L15FileState() - { - if (valid) { - if (is_dir) - fs_closedir(&dir); - else - fs_close(&file); - valid = false; - } - } -}; - -// ── File ───────────────────────────────────────────────────────────────── - -class File -{ - public: - File() = default; - explicit File(InternalFileSystem &) {} // nRF52 compat constructor - - explicit operator bool() const { return _s && _s->valid; } - - int read(void *buf, uint16_t nbyte) - { - if (!_s || !_s->valid || _s->is_dir) - return -1; - ssize_t n = fs_read(&_s->file, buf, nbyte); - return n < 0 ? -1 : (int)n; - } - - int read() - { - uint8_t b; - return read(&b, 1) == 1 ? (int)b : -1; - } - - size_t write(const uint8_t *buf, size_t len) - { - if (!_s || !_s->valid || _s->is_dir) - return 0; - ssize_t n = fs_write(&_s->file, buf, len); - return n < 0 ? 0 : (size_t)n; - } - - size_t write(uint8_t b) { return write(&b, 1); } - - void flush() - { - if (_s && _s->valid && !_s->is_dir) - fs_sync(&_s->file); - } - - void close() { _s.reset(); } - - size_t size() - { - if (!_s || !_s->valid || _s->is_dir) - return 0; - struct fs_dirent entry; - if (fs_stat(_s->fullpath, &entry) == 0) - return (size_t)entry.size; - return 0; - } - - bool isDirectory() { return _s && _s->valid && _s->is_dir; } - - // Returns path from FS root, e.g. "/prefs/config.proto" - const char *name() { return _s ? _s->relpath : ""; } - - // Returns the next entry in a directory. Modifies the dir stream in _s. - File openNextFile(); - - void rewindDirectory() - { - if (!_s || !_s->valid || !_s->is_dir) - return; - // Zephyr has no rewinddir(); close + reopen the same handle. Skipping - // the close would leak the LittleFS dir state and the next openNextFile - // could return stale entries on some Zephyr versions. - fs_closedir(&_s->dir); - fs_dir_t_init(&_s->dir); - if (fs_opendir(&_s->dir, _s->fullpath) != 0) { - _s->valid = false; - } - } - - bool seek(uint32_t pos) - { - if (!_s || !_s->valid || _s->is_dir) - return false; - return fs_seek(&_s->file, (off_t)pos, FS_SEEK_SET) == 0; - } - - int available() - { - if (!_s || !_s->valid || _s->is_dir) - return 0; - off_t pos = fs_tell(&_s->file); - if (pos < 0) - return 0; - struct fs_dirent entry; - if (fs_stat(_s->fullpath, &entry) != 0) - return 0; - long rem = (long)entry.size - (long)pos; - return rem > 0 ? (int)rem : 0; - } - - int peek() { return -1; } - - // Internal: constructed by InternalFileSystem and openNextFile() - explicit File(std::shared_ptr s) : _s(std::move(s)) {} - - private: - std::shared_ptr _s; -}; - -// ── InternalFileSystem ──────────────────────────────────────────────────── - -class InternalFileSystem -{ - public: - bool begin(); - File open(const char *path, const char *mode); - bool exists(const char *path); - bool remove(const char *path); - bool rename(const char *from, const char *to); - bool mkdir(const char *path); - bool rmdir(const char *path); - bool rmdir_r(const char *path); // recursive delete (used by FSCommon rmDir) - uint32_t usedBytes() - { - struct fs_statvfs st = {}; - if (fs_statvfs(NRF54L15_FS_MOUNT, &st) != 0) - return 0; - // Zephyr returns block counts; convert to bytes. f_frsize is the - // fundamental fragment size (LittleFS reports it equal to the block - // size). used = (total - free) * frag_size. - if (st.f_blocks <= st.f_bfree) - return 0; - return (uint32_t)((st.f_blocks - st.f_bfree) * st.f_frsize); - } - uint32_t totalBytes() { return (uint32_t)FIXED_PARTITION_SIZE(storage_partition); } - bool format(); - - // Convert a FS-root-relative path to an absolute Zephyr path. - static void toabs(const char *rel, char *abs, size_t abssz); - - private: - bool _mounted = false; -}; - -} // namespace Adafruit_LittleFS_Namespace - -extern Adafruit_LittleFS_Namespace::InternalFileSystem InternalFS; diff --git a/src/platform/nrf54l15/NRF52Bluetooth.h b/src/platform/nrf54l15/NRF52Bluetooth.h deleted file mode 100644 index 55686d379e..0000000000 --- a/src/platform/nrf54l15/NRF52Bluetooth.h +++ /dev/null @@ -1,18 +0,0 @@ -// NRF52Bluetooth.h - stub for nRF54L15/Zephyr -// main.h includes this when ARCH_NRF52 is defined. -// Bluetooth is excluded (MESHTASTIC_EXCLUDE_BLUETOOTH=1); this satisfies the -// include chain without pulling in the nRF52 Bluefruit SDK. -#pragma once - -class NRF52Bluetooth -{ - public: - void setup() {} - void shutdown() {} - void startDisabled() {} - void resumeAdvertising() {} - void clearBonds() {} - bool isConnected() { return false; } - int getRssi() { return 0; } - void sendLog(const uint8_t *, size_t) {} -}; diff --git a/src/platform/nrf54l15/NRF54L15Bluetooth.cpp b/src/platform/nrf54l15/NRF54L15Bluetooth.cpp deleted file mode 100644 index 8cef2fb59c..0000000000 --- a/src/platform/nrf54l15/NRF54L15Bluetooth.cpp +++ /dev/null @@ -1,806 +0,0 @@ -// NRF54L15Bluetooth.cpp - Zephyr BLE GATT peripheral for Meshtastic nRF54L15 -// -// GATT profile (identical UUIDs to the nRF52 / NimBLE implementations): -// Service: 6ba1b218-15a8-461f-9fa8-5dcae273eafd -// fromNum: ed9da18c-a800-4f66-a670-aa7547e34453 READ | NOTIFY -// fromRadio: 2c55e69e-4993-11ed-b878-0242ac120002 READ -// toRadio: f75c76d2-129e-4dad-a1dd-7866124401e7 WRITE -// logRadio: 5a3d6e49-06e6-4423-9944-e9de8cdf9547 READ | NOTIFY | INDICATE -// -// Threading model: -// - BT RX thread: connected_cb / disconnected_cb / GATT read_/write_ -// callbacks -// - Meshtastic OSThread scheduler (cooperative, main thread): -// BleDeferredThread -// polls pendingToRadio and runs the zombie-connection watchdog every 100 ms -// - PhoneAPI::onNowHasData: sends fromNum notify synchronously from whichever -// thread pushed the packet (bt_gatt_notify is thread-safe in Zephyr) -// - active_conn protected by ble_mutex where needed - -#include "NRF54L15Bluetooth.h" -#include "BluetoothCommon.h" -#include "BluetoothStatus.h" -#include "PowerFSM.h" -#include "UptimeClock.h" -#include "concurrency/OSThread.h" -#include "configuration.h" -#include "main.h" -#include "mesh/PhoneAPI.h" -#include "mesh/mesh-pb-constants.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -// ── UUID definitions (little-endian per Bluetooth spec) -// ─────────────────────── Syntax: replace hyphens with commas, prefix 0x - -// matches BT_UUID_128_ENCODE doc. - -#define MESH_SVC_UUID_VAL BT_UUID_128_ENCODE(0x6ba1b218, 0x15a8, 0x461f, 0x9fa8, 0x5dcae273eafd) -#define FROMNUM_UUID_VAL BT_UUID_128_ENCODE(0xed9da18c, 0xa800, 0x4f66, 0xa670, 0xaa7547e34453) -#define FROMRADIO_UUID_VAL BT_UUID_128_ENCODE(0x2c55e69e, 0x4993, 0x11ed, 0xb878, 0x0242ac120002) -#define TORADIO_UUID_VAL BT_UUID_128_ENCODE(0xf75c76d2, 0x129e, 0x4dad, 0xa1dd, 0x7866124401e7) -#define LOGRADIO_UUID_VAL BT_UUID_128_ENCODE(0x5a3d6e49, 0x06e6, 0x4423, 0x9944, 0xe9de8cdf9547) - -static const struct bt_uuid_128 mesh_svc_uuid = BT_UUID_INIT_128(MESH_SVC_UUID_VAL); -static const struct bt_uuid_128 fromnum_uuid = BT_UUID_INIT_128(FROMNUM_UUID_VAL); -static const struct bt_uuid_128 fromradio_uuid = BT_UUID_INIT_128(FROMRADIO_UUID_VAL); -static const struct bt_uuid_128 toradio_uuid = BT_UUID_INIT_128(TORADIO_UUID_VAL); -static const struct bt_uuid_128 logradio_uuid = BT_UUID_INIT_128(LOGRADIO_UUID_VAL); - -// ── Module state ───────────────────────────────────────────────────────────── - -static struct bt_conn *active_conn = nullptr; -static K_MUTEX_DEFINE(ble_mutex); - -// Take a reference to active_conn under ble_mutex. Returns nullptr if there is -// no active connection. Caller MUST bt_conn_unref() when done. -// -// Reading `active_conn` outside this lock races with disconnected_cb which can -// unref + null it on the BT RX thread - touching the freed pointer (even just -// to bt_conn_ref it) is a use-after-free. -static struct bt_conn *acquire_active_conn() -{ - struct bt_conn *conn = nullptr; - k_mutex_lock(&ble_mutex, K_FOREVER); - if (active_conn) { - conn = bt_conn_ref(active_conn); - } - k_mutex_unlock(&ble_mutex); - return conn; -} - -static bool bt_initialized = false; // bt_enable() called at most once -static bool ble_enabled = false; // set by setup(), cleared by shutdown() - -// Forward declarations - BT_GATT_SERVICE_DEFINE(mesh_svc, ...) is below, but -// read_fromradio() (defined earlier) needs to reference the service to notify -// on fromNum after each non-empty read. -#define FROMNUM_ATTR_IDX 2 -#define LOGRADIO_ATTR_IDX 9 -extern const struct bt_gatt_service_static mesh_svc; - -static void start_advertising(); // forward declaration (defined in advertising - // section below) - -// Work item for advertising restart after disconnect. -// -// disconnected_cb runs on the BT RX thread (the same thread that processes -// HCI Command Complete events). Calling bt_le_adv_start() → -// bt_hci_cmd_send_sync() directly from that thread deadlocks: the thread blocks -// on k_sem_take waiting for Command Complete, but it is the very thread that -// would process it. After 10 s the host panics with "Controller unresponsive, -// opcode 0x2006 timeout". -// -// Fix: submit a k_work item. The system workqueue runs bt_adv_restart_work_fn -// on its own thread → no deadlock. -static struct k_work adv_restart_work; - -static void adv_restart_work_fn(struct k_work *work) -{ - if (ble_enabled) { - start_advertising(); - } -} - -// CCC state: 0=off, BT_GATT_CCC_NOTIFY=notify, BT_GATT_CCC_INDICATE=indicate -static uint16_t fromnum_ccc_val = 0; -static uint16_t logradio_ccc_val = 0; - -// Scratch buffers - only one BLE operation at a time -static uint8_t fromRadioBytes[meshtastic_FromRadio_size]; -static size_t fromRadioLen = 0; -static uint8_t toRadioBytes[meshtastic_ToRadio_size]; -static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE]; -static uint32_t fromNumValue = 0; - -// Deferred ToRadio processing -// -// write_toradio() runs on the BT RX workqueue thread (6 KB stack). Calling -// phoneAPI->handleToRadio() directly triggers handleStartConfig → -// getFiles("/", 10) → nanopb encode, which overflows the stack on the exact -// "Client wants config" write. Instead we copy the payload into a pending -// buffer under a mutex and let BleDeferredThread (running on the Meshtastic -// OSThread scheduler, 24 KB stack) do the actual call outside the lock. -// -// The mutex makes the producer/consumer handoff race-free - producer may -// overwrite a pending buffer the consumer hasn't read yet (dropped packet), -// but partial reads / torn writes are impossible. -K_MUTEX_DEFINE(pendingToRadioMutex); -static uint8_t pendingToRadioBuf[MAX_TO_FROM_RADIO_SIZE]; -static size_t pendingToRadioLen = 0; -static bool pendingToRadio = false; - -// Zombie-connection watchdog state. -// -// The nRF54L15 Zephyr 4.2.1 SW-LL occasionally fails to forward an -// LE Disconnection Complete event to the host: when iOS tears down the link -// (either explicitly by the user or via supervision timeout), the LL layer -// drops the connection but disconnected_cb never fires, active_conn stays -// non-null and advertising never restarts - the device vanishes from scans -// until power cycle. Track the connected timestamp and the last time we -// observed ATT traffic; a long ATT idle on an "active" connection means we -// are zombied. A cold reboot is the only path that reliably recovers (any -// bt_hci_cmd_send_sync after this state, e.g. bt_le_adv_start or -// bt_conn_disconnect, hangs in k_sem_take and later panics with "Controller -// unresponsive, opcode 0x2006 timeout"). -static uint32_t connect_time_ms = 0; -static uint32_t last_att_time_ms = 0; - -// ── BluetoothPhoneAPI -// ───────────────────────────────────────────────────────── - -class BluetoothPhoneAPI : public PhoneAPI -{ - virtual void onNowHasData(uint32_t fromRadioNum) override; - virtual bool checkIsConnected() override; - - public: - BluetoothPhoneAPI() { api_type = TYPE_BLE; } -}; - -static BluetoothPhoneAPI *phoneAPI = nullptr; - -// ── CCC change callbacks -// ────────────────────────────────────────────────────── - -static void fromnum_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value) -{ - fromnum_ccc_val = value; - LOG_INFO("BLE fromNum CCC: %u", value); -} - -static void logradio_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value) -{ - logradio_ccc_val = value; - LOG_INFO("BLE logRadio CCC: %u", value); -} - -// ── GATT attribute callbacks -// ────────────────────────────────────────────────── - -static ssize_t read_fromnum(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) -{ - LOG_INFO("GATT read_fromnum: fromNum=%u offset=%u", fromNumValue, offset); - return bt_gatt_attr_read(conn, attr, buf, len, offset, &fromNumValue, sizeof(fromNumValue)); -} - -static ssize_t read_fromradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) -{ - if (offset == 0) { - // First chunk: pull the next packet from the queue. - // Subsequent chunks (offset > 0) are ATT_READ_BLOB continuations of the - // same value and must reuse fromRadioBytes untouched. - fromRadioLen = phoneAPI ? phoneAPI->getFromRadio(fromRadioBytes) : 0; - LOG_DEBUG("GATT read_fromradio len=%u", (unsigned)fromRadioLen); - } - last_att_time_ms = k_uptime_get_32(); - return bt_gatt_attr_read(conn, attr, buf, len, offset, fromRadioBytes, fromRadioLen); -} - -static ssize_t read_logradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) -{ - // logRadio is write-only from the device side (notify/indicate). - // Return an empty read so GATT discovery doesn't fail with NOT_PERMITTED. - return bt_gatt_attr_read(conn, attr, buf, len, offset, NULL, 0); -} - -static ssize_t write_toradio(struct bt_conn *conn, const struct bt_gatt_attr *attr, const void *buf, uint16_t len, - uint16_t offset, uint8_t flags) -{ - // Writes >MTU-3 arrive here with offset=0 and - // flags=BT_GATT_WRITE_FLAG_EXECUTE after Zephyr reassembles the ATT Prepare - // Write fragments (CONFIG_BT_ATT_PREPARE_COUNT>0). Single writes arrive with - // flags=0. - LOG_DEBUG("GATT write_toradio len=%u flags=0x%x", len, flags); - if (offset != 0) { - return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); - } - // Reject any write that won't fit in the dedup buffer (lastToRadio) or the - // pending handoff buffer (pendingToRadioBuf), both sized - // MAX_TO_FROM_RADIO_SIZE. Returning success while silently dropping a - // payload would let the phone believe a config write was applied. - if (len > sizeof(toRadioBytes) || len > MAX_TO_FROM_RADIO_SIZE) { - return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); - } - - // Deduplicate - drop packet if identical to the last one we processed - if (memcmp(lastToRadio, buf, len) != 0) { - memcpy(lastToRadio, buf, len); - if (len < MAX_TO_FROM_RADIO_SIZE) { - memset(lastToRadio + len, 0, MAX_TO_FROM_RADIO_SIZE - len); - } - // Defer handleToRadio() to BleDeferredThread (24 KB stack). - // Running it here on bt_workq (6 KB) overflows during handleStartConfig. - // Always overwrite pending - we already dedup'd above via lastToRadio, - // so any new write here is genuinely new data that must be delivered. - k_mutex_lock(&pendingToRadioMutex, K_FOREVER); - memcpy(pendingToRadioBuf, buf, len); - pendingToRadioLen = len; - pendingToRadio = true; - k_mutex_unlock(&pendingToRadioMutex); - } - last_att_time_ms = k_uptime_get_32(); - return (ssize_t)len; -} - -// ── GATT service definition (static, linked at compile time) -// ────────────────── -// -// Attribute indices (0-based): -// [0] Primary Service declaration -// [1] fromNum characteristic declaration -// [2] fromNum value ← notify target (FROMNUM_ATTR_IDX) -// [3] fromNum CCC descriptor -// [4] fromRadio characteristic declaration -// [5] fromRadio value -// [6] toRadio characteristic declaration -// [7] toRadio value -// [8] logRadio characteristic declaration -// [9] logRadio value ← notify target (LOGRADIO_ATTR_IDX) -// [10] logRadio CCC descriptor - -// All user characteristics require authenticated encryption (MITM passkey) -// before the client can read/write. This mirrors the nrf52 -// SECMODE_ENC_WITH_MITM service permission. The stack returns "Insufficient -// Authentication" on the first access attempt, prompting the client to pair -// with the configured PIN. -#define MESH_PERM_READ (BT_GATT_PERM_READ | BT_GATT_PERM_READ_AUTHEN) -#define MESH_PERM_WRITE (BT_GATT_PERM_WRITE | BT_GATT_PERM_WRITE_AUTHEN) - -BT_GATT_SERVICE_DEFINE(mesh_svc, BT_GATT_PRIMARY_SERVICE(&mesh_svc_uuid.uuid), - - // fromNum: READ | NOTIFY - packet-counter triggers phone to read fromRadio - BT_GATT_CHARACTERISTIC(&fromnum_uuid.uuid, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, MESH_PERM_READ, - read_fromnum, NULL, &fromNumValue), - BT_GATT_CCC(fromnum_ccc_changed, MESH_PERM_READ | MESH_PERM_WRITE), - - // fromRadio: READ - phone polls this after receiving a fromNum notification - BT_GATT_CHARACTERISTIC(&fromradio_uuid.uuid, BT_GATT_CHRC_READ, MESH_PERM_READ, read_fromradio, NULL, - NULL), - - // toRadio: WRITE - phone sends protobuf packets to the device - BT_GATT_CHARACTERISTIC(&toradio_uuid.uuid, BT_GATT_CHRC_WRITE, MESH_PERM_WRITE, NULL, write_toradio, NULL), - - // logRadio: READ | NOTIFY | INDICATE - log stream to phone when connected - BT_GATT_CHARACTERISTIC(&logradio_uuid.uuid, - BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_INDICATE, MESH_PERM_READ, - read_logradio, NULL, NULL), - BT_GATT_CCC(logradio_ccc_changed, MESH_PERM_READ | MESH_PERM_WRITE), ); - -// ── Advertising -// ─────────────────────────────────────────────────────────────── -// -// Use legacy advertising (bt_le_adv_start / HCI 0x2006 path). -// -// History: we previously used bt_le_ext_adv_create (true extended advertising) -// because bt_le_adv_start() with CONFIG_BT_EXT_ADV=y was translated internally -// to the extended HCI path with LEGACY-bit (0x2036), which produced -// non-connectable PDUs on the nRF54L15 SW-LL. The true extended path -// (0x203x, AUX_ADV_IND) was connectable but caused two problems: -// 1. iOS CoreBluetooth does not reliably complete GATT after connecting via -// extended advertising (zero ATT PDUs observed in all test sessions). -// 2. After each connection the controller auto-stops the advertising set, and -// the subsequent bt_le_ext_adv_delete() sends LE Remove Advertising Set -// (0x203c) which times out → kernel oops at hci_core.c:506. -// -// With CONFIG_BT_EXT_ADV=n the host uses pure legacy HCI commands - the same -// path Nordic NCS uses in all nRF54L15 examples (peripheral_uart, -// peripheral_lbs) and which is universally iOS-compatible. The legacy data -// payload is 31 bytes: -// FLAGS (3B) + UUID128 (18B) = 21B in adv; NAME in scan-response (17B). - -static void start_advertising() -{ - // IMPORTANT: BT_DATA_BYTES() uses C99 compound literals that GCC C++ treats - // as temporaries; with -Os the compiler may elide writes, leaving stack - // uninitialized. Use static const arrays for stable data (flags, UUID) - // and a runtime pointer for the dynamic device name. - static const uint8_t adv_flags_val[] = {BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR}; - static const uint8_t adv_uuid128_val[] = {MESH_SVC_UUID_VAL}; - - const char *name = bt_get_name(); - size_t full_name_len = strlen(name); - - // Legacy scan-response payload is 31 bytes total. Each AD entry costs 2 - // bytes (length + type), leaving 29 bytes for the name. With - // CONFIG_BT_DEVICE_NAME_MAX=32 the name can exceed that - truncate and - // mark as SHORTENED so bt_le_adv_start() doesn't reject the payload. - constexpr size_t LEGACY_SCAN_RSP_NAME_MAX = 31 - 2; - bool name_shortened = full_name_len > LEGACY_SCAN_RSP_NAME_MAX; - uint8_t name_len = (uint8_t)(name_shortened ? LEGACY_SCAN_RSP_NAME_MAX : full_name_len); - - // Primary advertising data: FLAGS + Meshtastic service UUID128 (21 bytes - // total) - struct bt_data ad[] = { - {BT_DATA_FLAGS, sizeof(adv_flags_val), adv_flags_val}, - {BT_DATA_UUID128_ALL, sizeof(adv_uuid128_val), adv_uuid128_val}, - }; - // Scan response: device name (discovered after scan request) - struct bt_data sd[] = { - {(uint8_t)(name_shortened ? BT_DATA_NAME_SHORTENED : BT_DATA_NAME_COMPLETE), name_len, (const uint8_t *)name}, - }; - - // BT_LE_ADV_OPT_CONN = connectable legacy ADV_IND + stops after first - // connection (replaces deprecated - // CONNECTABLE|ONE_TIME in Zephyr 4.2.1; - // BT_LE_ADV_OPT_CONN = BIT(0)|BIT(1)) - // BT_LE_ADV_OPT_USE_IDENTITY = use static random identity address (stable - // across reboots) Advertising restart after disconnect is via - // adv_restart_work (system workqueue) so calling bt_le_adv_start() from the - // BT RX thread context is avoided. - int err = bt_le_adv_start(BT_LE_ADV_PARAM(BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_USE_IDENTITY, BT_GAP_ADV_FAST_INT_MIN_2, - BT_GAP_ADV_FAST_INT_MAX_2, NULL), - ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); - - if (err == -EALREADY) { - return; - } - if (err) { - LOG_WARN("BLE adv start failed: %d", err); - } else { - LOG_INFO("BLE advertising as '%s'", bt_get_name()); - } -} - -static void stop_advertising() -{ - bt_le_adv_stop(); -} - -// ── Connection callbacks -// ────────────────────────────────────────────────────── - -static void connected_cb(struct bt_conn *conn, uint8_t err) -{ - if (err) { - LOG_WARN("BLE connection failed, err=0x%02x", err); - return; - } - - k_mutex_lock(&ble_mutex, K_FOREVER); - active_conn = bt_conn_ref(conn); - k_mutex_unlock(&ble_mutex); - - memset(lastToRadio, 0, sizeof(lastToRadio)); - connect_time_ms = Time::skipZero(k_uptime_get_32()); - last_att_time_ms = connect_time_ms; - - char addr[BT_ADDR_LE_STR_LEN]; - bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); - LOG_INFO("BLE connected: %s", addr); - - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); - bluetoothStatus->updateStatus(&newStatus); - - // nRF54L15-DK has no screen - cannot display a PIN to the user. - // Requesting BT_SECURITY_L2 causes the OS to show a pairing dialog that - // the user dismisses, triggering disconnect + advertising restart failure. - // Skip security negotiation; the Meshtastic app works over plain GATT. - // (Security can be re-enabled once a display or NFC OOB path is available.) -} - -static void disconnected_cb(struct bt_conn *conn, uint8_t reason) -{ - LOG_INFO("BLE disconnected, reason=0x%02x", reason); - - k_mutex_lock(&ble_mutex, K_FOREVER); - if (active_conn) { - bt_conn_unref(active_conn); - active_conn = nullptr; - } - k_mutex_unlock(&ble_mutex); - - fromnum_ccc_val = 0; - logradio_ccc_val = 0; - connect_time_ms = 0; - last_att_time_ms = 0; - - if (phoneAPI) { - phoneAPI->close(); - } - memset(lastToRadio, 0, sizeof(lastToRadio)); - - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); - bluetoothStatus->updateStatus(&newStatus); - - // Schedule advertising restart via work queue - NOT from this callback - // directly. disconnected_cb runs on the BT RX thread; calling - // bt_le_adv_start() here would deadlock (see adv_restart_work comment above). - if (ble_enabled) { - k_work_submit(&adv_restart_work); - } -} - -#if defined(CONFIG_BT_SMP) -static void security_changed_cb(struct bt_conn *conn, bt_security_t level, enum bt_security_err err) -{ - if (err == BT_SECURITY_ERR_PIN_OR_KEY_MISSING) { - // Phone has a stale bond (device was wiped/reflashed). Unpair the stale - // entry so the phone re-pairs cleanly on the next connection attempt. - LOG_WARN("BLE stale bond (key missing) - unpairing"); - bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn)); - bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL); - } else if (err) { - LOG_WARN("BLE security change failed: level=%d err=%d", (int)level, (int)err); - } else { - LOG_INFO("BLE security level %d established", (int)level); - } -} -#endif /* CONFIG_BT_SMP */ - -BT_CONN_CB_DEFINE(conn_callbacks) = { - .connected = connected_cb, - .disconnected = disconnected_cb, -#if defined(CONFIG_BT_SMP) - .security_changed = security_changed_cb, -#endif -}; - -// ── Pairing / auth callbacks -// ────────────────────────────────────────────────── - -#if defined(CONFIG_BT_SMP) -static uint32_t configuredPasskey; - -static void auth_passkey_display(struct bt_conn *conn, unsigned int passkey) -{ - char passkey_str[7]; - snprintf(passkey_str, sizeof(passkey_str), "%06u", passkey); - configuredPasskey = passkey; - LOG_INFO("BLE pairing PIN: %s", passkey_str); - powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - - std::string textkey(passkey_str); - meshtastic::BluetoothStatus pairingStatus(textkey); - bluetoothStatus->updateStatus(&pairingStatus); -} - -static void auth_cancel(struct bt_conn *conn) -{ - LOG_WARN("BLE pairing cancelled"); -} - -static struct bt_conn_auth_cb auth_cb = { - .passkey_display = auth_passkey_display, - .passkey_entry = NULL, - .cancel = auth_cancel, -}; - -static void pairing_complete_cb(struct bt_conn *conn, bool bonded) -{ - LOG_INFO("BLE pairing complete, bonded=%d", (int)bonded); - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED); - bluetoothStatus->updateStatus(&newStatus); -} - -static void pairing_failed_cb(struct bt_conn *conn, enum bt_security_err reason) -{ - LOG_WARN("BLE pairing failed, reason=%d", (int)reason); - meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); - bluetoothStatus->updateStatus(&newStatus); -} - -static struct bt_conn_auth_info_cb auth_info_cb = { - .pairing_complete = pairing_complete_cb, - .pairing_failed = pairing_failed_cb, -}; -#endif /* CONFIG_BT_SMP */ - -// ── BluetoothPhoneAPI methods -// ───────────────────────────────────────────────── - -void BluetoothPhoneAPI::onNowHasData(uint32_t fromRadioNum) -{ - PhoneAPI::onNowHasData(fromRadioNum); - fromNumValue = fromRadioNum; - - if (!(fromnum_ccc_val & BT_GATT_CCC_NOTIFY)) - return; - - // active_conn may be torn down on another thread while we're dispatching - // this notify - acquire under ble_mutex so disconnected_cb can't free the - // conn between the null check and bt_conn_ref. - struct bt_conn *conn = acquire_active_conn(); - if (!conn) - return; - bt_gatt_notify(conn, &mesh_svc.attrs[FROMNUM_ATTR_IDX], &fromNumValue, sizeof(fromNumValue)); - bt_conn_unref(conn); -} - -bool BluetoothPhoneAPI::checkIsConnected() -{ - return active_conn != nullptr; -} - -// ── Deferred ToRadio processor + zombie-connection watchdog ────────────────── -// -// write_toradio() runs on the BT RX workqueue thread (CONFIG_BT_RX_STACK_SIZE) -// and cannot execute phoneAPI->handleToRadio() directly: handleStartConfig -// recurses through nanopb encode + state machine init and overflows the RX -// stack. This thread runs on the Meshtastic OSThread scheduler (24 KB stack), -// picks up the pending ToRadio buffer flagged by write_toradio(), and calls -// handleToRadio() with plenty of headroom. -// -// Real-time fromNum notifications are sent synchronously from -// BluetoothPhoneAPI::onNowHasData() (called by PhoneAPI when new data is -// queued). -// -// Zombie-connection detection has two tiers: -// -// (1) Liveness probe. After IDLE_BEFORE_PROBE_MS without ATT traffic, send -// a bt_gatt_notify to fromNum every PROBE_INTERVAL_MS. If the -// controller replies -ENOTCONN the LL link is definitely dead but the -// host didn't forward LE Disconnection Complete → reboot. We avoid -// probing during normal activity so iOS isn't woken up unnecessarily -// (each probe wakes iOS → triggers a zero-byte FromRadio drain). -// -// (2) Hard watchdog. Absolute HARD_WATCHDOG_MS ceiling on ATT idle as a -// fallback if probes somehow don't detect the zombie. -class BleDeferredThread : public concurrency::OSThread -{ - static constexpr uint32_t IDLE_BEFORE_PROBE_MS = 30000; // 30 s: start probing - static constexpr uint32_t PROBE_INTERVAL_MS = 5000; // 5 s: between probes - static constexpr uint32_t HARD_WATCHDOG_MS = 60000; // 1 min: last resort - - uint32_t last_probe_ms = 0; - - public: - BleDeferredThread() : concurrency::OSThread("BleDeferred") {} - - protected: - int32_t runOnce() override - { - // Snapshot the pending ToRadio buffer under the mutex, then release - // the lock before calling into handleToRadio (which can be slow and - // must not block the BT RX thread producer). - uint8_t buf[MAX_TO_FROM_RADIO_SIZE]; - size_t n = 0; - bool have_pending = false; - k_mutex_lock(&pendingToRadioMutex, K_FOREVER); - if (pendingToRadio) { - memcpy(buf, pendingToRadioBuf, pendingToRadioLen); - n = pendingToRadioLen; - pendingToRadio = false; - have_pending = true; - } - k_mutex_unlock(&pendingToRadioMutex); - if (have_pending && phoneAPI) { - phoneAPI->handleToRadio(buf, n); - } - - // Take a reference to active_conn so it can't be freed underneath us - // if disconnected_cb fires on another thread while we're dispatching. - struct bt_conn *conn = acquire_active_conn(); - if (!conn || connect_time_ms == 0) { - if (conn) - bt_conn_unref(conn); - last_probe_ms = 0; - return 100; - } - - uint32_t now = k_uptime_get_32(); - uint32_t att_idle = now - last_att_time_ms; - - // Liveness probe - only when ATT has been quiet for a while. - if (att_idle > IDLE_BEFORE_PROBE_MS && (now - last_probe_ms) >= PROBE_INTERVAL_MS && - (fromnum_ccc_val & BT_GATT_CCC_NOTIFY)) { - last_probe_ms = now; - int err = bt_gatt_notify(conn, &mesh_svc.attrs[FROMNUM_ATTR_IDX], &fromNumValue, sizeof(fromNumValue)); - if (err == -ENOTCONN) { - LOG_WARN("BLE zombie (probe ENOTCONN); rebooting"); - bt_conn_unref(conn); - k_sleep(K_MSEC(50)); // flush log - sys_reboot(SYS_REBOOT_COLD); - } - } - bt_conn_unref(conn); - - // Hard ceiling - last-resort reboot if probes miss the zombie. - if (att_idle > HARD_WATCHDOG_MS && (now - connect_time_ms) > HARD_WATCHDOG_MS) { - LOG_WARN("BLE zombie (hard watchdog %us); rebooting", HARD_WATCHDOG_MS / 1000); - k_sleep(K_MSEC(50)); - sys_reboot(SYS_REBOOT_COLD); - } - return 100; - } -}; - -static BleDeferredThread *bleDeferredThread = nullptr; - -// ── BT stack pre-initializer (call from main thread before OSThreads start) ── -// -// bt_enable() requires substantially more stack than a Meshtastic OSThread -// (PowerFSMThread) provides - calling it there causes a stack overflow. -// Call this from nrf54l15Setup() (main Zephyr thread, CONFIG_MAIN_STACK_SIZE) -// so that by the time NRF54L15Bluetooth::setup() runs from PowerFSMThread, -// bt_initialized is already true and bt_enable() is skipped. - -void nrf54l15_bt_preinit() -{ - if (!bt_initialized) { - int err = bt_enable(NULL); - if (err) { - LOG_ERROR("BLE pre-init failed: %d", err); - return; - } - bt_initialized = true; - LOG_INFO("BLE stack pre-initialized on main thread"); - - // Phase 7: load bonding keys from LittleFS (/lfs/bt_settings). - // LittleFS is already mounted by fsInit() before nrf54l15Setup() runs. - // On first boot the file doesn't exist - settings_load() returns 0 (OK). - // On subsequent boots, previously bonded peers are restored so the - // phone can reconnect without re-pairing. - err = settings_load(); - if (err) { - LOG_WARN("settings_load failed: %d (OK on first boot)", err); - } else { - LOG_INFO("BT settings loaded from /lfs/bt_settings"); - } - } -} - -// ── NRF54L15Bluetooth public methods ───────────────────────────────────────── - -// Shared init: idempotent setup of work item, OSThread, auth callbacks, -// bt_enable, and device name. Leaves advertising control to the caller. -static bool nrf54l15_bt_init_common() -{ - k_work_init(&adv_restart_work, adv_restart_work_fn); - - if (!bleDeferredThread) { - bleDeferredThread = new BleDeferredThread(); - } - - if (!phoneAPI) { - phoneAPI = new BluetoothPhoneAPI(); - } - -#if defined(CONFIG_BT_SMP) - // NO_PIN is unsupported on this platform: the mesh GATT permissions are - // declared with BT_GATT_PERM_*_AUTHEN, prj.conf sets - // CONFIG_BT_SMP_ENFORCE_MITM=y, and the build pulls in the SMP/passkey path. - // If a user requested NO_PIN we'd register no auth callbacks → no display - // path for the passkey → every GATT access returns BT_ATT_ERR_AUTHENTICATION - // and the link is unusable. Fall back to RANDOM_PIN behavior with a warning - // instead of leaving BLE silently broken. - if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { - LOG_WARN("BLE: NO_PIN not supported on nRF54L15-DK (MITM-only build); " - "treat as RANDOM_PIN"); - } - - bt_conn_auth_cb_register(&auth_cb); - bt_conn_auth_info_cb_register(&auth_info_cb); - - // FIXED_PIN - register the configured passkey so the mobile app prompts - // the user for that specific number instead of a random display-only PIN. - // RANDOM_PIN (and clamped NO_PIN) keeps the default behavior: Zephyr - // generates a fresh passkey on each pairing attempt and fires - // auth_passkey_display with it. - if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN) { - configuredPasskey = config.bluetooth.fixed_pin; - int rc = bt_passkey_set(configuredPasskey); - if (rc) { - LOG_WARN("bt_passkey_set(%u) failed: %d", configuredPasskey, rc); - } else { - LOG_INFO("BLE fixed PIN: %06u", configuredPasskey); - } - } else { - bt_passkey_set(BT_PASSKEY_INVALID); // random per-pair - } -#endif /* CONFIG_BT_SMP */ - - if (!bt_initialized) { - int err = bt_enable(NULL); - if (err) { - LOG_ERROR("BLE enable failed: %d", err); - return false; - } - bt_initialized = true; - LOG_INFO("BLE stack enabled"); - } - - bt_set_name(getDeviceName()); - return true; -} - -void NRF54L15Bluetooth::setup() -{ - LOG_INFO("NRF54L15Bluetooth::setup()"); - if (!nrf54l15_bt_init_common()) { - return; - } - ble_enabled = true; - start_advertising(); -} - -void NRF54L15Bluetooth::shutdown() -{ - LOG_INFO("NRF54L15Bluetooth::shutdown()"); - ble_enabled = false; - stop_advertising(); - - struct bt_conn *conn = acquire_active_conn(); - if (conn) { - bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); - bt_conn_unref(conn); - } -} - -void NRF54L15Bluetooth::startDisabled() -{ - // Initialize BT stack but leave advertising off until resumeAdvertising(). - if (!nrf54l15_bt_init_common()) { - return; - } - ble_enabled = false; - LOG_INFO("BLE initialized, adv stopped (startDisabled)"); -} - -void NRF54L15Bluetooth::resumeAdvertising() -{ - ble_enabled = true; - start_advertising(); -} - -void NRF54L15Bluetooth::clearBonds() -{ - LOG_INFO("BLE clear bonds"); - bt_unpair(BT_ID_DEFAULT, BT_ADDR_LE_ANY); -} - -bool NRF54L15Bluetooth::isConnected() -{ - return active_conn != nullptr; -} - -int NRF54L15Bluetooth::getRssi() -{ - return 0; // TODO: Zephyr has no direct bt_conn_get_rssi; use HCI RSSI read - // command -} - -void NRF54L15Bluetooth::sendLog(const uint8_t *logMessage, size_t length) -{ - if (length > 512 || logradio_ccc_val == 0) { - return; - } - // Acquire a reference under ble_mutex so disconnected_cb can't free the - // connection between the null check and bt_gatt_notify. - struct bt_conn *conn = acquire_active_conn(); - if (!conn) { - return; - } - // Send as notify regardless of whether client subscribed to NOTIFY or - // INDICATE - bt_gatt_indicate() requires a params struct with a callback; - // notify is simpler and the app accepts both. Change to indicate if - // compatibility issues arise. - bt_gatt_notify(conn, &mesh_svc.attrs[LOGRADIO_ATTR_IDX], logMessage, (uint16_t)length); - bt_conn_unref(conn); -} diff --git a/src/platform/nrf54l15/NRF54L15Bluetooth.h b/src/platform/nrf54l15/NRF54L15Bluetooth.h deleted file mode 100644 index 36d434d37c..0000000000 --- a/src/platform/nrf54l15/NRF54L15Bluetooth.h +++ /dev/null @@ -1,29 +0,0 @@ -// NRF54L15Bluetooth.h - Zephyr BLE backend for nRF54L15 -// -// Implements the same interface as NRF52Bluetooth (same method names and -// signatures) so main.cpp and AdminModule can use nrf52Bluetooth pointer -// without knowing the underlying implementation. -// -// GATT profile is identical to the nRF52 implementation: -// Service: MESH_SERVICE_UUID -// toRadio: TORADIO_UUID (WRITE) -// fromRadio: FROMRADIO_UUID (READ) -// fromNum: FROMNUM_UUID (READ | NOTIFY) -// logRadio: LOGRADIO_UUID (READ | NOTIFY | INDICATE) - -#pragma once - -#include "BluetoothCommon.h" - -class NRF54L15Bluetooth : public BluetoothApi -{ - public: - void setup(); - void shutdown(); - void startDisabled(); - void resumeAdvertising(); - void clearBonds(); - bool isConnected(); - int getRssi(); - void sendLog(const uint8_t *logMessage, size_t length); -}; diff --git a/src/platform/nrf54l15/Nrf52SaadcLock.h b/src/platform/nrf54l15/Nrf52SaadcLock.h deleted file mode 100644 index 1e0a04bc21..0000000000 --- a/src/platform/nrf54l15/Nrf52SaadcLock.h +++ /dev/null @@ -1,17 +0,0 @@ -// Nrf52SaadcLock.h - stub for nRF54L15/Zephyr -// Power.cpp includes this when ARCH_NRF52 is defined. -// Phase 2: compile-only stub. -#pragma once - -#ifdef ARCH_NRF52 - -#include "concurrency/Lock.h" - -namespace concurrency -{ -/** Shared mutex for SAADC configuration and reads (VDD + battery analog path). - * On nRF54L15 ADC is handled differently; this is a compile-only stub. */ -extern Lock *nrf52SaadcLock; -} // namespace concurrency - -#endif diff --git a/src/platform/nrf54l15/Print.h b/src/platform/nrf54l15/Print.h deleted file mode 100644 index 5db9a7406b..0000000000 --- a/src/platform/nrf54l15/Print.h +++ /dev/null @@ -1,4 +0,0 @@ -// Print.h - shim for nRF54L15/Zephyr -// Meshtastic includes separately; redirect to our Arduino.h shim. -#pragma once -#include "Arduino.h" diff --git a/src/platform/nrf54l15/SPI.h b/src/platform/nrf54l15/SPI.h deleted file mode 100644 index b35af0dc96..0000000000 --- a/src/platform/nrf54l15/SPI.h +++ /dev/null @@ -1,62 +0,0 @@ -/** - * SPI.h - Arduino SPI shim for Zephyr/nRF54L15 - * - * Provides the Arduino SPIClass interface backed by Zephyr's SPI API. The - * backing controller is SPIM00 (HP domain, 3.0 V); the implementation in - * nrf54l15_arduino.cpp binds to DEVICE_DT_GET(DT_NODELABEL(spi00)) and the - * bus is configured in zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay. - * RadioLib uses ArduinoHal which calls transfer() byte-by-byte. - * - * CS pin is handled by RadioLib via digitalWrite() - hardware CS is not used. - */ - -#pragma once - -#include "Arduino.h" -#include - -#define SPI_MODE0 0 -#define SPI_MODE1 1 -#define SPI_MODE2 2 -#define SPI_MODE3 3 - -struct SPISettings { - uint32_t clock; - uint8_t bitOrder; - uint8_t dataMode; - - // Arduino API allows `SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0))` - implicit form is intentional. - // cppcheck-suppress noExplicitConstructor - SPISettings(uint32_t clock = 4000000, uint8_t bitOrder = MSBFIRST, uint8_t dataMode = SPI_MODE0) - : clock(clock), bitOrder(bitOrder), dataMode(dataMode) - { - } -}; - -class SPIClass -{ - public: - void begin() {} - void begin(uint8_t sck, uint8_t miso, uint8_t mosi, uint8_t ss = 0xFF) {} - void end() {} - void beginTransaction(SPISettings) {} - void endTransaction() {} - void setBitOrder(uint8_t order) {} - void setDataMode(uint8_t mode) {} - void setClockDivider(uint8_t div) {} - void setFrequency(uint32_t freq) {} - - // Real Zephyr SPI implementation - defined in nrf54l15_arduino.cpp - uint8_t transfer(uint8_t data); - uint16_t transfer16(uint16_t data); - void transfer(void *buf, size_t count); - void transferBytes(const uint8_t *tx, uint8_t *rx, uint32_t count); - uint8_t transfer(uint8_t tx, uint8_t *rx, uint32_t count) - { - transferBytes(&tx, rx, count); - return rx ? rx[0] : 0; - } -}; - -extern SPIClass SPI; -extern SPIClass SPI1; diff --git a/src/platform/nrf54l15/Stream.h b/src/platform/nrf54l15/Stream.h deleted file mode 100644 index 450423cbf2..0000000000 --- a/src/platform/nrf54l15/Stream.h +++ /dev/null @@ -1,5 +0,0 @@ -// Stream.h - shim for nRF54L15/Zephyr -// StreamAPI.h and other Meshtastic headers include . -// Redirect to our Arduino.h shim which defines the Stream base class. -#pragma once -#include "Arduino.h" diff --git a/src/platform/nrf54l15/Tone.h b/src/platform/nrf54l15/Tone.h deleted file mode 100644 index 4a12310b7b..0000000000 --- a/src/platform/nrf54l15/Tone.h +++ /dev/null @@ -1,4 +0,0 @@ -// Tone.h - shim for nRF54L15/Zephyr -// Tone functions are stubbed in Arduino.h; this header satisfies direct includes. -#pragma once -#include "Arduino.h" diff --git a/src/platform/nrf54l15/WProgram.h b/src/platform/nrf54l15/WProgram.h deleted file mode 100644 index a8e3dc0d54..0000000000 --- a/src/platform/nrf54l15/WProgram.h +++ /dev/null @@ -1,5 +0,0 @@ -// WProgram.h - shim for nRF54L15/Zephyr -// ArduinoThread (and other legacy Arduino libs) include . -// Redirect to our Arduino.h shim. -#pragma once -#include "Arduino.h" diff --git a/src/platform/nrf54l15/Wire.cpp b/src/platform/nrf54l15/Wire.cpp deleted file mode 100644 index 96f952df33..0000000000 --- a/src/platform/nrf54l15/Wire.cpp +++ /dev/null @@ -1,219 +0,0 @@ -// Wire.cpp - Arduino TwoWire backed by Zephyr i2c30 (TWIM30 hardware). -// -// The pinctrl + clock-frequency are configured in -// zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay. Runtime begin()/setClock() -// are best-effort: setClock() goes through i2c_configure() to actually change -// the bus speed; begin() just verifies the device is ready. - -#include "Wire.h" -#include "configuration.h" - -#include -#include -#include -#include - -// Resolve the i2c30 node at compile time. If the overlay has not enabled -// i2c30, this evaluates to a NULL device pointer and every call short-circuits -// to a NACK return code - matching the prior compile-only stub behavior. -#define I2C_NODE DT_NODELABEL(i2c30) - -static const struct device *getI2CDevice() -{ -#if DT_NODE_HAS_STATUS(I2C_NODE, okay) - static const struct device *const dev = DEVICE_DT_GET(I2C_NODE); - return dev; -#else - return nullptr; -#endif -} - -// Wire/Wire1 instances are defined in nrf54l15_arduino.cpp alongside the -// other Arduino singletons (Serial, SPI, …). - -TwoWire::TwoWire() : txAddr(0), txLen(0), txBuf{}, rxLen(0), rxPos(0), rxBuf{} {} - -void TwoWire::begin() -{ - const struct device *dev = getI2CDevice(); - if (dev == nullptr) { - LOG_WARN("Wire.begin(): i2c30 not enabled in DT overlay"); - return; - } - if (!device_is_ready(dev)) { - LOG_WARN("Wire.begin(): i2c30 device not ready"); - return; - } - LOG_INFO("Wire.begin(): i2c30 ready"); -} - -void TwoWire::begin(uint8_t /*sda*/, uint8_t /*scl*/) -{ - // SDA/SCL fixed by overlay pinctrl - pin args ignored. - begin(); -} - -void TwoWire::begin(int /*sda*/, int /*scl*/, uint32_t freq) -{ - begin(); - if (freq) { - setClock(freq); - } -} - -void TwoWire::end() -{ - // No-op: Zephyr i2c devices stay initialized for the lifetime of the - // application. Runtime PM (zephyr,pm-device-runtime-auto in the DT) - // handles low-power transitions when idle. -} - -void TwoWire::setClock(uint32_t freq) -{ - const struct device *dev = getI2CDevice(); - if (dev == nullptr) { - return; - } - uint32_t speed; - if (freq >= 1000000U) { - speed = I2C_SPEED_FAST_PLUS; // 1 MHz - } else if (freq >= 400000U) { - speed = I2C_SPEED_FAST; // 400 kHz - } else { - speed = I2C_SPEED_STANDARD; // 100 kHz - } - uint32_t cfg = I2C_MODE_CONTROLLER | I2C_SPEED_SET(speed); - int rc = i2c_configure(dev, cfg); - if (rc) { - LOG_WARN("Wire.setClock(%u) failed: %d", (unsigned)freq, rc); - } -} - -void TwoWire::beginTransmission(uint8_t addr) -{ - txAddr = addr; - txLen = 0; -} - -size_t TwoWire::write(uint8_t data) -{ - if (txLen >= WIRE_BUFFER_LENGTH) { - return 0; // overflow - endTransmission() will return 1 - } - txBuf[txLen++] = data; - return 1; -} - -size_t TwoWire::write(const uint8_t *data, size_t n) -{ - size_t written = 0; - for (size_t i = 0; i < n; i++) { - if (write(data[i]) == 0) { - break; - } - written++; - } - return written; -} - -uint8_t TwoWire::endTransmission(bool /*stop*/) -{ - // Arduino return codes: - // 0 = success - // 1 = data-too-long (overflow caught in write()) - // 2 = NACK on address - // 3 = NACK on data - // 4 = other error - // 5 = timeout - if (txLen > WIRE_BUFFER_LENGTH) { - return 1; - } - const struct device *dev = getI2CDevice(); - if (dev == nullptr || !device_is_ready(dev)) { - return 4; - } - int rc = i2c_write(dev, txBuf, txLen, txAddr); - txLen = 0; - if (rc == 0) { - return 0; - } - if (rc == -EIO) { - return 2; // address NACK is the most common -EIO source on nrf-twim - } - if (rc == -ETIMEDOUT) { - return 5; - } - return 4; -} - -uint8_t TwoWire::requestFrom(uint8_t addr, uint8_t quantity, bool /*stop*/) -{ - rxLen = 0; - rxPos = 0; - if (quantity == 0) { - return 0; - } - if (quantity > WIRE_BUFFER_LENGTH) { - quantity = WIRE_BUFFER_LENGTH; - } - const struct device *dev = getI2CDevice(); - if (dev == nullptr || !device_is_ready(dev)) { - return 0; - } - - // If there is a pending TX (driver wrote register address via write() - // without an explicit endTransmission()), use i2c_write_read so the - // repeated-start path matches the typical "set register pointer then - // read N bytes" sensor protocol. - int rc; - if (txLen > 0) { - rc = i2c_write_read(dev, addr, txBuf, txLen, rxBuf, quantity); - txLen = 0; - } else { - rc = i2c_read(dev, rxBuf, quantity, addr); - } - if (rc) { - return 0; - } - rxLen = quantity; - return quantity; -} - -int TwoWire::available() -{ - return rxLen - rxPos; -} - -int TwoWire::read() -{ - if (rxPos >= rxLen) { - return -1; - } - return rxBuf[rxPos++]; -} - -int TwoWire::peek() -{ - if (rxPos >= rxLen) { - return -1; - } - return rxBuf[rxPos]; -} - -size_t TwoWire::readBytes(uint8_t *buf, size_t len) -{ - size_t n = 0; - while (n < len) { - int b = read(); - if (b < 0) { - break; - } - buf[n++] = (uint8_t)b; - } - return n; -} - -TwoWire::operator bool() const -{ - return getI2CDevice() != nullptr; -} diff --git a/src/platform/nrf54l15/Wire.h b/src/platform/nrf54l15/Wire.h deleted file mode 100644 index febf24527e..0000000000 --- a/src/platform/nrf54l15/Wire.h +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Wire.h - Arduino TwoWire (I2C) shim for Zephyr / nRF54L15. - * - * Bus binding: the Zephyr device tree alias `i2c30` (TWIM30 hardware - * peripheral, HP domain, 3.0 V) is resolved at compile time via - * DEVICE_DT_GET in Wire.cpp. SDA/SCL pins are configured in the board - * overlay via pinctrl - `begin(sda, scl)` overloads are accepted for - * Arduino API compatibility but the pin arguments are ignored. - * - * Buffer sizes are sized for the worst-case I2C consumer we plan to use - * (NXP SE050 secure element, ~256-byte T=1 frames). BMP280 / INA228 / - * SHT40 / INA3221 read in single-digit bytes and fit trivially. - */ - -#pragma once - -#include "Arduino.h" -#include -#include - -#ifndef WIRE_BUFFER_LENGTH -#define WIRE_BUFFER_LENGTH 256 -#endif - -class TwoWire -{ - public: - TwoWire(); - - // ── Bus lifecycle ───────────────────────────────────────────────── - // begin() variants - pin arguments are accepted for API compatibility - // but ignored: SDA/SCL are fixed by the Zephyr overlay pinctrl. freq - // is also fixed by overlay clock-frequency (use setClock() at runtime). - void begin(); - void begin(uint8_t sda, uint8_t scl); - void begin(int sda, int scl, uint32_t freq); - void end(); - - void setClock(uint32_t freq); - void setClockStretchLimit(uint32_t) {} // no-op on TWIM hardware - - // ── Master write ───────────────────────────────────────────────── - void beginTransmission(uint8_t addr); - void beginTransmission(int addr) { beginTransmission((uint8_t)addr); } - // Return codes (Arduino convention): - // 0 = success, 1 = data-too-long, 2 = NACK on addr, 3 = NACK on data, - // 4 = other error, 5 = timeout. - uint8_t endTransmission(bool stop = true); - uint8_t endTransmission(uint8_t stop) { return endTransmission(stop != 0); } - - size_t write(uint8_t data); - size_t write(const uint8_t *data, size_t n); - - // ── Master read ────────────────────────────────────────────────── - uint8_t requestFrom(uint8_t addr, uint8_t quantity, bool stop = true); - uint8_t requestFrom(uint8_t addr, uint8_t quantity, uint8_t stop) { return requestFrom(addr, quantity, stop != 0); } - uint8_t requestFrom(int addr, int quantity, int stop = 1) { return requestFrom((uint8_t)addr, (uint8_t)quantity, stop != 0); } - - int available(); - int read(); - int peek(); - size_t readBytes(uint8_t *buf, size_t len); - size_t readBytes(char *buf, size_t len) { return readBytes((uint8_t *)buf, len); } - void flush() {} - - // Slave callbacks unsupported - peripheral-only stub. - void onReceive(void (*)(int)) {} - void onRequest(void (*)(void)) {} - - operator bool() const; - - private: - uint8_t txAddr; - uint16_t txLen; - uint8_t txBuf[WIRE_BUFFER_LENGTH]; - - uint16_t rxLen; - uint16_t rxPos; - uint8_t rxBuf[WIRE_BUFFER_LENGTH]; -}; - -extern TwoWire Wire; -extern TwoWire Wire1; // alias to Wire - only one I2C bus on this board diff --git a/src/platform/nrf54l15/architecture.h b/src/platform/nrf54l15/architecture.h deleted file mode 100644 index 24b3c980dd..0000000000 --- a/src/platform/nrf54l15/architecture.h +++ /dev/null @@ -1,79 +0,0 @@ -#pragma once - -#define ARCH_NRF54L15 - -// -// Feature flags for nRF54L15. -// -// The HAS_* macros below are Meshtastic's compile-time feature gate: every -// optional subsystem (BLE, screen, I2C, GPS, buttons, telemetry, sensors, -// radio, CPU shutdown, ...) is wrapped in `#if HAS_FOO` so a given board -// only pays for the features it actually ships. On memory-tight MCUs this -// is not cosmetic - it's the difference between a binary that fits in -// flash and one that doesn't, and between a build that links and one that -// drags in drivers for hardware the board doesn't have. Defaulting to 0 -// here (rather than inheriting nRF52 defaults) is deliberate: the -// nRF54L15-DK is a bare dev kit with no screen, no I2C sensors, no GPS, -// no user buttons - so every HAS_* flag starts off and gets flipped on -// explicitly by variants that add that hardware. -// -// Feature flags are also the cleanest way to absorb platform divergence -// without sprinkling `#ifdef ARCH_NRF54L15` across shared code. Anywhere -// a subsystem can be conditionally compiled via HAS_*, prefer that over -// per-arch guards: it keeps the core code arch-agnostic, makes it trivial -// to bring up the next board (flip the flags, don't patch call sites), -// and keeps the "does this platform support X?" question answerable by -// reading one file instead of grepping the tree. BLE in particular is -// deferred to Phase 2 on this port - the nRF54L15 uses MPSL/Zephyr BLE -// APIs rather than the Adafruit SoftDevice stack used by nRF52840 - so -// while HAS_BLUETOOTH defaults to 1, the actual implementation lives in -// NRF54L15Bluetooth.cpp behind its own Zephyr Kconfig gates. -// - -#ifndef HAS_BLUETOOTH -#define HAS_BLUETOOTH 1 -#endif -#ifndef HAS_SCREEN -#define HAS_SCREEN 0 -#endif -#ifndef HAS_WIRE -#define HAS_WIRE 0 -#endif -#ifndef HAS_GPS -#define HAS_GPS 0 -#endif -#ifndef HAS_BUTTON -#define HAS_BUTTON 0 -#endif -#ifndef HAS_TELEMETRY -#define HAS_TELEMETRY 0 -#endif -#ifndef HAS_SENSOR -#define HAS_SENSOR 0 -#endif -#ifndef HAS_RADIO -#define HAS_RADIO 1 -#endif -#ifndef HAS_CPU_SHUTDOWN -#define HAS_CPU_SHUTDOWN 0 -#endif - -// ADC reference - nRF54L15 SAADC uses VDD/4 internal ref by default -#ifndef AREF_VOLTAGE -#define AREF_VOLTAGE 3.6 -#endif -#ifndef BATTERY_SENSE_RESOLUTION_BITS -#define BATTERY_SENSE_RESOLUTION_BITS 12 -#endif - -// -// HW_VENDOR - maps build-time define to HardwareModel enum. -// PRIVATE_HW (255): the protobuf HardwareModel enum reserves DK / DIY boards -// without an SKU under this value; the nRF54L15-DK doesn't get a dedicated -// enum number. Variant manifest matches via custom_meshtastic_hw_model = 255. -// -#ifdef NRF54L15_DK -#define HW_VENDOR meshtastic_HardwareModel_PRIVATE_HW -#else -#define HW_VENDOR meshtastic_HardwareModel_UNSET -#endif diff --git a/src/platform/nrf54l15/bluefruit.h b/src/platform/nrf54l15/bluefruit.h deleted file mode 100644 index 306e810b4a..0000000000 --- a/src/platform/nrf54l15/bluefruit.h +++ /dev/null @@ -1,19 +0,0 @@ -// bluefruit.h - stub for nRF54L15/Zephyr -// NodeDB.cpp includes this when ARCH_NRF52 is defined. -// Bluetooth is excluded (MESHTASTIC_EXCLUDE_BLUETOOTH=1); this satisfies -// the include chain without pulling in the Adafruit Bluefruit SDK. -#pragma once - -struct BLEPeripheral { - void clearBonds() {} -}; -struct BLECentral { - void clearBonds() {} -}; - -struct BlueFruitClass { - BLEPeripheral Periph; - BLECentral Central; -}; - -extern BlueFruitClass Bluefruit; diff --git a/src/platform/nrf54l15/main-nrf54l15.cpp b/src/platform/nrf54l15/main-nrf54l15.cpp deleted file mode 100644 index b91fe67ee4..0000000000 --- a/src/platform/nrf54l15/main-nrf54l15.cpp +++ /dev/null @@ -1,222 +0,0 @@ -/* - * main-nrf54l15.cpp - Platform entry points for Nordic nRF54L15 - * - * Adapted from src/platform/nrf52/main-nrf52.cpp. - * SoftDevice, Adafruit BLE, and nRFCrypto are NOT available on nRF54L15. - * Phase 2 will add proper BLE via Zephyr MPSL APIs. - * - * TODO items are marked with "TODO(nrf54l15):" - */ - -#include "configuration.h" -#include -#include -#include -#include -#include - -#include "NodeDB.h" -#include "Power.h" -#include "PowerMon.h" -#include "Router.h" -#include "error.h" -#include "main.h" -#include "mesh/MeshService.h" -#include "meshUtils.h" -#include - -// ── Watchdog ────────────────────────────────────────────────────────────── -// TODO(nrf54l15): nRF54L15 has a WDT peripheral but nrfx_wdt driver support -// may differ depending on the Zephyr SDK version. Enable once confirmed. -#define APP_WATCHDOG_SECS 90 -static bool watchdog_running = false; - -static inline void watchdog_feed() {} // TODO(nrf54l15): replace with real WDT feed - -// ── Weak variant hooks ──────────────────────────────────────────────────── -void variant_shutdown() __attribute__((weak)); -void variant_shutdown() {} - -void variant_nrf54l15LoopHook(void) __attribute__((weak)); -void variant_nrf54l15LoopHook(void) {} - -// ── PowerHAL ───────────────────────────────────────────────────────────── -bool powerHAL_isVBUSConnected() -{ - // TODO(nrf54l15): nRF54L15 has a USB POWER peripheral - read USBREGSTATUS - return false; -} - -bool powerHAL_isPowerLevelSafe() -{ - // TODO(nrf54l15): implement SAADC VDD measurement similar to nRF52 - return true; -} - -void powerHAL_platformInit() -{ - // TODO(nrf54l15): configure POF comparator and analog reference if needed -} - -// ── Utilities ───────────────────────────────────────────────────────────── -bool loopCanSleep() -{ - return !Serial; -} - -void updateBatteryLevel(uint8_t level) -{ - (void)level; -} - -void __attribute__((noreturn)) __assert_func(const char *file, int line, const char *func, const char *failedexpr) -{ - LOG_ERROR("assert failed %s: %d, %s, test=%s", file, line, func, failedexpr); - NVIC_SystemReset(); -} - -void getMacAddr(uint8_t *dmac) -{ - // TODO(nrf54l15): verify FICR register layout for nRF54L15. - // nRF52840 uses NRF_FICR->DEVICEADDR[0/1]; nRF54L15 Zephyr HAL may differ. -#if defined(NRF_FICR) - const uint8_t *src = (const uint8_t *)NRF_FICR->DEVICEADDR; - dmac[5] = src[0]; - dmac[4] = src[1]; - dmac[3] = src[2]; - dmac[2] = src[3]; - dmac[1] = src[4]; - dmac[0] = src[5] | 0xc0; -#else - // Fallback: fixed placeholder until Zephyr FICR path is confirmed - dmac[0] = 0xC2; - dmac[1] = 0xA7; - dmac[2] = 0x54; - dmac[3] = 0x15; - dmac[4] = 0x00; - dmac[5] = 0x01; -#endif -} - -bool getDeviceId(uint8_t *deviceId) -{ - // nRF54L15: DEVICEID under the FICR->INFO sub-struct. Read unconditionally so a future build - // lacking NRF_FICR fails loudly rather than silently sharing getMacAddr()'s placeholder MAC. - uint64_t device_id_start = ((uint64_t)NRF_FICR->INFO.DEVICEID[1] << 32) | NRF_FICR->INFO.DEVICEID[0]; - uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0]; - memcpy(deviceId, &device_id_start, sizeof(device_id_start)); - memcpy(deviceId + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); - return true; -} - -// ── Bluetooth ───────────────────────────────────────────────────────────────── - -void setBluetoothEnable(bool enable) -{ - if (enable) { - static bool initialized = false; - if (!initialized) { - nrf54l15Bluetooth = new NRF54L15Bluetooth(); - nrf54l15Bluetooth->startDisabled(); - initialized = true; - } - if (nrf54l15Bluetooth) { - nrf54l15Bluetooth->resumeAdvertising(); - } - } else { - if (nrf54l15Bluetooth) { - nrf54l15Bluetooth->shutdown(); - } - } -} - -void clearBonds() -{ - if (!nrf54l15Bluetooth) { - nrf54l15Bluetooth = new NRF54L15Bluetooth(); - nrf54l15Bluetooth->setup(); - } - nrf54l15Bluetooth->clearBonds(); -} - -void enterDfuMode() -{ - // TODO(nrf54l15): nRF54L15 uses nRF Connect DFU (MCUboot/SUIT). - // Trigger via Zephyr boot_request_upgrade() or similar. - NVIC_SystemReset(); -} - -// ── printf via RTT ──────────────────────────────────────────────────────── -// TODO(nrf54l15): SEGGER_RTT may not be available with Zephyr; use printk() -// or a USB CDC console instead. Remove this override if it conflicts. -#ifdef SEGGER_RTT_PRINTF -int printf(const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - auto res = SEGGER_RTT_vprintf(0, fmt, &args); - va_end(args); - return res; -} -#endif - -// ── Deep sleep ──────────────────────────────────────────────────────────── -void cpuDeepSleep(uint32_t msecToWake) -{ -#if HAS_WIRE - Wire.end(); -#endif - SPI.end(); - if (Serial) - Serial.end(); - - variant_shutdown(); - - // TODO(nrf54l15): use Zephyr pm_system_suspend() or WFI for proper low-power - if (msecToWake != portMAX_DELAY) { - delay(msecToWake); - NVIC_SystemReset(); - } else { - // System off equivalent - halt - while (1) { - __WFI(); - } - } -} - -// ── Setup / Loop ────────────────────────────────────────────────────────── -// Forward declaration - defined in NRF54L15Bluetooth.cpp -void nrf54l15_bt_preinit(); - -void nrf54l15Setup() -{ - // nRF54L15 power peripheral layout differs from nRF52; RESETREAS not present here. - // TODO(Phase 3): use zephyr/drivers/hwinfo.h hwinfo_get_reset_cause() - LOG_DEBUG("Reset reason: (nRF54L15 power peripheral differs from nRF52, skipped)"); - - // TODO(nrf54l15): init SAADC, watchdog, and random seed via nrfx or Zephyr - // For now seed with a fixed value; replace with hardware entropy source. -#if defined(NRF_FICR) - randomSeed(analogRead(0) ^ (uint32_t)NRF_FICR->DEVICEADDR[0]); -#else - randomSeed(analogRead(0)); -#endif - - // Pre-initialize BT stack here on the main thread (CONFIG_MAIN_STACK_SIZE=8192). - // bt_enable() overflows the smaller PowerFSMThread stack when called later. - // NRF54L15Bluetooth::setup() checks bt_initialized and skips bt_enable() if true. - nrf54l15_bt_preinit(); -} - -void nrf54l15Loop() -{ - // First-call gate for the future WDT init - body will hold real init code, not just the bookkeeping flag. - // cppcheck-suppress duplicateConditionalAssign - if (!watchdog_running) { - // TODO(nrf54l15): enable WDT here - watchdog_running = true; - } - watchdog_feed(); - - variant_nrf54l15LoopHook(); -} diff --git a/src/platform/nrf54l15/nrf54l15_arduino.cpp b/src/platform/nrf54l15/nrf54l15_arduino.cpp deleted file mode 100644 index 8d8c30b323..0000000000 --- a/src/platform/nrf54l15/nrf54l15_arduino.cpp +++ /dev/null @@ -1,557 +0,0 @@ -/** - * nrf54l15_arduino.cpp - Arduino shim implementations for Zephyr/nRF54L15 - * - * Provides concrete implementations for Print, HardwareSerial, GPIO, SPI, - * and String methods declared in Arduino.h / SPI.h. - * - * Phase 3: real GPIO via Zephyr GPIO API and real SPI via Zephyr SPI API. - * Pin numbering convention: P0.n = n, P1.n = 16+n, P2.n = 32+n. - */ - -#include "Arduino.h" -#include "SPI.h" -#include "Wire.h" -#include -#include -#include -#include -#include -#include -#include -// ── Bluefruit singleton stub (satisfies NodeDB.cpp ARCH_NRF52 path) ────────── -#include "bluefruit.h" -BlueFruitClass Bluefruit; - -// ── _fini stub - ARM newlib's __libc_fini_array references _fini, but ──────── -// Zephyr startup doesn't provide it. Provide a weak no-op so the linker -// is satisfied when C++ global dtors or atexit() pull in __libc_fini_array. -extern "C" void __attribute__((weak)) _fini(void) {} - -// ── SPI / Wire singletons ───────────────────────────────────────────────────── -SPIClass SPI; -SPIClass SPI1; -TwoWire Wire; -TwoWire Wire1; - -// ── HardwareSerial singletons ──────────────────────────────────────────────── -HardwareSerial Serial; -HardwareSerial Serial1; -HardwareSerial Serial2; - -// ── Timing functions - C linkage to match extern "C" declarations ──────────── -extern "C" uint32_t millis(void) -{ - return (uint32_t)k_uptime_get_32(); -} -extern "C" uint32_t micros(void) -{ - return (uint32_t)(k_uptime_get() * 1000ULL); -} -extern "C" void delay(uint32_t ms) -{ - k_sleep(K_MSEC(ms)); -} -extern "C" void delayMicroseconds(uint32_t us) -{ - k_sleep(K_USEC(us)); -} -extern "C" void yield(void) -{ - k_yield(); -} - -// ── NVIC_SystemReset - wraps __NVIC_SystemReset from CMSIS core_cm33.h ─────── -// core_cm33.h has #define NVIC_SystemReset __NVIC_SystemReset, so undef it -// before defining our own implementation to prevent macro expansion collision. -#pragma push_macro("NVIC_SystemReset") -#undef NVIC_SystemReset -extern "C" void NVIC_SystemReset(void) -{ - sys_reboot(SYS_REBOOT_COLD); -} -#pragma pop_macro("NVIC_SystemReset") - -// ── HardwareSerial::write ───────────────────────────────────────────────────── -size_t HardwareSerial::write(uint8_t c) -{ - // TODO(nrf54l15 Phase 3): route through Zephyr UART / USB-CDC console - // For now use printk so we at least get something over RTT/UART0 - printk("%c", (char)c); - return 1; -} - -size_t HardwareSerial::write(const uint8_t *buf, size_t n) -{ - for (size_t i = 0; i < n; i++) - printk("%c", (char)buf[i]); - return n; -} - -// ── Print::printf ───────────────────────────────────────────────────────────── -int Print::printf(const char *fmt, ...) -{ - char buf[256]; - va_list args; - va_start(args, fmt); - int n = vsnprintf(buf, sizeof(buf), fmt, args); - va_end(args); - if (n > 0) - write((const uint8_t *)buf, (size_t)(n < (int)sizeof(buf) ? n : (int)sizeof(buf) - 1)); - return n; -} - -// ── strlcpy - BSD extension not in Zephyr newlib ──────────────────────────── -extern "C" size_t strlcpy(char *dst, const char *src, size_t size) -{ - size_t len = strlen(src); - if (size > 0) { - size_t copy = len < size - 1 ? len : size - 1; - memcpy(dst, src, copy); - dst[copy] = '\0'; - } - return len; -} - -// ── Print numeric helpers ───────────────────────────────────────────────────── -static size_t printNumber(Print &p, unsigned long n, uint8_t base) -{ - if (base == 0) - return p.write((uint8_t)n); - - char buf[8 * sizeof(long) + 1]; - char *end = buf + sizeof(buf) - 1; - *end = '\0'; - if (n == 0) { - *--end = '0'; - } else { - while (n > 0) { - unsigned long remainder = n % base; - *--end = (char)(remainder < 10 ? '0' + remainder : 'A' + remainder - 10); - n /= base; - } - } - return p.write((const uint8_t *)end, strlen(end)); -} - -static size_t printFloat(Print &p, double number, uint8_t digits) -{ - if (isnan(number)) - return p.print("nan"); - if (isinf(number)) - return p.print("inf"); - if (number > 4294967040.0 || number < -4294967040.0) - return p.print("ovf"); - - size_t n = 0; - if (number < 0.0) { - n += p.write('-'); - number = -number; - } - - // Round - double rounding = 0.5; - for (uint8_t i = 0; i < digits; i++) - rounding /= 10.0; - number += rounding; - - unsigned long int_part = (unsigned long)number; - double remainder = number - (double)int_part; - n += printNumber(p, int_part, 10); - if (digits > 0) { - n += p.write('.'); - for (uint8_t i = 0; i < digits; i++) { - remainder *= 10.0; - unsigned int d = (unsigned int)remainder; - n += p.write('0' + d); - remainder -= d; - } - } - return n; -} - -size_t Print::print(unsigned char n, int base) -{ - return printNumber(*this, n, base); -} -size_t Print::print(int n, int base) -{ - if (base == 10 && n < 0) { - size_t r = write('-'); - return r + printNumber(*this, (unsigned long)(-n), base); - } - return printNumber(*this, (unsigned long)n, base); -} -size_t Print::print(long n, int base) -{ - if (base == 10 && n < 0) { - size_t r = write('-'); - return r + printNumber(*this, (unsigned long)(-n), base); - } - return printNumber(*this, (unsigned long)n, base); -} -size_t Print::print(unsigned int n, int base) -{ - return printNumber(*this, n, base); -} -size_t Print::print(unsigned long n, int base) -{ - return printNumber(*this, n, base); -} -size_t Print::print(float n, int d) -{ - return printFloat(*this, n, d); -} -size_t Print::print(double n, int d) -{ - return printFloat(*this, n, d); -} - -// ── String::replace(String, String) ───────────────────────────────────────── -void String::replace(const String &from, const String &to) -{ - if (from.isEmpty() || !_buf) - return; - // Simple O(n²) replace - fine for typical Meshtastic string lengths - String result; - const char *p = _buf; - while (*p) { - if (strncmp(p, from.c_str(), from.length()) == 0) { - result += to; - p += from.length(); - } else { - result += *p++; - } - } - *this = result; -} - -// ═════════════════════════════════════════════════════════════════════════════ -// GPIO - Real Zephyr implementation (Phase 3) -// Pin mapping: P0.n = n (0-15), P1.n = 16+n (16-31), P2.n = 32+n (32-47) -// ═════════════════════════════════════════════════════════════════════════════ - -static const struct device *_gpio_dev_for_pin(uint32_t pin, gpio_pin_t *zpin) -{ - if (pin < 16) { - *zpin = (gpio_pin_t)pin; - return DEVICE_DT_GET(DT_NODELABEL(gpio0)); - } else if (pin < 32) { - *zpin = (gpio_pin_t)(pin - 16); - return DEVICE_DT_GET(DT_NODELABEL(gpio1)); - } else { - *zpin = (gpio_pin_t)(pin - 32); - return DEVICE_DT_GET(DT_NODELABEL(gpio2)); - } -} - -void pinMode(uint32_t pin, uint32_t mode) -{ - gpio_pin_t zpin; - const struct device *dev = _gpio_dev_for_pin(pin, &zpin); - if (!device_is_ready(dev)) - return; - - gpio_flags_t flags; - switch (mode) { - case OUTPUT: - flags = GPIO_OUTPUT_INACTIVE; - break; - case INPUT_PULLUP: - flags = GPIO_INPUT | GPIO_PULL_UP; - break; - case INPUT_PULLDOWN: - flags = GPIO_INPUT | GPIO_PULL_DOWN; - break; - default: - flags = GPIO_INPUT; - break; - } - gpio_pin_configure(dev, zpin, flags); -} - -// Bring-up diagnostics for the SX1262 wiring path. Off by default - enable by -// adding `-DNRF54L15_GPIO_DEBUG` to platformio.ini build_flags. Useful when -// validating CS/NRESET toggles after a wiring change, diagnosing a "stuck HIGH" -// BUSY before the first NRESET pulse, or tracing BUSY transitions during early -// boot. In normal operation these traces are noise (they bypass LOG level -// controls and print on every GPIO touch), so they are gated at compile time. -#ifdef NRF54L15_GPIO_DEBUG -#define GPIO_LOG_MAX 20 -static uint32_t _gpio_log_count = 0; -#endif - -void digitalWrite(uint32_t pin, uint32_t value) -{ -#ifdef NRF54L15_GPIO_DEBUG - // Before the very first NRESET pulse, snapshot BUSY state. - // If BUSY is already HIGH here, the chip never completed power-on calibration. - if (pin == 32 && value == 0) { - static bool _first_nreset = true; - if (_first_nreset) { - _first_nreset = false; - const struct device *bdev = DEVICE_DT_GET(DT_NODELABEL(gpio2)); - if (device_is_ready(bdev)) { - gpio_pin_configure(bdev, 3, GPIO_INPUT); // P2.03 = BUSY - int busy_before = gpio_pin_get(bdev, 3); - printk("[nrf54l15] BUSY before first NRESET = %d%s\n", busy_before, - busy_before ? " ← STUCK HIGH (chip damaged?)" : " ← LOW (chip OK)"); - } - } - } -#endif - - gpio_pin_t zpin; - const struct device *dev = _gpio_dev_for_pin(pin, &zpin); - if (!device_is_ready(dev)) { - // Genuine hardware/DTS misconfiguration - keep this regardless of the - // GPIO_DEBUG gate so it surfaces in production builds too. - printk("[GPIO] pin%u dev NOT READY\n", (unsigned)pin); - return; - } - gpio_pin_set(dev, zpin, (int)value); -#ifdef NRF54L15_GPIO_DEBUG - if ((pin == 37 || pin == 32) && _gpio_log_count < GPIO_LOG_MAX) { - // Read back the pin state to confirm it actually changed - int actual = gpio_pin_get(dev, zpin); - printk("[GPIO] pin%u → %u (read-back=%d)\n", (unsigned)pin, (unsigned)value, actual); - _gpio_log_count++; - } -#endif -} - -int digitalRead(uint32_t pin) -{ - gpio_pin_t zpin; - const struct device *dev = _gpio_dev_for_pin(pin, &zpin); - if (!device_is_ready(dev)) - return 0; - int v = gpio_pin_get(dev, zpin); -#ifdef NRF54L15_GPIO_DEBUG - // Log BUSY pin (35=P2.03) state changes + periodic updates for 10 seconds - if (pin == 35) { - static uint32_t busy_log_count = 0; - static int last_busy = -1; - static uint32_t first_read_ms = 0; - if (first_read_ms == 0) - first_read_ms = k_uptime_get_32(); - uint32_t elapsed_ms = k_uptime_get_32() - first_read_ms; - - // Always log state changes - if (v != last_busy) { - printk("[BUSY] %ums: state changed %d → %d\n", (unsigned)elapsed_ms, last_busy, v); - last_busy = v; - } - // Also log every 500ms for first 10 seconds so we can see timeline - if (elapsed_ms < 10000 && busy_log_count < 20 && (elapsed_ms / 500) > (busy_log_count)) { - printk("[BUSY] %ums: pin=%d (periodic)\n", (unsigned)elapsed_ms, v); - busy_log_count = (elapsed_ms / 500) + 1; - } - } -#endif - return v; -} - -// ─── attachInterrupt - supports up to NRF54L15_MAX_IRQS pins ──────────────── -#define NRF54L15_MAX_IRQS 8 - -struct _PinIrq { - struct gpio_callback cb; - voidFuncPtr user_cb; - const struct device *dev; - gpio_pin_t zpin; - bool used; -}; - -static _PinIrq _irq_table[NRF54L15_MAX_IRQS]; - -static void _gpio_irq_dispatch(const struct device *dev, struct gpio_callback *cb, uint32_t pins) -{ - _PinIrq *irq = CONTAINER_OF(cb, _PinIrq, cb); - if (irq->user_cb) - irq->user_cb(); -} - -void attachInterrupt(uint32_t pin, voidFuncPtr cb, int mode) -{ - gpio_pin_t zpin; - const struct device *dev = _gpio_dev_for_pin(pin, &zpin); - if (!device_is_ready(dev)) - return; - - // Find a free slot (or reuse existing registration for same pin) - _PinIrq *slot = nullptr; - for (int i = 0; i < NRF54L15_MAX_IRQS; i++) { - if (_irq_table[i].used && _irq_table[i].dev == dev && _irq_table[i].zpin == zpin) { - // Re-register: remove old callback first - gpio_remove_callback(dev, &_irq_table[i].cb); - slot = &_irq_table[i]; - break; - } - if (!slot && !_irq_table[i].used) - slot = &_irq_table[i]; - } - if (!slot) - return; // table full - - gpio_flags_t irq_flags; - switch (mode) { - case RISING: - irq_flags = GPIO_INT_EDGE_RISING; - break; - case FALLING: - irq_flags = GPIO_INT_EDGE_FALLING; - break; - default: - irq_flags = GPIO_INT_EDGE_BOTH; - break; - } - - slot->user_cb = cb; - slot->dev = dev; - slot->zpin = zpin; - slot->used = true; - - gpio_pin_configure(dev, zpin, GPIO_INPUT); - gpio_init_callback(&slot->cb, _gpio_irq_dispatch, BIT(zpin)); - gpio_add_callback(dev, &slot->cb); - gpio_pin_interrupt_configure(dev, zpin, irq_flags); -} - -void detachInterrupt(uint32_t pin) -{ - gpio_pin_t zpin; - const struct device *dev = _gpio_dev_for_pin(pin, &zpin); - for (int i = 0; i < NRF54L15_MAX_IRQS; i++) { - if (_irq_table[i].used && _irq_table[i].dev == dev && _irq_table[i].zpin == zpin) { - gpio_pin_interrupt_configure(dev, zpin, GPIO_INT_DISABLE); - gpio_remove_callback(dev, &_irq_table[i].cb); - _irq_table[i].used = false; - break; - } - } -} - -// ═════════════════════════════════════════════════════════════════════════════ -// SPI - Real Zephyr implementation using SPIM00 (HP domain, 3.0V) -// CS is handled by RadioLib via digitalWrite() - hardware CS not used. -// Mode 0 (CPOL=0, CPHA=0), MSB first. -// ═════════════════════════════════════════════════════════════════════════════ - -// Use SPIM00 (HP domain, 3.0V) - SPIM20 is 1.8V LP domain, incompatible with SX1262. -// Lazy-init: DEVICE_DT_GET in global scope fails when the extern symbol is -// not visible in this translation unit. Use a function-local static instead. -static const struct device *_spi00(void) -{ - static const struct device *dev = nullptr; - if (!dev) { - dev = DEVICE_DT_GET(DT_NODELABEL(spi00)); - if (!device_is_ready(dev)) { - printk("[nrf54l15] spi00 NOT READY\n"); - dev = nullptr; - } else { - printk("[nrf54l15] spi00 ready\n"); - } - } - return dev; -} - -// SPI config: Mode 0, MSB first, no hardware CS (RadioLib does it manually) -// -// SPIM00 base clock = 128 MHz (nRF54L15 default when NRF_CONFIG_CPU_FREQ_MHZ -// is not set; SystemInit() applies 128 MHz). Hardware prescaler must be EVEN -// and in [4, 126] (SPIM00_PRESCALER_DIVISOR_RANGE_MIN/MAX from MDK). -// 1 MHz → prescaler = 128 > 126 → NRFX_ERROR_INVALID_PARAM → -EIO on every -// transfer. Minimum valid frequency is 2 MHz (prescaler = 64). -static const struct spi_config _spi00_cfg = { - .frequency = 2000000U, // 2 MHz - minimum valid for SPIM00 at 128 MHz base - .operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_TRANSFER_MSB, - .slave = 0, - .cs = {}, // CS = NULL → RadioLib handles CS via GPIO -}; - -// Static DMA buffers - stack-allocated bufs on nRF54L15 may not be reachable -// by SPIM20 EasyDMA. Static placement in .bss/.data is always in Global SRAM. -// rx_byte is pre-filled with 0xAA before every transfer so we can distinguish: -// 0xAA → DMA never wrote (EasyDMA can't reach the buffer) -// 0x00 → MISO actively driven LOW (chip in reset / bus fight) -// 0xFF → MISO floating HIGH -// other → real chip response -static uint8_t _spi_tx_byte __attribute__((aligned(4))); -static uint8_t _spi_rx_byte __attribute__((aligned(4))); - -// Dump the first SPI_DUMP_N byte exchanges so we can see what MISO returns. -#define SPI_DUMP_N 30 -static uint32_t _spi_dump_count = 0; - -uint8_t SPIClass::transfer(uint8_t data) -{ - const struct device *dev = _spi00(); - if (!dev) - return 0xFF; - - _spi_tx_byte = data; - _spi_rx_byte = 0xAA; // sentinel: if DMA doesn't write, we return 0xAA - - struct spi_buf tx_buf = {.buf = &_spi_tx_byte, .len = 1}; - struct spi_buf rx_buf = {.buf = &_spi_rx_byte, .len = 1}; - struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1}; - struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1}; - - static uint32_t spi_err_count = 0; - int ret = spi_transceive(dev, &_spi00_cfg, &tx_set, &rx_set); - if (ret != 0 && spi_err_count++ < 3) - printk("[SPI] err=%d tx=0x%02x\n", ret, data); - - if (_spi_dump_count < SPI_DUMP_N) { - printk("[SPI] #%u tx=0x%02x rx=0x%02x\n", (unsigned)_spi_dump_count, data, _spi_rx_byte); - _spi_dump_count++; - } - - return _spi_rx_byte; -} - -// Static DMA-safe buffers for transfer16 - same EasyDMA reachability concern -// applies as for the byte path: stack buffers from a caller thread may sit in -// per-thread RAM regions that EasyDMA cannot reach. -static uint8_t _spi_tx16[2] __attribute__((aligned(4))); -static uint8_t _spi_rx16[2] __attribute__((aligned(4))); - -uint16_t SPIClass::transfer16(uint16_t data) -{ - const struct device *dev = _spi00(); - if (!dev) - return 0xFFFF; - - _spi_tx16[0] = (uint8_t)(data >> 8); - _spi_tx16[1] = (uint8_t)(data & 0xFF); - _spi_rx16[0] = 0xAA; - _spi_rx16[1] = 0xAA; - struct spi_buf tx_buf = {.buf = _spi_tx16, .len = 2}; - struct spi_buf rx_buf = {.buf = _spi_rx16, .len = 2}; - struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1}; - struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1}; - spi_transceive(dev, &_spi00_cfg, &tx_set, &rx_set); - return ((uint16_t)_spi_rx16[0] << 8) | _spi_rx16[1]; -} - -void SPIClass::transferBytes(const uint8_t *tx, uint8_t *rx, uint32_t count) -{ - if (!count) - return; - const struct device *dev = _spi00(); - if (!dev) - return; - // Zephyr requires non-const buf pointer; cast is safe for tx-only direction - struct spi_buf tx_buf = {.buf = const_cast(tx), .len = count}; - struct spi_buf rx_buf = {.buf = rx, .len = count}; - struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1}; - struct spi_buf_set rx_set = {.buffers = rx_buf.buf ? &rx_buf : nullptr, .count = rx_buf.buf ? 1U : 0U}; - spi_transceive(dev, &_spi00_cfg, &tx_set, rx ? &rx_set : nullptr); -} - -void SPIClass::transfer(void *buf, size_t count) -{ - if (!count || !buf) - return; - transferBytes(reinterpret_cast(buf), reinterpret_cast(buf), (uint32_t)count); -} diff --git a/src/platform/nrf54l15/nrf54l15_main.cpp b/src/platform/nrf54l15/nrf54l15_main.cpp deleted file mode 100644 index 8dc5644e2d..0000000000 --- a/src/platform/nrf54l15/nrf54l15_main.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* - * nrf54l15_main.cpp - Zephyr entry point for Meshtastic nRF54L15 port - * - * Zephyr calls main() instead of Arduino's setup()/loop(). - * This file provides the main() that bootstraps the Arduino-style - * Meshtastic application loop. - */ - -#include -#include -#include -#include - -// Forward declarations from src/main.cpp -void setup(); -void loop(); - -// ── Crash info saved to noinit RAM (survives soft reset) ───────────────────── -// Zephyr's arch_esf does not expose the faulting SP directly; we capture PSP -// at entry to the fatal handler (the exception-basic frame lives there) and -// store xPSR alongside PC/LR for context. -struct crash_info { - uint32_t magic; - uint32_t reason; - uint32_t pc; - uint32_t psp; // stack pointer captured at fault entry - uint32_t xpsr; // saved program status (flags + exception number) - uint32_t lr; - uint32_t cfsr; // Configurable Fault Status Register -}; -static struct crash_info saved_crash __attribute__((section(".noinit"))); -#define CRASH_MAGIC 0xDEADBEEF - -// Override Zephyr's weak fatal handler to save crash info, then cold-reboot so -// main() can report the saved record on the next boot. We don't rely on -// CONFIG_RESET_ON_FATAL_ERROR (default off → k_fatal_halt would spin forever) -// - we issue sys_reboot() ourselves after flushing logs. -extern "C" void k_sys_fatal_error_handler(unsigned int reason, const struct arch_esf *esf) -{ - saved_crash.magic = CRASH_MAGIC; - saved_crash.reason = reason; - // Capture the faulting thread's stack pointer before we start using the - // handler's own stack for logging. - uint32_t psp_at_entry; - __asm__ volatile("mrs %0, psp" : "=r"(psp_at_entry)); - saved_crash.psp = psp_at_entry; - if (esf) { - saved_crash.pc = esf->basic.pc; - saved_crash.xpsr = esf->basic.xpsr; - saved_crash.lr = esf->basic.lr; - } - // Read Cortex-M33 SCB CFSR - saved_crash.cfsr = *((volatile uint32_t *)0xE000ED28U); - printk("[nrf54l15] FATAL reason=%u pc=0x%08x lr=0x%08x cfsr=0x%08x\n", reason, saved_crash.pc, saved_crash.lr, - saved_crash.cfsr); - - // Walk the failing thread's stack and print any word that looks like a - // Thumb code address (0x1000 - flash end, with the Thumb-mode low bit set). - // The Cortex-M exception frame at PSP holds r0,r1,r2,r3,r12,lr,pc,xpsr - // (8 words); deeper words are the caller's saved frame, which gives a - // crude but useful poor-man's backtrace when CONFIG_DEBUG_COREDUMP is off. - // Found the BLE-init bad_alloc → abort() chain (heap exhaustion under - // CONFIG_BT_BUF_ACL_RX_SIZE=251) when the fault dump alone showed only - // abort itself. Cheap (~150 B of code) and silent until a fault. - uint32_t psp; - __asm__ volatile("mrs %0, psp" : "=r"(psp)); - printk("[nrf54l15] PSP=0x%08x - stack walk:\n", psp); - // Validate PSP before dereferencing. Real faults frequently leave PSP - // pointing at corrupted/unmapped memory, and walking it blindly triggers a - // second fault inside this handler. Restrict to nRF54L15 SRAM (256 KB at - // 0x20000000) with 4-byte alignment, and clamp the walk so we never read - // past the end of RAM. - const uintptr_t SRAM_START = 0x20000000UL; - const uintptr_t SRAM_END = 0x20040000UL; - if (psp < SRAM_START || psp >= SRAM_END || (psp & 0x3U) != 0) { - printk("[nrf54l15] PSP out of SRAM range or unaligned, skipping walk\n"); - } else { - const uint32_t *sp = (const uint32_t *)psp; - int max_words = (int)((SRAM_END - psp) / sizeof(uint32_t)); - if (max_words > 96) - max_words = 96; - for (int i = 0; i < max_words; i++) { - uint32_t v = sp[i]; - if (v >= 0x00001000 && v < 0x00080000 && (v & 1)) { - printk("[nrf54l15] sp[%d]=0x%08x (code)\n", i, v); - } - } - } - - // Give the RTT/printk backend a chance to drain before we reset, otherwise - // the crash log line above is lost and the next boot's "Prev crash" line is - // the only forensic evidence we get. - k_busy_wait(50000); // 50 ms - sys_reboot(SYS_REBOOT_COLD); - // Unreachable; k_fatal_halt as a defensive backstop in case sys_reboot - // returns (it shouldn't). - k_fatal_halt(reason); -} - -int main(void) -{ - uint32_t reset_cause = 0; - hwinfo_get_reset_cause(&reset_cause); - hwinfo_clear_reset_cause(); - printk("[nrf54l15] Reset cause: 0x%08x\n", reset_cause); - - if (saved_crash.magic == CRASH_MAGIC) { - printk("[nrf54l15] Prev crash: reason=%u pc=0x%08x lr=0x%08x psp=0x%08x xpsr=0x%08x cfsr=0x%08x\n", saved_crash.reason, - saved_crash.pc, saved_crash.lr, saved_crash.psp, saved_crash.xpsr, saved_crash.cfsr); - saved_crash.magic = 0; - } - - printk("[nrf54l15] A: main() entry\n"); - printk("[nrf54l15] B: calling setup()\n"); - setup(); - printk("[nrf54l15] C: setup() returned\n"); - while (true) { - loop(); - } - return 0; -} diff --git a/src/platform/nrf54l15/utility/bonding.h b/src/platform/nrf54l15/utility/bonding.h deleted file mode 100644 index 951ee9e699..0000000000 --- a/src/platform/nrf54l15/utility/bonding.h +++ /dev/null @@ -1,11 +0,0 @@ -// utility/bonding.h - stub for nRF54L15/Zephyr -// NodeDB.cpp includes this when ARCH_NRF52 is defined. -// Bluetooth is excluded; this stub satisfies the include chain. -#pragma once - -// BLE role constants (from Bluefruit SDK) -#define BLE_GAP_ROLE_PERIPH 0x01 -#define BLE_GAP_ROLE_CENTRAL 0x02 - -// Stub for bond_print_list() -static inline void bond_print_list(uint8_t) {} diff --git a/variants/nrf54l15/cpp_overrides/lfs_assert.h b/variants/nrf54l15/cpp_overrides/lfs_assert.h new file mode 100644 index 0000000000..8c53ba570a --- /dev/null +++ b/variants/nrf54l15/cpp_overrides/lfs_assert.h @@ -0,0 +1,18 @@ +#pragma once +// Force-included so littlefs v2 (nRF54L core) routes assertions and logs through Meshtastic +// (lfs_assert() in main-nrf52.cpp formats the filesystem on corruption). +#ifdef __cplusplus +extern "C" { +#endif +void lfs_assert(const char *reason); +void logLegacy(const char *level, const char *fmt, ...); +#ifdef __cplusplus +} +#endif +#define LFS_ASSERT(test) \ + if (!(test)) \ + lfs_assert(#test) +#define LFS_LOG_(level, fmt, ...) logLegacy(level, "lfs:%d: " fmt "%s\n", __LINE__, __VA_ARGS__) +#define LFS_DEBUG(...) LFS_LOG_("DEBUG", __VA_ARGS__, "") +#define LFS_WARN(...) LFS_LOG_("WARN", __VA_ARGS__, "") +#define LFS_ERROR(...) LFS_LOG_("ERROR", __VA_ARGS__, "") diff --git a/variants/nrf54l15/nrf54l15.ini b/variants/nrf54l15/nrf54l15.ini index a40f1baa5b..0c1fea75fb 100644 --- a/variants/nrf54l15/nrf54l15.ini +++ b/variants/nrf54l15/nrf54l15.ini @@ -1,90 +1,49 @@ [nrf54l15_base] -# renovate: datasource=git-refs depName=Seeed-Studio/platform-seeedboards packageName=https://github.com/Seeed-Studio/platform-seeedboards gitBranch=main -platform = https://github.com/Seeed-Studio/platform-seeedboards.git#1ec1287f8e4bc4067a6fd593991e36875aef989f -; Pin the Zephyr package explicitly. Seeed's platform script only maps their -; own "seeed-xiao-*" board ids to a framework-zephyr package; any other board -; -- nrf54l15dk included -- falls back to whatever platform.json declares as -; the default, which is now framework-zephyr-nrf54lm20 (Zephyr 4.4.0). Its -; west manifest pulls a CMSIS_6 whose cmsis_gcc.h calls the ACLE builtins -; __sxtb16/__sxtab16, and none of the GCC ARM toolchains PlatformIO ships -; (8.2.1/9.2.1/9.3.1) declare them in arm_acle.h. In C that is only an -; implicit-declaration warning, so the pure-C Zephyr core never notices; in -; C++ it is a hard error, and any .cpp pulling in zephyr/kernel.h hits it. -; Without the pin a fresh package cache breaks this build with nothing in the -; tree having changed. -platform_packages = - platformio/framework-zephyr-nrf54lm20@https://dl.registry.platformio.org/download/platformio/tool/framework-zephyr/3.40201.251021/framework-zephyr-3.40201.251021.tar.gz - ; Don't renovate toolchain-gccarmnoneeabi - platformio/toolchain-gccarmnoneeabi@1.90301.200702 -framework = zephyr +; Out-of-tree platform (meshtastic/platform-nordicnrf54) with the s145 SoftDevice Arduino core; +; the platform pulls the Arduino core and the DFU bootloader from their own repositories. +platform = https://github.com/meshtastic/platform-nordicnrf54.git#v0.3.1 extends = arduino_base build_type = release build_flags = + -include variants/nrf54l15/cpp_overrides/lfs_assert.h ${arduino_base.build_flags} - -Isrc/platform/nrf54l15 + -Wno-unused-variable + -Isrc/platform/nrf52 + ; The nRF54L port rides on the nRF52 platform layer; ARCH_NRF54L marks the few divergent spots. + -DARCH_NRF54L + -DPRIVATE_HW + -DLOOP_STACK_SZ=2048 + -DCFG_BLE_TASK_STACKSIZE=2048 + -DNRF_USE_SERIAL_DFU -DMESHTASTIC_EXCLUDE_AUDIO=1 - -DMESHTASTIC_EXCLUDE_GPS=1 - -DMESHTASTIC_EXCLUDE_MQTT=1 - -DHAS_WIRE=1 - -DHAS_SENSOR=1 - -DHAS_BUTTON=0 - -DHAS_TELEMETRY=1 -DMESHTASTIC_EXCLUDE_PAXCOUNTER=1 - -DARDUINO=100 - -DMESHTASTIC_EXCLUDE_ACCELEROMETER=1 - -DMAX_NUM_NODES=40 - -fpermissive - # Libraries that Zephyr LDF misses; add include paths explicitly - -I.pio/libdeps/${PIOENV}/Crypto - -I.pio/libdeps/${PIOENV}/ArduinoThread - -I".pio/libdeps/${PIOENV}/ESP8266 and ESP32 OLED driver for SSD1306 displays/src" - -I.pio/libdeps/${PIOENV}/OneButton/src - -I.pio/libdeps/${PIOENV}/arduino-fsm - -I.pio/libdeps/${PIOENV}/TinyGPSPlus/src - -I.pio/libdeps/${PIOENV}/ErriezCRC32/src - -I.pio/libdeps/${PIOENV}/NonBlockingRTTTL/src - -I.pio/libdeps/${PIOENV}/RadioLib/src + -DMESHTASTIC_EXCLUDE_PKT_HISTORY_HASH=1 + -DOLEDDISPLAY_REDUCE_MEMORY + -Os + -std=gnu++17 +build_unflags = + -Ofast + -Og + -ggdb3 + -ggdb2 + -g3 + -g2 + -g + -g1 + -g0 + -std=c++11 + -std=gnu++11 build_src_filter = - ${arduino_base.build_src_filter} - - - - - - - - - - - - - - - - - - - - - + - -lib_compat_mode = off + ${arduino_base.build_src_filter} + - - - - - - - - - - lib_deps = ${arduino_base.lib_deps} ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=meshtastic/Crypto packageName=https://github.com/meshtastic/Crypto gitBranch=main https://github.com/meshtastic/Crypto/archive/591ff9a690e8168ccb7a36abde8d7783e448d395.zip - ; Cherry-picked sensor libs from environmental_base. The full - ; environmental_base pulls Adafruit_SSD1306 / GFX which need Arduino - ; pin macros (digitalPinToPort / portOutputRegister) that the Zephyr - ; Arduino shim does not implement. - https://github.com/adafruit/Adafruit_BusIO/archive/refs/tags/1.17.4.zip - https://github.com/adafruit/Adafruit_Sensor/archive/refs/tags/1.1.15.zip - https://github.com/adafruit/Adafruit_BMP280_Library/archive/refs/tags/3.0.0.zip - https://github.com/adafruit/Adafruit_BME280_Library/archive/refs/tags/2.3.0.zip - https://github.com/adafruit/Adafruit_INA260/archive/refs/tags/1.5.3.zip - https://github.com/adafruit/Adafruit_INA219/archive/refs/tags/1.2.3.zip - https://github.com/RobTillaart/INA3221_RT/archive/refs/tags/0.4.2.zip - https://github.com/RobTillaart/INA226/archive/refs/tags/0.6.6.zip - ; SHTXXSensor gates on __has_include(), a header shipped by - ; Sensirion/arduino-sht. Adafruit_SHT4X ships Adafruit_SHT4X.h instead and - ; has no consumer in src/, so it left the SHT40 driver out of the build. - https://github.com/Sensirion/arduino-sht/archive/refs/tags/v1.2.6.zip lib_ignore = BluetoothOTA lvgl - Adafruit_nRFCrypto diff --git a/variants/nrf54l15/nrf54l15dk/README.md b/variants/nrf54l15/nrf54l15dk/README.md deleted file mode 100644 index 053fddff45..0000000000 --- a/variants/nrf54l15/nrf54l15dk/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# nRF54L15-DK - EBYTE E22-900M30S Wiring Guide - -Board: **Nordic nRF54L15-DK (PCA10156)** -Radio: **EBYTE E22-900M30S** (SX1262, 30 dBm, 868/915 MHz) - ---- - -## Why P2 (HP domain) and not P1 - -The nRF54L15 splits its GPIOs across three supply domains: - -- **P0** - Main domain, **3.0 V** - usable -- **P1** - LP domain, **1.8 V** - **not compatible** with the SX1262 -- **P2** - HP domain, **3.0 V** - usable - -The SX1262 requires VIH ≥ 0.7 × VDD (≈ 2.31 V at VDD = 3.3 V). P1's 1.8 V output -leaves the chip stuck in reset with `BUSY` never going LOW. All E22 signals -therefore live on **P2** and are driven by **SPIM00**. - -> `P2.09` is normally wired to LED0 on the DK; we ignore the LED and use -> SPIM00's default MISO pin. The on-board MX25R64 NOR flash also sat on SPIM00 -> -> - it is deleted in the device-tree overlay to free the bus. - ---- - -## Connections - J2 header, P2 bank - -| E22-900M30S | GPIO | DK pin | Function | -| ----------- | ----- | ------ | ---------------------------------------------- | -| MISO | P2.04 | 36 | SPIM00 data in | -| NSS / CS | P2.05 | 37 | SPI chip-select (driven by RadioLib as a GPIO) | -| DIO1 | P2.06 | 38 | IRQ - modem interrupt (routed via gpiote30) | -| BUSY | P2.03 | 35 | Module busy (GPIO input) | -| NRESET | P2.00 | 32 | Module reset (GPIO output, active LOW) | -| RXEN | P2.07 | 39 | LNA enable - held HIGH via `SX126X_ANT_SW` | -| MOSI | P2.02 | 34 | SPIM00 data out | -| SCK | P2.01 | 33 | SPIM00 clock | -| GND | - | GND | Common ground | -| VCC | - | VDD | 3.3 V | - -> **Numbering convention**: `P0.n = n`, `P1.n = 16+n`, `P2.n = 32+n`. -> Example: `P2.04` → 32 + 4 = **36**. - ---- - -## DIO2 → TXEN bridge (required) - -The E22-900M30S does **not** connect DIO2 to TXEN internally. A physical bridge on the module is required: - -1. Locate the `DIO2` and `TXEN` pads on the underside of the E22 module. -2. Solder a wire bridge or a 0 Ω resistor between the two pads. -3. With this bridge, the SX1262 drives the PA automatically via `SX126X_DIO2_AS_RF_SWITCH`. - -Without this bridge the module **will not transmit** (PA is never enabled). - ---- - -## RXEN - LNA always on - -`RXEN` (P2.07) is held HIGH permanently via `SX126X_ANT_SW 39` in `variant.h`. -**Do not use** `SX126X_RXEN` - RadioLib would drive it LOW in IDLE state and -the LNA would stay disabled (radio deaf in RX). - ---- - -## Reserved DK pins - do not reuse - -| Pins | Reserved function | -| ----------- | -------------------------------------------------------- | -| P0.00-P0.03 | IMCU debug UART (uart30, J-Link VCOM - used by RTT host) | -| P0.04 | BTN3 | -| P1.00-P1.01 | 32 kHz crystal | -| P1.02-P1.03 | NFC antenna | -| P1.10 | LED1 (status LED - kept) | -| P1.13 | BTN0 (only remaining user button) | -| P1.14 | LED3 | -| P2.01-P2.05 | SPIM00 / E22 (see connection table above) | -| P2.08-P2.10 | Trace ETM / LED2 (avoid) | - ---- - -## Build and flash - -```bash -# Build -pio run -e nrf54l15dk - -# Flash (requires a J-Link connected via the DK's on-board IMCU) -pio run -e nrf54l15dk -t upload - -# Monitor RTT (channel 1 = Meshtastic logs) -JLinkRTTLogger -device nRF54L15_xxAA -if SWD -speed 4000 -RTTChannel 1 boot.log -``` - -Expected boot log: - -```text -*** Booting Zephyr OS build zephyr-v40201 *** -[nrf54l15] Reset cause: ... -[nrf54l15] B: calling setup() -INFO | ... SX1262 -INFO | ... lora.begin() = 0 ← RADIOLIB_ERR_NONE -[nrf54l15] C: setup() returned -``` - -If you see `Record critical error 3` (`NO_RADIO`), check: DIO2→TXEN bridge, -supply voltages (the E22 must see 3.0-3.3 V on P2, not 1.8 V), and SPI wiring -continuity. diff --git a/variants/nrf54l15/nrf54l15dk/platformio.ini b/variants/nrf54l15/nrf54l15dk/platformio.ini index 8f570f7448..52e25525be 100644 --- a/variants/nrf54l15/nrf54l15dk/platformio.ini +++ b/variants/nrf54l15/nrf54l15dk/platformio.ini @@ -11,14 +11,16 @@ custom_meshtastic_display_name = Nordic nRF54L15-DK extends = nrf54l15_base board = nrf54l15dk board_level = extra -debug_tool = jlink upload_protocol = jlink -board_runner_args_jlink = --device nRF54L15_xxAA --speed 4000 +debug_tool = jlink build_flags = ${nrf54l15_base.build_flags} -Ivariants/nrf54l15/nrf54l15dk -DNRF54L15_DK - -DMESHTASTIC_EXCLUDE_FILES_MANIFEST=1 + -DMESHTASTIC_EXCLUDE_GPS=1 + -DHAS_GPS=0 + -DHAS_SCREEN=0 + -DMESHTASTIC_EXCLUDE_SCREEN=1 build_src_filter = ${nrf54l15_base.build_src_filter} +<../variants/nrf54l15/nrf54l15dk> diff --git a/variants/nrf54l15/nrf54l15dk/variant.cpp b/variants/nrf54l15/nrf54l15dk/variant.cpp index b2982e7c55..2e82825ee0 100644 --- a/variants/nrf54l15/nrf54l15dk/variant.cpp +++ b/variants/nrf54l15/nrf54l15dk/variant.cpp @@ -1,9 +1,19 @@ #include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +// Arduino pin index == physical GPIO number: P0.n = n, P1.n = 32 + n, P2.n = 64 + n +const uint32_t g_ADigitalPinMap[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95}; void initVariant() { - // Minimal board init for nRF54L15-DK. - // GPIO/SPI peripheral setup is handled by the Zephyr device tree overlay - // (zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay). - // Add any board-level power sequencing here if needed. + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); } diff --git a/variants/nrf54l15/nrf54l15dk/variant.h b/variants/nrf54l15/nrf54l15dk/variant.h index b8417b68b7..7d263cfd46 100644 --- a/variants/nrf54l15/nrf54l15dk/variant.h +++ b/variants/nrf54l15/nrf54l15dk/variant.h @@ -1,86 +1,88 @@ #pragma once /* - * Nordic nRF54L15-DK (PCA10156) - Meshtastic variant + * Nordic nRF54L15-DK (PCA10156) with an EBYTE E22-900M30S (SX1262) on the J2 header. * - * ── GPIO voltage domains ───────────────────────────────────────────────────── - * P0 (gpio0 @ 0x10A000) Main domain 3.0 V ← usable - * P1 (gpio1 @ 0xd8200 ) LP domain 1.8 V ← NOT compatible with E22 - * P2 (gpio2 @ 0x50400 ) HP domain 3.0 V ← usable + * This header shadows the framework's variants/nrf54l15dk/variant.h, so it carries the core + * pin table definitions as well. Arduino pin = physical GPIO: P0.n = n, P1.n = 32+n, P2.n = 64+n. * - * The SX1262 needs VIH ≥ 0.7 × VDD = 2.31 V (VDD = 3.3 V). - * P1 outputs only 1.8 V → chip stays in reset, BUSY never goes LOW. - * All E22 signals are therefore on P2 (3.0 V), driven by SPIM00. + * GPIO supply domains: P0 3.0 V, P1 1.8 V (too low for the SX1262), P2 3.0 V. + * Serial peripherals are port bound: SERIAL00 (UARTE00/SPIM00) -> P2, SERIAL2x -> P1, SERIAL30 -> P0. * - * EBYTE E22-900M30S (SX1262) wiring - J2 header, all P2: - * - * E22 pin GPIO pin# Notes - * ───────────────────────────────────────────────────────────────────── - * MISO → P2.04 36 SPIM00 data in - * NSS/CS → P2.05 37 SPI chip-select (RadioLib GPIO) - * DIO1 → P2.06 38 IRQ - interrupt via gpiote30 - * BUSY → P2.03 35 GPIO input - * NRESET → P2.00 32 GPIO output - * RXEN → P2.07 39 Held HIGH via ANT_SW (LNA always active) - * MOSI → P2.02 34 SPIM00 data out - * SCK → P2.01 33 SPIM00 clock - * - * DIO2 → TXEN bridge required on E22 module (solder bridge / wire). - * DIO3 drives TCXO reference (1.8 V). - * - * Pin numbering convention: P0.n = n, P1.n = 16+n, P2.n = 32+n. - * - * Reserved / do-not-use DK pins: - * P0.00-P0.02 IMCU VCOM TX/RX/RTS pads (uart30 disabled; pads idle) - * P0.03 I2C SDA (TWIM30) - sensor bus - * P0.04 I2C SCL (TWIM30) - sensor bus; SW3 button on this pad, - * DO NOT press SW3 while I2C is active - * P1.00-P1.01 32 kHz crystal - * P1.02-P1.03 NFC antenna - * P1.10 LED1 (status LED - keep) - * P1.13 BTN0 - main user button - * P1.14 LED3 - * P2.01-P2.05 SPIM00 / E22 (see above) - * P2.08-P2.10 Trace pins (avoid) + * E22 wiring (all P2, SPIM00): + * SCK P2.01, MOSI P2.02, BUSY P2.03, MISO P2.04, NSS P2.05, DIO1 P2.06, RXEN P2.07, NRESET P2.00 + * DIO2 -> TXEN bridge on the module, DIO3 drives the TCXO (1.8 V). */ -#ifndef NRF54L15_DK -#define NRF54L15_DK +#define VARIANT_MCK (128000000ul) +#define USE_LFXO + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { #endif -// ── SX1262 / E22-900M30S - all P2, HP domain (3.0 V) ──────────────────────── -#define USE_SX1262 -#define SX126X_CS 37 // P2.05 - chip-select -#define SX126X_DIO1 38 // P2.06 - IRQ (gpiote30 capable) -#define SX126X_BUSY 35 // P2.03 - BUSY -#define SX126X_RESET 32 // P2.00 - NRESET +#define PINS_COUNT (96) +#define NUM_DIGITAL_PINS (96) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) +#define ADC_RESOLUTION 14 -// RXEN (P2.07) held HIGH permanently - LNA always active. -// RadioLib must NOT toggle it; ANT_SW drives it HIGH before lora.begin(). -#define SX126X_ANT_SW 39 // P2.07 - RXEN driven HIGH at init +// LEDs (active low): LED1 P1.10 status, LED0 P2.09 +#define PIN_LED1 42 +#define PIN_LED2 73 +#define LED_BUILTIN PIN_LED1 +#define LED_STATE_ON 0 -// DIO2 controls TXEN via bridge on E22 module. -// DIO3 provides 1.8 V TCXO reference. -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8f - -// ── LEDs (active HIGH) ─────────────────────────────────────────────────────── -#define PIN_LED1 26 // P1.10 - LED1 (status LED, LP domain - output only, OK) -#define PIN_LED2 41 // P2.09 - LED0 on DK (remapped; P2.07 now used for RXEN) -#define LED_STATE_ON 1 - -// ── Buttons (active LOW, internal pull-up) ─────────────────────────────────── -// BTN1 (P1.09), BTN2 (P1.08) and BTN3 (P0.04) deleted from DTS - only BTN0 -// remains. BTN3's pad (P0.04) is now I2C SCL. -#define PIN_BUTTON1 29 // P1.13 - BTN0 +// BTN0 P1.13 (active low) +#define PIN_BUTTON1 45 #define BUTTON_NEED_PULLUP -// ── I2C bus (TWIM30, HP domain, 3.0 V) ────────────────────────────────────── -// SDA=P0.03, SCL=P0.04. Pinctrl + clock-frequency live in the board overlay. -// External 4.7 kΩ pull-ups required on both lines. Meshtastic's Arduino -// TwoWire layer (src/platform/nrf54l15/Wire.cpp) resolves the device at -// compile time via DT_NODELABEL(i2c30); these PIN_WIRE_* defines are kept -// for parity with the Arduino convention used by other variants. -#define PIN_WIRE_SDA 3 // P0.03 -#define PIN_WIRE_SCL 4 // P0.04 +// Serial1: VCOM0 of the on-board J-Link (UARTE20): TX P1.04, RX P1.05 +#define PIN_SERIAL1_RX 37 +#define PIN_SERIAL1_TX 36 +#define SERIAL1_UARTE NRF_UARTE20 +#define SERIAL1_IRQN SERIAL20_IRQn +#define SERIAL1_IRQ_HANDLER SERIAL20_IRQHandler + +// Serial2 (UARTE21, serial module): RX P1.15, TX P1.16; only P1 pins can be assigned to it +#define PIN_SERIAL2_RX 47 +#define PIN_SERIAL2_TX 48 +#define SERIAL2_UARTE NRF_UARTE21 +#define SERIAL2_IRQN SERIAL21_IRQn +#define SERIAL2_IRQ_HANDLER SERIAL21_IRQHandler + +// SPI (SPIM00) for the E22 +#define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO 68 +#define PIN_SPI_MOSI 66 +#define PIN_SPI_SCK 65 +static const uint8_t SS = 69; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +// I2C (TWIM30): SDA P0.03, SCL P0.04, external 4.7k pull-ups required #define WIRE_INTERFACES_COUNT 1 +#define PIN_WIRE_SDA 3 +#define PIN_WIRE_SCL 4 +#define WIRE_TWIM NRF_TWIM30 +#define WIRE_TWIS NRF_TWIS30 +#define WIRE_IRQN SERIAL30_IRQn +#define WIRE_IRQ_HANDLER SERIAL30_IRQHandler + +#ifdef __cplusplus +} +#endif + +// SX1262 / E22-900M30S +#define USE_SX1262 +#define SX126X_CS 69 +#define SX126X_DIO1 70 +#define SX126X_BUSY 67 +#define SX126X_RESET 64 +// RXEN is held high permanently (LNA always on); TXEN follows DIO2. +#define SX126X_ANT_SW 71 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8f diff --git a/variants/nrf54l15/xiao_nrf54l15/platformio.ini b/variants/nrf54l15/xiao_nrf54l15/platformio.ini new file mode 100644 index 0000000000..4397d23e5c --- /dev/null +++ b/variants/nrf54l15/xiao_nrf54l15/platformio.ini @@ -0,0 +1,34 @@ +[env:xiao_nrf54l15] +# PRIVATE_HW (255) until the protobuf HardwareModel enum gets an entry for this board. +custom_meshtastic_hw_model = 255 +custom_meshtastic_hw_model_slug = XIAO_NRF54L15 +custom_meshtastic_architecture = nrf54l15 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Seeed XIAO nRF54L15 + +extends = nrf54l15_base +board = xiao_nrf54l15 +board_level = extra + +build_flags = ${nrf54l15_base.build_flags} + -Ivariants/nrf54l15/xiao_nrf54l15 + -DXIAO_NRF54L15 + -DMESHTASTIC_EXCLUDE_GPS=1 + -DHAS_GPS=0 + +build_src_filter = ${nrf54l15_base.build_src_filter} + +<../variants/nrf54l15/xiao_nrf54l15> + +[env:xiao_nrf54l15_lr2021] +# Seeed LoRa Plus expansion board: Wio-LR2021, SSD1306 OLED and Grove I2C on D4/D5, K1 on D14. +custom_meshtastic_hw_model = 255 +custom_meshtastic_hw_model_slug = XIAO_NRF54L15_LR2021 +custom_meshtastic_architecture = nrf54l15 +custom_meshtastic_actively_supported = false +custom_meshtastic_support_level = 3 +custom_meshtastic_display_name = Seeed XIAO nRF54L15 LoRa Plus + +extends = env:xiao_nrf54l15 +build_flags = ${env:xiao_nrf54l15.build_flags} + -DXIAO_NRF54L15_LR2021 diff --git a/variants/nrf54l15/xiao_nrf54l15/variant.cpp b/variants/nrf54l15/xiao_nrf54l15/variant.cpp new file mode 100644 index 0000000000..a1387cd7e9 --- /dev/null +++ b/variants/nrf54l15/xiao_nrf54l15/variant.cpp @@ -0,0 +1,22 @@ +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0..D5 (P1 header pins) + 36, 37, 38, 39, 42, 43, + // D6..D10 (P2 header pins): TX, RX, SCK, MISO, MOSI + 72, 71, 65, 68, 66, + // 11..15: P0.03, P0.04, P2.10, P2.09, P2.06 + 3, 4, 74, 73, 70, + // 16 LED P2.00, 17 button P0.00, 18 SAMD11 RX (nRF TX) P1.09, 19 SAMD11 TX (nRF RX) P1.08 + 64, 0, 41, 40, + // 20 IMU/mic power P0.01, 21 RF switch power P2.03, 22 RF path select P2.05, 23 VBAT divider enable P1.15 + 1, 67, 69, 47}; + +void initVariant() +{ + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); +} diff --git a/variants/nrf54l15/xiao_nrf54l15/variant.h b/variants/nrf54l15/xiao_nrf54l15/variant.h new file mode 100644 index 0000000000..dae47b2dbd --- /dev/null +++ b/variants/nrf54l15/xiao_nrf54l15/variant.h @@ -0,0 +1,140 @@ +#pragma once + +/* + * Seeed XIAO nRF54L15 with a Wio-SX1262 for XIAO (SKU 113010003), or with XIAO_NRF54L15_LR2021 the + * Wio-LR2021 on the LoRa Plus expansion board (SKU 100039980, SSD1306 OLED and Grove I2C on D4/D5). + * The two modules share D1..D3 with different roles, so they are separate build environments. + * + * This header shadows the framework's variants/xiao_nrf54l15/variant.h, so it carries the core + * pin table definitions as well. Arduino pins 0..10 are the XIAO header D0..D10, 11..23 are + * internal signals (see variant.cpp for the physical GPIO of each index). + */ + +#define VARIANT_MCK (128000000ul) +#define USE_LFXO + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define PINS_COUNT (24) +#define NUM_DIGITAL_PINS (24) +#define NUM_ANALOG_INPUTS (8) +#define NUM_ANALOG_OUTPUTS (0) +#define ADC_RESOLUTION 14 + +#define D0 (0ul) +#define D1 (1ul) +#define D2 (2ul) +#define D3 (3ul) +#define D4 (4ul) +#define D5 (5ul) +#define D6 (6ul) +#define D7 (7ul) +#define D8 (8ul) +#define D9 (9ul) +#define D10 (10ul) + +#define PIN_A0 D0 +#define PIN_A1 D1 +#define PIN_A2 D2 +#define PIN_A3 D3 +#define PIN_A4 D4 +#define PIN_A5 D5 +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; + +// User LED P2.00 (active low) +#define PIN_LED1 16 +#define LED_BUILTIN PIN_LED1 +#define LED_STATE_ON 0 + +// User button P0.00 (active low) +#define PIN_BUTTON1 17 +#define BUTTON_NEED_PULLUP +#ifdef XIAO_NRF54L15_LR2021 +// K1 on the LoRa Plus expansion board, D14 (P2.09) +#define PIN_BUTTON2 14 +#endif + +// Serial1: the SAMD11 USB-CDC bridge (UARTE20): nRF TX P1.09, nRF RX P1.08 +#define PIN_SERIAL1_TX 18 +#define PIN_SERIAL1_RX 19 +#define SERIAL1_UARTE NRF_UARTE20 +#define SERIAL1_IRQN SERIAL20_IRQn +#define SERIAL1_IRQ_HANDLER SERIAL20_IRQHandler + +// Serial2: header D6 (TX) / D7 (RX) on UARTE21 +#define PIN_SERIAL2_TX D6 +#define PIN_SERIAL2_RX D7 +#define SERIAL2_UARTE NRF_UARTE21 +#define SERIAL2_IRQN SERIAL21_IRQn +#define SERIAL2_IRQ_HANDLER SERIAL21_IRQHandler + +// SPI (SPIM00): D8 SCK, D9 MISO, D10 MOSI +#define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO D9 +#define PIN_SPI_MOSI D10 +#define PIN_SPI_SCK D8 +#ifdef XIAO_NRF54L15_LR2021 +static const uint8_t SS = D3; +#else +static const uint8_t SS = D4; +#endif +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +#ifdef XIAO_NRF54L15_LR2021 +// Wire (TWIM22): header I2C on D4 (SDA, P1.10) / D5 (SCL, P1.11) +#define PIN_WIRE_SDA D4 +#define PIN_WIRE_SCL D5 +#define WIRE_TWIM NRF_TWIM22 +#define WIRE_TWIS NRF_TWIS22 +#define WIRE_IRQN SERIAL22_IRQn +#define WIRE_IRQ_HANDLER SERIAL22_IRQHandler +#else +// Wire (TWIM30): the Sense variant's internal sensor bus, SDA P0.04, SCL P0.03; D4/D5 belong to the radio +#define PIN_WIRE_SDA 12 +#define PIN_WIRE_SCL 11 +#define WIRE_TWIM NRF_TWIM30 +#define WIRE_TWIS NRF_TWIS30 +#define WIRE_IRQN SERIAL30_IRQn +#define WIRE_IRQ_HANDLER SERIAL30_IRQHandler +#endif +#define WIRE_INTERFACES_COUNT 1 + +#ifdef __cplusplus +} +#endif + +#ifdef XIAO_NRF54L15_LR2021 +// Wio-LR2021: NSS D3, IRQ on DIO8 D0, NRESET D2, BUSY D1, switchless RF, 32 MHz crystal (no TCXO), +// DIO7/DIO11 reach D6/D7 through 470R and stay unused +#define USE_LR2021 +#define LR2021_SPI_NSS_PIN D3 +#define LR2021_IRQ_PIN D0 +#define LR2021_NRESET_PIN D2 +#define LR2021_BUSY_PIN D1 +#define LR2021_SPI_SCK_PIN PIN_SPI_SCK +#define LR2021_SPI_MOSI_PIN PIN_SPI_MOSI +#define LR2021_SPI_MISO_PIN PIN_SPI_MISO +#define IRQ_DIO_NUM 8 +#else +// Wio-SX1262 for XIAO +#define USE_SX1262 +#define SX126X_CS D4 +#define SX126X_DIO1 D1 +#define SX126X_BUSY D3 +#define SX126X_RESET D2 +#define SX126X_RXEN D5 +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif diff --git a/zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay b/zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay deleted file mode 100644 index 4c861312c1..0000000000 --- a/zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay +++ /dev/null @@ -1,117 +0,0 @@ -#include - -/* - * Zephyr device tree overlay — Nordic nRF54L15-DK + EBYTE E22-900M30S (SX1262) - * - * P1 GPIO bank runs at 1.8V VDDIO (LP domain) — NOT compatible with SX1262 - * which needs VIH ≥ 2.31V (0.7 × 3.3V). All E22 signals therefore use P2, - * which is in the HP domain (3.0V VDDIO), via SPIM00. - * - * SPIM00 (HP domain, 3.0V) replaces SPIM20 (LP domain, 1.8V). - * Default pinctrl from cpuapp_common.dtsi already maps SPIM00 to P2 pins: - * MISO = P2.04, MOSI = P2.02, SCK = P2.01 - * - * The on-board MX25R64 NOR flash was attached to SPIM00; it is not used by - * Meshtastic (LittleFS lives in internal RRAM), so we delete its DTS node to - * free the bus and P2.05 (its CS pin) for our use. - * - * Physical wiring (all P2, HP domain): - * E22 MISO → P2.04 (SPIM00 MISO) - * E22 NSS → P2.05 (freed from MX25R64 CS, RadioLib GPIO) - * E22 DIO1 → P2.06 (IRQ — interrupt capable via gpiote30) - * E22 BUSY → P2.03 (GPIO input) - * E22 RST → P2.00 (GPIO output) - * E22 RXEN → P2.07 (held HIGH via ANT_SW; replaces LED2 on DK) - * E22 MOSI → P2.02 (SPIM00 MOSI) - * E22 SCK → P2.01 (SPIM00 SCK) - * - * Pin numbering convention: P0.n = n, P1.n = 16+n, P2.n = 32+n. - */ - -/* Disable uart20 — no longer needed for LoRa (P1 domain abandoned). */ -&uart20 { - status = "disabled"; -}; - -/* Disable uart30 to free P0.00–P0.03 from the uart30_default pinctrl (which - * also reserves P0.03 as UART30 CTS). The peripheral instance 30 is shared - * with i2c30/spi30 — only one of {UARTE30, TWIM30, SPIM30} can be enabled at - * a time. We pick TWIM30 (i2c30) for sensors (SHT40 / INA3221 / SE050 on the - * custom PCB; BMP280 / INA228 on the DK for bring-up). Console stays on RTT - * via CONFIG_RTT_CONSOLE=y. */ -&uart30 { - status = "disabled"; -}; - -/* I2C bus via TWIM30 (HP domain, 3.0 V), SDA=P0.03 / SCL=P0.04. - * P0.03 was the UART30 CTS; freed by disabling uart30 above. - * P0.04 was button SW3 on the DK; deleted below. The pad still routes to - * the SW3 button on the board — DO NOT press SW3 during I2C use, it will - * short SCL to GND mid-transaction. - * 400 kHz is the highest standard rate supported by all three target sensors - * (BMP280, INA228, SE050). External 4.7 kΩ pull-ups required on both lines. */ -&pinctrl { - i2c30_default: i2c30_default { - group1 { - psels = , - ; - bias-pull-up; - }; - }; - - i2c30_sleep: i2c30_sleep { - group1 { - psels = , - ; - low-power-enable; - }; - }; -}; - -&i2c30 { - status = "okay"; - clock-frequency = ; /* 400 kHz */ - pinctrl-0 = <&i2c30_default>; - pinctrl-1 = <&i2c30_sleep>; - pinctrl-names = "default", "sleep"; -}; - -/ { - buttons { - /* button1 (P1.09) and button2 (P1.08) no longer repurposed — - * E22 now uses P2. button3 (P0.04) is now I2C SCL — its node - * must be removed before the i2c30 pinctrl can claim the pad. */ - /delete-node/ button_1; - /delete-node/ button_2; - /delete-node/ button_3; - }; - - aliases { - /delete-property/ sw1; - /delete-property/ sw2; - /delete-property/ sw3; - }; -}; - -/* - * Override SPIM00 to remove the MX25R64 NOR flash child. - * The SPIM00 peripheral itself stays enabled (status = "okay" from - * cpuapp_common.dtsi) with its existing pinctrl (P2.01/P2.02/P2.04). - * RadioLib drives CS (P2.05) as a plain GPIO — no hardware CS needed. - */ -&spi00 { - /delete-node/ mx25r6435f@0; -}; - -/* - * Grow storage_partition from 36 KB to 700 KB by reclaiming slot1_partition. - * slot1 (image-1) is the MCUboot secondary slot for dual-bank OTA, which we - * don't use (flashing is direct via J-Link). With only 9 blocks (36 KB / 4 KB) - * LittleFS ran out of space during COW writes of config.proto. - * New layout: storage_partition spans 0xb6000..0x165000 (700 KB, ~175 blocks). - */ -/delete-node/ &slot1_partition; - -&storage_partition { - reg = <0xb6000 0xaf000>; -}; diff --git a/zephyr/prj.conf b/zephyr/prj.conf deleted file mode 100644 index 0723d0cd67..0000000000 --- a/zephyr/prj.conf +++ /dev/null @@ -1,299 +0,0 @@ -# Zephyr project configuration for nRF54L15 Meshtastic port -# -# NOTE: this prj.conf is shared by ALL Zephyr PlatformIO environments -# in this project. Keep it compatible with any future Zephyr targets. - -# ── C++ support (required by Meshtastic) ────────────────────────────────────── -CONFIG_CPP=y -CONFIG_STD_CPP17=y -# Full libstdc++ — provides , , , , etc. -# Works with either newlib or picolibc (Zephyr auto-selects based on board). -CONFIG_REQUIRES_FULL_LIBCPP=y -# Disable C++ exceptions — not needed by Meshtastic and saves RAM/ROM -CONFIG_CPP_EXCEPTIONS=n - -# ── Peripheral subsystems ───────────────────────────────────────────────────── -CONFIG_SPI=y -CONFIG_I2C=y -CONFIG_GPIO=y - -# sys_reboot() used by BLE zombie-connection watchdog (BleDeferredThread). -# nRF54L15 SW-LL occasionally drops the BLE link without forwarding -# LE Disconnection Complete to the host; cold reboot is the only reliable -# recovery path. -CONFIG_REBOOT=y - -# ── ATT Prepare/Execute Write (LONG WRITE) ─────────────────────────────────── -# iOS CoreBluetooth automatically fragments writes >MTU-3 via ATT Prepare Write -# (opcode 0x16). Default CONFIG_BT_ATT_PREPARE_COUNT=0 makes Zephyr reject with -# "Request Not Supported" (0x06), which iOS surfaces as a write error → -# disconnect. With MTU=65 any ToRadio write >62 bytes triggers this path. -# Enabling this allocates N prep_pool buffers (each BT_ATT_BUF_SIZE = 65 bytes) -# and reassembles fragments into a single write_toradio() call on execute. -# 4 × 65 = 260 B max assembled value — enough for typical iOS NodeInfo/admin -# writes after config stream completes. -CONFIG_BT_ATT_PREPARE_COUNT=4 - -# ── Filesystem — LittleFS on storage_partition (RRAM) ─────────────────────── -# Size is set by the board overlay (the nRF54L15-DK overlay reclaims slot1 to -# expand storage_partition to ~700 KB). Capacity is reported at runtime via -# FIXED_PARTITION_SIZE(storage_partition) in InternalFileSystem::totalBytes(). -CONFIG_FLASH=y -CONFIG_FLASH_MAP=y -CONFIG_FLASH_PAGE_LAYOUT=y -CONFIG_FILE_SYSTEM=y -CONFIG_FILE_SYSTEM_LITTLEFS=y -CONFIG_FILE_SYSTEM_MKFS=y -# Disable SPI NOR flash driver — MX25R64 node deleted from DTS, SPIM00 used -# exclusively by RadioLib (SX1262). Without this, the spi_nor driver claims -# SPIM00 at boot and tries to read MX25R64 ID (gets garbage since the chip is -# not wired), producing "Device id a8 a8 a8 does not match config c2 28 17". -CONFIG_SPI_NOR=n -# Disable runtime PM — keeps SPI initialization path simple; avoids any -# interaction between PM auto-suspend/resume cycles and the SPIM00 clock -# request mechanism (CONFIG_CLOCK_CONTROL_NRF_HSFLL_GLOBAL). -CONFIG_PM_DEVICE_RUNTIME=n -# Suppress Zephyr FS subsystem's internal error/warning logs (ENOENT on -# missing files and EEXIST on duplicate mkdir are expected and handled). -CONFIG_FS_LOG_LEVEL_OFF=y - -# ── Console / logging ───────────────────────────────────────────────────────── -# Use SEGGER RTT for console — does not require COM3 (CDC UART), reads via SWD -CONFIG_UART_CONSOLE=n -CONFIG_USE_SEGGER_RTT=y -CONFIG_RTT_CONSOLE=y -CONFIG_LOG=y -CONFIG_LOG_BACKEND_RTT=y -CONFIG_LOG_DEFAULT_LEVEL=2 -# Immediate mode: log writes go directly to RTT backend without a separate thread. -# Deferred mode requires the log thread to run (lowest priority — never gets CPU -# in heavy setup()/loop() workloads), leaving the RTT buffer empty indefinitely. -CONFIG_LOG_MODE_IMMEDIATE=y -# Force RTT control block re-init on every boot — prevents stale/corrupted CB after crash -CONFIG_SEGGER_RTT_INIT_MODE_ALWAYS=y -# Use RTT channel 1 for the LOG backend, channel 0 (Terminal) for direct printk. -# Sharing channel 0 forces LOG_PRINTK=y (deferred) to avoid corruption. -CONFIG_LOG_BACKEND_RTT_BUFFER=1 -# Buffer sizes shrunk from 24576 → 4096 to free ~40 KB of BSS for newlib heap. -# At 24576 the BSS pushed _end up so far that newlib heap was only ~25 KB, -# and BUF_ACL_RX_SIZE=152 + BLE/PhoneAPI lazy init ran out of malloc space. -# 4 KB still gives several seconds of log retention before host attaches. -CONFIG_LOG_BACKEND_RTT_BUFFER_SIZE=4096 -# Overwrite oldest data if buffer fills — never stalls -CONFIG_LOG_BACKEND_RTT_MODE_BLOCK=n -CONFIG_LOG_BACKEND_RTT_MODE_OVERWRITE=y -CONFIG_LOG_BACKEND_RTT_OUTPUT_BUFFER_SIZE=256 - -# ── LFXO clock source — use RC oscillator to avoid ~2s crystal stabilization -# disrupting the GRTC timer and hanging k_sleep -CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y - -# ── Stack sizes — Meshtastic setup() is heavy (RadioLib, NodeDB, printf) ────── -# bt_enable() called from nrf54l15Setup() needs >8KB. -# Phase 7: CONFIG_BT_SETTINGS=y causes bt_set_name() → settings_save_one() → -# settings_file_save() → LittleFS I/O. The I/O chain needs ~3 KB of stack -# headroom beyond what the BT init alone requires. Increase main stack to 24KB -# and system workqueue to 8KB to cover both the cooperative-OSThread call path -# (which runs on the main thread) and any async flash work items. -CONFIG_MAIN_STACK_SIZE=24576 -CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=8192 -# Log processing thread stack — default 768 overflows when processing RTT fault dump -CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048 - -# ── Fault/exception diagnostics — identify ~2000ms crash ───────────────────── -CONFIG_FAULT_DUMP=2 -CONFIG_EXCEPTION_DEBUG=y -CONFIG_STACK_SENTINEL=y -# Thread names + extra exception info — fault dumps then identify the failing -# thread (otherwise "Current thread: 0x... (unknown)") and include r4-r11 + psp -# so the custom k_sys_fatal_error_handler can walk the stack to show the caller -# chain. Cheap (~32 B/thread for names, no perf hit) and very useful when a -# crash recurs in the field. -CONFIG_THREAD_NAME=y -CONFIG_EXTRA_EXCEPTION_INFO=y -# SEGGER RTT buffer — keep modest (4 KB) to leave RAM for newlib heap. -CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096 -# Report reset reason from previous crash -CONFIG_HWINFO=y - -# ── Bluetooth ───────────────────────────────────────────────────────────────── -# Zephyr BT host + LL SW controller (MPSL) — peripheral role only -CONFIG_BT=y -CONFIG_BT_PERIPHERAL=y -# SMP / LE Encryption enabled so the Meshtastic app pairs with a PIN before -# accessing the GATT service. -CONFIG_BT_SMP=y -# Enforce MITM so clients must complete passkey exchange — without this Just -# Works pairings complete silently without prompting the user for a PIN. -CONFIG_BT_SMP_ENFORCE_MITM=y -# Fixed-passkey path so the device (no display) can advertise a known PIN via -# bt_passkey_set() when config.bluetooth.mode == FIXED_PIN. -CONFIG_BT_FIXED_PASSKEY=y -# Allow legacy pairing as fallback. SC_PAIR_ONLY=y has been observed to cause -# some clients to abort pairing with reason 0x01 within 150 ms of the pairing -# request, before any PIN dialog appears. Accepting legacy lets the same -# clients fall through to Passkey Entry successfully. -CONFIG_BT_SMP_SC_PAIR_ONLY=n -# BT_LL_SW_SPLIT is auto-selected from DT (zephyr,bt-hci-ll-sw-split node in nRF54L15 DTS) -# Do NOT set CONFIG_BT_CTLR=y (deprecated — radio silently non-functional) -# Dynamic device name so bt_set_name() can embed the node short ID at runtime -CONFIG_BT_DEVICE_NAME_DYNAMIC=y -CONFIG_BT_DEVICE_NAME_MAX=32 -# Only need one simultaneous central connection -CONFIG_BT_MAX_CONN=1 -# BT subsystem logging — INF for connection/service diagnostics -CONFIG_BT_LOG_LEVEL_INF=y -# BT thread stacks — defaults are too small for nRF54L15 SW-LL init. -# prio_recv_thread overflows at 2048; bump all BT stacks to safe sizes. -# BT RX thread runs ALL our GATT write callbacks (rx_work_handler → hci_acl → -# bt_conn_recv → bt_l2cap_recv → bt_att_recv → write_toradio_cb → -# PhoneAPI::handleStartConfig → getFiles("/", 10) recursion + nanopb encode). -# 4096 overflows on "Client wants config" → abort() / kernel panic (reason 4). -CONFIG_BT_RX_STACK_SIZE=4096 -CONFIG_BT_HCI_TX_STACK_SIZE=1024 -# bt_long_wq runs bt_pub_key_gen (ECC P256 keygen) on this thread. -# Defaults (prio=10, stack=1400) leave it starved by Meshtastic app threads -# at boot: pub_key gen never completes, so smp_public_key() defers -# indefinitely waiting for sc_public_key, and every SC pairing attempt -# stalls right after exchanging public keys (no PIN prompt on iOS, every -# AUTHEN-gated char rejects with ATT error 0x05). -# Prio 0 = highest preemptible, ties with main; stack 4096 clears P256M -# driver frames with margin. -CONFIG_BT_LONG_WQ_PRIO=0 -CONFIG_BT_LONG_WQ_STACK_SIZE=4096 -# Use legacy advertising (bt_le_adv_start / HCI 0x2006 path). -# With CONFIG_BT_EXT_ADV=y, bt_le_adv_start() is internally translated to the -# extended HCI path with LEGACY-bit (0x2036), which produces non-connectable PDUs -# on the nRF54L15 SW-LL. With CONFIG_BT_EXT_ADV=n the host uses pure legacy HCI -# commands (0x2006/0x2008/0x200a) — the same path Nordic uses in all nRF54L15 -# NCS examples (peripheral_uart, peripheral_lbs), which is iOS-compatible and -# avoids the LE Remove Advertising Set (0x203c) controller timeout crash. -CONFIG_BT_EXT_ADV=n - -# ── Phase 7: BT bond persistence ────────────────────────────────────────────── -# CONFIG_BT_SETTINGS enables the BT host settings integration: the stack -# automatically calls settings_save_subtree("bt/keys") after pairing, and -# settings_load() on boot restores previously bonded peers. -# -# Backend: SETTINGS_FILE stores all key-value pairs in a single flat file in -# LittleFS. No new partition needed — the existing storage_partition (mounted -# at /lfs, size set by the board overlay) is used. File path: /lfs/bt_settings. -# -# Ordering guarantee: LittleFS is mounted by fsInit() BEFORE nrf54l15Setup() -# calls nrf54l15_bt_preinit(), so the file backend is always available when -# settings_load() is called after bt_enable(). -CONFIG_BT_SETTINGS=y -CONFIG_SETTINGS=y -CONFIG_SETTINGS_FILE=y -CONFIG_SETTINGS_FILE_PATH="/lfs/bt_settings" -# BT_MAX_PAIRED default is 1 — first bond (e.g. iOS) blocks every subsequent -# peer's SMP pairing request with "Unable to get keys" because there is no free -# bt_keys slot to allocate. Raise to 4 so the device can simultaneously hold -# iOS, Windows, Linux, and one spare bond. Add OVERWRITE_OLDEST so that when -# the table fills, the LRU peer is evicted instead of rejecting the new pair. -CONFIG_BT_MAX_PAIRED=4 -CONFIG_BT_KEYS_OVERWRITE_OLDEST=y -# Disable GATT database caching and Service Changed characteristic. -# CONFIG_BT_GATT_CACHING (default y with BT_SETTINGS) marks every new client as -# "not change-aware" and returns ATT_ERR_DB_OUT_OF_SYNC (0x12) on every GATT -# request until the client reads the DB-hash characteristic. The Meshtastic app -# does not implement GATT caching and silently aborts service discovery on 0x12, -# causing the connection to stall with zero GATT activity. -# CONFIG_BT_GATT_SERVICE_CHANGED (default y) adds the Generic Attribute Profile -# service; disabling it is required before BT_GATT_CACHING can be disabled. -CONFIG_BT_GATT_SERVICE_CHANGED=n -CONFIG_BT_GATT_CACHING=n -# Disable automatic PHY update (1M→2M) after connection. -# The nRF54L15 SW-LL fails the LL_PHY_REQ/RSP exchange and disconnects -# exactly 1.786s after connection — before any ATT/GATT operations. -CONFIG_BT_AUTO_PHY_UPDATE=n -# ATT/GATT/L2CAP debug logging — see exactly what happens after connection -CONFIG_BT_ATT_LOG_LEVEL_DBG=y -CONFIG_BT_GATT_LOG_LEVEL_DBG=y -CONFIG_BT_SMP_LOG_LEVEL_DBG=y -# L2CAP DBG: shows recv on fixed ATT channel — confirms whether iOS sends any data -CONFIG_BT_L2CAP_LOG_LEVEL_DBG=y -# Keep bt_conn at INF — DBG floods RTT buffer every ~150µs (tx_processor loop), -# overwriting all ATT/GATT messages before they can be read. -# Connection events (connected/disconnected) are logged at INF level. -CONFIG_BT_CONN_LOG_LEVEL_INF=y -# Keep HCI logs at INF to save RAM (log thread processing buffers, etc.). -# (Earlier DBG was used to diagnose the hci_acl → L2CAP stall — fix applied.) -CONFIG_BT_HCI_CORE_LOG_LEVEL_INF=y -CONFIG_BT_HCI_DRIVER_LOG_LEVEL_INF=y -# Fix: ACL packets reach hci_acl() but never reach bt_l2cap_recv(). -# Root cause: bt_conn_recv() calls bt_conn_tx_notify(conn, true) which submits -# tx_complete_work to k_sys_work_q and blocks on k_work_flush(). The BT rx -# workqueue (bt_workq) is stuck in k_work_flush waiting for the system -# workqueue, which is busy with LittleFS I/O / other work → dead stall until -# iOS supervision timeout fires (5s) and disconnects with reason 0x13. -# Solution: dedicate a separate workqueue for TX notify processing so it is -# independent from the system workqueue. -CONFIG_BT_CONN_TX_NOTIFY_WQ=y -# Dedicated workqueue only runs tx_notify_process() (iterates tx_complete list, -# calls short callbacks). Default 8192 is overkill and eats malloc heap needed -# by PowerFSM init → realloc() returns NULL → bus fault during FSM::add_transition. -CONFIG_BT_CONN_TX_NOTIFY_WQ_STACK_SIZE=2048 - -# ── ATT/L2CAP MTU — larger payloads for Meshtastic packets ─────────────────── -# TX side: controller sends up to L2CAP_TX_MTU bytes per ATT operation. -# RX side: server ATT MTU is min(BT_L2CAP_TX_MTU, BT_BUF_ACL_RX_SIZE - 4). -# Both set to 247 / 251 → ATT MTU = 247 in each direction, matching Zephyr's -# samples/bluetooth/mtu_update reference. This means typical iOS ToRadio -# writes (NodeInfo, channel settings, common admin packets) fit in a single -# ATT_WRITE_REQ and avoid the ATT Prepare/Execute Write path entirely. -# CONFIG_BT_ATT_PREPARE_COUNT=4 (above) still backstops oversized writes. -# -# Heap dependency: bumping BUF_ACL_RX_SIZE > default (~69) grows the BT host -# net_buf pools in BSS, which proportionally shrinks the newlib heap arena -# (MAX_HEAP_SIZE = SRAM_SIZE - (_end - SRAM_BASE), so any BSS growth steals -# from the heap directly). Empirically the lazy BLE init path -# (setBluetoothEnable → startDisabled → bt_set_name → settings_save → -# LittleFS) needs ~12 KB of newlib heap to run without bad_alloc. At -# BUF_ACL_RX_SIZE=251 with the previous 24 KB RTT buffers (LOG_BACKEND_RTT + -# SEGGER_RTT_BUFFER_SIZE_UP), the heap collapsed to ~4 KB free at -# transition time → `new char[]` in RedirectablePrint::log returned NULL → -# libstdc++ called abort() from main thread. Shrinking both RTT buffers to -# 4 KB (above) frees ~40 KB of BSS for the heap and resolves it. -# -# DLE stays off (BT_DATA_LEN_UPDATE=n below): the LLCP remote table at -# ull_llcp_remote.c:878 is guarded by #ifdef CONFIG_BT_CTLR_DATA_LENGTH, so -# the controller answers iOS's LL_LENGTH_REQ with LL_UNKNOWN_RSP and falls -# back to 27-byte LL PDUs. The host reassembles LL PDUs into L2CAP frames -# up to BT_BUF_ACL_RX_SIZE before dispatching to ATT. -CONFIG_BT_L2CAP_TX_MTU=247 -# Server ATT MTU = BUF_ACL_RX_SIZE - 4 = 247 (matches L2CAP_TX_MTU) -CONFIG_BT_BUF_ACL_RX_SIZE=251 - -# ── Fix: LL Feature Exchange collision (ROOT CAUSE of iOS GATT hang) ───────── -# On connection, Zephyr host calls bt_hci_le_read_remote_features() because -# BT_CTLR_PER_INIT_FEAT_XCHG=y makes can_initiate_feature_exchange() return -# true for peripheral role. This makes the controller send LL_PER_INIT_FEAT_XCHG -# to iOS right after connecting. -# iOS (as central) simultaneously sends LL_FEATURE_REQ to the peripheral. -# The nRF54L15 SW-LL mishandles this COLLISION: iOS waits for LL_FEATURE_RSP -# to its LL_FEATURE_REQ, never gets it, and stalls — sending zero L2CAP bytes. -# BT_CTLR_PER_INIT_FEAT_XCHG=n: host does NOT send HCI_LE_Read_Remote_Features -# as peripheral → no LL_PER_INIT_FEAT_XCHG sent → no collision → iOS feature -# exchange completes → iOS proceeds to L2CAP/ATT. -CONFIG_BT_CTLR_PER_INIT_FEAT_XCHG=n -# Fix: LL Connection Parameter Request handling. -# BT_CTLR_CONN_PARAM_REQ=n was ineffective: the LLCP remote decode table in -# ull_llcp_remote.c hardcodes PDU_DATA_LLCTRL_TYPE_CONN_PARAM_REQ → PROC_CONN_PARAM_REQ -# regardless of Kconfig. With =n the handler is compiled out → controller asserts / -# enters broken state when iOS sends LL_CONN_PARAM_REQ (which is optional from Central). -# Fix: =y so the procedure is actually handled. To avoid host/peripheral vs Central -# collision at 5 s (deferred_work → send_conn_le_param_update), disable auto-update -# below so the host never initiates HCI_LE_Connection_Update. -CONFIG_BT_CTLR_CONN_PARAM_REQ=y -# Prevent Zephyr host from initiating connection parameter update 5 s after connect. -# With CONN_PARAM_REQ=y, if iOS (Central) already issued LL_CONN_PARAM_REQ and the -# SW-LL is mid-procedure, a simultaneous host-initiated HCI_LE_Connection_Update -# creates an LL collision. Disabling the auto-update avoids the collision entirely. -CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=n -# Disable optional LL procedures (belt-and-suspenders while debugging): -# BT_PHY_UPDATE=n + BT_CTLR_PHY_2M=n: no PHY update procedure → iOS doesn't attempt LL_PHY_REQ -# BT_DATA_LEN_UPDATE=n: no DLE → controller sends LL_UNKNOWN_RSP to LL_LENGTH_REQ -CONFIG_BT_CTLR_PHY_2M=n -CONFIG_BT_PHY_UPDATE=n -CONFIG_BT_DATA_LEN_UPDATE=n