Skip to content

feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)#10193

Open
cvaldess wants to merge 6 commits intomeshtastic:developfrom
cvaldess:feature/nrf54l15-port
Open

feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)#10193
cvaldess wants to merge 6 commits intomeshtastic:developfrom
cvaldess:feature/nrf54l15-port

Conversation

@cvaldess
Copy link
Copy Markdown

@cvaldess cvaldess commented Apr 17, 2026

Summary

  • Adds community hardware variant for the Nordic nRF54L15-DK (PCA10156) with an external EBYTE E22-900M30S (SX1262, 30 dBm, 868/915 MHz) LoRa module.
  • First Meshtastic port running on the Zephyr RTOS — all other Nordic targets use the nRF5 SoftDevice Arduino stack. The new code lives under src/platform/nrf54l15/ and does not touch the existing src/platform/nrf52/ tree.
  • Bluetooth LE peripheral implemented directly on the Zephyr BT host stack (NRF54L15Bluetooth.*): Meshtastic GATT service, legacy connectable advertising, just-works pairing, MTU exchange up to 247, iOS-friendly connection parameters.
  • LittleFS-backed InternalFileSystem on SPIM20 for config/NodeDB persistence.
  • Build env: nrf54l15dk. Preset region is EU_868 at 869.5875 MHz / SFNarrow (easy to override via user prefs).

Hardware

Component Model Notes
MCU board Nordic nRF54L15-DK (PCA10156) Zephyr, Cortex-M33
LoRa EBYTE E22-900M30S SX1262 + 30 dBm PA, 868/915 MHz

E22 pins on the DK's J2 header (all E22 pins sit in the P2 HP-domain, 3.0 V; P1 at 1.8 V is below the SX1262 VIH threshold) — full table + reserved-pin map in variants/nrf54l15/nrf54l15dk/README.md.

Note: The E22-900M30S does not wire DIO2 to TXEN internally. A solder/wire bridge between DIO2 and TXEN on the module is required; without it the module will not transmit. SX126X_DIO2_AS_RF_SWITCH then lets the SX1262 drive the PA automatically.

Files changed

  • src/platform/nrf54l15/ — new platform layer (Arduino shims over Zephyr, SPI/Wire/Stream, InternalFileSystem, NRF54L15Bluetooth, main entry point).
  • variants/nrf54l15/nrf54l15dk/ — variant config (PlatformIO env, DT overlay, pin map, wiring README).
  • zephyr/prj.conf + zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay — Zephyr project + board-level config (BT, GPIO, SPI, RTT logging, heap/stack budgets).
  • boards/nrf54l15dk.json — PlatformIO board definition (the Seeed platform only ships the XIAO variants).
  • extra_scripts/nrf54l15_linker.py — post-script that parses build.ninja and runs the Zephyr two-pass linker-script generation directly, working around a PlatformIO + old-Ninja issue where the second pass never fires on its own.
  • variants/rp2350/rp2350.ini — exclude platform/nrf54l15/ from the RP2350 build_src_filter.
  • Shared source (src/main.*, src/FSCommon.*, src/RedirectablePrint.cpp, src/mesh/{Channels,NodeDB,RadioLibInterface,MeshService,PhoneAPI}.cpp, src/mesh/RadioLibInterface.h, src/modules/AdminModule.cpp) — small guards/helpers so the Zephyr build compiles alongside the Arduino targets. Behavior on existing boards is unchanged.
  • .gitignore — add flash.jlink and rtt_*.txt (nRF J-Link / RTT debug artifacts).

Hardware model

HW_VENDOR maps to meshtastic_HardwareModel_PRIVATE_HW until a dedicated protobuf enum value is assigned upstream. The variant declares custom_meshtastic_hw_model = 132 so the enum can be wired through the protobufs repo after merge.

Notes on scope

  • No changes to existing board variants or to the nRF52 platform tree.
  • Shared-source changes are minimal and wrapped in #ifdef guards so other targets are unaffected; easy to review.
  • The iOS app previously required a specific PREPARE_COUNT / MTU configuration on the Zephyr side — that's included here.
  • UART0 is held at reset polarity by the DK's interface MCU, so Meshtastic logs go out over SEGGER RTT (channel 1 in immediate mode) rather than USB serial.

Test plan

  • Firmware builds cleanly (pio run -e nrf54l15dk) against current develop.
  • iOS companion app pairs + connects over BLE, full config round-trip, ATT MTU negotiates to 247.
  • LoRa TX/RX end-to-end with a canonical T-Beam on the same Meshtastic channel at 868 MHz.
  • NodeDB updates propagate both directions; traceroute completes.
  • Reset causes + boot logs stream over RTT channel 1.
  • Any additional hardware / CI validation requested during review.

🤖 Generated with Claude Code

@github-actions github-actions Bot added needs-review Needs human review hardware-support Hardware related: new devices or modules, problems specific to hardware labels Apr 17, 2026
@cvaldess
Copy link
Copy Markdown
Author

Heads-up for reviewers — one Zephyr framework patch was applied locally and is intentionally not in this PR.

On my bench the BLE host's TX processor (tx_processortx_work) gets starved on sys_work_q during an active connection: the kick TX log fires but tx_processor: TX process start never runs until the connection ends. The ATT_EXCHANGE_MTU_RSP therefore never goes out, iOS hits its 5 s supervision timeout, and we disconnect with reason 0x13.

The workaround is a one-line change in framework-zephyr/subsys/bluetooth/host/hci_core.c inside bt_tx_irq_raise():

#if defined(CONFIG_BT_RECV_WORKQ_BT)
    k_work_submit_to_queue(&bt_workq, &tx_work);
#else
    k_work_submit(&tx_work);
#endif

i.e. submit tx_work to the dedicated BT workqueue instead of sys_work_q. With this patch BLE is rock-solid (iOS pairing + full config stream + ATT MTU=247 + mesh TX/RX validated E2E).

I'm deliberately not shipping this as a framework fork in the PR — it belongs upstream in Zephyr / nrfxlib, not in the Meshtastic repo. If CI (or another reviewer's hardware) can't reproduce stable BLE, this is almost certainly why. Happy to file the upstream Zephyr issue if useful; flagging it here so it's not a surprise.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Zephyr-based Meshtastic platform/variant for the Nordic nRF54L15-DK, including a BLE peripheral implementation, LittleFS-backed persistence, and build-system wiring to integrate the new target without impacting existing Arduino-based platforms.

Changes:

  • Introduces src/platform/nrf54l15/ Zephyr “Arduino shim” layer plus a Zephyr BT-host GATT peripheral (NRF54L15Bluetooth).
  • Adds the nRF54L15-DK variant + PlatformIO environment/board definition and Zephyr Kconfig/DTS overlay configuration.
  • Makes small cross-platform guards/adjustments in shared code to compile and run with the new Zephyr target.

Reviewed changes

Copilot reviewed 44 out of 45 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
zephyr/prj.conf Adds Zephyr Kconfig enabling C++17, RTT logging, LittleFS, and detailed BLE host/controller tuning for iOS compatibility.
zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay Board-level DTS overlay to disable unused peripherals and free SPI resources for the SX1262 wiring.
variants/rp2350/rp2350.ini Excludes the new platform/nrf54l15/ source tree from RP2350 builds.
variants/nrf54l15/nrf54l15dk/variant.h Defines the nRF54L15-DK pin map and radio configuration macros for the E22 (SX1262) module.
variants/nrf54l15/nrf54l15dk/variant.cpp Adds the variant init hook (currently minimal/no-op).
variants/nrf54l15/nrf54l15dk/platformio.ini Adds the nrf54l15dk PlatformIO environment configuration.
variants/nrf54l15/nrf54l15dk/nrf54l15dk.overlay Provides an additional DTS overlay under variants/ (currently conflicting with the board overlay).
variants/nrf54l15/nrf54l15dk/README.md Documents wiring, required DIO2→TXEN bridge, build/flash, and RTT monitoring steps.
variants/nrf54l15/nrf54l15.ini Adds the nRF54L15 Zephyr base PlatformIO configuration, build flags, and src filters.
src/platform/nrf54l15/utility/bonding.h Adds a stub header to satisfy bonding-related include chains when building under Zephyr.
src/platform/nrf54l15/nrf54l15_main.cpp Adds the Zephyr main() entry point with crash capture/logging plumbing.
src/platform/nrf54l15/nrf54l15_arduino.cpp Implements Arduino API shims (timing/GPIO/SPI/String/Print/etc.) atop Zephyr primitives.
src/platform/nrf54l15/main-nrf54l15.cpp Adds nRF54L15 platform hooks for setup/loop, power, BLE enable/disable, and deep sleep stubs.
src/platform/nrf54l15/bluefruit.h Adds a Bluefruit SDK stub to satisfy nRF52-specific include paths.
src/platform/nrf54l15/architecture.h Defines ARCH_NRF54L15 and feature flags / HW_VENDOR mapping for the new platform.
src/platform/nrf54l15/Wire.h Adds an Arduino TwoWire stub (compile-only at present).
src/platform/nrf54l15/WProgram.h Adds legacy Arduino header shim redirecting to Arduino.h.
src/platform/nrf54l15/Tone.h Adds a tone/noTone shim header redirecting to Arduino.h.
src/platform/nrf54l15/Stream.h Adds a Stream shim header redirecting to Arduino.h.
src/platform/nrf54l15/SPI.h Adds an Arduino SPI shim header for Zephyr-backed SPI transfers.
src/platform/nrf54l15/Print.h Adds a Print shim header redirecting to Arduino.h.
src/platform/nrf54l15/Nrf52SaadcLock.h Adds a stub to satisfy nRF52 SAADC lock include chains.
src/platform/nrf54l15/NRF54L15Bluetooth.h Declares the Zephyr BLE backend implementing the project’s Bluetooth API interface.
src/platform/nrf54l15/NRF54L15Bluetooth.cpp Implements the Meshtastic BLE GATT service/peripheral using Zephyr BT host APIs + watchdog logic.
src/platform/nrf54l15/NRF52Bluetooth.h Adds a stub NRF52Bluetooth header for Zephyr builds.
src/platform/nrf54l15/InternalFileSystem.h Declares Zephyr LittleFS-backed InternalFileSystem compatible with existing FS abstractions.
src/platform/nrf54l15/InternalFileSystem.cpp Implements mount/open/read/write/dir traversal and recursive delete using Zephyr FS APIs.
src/platform/nrf54l15/IPAddress.h Adds a stub IPAddress type for code paths expecting Arduino networking types.
src/platform/nrf54l15/Arduino.h Adds the core Arduino compatibility layer used throughout Meshtastic and 3rd-party Arduino libs.
src/modules/AdminModule.cpp Adds guards/helpers around GPIO output config handling and nRF54L15 BLE status plumbing.
src/mesh/RadioLibInterface.h Clears the static instance on destruction; adds a TX_DONE missed-IRQ polling hook declaration.
src/mesh/RadioLibInterface.cpp Adds missed TX_DONE IRQ polling to recover from dropped IRQ events.
src/mesh/PhoneAPI.cpp Skips filesystem manifest enumeration on NRF54L15-DK due to an FS recursion abort on this target.
src/mesh/NodeDB.cpp Adds nRF54L15 device-id derivation; adds additional LoRa USERPREFS overrides and “always-apply” logic after load.
src/mesh/MeshService.cpp Avoids abort() on to-phone queue enqueue failures by releasing to pool and returning.
src/mesh/Channels.cpp Applies compile-time LoRa USERPREFS overrides when building default LoRa config.
src/main.h Adds nRF54L15 Bluetooth externs and updates platform hook declarations (including a new rp2040Loop declaration).
src/main.cpp Adds nRF54L15 setup/loop calls and adds an RP2040 loop hook call; includes InputBroker conditionally.
src/RedirectablePrint.cpp Adds BLE-log routing via nrf54l15Bluetooth when building for ARCH_NRF54L15.
src/FSCommon.h Wires FSCommon to use the new Zephyr InternalFS implementation on ARCH_NRF54L15.
src/FSCommon.cpp Enables recursive LittleFS directory deletion for ARCH_NRF54L15.
platformio.ini Registers a post-build script to work around Zephyr’s two-pass link script generation for nRF54L15.
extra_scripts/nrf54l15_linker.py Implements the post-build linker.cmd generation workaround by parsing build.ninja and invoking gcc -E.
boards/nrf54l15dk.json Adds a PlatformIO board definition for the nRF54L15-DK.
.gitignore Ignores J-Link/RTT debug artifacts.

Comment thread variants/nrf54l15/nrf54l15dk/README.md Outdated
Comment thread src/main.cpp Outdated
Comment thread zephyr/prj.conf
Comment thread src/platform/nrf54l15/nrf54l15_main.cpp
Comment thread src/platform/nrf54l15/nrf54l15_main.cpp
Comment thread variants/nrf54l15/nrf54l15dk/nrf54l15dk.overlay Outdated
Comment thread src/platform/nrf54l15/nrf54l15_arduino.cpp
Comment thread variants/nrf54l15/nrf54l15dk/README.md Outdated
Comment thread variants/nrf54l15/nrf54l15dk/variant.h
Comment thread src/platform/nrf54l15/SPI.h Outdated
cvaldess added a commit to cvaldess/protobufs that referenced this pull request Apr 17, 2026
Reserves enum value 132 for the Nordic nRF54L15-DK community firmware
port. The port is tracked in meshtastic/firmware#10193 and currently
uses HardwareModel_PRIVATE_HW as a placeholder until this PR merges
and the regenerated protobufs propagate back into the firmware tree.

Board: Nordic nRF54L15-DK (PCA10156), Zephyr RTOS, external EBYTE
E22-900M30S (SX1262) LoRa module. Firmware variant: nrf54l15dk.
@cvaldess
Copy link
Copy Markdown
Author

Companion PR to reserve the HardwareModel enum value has been opened: meshtastic/protobufs#896 (NRF54L15_DK = 132). Once that merges and the regenerated protobufs propagate back into src/mesh/generated/, HW_VENDOR in src/platform/nrf54l15/architecture.h can flip from PRIVATE_HW to NRF54L15_DK in a small follow-up.

@cvaldess cvaldess force-pushed the feature/nrf54l15-port branch from 0712a36 to 4b64236 Compare April 17, 2026 23:26
@cvaldess
Copy link
Copy Markdown
Author

Force-pushed 4b64236 with CI fixes:

  • Build failures (60 jobs)platform/nrf54l15/ was getting picked up by every other platform's build_src_filter. Added -<platform/nrf54l15/> exclusion to esp32-common.ini, nrf52.ini, rp2040.ini, stm32.ini, and portduino.ini. (rp2350.ini was already done.)
  • Trunk clang-format / black / markdownlint / prettier — ran the configured formatters on every flagged file.
  • bandit B602 / semgrep subprocess-shell-true — in extra_scripts/nrf54l15_linker.py, the gcc_cmd comes verbatim from our own build.ninja and contains Windows-style paths with spaces that can't be safely argv-split (shlex.split mangles them, build breaks). Kept shell=True with inline # nosec B602 + # nosemgrep suppressions and a comment explaining why.

check-label failure — a maintainer will need to apply one of the required labels (hardware-support or enhancement look right). External contributors can't set labels themselves.

@cvaldess cvaldess force-pushed the feature/nrf54l15-port branch from 4b64236 to b337f86 Compare April 17, 2026 23:45
@cvaldess
Copy link
Copy Markdown
Author

Force-pushed b337f86 addressing all Copilot review comments. Each item below maps to one of the unresolved threads.

Real bugs:

  • src/main.cpp — removed the stray rp2040Loop() call (function isn't defined anywhere; the stub declaration in main.h was never implemented).
  • src/platform/nrf54l15/nrf54l15_arduino.cpp — added _spi00() nullptr guards in SPIClass::transfer16() and SPIClass::transferBytes() to match the existing transfer(uint8_t) behavior.
  • src/platform/nrf54l15/nrf54l15_main.cpp — renamed the misused saved_crash.sp = esf->basic.xpsr to separate psp (captured via mrs psp on handler entry) and xpsr (from the exception frame). Print block updated accordingly. The handler comment now clarifies it saves to .noinit RAM before k_fatal_halt resets the SoC.
  • src/platform/nrf54l15/NRF54L15Bluetooth.cppBluetoothPhoneAPI::onNowHasData and BleDeferredThread::runOnce now take a bt_conn_ref/unref around any notify on active_conn to close the race with disconnected_cb on another thread. The pendingToRadioBuf/pendingToRadioLen/pendingToRadio trio is now protected by K_MUTEX_DEFINE(pendingToRadioMutex), with the mutex held only for the memcpy and dropped before handleToRadio() runs.

Documentation / config inconsistencies:

  • variants/nrf54l15/nrf54l15dk/README.md — rewritten to describe the actual P2 (HP-domain) wiring through SPIM00 on the J2 header, matching variant.h and the Zephyr board overlay. The old Spanish README was from the early P1/SPIM20 prototype phase (that path failed because P1 runs at 1.8 V, below the SX1262's 2.31 V VIH).
  • variants/nrf54l15/nrf54l15dk/nrf54l15dk.overlaydeleted (stale SPIM20/P1 config from the same prototype phase; unused by the build). variant.cpp's comment now points at zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay which is the active overlay.
  • src/platform/nrf54l15/SPI.h — header comment updated (says SPIM00, references the correct overlay path).
  • zephyr/prj.conf — added CONFIG_BT_EXT_ADV=n explicitly; previously the comment block explained why legacy advertising is required on the nRF54L15 SW-LL but the value was only coming from Kconfig defaults.

Tested on hardware after push: clean boot, SX1262 init OK, iOS pairs + streams config + mesh TX/RX still working end-to-end.

@vidplace7 vidplace7 requested a review from Jorropo April 18, 2026 01:11
Comment thread src/platform/nrf54l15/architecture.h Outdated
@cvaldess cvaldess force-pushed the feature/nrf54l15-port branch from b337f86 to e1d77ce Compare April 18, 2026 10:28
@cvaldess
Copy link
Copy Markdown
Author

Force-pushed e1d77ce addressing @fifieldt's review comment on src/platform/nrf54l15/architecture.h.

Expanded the feature-flags comment block into two paragraphs covering (1) what the HAS_* macros do in practice and why defaulting them to 0 on this bare DK is deliberate, and (2) why HAS_* flags should be preferred over #ifdef ARCH_X sprinkled across shared code when absorbing platform divergence. The BLE/SoftDevice context that used to live in this block is folded into the second paragraph.

No behavior change — comment-only edit on a single commit. Amended + force-pushed to keep the PR as one squashed commit, matching the earlier rounds.

cvaldess and others added 4 commits April 20, 2026 12:43
Adds a community hardware variant for the Nordic nRF54L15-DK (PCA10156)
with an external EBYTE E22-900M30S (SX1262) LoRa module. First Meshtastic
port running on the Zephyr RTOS; all other Nordic targets use the nRF5
SoftDevice stack.

Scope
-----
- New Zephyr-based platform layer under src/platform/nrf54l15/ providing
  Arduino-compatible shims (Arduino.h, SPI, Wire, Print, Stream) over the
  Zephyr APIs plus a LittleFS-backed InternalFileSystem on SPIM20.
- Bluetooth LE peripheral (NRF54L15Bluetooth.*) built on the Zephyr BT
  host stack, exposing the Meshtastic GATT service with legacy
  connectable advertising, just-works pairing, dynamic MTU exchange
  (up to 247 bytes), and iOS connection-parameter tweaks.
- Variant directory variants/nrf54l15/nrf54l15dk/ with pin map for the
  E22 module on connector J1, PlatformIO env (nrf54l15dk), Zephyr
  DT overlay and a wiring README.
- Zephyr project config (zephyr/prj.conf + board overlay) tuned for
  BT + LoRa: 16 KB main stack, 4 KB BT RX thread, RTT logging in
  immediate mode, newlib-nano heap sized to leave room for the GATT
  pools while still allowing ATT MTU=247.
- extra_scripts/nrf54l15_linker.py works around a PlatformIO + old Ninja
  issue where Zephyr's two-pass linker script generation does not run
  automatically; the post-script parses build.ninja and invokes the
  gcc -E step directly before the final link.
- boards/nrf54l15dk.json board definition (PlatformIO needs it for the
  DK; the Seeed platform only ships the XIAO variants).
- variants/rp2350/rp2350.ini excludes platform/nrf54l15/ from RP2350
  build_src_filter so the shared platform tree does not leak between
  targets.
- .gitignore: add nRF J-Link / RTT debug artifacts (flash.jlink,
  rtt_*.txt).

Shared source changes
---------------------
- src/main.{cpp,h}, src/RedirectablePrint.cpp, src/FSCommon.{cpp,h},
  src/mesh/{Channels,NodeDB,RadioLibInterface,MeshService,PhoneAPI}.cpp,
  src/mesh/RadioLibInterface.h, src/modules/AdminModule.cpp: add small
  guards / helpers so the Zephyr build compiles alongside the Arduino
  targets. Behavior on existing boards is unchanged.

Hardware model
--------------
HW_VENDOR maps to meshtastic_HardwareModel_PRIVATE_HW until a dedicated
protobuf enum value is assigned upstream. The variant declares
custom_meshtastic_hw_model = 132 so the maintainers can wire the new
enum value through the protobufs repo after merge.

Hardware note
-------------
The E22-900M30S does not connect its DIO2 pin to TXEN internally — a
wire/solder bridge between DIO2 and TXEN on the module is required for
TX to work. Details and full pin map are in the variant README.

Validation
----------
Built clean against develop. On real hardware (April 2026) the port
passes end-to-end: iOS companion app pairs and connects, configuration
round-trip works, LoRa TX/RX reaches a canonical tbeam on the same mesh
channel, NodeDB updates propagate both ways, and traceroute completes.
Zephyr LittleFS on nrf54l15 supports fs_rename natively, so route it
through the same atomic path as ESP32. The previous copyFile+remove
fallback truncated the destination before copying, leaving 0-byte files
if interrupted mid-write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LittleFS on the default 9-block (36KB) storage_partition ran out of
space during copy-on-write of config.proto, causing fs_write to return
ENOSPC and pb_encode to surface "io error" when saving configuration
via the mobile app.

Reclaim slot1_partition (the MCUboot secondary slot — unused since we
flash directly via J-Link) and grow storage_partition to span
0xb6000..0x165000 (~175 blocks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NodeDB rewrites LoRa config from USERPREFS_LORACONFIG_* on every boot,
which prevented reconfiguration via the BLE/serial app. Drop the
variant-level defaults; users configure region and modem preset through
the app like every other variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cvaldess cvaldess force-pushed the feature/nrf54l15-port branch from 1bdda74 to f5210fa Compare April 20, 2026 10:43
- Add MESH_PERM_READ/MESH_PERM_WRITE macros (READ_AUTHEN/WRITE_AUTHEN)
  on all mesh service characteristics so clients must complete passkey
  exchange before accessing fromNum/fromRadio/toRadio/logRadio.
- Wire FIXED_PIN mode to bt_passkey_set() so the device advertises a
  known PIN (config.bluetooth.fixed_pin); RANDOM_PIN keeps default
  per-pairing random passkey.
- Reduce BleDeferredThread HARD_WATCHDOG_MS from 3min to 1min.
- prj.conf: CONFIG_BT_SMP_ENFORCE_MITM=y, CONFIG_BT_FIXED_PASSKEY=y,
  CONFIG_BT_SMP_SC_PAIR_ONLY=n (legacy fallback for clients that abort
  SC pairing with reason 0x01 within 150ms).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hardware-support Hardware related: new devices or modules, problems specific to hardware needs-review Needs human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants