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
+121
-26
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.
|
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
|
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
|
peripheral in 8-bit parallel (LCD) mode streams it by DMA from a small ring of encoded
|
||||||
CPU is free while the panel refreshes.
|
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
|
**Status:** runs on hardware with a correct, tear-free image (drawing goes into a back
|
||||||
run on hardware: the panel shows a correct image (VCLK 5 MHz, HS and VS checked with
|
buffer that is swapped in at the start of a frame). Usable three ways: as an Arduino /
|
||||||
the ESP32's PCNT). Updates are not yet tear-free.
|
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
|
## 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
|
- 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)
|
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)
|
- 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
|
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.
|
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).
|
The pin map lives in [include/pins.h](include/pins.h).
|
||||||
|
|
||||||
## Timing
|
## 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
|
platformio.ini build config: envs still_alive (default) and esp32dev
|
||||||
include/pins.h pin map
|
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/still_alive/ Portal-style end-credits demo (env still_alive)
|
||||||
src/test_images/ test sketch: cycles through 12 test images (env esp32dev)
|
src/test_images/ test sketch: cycles through 12 test images (env esp32dev)
|
||||||
lib/EL512/ local library
|
esphome/ el512-panel.yaml (Home Assistant display), el512-smoke.yaml
|
||||||
src/EL512Panel.* framebuffer, row encoder, I2S DMA refresh
|
home_assistant/packages/ el512_panel.yaml: helper sensors for the ESPHome pages
|
||||||
src/EL512_GFX.* Adafruit_GFX subclass
|
|
||||||
tools/ import_still_alive.py
|
tools/ import_still_alive.py
|
||||||
test/ unit tests (placeholder)
|
test/ unit tests (placeholder)
|
||||||
docs/ datasheet and notes
|
docs/ datasheet and notes
|
||||||
@@ -89,23 +105,77 @@ pio run -e esp32dev -t upload # test images
|
|||||||
pio device monitor
|
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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
A recreation of Portal's ending: timed song text typed into a console on the
|
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
|
left, credits scrolling top right, ASCII art bottom right. The same show runs as the
|
||||||
and credits in `src/still_alive/imported.h` come from
|
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
|
[SDSkyKlouD/still-alive-web](https://github.com/SDSkyKlouD/still-alive-web) and can
|
||||||
be regenerated with:
|
be regenerated with:
|
||||||
|
|
||||||
```bash
|
```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
|
pio run -e still_alive -t upload
|
||||||
```
|
```
|
||||||
|
|
||||||
Without `imported.h` the demo plays placeholder text. Reset the board and start the
|
Start the song when the serial log says: 7.75 s after reset in the PlatformIO demo,
|
||||||
song when the serial log says (7.75 s after reset with the imported data).
|
or 7.75 s after the Still Alive page is shown in ESPHome.
|
||||||
|
|
||||||
**Disclaimer:** "Still Alive" was written by Jonathan Coulton for Portal (2007).
|
**Disclaimer:** "Still Alive" was written by Jonathan Coulton for Portal (2007).
|
||||||
Portal, its credits and ASCII art are © Valve Corporation (see
|
Portal, its credits and ASCII art are © Valve Corporation (see
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include <driver/gpio.h>
|
#include <driver/gpio.h>
|
||||||
#include <esp_attr.h>
|
#include <esp_attr.h>
|
||||||
#include <esp_cpu.h>
|
#include <esp_idf_version.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <esp_intr_alloc.h>
|
#include <esp_intr_alloc.h>
|
||||||
#include <esp_rom_gpio.h>
|
#include <esp_rom_gpio.h>
|
||||||
@@ -19,6 +19,14 @@
|
|||||||
#else
|
#else
|
||||||
#include <driver/periph_ctrl.h>
|
#include <driver/periph_ctrl.h>
|
||||||
#endif
|
#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>)
|
#if __has_include(<esp32/rom/lldesc.h>)
|
||||||
#include <esp32/rom/lldesc.h>
|
#include <esp32/rom/lldesc.h>
|
||||||
#else
|
#else
|
||||||
@@ -227,7 +235,7 @@ void IRAM_ATTR EL512Panel::fillChunk(uint16_t chunk) {
|
|||||||
|
|
||||||
void IRAM_ATTR EL512Panel::isr(void* arg) {
|
void IRAM_ATTR EL512Panel::isr(void* arg) {
|
||||||
auto* self = static_cast<EL512Panel*>(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;
|
if (self->_lastIsrCycle != 0 && now - self->_lastIsrCycle > self->_maxGap) self->_maxGap = now - self->_lastIsrCycle;
|
||||||
self->_lastIsrCycle = now;
|
self->_lastIsrCycle = now;
|
||||||
self->_refills = self->_refills + 1;
|
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,
|
`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`.
|
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 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
|
- Ring: 8 descriptors of 4 lines (2176 bytes each, 17 KB total), looped, `eof` on
|
||||||
allocated as 38 chunks of up to 7 lines (3808 bytes), one descriptor each, looped;
|
every descriptor. The EOF interrupt (IRAM, so it keeps running during flash writes)
|
||||||
`eof` set once per frame.
|
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
|
## 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
|
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).
|
line at x = 0 lands at x = 4).
|
||||||
- [ ] Confirm the column parity: the "checkerboard" and "VID/TVID columns" test images
|
- [ ] 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.
|
first column.
|
||||||
- [ ] Add the 5 V buffer (74HCT245/74AHCT541) and a pull-down on ENABLE so the panel
|
- [ ] 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
|
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:
|
Software:
|
||||||
|
|
||||||
- [ ] Tear-free updates: `show()` rewrites the stream while DMA reads it. Encode into
|
- [x] Tear-free updates: back buffer copied to the displayed buffer at line 0.
|
||||||
a second buffer and swap descriptors at the per-frame EOF, or encode only during
|
- [x] Memory: the full-frame stream (142 KB) is replaced by a 17 KB ring; ~190 KB
|
||||||
vertical blanking.
|
internal heap is left with Wi-Fi, the API and the ESPHome pages running.
|
||||||
- [ ] Unit test for `encodeRow()` (see `test/README`).
|
- [ ] 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