515 lines
16 KiB
C++
515 lines
16 KiB
C++
// ======================================================
|
|
// uDisplay_epd_panel.cpp - E-Paper Display Panel Implementation
|
|
// ======================================================
|
|
|
|
#include "uDisplay_EPD_panel.h"
|
|
#include <Arduino.h>
|
|
|
|
// EPD Command Definitions
|
|
static constexpr uint8_t DRIVER_OUTPUT_CONTROL = 0x01;
|
|
static constexpr uint8_t BOOSTER_SOFT_START_CONTROL = 0x0C;
|
|
static constexpr uint8_t GATE_SCAN_START_POSITION = 0x0F;
|
|
static constexpr uint8_t DEEP_SLEEP_MODE = 0x10;
|
|
static constexpr uint8_t DATA_ENTRY_MODE_SETTING = 0x11;
|
|
static constexpr uint8_t SW_RESET = 0x12;
|
|
static constexpr uint8_t TEMPERATURE_SENSOR_CONTROL = 0x1A;
|
|
static constexpr uint8_t MASTER_ACTIVATION = 0x20;
|
|
static constexpr uint8_t DISPLAY_UPDATE_CONTROL_1 = 0x21;
|
|
static constexpr uint8_t DISPLAY_UPDATE_CONTROL_2 = 0x22;
|
|
static constexpr uint8_t WRITE_RAM = 0x24;
|
|
static constexpr uint8_t WRITE_VCOM_REGISTER = 0x2C;
|
|
static constexpr uint8_t WRITE_LUT_REGISTER = 0x32;
|
|
static constexpr uint8_t SET_DUMMY_LINE_PERIOD = 0x3A;
|
|
static constexpr uint8_t SET_GATE_TIME = 0x3B;
|
|
static constexpr uint8_t BORDER_WAVEFORM_CONTROL = 0x3C;
|
|
static constexpr uint8_t SET_RAM_X_ADDRESS_START_END_POSITION = 0x44;
|
|
static constexpr uint8_t SET_RAM_Y_ADDRESS_START_END_POSITION = 0x45;
|
|
static constexpr uint8_t SET_RAM_X_ADDRESS_COUNTER = 0x4E;
|
|
static constexpr uint8_t SET_RAM_Y_ADDRESS_COUNTER = 0x4F;
|
|
static constexpr uint8_t TERMINATE_FRAME_READ_WRITE = 0xFF;
|
|
|
|
EPDPanel::EPDPanel(const EPDPanelConfig& config,
|
|
SPIController* spi_ctrl,
|
|
uint8_t* framebuffer)
|
|
: spi(spi_ctrl), cfg(config), fb_buffer(framebuffer), update_mode(0), rotation(0)
|
|
{
|
|
// Don't do automatic initialization here - let the descriptor init commands handle it
|
|
// The uDisplay framework will call send_spi_cmds() after panel creation
|
|
// which will handle reset, LUT setup, and initial display state
|
|
}
|
|
|
|
EPDPanel::~EPDPanel() {
|
|
// Panel doesn't own framebuffer or SPI controller
|
|
|
|
// Free owned LUT data
|
|
if (cfg.lut_full_data) {
|
|
free(cfg.lut_full_data);
|
|
}
|
|
|
|
if (cfg.lut_partial_data) {
|
|
free(cfg.lut_partial_data);
|
|
}
|
|
|
|
for (uint8_t i = 0; i < 5; i++) {
|
|
if (cfg.lut_array_data[i]) {
|
|
free(cfg.lut_array_data[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EPDPanel::delay_sync(int32_t ms) {
|
|
uint8_t busy_level = cfg.busy_invert ? LOW : HIGH;
|
|
uint32_t time = millis();
|
|
if (cfg.busy_pin >= 0) {
|
|
while (digitalRead(cfg.busy_pin) == busy_level) {
|
|
delay(1);
|
|
if ((millis() - time) > cfg.busy_timeout) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
delay(ms);
|
|
}
|
|
}
|
|
|
|
void EPDPanel::resetDisplay() {
|
|
if (cfg.reset_pin < 0) return;
|
|
|
|
pinMode(cfg.reset_pin, OUTPUT);
|
|
digitalWrite(cfg.reset_pin, HIGH);
|
|
delay(10);
|
|
digitalWrite(cfg.reset_pin, LOW);
|
|
delay(10);
|
|
digitalWrite(cfg.reset_pin, HIGH);
|
|
delay(10);
|
|
delay_sync(100); // Use delay_sync instead of waitBusy
|
|
}
|
|
|
|
void EPDPanel::waitBusy() {
|
|
// Deprecated - use delay_sync instead
|
|
delay_sync(cfg.update_time);
|
|
}
|
|
|
|
void EPDPanel::setLut(const uint8_t* lut, uint16_t len) {
|
|
if (!lut || len == 0) return;
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(WRITE_LUT_REGISTER);
|
|
for (uint16_t i = 0; i < len; i++) {
|
|
spi->writeData8(lut[i]);
|
|
}
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
}
|
|
|
|
void EPDPanel::setMemoryArea(int x_start, int y_start, int x_end, int y_end) {
|
|
int x_start1 = (x_start >> 3) & 0xFF;
|
|
int x_end1 = (x_end >> 3) & 0xFF;
|
|
int y_start1 = y_start & 0xFF;
|
|
int y_start2 = (y_start >> 8) & 0xFF;
|
|
int y_end1 = y_end & 0xFF;
|
|
int y_end2 = (y_end >> 8) & 0xFF;
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(SET_RAM_X_ADDRESS_START_END_POSITION);
|
|
spi->writeData8(x_start1);
|
|
spi->writeData8(x_end1);
|
|
|
|
spi->writeCommand(SET_RAM_Y_ADDRESS_START_END_POSITION);
|
|
if (cfg.ep_mode == 3) {
|
|
// ep_mode 3: reversed Y order
|
|
spi->writeData8(y_end1);
|
|
spi->writeData8(y_end2);
|
|
spi->writeData8(y_start1);
|
|
spi->writeData8(y_start2);
|
|
} else {
|
|
spi->writeData8(y_start1);
|
|
spi->writeData8(y_start2);
|
|
spi->writeData8(y_end1);
|
|
spi->writeData8(y_end2);
|
|
}
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
}
|
|
|
|
void EPDPanel::setMemoryPointer(int x, int y) {
|
|
int x1, y1, y2;
|
|
|
|
if (cfg.ep_mode == 3) {
|
|
x1 = (x >> 3) & 0xFF;
|
|
y--;
|
|
y1 = y & 0xFF;
|
|
y2 = (y >> 8) & 0xFF;
|
|
} else {
|
|
x1 = (x >> 3) & 0xFF;
|
|
y1 = y & 0xFF;
|
|
y2 = (y >> 8) & 0xFF;
|
|
}
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(SET_RAM_X_ADDRESS_COUNTER);
|
|
spi->writeData8(x1);
|
|
spi->writeCommand(SET_RAM_Y_ADDRESS_COUNTER);
|
|
spi->writeData8(y1);
|
|
spi->writeData8(y2);
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
}
|
|
|
|
void EPDPanel::clearFrameMemory(uint8_t color) {
|
|
setMemoryArea(0, 0, cfg.width - 1, cfg.height - 1);
|
|
setMemoryPointer(0, 0);
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(WRITE_RAM);
|
|
|
|
uint32_t pixel_count = (cfg.width * cfg.height) / 8;
|
|
for (uint32_t i = 0; i < pixel_count; i++) {
|
|
spi->writeData8(color);
|
|
}
|
|
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
}
|
|
|
|
void EPDPanel::displayFrame() {
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(DISPLAY_UPDATE_CONTROL_2);
|
|
spi->writeData8(0xC4);
|
|
spi->writeCommand(MASTER_ACTIVATION);
|
|
spi->writeData8(TERMINATE_FRAME_READ_WRITE);
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
|
|
delay_sync(cfg.update_time); // Use delay_sync with proper timing
|
|
}
|
|
|
|
void EPDPanel::drawAbsolutePixel(int x, int y, uint16_t color) {
|
|
// Bounds check using physical dimensions
|
|
if (x < 0 || x >= cfg.width || y < 0 || y >= cfg.height) {
|
|
return;
|
|
}
|
|
|
|
// CRITICAL: Must match Renderer::drawPixel() layout!
|
|
//
|
|
// Two rendering systems write to the SAME framebuffer:
|
|
// 1. Renderer::drawPixel() - used by DrawStringAt() for text (Splash Screen)
|
|
// 2. EPDPanel::drawPixel() - used by Adafruit_GFX for graphics (circles, lines)
|
|
//
|
|
// Both MUST use the same framebuffer layout: Y-column-wise
|
|
// Layout: fb[x + (y/8)*width] with bit position (y&7)
|
|
// This means 8 vertical pixels are stored in one byte.
|
|
//
|
|
// setFrameMemory() will convert Y-column to X-row format when sending to hardware.
|
|
|
|
if (color) {
|
|
fb_buffer[x + (y / 8) * cfg.width] |= (1 << (y & 7));
|
|
} else {
|
|
fb_buffer[x + (y / 8) * cfg.width] &= ~(1 << (y & 7));
|
|
}
|
|
}
|
|
|
|
// ===== UniversalPanel Interface Implementation =====
|
|
|
|
bool EPDPanel::drawPixel(int16_t x, int16_t y, uint16_t color) {
|
|
if (!fb_buffer) return false;
|
|
|
|
// Get rotated dimensions for bounds check
|
|
int16_t w = cfg.width, h = cfg.height;
|
|
if (rotation == 1 || rotation == 3) {
|
|
std::swap(w, h);
|
|
}
|
|
|
|
if ((x < 0) || (x >= w) || (y < 0) || (y >= h)) {
|
|
return false; // Out of bounds
|
|
}
|
|
|
|
// Apply rotation transformation using PHYSICAL dimensions (gxs/gys)
|
|
switch (rotation) {
|
|
case 1:
|
|
std::swap(x, y);
|
|
x = cfg.width - x - 1; // gxs
|
|
break;
|
|
case 2:
|
|
x = cfg.width - x - 1; // gxs
|
|
y = cfg.height - y - 1; // gys
|
|
break;
|
|
case 3:
|
|
std::swap(x, y);
|
|
y = cfg.height - y - 1; // gys
|
|
break;
|
|
}
|
|
|
|
// Convert color to monochrome and draw
|
|
drawAbsolutePixel(x, y, (color != 0) ? 1 : 0);
|
|
return true;
|
|
}
|
|
|
|
bool EPDPanel::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
|
|
// Use drawPixel to handle rotation properly
|
|
for (int16_t yp = y; yp < y + h; yp++) {
|
|
for (int16_t xp = x; xp < x + w; xp++) {
|
|
drawPixel(xp, yp, color);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool EPDPanel::pushColors(uint16_t *data, uint16_t len, bool first) {
|
|
// Convert RGB565 to monochrome and write to framebuffer
|
|
// Pixel is white if at least one of the 3 RGB components is above 50%
|
|
static constexpr uint16_t RGB16_TO_MONO = 0x8410;
|
|
|
|
if (!fb_buffer) return false;
|
|
|
|
// Write pixels to framebuffer based on window coordinates
|
|
// IMPORTANT: window coordinates are in LOGICAL (rotated) space,
|
|
// so we must use drawPixel (not drawAbsolutePixel) to apply rotation!
|
|
for (int16_t y = window_y1; y < window_y2 && len > 0; y++) {
|
|
for (int16_t x = window_x1; x < window_x2 && len > 0; x++, len--) {
|
|
uint16_t color = *data++;
|
|
// Convert to mono: white if any component > 50%
|
|
bool pixel = (color & RGB16_TO_MONO) ? true : false;
|
|
if (cfg.invert_colors) pixel = !pixel;
|
|
drawPixel(x, y, pixel ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
return true; // Handled by EPD panel
|
|
}
|
|
|
|
bool EPDPanel::setAddrWindow(int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
|
|
// Save window coordinates for pushColors
|
|
window_x1 = x0;
|
|
window_y1 = y0;
|
|
window_x2 = x1;
|
|
window_y2 = y1;
|
|
return true;
|
|
}
|
|
|
|
bool EPDPanel::drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color) {
|
|
while (w--) {
|
|
drawPixel(x, y, color);
|
|
x++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool EPDPanel::drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color) {
|
|
while (h--) {
|
|
drawPixel(x, y, color);
|
|
y++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool EPDPanel::displayOnff(int8_t on) {
|
|
// EPD doesn't have on/off in traditional sense
|
|
return true;
|
|
}
|
|
|
|
bool EPDPanel::invertDisplay(bool invert) {
|
|
// Toggle color inversion logic
|
|
cfg.invert_colors = invert;
|
|
|
|
// For EPD, we need to redraw the entire display when inversion changes
|
|
if (fb_buffer) {
|
|
// Invert the entire framebuffer
|
|
uint32_t byte_count = (cfg.width * cfg.height) / 8;
|
|
for (uint32_t i = 0; i < byte_count; i++) {
|
|
fb_buffer[i] = ~fb_buffer[i];
|
|
}
|
|
updateFrame();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool EPDPanel::setRotation(uint8_t rot) {
|
|
rotation = rot & 3; // Store rotation (0-3)
|
|
return true;
|
|
}
|
|
|
|
bool EPDPanel::updateFrame() {
|
|
if (!fb_buffer) return false;
|
|
|
|
// Handle different EPD modes
|
|
if (cfg.ep_mode == 1 || cfg.ep_mode == 3) {
|
|
// Mode 1 (2-LUT) or Mode 3 (command-based): Use descriptor command sequences
|
|
switch (update_mode) {
|
|
case 1: // DISPLAY_INIT_PARTIAL
|
|
if (cfg.epc_part_cnt && cfg.send_cmds_callback) {
|
|
cfg.send_cmds_callback(cfg.epcoffs_part, cfg.epc_part_cnt);
|
|
}
|
|
break;
|
|
case 2: // DISPLAY_INIT_FULL
|
|
if (cfg.epc_full_cnt && cfg.send_cmds_callback) {
|
|
cfg.send_cmds_callback(cfg.epcoffs_full, cfg.epc_full_cnt);
|
|
}
|
|
break;
|
|
default: // DISPLAY_INIT_MODE (0)
|
|
// Default: write framebuffer and display
|
|
setFrameMemory(fb_buffer, 0, 0, cfg.width, cfg.height);
|
|
displayFrame();
|
|
}
|
|
} else if (cfg.ep_mode == 2) {
|
|
// Mode 2 (5-LUT / 4.2" displays): Use internal displayFrame_42
|
|
displayFrame_42();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ===== ep_mode 2 Support (5-LUT mode) =====
|
|
|
|
void EPDPanel::setLuts() {
|
|
if (!cfg.lut_array || !cfg.lut_cnt) return;
|
|
|
|
for (uint8_t index = 0; index < 5; index++) {
|
|
if (cfg.lut_cmd[index] == 0 || !cfg.lut_array[index]) continue;
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(cfg.lut_cmd[index]);
|
|
for (uint8_t count = 0; count < cfg.lut_cnt[index]; count++) {
|
|
spi->writeData8(cfg.lut_array[index][count]);
|
|
}
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
}
|
|
}
|
|
|
|
void EPDPanel::clearFrame_42() {
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
|
|
spi->writeCommand(cfg.saw_1);
|
|
for (uint16_t j = 0; j < cfg.height; j++) {
|
|
for (uint16_t i = 0; i < cfg.width; i++) {
|
|
spi->writeData8(0xFF);
|
|
}
|
|
}
|
|
|
|
spi->writeCommand(cfg.saw_2);
|
|
for (uint16_t j = 0; j < cfg.height; j++) {
|
|
for (uint16_t i = 0; i < cfg.width; i++) {
|
|
spi->writeData8(0xFF);
|
|
}
|
|
}
|
|
|
|
spi->writeCommand(cfg.saw_3);
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
|
|
delay_sync(100);
|
|
}
|
|
|
|
void EPDPanel::displayFrame_42() {
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
|
|
spi->writeCommand(cfg.saw_1);
|
|
for(int i = 0; i < cfg.width / 8 * cfg.height; i++) {
|
|
spi->writeData8(0xFF);
|
|
}
|
|
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
delay(2);
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(cfg.saw_2);
|
|
for(int i = 0; i < cfg.width / 8 * cfg.height; i++) {
|
|
spi->writeData8(fb_buffer[i] ^ 0xff);
|
|
}
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
delay(2);
|
|
|
|
setLuts();
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(cfg.saw_3);
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
|
|
delay_sync(100);
|
|
}
|
|
|
|
// ===== Frame Memory Management =====
|
|
|
|
// Helper: Convert Y-column framebuffer to X-row format and send via SPI
|
|
// Y-column: fb[x + (y/8)*width] with bit (y&7) - 8 vertical pixels per byte
|
|
// X-row: 8 horizontal pixels per byte, MSB = leftmost pixel
|
|
void EPDPanel::sendYColumnAsXRow(const uint8_t* y_column_buffer, uint16_t buffer_width,
|
|
uint16_t rows, uint16_t cols_bytes) {
|
|
for (uint16_t row = 0; row < rows; row++) {
|
|
for (uint16_t x_byte = 0; x_byte < cols_bytes; x_byte++) {
|
|
uint8_t byte_out = 0;
|
|
for (uint8_t bit = 0; bit < 8; bit++) {
|
|
uint16_t x = x_byte * 8 + bit;
|
|
uint8_t pixel = (y_column_buffer[x + (row / 8) * buffer_width] >> (row & 7)) & 1;
|
|
if (pixel) byte_out |= (0x80 >> bit);
|
|
}
|
|
spi->writeData8(byte_out ^ 0xff);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EPDPanel::setFrameMemory(const uint8_t* image_buffer) {
|
|
setMemoryArea(0, 0, cfg.width - 1, cfg.height - 1);
|
|
setMemoryPointer(0, 0);
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(WRITE_RAM);
|
|
sendYColumnAsXRow(image_buffer, cfg.width, cfg.height, cfg.width / 8);
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
}
|
|
|
|
void EPDPanel::setFrameMemory(const uint8_t* image_buffer, uint16_t x, uint16_t y, uint16_t image_width, uint16_t image_height) {
|
|
if (!image_buffer) return;
|
|
|
|
// Align to 8-pixel boundary
|
|
x &= 0xFFF8;
|
|
image_width &= 0xFFF8;
|
|
|
|
uint16_t x_end = (x + image_width >= cfg.width) ? cfg.width - 1 : x + image_width - 1;
|
|
uint16_t y_end = (y + image_height >= cfg.height) ? cfg.height - 1 : y + image_height - 1;
|
|
|
|
// Full screen optimization
|
|
if (!x && !y && image_width == cfg.width && image_height == cfg.height) {
|
|
setFrameMemory(image_buffer);
|
|
return;
|
|
}
|
|
|
|
setMemoryArea(x, y, x_end, y_end);
|
|
setMemoryPointer(x, y);
|
|
|
|
spi->beginTransaction();
|
|
spi->csLow();
|
|
spi->writeCommand(WRITE_RAM);
|
|
sendYColumnAsXRow(image_buffer, image_width, y_end - y + 1, (x_end - x + 1) / 8);
|
|
spi->csHigh();
|
|
spi->endTransaction();
|
|
}
|
|
|
|
void EPDPanel::sendEPData() {
|
|
// EP_SEND_DATA (0x66) - used by some display.ini files (e.g. v2)
|
|
// Must also convert Y-column to X-row format like setFrameMemory()
|
|
sendYColumnAsXRow(fb_buffer, cfg.width, cfg.height, cfg.width / 8);
|
|
}
|
|
|
|
// ===== Update Mode Control =====
|
|
|
|
void EPDPanel::setUpdateMode(uint8_t mode) {
|
|
update_mode = mode;
|
|
}
|