457 lines
16 KiB
C
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)
|