Add initial implementation of EInkDisplay from CidVonHighwind (#4)
This commit is contained in:
parent
8224d278c5
commit
a126d4b0bf
93
libs/display/EInkDisplay/include/EInkDisplay.h
Normal file
93
libs/display/EInkDisplay/include/EInkDisplay.h
Normal file
@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
|
||||
class EInkDisplay {
|
||||
public:
|
||||
// Constructor with pin configuration
|
||||
EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy);
|
||||
|
||||
// Destructor
|
||||
~EInkDisplay();
|
||||
|
||||
// Refresh modes (guarded to avoid redefinition in test builds)
|
||||
enum RefreshMode {
|
||||
FULL_REFRESH, // Full refresh with complete waveform
|
||||
HALF_REFRESH, // Half refresh (1720ms) - balanced quality and speed
|
||||
FAST_REFRESH // Fast refresh using custom LUT
|
||||
};
|
||||
|
||||
// Initialize the display hardware and driver
|
||||
void begin();
|
||||
|
||||
// Display dimensions
|
||||
static const uint16_t DISPLAY_WIDTH = 800;
|
||||
static const uint16_t DISPLAY_HEIGHT = 480;
|
||||
static const uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8;
|
||||
static const uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT;
|
||||
|
||||
// Frame buffer operations
|
||||
void clearScreen(uint8_t color = 0xFF);
|
||||
void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem = false);
|
||||
|
||||
void swapBuffers();
|
||||
void setFramebuffer(const uint8_t* bwBuffer);
|
||||
|
||||
void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer);
|
||||
void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer);
|
||||
void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer);
|
||||
|
||||
void displayBuffer(RefreshMode mode = FAST_REFRESH);
|
||||
void displayGrayBuffer(bool turnOffScreen = false);
|
||||
|
||||
void refreshDisplay(RefreshMode mode = FAST_REFRESH, bool turnOffScreen = false);
|
||||
|
||||
// debug function
|
||||
void grayscaleRevert();
|
||||
|
||||
// LUT control
|
||||
void setCustomLUT(bool enabled, const unsigned char* lutData = nullptr);
|
||||
|
||||
// Power management
|
||||
void deepSleep();
|
||||
|
||||
// Access to frame buffer
|
||||
uint8_t* getFrameBuffer() {
|
||||
return frameBuffer;
|
||||
}
|
||||
|
||||
// Save the current framebuffer to a PBM file (desktop/test builds only)
|
||||
void saveFrameBufferAsPBM(const char* filename);
|
||||
|
||||
private:
|
||||
// Pin configuration
|
||||
int8_t _sclk, _mosi, _cs, _dc, _rst, _busy;
|
||||
|
||||
// Frame buffer (statically allocated)
|
||||
uint8_t frameBuffer0[BUFFER_SIZE];
|
||||
uint8_t frameBuffer1[BUFFER_SIZE];
|
||||
|
||||
uint8_t* frameBuffer;
|
||||
uint8_t* frameBufferActive;
|
||||
|
||||
// SPI settings
|
||||
SPISettings spiSettings;
|
||||
|
||||
// State
|
||||
bool isScreenOn;
|
||||
bool customLutActive;
|
||||
bool inGrayscaleMode;
|
||||
bool drawGrayscale;
|
||||
|
||||
// Low-level display control
|
||||
void resetDisplay();
|
||||
void sendCommand(uint8_t command);
|
||||
void sendData(uint8_t data);
|
||||
void sendData(const uint8_t* data, uint16_t length);
|
||||
void waitWhileBusy(const char* comment = nullptr);
|
||||
void initDisplayController();
|
||||
|
||||
// Low-level display operations
|
||||
void setRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
||||
void writeRamBuffer(uint8_t ramBuffer, const uint8_t* data, uint32_t size);
|
||||
};
|
||||
14
libs/display/EInkDisplay/library.json
Normal file
14
libs/display/EInkDisplay/library.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "EInkDisplay",
|
||||
"version": "1.0.0",
|
||||
"description": "Low level E-Ink display driver, with support for grayscale and partial refresh modes.",
|
||||
"authors": [
|
||||
{
|
||||
"name": "CidVonHighwind",
|
||||
"url": "https://github.com/CidVonHighwind"
|
||||
}
|
||||
],
|
||||
"dependencies": {},
|
||||
"platforms": "espressif32",
|
||||
"frameworks": ["arduino"]
|
||||
}
|
||||
573
libs/display/EInkDisplay/src/EInkDisplay.cpp
Normal file
573
libs/display/EInkDisplay/src/EInkDisplay.cpp
Normal file
@ -0,0 +1,573 @@
|
||||
#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),
|
||||
frameBufferActive(nullptr),
|
||||
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);
|
||||
}
|
||||
|
||||
EInkDisplay::~EInkDisplay() {
|
||||
// No dynamic memory to clean up (buffers are statically allocated)
|
||||
}
|
||||
|
||||
void EInkDisplay::begin() {
|
||||
Serial.printf("[%lu] EInkDisplay: begin() called\n", millis());
|
||||
|
||||
frameBuffer = frameBuffer0;
|
||||
frameBufferActive = frameBuffer1;
|
||||
|
||||
// Initialize to white
|
||||
memset(frameBuffer0, 0xFF, BUFFER_SIZE);
|
||||
memset(frameBuffer1, 0xFF, BUFFER_SIZE);
|
||||
|
||||
Serial.printf("[%lu] Static frame buffers (2 x %lu bytes = 96KB)\n", millis(), BUFFER_SIZE);
|
||||
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(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||
const uint16_t WIDTH = 800;
|
||||
const uint16_t HEIGHT = 480;
|
||||
const uint8_t DATA_ENTRY_X_INC_Y_DEC = 0x01;
|
||||
|
||||
// Reverse Y coordinate (gates are reversed on this display)
|
||||
y = 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(uint8_t color) {
|
||||
memset(frameBuffer, color, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
void EInkDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
|
||||
bool fromProgmem) {
|
||||
if (!frameBuffer) {
|
||||
Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate bytes per line for the image
|
||||
uint16_t imageWidthBytes = w / 8;
|
||||
|
||||
// Copy image data to frame buffer
|
||||
for (uint16_t row = 0; row < h; row++) {
|
||||
uint16_t destY = y + row;
|
||||
if (destY >= DISPLAY_HEIGHT)
|
||||
break;
|
||||
|
||||
uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8);
|
||||
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";
|
||||
unsigned long startTime = millis();
|
||||
Serial.printf("[%lu] Writing frame buffer to %s RAM (%lu bytes)...\n", startTime, bufferName, size);
|
||||
|
||||
sendCommand(ramBuffer);
|
||||
sendData(data, size);
|
||||
|
||||
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) {
|
||||
memcpy(frameBuffer, bwBuffer, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
void EInkDisplay::swapBuffers() {
|
||||
uint8_t* temp = frameBuffer;
|
||||
frameBuffer = frameBufferActive;
|
||||
frameBufferActive = temp;
|
||||
}
|
||||
|
||||
void EInkDisplay::grayscaleRevert() {
|
||||
inGrayscaleMode = false;
|
||||
|
||||
// Load the revert LUT
|
||||
setCustomLUT(true, lut_grayscale_revert);
|
||||
refreshDisplay(FAST_REFRESH, false);
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
inGrayscaleMode = false;
|
||||
grayscaleRevert();
|
||||
}
|
||||
|
||||
// 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);
|
||||
writeRamBuffer(CMD_WRITE_RAM_RED, frameBufferActive, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
// swap active buffer for next time
|
||||
swapBuffers();
|
||||
|
||||
// Refresh the display
|
||||
refreshDisplay(mode, false);
|
||||
}
|
||||
|
||||
void EInkDisplay::displayGrayBuffer(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(RefreshMode mode, 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(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() {
|
||||
// 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user