Fix PlatformIO build; document ESPHome component and ring-buffer driver

- el512_panel.cpp: esp_cpu_get_cycle_count() exists only in ESP-IDF 5;
  use cpu_hal_get_cycle_count() on 4.4 (Arduino 2.x), which broke both
  PlatformIO environments.
- README: ring-buffer refresh, tear-free status, D1 mini ESP32, new layout
  (components/el512, esphome/, home_assistant/), ESPHome setup, Still Alive
  paths, and the Wi-Fi blanking findings with wiring advice.
- docs/README: ring, lookup table, double buffer, health counters; tear-free
  and memory items done; Wi-Fi blanking and boot blanking as open items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-18 21:05:06 +02:00
1 parent f3212ea969
commit 6d5e5315f0
3 files changed
+121 -26

No files matched your search

+84 -14
View File
@@ -5,16 +5,19 @@ ESP-WROOM-32 (classic ESP32), with an [Adafruit GFX](https://github.com/adafruit
compatible driver so the usual drawing, text and font APIs work.
The panel has no frame memory and needs a continuous video stream. The ESP32's I2S0
peripheral in 8-bit parallel (LCD) mode streams a precomputed frame from DMA, so the
CPU is free while the panel refreshes.
peripheral in 8-bit parallel (LCD) mode streams it by DMA from a small ring of encoded
lines that an interrupt keeps refilling (17 KB instead of a 142 KB full frame), so
there is room left for Wi-Fi.
**Status:** framebuffer, GFX layer, row encoder and I2S/DMA refresh are implemented and
run on hardware: the panel shows a correct image (VCLK 5 MHz, HS and VS checked with
the ESP32's PCNT). Updates are not yet tear-free.
**Status:** runs on hardware with a correct, tear-free image (drawing goes into a back
buffer that is swapped in at the start of a frame). Usable three ways: as an Arduino /
PlatformIO library with Adafruit GFX, as an [ESPHome](https://esphome.io) display
component for Home Assistant, and as the stand-alone Still Alive demo.
## Hardware
- ESP-WROOM-32 dev board (`esp32dev`)
- ESP32 board: a Wemos D1 mini ESP32 is used here (`esp32dev` in PlatformIO,
`wemos_d1_mini32` in ESPHome); pinout in [docs/D1_mini_esp32_schema.jpg](docs/D1_mini_esp32_schema.jpg)
- Planar EL512.256-H, configured for **two-bit parallel mode**: jumper 2 (DCONFIG) of pin
strip PS1 removed, jumper 1 (SELFTST) fitted (datasheet Table 3)
- Datasheet: [docs/datasheet/EL512.256-H3 FRB.pdf](docs/datasheet/EL512.256-H3%20FRB.pdf)
@@ -46,6 +49,13 @@ the ESP32 is still booting and VCLK is not running yet; add a pull-down (for exa
10 kΩ) on the buffer's ENABLE input so the panel stays off until `begin()` starts the
stream. Keep the cable under 300 mm, or add 47 Ω series resistors for up to 2 m.
With Wi-Fi on, the panel can blank for a moment when the ESP32 transmits (seen at page
changes, which report the new page to Home Assistant). The refresh measured clean on
the ESP32 side at those moments, so the signal is disturbed on the wires: connect all
six J1 GND pins, run them along the signal wires, keep the antenna away from the
cable, and preferably use the 5 V buffer above. Brief blanking during boot is expected
until ENABLE has its pull-down.
The pin map lives in [include/pins.h](include/pins.h).
## Timing
@@ -68,11 +78,17 @@ truth; if anything here disagrees with it, the datasheet wins.
```
platformio.ini build config: envs still_alive (default) and esp32dev
include/pins.h pin map
components/el512/ shared driver + ESPHome component (one copy of the code)
el512_panel.* framebuffer, DMA ring, refresh interrupt (ESP-IDF API)
still_alive.* Portal end-credits show, 5x7 font (el512_font.h)
still_alive_data.h show content, generated by tools/import_still_alive.py
el512_display.* ESPHome display platform `el512`
__init__.py, display.py ESPHome config schema
lib/EL512/ Arduino library: Adafruit_GFX front end (EL512_GFX)
src/still_alive/ Portal-style end-credits demo (env still_alive)
src/test_images/ test sketch: cycles through 12 test images (env esp32dev)
lib/EL512/ local library
src/EL512Panel.* framebuffer, row encoder, I2S DMA refresh
src/EL512_GFX.* Adafruit_GFX subclass
esphome/ el512-panel.yaml (Home Assistant display), el512-smoke.yaml
home_assistant/packages/ el512_panel.yaml: helper sensors for the ESPHome pages
tools/ import_still_alive.py
test/ unit tests (placeholder)
docs/ datasheet and notes
@@ -89,23 +105,77 @@ pio run -e esp32dev -t upload # test images
pio device monitor
```
## ESPHome / Home Assistant
[esphome/el512-panel.yaml](esphome/el512-panel.yaml) turns the panel into a Home
Assistant display with eight pages: clock and weather, forecast, agenda, a sensor
dashboard with a message line, now playing, Plex, solar, and Still Alive. Pages
rotate on an interval; Home Assistant gets a page select, a pause switch, the
interval, a message text, a "Play Still Alive" button, and Display / Low power mode
switches (ENABLE, _LOWPWR).
The component comes straight from this repository:
```yaml
external_components:
- source:
type: git
url: https://mangavalk.com/mangavalk/planar-EL512.256-H.git
ref: main
components: [el512]
display:
- platform: el512
id: panel
enable_pin: GPIO25 # optional, J2-6
low_power_pin: GPIO26 # optional, J2-5
still_alive_page: page_still_alive # optional
pages: ...
```
Data pins default to the wiring above (`vclk_pin`, `vid_pin`, `tvid_pin`, `hs_pin`,
`vs_pin` can be changed). Page lambdas can use helpers such as `el512::split()`,
`el512::weather_icon()`, `el512::fit()` and `el512::watts()`
([el512_helpers.h](components/el512/el512_helpers.h)).
Setup:
1. Copy [home_assistant/packages/el512_panel.yaml](home_assistant/packages/el512_panel.yaml)
to `/config/packages/` (with `homeassistant: packages: !include_dir_named packages`
in `configuration.yaml`) and reload the template entities. It adds
`sensor.el512_forecast`, `sensor.el512_agenda`, `sensor.el512_now_playing` and
`sensor.el512_plex_streams`.
2. Put `el512-panel.yaml` in the ESPHome add-on and add `el512_api_key` and
`el512_ota_password` to its `secrets.yaml`. The entity IDs at the top
(`substitutions`) and in the sensor list are this installation's; adjust them.
3. Flash once over USB (or use ESPHome's web installer), then add the device in Home
Assistant; later updates go wirelessly from the add-on.
The component logs a warning when the ESP32 side of the refresh has a problem (DMA
FIFO underrun, late ring refill, swap timeout); otherwise a debug summary once a minute.
For local builds, `esphome/el512-panel-local.yaml` (git-ignored) is the same config
with `source: {type: local, path: ../components}`; build it with the ESPHome CLI from
PowerShell or cmd (the ESP-IDF installer refuses to run under Git Bash/MSYS).
## Still Alive demo
![Still Alive demo running on the EL512.256-H](docs/images/still_alive.jpg)
A recreation of Portal's ending: timed song text typed into a console on the
left, credits scrolling top right, ASCII art bottom right. The text, timing, art
and credits in `src/still_alive/imported.h` come from
left, credits scrolling top right, ASCII art bottom right. The same show runs as the
PlatformIO demo and as a page in ESPHome. The text, timing, art and credits in
[components/el512/still_alive_data.h](components/el512/still_alive_data.h) come from
[SDSkyKlouD/still-alive-web](https://github.com/SDSkyKlouD/still-alive-web) and can
be regenerated with:
```bash
python tools/import_still_alive.py
python tools/import_still_alive.py # stops after the credits text (--until-ms 200000)
pio run -e still_alive -t upload
```
Without `imported.h` the demo plays placeholder text. Reset the board and start the
song when the serial log says (7.75 s after reset with the imported data).
Start the song when the serial log says: 7.75 s after reset in the PlatformIO demo,
or 7.75 s after the Still Alive page is shown in ESPHome.
**Disclaimer:** "Still Alive" was written by Jonathan Coulton for Portal (2007).
Portal, its credits and ASCII art are © Valve Corporation (see
+10 -2
View File
@@ -2,7 +2,7 @@
#include <driver/gpio.h>
#include <esp_attr.h>
#include <esp_cpu.h>
#include <esp_idf_version.h>
#include <esp_heap_caps.h>
#include <esp_intr_alloc.h>
#include <esp_rom_gpio.h>
@@ -19,6 +19,14 @@
#else
#include <driver/periph_ctrl.h>
#endif
// CPU cycle counter: ESP-IDF 5 (ESPHome) and 4.4 (Arduino 2.x / PlatformIO).
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#include <esp_cpu.h>
#define EL512_CYCLES() esp_cpu_get_cycle_count()
#else
#include <hal/cpu_hal.h>
#define EL512_CYCLES() cpu_hal_get_cycle_count()
#endif
#if __has_include(<esp32/rom/lldesc.h>)
#include <esp32/rom/lldesc.h>
#else
@@ -227,7 +235,7 @@ void IRAM_ATTR EL512Panel::fillChunk(uint16_t chunk) {
void IRAM_ATTR EL512Panel::isr(void* arg) {
auto* self = static_cast<EL512Panel*>(arg);
const uint32_t now = esp_cpu_get_cycle_count();
const uint32_t now = EL512_CYCLES();
if (self->_lastIsrCycle != 0 && now - self->_lastIsrCycle > self->_maxGap) self->_maxGap = now - self->_lastIsrCycle;
self->_lastIsrCycle = now;
self->_refills = self->_refills + 1;
+27 -10
View File
@@ -26,9 +26,20 @@ I2S0 setup (`EL512Panel::startI2S()`), following bitluni's ESP32Lib 8-bit mode:
`tx_right_first`. With this setup each 32-bit DMA word goes out as bytes 2, 3, 0, 1,
so `encodeRow()` stores stream byte i at `i ^ 2`.
- Stream bits 0..3 -> `I2S0O_DATA_OUT16_IDX` + n; BCK -> VCLK (`INVERT_VCLK` flips it).
- Stream: 261 lines × 544 bytes ≈ 142 KB, too big for one DMA block, so it is
allocated as 38 chunks of up to 7 lines (3808 bytes), one descriptor each, looped;
`eof` set once per frame.
- Ring: 8 descriptors of 4 lines (2176 bytes each, 17 KB total), looped, `eof` on
every descriptor. The EOF interrupt (IRAM, so it keeps running during flash writes)
re-encodes the descriptor that just finished with the lines 32 later, so the ring
stays ~1.7 ms ahead of the panel. Each descriptor tracks its own first line, so a
missed interrupt costs one stale chunk instead of shifting the picture.
- Encoding: a 256-entry table turns one framebuffer byte (8 pixels) into its 8 stream
bytes, already in `^ 2` order; a line takes about a microsecond.
- Double buffer: drawing goes into `_fb`; `show()` asks the interrupt to copy it to
the displayed buffer just before it encodes line 0, and waits for that (≤ 1 frame).
- The driver uses ESP-IDF calls only and lives in `components/el512/`; PlatformIO
builds it from there as library `EL512Core` (`lib_extra_dirs`), ESPHome as the
`el512` external component.
- Health counters (`takeStats()`): refills, largest refill gap, swaps, swap timeouts,
TX FIFO underruns. With Wi-Fi on: ~4600 refills/s, max gap < 0.4 ms, no underruns.
## From the datasheet
@@ -84,17 +95,23 @@ Hardware checks:
does not show on HS; on the panel it shuffles pixels in groups of 8 (a 1-pixel
line at x = 0 lands at x = 4).
- [ ] Confirm the column parity: the "checkerboard" and "VID/TVID columns" test images
in `main.cpp` must look as drawn, and the left border must sit on the panel's
in `src/test_images/main.cpp` must look as drawn, and the left border must sit on the panel's
first column.
- [ ] Add the 5 V buffer (74HCT245/74AHCT541) and a pull-down on ENABLE so the panel
stays off while the ESP32 boots without VCLK (Table 1: ENABLE pulled high
internally; Table 5 note 3: VCLK should be kept running).
internally; Table 5 note 3: VCLK should be kept running). The panel blanks a few
times during boot until then.
- [ ] Wi-Fi blanking: with Wi-Fi on (RSSI -90 dB, so full transmit power) the panel
blanks for a moment now and then, mostly at page changes (each reports the new
page to Home Assistant). Gone with `wifi: enable_on_boot: false`; ENABLE/_LOWPWR
were not connected; the health counters stayed clean during 3 blinks in 2 min.
So the signal is disturbed on the wires, not late from the ESP32. Next: all six
J1 GND pins along the signal wires, antenna away from the cable, the 5 V buffer;
`output_power: 14dB` as a quick check.
Software:
- [ ] Tear-free updates: `show()` rewrites the stream while DMA reads it. Encode into
a second buffer and swap descriptors at the per-frame EOF, or encode only during
vertical blanking.
- [x] Tear-free updates: back buffer copied to the displayed buffer at line 0.
- [x] Memory: the full-frame stream (142 KB) is replaced by a 17 KB ring; ~190 KB
internal heap is left with Wi-Fi, the API and the ESPHome pages running.
- [ ] Unit test for `encodeRow()` (see `test/README`).
- [ ] The ~142 KB stream (in 38 chunks) leaves ~190 KB heap; with Wi-Fi on it may
not fit. Fallback: a line ring refilled from the EOF interrupt.