diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 031177a..7db4a4e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -103,3 +103,17 @@ jobs: # applied — the failure docs/vscode-build.md warns about. out="${{ matrix.config }}/firmware-nrf54lm20a/zephyr" ls -l "$out"/hiveinside-nrf54lm20a-v*-${{ matrix.variant }}.signed.bin + + # Keep the images around so a change can be tried on hardware before it + # is tagged. Releases are built and published by release.yml; these are + # the same files without the manifest, checksums and flashing bundle. + - name: Keep the images for testers + uses: actions/upload-artifact@v4 + with: + name: images-${{ matrix.variant }} + path: | + hiveinside/${{ matrix.config }}/firmware-nrf54lm20a/zephyr/hiveinside-*.signed.bin + hiveinside/${{ matrix.config }}/firmware-nrf54lm20a/zephyr/zephyr.signed.hex + hiveinside/${{ matrix.config }}/mcuboot/zephyr/zephyr.hex + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..93f9392 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,289 @@ +# Build and publish the flashable HiveInside images. +# +# `build.yml` already builds exactly what a release needs — both configurations, +# with `west --sysbuild`, against the Zephyr revision pinned in west.yml — and +# then throws the result away. This workflow runs the same build and keeps the +# files, so a user with a XIAO and no Zephyr toolchain can flash a device. +# +# Triggered by pushing a tag (`v0.2`, …) it uploads the assets to that tag's +# release; if no release exists yet it creates a **draft** so the notes can +# still be written by hand. `workflow_dispatch` builds the identical set and +# leaves it as workflow artifacts, which is the way to rehearse a release +# without tagging anything. +# +# What gets published, per variant (`bringup` = console on, `lowpower` = the +# deployment profile of docs/low-power.md): +# +# hiveinside-nrf54lm20a-v-.signed.bin +# The BLE OTA payload — the signed application alone, MCUboot header +# included. This is the file HiveHub uploads to a node; the dashboard +# reads target and version out of the name. Never flash it over SWD. +# hiveinside-nrf54lm20a-v--factory.hex +# MCUboot at 0x0 plus the signed application in slot 0, merged into one +# file. This is the SWD factory/recovery image. Upstream Zephyr sysbuild +# does not emit NCS's merged.hex (docs/flashing.md), so it is merged here. +# manifest.txt / SHA256SUMS +# Size and CRC-32 of each OTA payload (the two numbers the OTA BEGIN +# frame carries), the Zephyr revision, the frozen west manifest, and the +# signing-key caveat. +# hiveinside-flash-tools.zip +# OpenOCD-based flashing scripts for people without a west workspace, +# plus the board's OpenOCD config taken from the same Zephyr revision. +# +# The version in the file names comes from src/hive_config.h — the same numbers +# the node advertises — and is deliberately independent of the repository tag. +# The build asserts they agree with what the build actually produced, so a +# forgotten version bump fails the release instead of shipping two different +# images under one version. +name: release + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - config: debug + variant: bringup + extra: "" + - config: lowpower + variant: lowpower + extra: >- + -DEXTRA_CONF_FILE=low-power.conf + -DEXTRA_DTC_OVERLAY_FILE=low-power.overlay + name: build (${{ matrix.variant }}) + steps: + - uses: actions/checkout@v4 + with: + path: hiveinside + + - uses: zephyrproject-rtos/action-zephyr-setup@v1 + with: + app-path: hiveinside + toolchains: arm-zephyr-eabi + + - name: Read the firmware version + working-directory: hiveinside + run: | + set -eux + # The three numbers beacon.c advertises, in MAJOR.MINOR.PATCH order. + version=$(sed -nE \ + 's/^#define HIVEINSIDE_FW_VERSION_(MAJOR|MINOR|PATCH)[[:space:]]+([0-9]+).*/\2/p' \ + firmware-nrf54lm20a/src/hive_config.h | paste -sd.) + # Three components or the sed above stopped matching the header. + echo "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' + echo "FW_VERSION=$version" >> "$GITHUB_ENV" + + - name: Build ${{ matrix.config }} + working-directory: hiveinside + env: + EXTRA: ${{ matrix.extra }} + run: | + set -eux + west build --pristine=always --sysbuild \ + -b xiao_nrf54lm20a/nrf54lm20a/cpuapp \ + -d "${{ matrix.config }}" \ + firmware-nrf54lm20a \ + ${EXTRA:+-- $EXTRA} + + - name: Collect the release images + working-directory: hiveinside + env: + CONFIG: ${{ matrix.config }} + VARIANT: ${{ matrix.variant }} + run: | + set -eux + # Merging the two sysbuild images needs intelhex. The second form is + # for a runner whose Python is marked externally managed. + python3 -m pip install --quiet intelhex \ + || python3 -m pip install --quiet --break-system-packages intelhex + + out="$CONFIG/firmware-nrf54lm20a/zephyr" + stem="hiveinside-nrf54lm20a-v$FW_VERSION-$VARIANT" + mkdir -p dist + + # The OTA payload. The build already stamped this name; asserting it + # here is what catches a version bump that never happened, and a + # config fragment that was silently not applied (the suffix is + # derived from CONFIG_SERIAL in the image actually built). + cp "$out/$stem.signed.bin" "dist/$stem.signed.bin" + + # The SWD image. Upstream Zephyr records the separately flashable + # domains in domains.yaml and leaves merging to `west flash`; a + # release consumer has no build tree, so merge the two images that + # `west flash` would program into one file. The partitions are + # disjoint, so hexmerge's default --overlap=error doubles as a check + # that the layout is what we think it is. + hexmerge.py -o "dist/$stem-factory.hex" \ + "$CONFIG/mcuboot/zephyr/zephyr.hex" \ + "$out/zephyr.signed.hex" + + # Size and CRC-32 of the exact bytes that get uploaded — the two + # values the OTA BEGIN frame carries (docs/ota-over-ble.md). + python3 - "dist/$stem.signed.bin" > "dist/sizes-$VARIANT.txt" <<'PY' + import pathlib, sys, zlib + p = pathlib.Path(sys.argv[1]) + data = p.read_bytes() + print(f"{p.name}\t{len(data)}\t0x{zlib.crc32(data):08x}") + PY + + # Keep the configuration that produced the images, so a release can + # be reproduced and audited later (ota-over-ble.md release checklist, + # step 1). The two .config files are also where a signing-key or + # MCUboot-mode mismatch between bootloader and application shows up, + # which is the first thing to compare when a device rejects slot 0. + mkdir -p "dist/build-info-$VARIANT" + cp "$out/.config" "dist/build-info-$VARIANT/app.config" + cp "$CONFIG/mcuboot/zephyr/.config" "dist/build-info-$VARIANT/mcuboot.config" + cp "$CONFIG/domains.yaml" "dist/build-info-$VARIANT/domains.yaml" + cp "$out/zephyr.dts" "dist/build-info-$VARIANT/app.dts" + + - name: Collect the flashing tools and the frozen manifest + if: matrix.variant == 'bringup' + working-directory: hiveinside + run: | + set -eux + # Both are variant-independent, so they are collected once. + west manifest --freeze > dist/west-manifest-frozen.yml + + # The board's OpenOCD config, from the same Zephyr revision the + # images were built against, so the shipped flashing path matches the + # shipped binaries. It is copied unmodified, keeps its Apache-2.0 + # SPDX header, and is attributed in the bundle's README; the RRAM + # write-buffer fix is applied by the flashing script at run time + # rather than baked in here (see tools/flash/README.md). + zephyr_base=$(west topdir)/zephyr + cfg="$zephyr_base/boards/seeed/xiao_nrf54lm20a/support/openocd.cfg" + test -f "$cfg" + mkdir -p dist/openocd + cp "$cfg" dist/openocd/xiao_nrf54lm20a.cfg + git -C "$zephyr_base" rev-parse HEAD > dist/openocd/ZEPHYR_REVISION + + - uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.variant }} + path: hiveinside/dist + if-no-files-found: error + retention-days: 7 + + package: + name: package + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + path: hiveinside + + - uses: actions/download-artifact@v4 + with: + path: staging + pattern: release-* + merge-multiple: true + + - name: Assemble the release assets + run: | + set -eux + mkdir -p dist + cp staging/hiveinside-nrf54lm20a-*.signed.bin dist/ + cp staging/hiveinside-nrf54lm20a-*-factory.hex dist/ + + # The flashing bundle: our scripts plus the board's OpenOCD config. + bundle=$(mktemp -d)/hiveinside-flash-tools + mkdir -p "$bundle" + cp hiveinside/tools/flash/flash.sh hiveinside/tools/flash/flash.ps1 "$bundle/" + cp hiveinside/tools/flash/README.md "$bundle/" + chmod +x "$bundle/flash.sh" + cp -R staging/openocd "$bundle/openocd" + (cd "$(dirname "$bundle")" && zip -qr hiveinside-flash-tools.zip hiveinside-flash-tools) + cp "$(dirname "$bundle")/hiveinside-flash-tools.zip" dist/ + + # The enclosure, so printing it needs no repository clone. + (cd hiveinside && zip -qr "$GITHUB_WORKSPACE/dist/hiveinside-enclosure.zip" enclosure) + + # Everything needed to verify, upload and reproduce the images. + version=$(ls dist/hiveinside-nrf54lm20a-v*-bringup.signed.bin \ + | sed -E 's/.*-v([0-9]+\.[0-9]+\.[0-9]+)-bringup.*/\1/') + { + echo "HiveInside release images" + echo + echo "firmware version : $version (from src/hive_config.h; the version the node advertises)" + echo "repository tag : ${GITHUB_REF_NAME}" + echo "commit : ${GITHUB_SHA}" + echo "board target : xiao_nrf54lm20a/nrf54lm20a/cpuapp" + echo "zephyr revision : $(cat staging/openocd/ZEPHYR_REVISION)" + echo "built by : ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + echo + echo "Signing key: MCUboot's RSA-2048 *development* key, which is public." + echo "That provides image formatting, not production authenticity — any" + echo "image signed with the same public key is accepted by these builds." + echo + echo "OTA payloads (upload to HiveHub; size and CRC-32 are what the OTA" + echo "BEGIN frame carries):" + echo + printf '%-52s %10s %12s\n' file bytes crc32 + cat staging/sizes-*.txt | sort | while IFS=$'\t' read -r name size crc; do + printf '%-52s %10s %12s\n' "$name" "$size" "$crc" + done + echo + echo "Variants: -bringup has the console on (bench work); -lowpower is the" + echo "deployment profile from docs/low-power.md. Flashing the wrong one" + echo "into a sealed hive fails silently — it boots and advertises the same." + echo + echo "The frozen west manifest and the per-variant Kconfig are attached as" + echo "build-info; rebuilding from them reproduces these images." + } > dist/manifest.txt + + cp staging/west-manifest-frozen.yml dist/ + for variant in bringup lowpower; do + (cd staging && zip -qr "$GITHUB_WORKSPACE/dist/build-info-$variant.zip" "build-info-$variant") + done + + # Write the sums outside dist/ first: the file must not end up + # hashing itself. + (cd dist && sha256sum ./* > ../SHA256SUMS && mv ../SHA256SUMS .) + ls -l dist + + - uses: actions/upload-artifact@v4 + with: + name: release-assets + path: dist + if-no-files-found: error + retention-days: 7 + + - name: Attach the assets to the release + if: startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -eux + if gh release view "$TAG" >/dev/null 2>&1; then + gh release upload "$TAG" "$GITHUB_WORKSPACE"/dist/* --clobber + else + # No release for this tag yet. Create it as a draft rather than + # publishing: the notes for this repository are written by hand, + # and a draft lets that happen with the assets already in place. + gh release create "$TAG" \ + --draft \ + --title "$TAG" \ + --notes "Draft created by the release workflow. Assets are attached; write the notes before publishing." \ + "$GITHUB_WORKSPACE"/dist/* + fi diff --git a/README.md b/README.md index 7ee9fd6..1356229 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ HiveInside/ │ ├── sysbuild/ MCUboot child-image config for `west --sysbuild` │ └── src/ main + sensor + FFT + beacon + OTA modules ├── enclosure/ Current 3D-printable enclosure files +├── tools/flash/ OpenOCD flashing scripts for the prebuilt release images ├── docs/ wiring, building, flashing, OTA, Home Assistant └── README.md ``` @@ -96,7 +97,16 @@ HiveInside/ ## Quick start (XIAO nRF54LM20A Sense) -Set up a west workspace from this repository, then build and flash: +**Just want a running node?** Every +[release](https://github.com/MacNite/HiveInside/releases) ships prebuilt images: +a `-factory.hex` you flash over USB with the bundled `flash.sh` / `flash.ps1` +(OpenOCD, no toolchain), and a `.signed.bin` you push to an existing node over +the air through HiveHub. Pick the `lowpower` variant for a hive and `bringup` +for the bench. See +[`docs/flashing.md`](docs/flashing.md#flashing-a-prebuilt-release-image). + +To build it yourself, set up a west workspace from this repository, then build +and flash: ```bash west init -m https://github.com/MacNite/HiveInside hiveinside-workspace diff --git a/claude.md b/claude.md index 0aaef14..b8978d9 100644 --- a/claude.md +++ b/claude.md @@ -37,6 +37,8 @@ firmware-over-BLE updates. * `west.yml` — west manifest, pinning the upstream Zephyr revision to build against. * `enclosure/` — 3D-printable enclosure (STL/STEP/F3Z). +* `tools/flash/` — OpenOCD flashing scripts shipped with the release images + (`.github/workflows/release.yml` builds and publishes those images). * `website/` — the project page published to GitHub Pages. * `docs/` — wiring, VS Code build, flashing, OTA-over-BLE, low-power profile, Home Assistant. diff --git a/docs/flashing.md b/docs/flashing.md index 445133a..a88cd8f 100644 --- a/docs/flashing.md +++ b/docs/flashing.md @@ -1,5 +1,106 @@ # Flashing HiveInside +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](#target-xiao-nrf54lm20a-sense). + +--- + +## Flashing a prebuilt release image + +Every [release](https://github.com/MacNite/HiveInside/releases) carries the +images CI built, in two variants, plus the tools to put them on a board. + +### Pick the right file + +| File | What it is | Flash it with | +| --- | --- | --- | +| `hiveinside-nrf54lm20a-v--factory.hex` | MCUboot at `0x0` **plus** the signed application in slot 0 — a complete, bootable device image | SWD over USB (below) | +| `hiveinside-nrf54lm20a-v-.signed.bin` | The signed application alone: the **BLE OTA payload** | HiveHub, over the air | + +`` is either **`lowpower`** — the deployment profile of +[`low-power.md`](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. + +`` 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.bin` over SWD.** It links at the slot-0 offset +> behind an MCUboot header, so on its own it leaves nothing at `0x0`: the CPU +> faults before `main()` and the device is completely silent. Use the +> `-factory.hex` for SWD and the `.signed.bin` only for OTA. + +### Over USB, with the flashing bundle + +Download `hiveinside-flash-tools.zip` and the factory hex for your variant from +the same release, unzip, and run: + +```bash +./flash.sh hiveinside-nrf54lm20a-v0.5.0-lowpower-factory.hex +``` + +```powershell +.\flash.ps1 -Image hiveinside-nrf54lm20a-v0.5.0-lowpower-factory.hex +``` + +It 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](#known-bug-the-openocd-rram-loader-drops-the-last-partial-write); +the bundle's README explains the one-line fix). + +The equivalent by hand, if you would rather not run a script, is: + +```bash +openocd -f \ + -c init -c "targets nrf54lm20a.cpu" -c "reset init" \ + -c "nrf54lm20a-load .hex" \ + -c "reset init" -c "verify_image .hex" \ + -c "reset run" -c shutdown +``` + +### Over the air, for a node that already runs + +Upload 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`](ota-over-ble.md). + +### Check what you downloaded + +```bash +sha256sum -c SHA256SUMS # shasum -a 256 -c SHA256SUMS on macOS +``` + +`manifest.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-.zip` are there +to reproduce it. + +Release images are signed with **MCUboot's development key, which is public** — +that is image formatting, not proof of origin. A device running these images +accepts any image signed with the same public key. For a deployment you control, +provision your own key (see the checklist in +[`ota-over-ble.md`](ota-over-ble.md#production-release-and-recovery-checklist)) +and build your own images. + +### If the board does not come up + +The troubleshooting further down this document applies unchanged — start at +[Device is silent after flashing](#device-is-silent-after-flashing-no-console-no-ble). + +--- + ## Target: XIAO nRF54LM20A Sense This firmware boots **through MCUboot** so it can accept firmware-over-BLE @@ -65,15 +166,18 @@ compile check and nothing more. > the classic "builds fine but never boots" symptom. Always build with > `--sysbuild` and flash with `west flash`. -The board definition's default runner uses OpenOCD's CMSIS-DAP path, filtered to -the on-board debugger's fixed VID:PID (`0x2886:0x0068`), so it binds to this -board's debugger and ignores unrelated CMSIS-DAP dongles. `west flash` uses that -same on-board debugger. +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 definition also lists `probe-rs` and -J-Link for contributors who deliberately attach an external probe. +(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. ### Serial console over the same USB cable @@ -131,10 +235,11 @@ probe. A spare XIAO RP2040 works as the SWD probe: | GND | GND | GND | Power the target from its own USB (or the probe's 3V3 — not both). -3. Flash the merged image through the external probe with a matching `west` - runner (for example `west flash --runner probe-rs`, with the nRF54LM20A - target pack installed). The on-board debugger's OpenOCD path is filtered to - its fixed VID:PID and will not bind to the RP2040 (`0x2E8A:0x000C`). +3. Flash the merged image through the external probe with the OpenOCD runner. + The board registers no `probe-rs` or `pyocd` runner, 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 @@ -158,9 +263,9 @@ 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`. The board definition also lists pyOCD, probe-rs, -and J-Link as optional protocols that require a verified compatible probe and -board revision. +`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 prefixed `bm_`). That is a separate, RTOS-free SDK line built on the diff --git a/docs/ota-over-ble.md b/docs/ota-over-ble.md index e2fcaf6..acc7375 100644 --- a/docs/ota-over-ble.md +++ b/docs/ota-over-ble.md @@ -365,6 +365,14 @@ For every release: whose bootloader, signing key, partition layout, or radio/application startup is broken. +Steps 1 and 3 are mechanised: `.github/workflows/release.yml` builds both +variants pristine on a tag, publishes the version-stamped payload together with +a `manifest.txt` carrying each payload's byte size and CRC-32, and attaches the +frozen west manifest and both images' Kconfig so the build can be reproduced. +Steps 2, 4 and 5 still need a person and a board. Note that those published +images use the **development** signing key — a deployment with its own key has +to build and publish its own artifacts. + ## Implementation `firmware-nrf54lm20a/src/ota.c` streams into the board's MCUboot secondary slot, diff --git a/tools/flash/README.md b/tools/flash/README.md new file mode 100644 index 0000000..a76599d --- /dev/null +++ b/tools/flash/README.md @@ -0,0 +1,139 @@ +# HiveInside flashing tools + +Flash a **prebuilt** HiveInside image onto a Seeed XIAO nRF54LM20A Sense with +nothing but OpenOCD and a USB-C cable — no Zephyr toolchain, no west workspace, +no external probe. The board's on-board SAMD11 CMSIS-DAP debugger does the work. + +These scripts also ship as `hiveinside-flash-tools.zip` on every +[release](https://github.com/MacNite/HiveInside/releases), together with the +board's OpenOCD config taken from the exact Zephyr revision the images were +built against. + +If you already have a west workspace, keep using `west flash` — see +[`docs/flashing.md`](../../docs/flashing.md). This is for everyone else. + +## Which file do I flash? + +Each release carries two images per variant. They are not interchangeable: + +| File | Use it for | How | +| --- | --- | --- | +| `hiveinside-nrf54lm20a-v--factory.hex` | A **new or bricked board**, over USB | the scripts here | +| `hiveinside-nrf54lm20a-v-.signed.bin` | **Updating a running node**, over the air | upload it in HiveHub | + +> ⚠️ Never flash the `.signed.bin` over SWD. It is the application alone, so it +> leaves nothing at `0x0`: the CPU faults before `main()` and the device goes +> completely silent — no console, no BLE. The scripts refuse it for you. + +And pick the right **variant**: + +* **`lowpower`** — the deployment profile. No console, no LED, minimum idle + current. This is what goes into a hive. +* **`bringup`** — console on at 115200 8N1. Use it on the bench, while wiring up + an SHT40 or watching the readout. + +Both advertise identically, so a `bringup` image in a sealed hive looks fine +from HiveHub and simply drains the battery. Check the suffix before you flash. + +## Prerequisites + +OpenOCD 0.12 or newer: + +```bash +sudo apt install openocd # Debian/Ubuntu +brew install open-ocd # macOS +sudo pacman -S openocd # Arch +winget install OpenOCD.OpenOCD # Windows (or the xPack OpenOCD build) +``` + +On Linux, allow non-root access to the debugger (Seeed VID `2886`): + +```bash +echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="2886", MODE="0666"' \ + | sudo tee /etc/udev/rules.d/99-seeed-cmsis-dap.rules +sudo udevadm control --reload-rules && sudo udevadm trigger +``` + +Replug the board afterwards. + +## Flashing + +Unzip the release bundle, put the factory hex next to the scripts (or pass its +path), and run: + +```bash +./flash.sh # picks the single factory hex it finds +./flash.sh hiveinside-nrf54lm20a-v0.5.0-lowpower-factory.hex +``` + +```powershell +.\flash.ps1 +.\flash.ps1 -Image hiveinside-nrf54lm20a-v0.5.0-lowpower-factory.hex +``` + +Both take `--openocd-cfg` / `-OpenocdCfg`, `--openocd` / `-Openocd` and +`--no-verify` / `-NoVerify`; `--help` prints the details. + +The scripts run the same OpenOCD sequence `west flash --verify` runs for this +board, and verify the image by reading it back. **Leave the verification on** — +see below. + +## Why the scripts patch the board config + +The board's OpenOCD flash loader upstream is: + +```tcl +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**, and the proc never commits that buffer. The nRF54L RRAM +controller only writes a 128-bit line out once the line fills, so any tail that +does not reach a 16-byte boundary stays in the buffer and never lands in RRAM. +On a signed image that truncates the signature TLV, and MCUboot then reports +`E: Image in the primary slot is not valid!` — with a silent, dark device to +show for it, because the bootloader console is disabled for size. + +The scripts copy the config to a temp file and write `0x1` instead: same +register, write buffer off, every write commits. Flashing is slower, and +correct. Nothing on your disk is modified, and if a future Zephyr fixes the +loader upstream the scripts detect that and use the config unchanged. + +This is upstream board support, not a HiveInside bug, and it affects any image +whose length is not a multiple of 16 bytes. + +## Verifying what you downloaded + +```bash +sha256sum -c SHA256SUMS # shasum -a 256 -c SHA256SUMS on macOS +``` + +`manifest.txt` in the same release lists each OTA payload's byte size and +CRC-32 — the two values the OTA `BEGIN` frame carries — plus the Zephyr +revision and the commit the images were built from. + +**On authenticity:** release images are signed with MCUboot's **development** +key, which is public. That gives image formatting, not proof of origin: a +device running these images will accept any image signed with the same public +key. Check the SHA-256 above to know you got the file we built. + +## When it does not work + +`docs/flashing.md` in the repository has the full troubleshooting path — +a silent device after flashing, `E: Image in the primary slot is not valid!`, +serial-port names per OS, and using an external probe. Start there. + +## Contents of the release bundle + +``` +flash.sh, flash.ps1 these scripts +README.md this file +openocd/xiao_nrf54lm20a.cfg the board's OpenOCD config, unmodified, + from zephyrproject-rtos/zephyr (Apache-2.0) +openocd/ZEPHYR_REVISION the Zephyr commit it was taken from — the same + revision the images in the release were built + against +``` diff --git a/tools/flash/flash.ps1 b/tools/flash/flash.ps1 new file mode 100644 index 0000000..f93ce24 --- /dev/null +++ b/tools/flash/flash.ps1 @@ -0,0 +1,160 @@ +<# +.SYNOPSIS +Flash a prebuilt HiveInside factory image onto a XIAO nRF54LM20A Sense. + +.DESCRIPTION +Windows counterpart of flash.sh. It drives the board's on-board CMSIS-DAP +debugger with OpenOCD only — no Zephyr toolchain and no west workspace — and +runs the same OpenOCD sequence `west flash --verify` runs for this board, with +the board's RRAM loader corrected first: the upstream one leaves the RRAM write +buffer enabled and never commits it, so the tail of any image whose length is +not a multiple of 16 bytes is silently dropped and MCUboot then rejects slot 0. + +.PARAMETER Image +The factory image to flash. Defaults to the single hiveinside-*-factory.hex in +the current directory, or next to this script. + +.PARAMETER OpenocdCfg +Board OpenOCD config. Defaults to openocd\xiao_nrf54lm20a.cfg next to this +script, or to the one in a west workspace found from the current directory. + +.PARAMETER Openocd +OpenOCD executable (default: openocd from PATH). + +.PARAMETER NoVerify +Skip the read-back verification. Not recommended. + +.EXAMPLE +.\flash.ps1 +.EXAMPLE +.\flash.ps1 -Image hiveinside-nrf54lm20a-v0.5.0-lowpower-factory.hex +#> +[CmdletBinding()] +param( + [string] $Image, + [string] $OpenocdCfg, + [string] $Openocd = 'openocd', + [switch] $NoVerify +) + +$ErrorActionPreference = 'Stop' +$scriptDir = $PSScriptRoot + +function Die([string] $Message) { + Write-Error $Message + exit 1 +} + +# ── The image ────────────────────────────────────────────────────────────── +# +# Only the factory hex is flashable over SWD. The .signed.bin beside it is the +# BLE OTA payload — the application alone — so flashing that leaves nothing at +# 0x0 and the device goes completely silent. +if (-not $Image) { + foreach ($dir in @($PWD.Path, $scriptDir)) { + $found = @(Get-ChildItem -Path $dir -Filter 'hiveinside-*-factory.hex' -File -ErrorAction SilentlyContinue) + if ($found.Count -gt 0) { break } + } + if ($found.Count -eq 0) { + Die 'No hiveinside-*-factory.hex found here; pass one with -Image.' + } + if ($found.Count -gt 1) { + Die ("Several factory images found; name the one to flash:`n " + + ($found.Name -join "`n ")) + } + $Image = $found[0].FullName +} + +if ($Image -like '*.signed.bin') { + Die ("$Image is the BLE OTA payload, not an SWD image.`n" + + 'Flash hiveinside---factory.hex instead; upload the ' + + '.signed.bin through HiveHub.') +} +if ($Image -notlike '*.hex') { Die "Expected a .hex factory image, got: $Image" } +if (-not (Test-Path -LiteralPath $Image)) { Die "No such file: $Image" } +$Image = (Resolve-Path -LiteralPath $Image).Path + +if (-not (Get-Command $Openocd -ErrorAction SilentlyContinue)) { + Die ("OpenOCD not found (looked for '$Openocd').`n" + + 'Install it (for example: winget install OpenOCD.OpenOCD, or the ' + + 'xPack OpenOCD build) or pass -Openocd C:\path\to\openocd.exe.') +} + +# ── The board config ─────────────────────────────────────────────────────── +if (-not $OpenocdCfg) { + $bundled = Join-Path $scriptDir 'openocd\xiao_nrf54lm20a.cfg' + if (Test-Path -LiteralPath $bundled) { + $OpenocdCfg = $bundled + } elseif (Get-Command west -ErrorAction SilentlyContinue) { + $topdir = @(& west topdir 2>$null) | Select-Object -First 1 + if ($LASTEXITCODE -eq 0 -and $topdir) { + $OpenocdCfg = Join-Path $topdir.Trim() ` + 'zephyr\boards\seeed\xiao_nrf54lm20a\support\openocd.cfg' + } + } +} +if (-not $OpenocdCfg -or -not (Test-Path -LiteralPath $OpenocdCfg)) { + Die ("No board OpenOCD config found; pass one with -OpenocdCfg.`n" + + 'It is boards/seeed/xiao_nrf54lm20a/support/openocd.cfg in a Zephyr ' + + "checkout, and ships in openocd\ inside hiveinside-flash-tools.zip.") +} + +# ── The RRAM write-buffer fix ────────────────────────────────────────────── +# +# 0x5004e500 is RRAMC.CONFIG. Upstream writes 0x101 — WEN=1 plus a one-line +# (16-byte) write buffer that the proc never commits, so a trailing partial +# line never reaches RRAM. 0x1 disables the buffer: every write commits. +# Slower, and correct. +$workdir = New-Item -ItemType Directory -Path (Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())) +try { + $cfgText = Get-Content -LiteralPath $OpenocdCfg -Raw + if ($cfgText -match '0x5004e500 0x101') { + $cfgText = $cfgText -replace 'mww 0x5004e500 0x101', 'mww 0x5004e500 0x1' + Write-Host 'Applied the RRAM write-buffer fix to a temporary copy of the board config.' + } elseif ($cfgText -match '0x5004e500 0x1\b') { + Write-Host 'Board config already commits every RRAM write; using it unchanged.' + } else { + Die ("$OpenocdCfg has no recognisable nrf54lm20a-load RRAM setup.`n" + + 'Upstream may have restructured it — check it by hand before flashing.') + } + $cfg = Join-Path $workdir.FullName 'board.cfg' + Set-Content -LiteralPath $cfg -Value $cfgText -NoNewline + + # OpenOCD passes the file name through Tcl, which splits on whitespace. + if ($Image -match '\s') { + $copy = Join-Path $workdir.FullName 'image.hex' + Copy-Item -LiteralPath $Image -Destination $copy + $Image = $copy + } + # Tcl also treats a backslash as an escape. + $tclImage = $Image -replace '\\', '/' + + $openocdArgs = @( + '-f', $cfg, + '-c', 'init', + '-c', 'targets nrf54lm20a.cpu', + '-c', 'reset init', + '-c', "nrf54lm20a-load $tclImage" + ) + if (-not $NoVerify) { + # The check that catches the truncation the fix above prevents. + $openocdArgs += @('-c', 'reset init', '-c', "verify_image $tclImage") + } + $openocdArgs += @('-c', 'reset run', '-c', 'shutdown') + + Write-Host "Flashing $Image" + & $Openocd @openocdArgs + if ($LASTEXITCODE -ne 0) { Die "OpenOCD exited with $LASTEXITCODE" } +} finally { + Remove-Item -Recurse -Force -LiteralPath $workdir.FullName -ErrorAction SilentlyContinue +} + +Write-Host @' + +Done. The node reboots into MCUboot and then the application. + +A bringup image prints its banner on the board's USB serial port at 115200 8N1 +(a COM port on Windows) — press RST with the monitor attached to see it. +A lowpower image has no console by design; look for its BLE advertisement +(HiveInside-XXXX) instead. +'@ diff --git a/tools/flash/flash.sh b/tools/flash/flash.sh new file mode 100755 index 0000000..6f02ca6 --- /dev/null +++ b/tools/flash/flash.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Flash a prebuilt HiveInside factory image onto a XIAO nRF54LM20A Sense over +# the board's on-board CMSIS-DAP debugger, using OpenOCD only — no Zephyr +# toolchain and no west workspace required. +# +# It runs the same OpenOCD sequence `west flash --verify` runs for this board, +# with one difference that matters: the board's RRAM loader is corrected first +# (see rram_write_buffer_fix below), because the upstream one silently drops +# the tail of any image whose length is not a multiple of 16 bytes. +# +# Usage: ./flash.sh [options] [image.hex] +# See README.md next to this script, or docs/flashing.md in the repository. + +set -euo pipefail + +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +openocd_bin=${OPENOCD:-openocd} +openocd_cfg=${HIVEINSIDE_OPENOCD_CFG:-} +image="" +verify=1 + +die() { printf 'error: %s\n' "$*" >&2; exit 1; } + +usage() { + sed -n '2,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + cat <<'USAGE' + +Options: + --openocd-cfg PATH Board OpenOCD config to use. Defaults to the copy in + openocd/ next to this script, or to the one in a west + workspace found from the current directory. + --openocd PATH OpenOCD binary (default: openocd, or $OPENOCD). + --no-verify Skip the read-back verification. Not recommended. + -h, --help This text. +USAGE +} + +while [ $# -gt 0 ]; do + case "$1" in + --openocd-cfg) openocd_cfg=${2:-}; shift 2 ;; + --openocd) openocd_bin=${2:-}; shift 2 ;; + --no-verify) verify=0; shift ;; + -h|--help) usage; exit 0 ;; + -*) die "unknown option: $1 (try --help)" ;; + *) [ -z "$image" ] || die "more than one image given"; image=$1; shift ;; + esac +done + +# ── The image ────────────────────────────────────────────────────────────── +# +# Only the factory hex is flashable over SWD. The .signed.bin next to it is the +# BLE OTA payload: it is the application alone, so flashing it leaves nothing +# at 0x0 and the device goes completely silent. +if [ -z "$image" ]; then + candidates=() + for dir in "$PWD" "$script_dir"; do + while IFS= read -r -d '' f; do candidates+=("$f"); done \ + < <(find "$dir" -maxdepth 1 -name 'hiveinside-*-factory.hex' -print0 2>/dev/null) + [ ${#candidates[@]} -eq 0 ] || break + done + case ${#candidates[@]} in + 0) die "no hiveinside-*-factory.hex found here; pass one as an argument" ;; + 1) image=${candidates[0]} ;; + *) die "several factory images found; name the one to flash: +$(printf ' %s\n' "${candidates[@]}")" ;; + esac +fi + +case "$image" in +*.signed.bin) + die "$image is the BLE OTA payload, not an SWD image. +Flash hiveinside---factory.hex instead; upload the .signed.bin +through HiveHub." ;; +*.hex) ;; +*) die "expected a .hex factory image, got: $image" ;; +esac +[ -f "$image" ] || die "no such file: $image" + +command -v "$openocd_bin" >/dev/null 2>&1 \ + || die "openocd not found (looked for '$openocd_bin'). +Install it: apt install openocd | brew install open-ocd | pacman -S openocd, +or point at one with --openocd /path/to/openocd." + +# ── The board config ─────────────────────────────────────────────────────── +if [ -z "$openocd_cfg" ]; then + if [ -f "$script_dir/openocd/xiao_nrf54lm20a.cfg" ]; then + openocd_cfg="$script_dir/openocd/xiao_nrf54lm20a.cfg" + elif topdir=$(west topdir 2>/dev/null); then + openocd_cfg="$topdir/zephyr/boards/seeed/xiao_nrf54lm20a/support/openocd.cfg" + fi +fi +[ -n "$openocd_cfg" ] && [ -f "$openocd_cfg" ] \ + || die "no board OpenOCD config found; pass one with --openocd-cfg. +It is boards/seeed/xiao_nrf54lm20a/support/openocd.cfg in a Zephyr checkout, +and ships in openocd/ inside the release's hiveinside-flash-tools.zip." + +workdir=$(mktemp -d) +trap 'rm -rf "$workdir"' EXIT + +# ── rram_write_buffer_fix ────────────────────────────────────────────────── +# +# The upstream loader 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*, and the proc never commits that buffer. The nRF54L RRAM controller +# only writes a 128-bit line out once the line fills, so whatever does not +# reach a 16-byte boundary stays in the buffer and never lands in RRAM. On a +# signed image that silently truncates the signature TLV, and MCUboot then +# reports `E: Image in the primary slot is not valid!`. +# +# 0x1 is the same register with the write buffer disabled: every write commits. +# Slower, and correct. +cfg="$workdir/board.cfg" +if grep -q '0x5004e500 0x101' "$openocd_cfg"; then + sed 's/mww 0x5004e500 0x101/mww 0x5004e500 0x1/' "$openocd_cfg" > "$cfg" + printf 'Applied the RRAM write-buffer fix to a temporary copy of the board config.\n' +elif grep -q '0x5004e500 0x1' "$openocd_cfg"; then + cp "$openocd_cfg" "$cfg" + printf 'Board config already commits every RRAM write; using it unchanged.\n' +else + die "$openocd_cfg has no recognisable nrf54lm20a-load RRAM setup. +Upstream may have restructured it — check it by hand before flashing." +fi + +# OpenOCD passes the file name through Tcl, which splits on whitespace. +if printf '%s' "$image" | grep -q '[[:space:]]'; then + cp "$image" "$workdir/image.hex" + image="$workdir/image.hex" +fi + +verify_args=() +if [ "$verify" -eq 1 ]; then + # Read the image back. This is the check that catches the truncation the + # fix above prevents, and any half-written flash. + verify_args=(-c "reset init" -c "verify_image $image") +fi + +printf 'Flashing %s\n' "$image" +"$openocd_bin" -f "$cfg" \ + -c "init" \ + -c "targets nrf54lm20a.cpu" \ + -c "reset init" \ + -c "nrf54lm20a-load $image" \ + "${verify_args[@]}" \ + -c "reset run" \ + -c "shutdown" + +cat <<'DONE' + +Done. The node reboots into MCUboot and then the application. + +A bringup image prints its banner on the board's USB serial port at 115200 8N1 +(/dev/ttyACM0 on Linux, /dev/cu.usbmodem* on macOS) — press RST with the monitor +attached to see it. A lowpower image has no console by design; look for its +BLE advertisement (HiveInside-XXXX) instead. +DONE diff --git a/website/index.html b/website/index.html index 53a2513..dfbceb9 100644 --- a/website/index.html +++ b/website/index.html @@ -94,6 +94,7 @@

The sensor that lives
inside the hive.

✨ What it does 🔧 Hardware ⚡ Build & flash + ⬇️ Download firmware ★ Star on GitHub
@@ -269,6 +270,15 @@

Build & flash it

The firmware is a Zephyr application built with west and sysbuild — the only supported build path. Everything below runs over the single USB-C cable; no external debug probe is needed.

+ +
📦 Don't want to build it? Every + release ships prebuilt images: a + -factory.hex for a fresh board, flashed over the same USB-C cable with the bundled + flash.sh / flash.ps1 (OpenOCD only, no toolchain), and a + .signed.bin to push to a node that is already running, over the air through HiveHub. + Take the lowpower variant for a hive and bringup for the bench — + flashing a prebuilt image →
+
1
@@ -371,7 +381,7 @@

Dig deeper

📻 Firmware & frame

The readout format, the full BLE frame layout, the watchdog, and the roadmap.

firmware-nrf54lm20a/README.md →

🔌 Wiring

SHT40, battery and the optional external accelerometer.

docs/wiring.md →

-

⚡ Flashing

Flashing details, the external-probe alternative, and what to do when a device is silent.

docs/flashing.md →

+

⚡ Flashing

Prebuilt release images, flashing details, the external-probe alternative, and what to do when a device is silent.

docs/flashing.md →

🔋 Low power

The code-and-hardware audit, the deployment build profile, and how to measure the result properly.

docs/low-power.md →

📥 OTA over BLE

The streaming relay, the partition layout, and the release artifact naming.

docs/ota-over-ble.md →

🧑‍💻 Building in VS Code

The nRF Connect extension setup with the debug and lowpower configurations.

docs/vscode-build.md →