diff --git a/firmware/platformio.ini b/firmware/platformio.ini index 03bdeb0..4c91274 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -297,4 +297,92 @@ build_flags = -DEPD_WIDTH=400 -DEPD_HEIGHT=300 -DEPD_PANEL_42_GXEPD2_M01 - -DALLOW_INSECURE_FALLBACK=0 \ No newline at end of file + -DALLOW_INSECURE_FALLBACK=0 + +# ── Seeed XIAO ESP32S3 适配───────────────────────────────────────────────────────── +# 默认墨水屏接线参考 Seeed 官方的 XIAO ePaper driver board 原理图,使用软件SPI驱动时可按此接线; +# BUSY=GPIO4, RST=GPIO38, DC=GPIO10, CS=GPIO44, MOSI=GPIO9, SCK=GPIO7 +# XIAO ePaper driver board 原理图显示: +# BUTTON1 -> GPIO2, BUTTON2 -> GPIO3, BUTTON3 -> GPIO5 +# 默认配置按钮使用 BUTTON1: GPIO2 +# 根据原理图,板载 BAT_ADC 连接到 GPIO1 (D0/A0),与 EPD_DC 无冲突。 +# (旧注释曾错误地指向 GPIO10,实际上 GPIO10 是 EPD_DC 独占。) +# 如需自定义按钮/电池采样脚,可在 build_flags 追加覆盖: +# -D PIN_CFG_BTN= +# -D PIN_BAT_ADC= + +[env:epd_583_xiao_esp32s3] +extends = common +board = seeed_xiao_esp32s3 +# 5.83" black/white/yellow 3-color panel (UC8179) +lib_ignore = + GxEPD2 + Adafruit GFX Library + Adafruit BusIO +build_flags = + -DBOARD_PROFILE_XIAO_ESP32S3 + -D PIN_CFG_BTN=2 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DEPD_WIDTH=648 + -DEPD_HEIGHT=480 + -DEPD_PANEL_583_GDEY0583Z21 + -DEPD_BPP=2 + -DEPD_GXEPD2_SPI_HZ=1000000 + -DALLOW_INSECURE_FALLBACK=0 + +[env:epd_42_uc8176_xiao_esp32s3] +extends = common +board = seeed_xiao_esp32s3 +# 4.2" black/white/red 3-color panel (UC8176) +build_flags = + -DBOARD_PROFILE_XIAO_ESP32S3 + -D PIN_CFG_BTN=2 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DEPD_WIDTH=400 + -DEPD_HEIGHT=300 + -DEPD_PANEL_42_UC8176 + -DEPD_BPP=2 + -DEPD_GXEPD2_SPI_HZ=1000000 + -DALLOW_INSECURE_FALLBACK=0 + +[env:epd_42_hink_ssd1683_xiao_esp32s3] +extends = common +board = seeed_xiao_esp32s3 +# 4.2" HINK black/white/red 3-color panel (SSD1683) +lib_ignore = + GxEPD2 + Adafruit GFX Library + Adafruit BusIO +build_flags = + -DBOARD_PROFILE_XIAO_ESP32S3 + -D PIN_CFG_BTN=2 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DEPD_WIDTH=400 + -DEPD_HEIGHT=300 + -DEPD_PANEL_42_HINK_SSD1683 + -DEPD_BPP=2 + -DEPD_GXEPD2_SPI_HZ=1000000 + -DALLOW_INSECURE_FALLBACK=0 + +[env:epd_75_xiao_esp32s3] +extends = common +board = seeed_xiao_esp32s3 +# 7.5" black/white/red 3-color panel (UC8179) +lib_ignore = + GxEPD2 + Adafruit GFX Library + Adafruit BusIO +build_flags = + -DBOARD_PROFILE_XIAO_ESP32S3 + -D PIN_CFG_BTN=2 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DEPD_WIDTH=800 + -DEPD_HEIGHT=480 + -DEPD_PANEL_75_GDEY075Z08 + -DEPD_BPP=2 + -DEPD_GXEPD2_SPI_HZ=1000000 + -DALLOW_INSECURE_FALLBACK=0 diff --git a/firmware/src/config.h b/firmware/src/config.h index 6e37d55..90f7912 100644 --- a/firmware/src/config.h +++ b/firmware/src/config.h @@ -23,6 +23,20 @@ #define PIN_BAT_ADC 35 #define PIN_CFG_BTN 0 #define PIN_LED 2 +#elif defined(BOARD_PROFILE_XIAO_ESP32S3) +#define PIN_EPD_MOSI 9 +#define PIN_EPD_SCK 7 +#define PIN_EPD_CS 44 +#define PIN_EPD_DC 10 +#define PIN_EPD_RST 38 +#define PIN_EPD_BUSY 4 +// BAT_ADC → GPIO1 via R28/R29 (1:1 divider), gated by ADC_EN on GPIO6 +// which controls a TPS22916 load switch between VBAT and the divider. +// Pull ADC_EN HIGH to enable sampling, LOW otherwise (saves ~quiescent current). +#define PIN_BAT_ADC 1 +#define PIN_BAT_ADC_EN 6 +#define PIN_CFG_BTN 2 +#define PIN_LED LED_BUILTIN #else #error "Unsupported board profile" #endif @@ -51,6 +65,15 @@ static const int IMG_BUF_LEN = ROW_BYTES * H; #ifndef EPD_BPP #define EPD_BPP 1 #endif + +#if defined(EPD_PANEL_75_GDEY075Z08) || defined(EPD_PANEL_583_GDEY0583Z21) || defined(EPD_PANEL_42_UC8176) || defined(EPD_PANEL_42_HINK_SSD1683) +#define EPD_COLOR_CAPABILITY 3 +#elif EPD_BPP >= 2 +#define EPD_COLOR_CAPABILITY 4 +#else +#define EPD_COLOR_CAPABILITY 2 +#endif + static const int COLOR_BUF_LEN = (W * H) / 4; // 2bpp: 4 pixels per byte // Shared framebuffers (defined in main.cpp) diff --git a/firmware/src/epd_driver.cpp b/firmware/src/epd_driver.cpp index 4c04424..7671142 100644 --- a/firmware/src/epd_driver.cpp +++ b/firmware/src/epd_driver.cpp @@ -1,6 +1,8 @@ #include "epd_driver.h" #include "config.h" +#if !defined(EPD_PANEL_75_GDEY075Z08) && !defined(EPD_PANEL_583_GDEY0583Z21) && !defined(EPD_PANEL_42_UC8176) && !defined(EPD_PANEL_42_HINK_SSD1683) + #if defined(EPD_PANEL_42_SSD1683_BW) || defined(EPD_PANEL_42_DKE_RY683) || defined(EPD_PANEL_42_GDEM042F52) // ── Software SPI (bit-bang) for 4.2" SSD1683 BW panels ── @@ -738,4 +740,6 @@ void epdSleep() { #endif } -#endif // EPD_PANEL_42_SSD1683_BW +#endif + +#endif diff --git a/firmware/src/epd_driver_gdey0583z21.cpp b/firmware/src/epd_driver_gdey0583z21.cpp new file mode 100644 index 0000000..0f0b577 --- /dev/null +++ b/firmware/src/epd_driver_gdey0583z21.cpp @@ -0,0 +1,234 @@ +#include "epd_driver.h" +#include "config.h" + +#if defined(EPD_PANEL_583_GDEY0583Z21) + +// GDEY0583Z21 5.83" Black/White/Yellow tri-color e-ink panel +// Init sequence follows the GDEQ0583Z31 (5.83" BWR) reference from GxEPD2, +// which is the tri-color sibling of the GDEQ0583T31 used in epd_583_wroom32e. +// Reference: GxEPD2 src/gdeq3c/GxEPD2_583c_GDEQ0583Z31.cpp + +#include + +#ifndef EPD_GXEPD2_SPI_HZ +#define EPD_GXEPD2_SPI_HZ 4000000 +#endif + +static bool gdey0583_initialized = false; + +static uint8_t* epdColorPlaneBuffer() { + return colorBuf + IMG_BUF_LEN; +} + +// ── Low-level SPI helpers ──────────────────────────────────── + +static void gdey0583BeginTransfer(bool data_mode) { + digitalWrite(PIN_EPD_DC, data_mode ? HIGH : LOW); + digitalWrite(PIN_EPD_CS, LOW); + SPI.beginTransaction(SPISettings(EPD_GXEPD2_SPI_HZ, MSBFIRST, SPI_MODE0)); +} + +static void gdey0583EndTransfer() { + SPI.endTransaction(); + digitalWrite(PIN_EPD_CS, HIGH); +} + +static void gdey0583WriteCommand(uint8_t cmd) { + gdey0583BeginTransfer(false); + SPI.transfer(cmd); + gdey0583EndTransfer(); +} + +static void gdey0583WriteData(uint8_t data) { + gdey0583BeginTransfer(true); + SPI.transfer(data); + gdey0583EndTransfer(); +} + +// BUSY=LOW means panel is busy; wait for HIGH (same polarity as GDEQ0583Z31) +static void gdey0583WaitBusy(unsigned long timeout_ms = 30000) { + const unsigned long t0 = millis(); + while (digitalRead(PIN_EPD_BUSY) == LOW) { + delay(10); + if (millis() - t0 > timeout_ms) { + Serial.println("GDEY0583Z21 busy timeout"); + return; + } + } +} + +// ── Controller init (follows GDEQ0583Z31 _InitDisplay) ────── +// Power on is NOT done here; it is done inside gdey0583Refresh() +// before the refresh command, matching GxEPD2's _Update_Full flow. + +static void gdey0583InitController() { + if (gdey0583_initialized) return; + + SPI.begin(PIN_EPD_SCK, -1, PIN_EPD_MOSI, PIN_EPD_CS); + + // Hardware reset + digitalWrite(PIN_EPD_RST, HIGH); delay(10); + digitalWrite(PIN_EPD_RST, LOW); delay(10); + digitalWrite(PIN_EPD_RST, HIGH); delay(10); + gdey0583WaitBusy(); + + gdey0583WriteCommand(0x00); // PSR: BWR/BWY 3-color mode, LUT from OTP + gdey0583WriteData(0x0F); // (GDEQ0583T31 BW uses 0x1F; 0x0F selects 3-color) + + gdey0583WriteCommand(0x50); // CDI: VCOM and data interval (from GDEQ0583Z31) + gdey0583WriteData(0x11); + gdey0583WriteData(0x07); + + gdey0583_initialized = true; +} + +// ── Partial RAM area (0x90 window command, GDEQ0583Z31 format) ─ + +static void gdey0583SetWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + const uint16_t xe = (x + w - 1) | 0x0007; + const uint16_t ye = y + h - 1; + x &= 0xFFF8; + + gdey0583WriteCommand(0x90); + gdey0583WriteData(x >> 8); + gdey0583WriteData(x & 0xFF); + gdey0583WriteData(xe >> 8); + gdey0583WriteData(xe & 0xFF); + gdey0583WriteData(y >> 8); + gdey0583WriteData(y & 0xFF); + gdey0583WriteData(ye >> 8); + gdey0583WriteData(ye & 0xFF); + gdey0583WriteData(0x01); +} + +// ── Image plane writers ────────────────────────────────────── + +static void gdey0583WritePlane(uint8_t command, const uint8_t* buffer, uint8_t fill, bool invert) { + gdey0583WriteCommand(command); + gdey0583BeginTransfer(true); + for (int i = 0; i < IMG_BUF_LEN; i++) { + uint8_t value = buffer ? buffer[i] : fill; + if (invert) value = ~value; + SPI.transfer(value); + } + gdey0583EndTransfer(); +} + +// Write black + color planes into panel RAM. +// 0x10 (black): standard polarity — decode 0=black matches panel 0=black, no invert. +// 0x13 (color): inverted polarity — decode 0=yellow but panel reads 1=yellow, so invert. +static void gdey0583WriteImage(const uint8_t* black_plane, const uint8_t* color_plane) { + gdey0583WriteCommand(0x91); // partial in + gdey0583SetWindow(0, 0, W, H); + gdey0583WritePlane(0x10, black_plane, 0xFF, false); // standard polarity: 0=black, 1=white + gdey0583WritePlane(0x13, color_plane, 0xFF, true); // inverted polarity: 1=yellow, 0=white + gdey0583WriteCommand(0x92); // partial out +} + +// ── Refresh (follows GDEQ0583Z31 _PowerOn + _Update_Full) ─── +// Power on is done here, immediately before the refresh command. +// Full refresh for tri-color takes up to 30 seconds. + +static void gdey0583Refresh() { + gdey0583WriteCommand(0x04); // power on + gdey0583WaitBusy(5000); // ~140ms typical (GDEQ0583Z31 power_on_time=140ms) + + gdey0583WriteCommand(0x12); // display refresh + delay(100); + gdey0583WaitBusy(35000); // tri-color full refresh up to 30s (Z31 full_refresh_time=30000ms) +} + +// ── 2bpp decode ────────────────────────────────────────────── +// Reads raw2bpp backwards so that color_plane (upper half of colorBuf) +// does not clobber unread 2bpp source bytes. + +static void decodeRaw2bppToTriColorPlanes(const uint8_t* raw2bpp, uint8_t* black_plane, uint8_t* color_plane) { + memset(black_plane, 0xFF, IMG_BUF_LEN); + + for (int out = IMG_BUF_LEN - 1; out >= 0; out--) { + const uint8_t src0 = raw2bpp[out * 2]; + const uint8_t src1 = raw2bpp[out * 2 + 1]; + uint8_t black_byte = 0xFF; + uint8_t color_byte = 0xFF; + + for (int px = 0; px < 4; px++) { + const uint8_t code = (src0 >> (6 - px * 2)) & 0x03; + const uint8_t mask = 0x80 >> px; + if (code == 0x00) black_byte &= ~mask; + else if (code >= 0x02) color_byte &= ~mask; + } + for (int px = 0; px < 4; px++) { + const uint8_t code = (src1 >> (6 - px * 2)) & 0x03; + const uint8_t mask = 0x08 >> px; + if (code == 0x00) black_byte &= ~mask; + else if (code >= 0x02) color_byte &= ~mask; + } + + black_plane[out] = black_byte; + color_plane[out] = color_byte; + } +} + +static void epdDisplayPreparedPlanes(const uint8_t* black_plane, const uint8_t* color_plane) { + gdey0583InitController(); + gdey0583WriteImage(black_plane, color_plane); + gdey0583Refresh(); +} + +static void epdDisplayMonoFrame(const uint8_t* image) { + memset(epdColorPlaneBuffer(), 0xFF, IMG_BUF_LEN); + epdDisplayPreparedPlanes(image, epdColorPlaneBuffer()); +} + +// ── Public epd* contract ───────────────────────────────────── + +void gpioInit() { + pinMode(PIN_EPD_BUSY, INPUT); + pinMode(PIN_EPD_RST, OUTPUT); + pinMode(PIN_EPD_DC, OUTPUT); + pinMode(PIN_EPD_CS, OUTPUT); + pinMode(PIN_EPD_SCK, OUTPUT); + pinMode(PIN_EPD_MOSI, OUTPUT); + pinMode(PIN_CFG_BTN, INPUT_PULLUP); + digitalWrite(PIN_EPD_RST, HIGH); + digitalWrite(PIN_EPD_CS, HIGH); + digitalWrite(PIN_EPD_SCK, LOW); +} + +void epdInit() { + gdey0583InitController(); +} + +void epdInitFast() { + epdInit(); +} + +void epdDisplay(const uint8_t* image) { + epdDisplayMonoFrame(image); +} + +void epdDisplay2bpp(const uint8_t* image2bpp) { + decodeRaw2bppToTriColorPlanes(image2bpp, imgBuf, epdColorPlaneBuffer()); + epdDisplayPreparedPlanes(imgBuf, epdColorPlaneBuffer()); +} + +void epdDisplayFast(const uint8_t* image) { + epdDisplay(image); +} + +void epdPartialDisplay(uint8_t* data, int xStart, int yStart, int xEnd, int yEnd) { + (void)data; (void)xStart; (void)yStart; (void)xEnd; (void)yEnd; + epdDisplay(imgBuf); +} + +void epdSleep() { + if (!gdey0583_initialized) return; + gdey0583WriteCommand(0x02); // power off (GDEQ0583Z31 _PowerOff) + gdey0583WaitBusy(5000); + gdey0583WriteCommand(0x07); // deep sleep (GDEQ0583Z31 hibernate) + gdey0583WriteData(0xA5); + delay(20); + gdey0583_initialized = false; +} + +#endif diff --git a/firmware/src/epd_driver_hink_ssd1683.cpp b/firmware/src/epd_driver_hink_ssd1683.cpp new file mode 100644 index 0000000..5f978b7 --- /dev/null +++ b/firmware/src/epd_driver_hink_ssd1683.cpp @@ -0,0 +1,259 @@ +#include "epd_driver.h" +#include "config.h" + +#if defined(EPD_PANEL_42_HINK_SSD1683) + +// HINK 4.2" Black/White/Red tri-color e-ink panel +// Controller: SSD1683 +// Reference: GxEPD2 src/gdey3c/GxEPD2_420c_GDEY042Z98.cpp (SSD1683) +// BUSY=HIGH means busy; wait for LOW. + +#include + +#ifndef EPD_GXEPD2_SPI_HZ +#define EPD_GXEPD2_SPI_HZ 4000000 +#endif + +static bool ssd1683_initialized = false; + +static uint8_t* epdColorPlaneBuffer() { + return colorBuf + IMG_BUF_LEN; +} + +// ── Low-level SPI helpers ──────────────────────────────────── + +static void ssd1683BeginTransfer(bool data_mode) { + digitalWrite(PIN_EPD_DC, data_mode ? HIGH : LOW); + digitalWrite(PIN_EPD_CS, LOW); + SPI.beginTransaction(SPISettings(EPD_GXEPD2_SPI_HZ, MSBFIRST, SPI_MODE0)); +} + +static void ssd1683EndTransfer() { + SPI.endTransaction(); + digitalWrite(PIN_EPD_CS, HIGH); +} + +static void ssd1683WriteCommand(uint8_t cmd) { + ssd1683BeginTransfer(false); + SPI.transfer(cmd); + ssd1683EndTransfer(); +} + +static void ssd1683WriteData(uint8_t data) { + ssd1683BeginTransfer(true); + SPI.transfer(data); + ssd1683EndTransfer(); +} + +static void ssd1683WaitBusy(unsigned long timeout_ms = 30000) { + const unsigned long t0 = millis(); + while (digitalRead(PIN_EPD_BUSY) == HIGH) { + delay(10); + if (millis() - t0 > timeout_ms) { + Serial.println("SSD1683 busy timeout"); + return; + } + } +} + +static void ssd1683Reset() { + delay(20); + digitalWrite(PIN_EPD_RST, LOW); + delay(20); + digitalWrite(PIN_EPD_RST, HIGH); + delay(130); +} + +// ── 2bpp decode ────────────────────────────────────────────── +// raw2bpp lives in colorBuf[0..COLOR_BUF_LEN-1]. +// color_plane reuses the upper half (colorBuf + IMG_BUF_LEN). +// DO NOT memset color_plane: it would clobber unread 2bpp source bytes +// for out >= IMG_BUF_LEN/2, producing a solid-black bottom half. +// Active-LOW convention (matches uc8179/gdey0583): 0=black/color, 1=white/no-color. +// Loop runs backward so writes to color_plane never overtake reads from raw2bpp. + +static void decodeRaw2bppToTriColorPlanes(const uint8_t* raw2bpp, uint8_t* black_plane, uint8_t* color_plane) { + memset(black_plane, 0xFF, IMG_BUF_LEN); + + for (int out = IMG_BUF_LEN - 1; out >= 0; out--) { + const uint8_t src0 = raw2bpp[out * 2]; + const uint8_t src1 = raw2bpp[out * 2 + 1]; + uint8_t black_byte = 0xFF; + uint8_t color_byte = 0xFF; + + for (int px = 0; px < 4; px++) { + const uint8_t code = (src0 >> (6 - px * 2)) & 0x03; + const uint8_t mask = 0x80 >> px; + if (code == 0x00) black_byte &= ~mask; + else if (code >= 0x02) color_byte &= ~mask; + } + for (int px = 0; px < 4; px++) { + const uint8_t code = (src1 >> (6 - px * 2)) & 0x03; + const uint8_t mask = 0x08 >> px; + if (code == 0x00) black_byte &= ~mask; + else if (code >= 0x02) color_byte &= ~mask; + } + + black_plane[out] = black_byte; + color_plane[out] = color_byte; + } +} + +// ── RAM window + pointer reset ─────────────────────────────── +// Called before each plane write (matches GDEY042Z98/SSD1683 reference). +// SSD1683 requires full window re-declaration to correctly reset +// the Y address counter after a previous plane write. + +static void ssd1683SetFullWindowAndPointer() { + ssd1683WriteCommand(0x11); // data entry mode: X inc, Y inc + ssd1683WriteData(0x03); + ssd1683WriteCommand(0x44); // RAM X window + ssd1683WriteData(0x00); + ssd1683WriteData((W - 1) / 8); + ssd1683WriteCommand(0x45); // RAM Y window + ssd1683WriteData(0x00); + ssd1683WriteData(0x00); + ssd1683WriteData((H - 1) & 0xFF); + ssd1683WriteData(((H - 1) >> 8) & 0xFF); + ssd1683WriteCommand(0x4E); // X address counter + ssd1683WriteData(0x00); + ssd1683WriteCommand(0x4F); // Y address counter + ssd1683WriteData(0x00); + ssd1683WriteData(0x00); +} + +// ── Controller init (follows GDEY042Z98/SSD1683 _InitDisplay) ─ + +static void ssd1683InitController() { + if (ssd1683_initialized) return; + + const uint16_t y_end = H - 1; + + SPI.begin(PIN_EPD_SCK, -1, PIN_EPD_MOSI, PIN_EPD_CS); + ssd1683Reset(); + + ssd1683WriteCommand(0x12); // software reset + delay(10); // SSD1683 datasheet: wait ≥10ms after SWRESET + + ssd1683WriteCommand(0x01); // driver output control + ssd1683WriteData(y_end & 0xFF); + ssd1683WriteData((y_end >> 8) & 0xFF); + ssd1683WriteData(0x00); + + ssd1683WriteCommand(0x3C); // border waveform + ssd1683WriteData(0x05); + + ssd1683WriteCommand(0x18); // internal temperature sensor + ssd1683WriteData(0x80); + + ssd1683SetFullWindowAndPointer(); + + ssd1683_initialized = true; +} + +// ── Frame write ────────────────────────────────────────────── +// 0x24 BW RAM: active-LOW (0=black, 1=white) +// 0x26 color RAM: active-HIGH (1=red/color, 0=no-color) +// black_plane/color_plane from decode: active-LOW (0=black/color, 1=white/no-color) +// → 0x24: write directly; 0x26: invert before writing. + +static void ssd1683WriteFrame(const uint8_t* black_plane, const uint8_t* color_plane) { + ssd1683InitController(); + + ssd1683SetFullWindowAndPointer(); + ssd1683WriteCommand(0x26); // color RAM (active-HIGH) ← invert active-LOW color_plane + ssd1683BeginTransfer(true); + for (int i = 0; i < IMG_BUF_LEN; i++) { + SPI.transfer(color_plane ? static_cast(~color_plane[i]) : 0x00); + } + ssd1683EndTransfer(); + + ssd1683SetFullWindowAndPointer(); + ssd1683WriteCommand(0x24); // BW RAM (active-LOW) ← write directly + ssd1683BeginTransfer(true); + for (int i = 0; i < IMG_BUF_LEN; i++) { + SPI.transfer(black_plane ? black_plane[i] : 0xFF); + } + ssd1683EndTransfer(); + + ssd1683WriteCommand(0x22); // display update sequence: clock+analog, load LUT+temp, display, disable + ssd1683WriteData(0xF7); + ssd1683WriteCommand(0x20); // master activation + ssd1683WaitBusy(); +} + +static void ssd1683DisplayMonoFrame(const uint8_t* image) { + ssd1683InitController(); + + ssd1683SetFullWindowAndPointer(); + ssd1683WriteCommand(0x26); // color RAM: all 0x00 = no color + ssd1683BeginTransfer(true); + for (int i = 0; i < IMG_BUF_LEN; i++) { + SPI.transfer(0x00); + } + ssd1683EndTransfer(); + + ssd1683SetFullWindowAndPointer(); + ssd1683WriteCommand(0x24); // BW RAM: image is 0=black (active-LOW), write directly + ssd1683BeginTransfer(true); + for (int i = 0; i < IMG_BUF_LEN; i++) { + SPI.transfer(image[i]); + } + ssd1683EndTransfer(); + + ssd1683WriteCommand(0x22); + ssd1683WriteData(0xF7); + ssd1683WriteCommand(0x20); + ssd1683WaitBusy(); +} + +// ── Public epd* contract ───────────────────────────────────── + +void gpioInit() { + pinMode(PIN_EPD_BUSY, INPUT); + pinMode(PIN_EPD_RST, OUTPUT); + pinMode(PIN_EPD_DC, OUTPUT); + pinMode(PIN_EPD_CS, OUTPUT); + pinMode(PIN_EPD_SCK, OUTPUT); + pinMode(PIN_EPD_MOSI, OUTPUT); + pinMode(PIN_CFG_BTN, INPUT_PULLUP); + digitalWrite(PIN_EPD_RST, HIGH); + digitalWrite(PIN_EPD_CS, HIGH); + digitalWrite(PIN_EPD_SCK, LOW); +} + +void epdInit() { + ssd1683InitController(); +} + +void epdInitFast() { + epdInit(); +} + +void epdDisplay(const uint8_t* image) { + ssd1683DisplayMonoFrame(image); +} + +void epdDisplay2bpp(const uint8_t* image2bpp) { + decodeRaw2bppToTriColorPlanes(image2bpp, imgBuf, epdColorPlaneBuffer()); + ssd1683WriteFrame(imgBuf, epdColorPlaneBuffer()); +} + +void epdDisplayFast(const uint8_t* image) { + epdDisplay(image); +} + +void epdPartialDisplay(uint8_t* data, int xStart, int yStart, int xEnd, int yEnd) { + (void)data; (void)xStart; (void)yStart; (void)xEnd; (void)yEnd; + epdDisplay(imgBuf); +} + +void epdSleep() { + if (!ssd1683_initialized) return; + ssd1683WriteCommand(0x10); // deep sleep + ssd1683WriteData(0x11); + delay(20); + ssd1683_initialized = false; +} + +#endif diff --git a/firmware/src/epd_driver_uc8176.cpp b/firmware/src/epd_driver_uc8176.cpp new file mode 100644 index 0000000..11729ec --- /dev/null +++ b/firmware/src/epd_driver_uc8176.cpp @@ -0,0 +1,118 @@ +#include "epd_driver.h" +#include "config.h" + +#if defined(EPD_PANEL_42_UC8176) + +#include +#include +#include + +#ifndef EPD_GXEPD2_SPI_HZ +#define EPD_GXEPD2_SPI_HZ 4000000 +#endif + +// Use the 4.2" UC8176 tri-color path for 400x300 black/white/red panels. +static GxEPD2_3C display( + GxEPD2_420c(PIN_EPD_CS, PIN_EPD_DC, PIN_EPD_RST, PIN_EPD_BUSY)); + +static bool uc8176_initialized = false; + +static uint8_t* epdColorPlaneBuffer() { + return colorBuf + IMG_BUF_LEN; +} + +static void decodeRaw2bppToTriColorPlanes(const uint8_t* raw2bpp, uint8_t* black_plane, uint8_t* color_plane) { + // raw2bpp lives in colorBuf(), and color_plane reuses its upper half. + // Clearing color_plane here would destroy unread 2bpp source bytes. + memset(black_plane, 0xFF, IMG_BUF_LEN); + + for (int out = IMG_BUF_LEN - 1; out >= 0; out--) { + const uint8_t src0 = raw2bpp[out * 2]; + const uint8_t src1 = raw2bpp[out * 2 + 1]; + uint8_t black_byte = 0xFF; + uint8_t color_byte = 0xFF; + + for (int px = 0; px < 4; px++) { + const uint8_t code = (src0 >> (6 - px * 2)) & 0x03; + const uint8_t mask = 0x80 >> px; + if (code == 0x00) { + black_byte &= ~mask; + } else if (code >= 0x02) { + color_byte &= ~mask; + } + } + for (int px = 0; px < 4; px++) { + const uint8_t code = (src1 >> (6 - px * 2)) & 0x03; + const uint8_t mask = 0x08 >> px; + if (code == 0x00) { + black_byte &= ~mask; + } else if (code >= 0x02) { + color_byte &= ~mask; + } + } + + black_plane[out] = black_byte; + color_plane[out] = color_byte; + } +} + +static void uc8176WriteFrame(const uint8_t* black_plane, const uint8_t* color_plane) { + display.writeImage(black_plane, color_plane, 0, 0, W, H, false, false, false); + display.refresh(false); + display.powerOff(); +} + +static void uc8176DisplayMonoFrame(const uint8_t* image) { + memset(epdColorPlaneBuffer(), 0xFF, IMG_BUF_LEN); + uc8176WriteFrame(image, epdColorPlaneBuffer()); +} + +void gpioInit() { + pinMode(PIN_CFG_BTN, INPUT_PULLUP); + SPI.begin(PIN_EPD_SCK, -1, PIN_EPD_MOSI, PIN_EPD_CS); +} + +void epdInit() { + if (uc8176_initialized) return; + + display.epd2.selectSPI(SPI, SPISettings(EPD_GXEPD2_SPI_HZ, MSBFIRST, SPI_MODE0)); + display.init(0); + display.setRotation(0); + uc8176_initialized = true; +} + +void epdInitFast() { + epdInit(); +} + +void epdDisplay(const uint8_t* image) { + epdInit(); + uc8176DisplayMonoFrame(image); +} + +void epdDisplay2bpp(const uint8_t* image2bpp) { + epdInit(); + decodeRaw2bppToTriColorPlanes(image2bpp, imgBuf, epdColorPlaneBuffer()); + uc8176WriteFrame(imgBuf, epdColorPlaneBuffer()); +} + +void epdDisplayFast(const uint8_t* image) { + epdDisplay(image); +} + +void epdPartialDisplay(uint8_t* data, int xStart, int yStart, int xEnd, int yEnd) { + (void)data; + (void)xStart; + (void)yStart; + (void)xEnd; + (void)yEnd; + epdDisplay(imgBuf); +} + +void epdSleep() { + if (!uc8176_initialized) return; + display.hibernate(); + uc8176_initialized = false; +} + +#endif diff --git a/firmware/src/epd_driver_uc8179.cpp b/firmware/src/epd_driver_uc8179.cpp new file mode 100644 index 0000000..c9aec78 --- /dev/null +++ b/firmware/src/epd_driver_uc8179.cpp @@ -0,0 +1,262 @@ +#include "epd_driver.h" +#include "config.h" + +#if defined(EPD_PANEL_75_GDEY075Z08) + +#include + +#ifndef EPD_GXEPD2_SPI_HZ +#define EPD_GXEPD2_SPI_HZ 4000000 +#endif + +static bool uc8179_initialized = false; + +// The 7.5" UC8179 path still implements the same public epd* contract used by +// the rest of the project. These helpers only prepare the controller-specific +// black/red planes behind that interface. +static uint8_t* epdColorPlaneBuffer() { + return colorBuf + IMG_BUF_LEN; +} + +static void uc8179BeginTransfer(bool data_mode) { + digitalWrite(PIN_EPD_DC, data_mode ? HIGH : LOW); + digitalWrite(PIN_EPD_CS, LOW); + SPI.beginTransaction(SPISettings(EPD_GXEPD2_SPI_HZ, MSBFIRST, SPI_MODE0)); +} + +static void uc8179EndTransfer() { + SPI.endTransaction(); + digitalWrite(PIN_EPD_CS, HIGH); +} + +static void uc8179WriteCommand(uint8_t cmd) { + uc8179BeginTransfer(false); + SPI.transfer(cmd); + uc8179EndTransfer(); +} + +static void uc8179WriteData(uint8_t data) { + uc8179BeginTransfer(true); + SPI.transfer(data); + uc8179EndTransfer(); +} + +static void uc8179WaitBusy(unsigned long timeout_ms = 30000) { + const unsigned long t0 = millis(); + while (digitalRead(PIN_EPD_BUSY) == LOW) { + delay(10); + if (millis() - t0 > timeout_ms) { + Serial.println("UC8179 busy timeout"); + return; + } + } +} + +static void uc8179Reset() { + digitalWrite(PIN_EPD_RST, HIGH); + delay(10); + digitalWrite(PIN_EPD_RST, LOW); + delay(10); + digitalWrite(PIN_EPD_RST, HIGH); + delay(10); +} + +static void uc8179WriteWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + const uint16_t xe = (x + w - 1) | 0x0007; + const uint16_t ye = y + h - 1; + x &= 0xFFF8; + + uc8179WriteCommand(0x90); + uc8179WriteData(x >> 8); + uc8179WriteData(x & 0xFF); + uc8179WriteData(xe >> 8); + uc8179WriteData(xe & 0xFF); + uc8179WriteData(y >> 8); + uc8179WriteData(y & 0xFF); + uc8179WriteData(ye >> 8); + uc8179WriteData(ye & 0xFF); + uc8179WriteData(0x00); +} + +static void uc8179PowerOn() { + uc8179WriteCommand(0x04); + uc8179WaitBusy(500); +} + +static void uc8179PowerOff() { + uc8179WriteCommand(0x02); + uc8179WaitBusy(500); +} + +static void uc8179InitController() { + if (uc8179_initialized) return; + + SPI.begin(PIN_EPD_SCK, -1, PIN_EPD_MOSI, PIN_EPD_CS); + + uc8179Reset(); + + uc8179WriteCommand(0x01); + uc8179WriteData(0x07); + uc8179WriteData(0x07); + uc8179WriteData(0x3F); + uc8179WriteData(0x3F); + + uc8179WriteCommand(0x00); + uc8179WriteData(0x0F); + + uc8179WriteCommand(0x61); + uc8179WriteData(W >> 8); + uc8179WriteData(W & 0xFF); + uc8179WriteData(H >> 8); + uc8179WriteData(H & 0xFF); + + uc8179WriteCommand(0x15); + uc8179WriteData(0x00); + + // Match the known-good UC8179 reference implementation. + uc8179WriteCommand(0x50); + uc8179WriteData(0x77); + + uc8179WriteCommand(0x60); + uc8179WriteData(0x22); + + uc8179_initialized = true; +} + +static void decodeRaw2bppToTriColorPlanes(const uint8_t* raw2bpp, uint8_t* black_plane, uint8_t* color_plane) { + // raw2bpp lives in colorBuf(), and color_plane reuses its upper half. + // Clearing color_plane here would destroy unread 2bpp source bytes and + // turns the lower part of mixed images into solid red (0xFF => 0b11). + memset(black_plane, 0xFF, IMG_BUF_LEN); + + for (int out = IMG_BUF_LEN - 1; out >= 0; out--) { + const uint8_t src0 = raw2bpp[out * 2]; + const uint8_t src1 = raw2bpp[out * 2 + 1]; + uint8_t black_byte = 0xFF; + uint8_t color_byte = 0xFF; + + for (int px = 0; px < 4; px++) { + const uint8_t code = (src0 >> (6 - px * 2)) & 0x03; + const uint8_t mask = 0x80 >> px; + if (code == 0x00) { + black_byte &= ~mask; + } else if (code >= 0x02) { + color_byte &= ~mask; + } + } + for (int px = 0; px < 4; px++) { + const uint8_t code = (src1 >> (6 - px * 2)) & 0x03; + const uint8_t mask = 0x08 >> px; + if (code == 0x00) { + black_byte &= ~mask; + } else if (code >= 0x02) { + color_byte &= ~mask; + } + } + + black_plane[out] = black_byte; + color_plane[out] = color_byte; + } +} + +static void uc8179WritePlane(uint8_t command, const uint8_t* buffer, uint8_t fill, bool invert = false) { + uc8179WriteCommand(command); + uc8179BeginTransfer(true); + for (int i = 0; i < IMG_BUF_LEN; i++) { + uint8_t value = buffer ? buffer[i] : fill; + if (invert) value = ~value; + SPI.transfer(value); + } + uc8179EndTransfer(); +} + +static void uc8179WriteImage(const uint8_t* black_plane, const uint8_t* color_plane, bool invert_color = false) { + uc8179WriteCommand(0x91); + uc8179WriteWindow(0, 0, W, H); + uc8179WritePlane(0x10, black_plane, 0xFF, false); + uc8179WritePlane(0x13, color_plane, 0xFF, invert_color); + uc8179WriteCommand(0x92); +} + +static void uc8179Refresh() { + uc8179PowerOn(); + uc8179WriteWindow(0, 0, W, H); + uc8179WriteCommand(0x12); + delay(100); + uc8179WaitBusy(30000); + uc8179PowerOff(); +} + +static void uc8179ClearWhite() { + uc8179WriteImage(nullptr, nullptr, false); + uc8179Refresh(); +} + +static void epdDisplayPreparedPlanes(const uint8_t* black_plane, const uint8_t* color_plane) { + uc8179InitController(); + uc8179WriteImage(black_plane, color_plane, false); + uc8179Refresh(); +} + +static void epdDisplayMonoFrame(const uint8_t* image) { + memset(epdColorPlaneBuffer(), 0xFF, IMG_BUF_LEN); + epdDisplayPreparedPlanes(image, epdColorPlaneBuffer()); +} + +void gpioInit() { + pinMode(PIN_EPD_BUSY, INPUT); + pinMode(PIN_EPD_RST, OUTPUT); + pinMode(PIN_EPD_DC, OUTPUT); + pinMode(PIN_EPD_CS, OUTPUT); + pinMode(PIN_EPD_SCK, OUTPUT); + pinMode(PIN_EPD_MOSI, OUTPUT); + pinMode(PIN_CFG_BTN, INPUT_PULLUP); + digitalWrite(PIN_EPD_RST, HIGH); + digitalWrite(PIN_EPD_CS, HIGH); + digitalWrite(PIN_EPD_SCK, LOW); +} + +void epdInit() { + uc8179InitController(); +} + +void epdInitFast() { + epdInit(); +} + +void epdDisplay(const uint8_t* image) { + epdDisplayMonoFrame(image); +} + +void epdDisplay2bpp(const uint8_t* image2bpp) { + decodeRaw2bppToTriColorPlanes(image2bpp, imgBuf, epdColorPlaneBuffer()); + epdDisplayPreparedPlanes(imgBuf, epdColorPlaneBuffer()); +} + +void epdDisplayFast(const uint8_t* image) { + // This panel path currently uses the same stable full-refresh flow for fast + // refresh requests, matching the project's existing "safe fallback" style. + epdDisplay(image); +} + +void epdPartialDisplay(uint8_t* data, int xStart, int yStart, int xEnd, int yEnd) { + (void)data; + (void)xStart; + (void)yStart; + (void)xEnd; + (void)yEnd; + // Partial refresh is not implemented for the UC8179 tri-color path yet, so + // keep using the project's existing full-frame fallback behavior. + epdDisplay(imgBuf); +} + +void epdSleep() { + if (!uc8179_initialized) return; + uc8179PowerOff(); + uc8179WriteCommand(0x07); + uc8179WriteData(0xA5); + delay(20); + uc8179_initialized = false; +} + +#endif diff --git a/firmware/src/network.cpp b/firmware/src/network.cpp index f6c1883..d75eeb6 100644 --- a/firmware/src/network.cpp +++ b/firmware/src/network.cpp @@ -27,6 +27,20 @@ static bool checkAbort() { static bool recoverDeviceTokenIfUnauthorized(int code); +// XIAO ePaper Monitor Kit gates the battery divider (VBAT→R28/R29→GPIO1) with a +// TPS22916 load switch controlled by ADC_EN on GPIO6. Assert HIGH before reading. +#if defined(BOARD_PROFILE_XIAO_ESP32S3) && defined(PIN_BAT_ADC_EN) +static void beginBatteryAdcSample() { + pinMode(PIN_BAT_ADC_EN, OUTPUT); + digitalWrite(PIN_BAT_ADC_EN, HIGH); + delay(2); // Settling: load switch turn-on + divider RC +} + +static void endBatteryAdcSample() { + digitalWrite(PIN_BAT_ADC_EN, LOW); +} +#endif + // ── WiFi connection ───────────────────────────────────────── bool connectWiFi() { @@ -106,14 +120,23 @@ bool connectWiFi() { // ── Battery voltage ───────────────────────────────────────── float readBatteryVoltage() { - const int SAMPLES = 16; - const int DISCARD = 2; // Discard highest and lowest outliers + const int SAMPLES = 64; + const int DISCARD = 8; // Discard highest and lowest outliers (top/bottom 1/8) int readings[SAMPLES]; +#if defined(BOARD_PROFILE_XIAO_ESP32S3) && defined(PIN_BAT_ADC_EN) + beginBatteryAdcSample(); +#endif + analogSetPinAttenuation(PIN_BAT_ADC, ADC_11db); + // Spread samples across ~32ms to average out low-frequency supply ripple + // (WiFi TX bursts, DC-DC switching) in addition to ADC noise. for (int i = 0; i < SAMPLES; i++) { readings[i] = analogRead(PIN_BAT_ADC); - delayMicroseconds(100); + delayMicroseconds(500); } +#if defined(BOARD_PROFILE_XIAO_ESP32S3) && defined(PIN_BAT_ADC_EN) + endBatteryAdcSample(); +#endif // Sort for outlier removal for (int i = 0; i < SAMPLES - 1; i++) @@ -406,11 +429,7 @@ bool fetchBMP(bool nextMode, bool *isFallback, bool *outForceRefresh) { float v = readBatteryVoltage(); String mac = WiFi.macAddress(); int rssi = WiFi.RSSI(); -#if EPD_BPP >= 2 - const int colorCapability = 4; -#else - const int colorCapability = 2; -#endif + const int colorCapability = EPD_COLOR_CAPABILITY; #if DEBUG_MODE int effectiveRefreshMin = DEBUG_REFRESH_MIN; #else