Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ data/boot/logo.*
managed_components/*
arduino-lib-builder*
dependencies.lock

# JLink / RTT debug artifacts (nRF SoCs)
flash.jlink
rtt_*.txt
idf_component.yml
CMakeLists.txt
/sdkconfig.*
Expand Down
34 changes: 34 additions & 0 deletions boards/nrf54l15dk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"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"
}
137 changes: 137 additions & 0 deletions extra_scripts/nrf54l15_linker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# 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("env")
import os
import re
import subprocess

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 looks like:
build zephyr/linker.cmd | ...: CUSTOM_COMMAND ...
COMMAND = cmd.exe /C "cd /D ZEPHYR_DIR && arm-none-eabi-gcc.exe ... -o linker.cmd && cmake.exe -E cmake_transform_depfile ..."
DESC = Generating linker.cmd

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 = ") :]

# The value is: C:\Windows\system32\cmd.exe /C "cd /D DIR && GCC_CMD && cmake ..."
# Extract the content between the outermost double-quotes.
m = re.search(r'/C\s+"(.*)"', command_val)
if not m:
raise RuntimeError(
"nRF54L15 linker fix: unexpected COMMAND format in build.ninja:\n%s"
% command_val[:200]
)
Comment thread
cvaldess marked this conversation as resolved.
Outdated

inner = m.group(1) # "cd /D DIR && GCC_CMD && cmake ..."
parts = inner.split(" && ")

cwd = None
gcc_cmd = None
for part in parts:
part = part.strip()
if part.startswith("cd /D "):
cwd = part[len("cd /D ") :]
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)
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ test_build_src = true
extra_scripts =
pre:bin/platformio-pre.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!
Expand Down
11 changes: 7 additions & 4 deletions src/FSCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,13 @@ bool renameFile(const char *pathFrom, const char *pathTo)
{
#ifdef FSCom

#ifdef ARCH_ESP32
#if defined(ARCH_ESP32) || defined(ARCH_NRF54L15)
// take SPI Lock
spiLock->lock();
// rename was fixed for ESP32 IDF LittleFS in April
// ESP32 IDF LittleFS (fixed April) and Zephyr LittleFS (nrf54l15) both
// support atomic fs_rename. Using it avoids the copyFile fallback which
// truncates the destination before copying — any interruption leaves a
// 0-byte file.
bool result = FSCom.rename(pathFrom, pathTo);
spiLock->unlock();
return result;
Expand Down Expand Up @@ -271,8 +274,8 @@ void rmDir(const char *dirname)

#if (defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO))
listDir(dirname, 10, true);
#elif defined(ARCH_NRF52)
// nRF52 implementation of LittleFS has a recursive delete function
#elif defined(ARCH_NRF52) || defined(ARCH_NRF54L15)
// LittleFS rmdir_r for nRF52 and nRF54L15
FSCom.rmdir_r(dirname);
#endif

Expand Down
8 changes: 8 additions & 0 deletions src/FSCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ 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

void fsInit();
void fsListFiles();
bool copyFile(const char *from, const char *to);
Expand Down
4 changes: 4 additions & 0 deletions src/RedirectablePrint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ 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;
Expand All @@ -241,6 +243,8 @@ 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
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#if !MESHTASTIC_EXCLUDE_GPS
#include "GPS.h"
#endif
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
#include "input/InputBroker.h"
#endif
#include "MeshRadio.h"
#include "MeshService.h"
#include "NodeDB.h"
Expand Down Expand Up @@ -59,6 +62,12 @@ NimbleBluetooth *nimbleBluetooth = nullptr;
NRF52Bluetooth *nrf52Bluetooth = nullptr;
#endif

#ifdef ARCH_NRF54L15
void nrf54l15Setup();
void nrf54l15Loop();
NRF54L15Bluetooth *nrf54l15Bluetooth = nullptr;
#endif

#if HAS_WIFI || defined(USE_WS5500)
#include "mesh/api/WiFiServerAPI.h"
#include "mesh/wifi/WiFiAPClient.h"
Expand Down Expand Up @@ -696,6 +705,9 @@ void setup()
#ifdef ARCH_NRF52
nrf52Setup();
#endif
#ifdef ARCH_NRF54L15
nrf54l15Setup();
#endif

#ifdef ARCH_RP2040
rp2040Setup();
Expand Down Expand Up @@ -1119,6 +1131,9 @@ void loop()
#endif
#ifdef ARCH_NRF52
nrf52Loop();
#endif
#ifdef ARCH_NRF54L15
nrf54l15Loop();
#endif
power->powerCommandsCheck();

Expand Down
6 changes: 5 additions & 1 deletion src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ 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
Expand Down Expand Up @@ -91,7 +95,7 @@ extern bool runASAP;

extern bool pauseBluetoothLogging;

void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), clearBonds(), enterDfuMode();
void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), rp2040Loop(), clearBonds(), enterDfuMode();

meshtastic_DeviceMetadata getDeviceMetadata();
#if !MESHTASTIC_EXCLUDE_I2C
Expand Down
17 changes: 17 additions & 0 deletions src/mesh/Channels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ void Channels::initDefaultLoraConfig()
#ifdef USERPREFS_LORACONFIG_CHANNEL_NUM
loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM;
#endif

// Apply any hardcoded USERPREFS overrides for custom modem config (e.g. region-locked boards)
#ifdef USERPREFS_LORACONFIG_USE_PRESET
loraConfig.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
#endif
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
loraConfig.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
#endif
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
loraConfig.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
#endif
#ifdef USERPREFS_LORACONFIG_CODING_RATE
loraConfig.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
#endif
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
loraConfig.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
#endif
}

bool Channels::ensureLicensedOperation()
Expand Down
10 changes: 7 additions & 3 deletions src/mesh/MeshService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,9 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p)

if (toPhoneQueue.enqueue(p, 0) == false) {
LOG_CRIT("Failed to queue a packet into toPhoneQueue!");
abort();
releaseToPool(p);
fromNum++; // notify observers so phone can resync
return;
}
fromNum++;
}
Expand All @@ -350,7 +352,8 @@ void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage

if (toPhoneMqttProxyQueue.enqueue(m, 0) == false) {
LOG_CRIT("Failed to queue a packet into toPhoneMqttProxyQueue!");
abort();
releaseMqttClientProxyMessageToPool(m);
return;
}
fromNum++;
}
Expand Down Expand Up @@ -382,7 +385,8 @@ void MeshService::sendClientNotification(meshtastic_ClientNotification *n)

if (toPhoneClientNotificationQueue.enqueue(n, 0) == false) {
LOG_CRIT("Failed to queue a notification into toPhoneClientNotificationQueue!");
abort();
releaseClientNotificationToPool(n);
return;
}
fromNum++;
}
Expand Down
Loading
Loading