|
@@ -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
|