Kaynağa Gözat

harness: diagnostic-fill Pattern A (row/byte ordering)

Add byte-recording to the spy (trace.h, opt-in; printing unchanged
otherwise) and run_diag_harness.h, which fills the 1bpp plane buffer with a
positional tag, records the exact wire-byte stream, and inverts it to
recover the driver's buffer->wire row permutation (sequential / reversed /
row_interleave / unknown) plus a within-row order check. Primes with an
untracked write and skips uniform bursts so only the tagged writeImage
burst is analysed; auto-detects plane inversion.

Validated host-only on 290_T94, 426 (reversed-Y), 583 (UC8179) and the
3-color 213 (two planes): all SEQUENTIAL, within-row OK - matching their
descriptors (none interleave). Scope note: this is the byte-preserving
1bpp path; packed panels that re-pack 1bpp->2bpp in writeImage (e.g. the
579 interleave) need the native path - a follow-on, and pending the 579
hardware re-test.
ZiB 3 hafta önce
ebeveyn
işleme
d6e65922fb

+ 8 - 0
tools/gxepd2_harness/run_213_z98c_diag.cpp

@@ -0,0 +1,8 @@
+#include "run_diag_harness.h"
+
+#include "epd3c/GxEPD2_213_Z98c.h"
+
+int main() {
+  run_diag_harness<GxEPD2_213_Z98c>("GxEPD2_213_Z98c");
+  return 0;
+}

+ 8 - 0
tools/gxepd2_harness/run_290_t94_diag.cpp

@@ -0,0 +1,8 @@
+#include "run_diag_harness.h"
+
+#include "epd/GxEPD2_290_T94.h"
+
+int main() {
+  run_diag_harness<GxEPD2_290_T94>("GxEPD2_290_T94");
+  return 0;
+}

+ 8 - 0
tools/gxepd2_harness/run_426_gdeq0426t82_diag.cpp

@@ -0,0 +1,8 @@
+#include "run_diag_harness.h"
+
+#include "gdeq/GxEPD2_426_GDEQ0426T82.h"
+
+int main() {
+  run_diag_harness<GxEPD2_426_GDEQ0426T82>("GxEPD2_426_GDEQ0426T82");
+  return 0;
+}

+ 8 - 0
tools/gxepd2_harness/run_583_gdeq0583t31_diag.cpp

@@ -0,0 +1,8 @@
+#include "run_diag_harness.h"
+
+#include "gdeq/GxEPD2_583_GDEQ0583T31.h"
+
+int main() {
+  run_diag_harness<GxEPD2_583_GDEQ0583T31>("GxEPD2_583_GDEQ0583T31");
+  return 0;
+}

+ 197 - 0
tools/gxepd2_harness/run_diag_harness.h

@@ -0,0 +1,197 @@
+#pragma once
+
+// Diagnostic-fill harness, Pattern A (byte/row ordering) - see the
+// gxepd2-diag-fill-spec memory. Instead of the uniform 0xA5 fill, it fills
+// the 1bpp plane buffer with a positional TAG, records the exact wire-byte
+// stream (trace.h recording), and inverts the tag to recover the driver's
+// buffer->wire row permutation (identity / reversed / row_interleave /
+// unknown). Host-only, no hardware.
+//
+// Scope: 1bpp byte-preserving writeImage path (mono, and the per-plane
+// bursts of a BW+accent panel). Packed panels that re-pack 1bpp planes into
+// 2bpp/4bpp codes in writeImage do NOT preserve byte identity, so Pattern A
+// there needs the native (writeNative) path or address-sequence analysis -
+// a documented follow-on, see the spec memory.
+//
+// Two fills:
+//   ROW_TAG: each row r -> byte[0]=(r>>8)&0xFF, byte[1]=r&0xFF, rest 0.
+//            Group the plane burst into rows of bpr; [hi,lo] of each group
+//            recovers that wire-row's source row -> the permutation.
+//   COL_TAG: byte[c]=c&0xFF across every row -> each wire row should read
+//            0,1,2,... confirming within-row order is identity (catches a
+//            byte-stride/offset bug - the shape of the 583 smear).
+
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <vector>
+
+#include "spy/Arduino.h"
+#include "spy/SPI.h"
+#include "trace.h"
+
+template<typename Driver> struct DiagPeek : Driver {
+  using Driver::Driver;
+  int busy_level() const { return this->_busy_level; }
+};
+
+// Pull the plane-sized data bursts out of the recorded (is_cmd,byte) stream:
+// maximal runs of consecutive data bytes whose length == plane_bytes.
+inline std::vector<std::vector<uint8_t>> extract_plane_bursts(uint32_t plane_bytes) {
+  std::vector<std::vector<uint8_t>> bursts;
+  std::vector<uint8_t> run;
+  auto flush = [&]() {
+    if (run.size() == plane_bytes) {
+      // Skip all-uniform bursts (e.g. a writeScreenBuffer prime) - they
+      // carry no tag, only the tagged writeImage burst is of interest.
+      bool uniform = true;
+      for (uint8_t v : run)
+        if (v != run[0]) {
+          uniform = false;
+          break;
+        }
+      if (!uniform)
+        bursts.push_back(run);
+    }
+    run.clear();
+  };
+  for (const auto &ev : Trace::record) {
+    if (ev.first) {  // command byte -> ends any data run
+      flush();
+    } else {
+      run.push_back(ev.second);
+    }
+  }
+  flush();
+  return bursts;
+}
+
+// true if most data bytes are 0xFF rather than 0x00 - the driver inverted
+// the plane (our tag is mostly 0x00 bytes), so decode with ^0xFF.
+inline bool burst_is_inverted(const std::vector<uint8_t> &b) {
+  uint32_t zeros = 0, ones = 0;
+  for (uint8_t v : b) {
+    if (v == 0x00)
+      zeros++;
+    else if (v == 0xFF)
+      ones++;
+  }
+  return ones > zeros;
+}
+
+template<typename Driver> void analyze_row_tag(const char *name) {
+  const uint16_t W = Driver::WIDTH, H = Driver::HEIGHT;
+  const uint32_t plane_bytes = (uint32_t) W * H / 8;
+  const uint32_t bpr = (uint32_t) W / 8;
+  if (bpr < 2) {
+    printf("  [row-tag] skipped: bpr<2 (needs 2-byte row tag)\n");
+    return;
+  }
+
+  auto *black = new uint8_t[plane_bytes]();
+  auto *color = new uint8_t[plane_bytes]();
+  for (uint16_t r = 0; r < H; r++) {
+    black[r * bpr + 0] = color[r * bpr + 0] = (r >> 8) & 0xFF;
+    black[r * bpr + 1] = color[r * bpr + 1] = r & 0xFF;
+  }
+
+  DiagPeek<Driver> d(10, 11, 12, 13);
+  Trace::set_dc_pin(11);
+  Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
+  d.selectSPI(SPI, SPISettings());
+  d.init(0);
+  d.writeImage(black, color, 0, 0, W, H);  // prime: clears _initial_write (its screen-buffer prime is untracked)
+  Trace::start_record();
+  d.writeImage(black, color, 0, 0, W, H);  // tagged write, now with no priming burst
+  Trace::stop_record();
+  delete[] black;
+  delete[] color;
+
+  auto bursts = extract_plane_bursts(plane_bytes);
+  printf("  [row-tag] %s %ux%u: bpr=%u, plane_bytes=%u, plane bursts found=%zu\n", name, W, H, (unsigned) bpr,
+         (unsigned) plane_bytes, bursts.size());
+
+  for (size_t p = 0; p < bursts.size(); p++) {
+    const auto &data = bursts[p];
+    bool inv = burst_is_inverted(data);
+    uint8_t x = inv ? 0xFF : 0x00;
+    std::vector<uint32_t> recovered(H);
+    for (uint16_t g = 0; g < H; g++)
+      recovered[g] = (uint32_t) ((data[g * bpr] ^ x) << 8) | (data[g * bpr + 1] ^ x);
+
+    bool identity = true, reversed = true, interleave = true;
+    for (uint16_t g = 0; g < H; g++) {
+      if (recovered[g] != g)
+        identity = false;
+      if (recovered[g] != (uint32_t) (H - 1 - g))
+        reversed = false;
+      uint32_t expect = (g % 2 == 0) ? (g / 2) : (H - 1 - (g / 2));
+      if (recovered[g] != expect)
+        interleave = false;
+    }
+    const char *verdict = identity  ? "SEQUENTIAL (identity)"
+                          : reversed ? "REVERSED rows"
+                          : interleave ? "ROW_INTERLEAVE (top/bottom pairs)"
+                                       : "UNKNOWN permutation";
+    printf("    plane %zu: invert=%s -> %s\n", p, inv ? "yes" : "no", verdict);
+    printf("      first rows:");
+    for (uint16_t g = 0; g < 6 && g < H; g++)
+      printf(" %u", recovered[g]);
+    printf("  | last rows:");
+    for (uint16_t g = (H > 6 ? H - 6 : 0); g < H; g++)
+      printf(" %u", recovered[g]);
+    printf("\n");
+  }
+}
+
+template<typename Driver> void analyze_col_tag(const char *name) {
+  const uint16_t W = Driver::WIDTH, H = Driver::HEIGHT;
+  const uint32_t plane_bytes = (uint32_t) W * H / 8;
+  const uint32_t bpr = (uint32_t) W / 8;
+
+  auto *black = new uint8_t[plane_bytes]();
+  auto *color = new uint8_t[plane_bytes]();
+  for (uint16_t r = 0; r < H; r++)
+    for (uint32_t c = 0; c < bpr; c++)
+      black[r * bpr + c] = color[r * bpr + c] = c & 0xFF;
+
+  DiagPeek<Driver> d(10, 11, 12, 13);
+  Trace::set_dc_pin(11);
+  Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
+  d.selectSPI(SPI, SPISettings());
+  d.init(0);
+  d.writeImage(black, color, 0, 0, W, H);  // prime (untracked)
+  Trace::start_record();
+  d.writeImage(black, color, 0, 0, W, H);  // tagged write
+  Trace::stop_record();
+  delete[] black;
+  delete[] color;
+
+  auto bursts = extract_plane_bursts(plane_bytes);
+  for (size_t p = 0; p < bursts.size(); p++) {
+    const auto &data = bursts[p];
+    bool inv = burst_is_inverted(data);
+    uint8_t x = inv ? 0xFF : 0x00;
+    // Within-row order is identity iff every row reads 0,1,2,...,bpr-1 (mod 256).
+    bool ok = true;
+    uint16_t bad_row = 0, bad_col = 0;
+    for (uint16_t g = 0; g < H && ok; g++)
+      for (uint32_t c = 0; c < bpr; c++)
+        if ((uint8_t) (data[g * bpr + c] ^ x) != (uint8_t) (c & 0xFF)) {
+          ok = false;
+          bad_row = g;
+          bad_col = c;
+          break;
+        }
+    if (ok)
+      printf("    plane %zu: within-row order OK (0..bpr-1 each row)\n", p);
+    else
+      printf("    plane %zu: within-row MISMATCH at row %u col %u\n", p, bad_row, bad_col);
+  }
+}
+
+template<typename Driver> void run_diag_harness(const char *name) {
+  printf("=== %s: DIAGNOSTIC FILL (Pattern A: row/byte ordering) ===\n", name);
+  analyze_row_tag<Driver>(name);
+  analyze_col_tag<Driver>(name);
+}

+ 20 - 1
tools/gxepd2_harness/trace.h

@@ -9,6 +9,8 @@
 
 #include <cstdint>
 #include <cstdio>
+#include <utility>
+#include <vector>
 
 enum class EventKind { PIN_MODE, DIGITAL_WRITE, DIGITAL_READ, DELAY, SPI_BEGIN_TXN, SPI_TRANSFER, SPI_SETTINGS };
 
@@ -38,10 +40,27 @@ struct Trace {
   // false = data byte - matches GxEPD2_EPD::_writeCommand()/_writeData()
   // exactly (DC low while the command byte goes out, high for data).
   static uint8_t spi_transfer(uint8_t byte) {
-    printf("SPI.transfer(0x%02X) dc=%s\n", byte, dc_is_low ? "LOW(cmd)" : "HIGH(data)");
+    if (recording)
+      record.push_back({dc_is_low, byte});  // (is_command, byte)
+    else
+      printf("SPI.transfer(0x%02X) dc=%s\n", byte, dc_is_low ? "LOW(cmd)" : "HIGH(data)");
     return 0;
   }
 
+  // Byte recording for the diagnostic-fill analyzer (run_diag_harness.h):
+  // when on, SPI bytes are collected (with their command/data flag) instead
+  // of printed, so the analyzer can invert the fill pattern off the exact
+  // byte stream rather than parsing text. Off by default - every other
+  // harness still prints as before.
+  static void start_record() {
+    record.clear();
+    recording = true;
+  }
+  static void stop_record() { recording = false; }
+
+  static inline bool recording = false;
+  static inline std::vector<std::pair<bool, uint8_t>> record;  // (is_command, byte)
+
   static void set_dc_pin(int pin) { dc_pin = pin; }
   static void note_digital_write_for_dc(int pin, int value) {
     if (pin == dc_pin)