fwt_software/docs/hardware-porting-plan.md

20 KiB
Raw Blame History

LattePanda IOTA Porting Plan

Draft for review, 2026-09-11. Covers both repos (software/, firmware/) plus the harness. Hardware facts for the new board are in docs/lattepanda-iota.md.

Companion document: hardware-config-plan.md covers splitting config.ini, the hardware.ini assembly profile and the firmware config handshake. That work stands on its own and is a prerequisite for this port.


0. Baseline — what is actually deployed today

Read live from the tower (ggs@10.11.12.111) rather than from the docs:

Value
Board LattePanda 3 Delta (DMI product_name) — not the Sigma
CPU Intel Celeron N5105, 4C/4T
OS Ubuntu 22.04.5 LTS, kernel 6.8.0-138
RAM / disk 8 GB / 57 GB eMMC, 67 % full

Device bindings in the live config.ini:

Function Binding Backing hardware
Motor MCU /dev/serial/by-id/usb-Arduino_LLC_Arduino_Leonardo-if00 USB 2341:8036, onboard ATmega32U4
IMU (Xsens MTi) /dev/ttyS0 onboard RS-232 header
Ambient (SHT41) /dev/i2c-2 @ 0x44 Synopsys DesignWare LPSS I2C
Camera DEV_1AB22C049CD1 USB3 1ab2:0001 Alvium 1800 U-2040c
Modem /dev/ttyUSB0-3 USB 2c7c:0801 Quectel RM520N-GL, M.2 B-key
Extra NIC — USB 0b95:1790 ASIX AX88179 GbE

Doc bug: docs/deployment.md:3 says "LattePanda Sigma". It is a 3 Delta. Fix as part of this work.


The port

What the configuration work changes for this port: the IOTA is one new setup kit at hardware/lattepanda-iota/ (config plan §8) whose install.sh emits a hardware.ini with [Board] model = lattepanda-iota and the new device paths.

This is not only a board swap. The new tower also has a different yaw motor and gear ratio, so its [Motor.yaw] block differs too: steps_per_rev, global_scaler, all three speeds, and enc_deviation (config plan §5). Two consequences worth planning for:

  • Calibration is mandatory, not inherited. yaw_counts_per_deg is derived from steps_per_rev, so gimbal heading setup must be run on this tower before its headings mean anything.
  • Yaw homing must be re-tuned before it is trusted. It finds the endstop by driving into it, so both homing_speed and enc_deviation_limit want measuring on the real drivetrain — the free-running encoder gap that the limit is set against is a property of the gear ratio.

1. Verdict up front

The IOTA is a performance and lifecycle upgrade (N150 Gracemont vs N5105 Tremont, LPDDR5 with ECC, 3× USB 3.2 Gen 2, TPM, RTC), and the 10–15 V DC input matches the tower's 12 V rail. The compute story is good.

The cost is concentrated in three places, in increasing order of pain:

  1. Firmware port — real but small and well-bounded.
  2. Logic-level change, 5 V → 3.3 V — turns out to be nearly free (see B.3).
  3. Three orphaned peripherals — the IMU, the ambient sensor, and the 5G modem all lose the port they currently use. The modem is the one with no clean answer.

2. Firmware port — ATmega32U4 → RP2040

Pin budget is a non-issue. The firmware needs 7 GPIO: 4 SPI + 2 chip-selects + 2 enables (the pitch endstops go to the TMC's own REFL/REFR, not to the MCU). The IOTA breaks out 14 clean RP2040 pins.

AS BUILT on tower 10.11.12.121 (wired 2026-09-15). One TMC5160-BOB, yaw only — the proposed map below it was never wired, and src/platform_iota.h carries these numbers:

Signal RP2040 TMC5160-BOB Was (Leonardo)
Logic supply 3.3 V 3.3V pin 1 VCC_IO 5 V
Signal ground GND pin 2 GND GND
SPI CSN (yaw) GP1 pin 3 CSN D8
SPI SCK GP2 pin 4 SCK ICSP
SPI MOSI GP3 pin 5 SDI ICSP
SPI MISO GP0 pin 6 SDO ICSP
Yaw DRV_ENN GP4 pin 9 DRV_ENN D7

These are a legal SPI0 group, which is not automatic: SPI0 takes SCK only from {2,6,18,22}, TX from {3,7,19,23} and RX from {0,4,16,20}. GP1 is SPI0's hardware CSn (the driver toggles it in software regardless) and GP4 is a plain output.

The originally proposed map, kept for the record
Signal RP2040 Header pin Was (Leonardo)
SPI SCK GP2 9 ICSP
SPI MOSI GP3 10 ICSP
SPI MISO GP4 11 ICSP
Yaw CSN GP5 12 D8
Yaw DRV_ENN GP6 13 D7
Pitch CSN GP7 25 D1
Pitch DRV_ENN GP8 26 D0
(spare, I2C0 for a rehomed SHT41) GP12 / GP13 30 / 31 —

Toolchain:

[env:iota_rp2040]
platform         = https://github.com/maxgerhardt/platform-raspberrypi.git
board            = pico
framework        = arduino
board_build.core = earlephilhower
monitor_speed    = 115200

Note the IOTA's RP2040 has 8 MB flash (Pico has 2 MB) — use a board definition with the right flash size or override it, or the filesystem/OTA layout will be wrong.

Code changes: small and enumerable.

  • tmc5160.cpp: call SPI.setSCK/setTX/setRX(...) before SPI.begin(). SPISettings, beginTransaction, transfer are API-identical — the TMC driver logic is untouched.
  • main.cpp: MCUSR → rp2040.getResetReason(); freeRam() → rp2040.getFreeHeap().
  • F() is a no-op on RP2040 — harmless, leave it.
  • Serial is USB CDC on both — the host protocol and command.cpp are unchanged.

✅ Remote flashing — MEASURED on the board, 2026-09-15. Neither documented path works yet

Tested end to end on tower 10.11.12.121. Results first, the original research below:

Path Board running MicroPython (factory) Board running our firmware
1200-baud touch (pio run --target upload) no reset — CDC does not implement it resets, but reboots into the APPLICATION, not BOOTSEL — picotool waits and times out
picotool reboot -f -u refused — no picotool reset interface refused — Unable to locate reset interface on the device
BOOTSEL serial command (added 2026-09-15) n/a works — this is the answer
machine.bootloader() over the MicroPython REPL works — re-enumerates as 2e8a:0003 RP2 Boot n/a, MicroPython is gone once flashed

So the first flash of a factory board needs the REPL trick or a physical BOOTSEL press — one time per board, and worth knowing before someone drives to a mast. The update case is the one that still has no answer: our firmware honours the 1200-baud reset but returns to the application, so a tower cannot currently be re-flashed over the network at all.

Fixed by a BOOTSEL serial command calling arduino-pico's rp2040.rebootToBootloader() — implemented and proven on tower 121 the same day. The full loop now runs with nobody at the board:

firmware running  --BOOTSEL over serial-->  2e8a:0003 RP2 Boot
                  --deploy.sh over ssh--->  firmware running

It disables every axis first, and near misses (BOOT, BOOTSE) are rejected as unknown commands rather than acted on. The remaining exposure is accepted deliberately: anything that can write to the port — including the host's gimbal raw passthrough — can take the gimbal out of service until it is re-flashed. That is judged the lesser risk against a guaranteed site visit for every firmware update, because entering the bootrom erases nothing and the board is recoverable remotely.

One physical BOOTSEL press is still needed once per board, to install the first image that carries the command.

One precondition was also missing entirely and is now in the setup kit: a udev rule for 2e8a, without which picotool finds the board and refuses it with "unable to connect. Maybe try 'sudo'".

Original research, 2026-09-11

⚠️ Remote flashing — researched 2026-09-11; workable, still to be proven on the board

The Leonardo is flashed over USB from the tower host (firmware/deploy.sh) via the 1200-baud-touch reset. The RP2040's documented flow is hold BOOTSEL, tap RST, drag a .uf2 — a person standing at the board. On a remote fire tower that is a serious regression.

Two independent paths exist, and the first is the same mechanism the Leonardo already uses:

  1. 1200-baud touch — the Arduino standard (1200 bps on the CDC port = reset to bootloader). Arduino-Pico's upload tool issues the kick, waits for the board to re-enumerate as the RPI-RP2 mass-storage device, and copies the .uf2 across. Requires the running firmware to present a USB CDC port — which ours does, since that is the host link.
  2. picotool reboot -f -u — forces a running board into BOOTSEL over USB. Also requires USB-CDC enabled in the running image.

What still has to be checked on the real board, because the docs do not settle it:

  • Arduino-Pico's PlatformIO integration documents neither its default upload protocol nor whether it performs the 1200-baud touch automatically, so firmware/deploy.sh may need an explicit upload_protocol, or a picotool reboot -f -u step before pio run --target upload.
  • There are user reports of "No accessible RP2040 devices in BOOTSEL mode were found", so the auto-reset is not bulletproof out of the box.

The failure modes are asymmetric, and that shapes the firmware:

  • An interrupted or failed flash self-recovers — the RP2040 bootrom falls back to USB mass storage when flash holds no valid image, so the board returns as RPI-RP2 by itself.
  • A valid but broken image that never brings up USB CDC is unrecoverable remotely. Neither path can reach it; only the physical BOOTSEL button can. That is the one that costs a site visit.

Hence the design rule for the port: bring Serial (USB CDC) up as early and unconditionally as possible — before any hardware init that could hang, in particular before the SPI/TMC bring-up. A watchdog that resets the MCU if the main loop stalls earns its place for the same reason.

3. Electrical — the 5 V → 3.3 V move is nearly free

The RP2040 is not 5 V tolerant, which sounds expensive. It isn't here, because the MCU's only external connection is SPI + enable into the TMC5160-BOB, and:

  • TMC5160-BOB pin 1 VCC_IO accepts 3.3 V or 5 V (COMPONENTS.md §3). Move it from the 5 V rail to the IOTA's 3.3 V (header pin 17, 1.2 A budget). Every BOB logic pin then references 3.3 V.
  • AS5047P has a documented 3.3 V mode (tie VDD/VDD3V3, supply 3.0–3.6 V — COMPONENTS.md § encoder). It feeds the TMC's ENCA/ENCB/ENCN, not the MCU, so it only needs to match VCC_IO.
  • Pitch endstops go to REFL/REFR on the BOB and follow VCC_IO automatically.

So: move the whole logic domain to 3.3 V; no level shifters anywhere. Motor supply VS (9–36 V) is untouched. Budget the 3.3 V rail: 2 × BOB VCC_IO + 2 × AS5047P is tens of mA against 1.2 A — ample.

4. The three orphaned peripherals

(a) IMU — Xsens MTi, today on /dev/ttyS0

The IOTA header does expose a PCH serial on pins 3/4, documented as COM1 / ttyS0, 3.3 V. If that enumerates as ttyS0 the config value doesn't even change.

Option Notes
Header COM1 + MAX3232 transceiver (preferred) Keeps [IMU] device = /dev/ttyS0, costs no USB port. Needed if the MTi is true RS-232 levels; not needed if it is 3.3 V TTL
USB↔RS-232 adapter Zero doubt, works today, but burns one of three USB ports and changes the device path to a by-id node

Action: confirm the MTi model's signalling levels, and confirm the header enumerates as ttyS0 (BIOS may gate it).

(b) Ambient sensor — SHT41, today on /dev/i2c-2

The published IOTA pinout exposes no PCH-side I2C — the header's only PCH serial is COM1; all I2C is RP2040-side. [VERIFY against the schematic], because this determines the work.

Option Cost
USB-I2C bridge (MCP2221A) (preferred for first deployment) Mainline hid-mcp2221 registers a real /dev/i2c-* adapter. Zero code change — just repoint [Env] i2c_device. Costs a USB port
Rehome to the RP2040 (I2C0 on GP12/GP13) Most integrated, frees the USB port, but needs firmware + a serial-protocol field + a new IEnvSensor implementation. Good follow-up, wrong thing to do during a board bring-up

Either way, test hardware env (config plan §11) should assert the bus is a real I2C adapter, not SMBus-only.

(c) 5G modem — Quectel RM520N-GL, M.2 B-key

The IOTA has no M.2 B-key slot. This is the least-solved item.

Option Notes
USB-attached M.2 B-key enclosure RM520N-GL supports USB 3.0 mode; keeps the existing modem and SIM. Costs a USB port and adds a box
External 5G/LTE router, IOTA on Ethernet Cleanest separation, modem no longer the host's problem; changes the tower's network design and BOM
PCIe FPC Not viable — the only FPC adapter is M-key, and that lane is already the NVMe path. One PCIe 3.0 ×1 lane, one device

USB port arithmetic: 3 ports. Camera (1) + modem enclosure (1) + USB-I2C bridge (1) = full, with the ASIX USB-NIC and any USB-RS232 adapter unplaceable. An external router plus the header COM1 for the IMU is the combination that fits. Decide this before ordering.

5. Camera, USB and performance — re-measure, don't assume

Known-issue #19 and the acquisition work were characterised on the Delta's Jasper Lake xHCI. The N150 is a different controller; the 20 MB-frame ceiling is a host finding and must be re-measured.

  • Re-apply the preconditions: usbfs_memory_mb=1000 (kernel cmdline) and USB3 LPM off via a udev rule on idVendor 1ab2. Fold both into hardware/lattepanda-iota/preconditions/ (config plan §8) so they survive reboots by construction.
  • Give the Alvium a port not shared with the modem or storage; re-run the frame-size/pacing sweep.
  • Benchmark JPEG XL encode at the production frame size. The N150 should beat the N5105 comfortably, but encode throughput sets the capture rate — measure it, don't infer it.

6. Storage, power, enclosure

  • Storage: the Delta is at 67 % of 57 GB. Take the 128 GB IOTA, and budget the DFR1250 FPC→M.2 M-key adapter + NVMe (~810 MB/s measured) if images are retained on the tower. Note this consumes the PCIe lane — see B.4(c).
  • Power: PH2.0 4-pin, 10–15 V DC — matches the existing 12 V rail. USB-C is power input only.
  • Thermal: ~4 W idle, ~36 W peak. Settles ~70 °C at 2.2–2.3 GHz under sustained all-core load with the active cooler. Consider the 6 W TDP mode for a sealed enclosure; the stock cooler is loud but that is irrelevant on a tower.
  • Mounting: 88 × 70 mm vs the Delta's 125 × 78 mm — new bracket and harness lengths.

Sequencing

Phase Work Gate
0 Buy one IOTA. Answer the 7 open questions in the datasheet §8 — especially PCH I2C, COM1 levels/enumeration, and remote RP2040 flashing Any "no" here changes the BOM or the schedule
1 Prerequisite: hardware-config-plan.md phases 1–2 — hardware.ini, the firmware config handshake, presence reconciliation and test hardware. Delivered against the Delta fleet first test hardware green on every existing tower
2 Firmware port: platform_*.h, pins + NUM_AXES generated from hardware.ini, [env:iota_rp2040]; native tests stay green Bench RP2040 + one TMC5160-BOB at 3.3 V: IOIN reads version=0x30, ENABLE/MOVE work under USE DEFAULTS
3 Bench-integrate the full gimbal on the IOTA: both axes, HOME, DIAG → PASS. Re-measure the yaw free-running encoder gap and set enc_deviation from it; walk homing_speed down from a deliberately gentle value Firmware bring-up checklist in firmware/docs/hardware-notes.md passes; homing repeatable with no audible grind; gimbal heading setup run and saved
4 Host: Ubuntu 24.04, Vimba X, preconditions, peripherals rehomed, test hardware green Full --init --start sweep with real camera + motors
5 Performance: frame-size/pacing sweep, JPEG XL encode benchmark vs the Delta Meets or beats the Delta's rate
6 Field swap on one tower, Delta kept as rollback 72 h clean run

Phases 1 and 2 are independent and can run in parallel. Phase 1 has standalone value even if the IOTA is rejected — it fixes the deployment.md drift and automates known-issue #19.


Progress

Firmware branch worktree-rp2040-platform (worktree at firmware/.claude/worktrees/rp2040-platform), commit aab1925.

Phase Item State
0 Remote-flash question researched — two viable paths, design rule derived (§2) done, and now measured on the board: neither works (§2). First flash needs the MicroPython REPL or BOOTSEL; remote update is unsolved pending a BOOTSEL command
2 Phase 2 gate: RP2040 + TMC5160-BOB at 3.3 V PASSED on tower 121, 2026-09-15: IOIN Y=0x3000001E version=0x30, encoder reading (X_ENC), one axis, speed=3000
2 src/platform.h — reset cause and free RAM behind one interface per MCU family done
2 Serial.begin() moved ahead of all hardware init, so a wedged image stays flashable done
2 [env:iota_rp2040] PlatformIO target (arduino-pico core, 8 MB flash) done and built on tower 121, 2026-09-15: 49452 B flash (0.6 % of 8 MB), 9068 B RAM. leonardo still builds (19254 B) and the 22 native tests still pass
2 Per-board pin map + NUM_AXES in platform_leonardo.h / platform_iota.h, as-built wiring done — compile-time, not yet generated from hardware.ini
2 Pins + NUM_AXES generated from hardware.ini not started — needs the config plan's phase 2 first
1 Everything else blocked on hardware-config-plan.md

Not compile-verified. PlatformIO is not installed on the dev box and the firmware is built remotely on the tower by firmware/deploy.sh. What was verified locally: both branches of platform.h syntax-check against stub headers — the AVR branch under gnu++11 (what the Arduino AVR core uses) and the RP2040 branch under gnu++17 (what arduino-pico uses) — and an unrecognised architecture fails on the #error rather than compiling silently.

To actually build it: cd firmware && ./deploy.sh --compile-only compiles on the device without flashing. That rsyncs over the tower's firmware source tree, so it is a deliberate step, not one to take by surprise.


Decisions needed before work starts

Configuration decisions live in hardware-config-plan.md; these are the ones specific to the hardware.

  1. Is the IOTA replacing the Delta fleet-wide, or becoming a second supported platform? Either way the configuration work stands alone and is worth doing first.
  2. Modem strategy — external router (recommended; it also resolves the USB-port squeeze) vs USB M.2 enclosure. A BOM/network decision, not a software one.
  3. SHT41 — USB-I2C bridge now (zero code) vs rehome to the RP2040 (cleaner, later). Recommendation: bridge now, rehome as a follow-up.
  4. Remote firmware flashing — researched: two viable paths, both requiring the running image to present USB CDC (§2). Phase 0 still has to prove one end-to-end on the board, and the firmware must bring Serial up before anything that can hang.
  5. Confirm the new tower's drivetrain figures — steps_per_rev, global_scaler and the rated motor current for the new yaw motor and gear ratio. Everything in §2 and phase 3 depends on them.