Public Access
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:
1 parent
f3212ea969
commit
6d5e5315f0
3 files changed
+128
-33
No files matched your search
@@ -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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
Reference in new issue
Block a user