Parcourir la source

harness: add fast-partial capture program (GxEPD2_290_T94)

Host-only capture (no hardware) that drives a fast-partial-capable driver
past the full-refresh cutoff to record what fast partial needs: the
previous-buffer sync (writeImageAgain -> 0x26 then 0x24), the partial
refresh waveform (refresh(x,y,w,h) -> _Update_Part, 0x22=0xFC vs full
0xF7), and - via a coordinate sweep of refresh(x,y,w,h) at distinct rects -
the parameterized RAM-window encoder, derived by observation and
cross-checked against the full-frame window rather than hand-computed.

run_290_t94_partial.cpp targets the vendored SSD1680 driver. Compiles and
runs clean; the sweep reproduces the SSD1680 window encoding exactly
(0x44 = [x/8, (x+w-1)/8]; 0x45 = y, y+h-1 as 16-bit LE; 0x4E/0x4F cursors).
Capture output is analysis for review; not wired into the engine.
ZiB il y a 3 semaines
Parent
commit
560f5e9fbe

+ 14 - 0
tools/gxepd2_harness/run_290_t94_partial.cpp

@@ -0,0 +1,14 @@
+// Fast-partial capture for GxEPD2_290_T94 (SSD1680, 128x296 mono) - the
+// vendored driver with hasFastPartialUpdate = true, same controller family
+// as our already-ported GDEY0213Z98. Host-only: compiles the real vendor
+// driver against the spy Arduino/SPI shims, no hardware. See
+// run_partial_harness.h for what each phase captures.
+
+#include "run_partial_harness.h"
+
+#include "epd/GxEPD2_290_T94.h"
+
+int main() {
+  run_partial_harness<GxEPD2_290_T94>("GxEPD2_290_T94");
+  return 0;
+}

+ 105 - 0
tools/gxepd2_harness/run_partial_harness.h

@@ -0,0 +1,105 @@
+#pragma once
+
+// Drives a fast-partial-capable vendor driver PAST the full-refresh capture
+// cutoff, to record the sequences fast partial needs that a plain
+// writeImage()+refresh() trace (run_harness.h) deliberately discards:
+//
+//   [B] the previous-buffer sync - writeImageAgain() writes the frame to the
+//       controller's "previous" buffer (0x26) then "current" (0x24), so the
+//       next differential refresh baselines correctly.
+//   [C] the partial refresh waveform - refresh(x,y,w,h) -> _Update_Part(),
+//       a different 0x22 "display update control 2" byte (and often a much
+//       shorter busy wait) than the full _Update_Full().
+//   [D] the parameterized RAM-window encoder - a COORDINATE SWEEP calling
+//       the public refresh(x,y,w,h) at several distinct rects (its internal
+//       _setPartialRamArea is private, so we drive it through the public
+//       API; with x/w kept multiples of 8 the driver's coordinate rounding
+//       is a no-op, so the window bytes in each trace are exactly our rect).
+//       We diff the resulting traces to SOLVE the (x,y,w,h) -> wire-byte
+//       formula by observation instead of hand-computing it (the latter is
+//       what produced the GDEQ0583T31 0x8F-vs-0x87 off-by-one smear). This
+//       sweep is NOT fast-partial-specific: every panel sets a window from
+//       coordinates, so the same technique derives and cross-checks the
+//       window math for any driver that exposes a coordinate-parameterized
+//       window routine. The full-frame (0,0,W,H) case in the sweep is the
+//       same window our existing full-refresh capture already records, so
+//       it is a built-in cross-check on the derived encoder.
+
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+
+#include "spy/Arduino.h"
+#include "spy/SPI.h"
+#include "trace.h"
+
+template<typename Driver> struct PartialPeek : Driver {
+  using Driver::Driver;
+  int busy_level() const { return this->_busy_level; }
+  uint16_t reset_duration() const { return this->_reset_duration; }
+};
+
+template<typename Driver> void run_partial_harness(const char *name) {
+  printf("=== %s: FAST-PARTIAL capture : WIDTH=%u HEIGHT=%u ===\n", name, (unsigned) Driver::WIDTH,
+         (unsigned) Driver::HEIGHT);
+
+  constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
+  Trace::set_dc_pin(DC);
+
+  PartialPeek<Driver> d(CS, DC, RST, BUSY);
+  printf("busy_level=%d reset_duration=%u\n", d.busy_level(), d.reset_duration());
+  Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
+  d.selectSPI(SPI, SPISettings());
+
+  const uint32_t plane_bytes = (static_cast<uint32_t>(Driver::WIDTH) * Driver::HEIGHT) / 8;
+  auto *frame0 = new uint8_t[plane_bytes];
+  auto *frame1 = new uint8_t[plane_bytes];
+  memset(frame0, 0xA5, plane_bytes);  // distinctive, != 0x00/0xFF
+  memset(frame1, 0x5A, plane_bytes);  // a different frame, to make the partial diff non-trivial
+
+  // [PHASE A] Full init + full update. Same as the existing full-refresh
+  // capture; also clears the driver's _initial_refresh so the partial
+  // refresh in phase C actually takes the _Update_Part path (the vendor
+  // driver forces the very first refresh to be full regardless).
+  printf("\n--- [PHASE A] init(0) + writeImage(full) + refresh(false)  (full baseline) ---\n");
+  d.init(0);
+  d.writeImage(frame0, 0, 0, Driver::WIDTH, Driver::HEIGHT);
+  d.refresh(false);
+
+  // [PHASE B] Previous-buffer sync.
+  printf("\n--- [PHASE B] writeImageAgain(full)  (previous 0x26 then current 0x24) ---\n");
+  d.writeImageAgain(frame0, 0, 0, Driver::WIDTH, Driver::HEIGHT);
+
+  // [PHASE C] A real partial update: new content to current, then a
+  // windowed refresh -> _Update_Part waveform + the partial window bytes.
+  printf("\n--- [PHASE C] writeImage(new) + refresh(16,24,32,48)  (partial update) ---\n");
+  d.writeImage(frame1, 0, 0, Driver::WIDTH, Driver::HEIGHT);
+  d.refresh(16, 24, 32, 48);
+
+  // [PHASE D] Window-encoder sweep via refresh(x,y,w,h). Distinct rects with
+  // x/w multiples of 8 (so the driver's rounding is a no-op and the window
+  // bytes are exactly the rect); y/h deliberately non-round to expose the
+  // y encoding and the inclusive (y+h-1) end math. D0 is the full frame, a
+  // cross-check against the already-captured full window. Each refresh also
+  // emits an _Update_Part waveform (expected trailing noise); the window
+  // bytes are what appear between the ">>> SWEEP" line and the 0x22 0xFC.
+  printf("\n--- [PHASE D] window-encoder sweep (refresh(x,y,w,h) per rect) ---\n");
+  struct Rect {
+    uint16_t x, y, w, h;
+    const char *label;
+  };
+  const Rect rects[] = {
+      {0, 0, Driver::WIDTH, Driver::HEIGHT, "D0 full (0,0,W,H) - cross-check vs captured full window"},
+      {0, 0, 8, 8, "D1 origin-min (0,0,8,8)"},
+      {16, 24, 8, 8, "D2 offset (16,24,8,8)"},
+      {32, 40, 40, 48, "D3 block (32,40,40,48)"},
+      {120, 288, 8, 8, "D4 far-corner (120,288,8,8) - high y byte"},
+  };
+  for (const auto &r : rects) {
+    printf(">>> SWEEP %s\n", r.label);
+    d.refresh(r.x, r.y, r.w, r.h);
+  }
+
+  delete[] frame0;
+  delete[] frame1;
+}