Project check against the datasheet; fix column parity

The datasheet numbers columns from 1 (Fig. 3: "row 1", "Last pixel
(512 / 256)") and puts odd columns on TVID, so x = 0 (column 1) now
goes to TVID and x = 1 to VID; the encoder had them swapped.

README wiring now lists the J1/J2 pins and power rails from Table 1,
the 5 V buffer, and a pull-down on ENABLE so the panel stays off while
the ESP32 boots without VCLK. encodeRow() documents its T4/T5 margins.
docs/README.md open items are regrouped into done, hardware checks and
software work. main.cpp counts seconds, not frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-05 21:17:29 +02:00
1 parent 37d5bcc1f8
commit ab3e6ad0ac
6 files changed
+72 -31

No files matched your search

+23 -10
View File
@@ -22,16 +22,29 @@ yet tear-free.
## Wiring ## Wiring
| Panel signal | Function | ESP32 GPIO | DMA byte bit | | Panel signal | Panel pin (Table 1) | Function | ESP32 GPIO | DMA byte bit |
|--------------|-----------------------------------------|-----------:|-------------:| |--------------|---------------------|----------------------------------------------|-----------:|-------------:|
| VCLK | Video clock (rising edge) | 18 | I2S0 BCK | | VCLK | J1-13 | Video clock, data latched on rising edge | 18 | I2S0 BCK |
| VID | Pixel data, even columns | 19 | 0 | | VID | J1-15 | Pixel data, even columns (x = 1, 3, …) | 19 | 0 |
| TVID | Pixel data, odd columns (two-bit mode) | 21 | 1 | | TVID | J1-7 | Pixel data, odd columns (x = 0, 2, …) | 21 | 1 |
| HS | Horizontal sync | 22 | 2 | | HS | J1-11 | Horizontal sync | 22 | 2 |
| VS | Vertical sync | 23 | 3 | | VS | J1-9 | Vertical sync (rising edge used) | 23 | 3 |
| ENABLE | Display enable | 25 | — | | ENABLE | J2-6 | Display enable, low = off | 25 | — |
| _LOWPWR | Low-power mode (active low) | 26 | — | | _LOWPWR | J2-5 | Low-power mode, low = on | 26 | — |
| GND | Ground | GND | — | | GND | J1-6, 8, 10, 12, 14, 16 | Signal return | GND | — |
The datasheet numbers columns 1 to 512 from the left; x in the code is 0-based, so
x = 0 is column 1 (odd, TVID).
Power (Table 1, Table 4): Vcc1 +5 V logic on J1-3, 4 (4.75–5.25 V, 0.2 A max);
Vcc2 +11…+30 V on J1-1, 2 (0.9 A max at 12 V). J1-5 is reserved: do not connect.
The panel inputs are HCT compatible with internal pull-ups of unstated voltage, and the
ESP32 is not 5 V tolerant, so drive them through a 74HCT245 or 74AHCT541 powered from
5 V (see the plan). ENABLE is pulled high inside the panel, so the panel scans while
the ESP32 is still booting and VCLK is not running yet; add a pull-down (for example
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.
The pin map lives in [include/pins.h](include/pins.h). The pin map lives in [include/pins.h](include/pins.h).
+35 -10
View File
@@ -35,6 +35,8 @@ other doc, comment or the plan.
- Two-bit parallel mode is selected by removing jumper 2 (DCONFIG) of pin strip PS1 - Two-bit parallel mode is selected by removing jumper 2 (DCONFIG) of pin strip PS1
(Table 3; factory default has jumpers 1 and 2 fitted). Even columns on VID, odd (Table 3; factory default has jumpers 1 and 2 fitted). Even columns on VID, odd
columns on TVID, numbered from the left as seen by the viewer; 256 VCLKs per row. columns on TVID, numbered from the left as seen by the viewer; 256 VCLKs per row.
The datasheet numbers from 1 (Fig. 3: "row 1", "Last pixel (512 / 256)"), so the
leftmost pixel, x = 0 in the code, is column 1 and goes on TVID.
- VCLK is active on the rising edge and should be kept running. - VCLK is active on the rising edge and should be kept running.
- HS envelopes the pixels of a row: the visible pixels are the last 512 (two-bit mode: - HS envelopes the pixels of a row: the visible pixels are the last 512 (two-bit mode:
last 256 clocks) before HS falls. The number of VCLKs during HS high should be even. last 256 clocks) before HS falls. The number of VCLKs during HS high should be even.
@@ -49,14 +51,37 @@ other doc, comment or the plan.
## Open items ## Open items
- [x] Implement `EL512Panel::startI2S()` (I2S0 LCD mode, clock dividers for 5 MHz, Done:
- [x] Implement `EL512Panel::startI2S()` (I2S0 LCD mode, 5 MHz from PLL_D2 / 32,
GPIO matrix routing, circular `lldesc_t` chain). GPIO matrix routing, circular `lldesc_t` chain).
- [x] Handle the 8-bit LCD-mode byte-pair swap in `encodeRow()`. - [x] Handle the 8-bit LCD-mode byte-pair swap in `encodeRow()` (clock c at `c ^ 2`).
- [ ] Check on a scope: VCLK is 5 MHz on GPIO 18, HS is 256 clocks high / 16 low - [x] Map x = 0 (datasheet column 1, odd) to TVID, not VID.
(54.4 us period), VS is one line high per 14.2 ms frame, data changes on the
falling VCLK edge, VS/HS setup and hold (T4/T5). Unconfirmed until then: the Hardware checks (need the panel and a scope or logic analyser):
DATA_OUT16 base and the `^ 2` byte order. HS edges sit on word boundaries, so a
wrong order does not show on HS; on the panel it shuffles pixels in groups of 8 - [ ] Self test first (Table 3): remove PS1 jumper 1, power up; all pixels except the
(a 1-pixel line at x = 0 lands at x = 4). first half of the top row should light. Refit jumper 1, remove jumper 2.
- [ ] Tear-free updates (double buffer or swap at VS). - [ ] Scope: VCLK 5 MHz on GPIO 18; HS 256 clocks high / 16 low (54.4 µs, T6-T8);
- [ ] Unit test for `encodeRow()`. VS one line high per 14.2 ms frame (70.4 Hz); data changing on the falling VCLK
edge with >= 8 ns setup/hold (Table 6); T1 220.8 µs, T4/T5 as in `encodeRow()`.
If data changes on the rising edge, set `INVERT_VCLK`.
- [ ] Confirm the I2S0 data signal base (`I2S0O_DATA_OUT16_IDX`): if GPIO 19/21/22/23
stay idle, try `I2S0O_DATA_OUT8_IDX`.
- [ ] Confirm the `^ 2` byte order. HS edges sit on word boundaries, so a wrong order
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 in `main.cpp` must look identical to
the framebuffer, 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).
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.
- [ ] Unit test for `encodeRow()` (see `test/README`).
- [ ] The 71 KB stream is one contiguous DMA allocation; with Wi-Fi on it may not fit.
Fallback: per-line buffers or a line ring refilled from the EOF interrupt.
+7 -5
View File
@@ -118,8 +118,9 @@ void EL512Panel::clear(bool on) {
void EL512Panel::encodeRow(uint16_t line, uint8_t* out) const { void EL512Panel::encodeRow(uint16_t line, uint8_t* out) const {
// VS is held high during the last blanking line so that the first HS high // VS is held high during the last blanking line so that the first HS high
// time ending after its rising edge is row 0 (datasheet: video input timing). // time ending after its rising edge is row 0 (datasheet: Video Data Input).
// TODO: verify VS/HS setup and hold (T4/T5) on hardware. // VS rises 1104 clocks after the previous HS fall (T4 >= 1 VCLK) and 272
// clocks = 54.4 us before the next HS rise (T5 >= 3 us); check on a scope.
const uint8_t vs = (line == LINES_PER_FRAME - 1) ? BIT_VS : 0; const uint8_t vs = (line == LINES_PER_FRAME - 1) ? BIT_VS : 0;
if (line >= HEIGHT) { if (line >= HEIGHT) {
@@ -129,11 +130,12 @@ void EL512Panel::encodeRow(uint16_t line, uint8_t* out) const {
const uint8_t* src = _fb + (size_t)line * (WIDTH / 8); const uint8_t* src = _fb + (size_t)line * (WIDTH / 8);
for (uint16_t c = 0; c < DATA_CLOCKS_PER_ROW; c++) { for (uint16_t c = 0; c < DATA_CLOCKS_PER_ROW; c++) {
// Clock c carries columns 2c (VID, even) and 2c+1 (TVID, odd). // Clock c carries x = 2c and 2c+1. The datasheet numbers columns from 1, so
// x = 2c is column 2c+1 (odd -> TVID) and x = 2c+1 is column 2c+2 (even -> VID).
const uint8_t bits = (uint8_t)(src[c >> 2] << ((c & 3) * 2)); const uint8_t bits = (uint8_t)(src[c >> 2] << ((c & 3) * 2));
uint8_t v = BIT_HS | vs; uint8_t v = BIT_HS | vs;
if (bits & 0x80) v |= BIT_VID; if (bits & 0x80) v |= BIT_TVID;
if (bits & 0x40) v |= BIT_TVID; if (bits & 0x40) v |= BIT_VID;
// 8-bit I2S LCD mode sends each 32-bit word as bytes 2, 3, 0, 1. // 8-bit I2S LCD mode sends each 32-bit word as bytes 2, 3, 0, 1.
out[c ^ 2] = v; out[c ^ 2] = v;
} }
+2 -2
View File
@@ -4,8 +4,8 @@
// We drive it from I2S0 in 8-bit parallel (LCD) mode with a circular DMA // We drive it from I2S0 in 8-bit parallel (LCD) mode with a circular DMA
// descriptor chain, one byte per VCLK: // descriptor chain, one byte per VCLK:
// //
// byte bit 0 = VID (even columns) // byte bit 0 = VID (even columns 2..512 = x 1, 3, ..., 511)
// byte bit 1 = TVID (odd columns) // byte bit 1 = TVID (odd columns 1..511 = x 0, 2, ..., 510)
// byte bit 2 = HS (high while the 256 data clocks of a row are sent) // byte bit 2 = HS (high while the 256 data clocks of a row are sent)
// byte bit 3 = VS (marks the top row) // byte bit 3 = VS (marks the top row)
// I2S BCK -> VCLK (panel samples data on the rising edge) // I2S BCK -> VCLK (panel samples data on the rising edge)
+2 -2
View File
@@ -52,12 +52,12 @@ void setup() {
void loop() { void loop() {
static uint32_t last = 0; static uint32_t last = 0;
static uint32_t frames = 0; static uint32_t seconds = 0;
if (millis() - last >= 1000) { if (millis() - last >= 1000) {
last = millis(); last = millis();
display.fillRect(8, 72, 120, 8, 0); display.fillRect(8, 72, 120, 8, 0);
display.setCursor(8, 72); display.setCursor(8, 72);
display.printf("uptime %lus", (unsigned long)frames++); display.printf("uptime %lus", (unsigned long)seconds++);
display.display(); display.display();
} }
} }
+3 -2
View File
@@ -1,8 +1,9 @@
Unit tests (PlatformIO Unit Testing / Unity) go here. Unit tests (PlatformIO Unit Testing / Unity) go here.
Planned first test: EL512Panel::encodeRow() - check that HS is high for exactly Planned first test: EL512Panel::encodeRow() - check that HS is high for exactly
256 clocks per visible row, VS appears only on the last blanking line, and that 256 clocks per visible row, VS appears only on the last blanking line, that x = 0
even/odd columns land on VID/TVID. (datasheet column 1, odd) lands on TVID and x = 1 on VID, and that clock c is
stored at index c ^ 2 (8-bit I2S byte order).
Run with: pio test -e esp32dev Run with: pio test -e esp32dev