Просмотр исходного кода

emitter: segment dual-region panels (GDEY0579F51 reproduces end to end)

The descriptor emitter now models the region-select layer of a dual-chip
master/slave panel, so GxEPD2_579c_GDEY0579F51 is reproduced from its driver
instead of hand-assembled. Four additions on the existing interleaved
multi-plane path, all observation-based:

- INIT recovery. A dual-region panel runs its whole _InitDisplay during the
  writeScreenBuffer clear, and that init reuses the region-select opcode (0xA2
  select per region for PSR), so the single-plane "cut at first window opcode"
  rule fails. Instead take init from the clear stage and drop only the
  contiguous trailing run of window-set commands before the first clear burst.

- plane_width, decoded from the region window-set payload (x_end-x+1 = 396).

- line_order. A region whose burst is twice its pixel bytes is sending two wire
  lines per row (GxEPD2 writeNative's doubled emission) -> candidate
  PAIRED_EDGES. The doubling is observable; the exact edges-to-middle row order
  is not (a uniform fill can't show which buffer row each line carries), so it's
  emitted as a candidate for the diag native-path pass / driver source to
  confirm.

- refresh keeps its distinct both-region window (dropped for a shared-region
  panel as a duplicate), and trims the window GxEPD2 re-sets after the trigger
  to prime its next writeImage (dead in our per-cycle replay).

The emitted 579 is structurally identical to the committed hand-derived header
(INIT/WINDOW/REFRESH, plane layout, plane_width, line_order, blank_code all
match; only the dev-tunable busy_timeout differs). Promoted out of
check_descriptors' KNOWN_DIFF -> all 9 panels reproduce, 0 known-diff. The
shipped panel_579c header is unchanged (keeps its tuned timeout and history).

What still needs hand work: a panel with more than two regions, or asymmetric
regions, and the exact PAIRED_EDGES row order confirmation.
ZiB 3 недель назад
Родитель
Сommit
924158f1dc
4 измененных файлов с 119 добавлено и 19 удалено
  1. 8 4
      CONTRIBUTING.md
  2. 6 3
      tools/check_descriptors.py
  3. 15 7
      tools/gxepd2_harness/README.md
  4. 90 5
      tools/gxepd2_harness/emit_descriptor.h

+ 8 - 4
CONTRIBUTING.md

@@ -113,10 +113,14 @@ These are hardware facts no command trace contains:
   confirmable on hardware. The engine always does a busy wait after the reset
   pulse, because controllers that load from OTP hold BUSY during that load and
   silently drop any command sent before it clears.
-- **True dual-region command structure.** Dual-chip panels (master/slave) carry
-  `0xA2`-style region selects in init and refresh that the emitter's
-  single/shared-region segmentation doesn't yet model; those descriptors still
-  take manual assembly for the region layer.
+- **Paired-edges row order.** The emitter now segments dual-chip (master/slave)
+  panels end to end — the `0xA2`-style region selects in init / window / refresh,
+  per-region `plane_width`, and a candidate `line_order` (it flags `PAIRED_EDGES`
+  when a region's burst is twice its pixel bytes). What a plain capture still
+  can't see is *which* buffer row each wire line carries, so for a `PAIRED_EDGES`
+  panel the exact edges-to-middle order is confirmed from the diag native-path
+  pass or the driver source, not the capture alone. A panel with more than two
+  regions, or asymmetric regions, would still want manual review.
 
 ## Execution engine (`cycle_engine.h`)
 

+ 6 - 3
tools/check_descriptors.py

@@ -33,9 +33,12 @@ import capture as cap  # noqa: E402  (resolution + emit, reused)
 
 PANEL_DIR = ROOT / "components" / "gxepd2"
 
-# Dual-region panels the emitter's single/shared-region segmentation doesn't
-# model yet (its init/refresh region-select layer). Expected to differ.
-KNOWN_DIFF = {"GxEPD2_579c_GDEY0579F51"}
+# Panels the emitter's segmentation doesn't reproduce yet. The dual-region
+# GDEY0579F51 used to live here; the emitter now models the region-select layer
+# (per-region init/window/refresh, plane_width, paired-edges line doubling), so
+# it reproduces and has been promoted out. A future panel the emitter can't yet
+# segment goes here with a one-line why.
+KNOWN_DIFF: set = set()
 
 
 def registry():

+ 15 - 7
tools/gxepd2_harness/README.md

@@ -39,9 +39,14 @@ resolution / busy-polarity / timeout from the driver, and derives
 `GxEPD2_4C` wrapper, reads the wire code for each named colour, validates it
 against the engine's `GXEPD2_PALETTE_4C`, and fills `bits_per_pixel` /
 `palette` / `blank_code` automatically (an unexpected code map stays a TODO
-carrying the observed table). Review the result, add any packed-format fields
-still left as TODO — `bits_per_pixel` / `palette` for a 7-colour panel (no
-resolver yet) and `plane_width` / `line_order` for dual-region — then save it
+carrying the observed table). A **dual-region** panel (per-region select +
+window, e.g. GDEY0579F51 / HX8717) is segmented too: its per-region init /
+window / refresh, `plane_width` (decoded from the region window-set) and a
+candidate `line_order` (`PAIRED_EDGES` when a region's burst is 2x its pixel
+bytes) all come out, with only the exact paired-edges row order left for the
+diag native-path pass to confirm. Review the result, add any packed-format
+fields still left as TODO — `bits_per_pixel` / `palette` for a 7-colour panel
+(no resolver yet) — then save it
 as `components/gxepd2/panel_<panel>.h`. `capture.py` also prints the exact
 `PANELS` line to paste into `display.py` (key / descriptor macro / format
 filled from the capture; `part` / `controller` scraped from the driver header
@@ -61,10 +66,13 @@ tools/check_descriptors.py        # all panels; -v shows the diff on a mismatch
 ```
 
 A `DIFF` means either an emitter regression or **GxEPD2 upstream drift** (a
-vendor bump changed that driver — review the panel). Dual-region panels the
-emitter doesn't fully model yet are listed in its `KNOWN_DIFF` and reported as
-`XFAIL`; panels with no GxEPD2 driver source (the synthetic `MASTER_ONLY`
-variant) are skipped. Run it after any `third_party/GxEPD2/` update.
+vendor bump changed that driver — review the panel). Any panel the emitter
+can't yet segment is listed in its `KNOWN_DIFF` and reported as `XFAIL` (none
+at present — the dual-region GDEY0579F51 was promoted out once the emitter
+modelled the region-select layer; an `XPASS` means a `KNOWN_DIFF` entry now
+reproduces and should be removed). Panels with no GxEPD2 driver source (the
+synthetic `MASTER_ONLY` variant) are skipped. Run it after any
+`third_party/GxEPD2/` update.
 
 ## Other programs
 

+ 90 - 5
tools/gxepd2_harness/emit_descriptor.h

@@ -293,6 +293,9 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
   std::vector<Cmd> init_seq, window0, window1;
   std::vector<uint8_t> plane_cmds;
   bool per_plane = false;
+  bool dual_region = false;        // two physical regions (per-region select + window)
+  int plane_width_val = 0;         // decoded per-region width (dual_region only)
+  uint32_t region0_burst = 0;      // region 0's image burst length, for line-order detection
 
   if (bursts.size() >= 2) {
     // Multi-plane. Two observed layouts:
@@ -359,6 +362,50 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
     for (size_t k = 1; k < windows.size(); k++)
       if (!cmds_equal(windows[0], windows[k]))
         per_plane = true;  // per-plane windows differ => separate regions
+
+    // Dual-region refinement (e.g. GDEY0579F51 / HX8717: a two-chip panel whose
+    // "planes" are two physical half-width regions, each gated by a region-
+    // select command before its own window + burst). The generic interleaved
+    // path above already recovers the per-region windows and plane commands;
+    // three things it can't are filled here.
+    if (interleaved && per_plane) {
+      dual_region = true;
+      std::set<uint8_t> wops;  // region-select + window-set opcodes
+      for (const auto &c : window0)
+        wops.insert(c.cmd);
+      for (const auto &c : window1)
+        wops.insert(c.cmd);
+
+      // (1) INIT. A dual-region panel runs its whole _InitDisplay during the
+      // earlier writeScreenBuffer clear, and that init REUSES the region-select
+      // opcode (select master/slave for each region's PSR), so the single-plane
+      // "cut at the first window opcode" rule would cut at the very first init
+      // command. Instead take init from the clear stage and drop only the
+      // CONTIGUOUS trailing run of window-set commands before the first clear
+      // burst (the both-/first-region window-sets); the select/PSR pairs earlier
+      // in init survive because a non-window command (BTST, power-on, ...)
+      // separates them from that trailing run.
+      const size_t cb = plane_index(init_stage);  // first clear burst
+      size_t cut = cb;
+      while (cut > 0 && wops.count(init_stage[cut - 1].cmd))
+        cut--;
+      init_seq.assign(init_stage.begin(), init_stage.begin() + cut);
+
+      // (2) plane_width: each region addresses its own half. Decode it from the
+      // window-set command's payload - first two big-endian 16-bit fields are
+      // x and x_end, as every window encoder observed so far lays them out.
+      for (const auto &c : window0)
+        if (c.data.size() >= 4) {
+          int x = (c.data[0] << 8) | c.data[1];
+          int xe = (c.data[2] << 8) | c.data[3];
+          plane_width_val = xe - x + 1;
+          break;
+        }
+
+      // (3) line doubling: region 0's burst length, compared in the emit stage
+      // against plane_width*height*bpp/8 to flag PAIRED_EDGES.
+      region0_burst = (uint32_t) image_stage[bursts[0]].data.size();
+    }
   } else {
     // Single plane: window = the image call's pre-burst block; init comes
     // from the writeScreenBuffer stage, cut at the first window-set command
@@ -407,16 +454,32 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
   if (!bursts.empty())
     for (size_t i = bursts.back() + 1; i < image_stage.size() && image_stage[i].data.size() < PLANE_BURST_MIN; i++)
       refresh_seq.push_back(image_stage[i]);
+  // A shared-region panel re-sets its one window in refresh() and we drop that
+  // duplicate; a dual-region panel's refresh instead sets a DISTINCT both-region
+  // window (its own select payload), which is part of the refresh - keep it.
   for (const auto &c : refresh_stage)
-    if (!all_wops.count(c.cmd))
+    if (dual_region || !all_wops.count(c.cmd))
       refresh_seq.push_back(c);
+  // The refresh ends at its trigger (the command we busy-wait on). A dual-region
+  // driver re-sets the window AFTER the trigger to prime its own next writeImage;
+  // our engine always sets its own window per cycle, so that trailing window is
+  // dead in replay - trim anything past the last busy-wait.
+  if (dual_region) {
+    size_t last_wait = refresh_seq.size();
+    for (size_t i = 0; i < refresh_seq.size(); i++)
+      if (refresh_seq[i].wait_busy)
+        last_wait = i;
+    if (last_wait < refresh_seq.size())
+      refresh_seq.resize(last_wait + 1);
+  }
 
   // --- emit ---
   printf("// GENERATED by emit_descriptor from %s - candidate.\n", class_name);
   printf("// Command sequences, plane layout and plane_invert are observed here;\n");
   printf("// for a packed panel bits_per_pixel/palette/blank_code are probe-filled\n");
-  printf("// when the colour family is recognised (4-colour), else left TODO.\n");
-  printf("// plane_width / line_order (dual-region) still come from the diag passes.\n");
+  printf("// when the colour family is recognised (4-colour), else left TODO. A\n");
+  printf("// dual-region panel also gets plane_width and a candidate line_order; the\n");
+  printf("// exact PAIRED_EDGES row order still wants diag/driver confirmation.\n");
   printf("#pragma once\n#include \"epd_command.h\"\n\nnamespace esphome::gxepd2 {\n\n");
 
   auto emit_seq = [&](const char *seqsuffix, std::vector<Cmd> &cmds) {
@@ -478,8 +541,30 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
   } else {
     printf("    .plane_invert = {false, false},  // unused: packed panel\n");
   }
-  if (per_plane)
-    printf("    .plane_width = 0,  // TODO: diag pass (separate-region width)\n");
+  if (per_plane) {
+    if (dual_region && plane_width_val > 0)
+      printf("    .plane_width = %d,  // decoded from the region window-set (x_end-x+1); sanity-check\n",
+             plane_width_val);
+    else
+      printf("    .plane_width = 0,  // TODO: diag pass (separate-region width)\n");
+  }
+  // line_order: a dual-region panel whose per-region burst is twice the region's
+  // own pixel bytes is sending two wire lines per row (GxEPD2 writeNative's
+  // doubled, both-edges-to-middle emission). The doubling is observable here;
+  // the exact row order that PAIRED_EDGES encodes is NOT (a uniform fill can't
+  // show which buffer row each line carries), so emit it as a candidate the
+  // diag native-path pass / driver source confirms.
+  if (dual_region && plane_width_val > 0 && depth_bpp > 0) {
+    uint32_t expect = (uint32_t) plane_width_val * Driver::HEIGHT * depth_bpp / 8;
+    if (expect && region0_burst == 2u * expect)
+      printf("    .line_order = LineOrder::PAIRED_EDGES,  // region burst %u B = 2x %u B (2 lines/row); "
+             "confirm edges-to-middle order via the diag native-path pass\n",
+             region0_burst, expect);
+    else if (expect && region0_burst != expect)
+      printf("    // NOTE: region burst %u B vs expected %u B (1 line/row) - line order unclear, "
+             "check with the diag native-path pass\n",
+             region0_burst, expect);
+  }
   if (pv.is_1bpp) {
     // 1bpp plane model: depth confirmed by the probe; palette/blank_code unused.
     printf("    .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0x%02X\n", pv.w0_white & 0xFF);