XIAO radio: build upstream's serial env, not our own Serial1 patch
MeshCore has carried companion-over-hardware-UART for a long time — the defines SERIAL_RX/SERIAL_TX, packaged per board as a *_companion_radio_serial env, and the ESP32-S3 twin of this very board already uses the same D6/D7 pads. Our patch was reimplementing it for the one platform it had never been wired into: at our old pin the SERIAL_RX branches lived inside the ESP32 and RP2040 blocks and nRF52 fell through to Serial. So the patch is now the two things actually missing, both upstream-shaped and submitted: a Serial1 reference so the shared block compiles on nRF52, and the Xiao_nrf52_companion_radio_serial env carrying the I2C escape in build_unflags rather than editing the flags every XIAO env inherits. Pin moves to a dev commit, since dev is the branch MeshCore takes work on. flash.py grew the check that was missing when it mattered: a drive letter is not an identity — when one board leaves the USB bus another inherits its letter — so it now reads INFO_UF2.TXT and refuses a bootloader that isn't a XIAO, or that carries S140 6.1.1 and would take this S140-v7 image at the wrong address and never boot. It also stops pretending copy-to-drive is the only route: our bootloader exposes a serial port and no mass storage at all, so it prints the serial-DFU command instead of a dead end. Verified from a clean clone at the new pin: env builds, uf2 converts to the same 811008 bytes at 0x27000 that is now running on the radio, and both Xiao_nrf52_companion_radio_usb and Xiao_S3_WIO_companion_radio_serial still build, so neither side of the new #if moved.
This commit is contained in:
1 parent
b015091ae0
commit
3a02a14be7
5 files changed
+191
-41
No files matched your search
@@ -17,3 +17,7 @@ venv/
|
||||
# MeshTerm runtime data
|
||||
*.db
|
||||
output/
|
||||
|
||||
# Built radio firmware (scripts/xiao-radio/build-firmware.sh writes it here)
|
||||
scripts/xiao-radio/*.uf2
|
||||
scripts/xiao-radio/_meshcore-build/
|
||||
@@ -53,11 +53,16 @@ sh build-firmware.sh
|
||||
```
|
||||
|
||||
This clones MeshCore, applies [`meshcore-uart1.patch`](meshcore-uart1.patch), builds the
|
||||
`Xiao_nrf52_companion_radio_usb` environment, and writes **`meshcore-xiao-radio.uf2`**.
|
||||
`Xiao_nrf52_companion_radio_serial` environment, and writes **`meshcore-xiao-radio.uf2`**.
|
||||
|
||||
The patch makes two changes that are *both* required (see [Why](#why-it-works)):
|
||||
- binds the companion protocol to **hardware UART1** (`Serial1`, D6/D7) instead of USB;
|
||||
- moves the **I²C bus off D6/D7** (to internal pins 16/17) so it can't steal the UART pins.
|
||||
- teaches MeshCore's existing **`SERIAL_RX` companion interface** to build on nRF52 — it was
|
||||
written for ESP32 and declares a `HardwareSerial` the Adafruit nRF52 core doesn't have;
|
||||
- adds the **`Xiao_nrf52_companion_radio_serial` env**, which puts the companion on D6/D7 and
|
||||
moves the **I²C bus off those pads** (to internal pins 16/17) so it can't steal them.
|
||||
|
||||
Both changes are upstream-shaped and have been **submitted to MeshCore**. If they land, this
|
||||
patch step disappears: the env ships with the firmware and you just build it.
|
||||
|
||||
---
|
||||
|
||||
@@ -69,8 +74,28 @@ Plug the **XIAO's own USB‑C** into your computer (it may stay wired to the Lyr
|
||||
python flash.py
|
||||
```
|
||||
|
||||
It triggers the bootloader and copies the firmware. If it can't, **double‑tap the XIAO's
|
||||
reset button** (it mounts as the `XIAO-SENSE` drive) and re‑run.
|
||||
It pulses the XIAO's app port at 1200 baud to drop it into the bootloader, checks that the
|
||||
bootloader it found really is a XIAO, and copies the firmware. If it can't get there,
|
||||
**double‑tap the XIAO's reset button** and re‑run.
|
||||
|
||||
**If no UF2 drive appears**, that isn't necessarily a fault: on our board the bootloader came
|
||||
up as a **serial port only, with no mass‑storage interface at all**, so there is nothing to
|
||||
copy a file onto. `flash.py` detects that and prints the serial‑DFU command to run from your
|
||||
MeshCore checkout instead:
|
||||
|
||||
```bash
|
||||
pio run -e Xiao_nrf52_companion_radio_serial -t upload --upload-port <bootloader port>
|
||||
```
|
||||
|
||||
That is the route that worked here — it ends in `Device programmed.` and the XIAO reboots
|
||||
itself into the radio firmware.
|
||||
|
||||
> ⚠️ **Check which board you are flashing.** A drive letter is not an identity — when one
|
||||
> board leaves the USB bus another can inherit its letter, so the volume you found may not be
|
||||
> the one you just touched. `flash.py` reads `INFO_UF2.TXT` and refuses anything that isn't a
|
||||
> XIAO, or that carries **SoftDevice S140 6.1.1** (which wants the app at `0x26000`, while
|
||||
> this firmware links for S140 v7 at `0x27000` — it would flash and then not boot). Override
|
||||
> with `--force` only if you know better than it does.
|
||||
|
||||
Sanity check: after flashing, the XIAO's USB serial port goes **silent to the companion
|
||||
protocol** — because the companion now lives on D6/D7, not USB. That silence is correct.
|
||||
@@ -105,6 +130,14 @@ should see your node come up (ours reports as `Johnputer-Pico`).
|
||||
|
||||
Everything below was the hard part; the scripts encode the answers so you don't repeat it.
|
||||
|
||||
- **MeshCore already had companion‑over‑UART; nRF52 was just never wired into it.** The
|
||||
`SERIAL_RX`/`SERIAL_TX` defines and the per‑board `*_companion_radio_serial` envs have
|
||||
shipped for a while — `Xiao_S3_WIO_companion_radio_serial` uses *the same D6/D7 pads* on the
|
||||
ESP32‑S3 twin of this board. The nRF52 build just doesn't compile: the shared code declares
|
||||
`HardwareSerial companion_serial(1)` and calls `setPins()`, and on the Adafruit nRF52 core
|
||||
`HardwareSerial` is abstract with no numbered constructor. `Serial1` is the concrete `Uart`
|
||||
the core always defines, and `Uart::setPins(pin_rx, pin_tx)` takes the same arguments in the
|
||||
same order — so one reference fixes it and the call site is untouched.
|
||||
- **Calculinux never wires UART1 to a pad.** The controller is enabled (so `/dev/ttyS1`
|
||||
exists) but reaches no pin. The RK3506 *matrix IO* can carry UART1 on almost any pad, so
|
||||
[`uart1-mux.py`](uart1-mux.py) pokes two register groups over `/dev/mem` to put UART1‑TX on
|
||||
@@ -127,15 +160,22 @@ Everything below was the hard part; the scripts encode the answers so you don't
|
||||
| Symptom | Check |
|
||||
|---|---|
|
||||
| `meshterm` can't open the port | Is `MT_USER` in `dialout`? (`groups`) — needs a fresh login after setup. |
|
||||
| Port opens but no node / silence | Re‑flash with the **`_usb`** firmware (not `_ble`); the BLE build ignores Serial1. Confirm the XIAO's USB serial is *silent* to companion frames. |
|
||||
| Still silent, wiring confirmed | Confirm the firmware has the **I²C‑remap** patch (`PIN_WIRE_SCL=16`, `PIN_WIRE_SDA=17`). |
|
||||
| Port opens but no node / silence | Re‑flash with the **`_serial`** env (not `_ble`/`_usb`); only that one defines `SERIAL_RX`/`SERIAL_TX`. Confirm the XIAO's USB serial is *silent* to companion frames. |
|
||||
| Still silent, wiring confirmed | Confirm the firmware has the **I²C‑remap** (`PIN_WIRE_SCL=16`, `PIN_WIRE_SDA=17`). |
|
||||
| Flashed fine, board never comes back | Wrong board, or wrong SoftDevice — check `INFO_UF2.TXT` said a XIAO and **S140 v7**, not 6.1.1. `flash.py` refuses both unless you passed `--force`. |
|
||||
| `flash.py` finds no UF2 drive | Expected on a bootloader that exposes CDC only. Use the `pio … -t upload --upload-port` line it prints. |
|
||||
| Nothing on `/dev/ttyS1` after reboot | `systemctl status uart1-radio-mux` — the mux must run each boot. |
|
||||
| Verify the raw link, as root | Route the mux, open `/dev/ttyS1` @115200, send an `APP_START` frame (`3c 0e 00 01` + 7×`00` + name); a good radio replies with a `>`‑framed `05` (SELF_INFO). |
|
||||
|
||||
### Updating MeshCore
|
||||
`build-firmware.sh` pins a tested commit. To track upstream, run it with
|
||||
`MESHCORE_COMMIT=main`; if the patch no longer applies cleanly, re‑apply the two edits in
|
||||
[`meshcore-uart1.patch`](meshcore-uart1.patch) by hand — they're two hunks.
|
||||
`build-firmware.sh` pins a tested commit on MeshCore's `dev` branch (that's where work lands
|
||||
first). To track upstream, run it with `MESHCORE_COMMIT=dev`; if the patch no longer applies
|
||||
cleanly, re‑apply the two edits in [`meshcore-uart1.patch`](meshcore-uart1.patch) by hand —
|
||||
they're two hunks.
|
||||
|
||||
**This patch is upstream in review.** If it merges, drop the `git apply` line from
|
||||
`build-firmware.sh` and delete the patch: `Xiao_nrf52_companion_radio_serial` becomes a stock
|
||||
env and there is nothing left to carry.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
# What it does:
|
||||
# 1. clones MeshCore (pinned to a tested commit) if you don't already have it
|
||||
# 2. applies meshcore-uart1.patch -- the two changes that make this work:
|
||||
# * bind the companion protocol to hardware UART1 (Serial1 = D6/D7) instead of USB
|
||||
# * move the I2C bus OFF D6/D7 (to internal pins 16/17) so it can't seize the UART pins
|
||||
# 3. builds the companion_radio_usb environment
|
||||
# * teach the existing SERIAL_RX companion interface to build on nRF52 (upstream
|
||||
# declares an ESP32-only HardwareSerial; nRF52 wants Serial1)
|
||||
# * add a Xiao_nrf52_companion_radio_serial env: companion on D6/D7, and I2C moved
|
||||
# off those pads (to internal pins 16/17) so it can't seize them
|
||||
# 3. builds that environment
|
||||
# 4. converts the .hex to .uf2
|
||||
#
|
||||
# The patch is upstream-shaped and has been submitted to MeshCore. Once it lands, delete
|
||||
# the `git apply` step below -- the env ships with the firmware and nothing needs patching.
|
||||
#
|
||||
# Prerequisites: git, and PlatformIO CLI on PATH (`pio`). Install pio with:
|
||||
# pip install platformio
|
||||
#
|
||||
@@ -23,10 +28,11 @@ set -eu
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
PATCH="$HERE/meshcore-uart1.patch"
|
||||
MC="${MESHCORE_DIR:-$HERE/_meshcore-build}"
|
||||
ENV=Xiao_nrf52_companion_radio_usb
|
||||
# Commit this patch was cut against. Newer MeshCore may need the two edits re-applied by hand
|
||||
# (see README "Updating"). Override with MESHCORE_COMMIT= to track upstream.
|
||||
PIN="${MESHCORE_COMMIT:-03b6ef4}"
|
||||
ENV=Xiao_nrf52_companion_radio_serial
|
||||
# Commit this patch was cut against (a `dev` commit -- that is the branch MeshCore takes work
|
||||
# on). Newer MeshCore may need the edits re-applied by hand (see README "Updating").
|
||||
# Override with MESHCORE_COMMIT= to track upstream.
|
||||
PIN="${MESHCORE_COMMIT:-e9edfc8e}"
|
||||
|
||||
command -v git >/dev/null 2>&1 || { echo "ERROR: git not found"; exit 1; }
|
||||
command -v pio >/dev/null 2>&1 || command -v platformio >/dev/null 2>&1 || {
|
||||
|
||||
@@ -41,7 +41,13 @@ def find_uf2_drive():
|
||||
|
||||
|
||||
def touch_1200():
|
||||
"""Best-effort: pulse any Seeed XIAO app serial port at 1200 baud to reset to bootloader."""
|
||||
"""Best-effort: pulse any Seeed XIAO app serial port at 1200 baud to reset to bootloader.
|
||||
|
||||
Matched on Seeed's USB vendor id (2886) *only*. Deliberately not widened to Adafruit's
|
||||
239A even though nRF52840 boards commonly use it: other nRF52840 gear on the same bench
|
||||
(a LilyGo T-Echo, say) answers to 239A, and knocking somebody else's radio into its
|
||||
bootloader is a rude way to find out you grabbed the wrong board.
|
||||
"""
|
||||
try:
|
||||
import serial
|
||||
import serial.tools.list_ports as lp
|
||||
@@ -57,13 +63,70 @@ def touch_1200():
|
||||
pass
|
||||
|
||||
|
||||
def bootloader_port():
|
||||
"""Return the COM/tty device of a XIAO sitting in its bootloader, or None."""
|
||||
try:
|
||||
import serial.tools.list_ports as lp
|
||||
except Exception:
|
||||
return None
|
||||
for p in lp.comports():
|
||||
hwid = (p.hwid or "").upper()
|
||||
if "2886" in hwid and "0045" in hwid: # Seeed VID + bootloader PID
|
||||
return p.device
|
||||
return None
|
||||
|
||||
|
||||
def read_uf2_info(drive):
|
||||
"""Parse INFO_UF2.TXT on ``drive`` into a dict of its ``Key: value`` lines."""
|
||||
info = {}
|
||||
try:
|
||||
with open(os.path.join(drive, "INFO_UF2.TXT")) as fh:
|
||||
for line in fh:
|
||||
if ":" in line:
|
||||
k, _, v = line.partition(":")
|
||||
info[k.strip()] = v.strip()
|
||||
except OSError:
|
||||
pass
|
||||
return info
|
||||
|
||||
|
||||
def check_drive_is_ours(drive, force=False):
|
||||
"""Refuse to write to a bootloader that isn't the board this firmware was built for.
|
||||
|
||||
Two different mistakes are cheap to make and expensive to debug. A drive letter is not an
|
||||
identity -- when one board leaves the bus another can inherit its letter, so the volume you
|
||||
found may not be the one you just touched. And this firmware links for SoftDevice S140 v7
|
||||
(app at 0x27000); a board carrying S140 6.1.1 wants it at 0x26000 and would simply not boot.
|
||||
"""
|
||||
info = read_uf2_info(drive)
|
||||
model = info.get("Model", "?")
|
||||
softdev = info.get("SoftDevice", "?")
|
||||
print(" %s reports: %s / %s" % (drive, model, softdev))
|
||||
|
||||
problems = []
|
||||
if "xiao" not in model.lower():
|
||||
problems.append("this is a %r bootloader, not a XIAO" % model)
|
||||
if "6." in softdev:
|
||||
problems.append("%s expects the app at 0x26000; this build is linked for S140 v7" % softdev)
|
||||
if problems and not force:
|
||||
sys.exit(
|
||||
"ERROR: refusing to flash %s --\n - %s\n"
|
||||
"Unplug the other board, or re-run with --force if you are sure."
|
||||
% (drive, "\n - ".join(problems))
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
uf2 = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "meshcore-xiao-radio.uf2")
|
||||
args = [a for a in sys.argv[1:] if a != "--force"]
|
||||
force = "--force" in sys.argv[1:]
|
||||
uf2 = args[0] if args else os.path.join(HERE, "meshcore-xiao-radio.uf2")
|
||||
if not os.path.exists(uf2):
|
||||
sys.exit("ERROR: firmware not found: %s\n(run build-firmware.sh first)" % uf2)
|
||||
|
||||
print(">> looking for XIAO bootloader drive ...")
|
||||
drive = find_uf2_drive()
|
||||
before = drive
|
||||
if not drive:
|
||||
touch_1200()
|
||||
for _ in range(30):
|
||||
@@ -71,12 +134,26 @@ def main():
|
||||
if drive:
|
||||
break
|
||||
time.sleep(1)
|
||||
if drive and drive == before:
|
||||
# The letter was already there before we touched anything, so it may belong to some
|
||||
# other board entirely -- check what it says rather than trusting the letter.
|
||||
print(" note: %s was already mounted before the touch" % drive)
|
||||
if not drive:
|
||||
port = bootloader_port()
|
||||
if port:
|
||||
sys.exit(
|
||||
"ERROR: the XIAO is in its bootloader on %s but exposes no UF2 drive.\n"
|
||||
"This bootloader presents a serial port only, so copy-to-drive can't work.\n"
|
||||
"Flash it over serial DFU instead, from your MeshCore checkout:\n"
|
||||
" pio run -e Xiao_nrf52_companion_radio_serial -t upload --upload-port %s"
|
||||
% (port, port)
|
||||
)
|
||||
sys.exit(
|
||||
"ERROR: no UF2 bootloader drive found.\n"
|
||||
"Double-tap the XIAO's reset button (it mounts as XIAO-SENSE), then re-run."
|
||||
"ERROR: no UF2 bootloader drive and no XIAO bootloader serial port found.\n"
|
||||
"Double-tap the XIAO's reset button, then re-run."
|
||||
)
|
||||
|
||||
check_drive_is_ours(drive, force=force)
|
||||
print(">> flashing %s -> %s" % (os.path.basename(uf2), drive))
|
||||
with open(uf2, "rb") as src:
|
||||
data = src.read()
|
||||
|
||||
@@ -1,29 +1,52 @@
|
||||
diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp
|
||||
index ef9b6bf..020c746 100644
|
||||
index 89f0e6cb..cf30f9ad 100644
|
||||
--- a/examples/companion_radio/main.cpp
|
||||
+++ b/examples/companion_radio/main.cpp
|
||||
@@ -159,7 +159,8 @@ void setup() {
|
||||
#ifdef BLE_PIN_CODE
|
||||
serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
|
||||
#else
|
||||
- serial_interface.begin(Serial);
|
||||
+ Serial1.begin(115200); // companion protocol over hardware UART1 (D6=TX, D7=RX)
|
||||
+ serial_interface.begin(Serial1);
|
||||
@@ -61,7 +61,13 @@ MultiSerialInterface interface_manager;
|
||||
#if defined(SERIAL_RX)
|
||||
#include <helpers/ArduinoSerialInterface.h>
|
||||
ArduinoSerialInterface hardware_serial_interface;
|
||||
- HardwareSerial companion_serial(1);
|
||||
+ #if defined(NRF52_PLATFORM)
|
||||
+ // nRF52 has no numbered-constructor HardwareSerial; Serial1 is the concrete UARTE0
|
||||
+ // instance the core always defines. setPins() below moves it to the requested pads.
|
||||
+ Uart& companion_serial = Serial1;
|
||||
+ #else
|
||||
+ HardwareSerial companion_serial(1);
|
||||
+ #endif
|
||||
#endif
|
||||
the_mesh.startInterface(serial_interface);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
|
||||
// platform file system
|
||||
diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini
|
||||
index a085433..065b617 100644
|
||||
index 1076bd8d..1eb216d1 100644
|
||||
--- a/variants/xiao_nrf52/platformio.ini
|
||||
+++ b/variants/xiao_nrf52/platformio.ini
|
||||
@@ -26,8 +26,8 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
- -D PIN_WIRE_SCL=D6
|
||||
- -D PIN_WIRE_SDA=D7
|
||||
@@ -119,3 +119,28 @@ build_src_filter = ${Xiao_nrf52.build_src_filter}
|
||||
+<../examples/kiss_modem/*.cpp>
|
||||
lib_deps =
|
||||
${Xiao_nrf52.lib_deps}
|
||||
+
|
||||
+[env:Xiao_nrf52_companion_radio_serial]
|
||||
+extends = Xiao_nrf52
|
||||
+board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld
|
||||
+board_upload.maximum_size = 708608
|
||||
+build_flags =
|
||||
+ ${Xiao_nrf52.build_flags}
|
||||
+ -I examples/companion_radio/ui-orig
|
||||
+ -D MAX_CONTACTS=350
|
||||
+ -D MAX_GROUP_CHANNELS=40
|
||||
+ -D OFFLINE_QUEUE_SIZE=256
|
||||
+ -D QSPIFLASH=1
|
||||
+ -D SERIAL_TX=D6
|
||||
+ -D SERIAL_RX=D7
|
||||
+ -D PIN_WIRE_SCL=16
|
||||
+ -D PIN_WIRE_SDA=17
|
||||
-D PIN_USER_BTN=PIN_BUTTON1
|
||||
-D DISPLAY_CLASS=NullDisplayDriver
|
||||
build_src_filter = ${nrf52_base.build_src_filter}
|
||||
+build_unflags =
|
||||
+ -D PIN_WIRE_SCL=D6
|
||||
+ -D PIN_WIRE_SDA=D7
|
||||
+build_src_filter = ${Xiao_nrf52.build_src_filter}
|
||||
+ +<../examples/companion_radio/*.cpp>
|
||||
+ +<../examples/companion_radio/ui-orig/*.cpp>
|
||||
+lib_deps =
|
||||
+ ${Xiao_nrf52.lib_deps}
|
||||
+ densaugeo/base64 @ ~1.4.0
|
||||
Reference in new issue
Block a user