kid-arcade · part 2

The 7-Inch Upgrade: Same Kid's Toy, Serious Hardware

Part 1 was the $25 toy: one chip, one file, one screen. It’s still the one to build first. But once it earned its place on car rides, I wanted a couch version — bigger screen, nicer type, room for the visuals to breathe. That turned into a port to genuinely serious hardware, and the port taught me more about modern ESP32 development than the original build did.

This is the full technical story: the architecture, the gotchas that each cost an evening, and the workflow that let one person do a two-chip embedded port in spare hours.

The port started with exactly one sentence typed into a terminal, preserved verbatim in the project’s prompt log: “i have a new EP4 panel connected can you see if u can connect?” No plan, no spec. The board had arrived that day. Everything below unfolded from there.

The hardware

Elecrow CrowPanel Advanced 7“ ESP32-P4 HMI. This is not a $25 board — it’s closer to $60–70 — but the spec sheet reads like a small tablet:

Component Spec
MCU ESP32-P4 (RISC-V, up to 360 MHz), 16 MB flash, 32 MB PSRAM
Display 7“ IPS, 1024×600, MIPI DSI (2 lanes) — the interface real tablets use, not SPI
Touch GT911 capacitive over I2C
Wireless None on the P4 itself — an onboard ESP32-C6 co-processor provides Wi-Fi/BT over SDIO
Audio I2S amplifier + speaker
Storage SD card slot (SDMMC)
Backlight PWM-dimmable

Two things on that table change your life as a builder. Capacitive touch means the per-panel calibration ritual from part 1 simply vanishes — the panel knows where your finger is. And MIPI DSI means the display pipeline is a real one: high resolution, DMA-fed, no SPI bottleneck.

One thing complicates it: the P4 has no radio. Wireless lives on a separate ESP32-C6 chip running its own firmware. You are now maintaining a two-chip system whether you like it or not.

Day-one gotcha before any code: the board has two USB ports — one is the UART/flash port, one isn’t — and nothing on the silkscreen makes that obvious. If the board won’t take firmware, you’re probably in the wrong port. The vendor’s documentation ZIP (Elecrow publishes it per-product) is worth downloading before the board arrives; feeding those PDFs to the AI pair up front is what turned pinout archaeology into lookups.

The architecture shift

The 4-inch build was Arduino-flavored: PlatformIO + TFT_eSPI, hand-drawn pixels. None of that carries over. The P4 build is native ESP-IDF (Espressif’s own framework) with LVGL 9 for UI:

Layer v1 (4-inch) v2 (7-inch)
Framework Arduino-style / PlatformIO ESP-IDF 6 (still driven via PlatformIO)
Display TFT_eSPI over SPI esp_lcd MIPI DSI + EK79007 panel driver
Touch XPT2046 raw reads esp_lcd_touch_gt911 component
UI Hand-drawn pixels LVGL 9 via esp_lvgl_port
Wireless none ESP-Hosted + esp_wifi_remote over SDIO to the C6

In part 1, I argued against LVGL. Here I reversed, and both calls were right for their board: at 320×480 with 520 KB of RAM, a UI framework is overhead; at 1024×600 with 32 MB of PSRAM and animated screen transitions, hand-drawing pixels is masochism. Frameworks earn their cost with the pixel count.

The panel mounts portrait: physically 1024×600, rotated in software to 600×1024. LVGL 9 handles the rotation; your layout code just thinks in portrait.

What made the port cheap: the game logic — question generation, round structure, the star economy — is plain C++ that never knew what a display was. Only the draw layer changed. That separation, made almost accidentally in v1, is the single most transferable design decision in this whole project.

Living with two chips

The C6 co-processor needs its own ~1.2 MB firmware blob. My solution: bake it into the P4’s flash as a dedicated partition, and have the P4 auto-flash the C6 over SDIO at boot if needed:

nvs,      data, nvs,   0x9000,   0x6000
phy_init, data, phy,   0xF000,   0x1000
slave_fw, data, 0xff,  0x10000,  0x13E000   ← C6 firmware blob lives here
otadata,  data, ota,   0x14E000, 0x2000
app0,     app,  ota_0, 0x150000, 0x400000   ← running app
app1,     app,  ota_1, 0x550000, 0x400000   ← OTA staging slot

A single flash.sh builds and flashes both chips in one shot. Gotcha #1 lived here: the C6 auto-flash logic must skip when the slave partition is empty, or first boot wedges — and you want a --force flag for when the C6 firmware genuinely changed.

The joke writes itself: this toy is proudly offline, and I shipped a second chip’s entire networking stack to support it. (The Wi-Fi does earn its keep — clock sync, a weather widget on the home screen, and OTA updates below.)

The three gotchas, so you don’t pay for them

1. The PSRAM wallpaper trick. The home screen is a full-screen photo — 1.2 MB of RGB565 pixels compiled into flash. Rendering it straight from flash caused visible jitter on every redraw: LVGL kept re-reading flash through the XIP cache, evicting hot code. Fix: copy the image flash → PSRAM once at boot, and point LVGL at the PSRAM copy forever. Jitter gone. If your embedded UI stutters only when a background image is visible, this is why.

2. Dual SDMMC. The C6 radio link uses SDMMC slot 1; the SD card uses slot 0. Different pins, same host peripheral — and ESP-Hosted has already initialized that host by the time you mount the card. On ESP-IDF 6.x the SD mount needs a dummy host init/deinit dance to coexist. Symptom otherwise: SD mount fails only when Wi-Fi is enabled, which sends you debugging entirely the wrong subsystem.

3. Sound without an audio pipeline. No sound files, no codec: each feedback tone is a sine wave generated as PCM at runtime and pushed to I2S — correct answer is a rising E5→A5, wrong is a soft low G3, round-complete is a little arpeggio. Volume is just amplitude scaling, wired to a settings slider. The one rule: every audio_play() spawns a detached FreeRTOS task, so the UI thread never blocks on a speaker.

The features a big screen unlocked

  • OTA updates. Admin panel → enter laptop’s IP → the device pulls new firmware over HTTP in 4 KB chunks into the spare app partition and reboots into it. After this existed, the USB cable stopped leaving the drawer — a firmware tweak while the kid is using the toy takes two minutes.

  • SD-card question bank. The device reads /sdcard/questions.csv at boot and appends to the built-in questions — never replaces them. A missing or bad card costs you nothing; the toy always works. One CSV line per question:

    ADD,7,8,15,14,16,0        # 7+8: options 15/14/16, first is correct
    SKIP,6,6,12,18,24,30,36,0 # skip-count by 6
    MULT,7,8,56,48,63,0       # 7×8

    New school topic → edit a text file on a card. No recompile, no laptop.

  • The quiet stuff that makes it feel finished: NTP clock with a time-of-day greeting, a screensaver after 2 minutes idle (black screen, small white clock — an always-on display, effectively), brightness/volume sliders persisting to NVS, and the same 5-questions-stars-only game rules as v1. The product philosophy didn’t change with the hardware. Only the canvas did.

The workflow that made this possible solo

This port is two chips, five subsystems, and a display interface I’d never touched — done in evening sessions. The honest enabler was AI tooling, used as direct pair-programmers:

  • Claude Code in the terminal did the heavy lifting: MIPI DSI panel bring-up, the LVGL port, the SDMMC coexistence fix, the OTA implementation. The rhythm was tight loops — agent writes and builds, I flash and report what the screen actually shows. ESP-IDF errors are verbose and ancient-forum-flavored; an agent that has effectively read every forum thread turns “cryptic linker error” into “your partition table is 4 KB short” in seconds.
  • A second AI tool as reviewer, not builder. I periodically ran the diff through a different model (Codex, and lately Google’s Antigravity with its own models) for a cold read. Different models miss different things; a reviewer with no memory of writing the code is usefully ruthless. Cheap insurance on firmware that a kid will mash on.
  • Notes as the source of truth. Every gotcha above went into a project note the moment it was solved — pinouts, the partition table, why the dummy SDMMC init exists. Both the AI sessions and future-me read those notes before touching code. On a months-long spare-time project, the constraint isn’t typing speed; it’s re-loading context. Write it down once, and every later session — human or AI — starts warm.

The division of labor stayed the same as part 1: the tools wrote most of the code; every product decision and every minute of holding-the-device testing was mine. That balance is the part I’d tell anyone to copy.

Which one should you build?

Build the $25 one from part 1 first — the value-per-dollar is absurd and the whole thing is an afternoon. Build this one when you’ve caught the bug, want a couch device, and are ready for a two-chip system with a real display pipeline.

Both are in his hands now. He doesn’t know one cost three times the other. He knows the big one has sound and his knight wallpaper on it — and that both of them are his, in a way no tablet with someone else’s business model ever would be.