Files
qemu-xteink/hw/display/xteink_x3_eink.c
T

457 lines
16 KiB
C

/*
* xteink X3 UC8253 e-ink panel
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/module.h"
#include "qemu/error-report.h"
#include "qemu/cutils.h"
#include "hw/irq.h"
#include "hw/display/xteink_x3_eink.h"
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
static QemuConsole *xteink_wasm_console;
static uint32_t xteink_wasm_generation;
EMSCRIPTEN_KEEPALIVE uintptr_t xteink_wasm_framebuffer(void)
{
DisplaySurface *surface = xteink_wasm_console
? qemu_console_surface(xteink_wasm_console)
: NULL;
return surface ? (uintptr_t)surface_data(surface) : 0;
}
EMSCRIPTEN_KEEPALIVE int xteink_wasm_width(void)
{
DisplaySurface *surface = xteink_wasm_console
? qemu_console_surface(xteink_wasm_console)
: NULL;
return surface ? surface_width(surface) : 0;
}
EMSCRIPTEN_KEEPALIVE int xteink_wasm_height(void)
{
DisplaySurface *surface = xteink_wasm_console
? qemu_console_surface(xteink_wasm_console)
: NULL;
return surface ? surface_height(surface) : 0;
}
EMSCRIPTEN_KEEPALIVE int xteink_wasm_stride(void)
{
DisplaySurface *surface = xteink_wasm_console
? qemu_console_surface(xteink_wasm_console)
: NULL;
return surface ? surface_stride(surface) : 0;
}
EMSCRIPTEN_KEEPALIVE uint32_t xteink_wasm_frame_generation(void)
{
return qatomic_read(&xteink_wasm_generation);
}
static void xteink_wasm_frame_updated(QemuConsole *console)
{
xteink_wasm_console = console;
qatomic_inc(&xteink_wasm_generation);
}
#else
static void xteink_wasm_frame_updated(QemuConsole *console)
{
}
#endif
#define CMD_POWER_OFF 0x02
#define CMD_POWER_ON 0x04
#define CMD_DTM1 0x10
#define CMD_REFRESH 0x12
#define CMD_DTM2 0x13
#define CMD_LUT_FIRST 0x20
#define CMD_LUT_LAST 0x24
#define BUSY_TIME_NS (5 * SCALE_MS)
#define XTEINK_X3_LUT_PREFIX_SIZE 13
typedef struct XteinkX3KnownLut {
bool grayscale;
uint8_t prefix[XTEINK_X3_LUT_COUNT][XTEINK_X3_LUT_PREFIX_SIZE];
} XteinkX3KnownLut;
/* These known banks have zero-filled bytes 13..41. Comparing both the listed
* prefixes and those tails identifies the complete 210-byte bank exactly. */
static const XteinkX3KnownLut xteink_x3_known_luts[] = {
{ false, {
{ 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0x20, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x04, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
} },
{ false, {
{ 0x00, 0x06, 0x01, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0xaa, 0x06, 0x01, 0x06, 0x06, 0x01, 0xa0, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0x55, 0x06, 0x01, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
} },
{ false, {
{ 0x00, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
{ 0x20, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
{ 0xaa, 0x04, 0x02, 0x04, 0x04, 0x01, 0x80, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
{ 0x55, 0x04, 0x02, 0x04, 0x04, 0x01, 0x40, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
{ 0x10, 0x04, 0x02, 0x04, 0x04, 0x01, 0x00, 0x04, 0x01, 0x00, 0x00, 0x01, 0x00 },
} },
{ false, {
{ 0x00, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
{ 0x4a, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
{ 0x0a, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
{ 0x04, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
{ 0x84, 0x18, 0x04, 0x0e, 0x0a, 0x01, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00 },
} },
{ true, {
{ 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
{ 0x20, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
{ 0x80, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
{ 0x54, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
{ 0x00, 0x03, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
} },
{ false, {
{ 0x00, 0x06, 0x03, 0x06, 0x06, 0x01, 0x00, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0x20, 0x06, 0x03, 0x06, 0x06, 0x01, 0x20, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0xaa, 0x06, 0x03, 0x06, 0x06, 0x01, 0xa8, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0x55, 0x06, 0x03, 0x06, 0x06, 0x01, 0x50, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
{ 0x10, 0x06, 0x03, 0x06, 0x06, 0x01, 0x14, 0x04, 0x01, 0x01, 0x00, 0x01, 0x00 },
} },
{ false, {
{ 0x01, 0x07, 0x01, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 },
{ 0x01, 0x07, 0x81, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 },
{ 0x01, 0x87, 0x81, 0x86, 0x86, 0x01, 0x01, 0x01, 0x86, 0x01, 0x00, 0x00, 0x01 },
{ 0x01, 0x47, 0x41, 0x46, 0x46, 0x01, 0x01, 0x01, 0x46, 0x01, 0x00, 0x00, 0x01 },
{ 0x01, 0x07, 0x01, 0x06, 0x06, 0x01, 0x01, 0x01, 0x06, 0x01, 0x00, 0x00, 0x01 },
} },
};
static bool xteink_x3_eink_lut_matches(const XteinkX3EinkState *s,
const XteinkX3KnownLut *known)
{
for (unsigned i = 0; i < XTEINK_X3_LUT_COUNT; i++) {
if (memcmp(s->lut_bank[i], known->prefix[i],
XTEINK_X3_LUT_PREFIX_SIZE)) {
return false;
}
for (unsigned j = XTEINK_X3_LUT_PREFIX_SIZE;
j < XTEINK_X3_LUT_SIZE; j++) {
if (s->lut_bank[i][j]) {
return false;
}
}
}
return true;
}
static bool xteink_x3_eink_classify_lut(XteinkX3EinkState *s)
{
for (unsigned i = 0; i < XTEINK_X3_LUT_COUNT; i++) {
if (s->lut_pos[i] != XTEINK_X3_LUT_SIZE) {
warn_report("xteink-x3-eink: incomplete LUT bank");
return false;
}
}
for (unsigned i = 0; i < ARRAY_SIZE(xteink_x3_known_luts); i++) {
if (xteink_x3_eink_lut_matches(s, &xteink_x3_known_luts[i])) {
s->grayscale_lut = xteink_x3_known_luts[i].grayscale;
s->lut_known = true;
s->unknown_lut_reported = false;
return true;
}
}
if (!s->unknown_lut_reported) {
warn_report("xteink-x3-eink: unknown LUT bank; refresh skipped");
qemu_hexdump(stderr, "xteink-x3-eink: LUT ", s->lut_bank,
sizeof(s->lut_bank));
s->unknown_lut_reported = true;
}
s->lut_known = false;
return false;
}
/* 4-level grayscale drive shades for levels 1..3; level 0 = no drive (keep). */
static const uint32_t xteink_x3_gray[4] = {
0x00ffffff, 0x00aaaaaa, 0x00555555, 0x00000000,
};
static void xteink_x3_eink_render(XteinkX3EinkState *s)
{
DisplaySurface *surface = qemu_console_surface(s->console);
uint32_t *pixels = (uint32_t *)surface_data(surface);
/* Persistence - E-ink retains undriven pixels. The reader's grayscale pass
* is a sparse antialiasing overlay (planes near-empty) on top of the crisp
* BW page from the prior full refresh; level 0 = no drive, so keep the
* existing pixel and only repaint driven (level>0) ones. BW refreshes drive
* every pixel, so they repaint in full. The surface is never cleared. */
for (unsigned y = 0; y < XTEINK_X3_HEIGHT; y++) {
unsigned row_off = (XTEINK_X3_HEIGHT - 1 - y) * XTEINK_X3_WIDTH_BYTES;
const uint8_t *msb = &s->new_plane[row_off];
const uint8_t *lsb = &s->old_plane[row_off];
for (unsigned x = 0; x < XTEINK_X3_WIDTH; x++) {
uint8_t bit = 0x80 >> (x % 8);
unsigned screen_x = XTEINK_X3_HEIGHT - 1 - y;
unsigned screen_y = x;
if (s->grayscale_lut) {
unsigned level = ((msb[x / 8] & bit) ? 2 : 0) |
((lsb[x / 8] & bit) ? 1 : 0);
if (level == 0) {
continue;
}
pixels[screen_y * XTEINK_X3_HEIGHT + screen_x] =
xteink_x3_gray[level];
} else {
pixels[screen_y * XTEINK_X3_HEIGHT + screen_x] =
(msb[x / 8] & bit) ? 0x00ffffff : 0;
}
}
}
xteink_wasm_frame_updated(s->console);
dpy_gfx_update(s->console, 0, 0, XTEINK_X3_HEIGHT, XTEINK_X3_WIDTH);
}
static void xteink_x3_eink_busy_done(void *opaque)
{
XteinkX3EinkState *s = XTEINK_X3_EINK(opaque);
qemu_set_irq(s->busy, 1);
}
static void xteink_x3_eink_pulse_busy(XteinkX3EinkState *s)
{
qemu_set_irq(s->busy, 0);
timer_mod(s->busy_timer,
qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + BUSY_TIME_NS);
}
static void xteink_x3_eink_reset(DeviceState *dev)
{
XteinkX3EinkState *s = XTEINK_X3_EINK(dev);
s->command = 0;
s->plane_pos = 0;
memset(s->old_plane, 0xff, sizeof(s->old_plane));
memset(s->new_plane, 0xff, sizeof(s->new_plane));
memset(s->lut_pos, 0, sizeof(s->lut_pos));
s->lut_dirty = false;
s->lut_known = true;
s->grayscale_lut = false;
s->unknown_lut_reported = false;
qemu_set_irq(s->busy, 1);
xteink_x3_eink_render(s);
}
static void xteink_x3_eink_set_dc(void *opaque, int n, int level)
{
XteinkX3EinkState *s = XTEINK_X3_EINK(opaque);
s->dc = level;
}
static void xteink_x3_eink_set_reset(void *opaque, int n, int level)
{
if (!level) {
xteink_x3_eink_reset(DEVICE(opaque));
}
}
static uint32_t xteink_x3_eink_transfer(SSIPeripheral *peripheral,
uint32_t value)
{
XteinkX3EinkState *s = XTEINK_X3_EINK(peripheral);
uint8_t byte = value;
if (!s->dc) {
s->command = byte;
if (byte == CMD_DTM1 || byte == CMD_DTM2) {
s->plane_pos = 0;
} else if (byte >= CMD_LUT_FIRST && byte <= CMD_LUT_LAST) {
s->lut_pos[byte - CMD_LUT_FIRST] = 0;
s->lut_dirty = true;
} else if (byte == CMD_REFRESH) {
if (s->lut_dirty) {
s->lut_known = xteink_x3_eink_classify_lut(s);
s->lut_dirty = false;
}
if (s->lut_known) {
xteink_x3_eink_render(s);
}
xteink_x3_eink_pulse_busy(s);
} else if (byte == CMD_POWER_ON || byte == CMD_POWER_OFF) {
xteink_x3_eink_pulse_busy(s);
}
} else if (s->command >= CMD_LUT_FIRST && s->command <= CMD_LUT_LAST) {
unsigned lut = s->command - CMD_LUT_FIRST;
if (s->lut_pos[lut] < XTEINK_X3_LUT_SIZE) {
s->lut_bank[lut][s->lut_pos[lut]++] = byte;
}
} else if (s->plane_pos < XTEINK_X3_PLANE_SIZE) {
if (s->command == CMD_DTM1) {
s->old_plane[s->plane_pos++] = byte;
} else if (s->command == CMD_DTM2) {
s->new_plane[s->plane_pos++] = byte;
}
}
return 0xff;
}
static void xteink_x3_eink_invalidate(void *opaque)
{
xteink_x3_eink_render(XTEINK_X3_EINK(opaque));
}
static void xteink_x3_eink_update(void *opaque)
{
}
static const GraphicHwOps xteink_x3_eink_graphics_ops = {
.invalidate = xteink_x3_eink_invalidate,
.gfx_update = xteink_x3_eink_update,
};
static void xteink_x3_eink_init(Object *obj)
{
XteinkX3EinkState *s = XTEINK_X3_EINK(obj);
s->console = graphic_console_init(DEVICE(s), 0,
&xteink_x3_eink_graphics_ops, s);
dpy_gfx_replace_surface(s->console,
qemu_create_displaysurface(XTEINK_X3_HEIGHT, XTEINK_X3_WIDTH));
s->busy_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
xteink_x3_eink_busy_done, s);
qdev_init_gpio_in_named(DEVICE(s), xteink_x3_eink_set_dc,
XTEINK_X3_EINK_DC, 1);
qdev_init_gpio_in_named(DEVICE(s), xteink_x3_eink_set_reset,
XTEINK_X3_EINK_RESET, 1);
qdev_init_gpio_out_named(DEVICE(s), &s->busy,
XTEINK_X3_EINK_BUSY, 1);
}
static void xteink_x3_eink_realize(SSIPeripheral *peripheral, Error **errp)
{
}
static void xteink_x3_eink_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
SSIPeripheralClass *ssi = SSI_PERIPHERAL_CLASS(klass);
ssi->realize = xteink_x3_eink_realize;
ssi->transfer = xteink_x3_eink_transfer;
ssi->cs_polarity = SSI_CS_LOW;
device_class_set_legacy_reset(dc, xteink_x3_eink_reset);
dc->user_creatable = false;
}
static const TypeInfo xteink_x3_eink_info = {
.name = TYPE_XTEINK_X3_EINK,
.parent = TYPE_SSI_PERIPHERAL,
.instance_size = sizeof(XteinkX3EinkState),
.instance_init = xteink_x3_eink_init,
.class_init = xteink_x3_eink_class_init,
};
typedef struct XteinkX4EinkState {
SSIPeripheral parent_obj;
QemuConsole *console;
qemu_irq busy;
} XteinkX4EinkState;
#define XTEINK_X4_EINK(obj) \
OBJECT_CHECK(XteinkX4EinkState, (obj), TYPE_XTEINK_X4_EINK)
static void xteink_x4_eink_render(XteinkX4EinkState *s)
{
DisplaySurface *surface = qemu_console_surface(s->console);
memset(surface_data(surface), 0xff,
surface_stride(surface) * surface_height(surface));
xteink_wasm_frame_updated(s->console);
dpy_gfx_update(s->console, 0, 0, 480, 800);
}
static void xteink_x4_eink_reset(DeviceState *dev)
{
XteinkX4EinkState *s = XTEINK_X4_EINK(dev);
qemu_set_irq(s->busy, 0);
xteink_x4_eink_render(s);
}
static void xteink_x4_eink_set_input(void *opaque, int n, int level)
{
}
static uint32_t xteink_x4_eink_transfer(SSIPeripheral *peripheral,
uint32_t value)
{
return 0xff;
}
static void xteink_x4_eink_invalidate(void *opaque)
{
xteink_x4_eink_render(XTEINK_X4_EINK(opaque));
}
static const GraphicHwOps xteink_x4_eink_graphics_ops = {
.invalidate = xteink_x4_eink_invalidate,
};
static void xteink_x4_eink_init(Object *obj)
{
XteinkX4EinkState *s = XTEINK_X4_EINK(obj);
s->console = graphic_console_init(DEVICE(s), 0,
&xteink_x4_eink_graphics_ops, s);
dpy_gfx_replace_surface(s->console, qemu_create_displaysurface(480, 800));
qdev_init_gpio_in_named(DEVICE(s), xteink_x4_eink_set_input,
XTEINK_X3_EINK_DC, 1);
qdev_init_gpio_in_named(DEVICE(s), xteink_x4_eink_set_input,
XTEINK_X3_EINK_RESET, 1);
qdev_init_gpio_out_named(DEVICE(s), &s->busy,
XTEINK_X3_EINK_BUSY, 1);
}
static void xteink_x4_eink_realize(SSIPeripheral *peripheral, Error **errp)
{
}
static void xteink_x4_eink_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
SSIPeripheralClass *ssi = SSI_PERIPHERAL_CLASS(klass);
ssi->realize = xteink_x4_eink_realize;
ssi->transfer = xteink_x4_eink_transfer;
ssi->cs_polarity = SSI_CS_LOW;
device_class_set_legacy_reset(dc, xteink_x4_eink_reset);
dc->user_creatable = false;
}
static const TypeInfo xteink_x4_eink_info = {
.name = TYPE_XTEINK_X4_EINK,
.parent = TYPE_SSI_PERIPHERAL,
.instance_size = sizeof(XteinkX4EinkState),
.instance_init = xteink_x4_eink_init,
.class_init = xteink_x4_eink_class_init,
};
static void xteink_eink_register_types(void)
{
type_register_static(&xteink_x3_eink_info);
type_register_static(&xteink_x4_eink_info);
}
type_init(xteink_eink_register_types)