From 6d5e5315f0b3a39a90fbc8ba1f74eadbf0e2ddd7 Mon Sep 17 00:00:00 2001 From: MangaValk Date: Sun, 18 Oct 2026 21:05:06 +0200 Subject: [PATCH] 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 --- README.md | 112 +++++++++++++++++++++++++------ components/el512/el512_panel.cpp | 12 +++- docs/README.md | 37 +++++++--- 3 files changed, 128 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index defb923..307eab8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -66,16 +76,22 @@ truth; if anything here disagrees with it, the datasheet wins. ## Layout ``` -platformio.ini build config: envs still_alive (default) and esp32dev -include/pins.h pin map -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 -tools/ import_still_alive.py -test/ unit tests (placeholder) -docs/ datasheet and notes +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) +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 ``` ## Build and flash @@ -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 diff --git a/components/el512/el512_panel.cpp b/components/el512/el512_panel.cpp index b162cf4..356f267 100644 --- a/components/el512/el512_panel.cpp +++ b/components/el512/el512_panel.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include @@ -19,6 +19,14 @@ #else #include #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 +#define EL512_CYCLES() esp_cpu_get_cycle_count() +#else +#include +#define EL512_CYCLES() cpu_hal_get_cycle_count() +#endif #if __has_include() #include #else @@ -227,7 +235,7 @@ void IRAM_ATTR EL512Panel::fillChunk(uint16_t chunk) { void IRAM_ATTR EL512Panel::isr(void* arg) { auto* self = static_cast(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; diff --git a/docs/README.md b/docs/README.md index 0d73aa5..ba4b310 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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.