// 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); }