Week 16 — System integration
This week aligns with Fab Academy guidance on system integration (see the 2026 Assignments and Assessment hub and the 2025 module text for System integration → learning outcomes): how separate subsystems in a final project gain power, signal paths, mounting, and clear ownership of firmware roles. For my individual assignment I stepped back from glossy packaging and documented a real merge problem: the ILI934-class display from Week 15 needs more pins and bus attention than my XIAO ESP32‑S3 happily offers while I keep other peripherals free, so the ESP32‑WROOM module continues to anchor the TFT. Separately, NanoStat impedance readout sits on an ESP32 Pico footprint for now—which means sooner or later I need a reliable MCU-to-MCU link. I used a breadboard I²C loop (XIAO as master → WROOM as slave) driven from Cursor-assisted sketches, but flashed with the Arduino IDE, because I wanted one controlled variable at a time ( hardware + protocol sanity before tightening the mechanical envelope). In parallel, ASRPRO / Tianwen carries the offline voice UI for the Forest Spirit plant companion: a fixed Mandarin phrase table, a three-byte I²C mailbox toward the ESP host, and a Cursor-assisted firmware pass when the block toolkit refused to finish 生成模型 (“generate model”) on my first try.
Individual assignment
1) Task and motivation
Integration weeks are rarely about a single flashy demo—they are where assumptions collide. Two constraints pushed me toward this experiment:
- Pin budget vs. TFT: The XIAO is attractive for footprint, but the display block wants SPI, reset/backlight housekeeping, optional touch I²C, and breathing room away from picky strapping pins (I chased that lesson hard in Week 15). Parking the heavy GPIO/SPI workload on WROOM keeps the UI path understandable.
- Sensor island: For the impedance front-end I still treat the Pico-class board as its own power/ground domain early in development. That isolates analog noise experiments but creates an explicit communication requirement back to whoever owns the UX (today: probing I²C; tomorrow: pinning down message format & error handling alongside packaging).
-
Voice MCU vs. host MCU: ASRPRO owns wake/command decoding inside the vendor runtime; my Week 15
UI stack should not pretend to be a speech recognizer. A narrow I²C mailbox keeps auditability:
one
snidper hit, plus UART traces while I shake out timing (see the ASRPRO subsection).
The concrete engineering goal for this weekly entry is modest but measurable: I²C characters exchanged between two Espressif-class chips on a solderless breadboard, reproducible wiring, firmware in-repo, with a terse failure log (pull-ups & pin translation) so future me cannot repeat the same mirage.
2) What I refreshed while wiring
I²C is a shared, open-drain bus. Masters and slaves only ever pull lines low;
highs come from pull-up resistors—either on-breakout defaults, purposeful resistors on the carrier,
or (as in my case) explicit 4.7 kΩ ladders to 3.3 V. When both sides are
naked module IO without those resistors, the scope looks sleepy and Wire.endTransmission() collapses
into NACK / timeout chatter long before any protocol nuance matters.
Arduino-ESP32 “GPIO numbers”≠“D# silk”: XIAO boards print Dx labels that fan out through the module pinmux. I assumed D4 → GPIO4 because the digits matched; reality on my unit maps the I²C test lines to GPIO 5 (SDA) and GPIO 6 (SCL) when referencing Seeed/XIAO documentation—exactly why I now photograph the silk + datasheet row whenever breadboarding.
3) Plan
Before touching the breadboard I sketched a first-version system map for 森之精灵 / Forest Spirit. Integration weeks are where that map meets pin counts, bus roles, and “who owns the UI stack”—so I kept the sketch visible while this week’s bench work stayed deliberately narrow (see the checklist at the end of this section).
v1 architecture — what each island owns
- ESP32‑S3 (XIAO) — host / orchestrator: My intent was to treat the S3 as the integration brain: it talks to the display stack over I²C, ingests voice events from ASRPRO on another I²C mailbox, samples environmental sensors, pulls impedance packets from the Pico/NanoStat slice, and forwards prompts to the cloud when dialogue needs more than offline phrases.
- ESP32‑WROOM — display & touch front-end: Week 15 already showed this module is a better home for the heavy SPI → ILI9341-class TFT workload. In v1 the WROOM also owns I²C → capacitive touch and renders whatever the S3 decides the plant companion should show. The S3 stays the policy layer; WROOM stays the pixels-and-touch layer.
- ASRPRO — offline voice UI: Microphone in, speaker out, wake/command decoding inside the Tianwen runtime. Recognized intents cross to the S3 as compact I²C events (not raw audio), so the host can trigger on-screen states or cloud dialogue without pretending to be a speech engine.
- Environmental sensors on the S3: DHT11 (temperature / humidity) plus a light sensor on a simple single-wire / GPIO path—enough context for “how is the room treating this plant?” before interpreting impedance traces.
- Pico + NanoStat — plant-side impedance: Impedance front-end on its own board, reporting structured readings back to the S3 over I²C so analog experiments stay isolated from the UI wiring mess.
- DeepSeek API + Tavily — cloud dialogue & lookup: When the companion needs open-ended conversation, the S3 would ship voice-derived prompts (plus local sensor context) to DeepSeek and stream replies back into the UI/voice loop. For calendar date, weather, and lightweight news/search I planned a Tavily integration so the robot is not stuck pretending it knows “today” without network access.
- Arduino UNO island (future motion): The sketch also reserves a separate UNO path for ultrasonic ranging, a PS2-style controller, and motor drive—not wired into the S3 hub in v1 on paper, but marked so holonomic motion does not get designed as an afterthought.
That v1 map is intentionally optimistic about how many buses one XIAO can master at once. This week I therefore scoped execution to one measurable link at a time—plain ESP‑to‑ESP I²C on the breadboard, plus the parallel ASRPRO mailbox thread documented in §5—before pretending the whole diagram is simultaneously stable.
This week’s bench checklist (kept deliberately narrow)
- Power both boards cleanly, tie GND, add >=4.7 kΩ pull-ups on SDA/SCL.
- Use the XIAO as I²C master transmitting chunked UTF‑8 payloads; compile in Cursor.
- Flash via the same Arduino IDE / esptool path I already trusted from earlier weeks.
-
Run WROOM slave firmware mirroring common ESP32 patterns:
onReceive → ring buffer → loop → Serial.print, never blocking inside the ISR (Serialin IRQ context is bait for deadlock).
4) Build & bring-up diary
D4/D5.
Mistake A — forgetting pull-ups: Everything compiled, flashing succeeded, Serial showed my
master banner, yet endTransmission() returned NACK/error codes. Only after
metering the highs did I admit the lines were drifting instead of snapping up between edges. Discrete
4.7 kΩ tied to 3.3 V brought textbook rise times back.
Mistake B — swapped GPIO assumption: Even with healthy pull-ups, I chased ghosts until I
accepted that XIAO D4 ⇒ GPIO 5 and D5 ⇒ GPIO 6 for the pins I routed
to the WROOM GPIO19 (SDA)/GPIO21 (SCL) pair documented in the sketch comments.
Updating the Wire.begin() pins ended the hallucination instantly.
5) ASRPRO (天问) voice mailbox — parallel integration thread
The breadboard I²C exercise above is intentionally vanilla ESP‑IDF / Arduino Wire. The voice
path for 森之精灵 / Forest Spirit is different: it runs on an ASRPRO module with
the Tianwen (天问) graphical toolchain—offline wake + command slots, vendor audio resources
(playid), and hooks that only make sense once you accept their asr.h /
setup.h contract instead of pretending it is a normal setup()/loop() sketch.
Why I cared this week: integration is not only wires—it is also who owns which API.
I needed one deterministic bridge from “chip heard phrase n” to “host MCU can act on it,” without stuffing
a full voice stack into the same firmware that already juggles display and sensors. A tiny I²C mailbox
(flag + 16‑bit snid) keeps that boundary explicit; details are in
DATA_FLOW.md.
What went wrong first: inside Tianwen’s block flow, hitting 生成模型 sat spinning for a long time and never returned a usable build. Rather than burning another evening on the Cloud-only step, I exported the vendor C++ example, dropped it into Cursor, and asked an agent to learn the template and merge in my phrase map, UART banner, mute/listen timing, and the mailbox publisher. Once the source looked like a coherent ASRPRO project again, the desktop compile path behaved; the block page stayed my place to define phrases and IDs, not the bottleneck for every iteration.
Tianwen pipe — three things that stalled me mid-debug
Compile noise from Cursor snippets: When I pasted AI-generated patches into the Tianwen C++ branch, the first passes did not compile: stray punctuation—extra commas, bogus preprocessor tokens, and other junk the model had inferred around Chinese comments—showed up as hard errors rather than benign warnings. I treated those lines like diff noise: compare against the toolchain error list, strip the offending characters, and re-compile until only real semantics were left behind.
Could not bind ASRPRO as I²C slave in Tianwen: I initially wanted ASRPRO to behave as an I²C peripheral so the ESP host could clock out the mailbox. In practice the Tianwen GUI never gave me a workable “ASRPRO as slave device” configuration for that topology, so I pivoted the bus hierarchy: ASRPRO acts as master on the dedicated voice mailbox I²C, while the Seeed XIAO ESP32‑S3 listens as peripheral on that pair. Separately—this is why I harp on isolation—the XIAO still masters its own TFT / UI peripherals on a second logical I²C path (different SDA/SCL pins and routing). Same chip, two roles across two wires: mailbox side is slave to ASRPRO; presentation side stays an ESP‑side master toward the ILI934‑class stack documented in Week 15. That decision forced an explicit rewiring session so I wasn’t multiplexing contention on one bus alias.
生成模型 (generate model) vs. trusting the exemplar: The cloud/blocked toolkit step stalled my first “happy path”; the fix that stuck was pairing the earlier punctuation cleanup with a deliberate study-the-official-sample workflow in Cursor (figures below)—having the agent internalize vendor entry points until the merged source structurally matched the reference tree and built locally. Same three‑byte semantics on the wire; iteration speed recovered once I stopped treating the spinner as the authoritative compile gate.
ASR_CODE(), hardware_init()) and my mailbox plan. This was faster than waiting on a
stuck cloud model step when I still had to verify timings on hardware.
Firmware behavior (test build):
ASR_CODE() runs on each hit: vol_set mutes the DAC briefly,
set_state_enter_wakeup opens a 5 s listen window, the mailbox publishes
snid (wake = 0, commands ≥ 1), and Serial at
115200 8N1 prints both the mapped Chinese phrase and the three mailbox bytes for cross-checks.
Phrase text is not open microphone transcription—it is the offline table mirrored in
snid_to_phrase().
Artifacts:
asrpro_forest_spirit_voice_test.cpp
(project-local copy for documentation—drop into your Tianwen ASRPRO tree where asr.h lives) and
DATA_FLOW.md for the host-facing summary.
6) Reproduce this bench (minimal table)
| Signal | XIAO ESP32‑S3 (master) | ESP32‑WROOM (slave) |
|---|---|---|
| SDA | GPIO 5 (my routing from D4 silk) | GPIO 19 |
| SCL | GPIO 6 (my routing from D5 silk) | GPIO 21 |
| References | Common GND; SDA/SCL pulled to 3.3 V with ~4.7 kΩ; logic at 3.3 V only. | |
| Parameters | Slave address 0x55; start near 100 kHz; master uses chunked writes plus a generous Wire timeout guard for clock stretching. | |
7) Source sketches in-repo
- i2c_master_esp32s3.ino — ESP32‑S3 master broadcaster + scanner.
- i2c_slave_wroom.ino — WROOM ISR-safe ring-buffer slave.
-
asrpro_forest_spirit_voice_test.cpp —
ASRPRO/Tianwen C++ voice test (I²C mailbox
0x56, UART traces, Forest Spirit phrase map). - DATA_FLOW.md — short host-facing description of the mailbox bytes.
Libraries/tooling: Arduino-ESP32 core, built-in Wire helper. Flashing/upload:
Arduino IDE board profiles for ESP32-S3 Dev Module and a stock Espressif dev-module
target for WROOM.
8) Conclusion & next integration hooks
Packaging is still explicitly unfinished—Fab Academy explicitly allows logging integration work before enclosure glamour shots land. Locally I proved that MCU islands cooperate once both I²C pull-ups and the XIAO GPIO numbering trap are respected. That gives my NanoStat Pico slice a plausible neighbor bus when I merge impedance packets back toward the TFT stack.
On the voice side, the ASRPRO mailbox is the complementary lesson: sometimes integration means
respecting a vendor runtime and carving out a narrow protocol (three bytes + UART debug) instead
of rewriting everything on the host. When Tianwen would not expose the ASRPRO peripheral binding I assumed, flipping
to ASRPRO master → XIAO peripheral on a dedicated mailbox bus—while leaving the TFT path on XIAO’s
separate I²C master instance—proved easier than fighting the GUI. Next I need to harden the ESP reader/listener logic
against missed transactions and line up the snid map with whatever UI states
final-project.html actually exposes on screen.
The next mechanical pass will cover real strain relief, disciplined ground returns,
and I²C routing once the breadboard disappears—those bullets feed directly back into final-project.html packaging notes whenever the CAD stabilizes.