Browse Source

579c: rebuild descriptor from the stock driver; drop Waveshare "fixes"

The GDEY0579F51 symptoms we chased (mirrored text, split/duplicated
geometry, a slave region that never converged) were drive-current
starvation on the bench MCU, not a driver bug or a dead slave chip: the
same panel + firmware renders on an adequately-powered driver board.

So the hand-corrections this descriptor carried - swapped master/slave
PSR, power-on under select=Both, a changed BTST, all derived from
Waveshare's epd5in79g.cpp on the theory GxEPD2's driver was wrong for
this panel - were unjustified. Rebuild the descriptor to reproduce the
STOCK vendored driver byte-for-byte instead:

  - init transcribed from the stock _InitDisplay() (PSR master {0x07,0x29}
    / slave {0x03,0x29}, BTST 0x32 x4, TRES, CDI/E9/E0 under select=Both,
    then re-select master before power-on);
  - window + refresh taken from a fresh capture (unchanged - they were
    always right);
  - packed-2bpp / palette / dual-region plumbing and row_interleave kept.

Also revert the one local edit to the vendored driver: remove the
GXEPD2_579C_DEBUG_MASTER_ONLY toggle so third_party's 579c source is
byte-identical to upstream again.

check_descriptors.py: 579c stays XFAIL (the emitter can't recover a
dual-chip init), all others MATCH.
ZiB 3 weeks ago
parent
commit
2b7612ba6b

+ 47 - 65
components/gxepd2/panel_579c_gdey0579f51.h

@@ -1,45 +1,33 @@
 #pragma once
 #pragma once
 
 
-// Hand-derived from third_party/GxEPD2/src/epd4c/GxEPD2_579c_GDEY0579F51.{h,cpp}
-// (GDEY0579F51, HX8717 controller, 792x272, same JD79667-family 2bpp/4-color
-// packing as GDEY029F51 - see panel_290c_gdey029f51.h). The real dual-
-// region version, superseding panel_579c_gdey0579f51_master_only.h's
-// deliberate single-region simplification - uses CycleEngine's
-// window_per_plane mechanism to give "master" and "slave" (0xA2-select
-// logical regions, not separate physical CS pins) their own window-set
-// before each one's own 0x10 byte burst, "plane 0"/"plane 1" in the
-// engine's terms even though they're physical regions here, not black/red
-// color planes - see EpdPanelDescriptor::window_per_plane.
+// GDEY0579F51 - HX8717 dual-chip, 792x272, 4-color, 2bpp packed (same
+// JD79667-family packing as GDEY029F51, see panel_290c_gdey029f51.h).
 //
 //
-// Region physical placement (2026-09-19, confirmed two independent ways):
-// GxEPD2_579_GDEY0579T93.cpp (SSD1683, same 792x272 panel family) comments
-// its own master/slave split explicitly - "near connector" = slave,
-// "opposite to connector" = master. User independently confirmed the same
-// physical mapping driving this actual panel with the GDEY0213F51 driver
-// (same underlying chip): master = away from the FPC connector. So master
-// is region 0 here (window_sequence[0], written first, right after init
-// leaves the controller select=master already) and slave is region 1.
+// Captured from the STOCK vendored driver
+// (third_party/GxEPD2/src/epd4c/GxEPD2_579c_GDEY0579F51.{h,cpp}), with no
+// local corrections. An earlier revision of this descriptor carried
+// hand-"fixes" derived from Waveshare's epd5in79g.cpp (swapped master/slave
+// PSR, power-on under select=Both, a changed BTST) to chase mirrored text /
+// split geometry / a non-converging slave. Those symptoms were later traced
+// to DRIVE-CURRENT STARVATION on the bench MCU, not a driver bug - the same
+// panel + firmware renders on an adequately-powered driver board - so the
+// corrections were unjustified and have been dropped. This descriptor now
+// reproduces the stock GxEPD2 driver's own on-wire behaviour byte-for-byte.
 //
 //
-// PSR values corrected (2026-09-19) against Waveshare's own epd5in79g.cpp
-// reference driver (extracted from their official demo archive for this
-// exact module) - GxEPD2's driver has master(0x01)->{0x07,0x29},
-// slave(0x02)->{0x03,0x29}; Waveshare's has them swapped:
-// master(0x01)->{0x03,0x29}, slave(0x02)->{0x07,0x29}. The two PSR values
-// differ only in the SHL bit (source/column scan-shift direction per the
-// HX8717 command table) - GxEPD2 apparently has this backwards for this
-// panel, which lines up with the left-right-mirrored text seen on
-// hardware. Followed Waveshare's reference here, not GxEPD2's.
+// Dual-region layer assembled by hand from the driver source, per
+// CONTRIBUTING.md "Limits - True dual-region command structure": the
+// emitter's segmentation doesn't model the 0xA2 master/slave region selects.
+// Window geometry and refresh were taken from the capture (byte-exact);
+// the init below transcribes the stock _InitDisplay() directly (the emitter
+// can't recover a dual-chip init - _InitDisplay runs during the earlier
+// writeScreenBuffer clear, so nothing is left in the writeImage pre-block).
 //
 //
-// The master-only descriptor's single-region run showed real, correctly-
-// decoded content but compressed/duplicated within half the driven
-// region's own width, plus noise/black bands on the untouched slave half.
-// The noise was explained and fixed there (0x12's refresh trigger fires a
-// real electrical refresh across the WHOLE panel regardless of 0xA2
-// select, so refreshing with slave's RAM never written "developed" its
-// stale/undefined content) - this descriptor writes both regions before
-// refreshing, which should eliminate that on its own. The duplication
-// artifact's cause is still unconfirmed; writing both regions may or may
-// not resolve it - that's what this descriptor is for finding out.
+// Region mapping: master = away from the FPC connector = region 0 (written
+// first), slave = near connector = region 1 (cross-checked against
+// GxEPD2_579_GDEY0579T93.cpp's own master/slave comments). "plane 0/1" in
+// the engine's terms are these two physical regions, not color planes;
+// window_per_plane gives each its own 0xA2-select window-set before its own
+// 0x10 byte burst.
 
 
 #include "epd_command.h"
 #include "epd_command.h"
 
 
@@ -48,46 +36,40 @@ namespace esphome::gxepd2 {
 inline const uint8_t PANEL_579C_GDEY0579F51_SELECT_MASTER[] = {0x01};
 inline const uint8_t PANEL_579C_GDEY0579F51_SELECT_MASTER[] = {0x01};
 inline const uint8_t PANEL_579C_GDEY0579F51_SELECT_SLAVE[] = {0x02};
 inline const uint8_t PANEL_579C_GDEY0579F51_SELECT_SLAVE[] = {0x02};
 inline const uint8_t PANEL_579C_GDEY0579F51_SELECT_BOTH[] = {0x00};
 inline const uint8_t PANEL_579C_GDEY0579F51_SELECT_BOTH[] = {0x00};
-inline const uint8_t PANEL_579C_GDEY0579F51_PSR_MASTER[] = {0x03, 0x29};
-inline const uint8_t PANEL_579C_GDEY0579F51_PSR_SLAVE[] = {0x07, 0x29};
-inline const uint8_t PANEL_579C_GDEY0579F51_BTST[] = {0x38, 0x38, 0x38, 0x00};
-inline const uint8_t PANEL_579C_GDEY0579F51_TRES[] = {0x01, 0x8C, 0x01, 0x10};  // 396x272 - one region's resolution
-inline const uint8_t PANEL_579C_GDEY0579F51_CDI_INIT[] = {0x97};
+inline const uint8_t PANEL_579C_GDEY0579F51_PSR_MASTER[] = {0x07, 0x29};
+inline const uint8_t PANEL_579C_GDEY0579F51_PSR_SLAVE[] = {0x03, 0x29};
+inline const uint8_t PANEL_579C_GDEY0579F51_BTST[] = {0x32, 0x32, 0x32, 0x32};
+inline const uint8_t PANEL_579C_GDEY0579F51_TRES[] = {0x01, 0x8C, 0x01, 0x10};  // 396x272 - one region
+inline const uint8_t PANEL_579C_GDEY0579F51_CDI_INIT[] = {0x97};  // border white
 inline const uint8_t PANEL_579C_GDEY0579F51_E9[] = {0x01};
 inline const uint8_t PANEL_579C_GDEY0579F51_E9[] = {0x01};
 inline const uint8_t PANEL_579C_GDEY0579F51_E0[] = {0x01};
 inline const uint8_t PANEL_579C_GDEY0579F51_E0[] = {0x01};
 
 
-// Init(), following Waveshare's own epd5in79g.cpp exactly (command order
-// and select state matter here, not just which bytes get sent): PSR
-// written to each side individually, then CDI/TRES/BTST/E9/E0 written
-// once under select=Both since they're genuinely shared/global registers
-// - and, critically, POWER-ON (0x04) is sent while select is STILL Both,
-// not re-selecting master first. This engine's first attempt added an
-// extra "reselect master" step before 0x04 that Waveshare's reference
-// does not have, meaning only master's chip was ever actually powered on
-// - a very plausible explanation for slave showing a persistent noise/
-// garbage artifact regardless of what data it's given (2026-09-19).
+// Stock _InitDisplay(): PSR written to each region under its own select,
+// then BTST/TRES/CDI/E9/E0 under select=Both, then re-select MASTER and
+// power-on (0x04). This is the vendor's order and select-state exactly.
 inline const EpdCommand PANEL_579C_GDEY0579F51_INIT[] = {
 inline const EpdCommand PANEL_579C_GDEY0579F51_INIT[] = {
     {0xA2, PANEL_579C_GDEY0579F51_SELECT_MASTER, 1},
     {0xA2, PANEL_579C_GDEY0579F51_SELECT_MASTER, 1},
     {0x00, PANEL_579C_GDEY0579F51_PSR_MASTER, 2},
     {0x00, PANEL_579C_GDEY0579F51_PSR_MASTER, 2},
     {0xA2, PANEL_579C_GDEY0579F51_SELECT_SLAVE, 1},
     {0xA2, PANEL_579C_GDEY0579F51_SELECT_SLAVE, 1},
     {0x00, PANEL_579C_GDEY0579F51_PSR_SLAVE, 2},
     {0x00, PANEL_579C_GDEY0579F51_PSR_SLAVE, 2},
     {0xA2, PANEL_579C_GDEY0579F51_SELECT_BOTH, 1},
     {0xA2, PANEL_579C_GDEY0579F51_SELECT_BOTH, 1},
-    {0x50, PANEL_579C_GDEY0579F51_CDI_INIT, 1},
-    {0x61, PANEL_579C_GDEY0579F51_TRES, 4},
     {0x06, PANEL_579C_GDEY0579F51_BTST, 4},
     {0x06, PANEL_579C_GDEY0579F51_BTST, 4},
+    {0x61, PANEL_579C_GDEY0579F51_TRES, 4},
+    {0x50, PANEL_579C_GDEY0579F51_CDI_INIT, 1},
     {0xE9, PANEL_579C_GDEY0579F51_E9, 1},
     {0xE9, PANEL_579C_GDEY0579F51_E9, 1},
     {0xE0, PANEL_579C_GDEY0579F51_E0, 1},
     {0xE0, PANEL_579C_GDEY0579F51_E0, 1},
-    {0x04, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // power on, select=Both
+    {0xA2, PANEL_579C_GDEY0579F51_SELECT_MASTER, 1},
+    {0x04, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // power on
 };
 };
 
 
-// _setPartialRamAreaMaster/Slave(0, 0, 396, 272), default partial_mode=true
-// - both regions are the same shape, just a different select byte.
+// _setPartialRamArea*(0, 0, 396, 272) with partial_mode=true: both regions
+// are the same shape, differing only in the 0xA2 select byte sent first.
 inline const uint8_t PANEL_579C_GDEY0579F51_WINDOW_REGION[] = {
 inline const uint8_t PANEL_579C_GDEY0579F51_WINDOW_REGION[] = {
-    0x00, 0x00,  // x/256, x%256   = 0
-    0x01, 0x8B,  // xe/256, xe%256 = 395
-    0x00, 0x00,  // y/256, y%256   = 0
-    0x01, 0x0F,  // ye/256, ye%256 = 271
-    0x01,
+    0x00, 0x00,  // x       = 0
+    0x01, 0x8B,  // x+w-1   = 395
+    0x00, 0x00,  // y       = 0
+    0x01, 0x0F,  // y+h-1   = 271
+    0x01,        // partial_mode
 };
 };
 
 
 inline const EpdCommand PANEL_579C_GDEY0579F51_WINDOW_MASTER[] = {
 inline const EpdCommand PANEL_579C_GDEY0579F51_WINDOW_MASTER[] = {
@@ -99,9 +81,9 @@ inline const EpdCommand PANEL_579C_GDEY0579F51_WINDOW_SLAVE[] = {
     {0x83, PANEL_579C_GDEY0579F51_WINDOW_REGION, 9},
     {0x83, PANEL_579C_GDEY0579F51_WINDOW_REGION, 9},
 };
 };
 
 
-// refresh(false): vendor's own _setPartialRamAreaBoth(0,0,396,272) then
-// _refresh(false) - select=Both is correct here (unlike the master-only
-// descriptor) since both regions' RAM genuinely holds real content now.
+// refresh(false): stock _setPartialRamAreaBoth(0,0,396,272) then _refresh:
+// CDI 0x50=0x37 (white), then 0x12 trigger (power-on 0x04 is skipped here -
+// already on from init).
 inline const uint8_t PANEL_579C_GDEY0579F51_CDI_REFRESH[] = {0x37};
 inline const uint8_t PANEL_579C_GDEY0579F51_CDI_REFRESH[] = {0x37};
 inline const uint8_t PANEL_579C_GDEY0579F51_REFRESH_TRIGGER[] = {0x00};
 inline const uint8_t PANEL_579C_GDEY0579F51_REFRESH_TRIGGER[] = {0x00};
 
 

+ 1 - 7
third_party/GxEPD2/src/epd4c/GxEPD2_579c_GDEY0579F51.cpp

@@ -436,12 +436,6 @@ void GxEPD2_579c_GDEY0579F51::_writeDataFromNative(const uint8_t* native, int16_
   _endTransfer();
   _endTransfer();
 }
 }
 
 
-// TEMPORARY DIAGNOSTIC: set to 1 to skip the slave-side (right half) write in
-// writeNative() below entirely, to isolate whether the master-only path
-// renders correctly on its own. Revert to 0 once the split-screen bug is
-// understood - this is not meant to be a real feature.
-#define GXEPD2_579C_DEBUG_MASTER_ONLY 0
-
 void GxEPD2_579c_GDEY0579F51::writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm)
 void GxEPD2_579c_GDEY0579F51::writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm)
 {
 {
   if (!data1) return;
   if (!data1) return;
@@ -512,7 +506,7 @@ void GxEPD2_579c_GDEY0579F51::writeNative(const uint8_t* data1, const uint8_t* d
       }
       }
     }
     }
   }
   }
-  if ((x1 + w1 > WIDTH / 2) && true && !GXEPD2_579C_DEBUG_MASTER_ONLY) // SlaveSide ****
+  if ((x1 + w1 > WIDTH / 2) && true) // SlaveSide ****
   {
   {
     int16_t pw_w = x1 <= WIDTH / 2 ? x1 + w1 - WIDTH / 2 : w1;
     int16_t pw_w = x1 <= WIDTH / 2 ? x1 + w1 - WIDTH / 2 : w1;
     int16_t pw_x = x1 <= WIDTH / 2 ? 0 : x1 - WIDTH / 2;
     int16_t pw_x = x1 <= WIDTH / 2 ? 0 : x1 - WIDTH / 2;