semtech lr2021 evaluation kit port

This commit is contained in:
liquidraver
2026-09-12 14:54:03 +02:00
parent c969b2fca3
commit a1b5cfc724
16 changed files with 1052 additions and 94 deletions
+1
View File
@@ -43,6 +43,7 @@ Linux_boards=(
nRF54L_boards=(
me25ls02/nrf54l15/cpuapp
xiao_nrf54l15/nrf54l15/cpuapp
seeed_lr2021_evk/nrf54l15/cpuapp
)
# SWD-only ARM platforms. Neither SoC has a USB device peripheral, so there is
+5
View File
@@ -226,6 +226,11 @@ BOARDS = [
# USB cable is enough (openocd/pyocd) -- still not a browser flash flow.
dict(stem="xiao_nrf54l15-nrf54l15-cpuapp", kind="nrf54l",
device="Seeed Studio Xiao nRF54L15", new=True, img="lora.svg"),
# Same XIAO nRF54L15 host and the same CMSIS-DAP story, but a different kit:
# the Semtech LoRa Plus expansion board with a Wio-LR2021 module. Separate
# tile because the radio and the whole D0..D10 wiring differ.
dict(stem="seeed_lr2021_evk-nrf54l15-cpuapp", kind="nrf54l",
device="Semtech LR2021 LoRa Plus EVK", new=True, img="lora.svg"),
# --- SWD-only ARM boards (download-only tiles): new tiles -------------
# Same shape as the nRF54L entries: no USB device peripheral means no
+136 -93
View File
@@ -1,93 +1,136 @@
# ZephCore 1.17.5-zephcore
> [!NOTE]
> **Draft — release in progress.** Covers what is on `dev` so far.
---
## Bluetooth range on ESP32 boards was 9 dB down
Every ESP32 board has been advertising and connecting at 0 dBm; stock MeshCore runs the same radios
at +9 dBm. Zephyr's driver builds its default from the `CONFIG_BT_CTLR_TX_PWR_*` chain, and with none
of them set it fell through to the 0 dB arm — silently, with nothing visible at build time or on the
node. ESP32 boards now transmit at +9 dBm.
> [!NOTE]
> **Nothing to change on your side.** No setting, no re-pairing. nRF52, nRF54L and MG24 were never
> affected — they set their own transmit power.
---
## GPS on Heltec WiFi LoRa 32 V4 and V4.3
Both boards bring a GPS UART and its power pins out to the header, and ZephCore described neither, so
attaching a module did nothing. They now match every other GPS board: NMEA on UART1 at 9600 baud, with
enable and reset driven rather than left floating.
> [!NOTE]
> **For an external module.** Neither board ships with a receiver; boards with nothing attached are
> unaffected.
---
## The confirmation prompt now works on single-button boards
Shutdown, DFU and off-grid confirm with two ENTER presses. On single-button boards ENTER is a
one-second hold, and the window was half a second — so the second hold always landed too late and
re-armed the prompt instead of confirming. The window is now three seconds on every board that emits
ENTER through a long-press filter. Six had it; eight did not:
| | |
|---|---|
| Heltec WiFi LoRa 32 V3 | Heltec Wireless Tracker |
| Heltec WiFi LoRa 32 V4 | Heltec Wireless Tracker V2 |
| Heltec WiFi LoRa 32 V4.3 | LilyGo T3-S3 |
| Meshnology W12 | TTGO T-Beam |
Joystick boards (Wio Tracker L1, GAT562) use a different menu and were never affected.
Thanks to **bisbille** for finding this and fixing the first two boards.
---
## Changing frequency or spreading factor now resets adaptive CAD
The learned CAD threshold is an offset from a per-SF, per-bandwidth base. Change the preset and the
base moves, but the node kept the old offset — leaving it too sensitive to transmit or too deaf to
defer. `set radio`, `set freq` and the app's radio settings now perform a full `set cad.reset` when
frequency, bandwidth or spreading factor changes. Coding rate is excluded: it changes airtime, not the
threshold. `tempradio` uses its preset's own base and hands the offset back on revert without writing
flash; `get cad` shows `a:tmp` while a window is open.
> [!NOTE]
> **Nothing to change on your side.** The reset is automatic and the node re-converges in an hour or
> two. `set cad.reset` by hand is no longer needed after a preset change.
Thanks to **Codes** for reporting it.
---
## ProMicro SX1262: Bluetooth dropped every few seconds, and the screen never worked
The nRF52840 SuperMini has no 32.768 kHz crystal, but the board was configured as though it did — so
the Bluetooth controller and the kernel tick ran off a floating oscillator while claiming 50 ppm
accuracy, and the link dropped every few seconds. It now uses the calibrated internal RC at 250 ppm,
matching stock MeshCore's settings for the same silicon. Timing that hangs off the same clock steadies
with it, including the return to receive after a transmission — so repeats of your own message, and
zero-hop ping replies, are no longer missed.
The OLED was never described for this board at all. It is now an SSD1306 on the same I2C pins stock
MeshCore uses, optional at runtime.
> [!NOTE]
> **Check transmit power if your module has no amplifier.** The 10 dBm default is the safe drive level
> for an E22-900M30S. On a bare module (HT-RA62, E22-900M22S) that is your antenna power — 12 dB under
> stock — which reads as no repeats and failed zero-hop pings. `set tx 22` once and it sticks.
Thanks to **Mike's Allotment** for the report.
---
## Also in this release
*To be filled in as further changes land.*
# ZephCore 1.17.5-zephcore
> [!NOTE]
> **Draft — release in progress.** Covers what is on `dev` so far.
---
## Bluetooth range on ESP32 boards was 9 dB down
Every ESP32 board has been advertising and connecting at 0 dBm; stock MeshCore runs the same radios
at +9 dBm. Zephyr's driver builds its default from the `CONFIG_BT_CTLR_TX_PWR_*` chain, and with none
of them set it fell through to the 0 dB arm — silently, with nothing visible at build time or on the
node. ESP32 boards now transmit at +9 dBm.
> [!NOTE]
> **Nothing to change on your side.** No setting, no re-pairing. nRF52, nRF54L and MG24 were never
> affected — they set their own transmit power.
---
## GPS on Heltec WiFi LoRa 32 V4 and V4.3
Both boards bring a GPS UART and its power pins out to the header, and ZephCore described neither, so
attaching a module did nothing. They now match every other GPS board: NMEA on UART1 at 9600 baud, with
enable and reset driven rather than left floating.
> [!NOTE]
> **For an external module.** Neither board ships with a receiver; boards with nothing attached are
> unaffected.
---
## The confirmation prompt now works on single-button boards
Shutdown, DFU and off-grid confirm with two ENTER presses. On single-button boards ENTER is a
one-second hold, and the window was half a second — so the second hold always landed too late and
re-armed the prompt instead of confirming. The window is now three seconds on every board that emits
ENTER through a long-press filter. Six had it; eight did not:
| | |
|---|---|
| Heltec WiFi LoRa 32 V3 | Heltec Wireless Tracker |
| Heltec WiFi LoRa 32 V4 | Heltec Wireless Tracker V2 |
| Heltec WiFi LoRa 32 V4.3 | LilyGo T3-S3 |
| Meshnology W12 | TTGO T-Beam |
Joystick boards (Wio Tracker L1, GAT562) use a different menu and were never affected.
Thanks to **bisbille** for finding this and fixing the first two boards.
---
## Changing frequency or spreading factor now resets adaptive CAD
The learned CAD threshold is an offset from a per-SF, per-bandwidth base. Change the preset and the
base moves, but the node kept the old offset — leaving it too sensitive to transmit or too deaf to
defer. `set radio`, `set freq` and the app's radio settings now perform a full `set cad.reset` when
frequency, bandwidth or spreading factor changes. Coding rate is excluded: it changes airtime, not the
threshold. `tempradio` uses its preset's own base and hands the offset back on revert without writing
flash; `get cad` shows `a:tmp` while a window is open.
> [!NOTE]
> **Nothing to change on your side.** The reset is automatic and the node re-converges in an hour or
> two. `set cad.reset` by hand is no longer needed after a preset change.
Thanks to **Codes** for reporting it.
---
## ProMicro SX1262: Bluetooth dropped every few seconds, and the screen never worked
The nRF52840 SuperMini has no 32.768 kHz crystal, but the board was configured as though it did — so
the Bluetooth controller and the kernel tick ran off a floating oscillator while claiming 50 ppm
accuracy, and the link dropped every few seconds. It now uses the calibrated internal RC at 250 ppm,
matching stock MeshCore's settings for the same silicon. Timing that hangs off the same clock steadies
with it, including the return to receive after a transmission — so repeats of your own message, and
zero-hop ping replies, are no longer missed.
The OLED was never described for this board at all. It is now an SSD1306 on the same I2C pins stock
MeshCore uses, optional at runtime.
That display also needed a second fix, found while bringing up the LR2021 EVK below, which had the
identical fault. On these Nordic chips the screen is redrawn in one large transfer, and the driver
needs somewhere to assemble it; the space reserved for that defaults to 16 bytes, far short of the
1025 a 128x64 screen needs. The panel answered every setup command and then never drew anything —
which looks exactly like a broken display rather than a misconfigured one. Both boards now reserve
enough.
> [!NOTE]
> **The ProMicro half of this is unconfirmed on hardware.** The fault was identified from the LR2021
> EVK's logs and the fix is the same single line, but nobody here has a ProMicro with a screen
> attached to check. If yours has an OLED, we would like to hear either way.
> [!NOTE]
> **Check transmit power if your module has no amplifier.** The 10 dBm default is the safe drive level
> for an E22-900M30S. On a bare module (HT-RA62, E22-900M22S) that is your antenna power — 12 dB under
> stock — which reads as no repeats and failed zero-hop pings. `set tx 22` once and it sticks.
Thanks to **Mike's Allotment** for the report.
---
## New board: Semtech LR2021 LoRa Plus Evaluation Kit
The **Semtech LR2021 LoRa Plus EVK** is now supported — Seeed's kit built around Semtech's
fourth-generation LoRa transceiver. It is three boards stacked: a XIAO nRF54L15 for the processor, the
LoRa Plus expansion board for the display, buttons, Grove ports and antenna sockets, and a Wio-LR2021
radio module. Companion and repeater builds are both provided, and the 128x64 OLED and the expansion
board's user button work as they do on any other screen-equipped board.
Build it with `seeed_lr2021_evk/nrf54l15/cpuapp`. It is a separate board from the plain **XIAO
nRF54L15**, which is the same processor on a Wio-SX1262 carrier — the two are wired differently and
the firmware is not interchangeable.
> [!IMPORTANT]
> **Two things on the hardware to check before first power-on.** The small two-pin **IDCC** header
> feeds power to the radio module; if its jumper is missing the radio is simply unpowered and looks
> dead. And the radio module connects to the board's SMA sockets through **U.FL pigtails you fit
> yourself** — connect the sub-GHz (LF) one before transmitting. Transmitting at full power into an
> unconnected antenna port can damage the amplifier.
> [!NOTE]
> **This board can only be flashed with SWD.** The nRF54L15 has no USB hardware, so there is no
> drag-and-drop UF2 and no update over a cable. The expansion board's USB-C socket reaches a SAMD11
> debug bridge, which is enough on its own — no separate probe needed — and the same socket carries the
> console at 115200 baud. The firmware is published as a `.hex` file, and the Mesh America configurator
> lists the board as a download rather than offering to flash it.
The kit's 2.4 GHz antenna port is not used. MeshCore is a sub-GHz protocol, so only the LF port carries
traffic.
---
## Also in this release
*To be filled in as further changes land.*
+10 -1
View File
@@ -143,11 +143,20 @@ revision.
|----------------------|------------------------------------------------------------------------|-----------------|
| XIAO nRF54L15 | `west build -b xiao_nrf54l15/nrf54l15/cpuapp zephcore --no-sysbuild` | `west flash` |
| MinewSemi ME25LS02 | `west build -b me25ls02/nrf54l15/cpuapp zephcore --no-sysbuild` | SWD (`west flash`) |
| Semtech LR2021 LoRa Plus EVK | `west build -b seeed_lr2021_evk/nrf54l15/cpuapp zephcore --no-sysbuild` | `west flash` |
Requires J-Link or CMSIS-DAP (built into XIAO board via SAMD11 bridge).
The `--no-sysbuild` flag is required (no MCUboot support yet).
The SoC has no USB peripheral at all, so neither board has a UF2 or DFU path — `zephyr.hex`
The LR2021 EVK is the Seeed/Semtech kit (SKU 100039980): a XIAO nRF54L15 plus the
LoRa Plus Expansion Board plus a Wio-LR2021 module, all in XIAO sockets sharing one
set of D0..D10 nets. It is a distinct board from `xiao_nrf54l15` above, which is the
same MCU on a Wio-SX1262 carrier with incompatible wiring. Two hardware gotchas
before first boot: the **IDCC jumper** must be fitted or the radio has no power, and
the **LF U.FL pigtail** must be connected before transmitting. The board's `.dts`
header carries the full pin map and the reasoning behind each choice.
The SoC has no USB peripheral at all, so none of these boards has a UF2 or DFU path — `zephyr.hex`
links at RRAM base 0x0 and is the complete image, written over SWD. On the ME25LS02's MX25LE02
carrier the USB-C port is a CH340x UART bridge (console only), so it needs an external probe.
@@ -174,6 +174,34 @@
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
/*
* Required by the SSD1306 below, not an optimisation.
*
* The display driver flushes with i2c_burst_write_dt(): a 1-byte control
* message plus the whole 1024-byte framebuffer, no STOP between. TWIM is
* EasyDMA and needs one contiguous buffer, so both are copied into the
* concat buffer — which defaults to 16 bytes (nordic,nrf-twim.yaml). Every
* flush then fails with "(1 + 1024 > 16)" while the panel still ACKs its
* whole init sequence, so the logs say the display initialised and the
* screen stays dark. The binding is explicit that this property "must be
* provided when interacting with devices like the SSD1306 display".
*
* 1088 = 1 + 1024, rounded. Nothing else on this bus needs more: the
* sensors in sensors-i2c.dtsi all do short register transfers.
*
* Boards on the legacy "nordic,nrf-twi" driver (rak4631, gat562_30s) never
* hit this — it is not EasyDMA. But nRF52840's i2c0 defaults to
* "nordic,nrf-twim", which this node keeps, so this board does.
* lilygo_timpulse_plus sets 257 for the same reason, sized to its 64x32.
* An SH1106 (wio_tracker_l1) writes per page instead, so 1 + width is
* enough there.
*
* Found 2026-09-12 on seeed_lr2021_evk, whose display was dark for exactly
* this reason. This board's OLED was only described in the 1.17.5 draft, and this
* fix has NOT been confirmed on hardware here — no panel to hand.
*/
zephyr,concat-buf-size = <1088>;
/* SSD1306 OLED — optional; absent panels fail device_is_ready() and are skipped */
ssd1306: ssd1306@3c {
compatible = "solomon,ssd1306";
@@ -0,0 +1,9 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: MIT
if BOARD_SEEED_LR2021_EVK_NRF54L15_CPUAPP
config HAS_BT_CTLR
default BT
endif # BOARD_SEEED_LR2021_EVK_NRF54L15_CPUAPP
@@ -0,0 +1,6 @@
# Semtech LR2021 LoRa Plus EVK board configuration
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: MIT
config BOARD_SEEED_LR2021_EVK
select SOC_NRF54L15_CPUAPP if BOARD_SEEED_LR2021_EVK_NRF54L15_CPUAPP
@@ -0,0 +1,19 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: MIT
#
# The nRF54L15 has no USB peripheral and this kit has no bootloader — the
# expansion board's USB-C goes to a SAMD11 that presents CMSIS-DAP for flashing
# and a USB CDC for the uart20 console. So openocd over CMSIS-DAP is the default
# path and is listed first; J-Link/nrfutil work through the SWD header (J7_SWD).
#
# support/openocd.cfg is Zephyr's XIAO nRF54L15 config, copied verbatim. It
# defaults the interface to cmsis-dap and carries the nRF54L CTRL-AP recovery
# procedure, which is what un-bricks a board whose APPROTECT got engaged.
board_runner_args(openocd "--cmd-load=nrf54l-load" -c "targets nrf54l.cpu")
board_runner_args(jlink "--device=nRF54L15_M33" "--speed=4000")
include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake)
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
@@ -0,0 +1,16 @@
# Semtech LR2021 LoRa Plus EVK (XIAO nRF54L15 + Wio-LR2021)
# Board-specific configuration
# Board identification
CONFIG_ZEPHCORE_BOARD_NAME="LR2021 LoRa Plus EVK"
# Device Information Service model name
CONFIG_BT_DIS_MODEL_NUMBER_STR="Seeed LR2021 LoRa Plus EVK"
# LR2021 radio driver
CONFIG_ZEPHCORE_RADIO_LR2021=y
# Same SoC and RAM budget as xiao_nrf54l15 — 450 contacts is the ceiling that
# still fits. This board additionally carries the OLED UI, so if RAM gets tight
# this is the first knob to turn.
CONFIG_ZEPHCORE_MAX_CONTACTS=450
@@ -0,0 +1,9 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: MIT
board:
name: seeed_lr2021_evk
full_name: Semtech LR2021 LoRa Plus EVK (XIAO nRF54L15 + Wio-LR2021)
vendor: seeed
socs:
- name: nrf54l15
@@ -0,0 +1,48 @@
# LR2021 LoRa Plus EVK bring-up logging.
#
# Everything needed to see why a peripheral did not come up, in one build:
# west build -b seeed_lr2021_evk/nrf54l15/cpuapp zephcore --pristine --no-sysbuild -- \
# -DEXTRA_CONF_FILE="boards/common/debug.conf;boards/nrf54l/seeed_lr2021_evk/bringup.conf"
#
# Not for daily use — this is loud and costs RAM.
#
# The console is uart20 through the expansion board's SAMD11 bridge, and that
# bridge's USB CDC stays enumerated on the host across nRF resets. So unlike the
# native-USB nRF52 boards, nothing is lost at boot: open the terminal first, then
# press RESET, and the log starts from line one. That is why this file does NOT
# carry meshtracker_x1's LOG_PROCESS_THREAD_STARTUP_DELAY_MS workaround — there
# is no enumeration race to lose the early messages to.
# LR2021: chip dumps, modem_cfg lines, decoded error words, raw SPI frames.
#
# This is the setting that answers the board's one genuinely open hardware
# question. The module schematic says 32 MHz crystal with the VTCXO pin wired as
# an NTC input, so the board declares no TCXO — but Semtech's own shield overlay
# for this kit declares a 1.8 V TCXO instead. At DBG the driver prints
# "init: TCXO disabled (XTAL mode)" when it takes our reading, and the whole
# HF_XOSC_START path is visible if it turns out we are wrong.
#
# Worth knowing which way the safety net runs: the driver's TCXO->XTAL fallback
# is gated on `tcxo_voltage_mv > 0`, so it only rescues the "declared a TCXO that
# is not there" direction — the one that wedged promicro_lr2021. This board
# declares none, so if the schematic reading were wrong the OTHER way there is no
# automatic recovery. It is not silent though: dump_chip_state() logs the chip's
# error word at LOG_INF on every init step, and HF_XOSC_START is bit 0. So
# `[post-cal] ... err=0x0001` is the tell. (Arduino MeshCore instead guesses TCXO
# first and retries at 0 V on RadioLib -706/-707 — see
# src/helpers/radiolib/CustomLR2021.h, which is where our fallback came from.)
CONFIG_LORA_LOG_LEVEL_DBG=y
# The OLED's I2C address is strapped by JP1 on the expansion board (0x3C default,
# 0x3D jumpered). A blank screen reads like a broken display driver, so make the
# bus and the display driver say which address they actually talked to.
CONFIG_I2C_LOG_LEVEL_INF=y
CONFIG_DISPLAY_LOG_LEVEL_DBG=y
# K1 on P2.09 has no external pull-up and no hardware debounce — confirm the
# presses arrive and that the longpress/multitap chain sees them.
CONFIG_INPUT_LOG_LEVEL_DBG=y
# This board's RRAM partition layout is new: app links at 0x0, LittleFS at
# 0x14E000. Show why a mount failed, not just that it did.
CONFIG_FS_LOG_LEVEL_DBG=y
@@ -0,0 +1,3 @@
# SPDX-License-Identifier: MIT
# Suppress "unique_unit_address_if_enabled" to handle some overlaps
list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled")
@@ -0,0 +1,68 @@
/*
* Semtech LR2021 LoRa Plus EVK — pin control
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: MIT
*
* Pin assignments are the XIAO nRF54L15's fixed connector mapping, copied from
* Zephyr's xiao_nrf54l15-pinctrl.dtsi. Only the three buses this kit actually
* uses are declared:
*
* uart20 P1.09 TX / P1.08 RX — console, via the SAMD11 CMSIS-DAP bridge
* i2c22 P1.10 SDA / P1.11 SCL — D4/D5: OLED + the two I2C Grove ports
* spi00 P2.01 SCK / P2.02 MOSI / P2.04 MISO — D8/D10/D9: LR2021
*
* uart21 (D6/D7) is deliberately absent — see the DIO7/DIO11 note in the .dts.
*/
&pinctrl {
/omit-if-no-ref/ uart20_default: uart20_default {
group1 {
psels = <NRF_PSEL(UART_TX, 1, 9)>;
};
group2 {
psels = <NRF_PSEL(UART_RX, 1, 8)>;
bias-pull-up;
};
};
/omit-if-no-ref/ uart20_sleep: uart20_sleep {
group1 {
psels = <NRF_PSEL(UART_TX, 1, 9)>,
<NRF_PSEL(UART_RX, 1, 8)>;
low-power-enable;
};
};
/omit-if-no-ref/ i2c22_default: i2c22_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 10)>,
<NRF_PSEL(TWIM_SCL, 1, 11)>;
};
};
/omit-if-no-ref/ i2c22_sleep: i2c22_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 10)>,
<NRF_PSEL(TWIM_SCL, 1, 11)>;
low-power-enable;
};
};
/omit-if-no-ref/ spi00_default: spi00_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 2, 1)>,
<NRF_PSEL(SPIM_MOSI, 2, 2)>,
<NRF_PSEL(SPIM_MISO, 2, 4)>;
};
};
/omit-if-no-ref/ spi00_sleep: spi00_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 2, 1)>,
<NRF_PSEL(SPIM_MOSI, 2, 2)>,
<NRF_PSEL(SPIM_MISO, 2, 4)>;
low-power-enable;
};
};
};
@@ -0,0 +1,480 @@
/*
* Semtech LR2021 LoRa Plus Evaluation Kit (Seeed SKU 100039980)
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: MIT
*
* Three boards in a XIAO-socket sandwich, not one board:
*
* Seeed XIAO nRF54L15 — the MCU
* LoRa Plus Expansion Board V2a — OLED, buttons, Grove, SMA, SWD, USB-C
* Wio-LR2021 module — LR2021WIO1XDCS, 868/915 MHz, switchless
*
* Both modules plug into the same expansion board and share ONE set of D0..D10
* nets. There is no bus mux: whatever the MCU drives on D0..D10 reaches the
* radio module directly.
*
* Verified 2026-09-12 against primary sources, all kept in
* devdocs/seeed_lr2021_evk/ — the Wio-LR2021 module schematic (e769v02a), the
* expansion board schematic (e758v03a), the XIAO nRF54L15 schematic, and Seeed's
* wiki pin table. Every control-pin assignment below is independently confirmed
* by Semtech's own devicetree for this kit (github.com/Lora-net/usp_zephyr,
* boards/shields/semtech_wio_lr20xx/), saved alongside those.
*
* XIAO connector map (Seeed wiki table / Zephyr seeed_xiao_connector.dtsi):
* D0 P1.04 LR2021 IRQ (chip DIO8) D8 P2.01 SPI SCK (spi00)
* D1 P1.05 LR2021 BUSY D9 P2.04 SPI MISO (spi00)
* D2 P1.06 LR2021 NRESET D10 P2.02 SPI MOSI (spi00)
* D3 P1.07 LR2021 NSS
* D4 P1.10 I2C SDA (i2c22) — OLED + 2x Grove
* D5 P1.11 I2C SCL (i2c22) — OLED + 2x Grove
* D6 P2.08 LR2021 DIO7 / uart21 TX / Grove UART (see the note at uart21)
* D7 P2.07 LR2021 DIO11 / uart21 RX / Grove UART
* D14 P2.09 K1 user button on the expansion board
*
* Hardware prerequisites, both easy to miss and both present as "the firmware
* does not work":
* - The IDCC 2-pin header carries SYS_3V3 to the radio module. Open jumper =
* the LR2021 has no power and probes as a dead chip. A shunt (or an ammeter)
* must be fitted.
* - The module terminates in U.FL; the SMA connectors are on the expansion
* board. Both are joined by pigtails the user fits. Do NOT transmit with the
* LF pigtail missing — +22 dBm into an open is a PA-damage risk, not just
* poor range.
*
* Flashing: the nRF54L15 has no USB peripheral, so there is no UF2 and no DFU.
* The expansion board's USB-C goes to a SAMD11 presenting CMSIS-DAP for SWD
* flashing plus a USB CDC for the uart20 console. Published artifact is
* zephyr.hex, which links at RRAM base 0x0.
*/
/dts-v1/;
#include <nordic/nrf54l15_cpuapp.dtsi>
#include "seeed_lr2021_evk-pinctrl.dtsi"
#include <zephyr/dt-bindings/input/input-event-codes.h>
/ {
model = "Semtech LR2021 LoRa Plus EVK";
compatible = "seeed,lr2021-evk";
chosen {
zephyr,sram = &cpuapp_sram;
zephyr,flash = &cpuapp_rram;
zephyr,flash-controller = &rram_controller;
zephyr,console = &uart20;
zephyr,shell-uart = &uart20;
zephyr,display = &ssd1306;
/*
* Deliberately no zephyr,code-partition: there is no bootloader
* on this kit, so the image links at RRAM base 0x0 and the ROM
* starts it directly. Same choice as me25ls02, the other
* bootloader-less nRF54L15 board here.
*/
};
aliases {
lora0 = &lora;
led0 = &user_led;
sw0 = &user_button;
};
leds {
compatible = "gpio-leds";
/* On the XIAO core board itself, not the expansion board. */
user_led: led_0 {
gpios = <&gpio2 0 GPIO_ACTIVE_HIGH>;
label = "User LED";
};
};
buttons: buttons {
compatible = "gpio-keys";
/*
* K1 on the expansion board: a bare TS-1111A between P2.09 and
* GND, no external pull-up and no hardware debounce, so the
* internal pull-up is required.
*
* The XIAO core board carries a second user button of its own on
* P0.00 (also pull-up / active low). It is deliberately left out:
* the UI is built around one button, and K1 is the one the user
* can actually reach with the boards stacked.
*/
user_button: button_0 {
gpios = <&gpio2 9 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
zephyr,code = <INPUT_KEY_0>;
label = "User Button";
};
};
/* Button chain — same mapping as RAK4631 (rak4631/board.overlay). */
user_btn_longpress {
compatible = "zephyr,input-longpress";
input = <&buttons>;
input-codes = <INPUT_KEY_0>;
short-codes = <INPUT_KEY_A>;
long-codes = <INPUT_KEY_ENTER>;
long-delay-ms = <1000>;
};
page_btn_multitap {
compatible = "zephcore,input-multi-tap";
input-codes = <INPUT_KEY_A>;
tap-codes = <INPUT_KEY_1 INPUT_KEY_LEFT>;
tap-delay-ms = <400>;
};
/*
* XIAO 2.4 GHz antenna switch. P2.05 (RF_SW_CTL) and P2.03 (RF_SW_PWR)
* gate the switch between the on-board chip antenna and the U.FL, per the
* XIAO schematic. Both are boot-on regulators exactly as upstream Zephyr
* declares them — drop either and the SoC's own BLE radio comes up deaf.
* Nothing to do with the LR2021, which has its own separate front end.
*/
rfsw_ctl: rfsw-ctl {
compatible = "regulator-fixed";
regulator-name = "rfsw-ctl";
enable-gpios = <&gpio2 5 GPIO_ACTIVE_LOW>;
regulator-boot-on;
};
rfsw_pwr: rfsw-pwr {
compatible = "regulator-fixed";
regulator-name = "rfsw-pwr";
enable-gpios = <&gpio2 3 GPIO_ACTIVE_HIGH>;
regulator-boot-on;
};
/*
* Battery sense rail enable (P1.15), gating the 2:1 divider on P1.14/AIN7.
* Declared so the rail is powered, but the ADC channel is NOT enabled —
* see the block at the bottom of this file for why.
*/
vbat_pwr: vbat-pwr {
compatible = "regulator-fixed";
regulator-name = "vbat";
enable-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
regulator-boot-on;
};
};
&cpuapp_sram {
status = "okay";
};
&grtc {
owned-channels = <0 1 2 3 4 5 6 7 8 9 10 11>;
/* Channels 7-11 reserved for Zero Latency IRQs, 3-4 for FLPR */
child-owned-channels = <3 4 7 8 9 10 11>;
status = "okay";
};
&clock {
status = "okay";
};
/* Console: P1.09 TX / P1.08 RX -> SAMD11 CMSIS-DAP bridge -> USB CDC */
&uart20 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart20_default>;
pinctrl-1 = <&uart20_sleep>;
pinctrl-names = "default", "sleep";
};
/*
* uart21 (D6/D7) stays disabled, and this is not just tidiness.
*
* A6_D6_TX and A7_D7_RX are simultaneously the nRF54L15's uart21, the Grove
* UART connector J6, SWD header pins 7/8, AND — through the Wio-LR2021
* castellation — the radio's DIO7 and DIO11. Driving them as a UART toggles two
* of the LR2021's DIOs. We configure neither DIO for anything (DIO8 is the IRQ,
* and the front end is switchless so no DIO drives an RF switch), so it is very
* likely harmless, but the coupling is real and Semtech's own sample overlay
* disables this UART too. If a Grove UART peripheral is ever wanted here, that
* needs bench verification, not reasoning.
*/
&uart21 {
status = "disabled";
};
/*
* i2c22 = D4/D5. Free for the OLED and the two I2C Grove ports because the
* Wio-LR2021 module leaves its pins 5/6 (SDA/SCL) unconnected — see the
* castellation table in devdocs/seeed_lr2021_evk/HARDWARE.md.
*
* Note this is the opposite of boards/nrf54l/xiao_nrf54l15, where the same bus
* is disabled because that carrier puts the SX1262's NSS and RXEN on D4/D5.
*/
&i2c22 {
status = "okay";
pinctrl-0 = <&i2c22_default>;
pinctrl-1 = <&i2c22_sleep>;
pinctrl-names = "default", "sleep";
clock-frequency = <400000>;
/*
* Mandatory for the SSD1306, not an optimisation.
*
* The display driver flushes the framebuffer with i2c_burst_write_dt():
* a 1-byte control message and the 1024-byte framebuffer, with no STOP
* between them. TWIM is EasyDMA and needs one contiguous buffer, so the
* driver copies both into the concat buffer — which defaults to 16 bytes
* (nordic,nrf-twim.yaml). Every flush then fails with
* "Need to use the internal driver buffer but its size is insufficient
* (1 + 1024 > 16)"
* and the panel stays dark even though it ACKed its whole init sequence.
* That is exactly what happened on the first hardware bring-up, 2026-09-12.
* The binding says as much: this property "must be provided when
* interacting with devices like the SSD1306 display".
*
* 1088 = 1 + 1024, rounded up. Semtech's shield overlay and Zephyr's own
* ssd1306 shield both use 4096, but this is a static RAM buffer and the
* companion build already sits near 88% of RAM, so it is sized to the
* largest transfer this bus can actually carry: nothing else lives on
* i2c22 but the panel and the (unpopulated) Grove ports.
*
* Boards driving an SSD1306 over "nordic,nrf-twi" — the legacy non-DMA
* driver, which is what rak4631 and gat562_30s use — do not need this.
* Boards on "nordic,nrf-twim" do, sized to their own panel:
* lilygo_timpulse_plus sets 257 for its 64x32 (1 + 256).
*/
zephyr,concat-buf-size = <1088>;
/*
* ZJY-2864KSWPG01, an SSD1306-class 128x64 panel strapped for IIC.
*
* Address: JP1 on the expansion board selects it via the panel's D/C pin
* (R24 4.7K to GND / R26 0R / R25 4.7K to VDD). 0x3C is the default
* strap and what Semtech's own overlay uses; 0x3D is the jumpered
* alternative. If the display stays blank, probe the bus before
* suspecting anything else.
*
* No reset-gpios: /RES is an RC power-on reset on the expansion board
* (R21 4.7K to VDD, C7 10uF to GND, D2 1N4148 discharge), not an MCU pin.
*
* Panel settings mirror Semtech's shield overlay for this exact kit.
*/
ssd1306: ssd1306@3c {
compatible = "solomon,ssd1306";
reg = <0x3c>;
width = <128>;
height = <64>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <63>;
segment-remap;
com-invdir;
inversion-on;
prechargep = <0x22>;
};
};
/* i2c30 (D11/D12) reaches only the Arduino headers, and the XIAO's own IMU,
* which ZephCore does not use. Left off to save the bus and its power. */
&i2c30 {
status = "disabled";
};
&spi00 {
status = "okay";
cs-gpios = <&gpio1 7 GPIO_ACTIVE_LOW>; /* D3 = P1.07 NSS */
pinctrl-0 = <&spi00_default>;
pinctrl-1 = <&spi00_sleep>;
pinctrl-names = "default", "sleep";
/*
* Nordic's 0xFF idle byte is read by the LR2021 as a bogus opcode, so the
* bus must idle at the chip's NOP. Both promicro_lr2021 and Semtech's own
* shield overlay do this; it is not optional.
*/
overrun-character = <0x00>;
/*
* SoC default rx-delay is kept. xiao_nrf54l15 — the same MCU board, same
* spi00, 8 MHz — keeps it and works; me25ls02 deletes it because
* MinewSemi's sample does. Neither has been A/B'd on hardware, so each
* board keeps its own known-good configuration rather than guessing.
*/
lora: lora@0 {
compatible = "semtech,lr2021";
reg = <0>;
/*
* 8 MHz, matching meshtracker_x1 and promicro_lr2021. Semtech's
* shield runs this module at 16 MHz — worth trying later as its
* own change, once the link is known good at 8.
*/
spi-max-frequency = <8000000>;
reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; /* D2 = P1.06 */
busy-gpios = <&gpio1 5 GPIO_ACTIVE_HIGH>; /* D1 = P1.05 */
dio1-gpios = <&gpio1 4 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>; /* D0 = P1.04 */
irq-dio = <8>; /* chip DIO8 -> module pin 1 -> D0 */
/*
* NO TCXO. The module schematic shows crystal Q1, 32 MHz, across
* XTA/XTB, and the chip's dual-function VTCXO/VNTC pin is wired
* as the NTC thermistor input (R3 + ERT-JZER104F), not as a TCXO
* supply. So tcxo-voltage-mv stays at its default of 0.
*
* Semtech's own shield overlay declares a 1.8 V TCXO here, which
* contradicts their schematic. The schematic wins. This is worth
* being firm about: declaring a TCXO on a module that has none
* WEDGED the chip on promicro_lr2021 — HF-XOSC startup hang, BUSY
* stuck, BLE never came up.
*/
rx-boosted;
/*
* Switchless dual-band front end: RFO_LF and RFO_HF each drive
* their own fixed matching network straight to their own U.FL.
* There is no antenna switch and no DIO driving one, so every
* rfswitch-* mask stays at its default of 0 — same as
* meshtracker_x1. ANT_LF is the sub-GHz SMA; the HF/2.4 GHz path
* is simply unused by MeshCore.
*/
};
};
&gpio0 {
status = "okay";
};
&gpio1 {
status = "okay";
};
&gpio2 {
status = "okay";
};
/* P1.x and P2.x — the LR2021 IRQ (P1.04) and the K1 button (P2.09). */
&gpiote20 {
status = "okay";
};
&gpiote30 {
status = "okay";
};
&regulators {
status = "okay";
};
&vregmain {
status = "okay";
regulator-initial-mode = <NRF5X_REG_MODE_DCDC>;
};
/*
* The XIAO fits its own 32 MHz and 32.768 kHz crystals and uses the SoC's
* internal load capacitors for both. Values copied verbatim from upstream
* Zephyr's xiao_nrf54l15 DTS.
*
* LFXO being real is the notable difference from me25ls02, which has no
* 32.768 kHz crystal and runs the calibrated RC at +/-250 ppm. This board gets
* crystal accuracy, so it is the better of the two for anything timing-sensitive.
*
* Unrelated, and worth not confusing: the Wio-LR2021 module has no 32 kHz
* crystal of its own, so the LR2021's internal LF clock is RC. We never use the
* chip's LF timer, so that does not matter here — but it is the reason not to
* copy an LF-XOSC setting onto the radio node from another board.
*/
&lfxo {
load-capacitors = "internal";
load-capacitance-femtofarad = <16000>;
status = "okay";
};
&hfxo {
load-capacitors = "internal";
load-capacitance-femtofarad = <16000>;
status = "okay";
};
&radio {
status = "okay";
};
&temp {
status = "okay";
};
/* Default Nordic RRAM map, then re-carved below for ZephCore. */
#include <vendor/nordic/nrf54l15_cpuapp_partition.dtsi>
/*
* RRAM layout (1524 KB total, 0x17D000).
*
* No bootloader: the application starts at 0x0 and the linker is given the
* whole RRAM, so slot0/slot1 are meaningless here and are deleted to keep
* anyone from assuming a DFU layout that does not exist. Layout matches
* me25ls02, the other bootloader-less nRF54L15 board.
*
* 0x000000 - 0x14E000 (1336 KB) application (links from 0x0)
* 0x14E000 - 0x165000 (92 KB) LittleFS: identity, prefs, contacts,
* channels, BLE bonds
* 0x165000 - 0x17D000 (96 KB) unused tail
*/
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
/delete-node/ &storage_partition;
&cpuapp_rram {
partitions {
lfs_partition: partition@14e000 {
compatible = "zephyr,mapped-partition";
label = "lfs";
reg = <0x14E000 0x17000>;
};
};
};
/* LittleFS auto-mount — standard /lfs mount point */
#include "../../common/filesystem.dtsi"
/*
* Battery voltage — NOT enabled, deliberately. Same reasoning as
* boards/nrf54l/xiao_nrf54l15/board.overlay, which shares this hardware.
*
* The divider is there (2:1 into P1.14/AIN7, gated by vbat_pwr above), but
* ZephyrBoard computes `mV = raw * vbat-mv-multiplier / 4096`, so the multiplier
* has to encode the ADC's full-scale voltage — and the nRF54L15 SAADC is not the
* nRF52 part we have that number for. Its internal reference is 0.9 V, not
* 0.6 V, and DS v1.0 section 8.18.7 states the single-ended range as
* "+/- 0.5*VREF/GAIN" while the saturation condition two lines later implies a
* full span of VREF/GAIN. Those two readings differ by a factor of two, and
* getting it wrong by 2x means a healthy pack reports as flat.
*
* To finish this: enable the block below on a board with a known cell voltage,
* read `raw`, and solve for the multiplier. With gain 1/4 and the 0.9 V
* reference the full scale is either 3.6 V (multiplier 7200 for the 2:1
* divider) or 1.8 V (multiplier 3600). One measurement settles it — and it
* settles it for the XIAO board too.
*
* &adc {
* status = "okay";
*
* #address-cells = <1>;
* #size-cells = <0>;
*
* channel@7 {
* reg = <7>;
* zephyr,gain = "ADC_GAIN_1_4";
* zephyr,reference = "ADC_REF_INTERNAL";
* zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
* zephyr,input-positive = <NRF_SAADC_AIN7>;
* zephyr,resolution = <12>;
* };
* };
*
* / {
* zephyr,user {
* io-channels = <&adc 7>;
* vbat-mv-multiplier = <7200>;
* };
* };
*/
@@ -0,0 +1,32 @@
# Semtech LR2021 LoRa Plus EVK default configuration
# SPDX-License-Identifier: MIT
CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y
CONFIG_NULL_POINTER_EXCEPTION_DETECTION_NONE=y
CONFIG_CACHE_MANAGEMENT=y
CONFIG_EXTERNAL_CACHE=y
# Console on uart20 -> SAMD11 CMSIS-DAP bridge -> USB CDC
# (the nRF54L15 has no USB peripheral of its own)
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_GPIO=y
CONFIG_PINCTRL=y
CONFIG_SPI=y
CONFIG_I2C=y
# RRAM + LittleFS
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NRF_GRTC_START_SYSCOUNTER=y
# rfsw_ctl / rfsw_pwr are regulator-fixed with regulator-boot-on, and they gate
# the XIAO's own 2.4 GHz antenna switch — without the regulator subsystem BLE
# comes up deaf. The init priority matches upstream's XIAO defconfig.
CONFIG_REGULATOR=y
CONFIG_REGULATOR_FIXED_INIT_PRIORITY=45
@@ -0,0 +1,182 @@
# SPDX-License-Identifier: Apache-2.0
if {[info exists env(OPENOCD_INTERFACE)]} {
set INTERFACE $env(OPENOCD_INTERFACE)
} else {
# By default connect over Debug USB port using the CMSIS-DAP interface
set INTERFACE "cmsis-dap"
}
source [find interface/$INTERFACE.cfg]
set _CHIPNAME nrf54l
# Work-area is a space in RAM used for flash programming, by default use 16 KiB.
if { [info exists WORKAREASIZE] } {
set _WORKAREASIZE $WORKAREASIZE
} else {
set _WORKAREASIZE 0x4000
}
if { [info exists CPUTAPID] } {
set _CPUTAPID $CPUTAPID
} else {
set _CPUTAPID 0x6ba02477
}
# Multidrop instance ID should be configurable by FW in TAD TINSTANCE register.
# Writes to the register are ignored due to a silicon erratum.
if { [info exists SWD_INSTANCE_ID] } {
set _SWD_INSTANCE_ID $SWD_INSTANCE_ID
} else {
set _SWD_INSTANCE_ID 0
}
transport select swd
swd newdap $_CHIPNAME cpu -expected-id $_CPUTAPID
if { [info exists SWD_MULTIDROP] } {
dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu -dp-id 0x001c0289 -instance-id $_SWD_INSTANCE_ID
} else {
dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu
}
set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME cortex_m -dap $_CHIPNAME.dap -ap-num 0
$_TARGETNAME configure -work-area-phys 0x20000000 -work-area-size $_WORKAREASIZE -work-area-backup 0
# Create target for the AUX access port (AUX-AP).
target create $_CHIPNAME.aux mem_ap -dap $_CHIPNAME.dap -ap-num 1
# AUX-AP is accessible only if CSW Prot[0] bit (Data Access) is set
$_CHIPNAME.dap apsel 1
$_CHIPNAME.dap apcsw 0x01000000 0x01000000
adapter speed 1000
# Use main processor as default target.
targets $_TARGETNAME
if {![using_hla]} {
$_TARGETNAME cortex_m reset_config sysresetreq
}
proc nrf54l-load {file} {
mww 0x5004b500 0x101
load_image $file
}
# Define CTRL_AP_NUM explicitly to avoid variable errors
set CTRL_AP_NUM 2
# Custom _nrf_check_ap_lock for nRF54L (adjusted unlocked_value to 1, based on PyOCD's CSW check)
proc _nrf_check_ap_lock { ctrl_ap_num unlocked_value } {
set target [target current]
set dap [$target cget -dap]
set err [catch {set APPROTECTSTATUS [$dap apreg $ctrl_ap_num 0xc]}] ;# 0xc is ERASEPROTECTSTATUS register
if {$err == 0 && $APPROTECTSTATUS < $unlocked_value} {
echo "\[$target\] device has AP lock engaged, trying recover."
poll off
return 1
}
return 0
}
# Custom _nrf_ctrl_ap_recover for nRF54L (adjusted IDR to 0x32880000)
proc _nrf_ctrl_ap_recover { ctrl_ap_num {is_cpunet 0} } {
set target [target current]
set dap [$target cget -dap]
set IDR [$dap apreg $ctrl_ap_num 0xfc]
if {$IDR != 0x32880000} { ;# IDR for nRF54L CTRL-AP
echo "Error: Cannot access nRF54L CTRL-AP! (IDR: 0x$IDR)"
return
}
poll off
# Reset and trigger ERASEALL task
$dap apreg $ctrl_ap_num 4 0 ;# CTRL_AP_ERASEALL = 0x004
$dap apreg $ctrl_ap_num 4 1
# First, wait for BUSY state
set timeout 300 ;# ~30s
for {set i 0} {$i < $timeout} {incr i} {
set ERASEALLSTATUS [$dap apreg $ctrl_ap_num 8]
if {$ERASEALLSTATUS == 2} { ;# BUSY status
break
}
if {$ERASEALLSTATUS == 3} { ;# ERROR status
echo "Error: Erase failed with ERROR status."
return
}
sleep 100
}
if {$i >= $timeout} {
echo "Error: Timeout waiting for BUSY status."
return
}
# Then, wait for READYTORESET state
for {set i 0} {$i < $timeout} {incr i} {
set ERASEALLSTATUS [$dap apreg $ctrl_ap_num 8]
if {$ERASEALLSTATUS == 1} { ;# READYTORESET status
echo "\[$target\] device has been successfully erased and unlocked."
break
}
if {$ERASEALLSTATUS == 3} { ;# ERROR status
echo "Error: Erase failed with ERROR status."
break
}
sleep 100
}
if {$i >= $timeout} {
echo "Error: Timeout waiting for READYTORESET status."
return
}
# Short delay before reset
sleep 10
# Assert reset: write 2 then 0
$dap apreg $ctrl_ap_num 0 2 ;# CTRL_AP_RESET = 0x000
sleep 10
$dap apreg $ctrl_ap_num 0 0
# Reset ERASEALL task
$dap apreg $ctrl_ap_num 4 0
if { $is_cpunet } {
reset init
} else {
sleep 200 ;# Slightly increased delay
$target arp_examine
poll on
}
}
lappend _telnet_autocomplete_skip _nrf_check_ap_lock _nrf_ctrl_ap_recover
if { ![using_hla] } {
# Check AP lock on examine-fail and auto-recover if locked
$_TARGETNAME configure -event examine-fail {
global CTRL_AP_NUM
set target [target current]
if { [_nrf_check_ap_lock $CTRL_AP_NUM 1] } {
nrf54l_mass_erase
$target arp_examine ;# Re-examine after recover
}
}
# Mass erase and unlock the device using proprietary nRF CTRL-AP (AP #2)
proc nrf54l_mass_erase {} {
global CTRL_AP_NUM
_nrf_ctrl_ap_recover $CTRL_AP_NUM ;# No cpunet for nRF54L
}
add_help_text nrf54l_mass_erase "Mass erase flash and unlock nRF54L device"
}
flash bank $_CHIPNAME.flash nrf5 0x00000000 0x0017D000 0 0 $_TARGETNAME
# flash bank $_CHIPNAME.uicr nrf5 0x00FFD000 0 0 0 $_TARGETNAME