158 lines
4.9 KiB
C
158 lines
4.9 KiB
C
// xteink X3 e-ink panel model for Wokwi.
|
|
//
|
|
// The X3 firmware drives a UC8179-style controller (see EInkDisplay.cpp
|
|
// setDisplayX3 / displayBuffer). This chip reconstructs the intended image
|
|
// from the SPI stream and paints it to a Wokwi framebuffer.
|
|
//
|
|
// Protocol facts this relies on (from the driver, not a datasheet):
|
|
// - SPI mode 0, MSB first. DC low = command byte, DC high = data byte.
|
|
// - Pixel planes are streamed after command 0x13 (new frame), 99 bytes/row,
|
|
// bottom-to-top (row i on the wire = framebuffer row H-1-i).
|
|
// - Bit = 1 -> white, bit = 0 -> black (0xFF = white byte).
|
|
// - Full-sync writes the plane inverted and flags it with 0x50 payload 0xA9;
|
|
// fast refresh writes it upright with 0x50 payload 0x29.
|
|
// - Command 0x12 triggers the refresh; BUSY is active LOW (idle HIGH).
|
|
//
|
|
// Commands 0x20-0x24 are LUT registers here (not RAM/activation), so they are
|
|
// ignored. Only 0x13 (plane), 0x50 (polarity), and 0x12/0x04/0x02 (refresh/
|
|
// power -> BUSY pulse) matter for rendering.
|
|
|
|
#include "wokwi-api.h"
|
|
|
|
#define WIDTH 792
|
|
#define HEIGHT 528
|
|
#define WIDTH_BYTES (WIDTH / 8) // 99
|
|
#define PLANE_SIZE (WIDTH_BYTES * HEIGHT) // 52272
|
|
|
|
#define CMD_NEW_PLANE 0x13
|
|
#define CMD_POLARITY 0x50
|
|
#define CMD_REFRESH 0x12
|
|
#define CMD_POWER_ON 0x04
|
|
#define CMD_POWER_OFF 0x02
|
|
|
|
#define BUSY_PULSE_US 5000
|
|
#define SPI_CHUNK 4096
|
|
|
|
typedef struct {
|
|
pin_t cs, dc, busy;
|
|
spi_dev_t spi;
|
|
buffer_t fb;
|
|
timer_t busy_timer;
|
|
uint8_t spi_buf[SPI_CHUNK];
|
|
|
|
bool span_is_command; // this CS-low span began with DC low
|
|
uint32_t span_pos; // byte index within the current span
|
|
uint8_t cmd; // last command byte
|
|
uint32_t plane_pos; // write cursor into plane[]
|
|
bool invert; // recovered from the 0x50 polarity selector
|
|
uint8_t plane[PLANE_SIZE];
|
|
} chip_state_t;
|
|
|
|
static chip_state_t chip;
|
|
|
|
static void render(void) {
|
|
static uint8_t row_rgba[WIDTH * 4];
|
|
for (uint32_t i = 0; i < HEIGHT; i++) {
|
|
const uint32_t y = HEIGHT - 1 - i; // un-mirror: wire is bottom-to-top
|
|
const uint8_t *src = &chip.plane[i * WIDTH_BYTES];
|
|
uint32_t o = 0;
|
|
for (uint32_t bx = 0; bx < WIDTH_BYTES; bx++) {
|
|
uint8_t v = src[bx];
|
|
if (chip.invert) v = (uint8_t)~v;
|
|
for (int bit = 0; bit < 8; bit++) {
|
|
const uint8_t px = (v & (0x80 >> bit)) ? 0xFF : 0x00; // 1=white
|
|
row_rgba[o++] = px; // R
|
|
row_rgba[o++] = px; // G
|
|
row_rgba[o++] = px; // B
|
|
row_rgba[o++] = 0xFF; // A
|
|
}
|
|
}
|
|
buffer_write(chip.fb, y * WIDTH * 4, row_rgba, WIDTH * 4);
|
|
}
|
|
}
|
|
|
|
static void pulse_busy(void) {
|
|
pin_write(chip.busy, LOW);
|
|
timer_start(chip.busy_timer, BUSY_PULSE_US, false);
|
|
}
|
|
|
|
static void on_busy_timer(void *user_data) {
|
|
(void)user_data;
|
|
pin_write(chip.busy, HIGH);
|
|
}
|
|
|
|
static void handle_command(uint8_t b) {
|
|
chip.cmd = b;
|
|
chip.span_pos = 1;
|
|
if (b == CMD_NEW_PLANE) chip.plane_pos = 0;
|
|
if (b == CMD_REFRESH) render();
|
|
if (b == CMD_REFRESH || b == CMD_POWER_ON || b == CMD_POWER_OFF) pulse_busy();
|
|
}
|
|
|
|
static void handle_data(uint8_t b) {
|
|
const uint32_t data_index = chip.span_pos - (chip.span_is_command ? 1 : 0);
|
|
if (chip.cmd == CMD_NEW_PLANE) {
|
|
if (chip.plane_pos < PLANE_SIZE) chip.plane[chip.plane_pos++] = b;
|
|
} else if (chip.cmd == CMD_POLARITY && data_index == 0) {
|
|
chip.invert = (b == 0xA9);
|
|
}
|
|
chip.span_pos++;
|
|
}
|
|
|
|
static void on_spi_done(void *user_data, uint8_t *buffer, uint32_t count) {
|
|
(void)user_data;
|
|
if (count == 0) return; // spi_stop with no data
|
|
|
|
if (chip.span_is_command) {
|
|
const uint8_t b = buffer[0];
|
|
if (chip.span_pos == 0)
|
|
handle_command(b);
|
|
else
|
|
handle_data(b);
|
|
if (pin_read(chip.cs) == LOW) spi_start(chip.spi, chip.spi_buf, 1);
|
|
} else {
|
|
for (uint32_t i = 0; i < count; i++) handle_data(buffer[i]);
|
|
if (pin_read(chip.cs) == LOW) spi_start(chip.spi, chip.spi_buf, SPI_CHUNK);
|
|
}
|
|
}
|
|
|
|
static void on_cs_change(void *user_data, pin_t pin, uint32_t value) {
|
|
(void)user_data;
|
|
(void)pin;
|
|
if (value == LOW) {
|
|
chip.span_is_command = (pin_read(chip.dc) == LOW);
|
|
chip.span_pos = 0;
|
|
spi_start(chip.spi, chip.spi_buf, chip.span_is_command ? 1 : SPI_CHUNK);
|
|
} else {
|
|
spi_stop(chip.spi);
|
|
}
|
|
}
|
|
|
|
void chip_init(void) {
|
|
const pin_t sck = pin_init("SCLK", INPUT);
|
|
const pin_t mosi = pin_init("MOSI", INPUT);
|
|
chip.cs = pin_init("CS", INPUT_PULLUP);
|
|
chip.dc = pin_init("DC", INPUT);
|
|
pin_init("RST", INPUT);
|
|
chip.busy = pin_init("BUSY", OUTPUT_HIGH);
|
|
|
|
uint32_t w = 0, h = 0;
|
|
chip.fb = framebuffer_init(&w, &h);
|
|
|
|
const spi_config_t spi_cfg = {
|
|
.sck = sck,
|
|
.mosi = mosi,
|
|
.miso = NO_PIN,
|
|
.mode = 0,
|
|
.done = on_spi_done,
|
|
.user_data = &chip,
|
|
};
|
|
chip.spi = spi_init(&spi_cfg);
|
|
|
|
const timer_config_t timer_cfg = {.callback = on_busy_timer, .user_data = &chip};
|
|
chip.busy_timer = timer_init(&timer_cfg);
|
|
|
|
const pin_watch_config_t cs_watch = {.edge = BOTH, .pin_change = on_cs_change, .user_data = &chip};
|
|
pin_watch(chip.cs, &cs_watch);
|
|
}
|