- Add rename() method to SDCardManager for file/directory moves - Improve EInkDisplay grayscale handling
709 lines
24 KiB
C++
709 lines
24 KiB
C++
#include "EInkDisplay.h"
|
|
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <vector>
|
|
|
|
// SSD1677 command definitions
|
|
// Initialization and reset
|
|
#define CMD_SOFT_RESET 0x12 // Soft reset
|
|
#define CMD_BOOSTER_SOFT_START 0x0C // Booster soft-start control
|
|
#define CMD_DRIVER_OUTPUT_CONTROL 0x01 // Driver output control
|
|
#define CMD_BORDER_WAVEFORM 0x3C // Border waveform control
|
|
#define CMD_TEMP_SENSOR_CONTROL 0x18 // Temperature sensor control
|
|
|
|
// RAM and buffer management
|
|
#define CMD_DATA_ENTRY_MODE 0x11 // Data entry mode
|
|
#define CMD_SET_RAM_X_RANGE 0x44 // Set RAM X address range
|
|
#define CMD_SET_RAM_Y_RANGE 0x45 // Set RAM Y address range
|
|
#define CMD_SET_RAM_X_COUNTER 0x4E // Set RAM X address counter
|
|
#define CMD_SET_RAM_Y_COUNTER 0x4F // Set RAM Y address counter
|
|
#define CMD_WRITE_RAM_BW 0x24 // Write to BW RAM (current frame)
|
|
#define CMD_WRITE_RAM_RED 0x26 // Write to RED RAM (used for fast refresh)
|
|
#define CMD_AUTO_WRITE_BW_RAM 0x46 // Auto write BW RAM
|
|
#define CMD_AUTO_WRITE_RED_RAM 0x47 // Auto write RED RAM
|
|
|
|
// Display update and refresh
|
|
#define CMD_DISPLAY_UPDATE_CTRL1 0x21 // Display update control 1
|
|
#define CMD_DISPLAY_UPDATE_CTRL2 0x22 // Display update control 2
|
|
#define CMD_MASTER_ACTIVATION 0x20 // Master activation
|
|
#define CTRL1_NORMAL 0x00 // Normal mode - compare RED vs BW for partial
|
|
#define CTRL1_BYPASS_RED 0x40 // Bypass RED RAM (treat as 0) - for full refresh
|
|
|
|
// LUT and voltage settings
|
|
#define CMD_WRITE_LUT 0x32 // Write LUT
|
|
#define CMD_GATE_VOLTAGE 0x03 // Gate voltage
|
|
#define CMD_SOURCE_VOLTAGE 0x04 // Source voltage
|
|
#define CMD_WRITE_VCOM 0x2C // Write VCOM
|
|
#define CMD_WRITE_TEMP 0x1A // Write temperature
|
|
|
|
// Power management
|
|
#define CMD_DEEP_SLEEP 0x10 // Deep sleep
|
|
|
|
// Custom LUT for fast refresh
|
|
const unsigned char lut_grayscale[] PROGMEM = {
|
|
// 00 black/white
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// 01 light gray
|
|
0x54, 0x54, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// 10 gray
|
|
0xAA, 0xA0, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// 11 dark gray
|
|
0xA2, 0x22, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// L4 (VCOM)
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
// TP/RP groups (global timing)
|
|
0x01, 0x01, 0x01, 0x01, 0x00, // G0: A=1 B=1 C=1 D=1 RP=0 (4 frames)
|
|
0x01, 0x01, 0x01, 0x01, 0x00, // G1: A=1 B=1 C=1 D=1 RP=0 (4 frames)
|
|
0x01, 0x01, 0x01, 0x01, 0x00, // G2: A=0 B=0 C=0 D=0 RP=0 (4 frames)
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G3: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G4: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G5: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G6: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G7: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G8: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G9: A=0 B=0 C=0 D=0 RP=0
|
|
|
|
// Frame rate
|
|
0x8F, 0x8F, 0x8F, 0x8F, 0x8F,
|
|
|
|
// Voltages (VGH, VSH1, VSH2, VSL, VCOM)
|
|
0x17, 0x41, 0xA8, 0x32, 0x30,
|
|
|
|
// Reserved
|
|
0x00, 0x00};
|
|
|
|
const unsigned char lut_grayscale_revert[] PROGMEM = {
|
|
// 00 black/white
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// 10 gray
|
|
0x54, 0x54, 0x54, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// 01 light gray
|
|
0xA8, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// 11 dark gray
|
|
0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// L4 (VCOM)
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
// TP/RP groups (global timing)
|
|
0x01, 0x01, 0x01, 0x01, 0x01, // G0: A=1 B=1 C=1 D=1 RP=0 (4 frames)
|
|
0x01, 0x01, 0x01, 0x01, 0x01, // G1: A=1 B=1 C=1 D=1 RP=0 (4 frames)
|
|
0x01, 0x01, 0x01, 0x01, 0x00, // G2: A=0 B=0 C=0 D=0 RP=0 (4 frames)
|
|
0x01, 0x01, 0x01, 0x01, 0x00, // G3: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G4: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G5: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G6: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G7: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G8: A=0 B=0 C=0 D=0 RP=0
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // G9: A=0 B=0 C=0 D=0 RP=0
|
|
|
|
// Frame rate
|
|
0x8F, 0x8F, 0x8F, 0x8F, 0x8F,
|
|
|
|
// Voltages (VGH, VSH1, VSH2, VSL, VCOM)
|
|
0x17, 0x41, 0xA8, 0x32, 0x30,
|
|
|
|
// Reserved
|
|
0x00, 0x00};
|
|
|
|
EInkDisplay::EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy)
|
|
: _sclk(sclk),
|
|
_mosi(mosi),
|
|
_cs(cs),
|
|
_dc(dc),
|
|
_rst(rst),
|
|
_busy(busy),
|
|
frameBuffer(nullptr),
|
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
frameBufferActive(nullptr),
|
|
#endif
|
|
customLutActive(false) {
|
|
Serial.printf("[%lu] EInkDisplay: Constructor called\n", millis());
|
|
Serial.printf("[%lu] SCLK=%d, MOSI=%d, CS=%d, DC=%d, RST=%d, BUSY=%d\n", millis(), sclk, mosi, cs, dc, rst, busy);
|
|
}
|
|
|
|
void EInkDisplay::begin() {
|
|
Serial.printf("[%lu] EInkDisplay: begin() called\n", millis());
|
|
|
|
frameBuffer = frameBuffer0;
|
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
frameBufferActive = frameBuffer1;
|
|
#endif
|
|
|
|
// Initialize to white
|
|
memset(frameBuffer0, 0xFF, BUFFER_SIZE);
|
|
#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
Serial.printf("[%lu] Static frame buffer (%lu bytes = 48KB)\n", millis(), BUFFER_SIZE);
|
|
#else
|
|
memset(frameBuffer1, 0xFF, BUFFER_SIZE);
|
|
Serial.printf("[%lu] Static frame buffers (2 x %lu bytes = 96KB)\n", millis(), BUFFER_SIZE);
|
|
#endif
|
|
|
|
Serial.printf("[%lu] Initializing e-ink display driver...\n", millis());
|
|
|
|
// Initialize SPI with custom pins
|
|
SPI.begin(_sclk, -1, _mosi, _cs);
|
|
spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0); // MODE0 is standard for SSD1677
|
|
Serial.printf("[%lu] SPI initialized at 40 MHz, Mode 0\n", millis());
|
|
|
|
// Setup GPIO pins
|
|
pinMode(_cs, OUTPUT);
|
|
pinMode(_dc, OUTPUT);
|
|
pinMode(_rst, OUTPUT);
|
|
pinMode(_busy, INPUT);
|
|
|
|
digitalWrite(_cs, HIGH);
|
|
digitalWrite(_dc, HIGH);
|
|
|
|
Serial.printf("[%lu] GPIO pins configured\n", millis());
|
|
|
|
// Reset display
|
|
resetDisplay();
|
|
|
|
// Initialize display controller
|
|
initDisplayController();
|
|
|
|
Serial.printf("[%lu] E-ink display driver initialized\n", millis());
|
|
}
|
|
|
|
// ============================================================================
|
|
// Low-level display control methods
|
|
// ============================================================================
|
|
|
|
void EInkDisplay::resetDisplay() {
|
|
Serial.printf("[%lu] Resetting display...\n", millis());
|
|
digitalWrite(_rst, HIGH);
|
|
delay(20);
|
|
digitalWrite(_rst, LOW);
|
|
delay(2);
|
|
digitalWrite(_rst, HIGH);
|
|
delay(20);
|
|
Serial.printf("[%lu] Display reset complete\n", millis());
|
|
}
|
|
|
|
void EInkDisplay::sendCommand(uint8_t command) {
|
|
SPI.beginTransaction(spiSettings);
|
|
digitalWrite(_dc, LOW); // Command mode
|
|
digitalWrite(_cs, LOW); // Select chip
|
|
SPI.transfer(command);
|
|
digitalWrite(_cs, HIGH); // Deselect chip
|
|
SPI.endTransaction();
|
|
}
|
|
|
|
void EInkDisplay::sendData(uint8_t data) {
|
|
SPI.beginTransaction(spiSettings);
|
|
digitalWrite(_dc, HIGH); // Data mode
|
|
digitalWrite(_cs, LOW); // Select chip
|
|
SPI.transfer(data);
|
|
digitalWrite(_cs, HIGH); // Deselect chip
|
|
SPI.endTransaction();
|
|
}
|
|
|
|
void EInkDisplay::sendData(const uint8_t* data, uint16_t length) {
|
|
SPI.beginTransaction(spiSettings);
|
|
digitalWrite(_dc, HIGH); // Data mode
|
|
digitalWrite(_cs, LOW); // Select chip
|
|
SPI.writeBytes(data, length); // Transfer all bytes
|
|
digitalWrite(_cs, HIGH); // Deselect chip
|
|
SPI.endTransaction();
|
|
}
|
|
|
|
void EInkDisplay::waitWhileBusy(const char* comment) {
|
|
unsigned long start = millis();
|
|
while (digitalRead(_busy) == HIGH) {
|
|
delay(1);
|
|
if (millis() - start > 10000) {
|
|
Serial.printf("[%lu] Timeout waiting for busy%s\n", millis(), comment ? comment : "");
|
|
break;
|
|
}
|
|
}
|
|
if (comment) {
|
|
Serial.printf("[%lu] Wait complete: %s (%lu ms)\n", millis(), comment, millis() - start);
|
|
}
|
|
}
|
|
|
|
void EInkDisplay::initDisplayController() {
|
|
Serial.printf("[%lu] Initializing SSD1677 controller...\n", millis());
|
|
|
|
const uint8_t TEMP_SENSOR_INTERNAL = 0x80;
|
|
|
|
// Soft reset
|
|
sendCommand(CMD_SOFT_RESET);
|
|
waitWhileBusy(" CMD_SOFT_RESET");
|
|
|
|
// Temperature sensor control (internal)
|
|
sendCommand(CMD_TEMP_SENSOR_CONTROL);
|
|
sendData(TEMP_SENSOR_INTERNAL);
|
|
|
|
// Booster soft-start control (GDEQ0426T82 specific values)
|
|
sendCommand(CMD_BOOSTER_SOFT_START);
|
|
sendData(0xAE);
|
|
sendData(0xC7);
|
|
sendData(0xC3);
|
|
sendData(0xC0);
|
|
sendData(0x40);
|
|
|
|
// Driver output control: set display height (480) and scan direction
|
|
const uint16_t HEIGHT = 480;
|
|
sendCommand(CMD_DRIVER_OUTPUT_CONTROL);
|
|
sendData((HEIGHT - 1) % 256); // gates A0..A7 (low byte)
|
|
sendData((HEIGHT - 1) / 256); // gates A8..A9 (high byte)
|
|
sendData(0x02); // SM=1 (interlaced), TB=0
|
|
|
|
// Border waveform control
|
|
sendCommand(CMD_BORDER_WAVEFORM);
|
|
sendData(0x01);
|
|
|
|
// Set up full screen RAM area
|
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
|
|
|
Serial.printf("[%lu] Clearing RAM buffers...\n", millis());
|
|
sendCommand(CMD_AUTO_WRITE_BW_RAM); // Auto write BW RAM
|
|
sendData(0xF7);
|
|
waitWhileBusy(" CMD_AUTO_WRITE_BW_RAM");
|
|
|
|
sendCommand(CMD_AUTO_WRITE_RED_RAM); // Auto write RED RAM
|
|
sendData(0xF7); // Fill with white pattern
|
|
waitWhileBusy(" CMD_AUTO_WRITE_RED_RAM");
|
|
|
|
Serial.printf("[%lu] SSD1677 controller initialized\n", millis());
|
|
}
|
|
|
|
void EInkDisplay::setRamArea(const uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
|
constexpr uint8_t DATA_ENTRY_X_INC_Y_DEC = 0x01;
|
|
|
|
// Reverse Y coordinate (gates are reversed on this display)
|
|
y = DISPLAY_HEIGHT - y - h;
|
|
|
|
// Set data entry mode (X increment, Y decrement for reversed gates)
|
|
sendCommand(CMD_DATA_ENTRY_MODE);
|
|
sendData(DATA_ENTRY_X_INC_Y_DEC);
|
|
|
|
// Set RAM X address range (start, end) - X is in PIXELS
|
|
sendCommand(CMD_SET_RAM_X_RANGE);
|
|
sendData(x % 256); // start low byte
|
|
sendData(x / 256); // start high byte
|
|
sendData((x + w - 1) % 256); // end low byte
|
|
sendData((x + w - 1) / 256); // end high byte
|
|
|
|
// Set RAM Y address range (start, end) - Y is in PIXELS
|
|
sendCommand(CMD_SET_RAM_Y_RANGE);
|
|
sendData((y + h - 1) % 256); // start low byte
|
|
sendData((y + h - 1) / 256); // start high byte
|
|
sendData(y % 256); // end low byte
|
|
sendData(y / 256); // end high byte
|
|
|
|
// Set RAM X address counter - X is in PIXELS
|
|
sendCommand(CMD_SET_RAM_X_COUNTER);
|
|
sendData(x % 256); // low byte
|
|
sendData(x / 256); // high byte
|
|
|
|
// Set RAM Y address counter - Y is in PIXELS
|
|
sendCommand(CMD_SET_RAM_Y_COUNTER);
|
|
sendData((y + h - 1) % 256); // low byte
|
|
sendData((y + h - 1) / 256); // high byte
|
|
}
|
|
|
|
void EInkDisplay::clearScreen(const uint8_t color) const {
|
|
memset(frameBuffer, color, BUFFER_SIZE);
|
|
}
|
|
|
|
void EInkDisplay::drawImage(const uint8_t* imageData, const uint16_t x, const uint16_t y, const uint16_t w, const uint16_t h,
|
|
const bool fromProgmem) const {
|
|
if (!frameBuffer) {
|
|
Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis());
|
|
return;
|
|
}
|
|
|
|
// Calculate bytes per line for the image
|
|
const uint16_t imageWidthBytes = w / 8;
|
|
|
|
// Copy image data to frame buffer
|
|
for (uint16_t row = 0; row < h; row++) {
|
|
const uint16_t destY = y + row;
|
|
if (destY >= DISPLAY_HEIGHT)
|
|
break;
|
|
|
|
const uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8);
|
|
const uint16_t srcOffset = row * imageWidthBytes;
|
|
|
|
for (uint16_t col = 0; col < imageWidthBytes; col++) {
|
|
if ((x / 8 + col) >= DISPLAY_WIDTH_BYTES)
|
|
break;
|
|
|
|
if (fromProgmem) {
|
|
frameBuffer[destOffset + col] = pgm_read_byte(&imageData[srcOffset + col]);
|
|
} else {
|
|
frameBuffer[destOffset + col] = imageData[srcOffset + col];
|
|
}
|
|
}
|
|
}
|
|
|
|
Serial.printf("[%lu] Image drawn to frame buffer\n", millis());
|
|
}
|
|
|
|
void EInkDisplay::writeRamBuffer(uint8_t ramBuffer, const uint8_t* data, uint32_t size) {
|
|
const char* bufferName = (ramBuffer == CMD_WRITE_RAM_BW) ? "BW" : "RED";
|
|
const unsigned long startTime = millis();
|
|
Serial.printf("[%lu] Writing frame buffer to %s RAM (%lu bytes)...\n", startTime, bufferName, size);
|
|
|
|
sendCommand(ramBuffer);
|
|
sendData(data, size);
|
|
|
|
const unsigned long duration = millis() - startTime;
|
|
Serial.printf("[%lu] %s RAM write complete (%lu ms)\n", millis(), bufferName, duration);
|
|
}
|
|
|
|
void EInkDisplay::setFramebuffer(const uint8_t* bwBuffer) const {
|
|
memcpy(frameBuffer, bwBuffer, BUFFER_SIZE);
|
|
}
|
|
|
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
void EInkDisplay::swapBuffers() {
|
|
uint8_t* temp = frameBuffer;
|
|
frameBuffer = frameBufferActive;
|
|
frameBufferActive = temp;
|
|
}
|
|
#endif
|
|
|
|
void EInkDisplay::grayscaleRevert() {
|
|
if (!inGrayscaleMode) {
|
|
return;
|
|
}
|
|
|
|
inGrayscaleMode = false;
|
|
|
|
// Load the revert LUT
|
|
setCustomLUT(true, lut_grayscale_revert);
|
|
refreshDisplay(FAST_REFRESH);
|
|
setCustomLUT(false);
|
|
}
|
|
|
|
void EInkDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) {
|
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
|
writeRamBuffer(CMD_WRITE_RAM_BW, lsbBuffer, BUFFER_SIZE);
|
|
}
|
|
|
|
void EInkDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) {
|
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
|
writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, BUFFER_SIZE);
|
|
}
|
|
|
|
void EInkDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) {
|
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
|
writeRamBuffer(CMD_WRITE_RAM_BW, lsbBuffer, BUFFER_SIZE);
|
|
writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, BUFFER_SIZE);
|
|
}
|
|
|
|
#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
/**
|
|
* In single buffer mode, this should be called with the previously written BW buffer
|
|
* to restore proper BW state after a grayscale display.
|
|
*
|
|
* The approach: Don't call grayscaleRevert() at all. Instead, just sync the RAMs with
|
|
* BW data and clear the grayscale mode flag. The physical pixels will stay in their
|
|
* current grayscale states, but subsequent refreshes will naturally transition them
|
|
* as the new BW content is displayed.
|
|
*/
|
|
void EInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) {
|
|
// Clear grayscale mode - we don't want grayscaleRevert to be called later
|
|
// because the RAMs won't have valid grayscale data anymore
|
|
inGrayscaleMode = false;
|
|
|
|
// Sync both RAMs with BW content for proper fast refresh behavior
|
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
|
writeRamBuffer(CMD_WRITE_RAM_BW, bwBuffer, BUFFER_SIZE);
|
|
writeRamBuffer(CMD_WRITE_RAM_RED, bwBuffer, BUFFER_SIZE);
|
|
}
|
|
#endif
|
|
|
|
void EInkDisplay::displayBuffer(RefreshMode mode) {
|
|
if (!isScreenOn) {
|
|
// Force half refresh if screen is off
|
|
mode = HALF_REFRESH;
|
|
}
|
|
|
|
// If currently in grayscale mode, revert first to black/white
|
|
if (inGrayscaleMode) {
|
|
grayscaleRevert(); // grayscaleRevert() sets inGrayscaleMode = false internally
|
|
}
|
|
|
|
// Set up full screen RAM area
|
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
|
|
|
if (mode != FAST_REFRESH) {
|
|
// For full refresh, write to both buffers before refresh
|
|
writeRamBuffer(CMD_WRITE_RAM_BW, frameBuffer, BUFFER_SIZE);
|
|
writeRamBuffer(CMD_WRITE_RAM_RED, frameBuffer, BUFFER_SIZE);
|
|
} else {
|
|
// For fast refresh, write to BW buffer only
|
|
writeRamBuffer(CMD_WRITE_RAM_BW, frameBuffer, BUFFER_SIZE);
|
|
// In single buffer mode, the RED RAM should already contain the previous frame
|
|
// In dual buffer mode, we write back frameBufferActive which is the last frame
|
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
writeRamBuffer(CMD_WRITE_RAM_RED, frameBufferActive, BUFFER_SIZE);
|
|
#endif
|
|
}
|
|
|
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
swapBuffers();
|
|
#endif
|
|
|
|
// Refresh the display
|
|
refreshDisplay(mode);
|
|
|
|
#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
// In single buffer mode always sync RED RAM after refresh to prepare for next fast refresh
|
|
// This ensures RED contains the currently displayed frame for differential comparison
|
|
setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
|
writeRamBuffer(CMD_WRITE_RAM_RED, frameBuffer, BUFFER_SIZE);
|
|
#endif
|
|
}
|
|
|
|
// EXPERIMENTAL: Windowed update support
|
|
// Displays only a rectangular region of the frame buffer, preserving the rest of the screen.
|
|
// Requirements: x and w must be byte-aligned (multiples of 8 pixels)
|
|
void EInkDisplay::displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
|
Serial.printf("[%lu] Displaying window at (%d,%d) size (%dx%d)\n", millis(), x, y, w, h);
|
|
|
|
// Validate bounds
|
|
if (x + w > DISPLAY_WIDTH || y + h > DISPLAY_HEIGHT) {
|
|
Serial.printf("[%lu] ERROR: Window bounds exceed display dimensions!\n", millis());
|
|
return;
|
|
}
|
|
|
|
// Validate byte alignment
|
|
if (x % 8 != 0 || w % 8 != 0) {
|
|
Serial.printf("[%lu] ERROR: Window x and width must be byte-aligned (multiples of 8)!\n", millis());
|
|
return;
|
|
}
|
|
|
|
if (!frameBuffer) {
|
|
Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis());
|
|
return;
|
|
}
|
|
|
|
// displayWindow is not supported while the rest of the screen has grayscale content, revert it
|
|
if (inGrayscaleMode) {
|
|
grayscaleRevert(); // grayscaleRevert() sets inGrayscaleMode = false internally
|
|
}
|
|
|
|
// Calculate window buffer size
|
|
const uint16_t windowWidthBytes = w / 8;
|
|
const uint32_t windowBufferSize = windowWidthBytes * h;
|
|
|
|
Serial.printf("[%lu] Window buffer size: %lu bytes (%d x %d pixels)\n", millis(), windowBufferSize, w, h);
|
|
|
|
// Allocate temporary buffer on stack
|
|
std::vector<uint8_t> windowBuffer(windowBufferSize);
|
|
|
|
// Extract window region from frame buffer
|
|
for (uint16_t row = 0; row < h; row++) {
|
|
const uint16_t srcY = y + row;
|
|
const uint16_t srcOffset = srcY * DISPLAY_WIDTH_BYTES + (x / 8);
|
|
const uint16_t dstOffset = row * windowWidthBytes;
|
|
memcpy(&windowBuffer[dstOffset], &frameBuffer[srcOffset], windowWidthBytes);
|
|
}
|
|
|
|
// Configure RAM area for window
|
|
setRamArea(x, y, w, h);
|
|
|
|
// Write to BW RAM (current frame)
|
|
writeRamBuffer(CMD_WRITE_RAM_BW, windowBuffer.data(), windowBufferSize);
|
|
|
|
#ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
// Dual buffer: Extract window from frameBufferActive (previous frame)
|
|
std::vector<uint8_t> previousWindowBuffer(windowBufferSize);
|
|
for (uint16_t row = 0; row < h; row++) {
|
|
const uint16_t srcY = y + row;
|
|
const uint16_t srcOffset = srcY * DISPLAY_WIDTH_BYTES + (x / 8);
|
|
const uint16_t dstOffset = row * windowWidthBytes;
|
|
memcpy(&previousWindowBuffer[dstOffset], &frameBufferActive[srcOffset], windowWidthBytes);
|
|
}
|
|
writeRamBuffer(CMD_WRITE_RAM_RED, previousWindowBuffer.data(), windowBufferSize);
|
|
#endif
|
|
|
|
// Perform fast refresh
|
|
refreshDisplay(FAST_REFRESH);
|
|
|
|
#ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE
|
|
// Post-refresh: Sync RED RAM with current window (for next fast refresh)
|
|
setRamArea(x, y, w, h);
|
|
writeRamBuffer(CMD_WRITE_RAM_RED, windowBuffer.data(), windowBufferSize);
|
|
#endif
|
|
|
|
Serial.printf("[%lu] Window display complete\n", millis());
|
|
}
|
|
|
|
void EInkDisplay::displayGrayBuffer(const bool turnOffScreen) {
|
|
drawGrayscale = false;
|
|
inGrayscaleMode = true;
|
|
|
|
// activate the custom LUT for grayscale rendering and refresh
|
|
setCustomLUT(true, lut_grayscale);
|
|
refreshDisplay(FAST_REFRESH, turnOffScreen);
|
|
setCustomLUT(false);
|
|
}
|
|
|
|
void EInkDisplay::refreshDisplay(const RefreshMode mode, const bool turnOffScreen) {
|
|
// Configure Display Update Control 1
|
|
sendCommand(CMD_DISPLAY_UPDATE_CTRL1);
|
|
sendData((mode == FAST_REFRESH) ? CTRL1_NORMAL : CTRL1_BYPASS_RED); // Configure buffer comparison mode
|
|
|
|
// best guess at display mode bits:
|
|
// bit | hex | name | effect
|
|
// ----+-----+--------------------------+-------------------------------------------
|
|
// 7 | 80 | CLOCK_ON | Start internal oscillator
|
|
// 6 | 40 | ANALOG_ON | Enable analog power rails (VGH/VGL drivers)
|
|
// 5 | 20 | TEMP_LOAD | Load temperature (internal or I2C)
|
|
// 4 | 10 | LUT_LOAD | Load waveform LUT
|
|
// 3 | 08 | MODE_SELECT | Mode 1/2
|
|
// 2 | 04 | DISPLAY_START | Run display
|
|
// 1 | 02 | ANALOG_OFF_PHASE | Shutdown step 1 (undocumented)
|
|
// 0 | 01 | CLOCK_OFF | Disable internal oscillator
|
|
|
|
// Select appropriate display mode based on refresh type
|
|
uint8_t displayMode = 0x00;
|
|
|
|
// Enable counter and analog if not already on
|
|
if (!isScreenOn) {
|
|
isScreenOn = true;
|
|
displayMode |= 0xC0; // Set CLOCK_ON and ANALOG_ON bits
|
|
}
|
|
|
|
// Turn off screen if requested
|
|
if (turnOffScreen) {
|
|
isScreenOn = false;
|
|
displayMode |= 0x03; // Set ANALOG_OFF_PHASE and CLOCK_OFF bits
|
|
}
|
|
|
|
if (mode == FULL_REFRESH) {
|
|
displayMode |= 0x34;
|
|
} else if (mode == HALF_REFRESH) {
|
|
// Write high temp to the register for a faster refresh
|
|
sendCommand(CMD_WRITE_TEMP);
|
|
sendData(0x5A);
|
|
displayMode |= 0xD4;
|
|
} else { // FAST_REFRESH
|
|
displayMode |= customLutActive ? 0x0C : 0x1C;
|
|
}
|
|
|
|
// Power on and refresh display
|
|
const char* refreshType = (mode == FULL_REFRESH) ? "full" : (mode == HALF_REFRESH) ? "half" : "fast";
|
|
Serial.printf("[%lu] Powering on display 0x%02X (%s refresh)...\n", millis(), displayMode, refreshType);
|
|
sendCommand(CMD_DISPLAY_UPDATE_CTRL2);
|
|
sendData(displayMode);
|
|
|
|
sendCommand(CMD_MASTER_ACTIVATION);
|
|
|
|
// Wait for display to finish updating
|
|
Serial.printf("[%lu] Waiting for display refresh...\n", millis());
|
|
waitWhileBusy(refreshType);
|
|
}
|
|
|
|
void EInkDisplay::setCustomLUT(const bool enabled, const unsigned char* lutData) {
|
|
if (enabled) {
|
|
Serial.printf("[%lu] Loading custom LUT...\n", millis());
|
|
|
|
// Load custom LUT (first 105 bytes: VS + TP/RP + frame rate)
|
|
sendCommand(CMD_WRITE_LUT);
|
|
for (uint16_t i = 0; i < 105; i++) {
|
|
sendData(pgm_read_byte(&lutData[i]));
|
|
}
|
|
|
|
// Set voltage values from bytes 105-109
|
|
sendCommand(CMD_GATE_VOLTAGE); // VGH
|
|
sendData(pgm_read_byte(&lutData[105]));
|
|
|
|
sendCommand(CMD_SOURCE_VOLTAGE); // VSH1, VSH2, VSL
|
|
sendData(pgm_read_byte(&lutData[106])); // VSH1
|
|
sendData(pgm_read_byte(&lutData[107])); // VSH2
|
|
sendData(pgm_read_byte(&lutData[108])); // VSL
|
|
|
|
sendCommand(CMD_WRITE_VCOM); // VCOM
|
|
sendData(pgm_read_byte(&lutData[109]));
|
|
|
|
customLutActive = true;
|
|
Serial.printf("[%lu] Custom LUT loaded\n", millis());
|
|
} else {
|
|
customLutActive = false;
|
|
Serial.printf("[%lu] Custom LUT disabled\n", millis());
|
|
}
|
|
}
|
|
|
|
void EInkDisplay::deepSleep() {
|
|
Serial.printf("[%lu] Preparing display for deep sleep...\n", millis());
|
|
|
|
// First, power down the display properly
|
|
// This shuts down the analog power rails and clock
|
|
if (isScreenOn) {
|
|
sendCommand(CMD_DISPLAY_UPDATE_CTRL1);
|
|
sendData(CTRL1_BYPASS_RED); // Normal mode
|
|
|
|
sendCommand(CMD_DISPLAY_UPDATE_CTRL2);
|
|
sendData(0x03); // Set ANALOG_OFF_PHASE (bit 1) and CLOCK_OFF (bit 0)
|
|
|
|
sendCommand(CMD_MASTER_ACTIVATION);
|
|
|
|
// Wait for the power-down sequence to complete
|
|
waitWhileBusy(" display power-down");
|
|
|
|
isScreenOn = false;
|
|
}
|
|
|
|
// Now enter deep sleep mode
|
|
Serial.printf("[%lu] Entering deep sleep mode...\n", millis());
|
|
sendCommand(CMD_DEEP_SLEEP);
|
|
sendData(0x01); // Enter deep sleep
|
|
}
|
|
|
|
void EInkDisplay::saveFrameBufferAsPBM(const char* filename) {
|
|
#ifndef ARDUINO
|
|
const uint8_t* buffer = getFrameBuffer();
|
|
|
|
std::ofstream file(filename, std::ios::binary);
|
|
if (!file) {
|
|
Serial.printf("Failed to open %s for writing\n", filename);
|
|
return;
|
|
}
|
|
|
|
// Rotate the image 90 degrees counterclockwise when saving
|
|
// Original buffer: 800x480 (landscape)
|
|
// Output image: 480x800 (portrait)
|
|
const int DISPLAY_WIDTH_LOCAL = DISPLAY_WIDTH; // 800
|
|
const int DISPLAY_HEIGHT_LOCAL = DISPLAY_HEIGHT; // 480
|
|
const int DISPLAY_WIDTH_BYTES_LOCAL = DISPLAY_WIDTH_LOCAL / 8;
|
|
|
|
file << "P4\n"; // Binary PBM
|
|
file << DISPLAY_HEIGHT_LOCAL << " " << DISPLAY_WIDTH_LOCAL << "\n";
|
|
|
|
// Create rotated buffer
|
|
std::vector<uint8_t> rotatedBuffer((DISPLAY_HEIGHT_LOCAL / 8) * DISPLAY_WIDTH_LOCAL, 0);
|
|
|
|
for (int outY = 0; outY < DISPLAY_WIDTH_LOCAL; outY++) {
|
|
for (int outX = 0; outX < DISPLAY_HEIGHT_LOCAL; outX++) {
|
|
int inX = outY;
|
|
int inY = DISPLAY_HEIGHT_LOCAL - 1 - outX;
|
|
|
|
int inByteIndex = inY * DISPLAY_WIDTH_BYTES_LOCAL + (inX / 8);
|
|
int inBitPosition = 7 - (inX % 8);
|
|
bool isWhite = (buffer[inByteIndex] >> inBitPosition) & 1;
|
|
|
|
int outByteIndex = outY * (DISPLAY_HEIGHT_LOCAL / 8) + (outX / 8);
|
|
int outBitPosition = 7 - (outX % 8);
|
|
if (!isWhite) { // Invert: e-ink white=1 -> PBM black=1
|
|
rotatedBuffer[outByteIndex] |= (1 << outBitPosition);
|
|
}
|
|
}
|
|
}
|
|
|
|
file.write(reinterpret_cast<const char*>(rotatedBuffer.data()), rotatedBuffer.size());
|
|
file.close();
|
|
Serial.printf("Saved framebuffer to %s\n", filename);
|
|
#else
|
|
(void)filename;
|
|
Serial.println("saveFrameBufferAsPBM is not supported on Arduino builds.");
|
|
#endif
|
|
}
|