feat(emulator): add runtime X3 and X4 selection

Model the ESP32-C3 I2C fingerprint path for stock firmware detection. Add a blank X4 panel stub, live GPIO inputs, and pin the exact QEMU revision.
This commit is contained in:
2026-07-19 20:51:44 -04:00
parent f972fbe78b
commit ca07c02e02
7 changed files with 508 additions and 137 deletions
@@ -129,10 +129,10 @@ index 36a9ace..c73f313 100644
system_ss.add(when: 'CONFIG_VIRTIO', if_true: files('virtio-dmabuf.c'))
diff --git i/hw/display/xteink_x3_eink.c w/hw/display/xteink_x3_eink.c
new file mode 100644
index 0000000..bc9a18c
index 0000000..b699aba
--- /dev/null
+++ w/hw/display/xteink_x3_eink.c
@@ -0,0 +1,165 @@
@@ -0,0 +1,248 @@
+/*
+ * xteink X3 UC8253 e-ink panel
+ *
@@ -292,17 +292,108 @@ index 0000000..bc9a18c
+ .class_init = xteink_x3_eink_class_init,
+};
+
+static void xteink_x3_eink_register_types(void)
+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)
+{
+ type_register_static(&xteink_x3_eink_info);
+ DisplaySurface *surface = qemu_console_surface(s->console);
+ memset(surface_data(surface), 0xff,
+ surface_stride(surface) * surface_height(surface));
+ dpy_gfx_update(s->console, 0, 0, 480, 800);
+}
+
+type_init(xteink_x3_eink_register_types)
+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)
diff --git i/hw/gpio/esp32_gpio.c w/hw/gpio/esp32_gpio.c
index 4fea1f5..55cac08 100644
index 4fea1f5..9ec8bd9 100644
--- i/hw/gpio/esp32_gpio.c
+++ w/hw/gpio/esp32_gpio.c
@@ -19,26 +19,77 @@
@@ -12,6 +12,7 @@
#include "qemu/log.h"
#include "qemu/error-report.h"
#include "qapi/error.h"
+#include "qapi/visitor.h"
#include "hw/hw.h"
#include "hw/sysbus.h"
#include "hw/registerfields.h"
@@ -19,26 +20,99 @@
#include "hw/qdev-properties.h"
#include "hw/gpio/esp32_gpio.h"
@@ -328,6 +419,28 @@ index 4fea1f5..55cac08 100644
+{
+ Esp32GpioState *s = ESP32_GPIO(opaque);
+ s->input_level = deposit32(s->input_level, pin, 1, !!level);
+}
+
+static void esp32_gpio_get_input(Object *obj, Visitor *v, const char *name,
+ void *opaque, Error **errp)
+{
+ Esp32GpioState *s = ESP32_GPIO(obj);
+ int pin = GPOINTER_TO_INT(opaque);
+ bool level = extract32(s->input_level, pin, 1);
+ visit_type_bool(v, name, &level, errp);
+}
+
+static void esp32_gpio_set_input_property(Object *obj, Visitor *v,
+ const char *name, void *opaque,
+ Error **errp)
+{
+ Esp32GpioState *s = ESP32_GPIO(obj);
+ int pin = GPOINTER_TO_INT(opaque);
+ bool level;
+
+ if (visit_type_bool(v, name, &level, errp)) {
+ esp32_gpio_set_input(s, pin, level);
+ }
+}
static uint64_t esp32_gpio_read(void *opaque, hwaddr addr, unsigned int size)
@@ -388,7 +501,7 @@ index 4fea1f5..55cac08 100644
}
static const MemoryRegionOps uart_ops = {
@@ -49,6 +100,11 @@ static const MemoryRegionOps uart_ops = {
@@ -49,6 +123,11 @@ static const MemoryRegionOps uart_ops = {
static void esp32_gpio_reset_hold(Object *obj, ResetType type)
{
@@ -400,7 +513,7 @@ index 4fea1f5..55cac08 100644
}
static void esp32_gpio_realize(DeviceState *dev, Error **errp)
@@ -67,6 +123,12 @@ static void esp32_gpio_init(Object *obj)
@@ -67,6 +146,19 @@ static void esp32_gpio_init(Object *obj)
TYPE_ESP32_GPIO, 0x1000);
sysbus_init_mmio(sbd, &s->iomem);
sysbus_init_irq(sbd, &s->irq);
@@ -408,13 +521,201 @@ index 4fea1f5..55cac08 100644
+ ESP32_GPIO_INPUT, ESP32_GPIO_COUNT);
+ qdev_init_gpio_out_named(DEVICE(obj), s->output_lines,
+ ESP32_GPIO_OUTPUT, ESP32_GPIO_COUNT);
+ for (int i = 0; i < ESP32_GPIO_COUNT; i++) {
+ char *name = g_strdup_printf("input-level[%d]", i);
+ object_property_add(obj, name, "bool", esp32_gpio_get_input,
+ esp32_gpio_set_input_property, NULL,
+ GINT_TO_POINTER(i));
+ g_free(name);
+ }
+
+ esp32_gpio_reset_hold(obj, RESET_TYPE_COLD);
}
static Property esp32_gpio_properties[] = {
diff --git i/hw/i2c/esp32_i2c.c w/hw/i2c/esp32_i2c.c
index 5f4022f..1477f15 100644
--- i/hw/i2c/esp32_i2c.c
+++ w/hw/i2c/esp32_i2c.c
@@ -4,6 +4,7 @@
#include "qemu/error-report.h"
#include "hw/i2c/esp32_i2c.h"
#include "hw/irq.h"
+#include "hw/qdev-properties.h"
static void esp32_i2c_do_transaction(Esp32I2CState * s);
static void esp32_i2c_update_irq(Esp32I2CState * s);
@@ -177,7 +178,17 @@ static void esp32_i2c_do_transaction(Esp32I2CState * s)
bool stop_or_end = false;
for (int i_cmd = 0; i_cmd < ESP32_I2C_CMD_COUNT && !stop_or_end; ++i_cmd) {
uint32_t cmd = s->cmd_reg[i_cmd];
- char opcode = FIELD_EX32(cmd, I2C_CMD, OPCODE);
+ int opcode = FIELD_EX32(cmd, I2C_CMD, OPCODE);
+ if (s->c3) {
+ switch (opcode) {
+ case 6: opcode = I2C_OPCODE_RSTART; break;
+ case 1: opcode = I2C_OPCODE_WRITE; break;
+ case 3: opcode = I2C_OPCODE_READ; break;
+ case 2: opcode = I2C_OPCODE_STOP; break;
+ case 4: opcode = I2C_OPCODE_END; break;
+ default: break;
+ }
+ }
switch (opcode) {
case I2C_OPCODE_RSTART:
i2c_end_transfer(s->bus);
@@ -261,9 +272,16 @@ static void esp32_i2c_init(Object * obj)
fifo8_create(&s->rx_fifo, ESP32_I2C_FIFO_LENGTH);
}
+static const Property esp32_i2c_properties[] = {
+ DEFINE_PROP_BOOL("c3", Esp32I2CState, c3, false),
+};
+
static void esp32_i2c_class_init(ObjectClass * klass, void * data)
{
+ DeviceClass *dc = DEVICE_CLASS(klass);
ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+ device_class_set_props(dc, esp32_i2c_properties);
rc->phases.hold = esp32_i2c_reset_hold;
}
diff --git i/hw/i2c/meson.build w/hw/i2c/meson.build
index e72dca0..5df10c2 100644
--- i/hw/i2c/meson.build
+++ w/hw/i2c/meson.build
@@ -17,6 +17,7 @@ i2c_ss.add(when: 'CONFIG_OMAP', if_true: files('omap_i2c.c'))
i2c_ss.add(when: 'CONFIG_PPC4XX', if_true: files('ppc4xx_i2c.c'))
i2c_ss.add(when: 'CONFIG_XTENSA_ESP32', if_true: files('esp32_i2c.c'))
i2c_ss.add(when: 'CONFIG_XTENSA_ESP32S3', if_true: files('esp32_i2c.c'))
+i2c_ss.add(when: 'CONFIG_RISCV_ESP32C3', if_true: files('esp32_i2c.c', 'xteink_fingerprint.c'))
i2c_ss.add(when: 'CONFIG_PCA954X', if_true: files('i2c_mux_pca954x.c'))
i2c_ss.add(when: 'CONFIG_PMBUS', if_true: files('pmbus_device.c'))
i2c_ss.add(when: 'CONFIG_BCM2835_I2C', if_true: files('bcm2835_i2c.c'))
diff --git i/hw/i2c/xteink_fingerprint.c w/hw/i2c/xteink_fingerprint.c
new file mode 100644
index 0000000..06a2e83
--- /dev/null
+++ w/hw/i2c/xteink_fingerprint.c
@@ -0,0 +1,105 @@
+/*
+ * xteink X3 I²C fingerprint chips.
+ *
+ * The crosspoint firmware distinguishes X3 from X4 by probing three I²C
+ * devices on SDA20/SCL0 (BQ27220 gauge @0x55, DS3231 RTC @0x68, QMI8658 IMU
+ * @0x6B). Presence of >=2/3 on two passes selects X3. These stubs return just
+ * enough register data to pass that probe; full gauge/RTC/IMU behaviour is not
+ * modelled (X3 runtime reads degrade gracefully, matching a missing sensor).
+ */
+#include "qemu/osdep.h"
+#include "hw/i2c/i2c.h"
+#include "migration/vmstate.h"
+
+#define TYPE_XTEINK_FPCHIP "xteink-fpchip"
+OBJECT_DECLARE_SIMPLE_TYPE(XteinkFpChipState, XTEINK_FPCHIP)
+
+struct XteinkFpChipState {
+ I2CSlave parent_obj;
+ uint8_t reg;
+ bool reg_set;
+};
+
+/* Fixed register bytes the firmware fingerprint checks. Multi-byte values are
+ * little-endian and read via auto-incrementing register pointer. */
+static uint8_t fpchip_reg(uint8_t addr, uint8_t reg)
+{
+ switch (addr) {
+ case 0x55: /* BQ27220: SOC 0x2C (<=100%), Voltage 0x08 (2500..5000mV) */
+ switch (reg) {
+ case 0x08: return 0x74; /* 0x0E74 = 3700 mV */
+ case 0x09: return 0x0E;
+ case 0x2C: return 0x50; /* 80% */
+ case 0x2D: return 0x00;
+ default: return 0x00;
+ }
+ case 0x68: /* DS3231: seconds 0x00 must be valid BCD */
+ return 0x00;
+ case 0x6B: /* QMI8658: WHO_AM_I 0x00 == 0x05 */
+ return (reg == 0x00) ? 0x05 : 0x00;
+ default:
+ return 0x00;
+ }
+}
+
+static int fpchip_event(I2CSlave *i2c, enum i2c_event event)
+{
+ XteinkFpChipState *s = XTEINK_FPCHIP(i2c);
+ if (event == I2C_START_SEND) {
+ s->reg_set = false;
+ }
+ return 0;
+}
+
+static int fpchip_send(I2CSlave *i2c, uint8_t data)
+{
+ XteinkFpChipState *s = XTEINK_FPCHIP(i2c);
+ if (!s->reg_set) {
+ s->reg = data;
+ s->reg_set = true;
+ }
+ return 0;
+}
+
+static uint8_t fpchip_recv(I2CSlave *i2c)
+{
+ XteinkFpChipState *s = XTEINK_FPCHIP(i2c);
+ return fpchip_reg(i2c->address, s->reg++);
+}
+
+static const VMStateDescription vmstate_fpchip = {
+ .name = TYPE_XTEINK_FPCHIP,
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (const VMStateField[]) {
+ VMSTATE_I2C_SLAVE(parent_obj, XteinkFpChipState),
+ VMSTATE_UINT8(reg, XteinkFpChipState),
+ VMSTATE_BOOL(reg_set, XteinkFpChipState),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static void fpchip_class_init(ObjectClass *klass, void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
+
+ k->event = fpchip_event;
+ k->recv = fpchip_recv;
+ k->send = fpchip_send;
+ dc->vmsd = &vmstate_fpchip;
+}
+
+static const TypeInfo fpchip_info = {
+ .name = TYPE_XTEINK_FPCHIP,
+ .parent = TYPE_I2C_SLAVE,
+ .instance_size = sizeof(XteinkFpChipState),
+ .class_init = fpchip_class_init,
+};
+
+static void fpchip_register_types(void)
+{
+ type_register_static(&fpchip_info);
+}
+
+type_init(fpchip_register_types)
diff --git i/hw/riscv/Kconfig w/hw/riscv/Kconfig
index 192ab47..293808d 100644
--- i/hw/riscv/Kconfig
+++ w/hw/riscv/Kconfig
@@ -119,4 +119,5 @@ config RISCV_ESP32C3
select OPENCORES_ETH
select UNIMP
select ESP_RGB
+ select I2C
diff --git i/hw/riscv/esp32c3.c w/hw/riscv/esp32c3.c
index ed698fb..a49eae6 100644
index ed698fb..2f1ccbf 100644
--- i/hw/riscv/esp32c3.c
+++ w/hw/riscv/esp32c3.c
@@ -22,6 +22,7 @@
@@ -425,16 +726,17 @@ index ed698fb..a49eae6 100644
#include "sysemu/sysemu.h"
#include "sysemu/kvm.h"
#include "sysemu/runstate.h"
@@ -40,6 +41,8 @@
@@ -40,6 +41,9 @@
#include "hw/timer/esp32c3_timg.h"
#include "hw/timer/esp32c3_systimer.h"
#include "hw/ssi/esp32c3_spi.h"
+#include "hw/ssi/esp32c3_spi2.h"
+#include "hw/adc/esp32c3_adc.h"
+#include "hw/i2c/esp32_i2c.h"
#include "hw/misc/esp32c3_rtc_cntl.h"
#include "hw/misc/esp32c3_aes.h"
#include "hw/misc/esp32c3_rsa.h"
@@ -49,6 +52,8 @@
@@ -49,6 +53,8 @@
#include "hw/misc/esp32c3_jtag.h"
#include "hw/dma/esp32c3_gdma.h"
#include "hw/display/esp_rgb.h"
@@ -443,39 +745,43 @@ index ed698fb..a49eae6 100644
#include "hw/net/can/esp32c3_twai.h"
#define ESP32C3_IO_WARNING 0
@@ -66,6 +71,7 @@ struct Esp32C3MachineState {
@@ -66,6 +72,8 @@ struct Esp32C3MachineState {
EspRISCVCPU soc;
BusState periph_bus;
MemoryRegion iomem;
+ bool xteink_x3;
+ bool xteink;
+ bool x4;
qemu_irq cpu_reset;
@@ -86,6 +92,8 @@ struct Esp32C3MachineState {
@@ -86,6 +94,9 @@ struct Esp32C3MachineState {
ESP32C3TimgState timg[2];
ESP32C3SysTimerState systimer;
ESP32C3SpiState spi1;
+ ESP32C3Spi2State spi2;
+ ESP32C3AdcState adc;
+ Esp32I2CState i2c;
ESP32C3RtcCntlState rtccntl;
ESP32C3UsbJtagState jtag;
ESPRgbState rgb;
@@ -420,9 +428,13 @@ static void esp32c3_machine_init(MachineState *machine)
@@ -420,9 +431,15 @@ static void esp32c3_machine_init(MachineState *machine)
object_initialize_child(OBJECT(machine), "timg1", &ms->timg[1], TYPE_ESP32C3_TIMG);
object_initialize_child(OBJECT(machine), "systimer", &ms->systimer, TYPE_ESP32C3_SYSTIMER);
object_initialize_child(OBJECT(machine), "spi1", &ms->spi1, TYPE_ESP32C3_SPI);
+ object_initialize_child(OBJECT(machine), "spi2", &ms->spi2, TYPE_ESP32C3_SPI2);
+ object_initialize_child(OBJECT(machine), "adc", &ms->adc, TYPE_ESP32C3_ADC);
+ object_initialize_child(OBJECT(machine), "i2c", &ms->i2c, TYPE_ESP32_I2C);
+ qdev_prop_set_bit(DEVICE(&ms->i2c), "c3", true);
object_initialize_child(OBJECT(machine), "rtccntl", &ms->rtccntl, TYPE_ESP32C3_RTC_CNTL);
object_initialize_child(OBJECT(machine), "jtag", &ms->jtag, TYPE_ESP32C3_JTAG);
- object_initialize_child(OBJECT(machine), "rgb", &ms->rgb, TYPE_ESP_RGB);
+ if (!ms->xteink_x3) {
+ if (!ms->xteink) {
+ object_initialize_child(OBJECT(machine), "rgb", &ms->rgb, TYPE_ESP_RGB);
+ }
object_initialize_child(OBJECT(machine), "twai", &ms->twai, TYPE_ESP32C3_TWAI);
/* Realize all the I/O peripherals we depend on */
@@ -476,6 +488,20 @@ static void esp32c3_machine_init(MachineState *machine)
@@ -476,6 +493,31 @@ static void esp32c3_machine_init(MachineState *machine)
}
}
@@ -492,31 +798,50 @@ index ed698fb..a49eae6 100644
+ MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->adc), 0);
+ memory_region_add_subregion_overlap(sys_mem, DR_REG_APB_SARADC_BASE, mr, 0);
+ }
+
+ /* I2C controller (X3/X4 fingerprint chips live on SDA20/SCL0). The
+ * interrupt must reach the matrix or the ESP-IDF driver blocks on its
+ * completion semaphore and every probe times out. */
+ {
+ sysbus_realize(SYS_BUS_DEVICE(&ms->i2c), &error_fatal);
+ MemoryRegion *mr = sysbus_mmio_get_region(SYS_BUS_DEVICE(&ms->i2c), 0);
+ memory_region_add_subregion_overlap(sys_mem, DR_REG_I2C_EXT_BASE, mr, 0);
+ sysbus_connect_irq(SYS_BUS_DEVICE(&ms->i2c), 0,
+ qdev_get_gpio_in(intmatrix_dev, ETS_I2C_EXT0_INTR_SOURCE));
+ }
+
for (int i = 0; i < ESP32C3_UART_COUNT; ++i) {
const hwaddr uart_base[] = { DR_REG_UART_BASE, DR_REG_UART1_BASE };
sysbus_realize(SYS_BUS_DEVICE(&ms->uart[i]), &error_fatal);
@@ -641,7 +667,7 @@ static void esp32c3_machine_init(MachineState *machine)
@@ -641,7 +683,7 @@ static void esp32c3_machine_init(MachineState *machine)
}
/* RGB display realization */
- {
+ if (!ms->xteink_x3) {
+ if (!ms->xteink) {
/* Give the internal RAM memory region to the display */
ms->rgb.intram = dram;
sysbus_realize(SYS_BUS_DEVICE(&ms->rgb), &error_fatal);
@@ -659,6 +685,42 @@ static void esp32c3_machine_init(MachineState *machine)
@@ -659,6 +701,50 @@ static void esp32c3_machine_init(MachineState *machine)
}
+static void xteink_x3_machine_init(MachineState *machine)
+static void xteink_machine_init(MachineState *machine)
+{
+ Esp32C3MachineState *ms = ESP32C3_MACHINE(machine);
+ ms->xteink_x3 = true;
+ ms->xteink = true;
+ esp32c3_machine_init(machine);
+
+ /* X3 exposes the fingerprint chips; X4 omits them so stock firmware's
+ * all-NAK probe selects X4. The X4 panel is a blank protocol stub. */
+ if (!ms->x4) {
+ i2c_slave_create_simple(ms->i2c.bus, "xteink-fpchip", 0x55);
+ i2c_slave_create_simple(ms->i2c.bus, "xteink-fpchip", 0x68);
+ i2c_slave_create_simple(ms->i2c.bus, "xteink-fpchip", 0x6B);
+ }
+
+ DeviceState *panel = ssi_create_peripheral(ms->spi2.bus,
+ TYPE_XTEINK_X3_EINK);
+ ms->x4 ? TYPE_XTEINK_X4_EINK : TYPE_XTEINK_X3_EINK);
+ qdev_connect_gpio_out_named(DEVICE(&ms->gpio), ESP32_GPIO_OUTPUT, 21,
+ qdev_get_gpio_in_named(panel, SSI_GPIO_CS, 0));
+ qdev_connect_gpio_out_named(DEVICE(&ms->gpio), ESP32_GPIO_OUTPUT, 4,
@@ -548,27 +873,48 @@ index ed698fb..a49eae6 100644
/* Initialize machine type */
static void esp32c3_machine_class_init(ObjectClass *oc, void *data)
{
@@ -683,9 +745,23 @@ static const TypeInfo esp32c3_info = {
@@ -683,9 +769,44 @@ static const TypeInfo esp32c3_info = {
.class_init = esp32c3_machine_class_init,
};
+static void xteink_x3_machine_class_init(ObjectClass *oc, void *data)
+static char *xteink_get_variant(Object *obj, Error **errp)
+{
+ MachineClass *mc = MACHINE_CLASS(oc);
+ mc->desc = "xteink X3 (ESP32-C3, UC8253 e-ink)";
+ mc->init = xteink_x3_machine_init;
+ return g_strdup(ESP32C3_MACHINE(obj)->x4 ? "x4" : "x3");
+}
+
+static const TypeInfo xteink_x3_info = {
+ .name = MACHINE_TYPE_NAME("xteink-x3"),
+static void xteink_set_variant(Object *obj, const char *value, Error **errp)
+{
+ Esp32C3MachineState *ms = ESP32C3_MACHINE(obj);
+ if (g_str_equal(value, "x3")) {
+ ms->x4 = false;
+ } else if (g_str_equal(value, "x4")) {
+ ms->x4 = true;
+ } else {
+ error_setg(errp, "invalid variant '%s' (expected x3 or x4)", value);
+ }
+}
+
+static void xteink_machine_class_init(ObjectClass *oc, void *data)
+{
+ MachineClass *mc = MACHINE_CLASS(oc);
+ mc->desc = "xteink X3/X4 (ESP32-C3); variant=x3|x4";
+ mc->init = xteink_machine_init;
+ object_class_property_add_str(oc, "variant",
+ xteink_get_variant, xteink_set_variant);
+ object_class_property_set_description(oc, "variant",
+ "xteink model: x3 (default) or x4");
+}
+
+static const TypeInfo xteink_info = {
+ .name = MACHINE_TYPE_NAME("xteink"),
+ .parent = TYPE_ESP32C3_MACHINE,
+ .class_init = xteink_x3_machine_class_init,
+ .class_init = xteink_machine_class_init,
+};
+
static void esp32c3_machine_type_init(void)
{
type_register_static(&esp32c3_info);
+ type_register_static(&xteink_x3_info);
+ type_register_static(&xteink_info);
}
type_init(esp32c3_machine_type_init);
@@ -785,10 +1131,10 @@ index 0000000..1b914e1
+};
diff --git i/include/hw/display/xteink_x3_eink.h w/include/hw/display/xteink_x3_eink.h
new file mode 100644
index 0000000..64de018
index 0000000..4adc5fc
--- /dev/null
+++ w/include/hw/display/xteink_x3_eink.h
@@ -0,0 +1,29 @@
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "hw/ssi/ssi.h"
@@ -796,6 +1142,7 @@ index 0000000..64de018
+#include "ui/console.h"
+
+#define TYPE_XTEINK_X3_EINK "xteink-x3-eink"
+#define TYPE_XTEINK_X4_EINK "xteink-x4-eink"
+OBJECT_DECLARE_SIMPLE_TYPE(XteinkX3EinkState, XTEINK_X3_EINK)
+
+#define XTEINK_X3_EINK_DC "dc"
@@ -843,6 +1190,18 @@ index 163b41a..e2f52ad 100644
} Esp32GpioState;
typedef struct Esp32GpioClass {
diff --git i/include/hw/i2c/esp32_i2c.h w/include/hw/i2c/esp32_i2c.h
index b17dc75..42f30c3 100644
--- i/include/hw/i2c/esp32_i2c.h
+++ w/include/hw/i2c/esp32_i2c.h
@@ -24,6 +24,7 @@ typedef struct Esp32I2CState {
Fifo8 rx_fifo;
Fifo8 tx_fifo;
bool trans_ongoing;
+ bool c3;
uint32_t ctr_reg;
uint32_t timeout_reg;
diff --git i/include/hw/ssi/esp32c3_spi2.h w/include/hw/ssi/esp32c3_spi2.h
new file mode 100644
index 0000000..2155c36