




ESP32-S3 driving a 128×64 HUB75 panel for generative patterns, with 4 knobs — open source
I've been building this for about two months and recently open-sourced everything. Patternflow is a custom ESP32-S3 board driving a 128×64 HUB75 LED matrix with 4 rotary encoders. Each pattern runs natively on the chip and the knobs map to pattern parameters in real time.
For context: I'm a visual design student, not an EE. Most of what I know about embedded came from this project, so this post leans toward the stories I actually lived through rather than firmware internals I'd struggle to defend.
Hardware
- ESP32-S3-WROOM-1 N16R8 (16MB flash, 8MB PSRAM)
- 128×64 HUB75 RGB LED matrix, driven by mrcodetastic/ESP32-HUB75-MatrixPanel-DMA
- 4 rotary encoders with push-buttons
- USB-C, single PCB
- Arduino IDE + ESP32
- KiCad sources in the repo
- 3D models(STL), Blender file in the repo
Cold boot bug
Most useful thing I learned. The board wouldn't reliably boot on first power-up; needed a manual reset or a power cycle. Symptoms were confusing:
- Quick reconnect (<1 min from unplug) → boots fine
- Long unplug (3+ min) → fails to start firmware
- Reset button → always works
Spent close to two weeks chasing this, mostly down wrong paths (slowing EN rise time, removing a 1000µF bulk cap that turned out to be a USB inrush violation but unrelated to the boot bug, pullup on HUB75 OE).
Actual root cause: GPIO0 was left NC in my original pinout. It's a strapping pin sampled at power-on — HIGH = normal boot, LOW = serial bootloader. With nothing on it externally, residual charge held it HIGH on quick reconnects (firmware ran). Long unplugs let it discharge into indeterminate territory, sometimes reading LOW on cold start → ESP entered bootloader → firmware never ran. Reset re-samples strapping pins under different conditions, so reset always worked.
Fix: 10kΩ pullup from GPIO0 to 3V3. Per the S3 datasheet, GPIO 0, 3, 45, 46 are all strapping pins and shouldn't be left floating.
Failed remote-streaming attempt
Tried building a mode where shaders authored in the browser (WebGL2/GLSL) are streamed live to the ESP32 and rendered on the panel — no firmware rebuild, and patterns too heavy for the chip become viable.
Data path: Browser WebGL2 → readPixels → chunked over WebSocket → ESP32 triple buffer → render task on Core 1 → HUB75.
Stalled at 13–17 fps with visible tearing and occasional brief blackouts.
Bottleneck identified: per-pixel drawPixel calls — ~70 ms just to call it 8192 times. The HUB75 library author has acknowledged in discussion #117 that per-pixel calls cap well below 30 fps on this stack, and the only paths forward are writing directly into the BCM bit-plane buffer or forking the driver.
Shelved for now. If anyone here has actually pushed HUB75 past the per-pixel call ceiling — BCM bit-plane writes, library fork, alternative driver — I'd really like to hear how.
Links
- Website: https://patternflow.work/
- GitHub: https://github.com/engmung/PatternFlow
- build guide: https://github.com/engmung/PatternFlow/blob/main/docs/BUILD.md
- More story: https://patternflow.work/journal/en
Happy to talk about the hardware, debugging, the remote-streaming attempt, anything.