There are two ways to get firmware onto a board:
- Flash a prebuilt release image — a download and one command, no Zephyr toolchain. Start below.
- Build it yourself with
west --sysbuild— the reference path, and the one to use when you are changing the firmware. That is the rest of this document, starting at Target: XIAO nRF54LM20A Sense.
Every release carries the images CI built, in two variants, plus the tools to put them on a board.
| File | What it is | Flash it with |
|---|---|---|
hiveinside-nrf54lm20a-v<version>-<variant>-factory.hex |
MCUboot at 0x0 plus the signed application in slot 0 — a complete, bootable device image |
SWD over USB (below) |
hiveinside-nrf54lm20a-v<version>-<variant>.signed.bin |
The signed application alone: the BLE OTA payload | HiveHub, over the air |
<variant> is either lowpower — the deployment profile of
low-power.md, no console, for a hive — or bringup, with
the console on at 115200 8N1 for bench work. Both advertise identically, so a
bringup image in a sealed hive looks healthy from HiveHub and just drains the
battery. Check the suffix before flashing.
<version> is the firmware version from src/hive_config.h — the same numbers
the node advertises — and is independent of the repository's release tag.
⚠️ Never flash a.signed.binover SWD. It links at the slot-0 offset behind an MCUboot header, so on its own it leaves nothing at0x0: the CPU faults beforemain()and the device is completely silent. Use the-factory.hexfor SWD and the.signed.binonly for OTA.
Download hiveinside-flash-tools.zip and the factory hex for your variant from
the same release, unzip, and run:
./flash.sh hiveinside-nrf54lm20a-v0.5.0-lowpower-factory.hex.\flash.ps1 -Image hiveinside-nrf54lm20a-v0.5.0-lowpower-factory.hexIt needs OpenOCD on PATH and nothing else; the bundle carries the board's
OpenOCD config from the Zephyr revision the images were built against. On Linux
add the udev rule from the bundle's README so the debugger is usable without
sudo. The scripts run the same OpenOCD sequence west flash --verify runs,
verify the image by reading it back, and — importantly — correct the board's
RRAM loader first (see
the OpenOCD RRAM bug;
the bundle's README explains the one-line fix).
The equivalent by hand, if you would rather not run a script, is:
openocd -f <board-openocd.cfg-with-the-RRAM-fix> \
-c init -c "targets nrf54lm20a.cpu" -c "reset init" \
-c "nrf54lm20a-load <image>.hex" \
-c "reset init" -c "verify_image <image>.hex" \
-c "reset run" -c shutdownUpload the .signed.bin in HiveHub's firmware form. It reads the target and
version straight out of the file name, streams the image into the node's
secondary slot, and MCUboot test-swaps with automatic rollback. Nothing is
committed until size and CRC-32 verify, so a failed transfer leaves the node on
its old firmware. See ota-over-ble.md.
sha256sum -c SHA256SUMS # shasum -a 256 -c SHA256SUMS on macOSmanifest.txt in the release lists each payload's size and CRC-32 (the values
the OTA BEGIN frame carries), the commit and the Zephyr revision behind the
build, and west-manifest-frozen.yml plus build-info-<variant>.zip are there
to reproduce it.
The release workflow requires a project-owned MCUboot signing key and refuses to
produce these artifacts when its HIVEINSIDE_SIGNING_KEY_PEM secret is absent.
This is a required deployment step: the SDK's default development key is public,
so a node built with it accepts firmware from anyone using that known key. For a
local deployment build, generate and select the key with
SB_CONFIG_BOOT_SIGNATURE_KEY_FILE exactly as shown in the
ota-over-ble.md production checklist.
Keep the private key outside the repository and preserve the matching public key
plus an SWD recovery path. Signing is currently the trust boundary because the
unencrypted OTA GATT writes have no HMAC; the filter-accept-list TODO remains in
ota_init() pending coordinated HiveHub bonding support.
The troubleshooting further down this document applies unchanged — start at Device is silent after flashing.
This firmware boots through MCUboot so it can accept firmware-over-BLE
updates (see ota-over-ble.md). A bootable image is therefore
MCUboot plus a signed application in slot 0. Only a west --sysbuild
build produces both, and both must reach the chip:
# From a west workspace carrying the XIAO nRF54LM20A board definition. The
# repository's own west.yml pins an upstream Zephyr revision that has it; an
# nRF Connect SDK workspace needs the board added out of tree (see below).
west build --sysbuild -b <board-target> -d debug path/to/firmware-nrf54lm20a
west flash -d debug # programs every sysbuild image over the on-board debugger
picocom -b 115200 /dev/ttyACM0 # serial console over the same cable (see below)west flash run against the top-level sysbuild build directory reads
build/domains.yaml and programs every image in flash order — MCUboot at 0x0
and the signed application at slot 0 (zephyr.signed.hex; Zephyr's
cmake/mcuboot.cmake repoints the runner at it). Flashing a single image, or
flashing build/firmware-nrf54lm20a/zephyr/zephyr.hex by hand, leaves the
device unbootable — see "Device is silent after flashing" below.
merged.hexis an nRF Connect SDK artifact, not an upstream Zephyr one. NCS's sysbuild concatenates the images intobuild/merged.hex; a plainzephyrprojectworkspace does not create that file, and it is not needed —west flashhandles the multi-image case on its own. Where this document andota-over-ble.mdrefer tomerged.hexas the SWD factory/recovery image, that applies to NCS builds; on upstream Zephyr the equivalent is "everythingwest flashprograms".
Use a pristine build when changing the board, partition layout, bootloader, or
signing configuration (west build --pristine --sysbuild ...). Before treating
the result as a release, inspect the generated partition report and retain the
artifacts for their distinct purposes: the full-flash image is for SWD
factory/recovery, while the application's zephyr.signed.bin is the BLE OTA
payload. Never send a full-flash image or the unsigned zephyr.bin through the
OTA characteristic.
See the production-key, release-test, and recovery checklist in
ota-over-ble.md.
The board target is the west name of the Seeed board definition with its
nRF54L core qualifier (for example xiao_nrf54lm20a/nrf54lm20a/cpuapp); use the
exact name the board's board.yml declares.
The XIAO nRF54LM20A Sense has an on-board SAMD11 CMSIS-DAP debugger (Seeed
USB VID:PID 0x2886:0x0068) connected to the SoC's SWD lines and brought out on
the USB-C connector. No external probe is required — plug the board into USB
and west flash programs the merged image over SWD through the on-board
debugger.
A plain west build produces a single application image. It never builds
MCUboot, signs the app, or emits a merged hex (sysbuild.conf and
sysbuild/mcuboot.conf are only read by a sysbuild build). It is useful as a
compile check and nothing more.
⚠️ Do not flash an image built without--sysbuild. With MCUboot enabled, the application links at the slot-0 offset (behind an MCUboot header), so flashing it alone leaves nothing at0x0: the CPU faults beforemain()runs and the device goes completely silent — no serial output, no BLE. This is the classic "builds fine but never boots" symptom. Always build with--sysbuildand flash withwest flash.
The board definition's default runner uses OpenOCD's CMSIS-DAP path. Its
support/openocd.cfg sources the generic interface/cmsis-dap.cfg without
filtering on VID:PID, so with several CMSIS-DAP adapters attached OpenOCD binds
to whichever it finds first — unplug the others, or select one explicitly with
an adapter usb location/-i argument.
Do not use pyocd on the current silicon: it aborts during APPROTECT
recovery with Memory transfer fault @ 0x00ffc31c-0x00ffc31f. CMSIS-DAP
(OpenOCD) is the supported path. The board's board.cmake registers exactly
three runners — openocd (the default), jlink and nrfutil — so
west flash --runner probe-rs and --runner pyocd are not available for this
board however the probe is attached.
The on-board SAMD11 also exposes a USB CDC ACM serial port and bridges it to
the SoC's uart20, so application printk() and Zephyr logs appear on the host
over the same USB-C cable used for flashing — no second adapter needed. It runs
at 115200 8N1 and enumerates on Linux as /dev/ttyACM0 (the index can
differ when other ACM devices are attached).
# Confirm which ACM node is the Seeed on-board debugger (VID 2886)
udevadm info -q property -n /dev/ttyACM0 | grep -E 'ID_VENDOR_ID|ID_MODEL|ID_SERIAL'
# ID_VENDOR_ID=2886 → Seeed XIAO nRF54LM20A on-board debugger
picocom -b 115200 /dev/ttyACM0 # or: screen /dev/ttyACM0 115200On macOS the same port appears as /dev/cu.usbmodem* — there is no
/dev/ttyACM0, so a copied-and-pasted /dev/ttyACM0 fails or silently binds
nothing. List the ports first and pick the Seeed one; note that the board
presents both a CDC ACM data port and the CMSIS-DAP interface, so more than one
entry can appear:
ls /dev/cu.usbmodem*
picocom -b 115200 /dev/cu.usbmodemXXXXThe startup banner
([HiveInside] nrf54lm20a fw <version> | USB + HiveHub BLE beacon) prints once
at boot, followed by a readout block every measurement cycle, so if the monitor is
opened afterwards, press RST with it connected to see the banner and the
first readout. The console is a plain polled UART, so the firmware never blocks
on a missing terminal — it boots and keeps sampling regardless. (The nRF54's
native usbhs is not wired to the port — the SAMD11 owns it — so the console
must ride the debugger's UART bridge rather than a native USB-CDC device on the
nRF54.)
The on-board debugger is sufficient for normal use; this is only for boards whose debugger is unavailable, or for bench setups that prefer an external probe. A spare XIAO RP2040 works as the SWD probe:
-
Flash it with Raspberry Pi Debugprobe firmware: double-tap reset to mount the
RPI-RP2drive, then drag ondebugprobe_on_pico.uf2(from theraspberrypi/debugprobereleases). It re-enumerates as a CMSIS-DAP probe (0x2E8A:0x000C). -
Wire probe → target (both are 3.3 V, no level shifting):
Probe (XIAO RP2040) Signal Target (XIAO nRF54LM20A) GP2 (pad D8) SWCLK SWCLK / SWDCLK GP3 (pad D10) SWDIO SWDIO GND GND GND Power the target from its own USB (or the probe's 3V3 — not both).
-
Flash the merged image through the external probe with the OpenOCD runner. The board registers no
probe-rsorpyocdrunner, and the CMSIS-DAP interface config does not filter on VID:PID, so unplug the target's own debugger (or keep only one adapter attached) to be sure OpenOCD binds to the RP2040 (0x2E8A:0x000C) rather than to the on-board SAMD11.
On Linux, add a udev rule so the probe is accessible without sudo
(SUBSYSTEM=="usb", ATTRS{idVendor}=="2e8a", MODE="0666"), then reload rules
and replug.
The nRF54LM20A firmware reads all four sensors (SHT40, IMU, microphone, nPM1300
battery), prints the readout to this serial console, runs the vibration and
acoustic FFT band analysis, broadcasts the HiveHub measurement beacon, and
accepts firmware-over-BLE updates through MCUboot. See
firmware-nrf54lm20a/README.md for the
readout format, the build details, and the serial-console setup, and
ota-over-ble.md for the OTA protocol.
Because the firmware boots through MCUboot, a bootable device needs both the
bootloader and the signed application, and only west --sysbuild produces them
— see the top of this document. Build it in a west workspace that has the XIAO
nRF54LM20A board definition available: the repository's west.yml pins an
upstream Zephyr revision carrying it, and an nRF Connect SDK workspace needs the
board added out of tree (see the board-target section below). west flash
programs every sysbuild image over the on-board
CMSIS-DAP debugger; with an external probe (below) select the matching runner, e.g.
west flash --runner jlink. Those are the only alternatives the board
registers — openocd, jlink and nrfutil — and jlink needs a real J-Link
probe.
⚠️ Not the "nRF Connect SDK Bare Metal" option (nrf-bm, board targets prefixedbm_). That is a separate, RTOS-free SDK line built on the SoftDevice and raw nrfx drivers — it has no Zephyr kernel, no Zephyr device model or devicetree-driven peripherals, and no Zephyr Bluetooth host. This firmware is a Zephyr application (zephyr/kernel.h,zephyr/bluetooth/*,zephyr/drivers/{regulator,i2c,sensor,gpio}.h,zephyr/audio/dmic.h,zephyr/dfu/mcuboot.h, plus the board overlays), so it cannot build there at any version. Use upstream Zephyr or the RTOS-based nRF Connect SDK. Note also thatbm_nrf54lm20dkis Nordic's nRF54LM20 DK, a different board from the Seeed XIAO nRF54LM20A Sense this firmware targets.
xiao_nrf54lm20a ships in upstream Zephyr, and only on main — no tagged
Zephyr release up to v4.4.2 contains boards/seeed/xiao_nrf54lm20a/, which is
why this repository's west.yml pins a main commit rather than a release.
The nRF Connect SDK uses its own Zephyr fork (nrfconnect/sdk-zephyr), which
does not carry it — not on main, v3.2-branch or v3.1-branch — so it
never appears in the VS Code extension's board-target dropdown, however new the
SDK is. Only Nordic's own nrf54lm20dk is there. Nothing is broken; the
definition is simply absent.
Two ways forward:
-
Build against upstream Zephyr —
west init -m <this repo>gives you a workspace at the pinned revision. This is the path the rest of this document describes and needs no extra setup. -
Add the board out-of-tree to the NCS workspace. Copy the whole
boards/seeed/xiao_nrf54lm20a/directory out of upstream Zephyr into a board root of your own, keeping theboards/<vendor>/<board>/structure:my-boards/ └── boards/seeed/xiao_nrf54lm20a/ ← the upstream directory, unmodifiedThen point the build at the directory that contains
boards/— not at the board directory itself:west build --sysbuild -b xiao_nrf54lm20a/nrf54lm20a/cpuapp \ path/to/firmware-nrf54lm20a -- -DBOARD_ROOT=/abs/path/to/my-boards
In the VS Code extension the same value goes in the nRF Connect board roots setting, after which the target shows up in the dropdown. Use an absolute path: sysbuild resolves relative
*_ROOTvariables against the application directory, which is rarely what you mean.The copy is self-contained:
seeed_xiao_connector.dtsilives inside the board directory, and the SoC-level includes it pulls in (nordic/nrf54lm20a_cpuapp.dtsi,vendor/nordic/nrf54lm20_a_b_cpuapp_partition.dtsi) are both present insdk-zephyr. Expect to re-sync the copy whenever upstream changes the board — this repo'sncs_fixups.overlayalready exists because the two trees do not always agree.
Two failure modes of the VS Code route are worth recognising, because neither is caused by the board files:
- "Loading boards…" that never finishes, or an error about an unrelated SoC
(
The SoC stm32c5a3xx was not found in the SDK). Adding a board root makes the extension re-scan every board it knows about, and it trips over its own stale board/SoC cache — the message says as much ("could be a result of thewest updateof the SDK that cleared the SoC cache"). Restart VS Code, or have the extension regenerate the cache; the STM32 name is noise, not a hint. - A build that fails "because of the Bare Metal SDK" after switching the SDK
in the dialog. The selected SDK is baked into the build configuration and
into
build/CMakeCache.txtwhen it is first created. Changing the dropdown does not retarget an existing configuration — delete the build configuration and itsbuild/directory, then create it again against the RTOS-based SDK.
None of this affects the command line. west build --sysbuild from a plain
zephyrproject workspace is the reference path and stays the quickest way to a
known-good image.
A --sysbuild build that dies while linking mcuboot/zephyr/zephyr_pre0.elf
with undefined reference to z_impl_k_mutex_lock / k_work_submit /
z_impl_k_usleep (usually preceded by the Kconfig warning I2C ... was assigned the value 'n' but got the value 'y') is the board's power-management drivers
leaking into the bootloader: MCUboot is built single-threaded on Nordic SoCs,
but the XIAO board tree enables CONFIG_REGULATOR plus the power_en regulator
and the nPM1300 on a bit-banged I²C bus for every sysbuild image, and those
drivers need the kernel mutex/work-queue APIs. sysbuild/mcuboot.conf turns
CONFIG_REGULATOR and CONFIG_MFD off for the bootloader image to prevent
this; if you see the error, check that fragment is being picked up (it must sit
next to the application, i.e. firmware-nrf54lm20a/sysbuild/mcuboot.conf) and
rebuild with --pristine. The failure is not host- or toolchain-specific, and
a plain west build does not reproduce it — it never builds MCUboot.
The firmware prints its banner as the very first statement of main(), before
touching any sensor, and starts advertising a few lines later. Missing sensors
never cause silence — an unconnected SHT40 just prints climate : n/a. So no
console output and no advertising means the application is not running at
all, and the fault is in the boot chain rather than in the application. Work
through it in this order:
- Is the console itself proven? Check the port name (macOS uses
/dev/cu.usbmodem*, not/dev/ttyACM0) and press RST with the monitor already attached — the banner prints once at reset and is easy to miss. Silence on BLE too makes a pure console problem unlikely, but this is the cheapest check. - Did both images reach the chip? Run
west flashagainst the top-level sysbuild build directory, not a sub-image directory, and read its output: it should program two images. Re-flash explicitly withwest flash --domain mcubootandwest flash --domain firmware-nrf54lm20aif in doubt. An application alone at slot 0 with nothing at0x0faults beforemain()and is completely silent — this is the same brick the non-sysbuild warning above describes. - Make MCUboot talk.
sysbuild/mcuboot.confdisables the bootloader's console for size, which also means a bootloader that refuses to start slot 0 fails silently. Temporarily comment out theCONFIG_SERIAL=n/CONFIG_CONSOLE=n/CONFIG_UART_CONSOLE=nlines there, addCONFIG_MCUBOOT_LOG_LEVEL_INF=y, rebuild with--pristineand re-flash. MCUboot then reports onuart20whether it found and validated a slot-0 image. A signature or image-magic complaint points at the signing key; no MCUboot output at all points back at step 2. - Check the signing key matched. The application must be signed with the
key the bootloader was built with. Zephyr does not fail the build when the
application has no signing key —
cmake/mcuboot.cmakeonly warns that the result "will not be bootable by MCUboot unless it is signed manually", andwest flashthen programs an unsigned image that MCUboot silently rejects. Confirmbuild/firmware-nrf54lm20a/zephyr/zephyr.signed.hexexists and that the configure log names the same key file for both images. See the next section for how to tell the two apart.
With the bootloader console enabled (step 3 above) a rejected application looks like this:
I: Starting bootloader
I: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
I: Boot source: none
E: Image in the primary slot is not valid!
E: Unable to find bootable image
magic=unset and image_ok=0x3 are not the problem — those describe the
image trailer, which an SWD-flashed image legitimately does not have, and
MCUboot boots such an image happily. The failure is boot_validate_slot()
rejecting slot 0, which has exactly two causes: the header/signature does not
verify against the key built into the bootloader, or the bytes on the chip are
not the bytes imgtool produced. Two commands separate them:
# (a) Is the image itself validly signed with the bootloader's key?
# Default sysbuild config uses MCUboot's RSA-2048 development key.
cd <west-topdir>
python bootloader/mcuboot/scripts/imgtool.py verify \
-k bootloader/mcuboot/root-rsa-2048.pem \
<build>/firmware-nrf54lm20a/zephyr/zephyr.signed.bin
# (b) Do the bytes on the chip match the hex? (openocd runner only)
west flash --domain firmware-nrf54lm20a --verifyIf (a) fails, the build is at fault — compare the two images' Kconfig, which is where a signing-type or key mismatch shows up:
grep -E 'MCUBOOT_SIGNATURE_KEY_FILE|MCUBOOT_GENERATE_UNSIGNED_IMAGE|ROM_START_OFFSET|MCUBOOT_BOOTLOADER_MODE' \
<build>/firmware-nrf54lm20a/zephyr/.config
grep -E 'BOOT_SIGNATURE_TYPE|BOOT_SIGNATURE_KEY_FILE|BOOT_VALIDATE_SLOT0|BOOT_SWAP|SINGLE_APPLICATION_SLOT' \
<build>/mcuboot/zephyr/.configThe bootloader and the application must agree on the signature type and the key,
and on the MCUboot mode. Sysbuild derives both sides from the same
SB_CONFIG_BOOT_SIGNATURE_TYPE_* / SB_CONFIG_BOOT_SIGNATURE_KEY_FILE (default:
RSA with $(ZEPHYR_MCUBOOT_MODULE_DIR)/root-rsa-2048.pem), so a mismatch means
something overrode one side — most often a CONFIG_BOOT_* line added to
sysbuild/mcuboot.conf, which changes only the bootloader and leaves the
application signed the old way. Set signing options in sysbuild.conf as
SB_CONFIG_* instead, so both images move together.
If (a) passes but (b) fails, the image is good and the programming step is at fault — see the next section, which is the known cause on this board.
Symptom. imgtool verify says the image is fine, but west flash --verify
reports a handful of 0xff bytes at the very end of the image:
Error: checksum mismatch - attempting binary compare
diff 0 address 0x00035130. Was 0xff instead of 0xf3
...
No more differences found.
Cause. The board's flash loader in
boards/seeed/xiao_nrf54lm20a/support/openocd.cfg is:
proc nrf54lm20a-load {file} {
mww 0x5004e500 0x101
load_image $file
}0x5004e500 is RRAMC.CONFIG; 0x101 sets WEN=1 and a one-line (16-byte)
write buffer. load_image then writes the image, and the proc stops — it never
commits the buffer. The nRF54L RRAM controller only writes a 128-bit line out
when that line fills, so whatever does not reach a 16-byte boundary is left in
the buffer and never lands in RRAM.
In the build this was diagnosed on, the application image was 151864 bytes;
151864 mod 16 = 8, so the final 8 bytes — the tail of the signature TLV —
were silently dropped. MCUboot then reads
a truncated signature and reports E: Image in the primary slot is not valid!.
Zephyr's own RRAM driver gets this right: soc_flash_nrf_rram.c has a
commit_changes() that triggers NRF_RRAMC_TASK_COMMIT_WRITEBUF whenever the
write length is not a multiple of the buffer size. The OpenOCD script has no
equivalent. This is upstream board support, not a HiveInside bug, and it affects
any image whose length is not 16-byte aligned.
Fix. Disable the write buffer in that proc — one character, and it needs no register offsets:
proc nrf54lm20a-load {file} {
mww 0x5004e500 0x1 ;# WEN=1, write-buffer size 0 -> every write commits
load_image $file
}Flashing gets slower, and correct. Keeping the buffer means flushing it after
load_image, either by triggering TASKS_COMMITWRITEBUF or by reading back the
last written byte — the Product Specification commits a line on "a read
operation from a 128-bit word line in the buffer that has already been written
to", which is the fallback Zephyr's driver uses when the commit task is
unavailable.
Re-flash both images afterwards, not just the application. MCUboot is written in three sections of 34636, 2720 and 60 bytes; the first and last are also not 16-byte aligned, so up to 12 bytes are missing from each. It boots regardless — those bytes happen not to be on any path that matters — but the bootloader on the device is not the bootloader that was built. Confirm with:
west flash --domain mcuboot --verify
west flash --domain firmware-nrf54lm20a --verifySee ota-over-ble.md for the firmware-over-BLE protocol and
the production release/recovery checklist.