Преглед изворни кода

harness: prototype descriptor emitter (single-plane panels)

Emit a candidate panel_*.h EpdPanelDescriptor by observing a real vendor
GxEPD2 driver run, instead of hand-transcribing its C++ - closing the
hand-derivation gap for the straight-line parts of a descriptor (init /
window / refresh command sequences + plane-write command).

trace.h gains an opt-in structured event log (SPI bytes + delays + busy
polls, in call order). emit_descriptor.h stages writeScreenBuffer() ->
writeImage() -> refresh() and phase-segments the stream at the public-API
boundary, using "the burst of pixel data is the plane write" as the pivot:
init is before the burst (cut at the first window-set command, since
SSD16xx sets the window inside writeScreenBuffer), window is the image
call's pre-burst commands, refresh is any post-burst commands (e.g. UC8179
0x92) followed by the refresh call minus its repeated window set. A delay
right before a busy-wait folds into wait_busy (it's the poll loop's, not a
command delay).

Validated byte-for-byte against the hand-written descriptors for three
different controller families: 290c (JD79661 4-color), 426 (SSD1677 mono,
window-in-init), 583 (UC8179 mono, post-burst 0x92). Multi-plane panels
(213 Z98c 3-color, 579 dual-region) are out of scope here - the single
plane/window assumption breaks and is the next extension. bits_per_pixel/
palette/plane_invert still come from the diag passes.
ZiB пре 3 недеља
родитељ
комит
b3d1b32e22

+ 241 - 0
tools/gxepd2_harness/emit_descriptor.h

@@ -0,0 +1,241 @@
+#pragma once
+
+// Prototype: emit a candidate panel_*.h EpdPanelDescriptor by *observing* a
+// real vendor GxEPD2 driver run, instead of hand-transcribing its C++. This
+// closes the hand-derivation gap for the straight-line parts of a descriptor
+// (init / window / refresh command sequences + the plane-write command) by
+// phase-segmenting the captured event stream at the public-API boundary - no
+// edits to the vendor source.
+//
+// Staging (the trick that separates the phases without instrumenting the
+// vendor's private methods):
+//   writeScreenBuffer()  -> lazy _InitDisplay() + a plane-write burst (clear).
+//                           Everything before that burst is the INIT sequence;
+//                           the command right before it is the PLANE-WRITE cmd.
+//   writeImage(pattern)  -> init already done, so this is just the WINDOW set
+//                           (commands before the pattern burst) + plane write.
+//   refresh()            -> re-sets the window then triggers; dropping the
+//                           duplicate window command leaves the REFRESH seq.
+//
+// Out of scope here (comes from the diag Pattern A/B passes, not one capture):
+// bits_per_pixel / palette / plane_invert / row_interleave. Those are emitted
+// as TODO placeholders so the diff against the hand file is about segmentation.
+
+#include <cstdint>
+#include <cstdio>
+#include <set>
+#include <vector>
+
+#include "spy/Arduino.h"
+#include "spy/SPI.h"
+#include "trace.h"
+
+namespace emit {
+
+// A data burst at least this long is a pixel-plane write, not config data.
+constexpr size_t PLANE_BURST_MIN = 256;
+
+struct Cmd {
+  uint8_t cmd;
+  std::vector<uint8_t> data;
+  bool wait_busy = false;
+  unsigned long delay_after = 0;
+};
+
+// Expose GxEPD2_EPD's protected busy level without parsing (same trick as
+// run_harness.h's Peek).
+template<typename Driver> struct Peek : Driver {
+  using Driver::Driver;
+  int busy_level() const { return this->_busy_level; }
+};
+
+// Group a slice of the event stream into EpdCommand-shaped records. Events
+// before the first SPI *command* (the reset pulse + its delays) are skipped -
+// the engine owns the hardware reset, so it never belongs in a descriptor.
+inline std::vector<Cmd> group(size_t lo, size_t hi) {
+  std::vector<Cmd> cmds;
+  bool started = false;
+  for (size_t i = lo; i < hi; i++) {
+    const auto &e = Trace::events[i];
+    switch (e.kind) {
+      case EventKind::SPI_TRANSFER:
+        if (e.is_command) {
+          cmds.push_back(Cmd{e.byte});
+          started = true;
+        } else if (started) {
+          cmds.back().data.push_back(e.byte);
+        }
+        break;
+      case EventKind::DELAY:
+        if (started)
+          cmds.back().delay_after += e.ms;
+        break;
+      case EventKind::DIGITAL_READ:  // the only reads are busy-pin polls
+        if (started) {
+          cmds.back().wait_busy = true;
+          // The delay(1) GxEPD2_EPD::_waitWhileBusy() runs inside its poll
+          // loop lands right before this read and is part of the busy-wait,
+          // not an inter-command delay - our engine owns busy-waiting, so
+          // fold it in rather than emit it. (This also absorbs a vendor's own
+          // tiny settle delay right before a refresh trigger, e.g. 290c's
+          // _refresh delay(1); immaterial, the panel asserts busy regardless.)
+          cmds.back().delay_after = 0;
+        }
+        break;
+      default:
+        break;  // DIGITAL_WRITE = reset/CS/DC toggles: engine's job, not data
+    }
+  }
+  return cmds;
+}
+
+// Index of the first command whose data burst marks a plane write, or np
+// (cmds.size()) if there's none in this slice.
+inline size_t plane_index(const std::vector<Cmd> &cmds) {
+  for (size_t i = 0; i < cmds.size(); i++)
+    if (cmds[i].data.size() >= PLANE_BURST_MIN)
+      return i;
+  return cmds.size();
+}
+
+inline void print_data_array(const char *macro, const char *suffix, const std::vector<uint8_t> &d) {
+  printf("inline const uint8_t %s_%s[] = {", macro, suffix);
+  for (size_t i = 0; i < d.size(); i++)
+    printf("%s0x%02X", i ? ", " : "", d[i]);
+  printf("};\n");
+}
+
+// Print one EpdCommand{} initializer line, with the optional fields only when
+// they're non-default (matches the hand files' style).
+inline void print_cmd(const char *macro, const char *suffix, const Cmd &c) {
+  if (c.data.empty() && !c.wait_busy && c.delay_after == 0) {
+    printf("    {0x%02X, nullptr, 0},\n", c.cmd);
+    return;
+  }
+  const char *dataref = c.data.empty() ? "nullptr" : nullptr;
+  if (dataref)
+    printf("    {0x%02X, nullptr, 0", c.cmd);
+  else
+    printf("    {0x%02X, %s_%s, %u", c.cmd, macro, suffix, (unsigned) c.data.size());
+  if (c.wait_busy || c.delay_after)
+    printf(", /*delay_before_ms=*/0, /*delay_after_ms=*/%lu, /*wait_busy_after=*/%s", c.delay_after,
+           c.wait_busy ? "true" : "false");
+  printf("},\n");
+}
+
+template<typename Driver> void emit_descriptor(const char *class_name, const char *macro) {
+  constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
+  Trace::set_dc_pin(DC);
+  Peek<Driver> driver(CS, DC, RST, BUSY);
+  const int bl = driver.busy_level();
+  Trace::last_busy_release_value = bl == HIGH ? LOW : HIGH;
+  driver.selectSPI(SPI, SPISettings());
+  driver.init(0);
+
+  const uint32_t plane_bytes = (static_cast<uint32_t>(Driver::WIDTH) * Driver::HEIGHT) / 8;
+  auto *black = new uint8_t[plane_bytes];
+  auto *color = new uint8_t[plane_bytes];
+  for (uint32_t i = 0; i < plane_bytes; i++) {
+    black[i] = 0xA5;
+    color[i] = 0xA5;
+  }
+
+  Trace::start_event_record();
+  const size_t s0 = 0;
+  driver.writeScreenBuffer();                                  // init + clear burst
+  const size_t s1 = Trace::events.size();
+  driver.writeImage(black, color, 0, 0, Driver::WIDTH, Driver::HEIGHT);  // window + plane
+  const size_t s2 = Trace::events.size();
+  driver.refresh(false);                                       // window-again + trigger
+  const size_t s3 = Trace::events.size();
+  Trace::stop_event_record();
+
+  delete[] black;
+  delete[] color;
+
+  // --- segment ---
+  auto init_stage = group(s0, s1);
+  auto image_stage = group(s1, s2);
+  auto refresh_stage = group(s2, s3);
+
+  const size_t ip = plane_index(init_stage);
+  const size_t wp = plane_index(image_stage);
+
+  // Window set = the commands the image call issues before its plane burst.
+  std::vector<Cmd> window_seq(image_stage.begin(), image_stage.begin() + wp);
+  std::set<uint8_t> window_ops;
+  for (const auto &c : window_seq)
+    window_ops.insert(c.cmd);
+
+  // Init = init-stage commands before its plane burst, BUT cut at the first
+  // window-set command: SSD16xx-style drivers set the RAM window inside
+  // writeScreenBuffer too, and that belongs in window_sequence, not init.
+  size_t init_end = ip;
+  for (size_t i = 0; i < ip; i++)
+    if (window_ops.count(init_stage[i].cmd)) {
+      init_end = i;
+      break;
+    }
+  std::vector<Cmd> init_seq(init_stage.begin(), init_stage.begin() + init_end);
+
+  const uint8_t plane_cmd = ip < init_stage.size() ? init_stage[ip].cmd : 0x00;
+  const uint8_t clear_fill = (ip < init_stage.size() && !init_stage[ip].data.empty()) ? init_stage[ip].data[0] : 0x00;
+
+  // Refresh = post-write commands (issued after the image's plane burst but
+  // before any next burst, e.g. UC8179's 0x92 "partial out" - they fire after
+  // the byte burst and before the trigger, so they lead refresh), then the
+  // refresh call itself minus any window-set it repeats.
+  std::vector<Cmd> refresh_seq;
+  for (size_t i = wp + 1; i < image_stage.size() && image_stage[i].data.size() < PLANE_BURST_MIN; i++)
+    refresh_seq.push_back(image_stage[i]);
+  for (const auto &c : refresh_stage)
+    if (!window_ops.count(c.cmd))
+      refresh_seq.push_back(c);
+
+  // --- emit ---
+  printf("// GENERATED by emit_descriptor from %s - candidate, segmentation only.\n", class_name);
+  printf("// bits_per_pixel/palette/plane_invert still come from the diag passes.\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) {
+    for (size_t i = 0; i < cmds.size(); i++)
+      if (!cmds[i].data.empty()) {
+        char suf[64];
+        snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
+        print_data_array(macro, suf, cmds[i].data);
+      }
+    printf("\ninline const EpdCommand %s_%s[] = {\n", macro, seqsuffix);
+    for (size_t i = 0; i < cmds.size(); i++) {
+      char suf[64];
+      snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
+      print_cmd(macro, suf, cmds[i]);
+    }
+    printf("};\n\n");
+  };
+
+  emit_seq("INIT", init_seq);
+  emit_seq("WINDOW_SEQ", window_seq);
+  emit_seq("REFRESH", refresh_seq);
+
+  printf("inline const EpdPanelDescriptor %s = {\n", macro);
+  printf("    .width = %u,\n", (unsigned) Driver::WIDTH);
+  printf("    .height = %u,\n", (unsigned) Driver::HEIGHT);
+  printf("    .busy_active_level = %s,\n", bl == HIGH ? "true" : "false");
+  printf("    .busy_timeout_ms = %u,\n", (unsigned) Driver::full_refresh_time + 5000u);
+  printf("    .init_sequence = %s_INIT,\n", macro);
+  printf("    .init_sequence_len = sizeof(%s_INIT) / sizeof(%s_INIT[0]),\n", macro, macro);
+  printf("    .window_sequence = {%s_WINDOW_SEQ, nullptr},\n", macro);
+  printf("    .window_sequence_len = {sizeof(%s_WINDOW_SEQ) / sizeof(%s_WINDOW_SEQ[0]), 0},\n", macro, macro);
+  printf("    .plane_count = 1,\n");
+  printf("    .plane_write_cmd = {0x%02X, 0x00},\n", plane_cmd);
+  printf("    .plane_invert = {false, false},  // TODO: diag pass\n");
+  printf("    .bits_per_pixel = 1,  // TODO: diag pass (clear fill byte observed: 0x%02X)\n", clear_fill);
+  printf("    .palette = nullptr,  // TODO: diag pass\n");
+  printf("    .palette_len = 0,\n");
+  printf("    .blank_code = 0x00,  // TODO: derive from clear fill 0x%02X\n", clear_fill);
+  printf("    .refresh_sequence = %s_REFRESH,\n", macro);
+  printf("    .refresh_sequence_len = sizeof(%s_REFRESH) / sizeof(%s_REFRESH[0]),\n", macro, macro);
+  printf("};\n\n}  // namespace esphome::gxepd2\n");
+}
+
+}  // namespace emit

+ 8 - 0
tools/gxepd2_harness/run_290c_gdey029f51_emit.cpp

@@ -0,0 +1,8 @@
+#include "emit_descriptor.h"
+
+#include "epd4c/GxEPD2_290c_GDEY029F51.h"
+
+int main() {
+  emit::emit_descriptor<GxEPD2_290c_GDEY029F51>("GxEPD2_290c_GDEY029F51", "PANEL_290C_GDEY029F51");
+  return 0;
+}

+ 52 - 4
tools/gxepd2_harness/trace.h

@@ -15,22 +15,68 @@
 enum class EventKind { PIN_MODE, DIGITAL_WRITE, DIGITAL_READ, DELAY, SPI_BEGIN_TXN, SPI_TRANSFER, SPI_SETTINGS };
 enum class EventKind { PIN_MODE, DIGITAL_WRITE, DIGITAL_READ, DELAY, SPI_BEGIN_TXN, SPI_TRANSFER, SPI_SETTINGS };
 
 
 struct Trace {
 struct Trace {
-  static void pin_mode(int pin, int mode) { printf("pinMode(%d, %d)\n", pin, mode); }
+  // Structured, ordered event log (opt-in via start_event_record) for the
+  // descriptor emitter (emit_descriptor.h). Unlike `record` below - which
+  // keeps SPI bytes only - this also keeps delays and busy-checks interleaved
+  // in call order, so the emitter can attach a delay / wait-busy to the
+  // command it follows. While event-recording, every Trace method suppresses
+  // its printf and appends an Event instead (DC tracking via
+  // note_digital_write_for_dc still runs, so each SPI byte's is_command flag
+  // stays correct).
+  struct Event {
+    EventKind kind;
+    bool is_command = false;  // SPI_TRANSFER: DC low at transfer time
+    uint8_t byte = 0;         // SPI_TRANSFER
+    int pin = 0;              // DIGITAL_WRITE / DIGITAL_READ
+    int value = 0;            // DIGITAL_WRITE
+    unsigned long ms = 0;     // DELAY
+  };
+  static inline bool event_recording = false;
+  static inline std::vector<Event> events;
+  static void start_event_record() {
+    events.clear();
+    event_recording = true;
+  }
+  static void stop_event_record() { event_recording = false; }
 
 
-  static void digital_write(int pin, int value) { printf("digitalWrite(%d, %d)\n", pin, value); }
+  static void pin_mode(int pin, int mode) {
+    if (event_recording)
+      return;
+    printf("pinMode(%d, %d)\n", pin, mode);
+  }
+
+  static void digital_write(int pin, int value) {
+    if (event_recording) {
+      events.push_back({EventKind::DIGITAL_WRITE, false, 0, pin, value, 0});
+      return;
+    }
+    printf("digitalWrite(%d, %d)\n", pin, value);
+  }
 
 
   // Every digitalRead() in these drivers is a busy-pin poll - there's
   // Every digitalRead() in these drivers is a busy-pin poll - there's
   // nothing else wired as an input. Log it as such and always report
   // nothing else wired as an input. Log it as such and always report
   // "released" so _waitWhileBusy()'s loop exits after one check; we
   // "released" so _waitWhileBusy()'s loop exits after one check; we
   // don't need real timing, just to observe *that* a wait happened here.
   // don't need real timing, just to observe *that* a wait happened here.
   static int digital_read(int pin) {
   static int digital_read(int pin) {
+    if (event_recording) {
+      events.push_back({EventKind::DIGITAL_READ, false, 0, pin, 0, 0});
+      return last_busy_release_value;
+    }
     printf("digitalRead(%d) -> busy-check, reporting released\n", pin);
     printf("digitalRead(%d) -> busy-check, reporting released\n", pin);
     return last_busy_release_value;
     return last_busy_release_value;
   }
   }
 
 
-  static void delay_ms(unsigned long ms) { printf("delay(%lu)\n", ms); }
+  static void delay_ms(unsigned long ms) {
+    if (event_recording) {
+      events.push_back({EventKind::DELAY, false, 0, 0, 0, ms});
+      return;
+    }
+    printf("delay(%lu)\n", ms);
+  }
 
 
   static void spi_settings(uint32_t clock_hz, uint8_t bit_order, uint8_t data_mode) {
   static void spi_settings(uint32_t clock_hz, uint8_t bit_order, uint8_t data_mode) {
+    if (event_recording)
+      return;
     printf("SPISettings(clock_hz=%u, bit_order=%u, data_mode=%u)\n", (unsigned) clock_hz, bit_order, data_mode);
     printf("SPISettings(clock_hz=%u, bit_order=%u, data_mode=%u)\n", (unsigned) clock_hz, bit_order, data_mode);
   }
   }
 
 
@@ -40,7 +86,9 @@ struct Trace {
   // false = data byte - matches GxEPD2_EPD::_writeCommand()/_writeData()
   // false = data byte - matches GxEPD2_EPD::_writeCommand()/_writeData()
   // exactly (DC low while the command byte goes out, high for data).
   // exactly (DC low while the command byte goes out, high for data).
   static uint8_t spi_transfer(uint8_t byte) {
   static uint8_t spi_transfer(uint8_t byte) {
-    if (recording)
+    if (event_recording)
+      events.push_back({EventKind::SPI_TRANSFER, dc_is_low, byte, 0, 0, 0});  // is_command = DC low
+    else if (recording)
       record.push_back({dc_is_low, byte});  // (is_command, byte)
       record.push_back({dc_is_low, byte});  // (is_command, byte)
     else
     else
       printf("SPI.transfer(0x%02X) dc=%s\n", byte, dc_is_low ? "LOW(cmd)" : "HIGH(data)");
       printf("SPI.transfer(0x%02X) dc=%s\n", byte, dc_is_low ? "LOW(cmd)" : "HIGH(data)");