335 lines
20 KiB
Markdown
335 lines
20 KiB
Markdown
# 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`](lattepanda-iota.md).
|
||
|
||
> **Companion document:** [hardware-config-plan.md](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](hardware-config-plan.md)) 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](hardware-config-plan.md)). 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.
|
||
|
||
<details><summary>The originally proposed map, kept for the record</summary>
|
||
|
||
| 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 | — |
|
||
|
||
</details>
|
||
|
||
**Toolchain:**
|
||
```ini
|
||
[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'"*.
|
||
>
|
||
> <details><summary>Original research, 2026-09-11</summary>
|
||
>
|
||
> ### ⚠️ 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.
|
||
>
|
||
> </details>
|
||
|
||
## 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](hardware-config-plan.md)) 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](hardware-config-plan.md)) 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](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](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](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.
|