Simple I2C device with QOM properties to access a transmit buffer.
Can be used for testing or prototyping.

Signed-off-by: Timur Maksimov <[email protected]>
---
 hw/i2c/Kconfig            |   5 ++
 hw/i2c/i2c-dummy-device.c | 168 ++++++++++++++++++++++++++++++++++++++++++++++
 hw/i2c/meson.build        |   3 +-
 3 files changed, 175 insertions(+), 1 deletion(-)

diff --git a/hw/i2c/Kconfig b/hw/i2c/Kconfig
index 0766130b59..04912671db 100644
--- a/hw/i2c/Kconfig
+++ b/hw/i2c/Kconfig
@@ -6,6 +6,11 @@ config I2C_DEVICES
     # to any board's i2c bus
     bool
 
+config I2C_DUMMY_DEVICE
+    bool
+    default y if TEST_DEVICES
+    depends on I2C
+
 config SMBUS
     bool
     select I2C
diff --git a/hw/i2c/i2c-dummy-device.c b/hw/i2c/i2c-dummy-device.c
new file mode 100644
index 0000000000..645525a572
--- /dev/null
+++ b/hw/i2c/i2c-dummy-device.c
@@ -0,0 +1,168 @@
+/*
+ * QEMU I2C Dummy Device
+ *
+ * Copyright (c) 2026 Timur Maksimov <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/i2c/i2c.h"
+#include "hw/core/qdev-properties.h"
+#include "migration/vmstate.h"
+
+#define TYPE_I2C_DUMMY_DEVICE "i2c-dummy-device"
+OBJECT_DECLARE_SIMPLE_TYPE(I2CDummyDeviceState, I2C_DUMMY_DEVICE)
+
+#define I2C_DUMMY_ARRAY_SIZE 64
+
+struct I2CDummyDeviceState {
+    I2CSlave parent_obj;
+
+    uint8_t tx[I2C_DUMMY_ARRAY_SIZE];
+    uint8_t rx[I2C_DUMMY_ARRAY_SIZE];
+
+    uint8_t tx_pos;
+    uint8_t rx_pos;
+    uint8_t event;
+};
+
+static void i2c_dummy_device_reset(DeviceState *dev)
+{
+    I2CDummyDeviceState *s = I2C_DUMMY_DEVICE(dev);
+
+    s->tx_pos = 0;
+    s->rx_pos = 0;
+    s->event = I2C_FINISH;
+    memset(s->tx, 0, sizeof s->tx);
+    /* Don't reset rx as it is a readonly parameter for the model */
+}
+
+static int i2c_dummy_device_event(I2CSlave *i2c, enum i2c_event event)
+{
+    I2CDummyDeviceState *s = I2C_DUMMY_DEVICE(i2c);
+
+    switch (event) {
+    case I2C_START_SEND:
+        s->tx_pos = 0;
+        break;
+
+    case I2C_START_RECV:
+        s->rx_pos = 0;
+        break;
+
+    default:
+        break;
+    }
+
+    s->event = event;
+    return 0;
+}
+
+static int i2c_dummy_device_send(I2CSlave *i2c, uint8_t data)
+{
+    I2CDummyDeviceState *s = I2C_DUMMY_DEVICE(i2c);
+
+    if (s->tx_pos >= I2C_DUMMY_ARRAY_SIZE) {
+        /*
+         * The TX buffer is full. NACK the byte.
+         */
+        return 1;
+    }
+
+    if (s->event == I2C_START_SEND) {
+        s->tx[s->tx_pos++] = data;
+    }
+
+    return 0;
+}
+
+static uint8_t i2c_dummy_device_recv(I2CSlave *i2c)
+{
+    I2CDummyDeviceState *s = I2C_DUMMY_DEVICE(i2c);
+
+    if (s->rx_pos >= I2C_DUMMY_ARRAY_SIZE || s->event != I2C_START_RECV) {
+        /*
+         * There is no more data available.
+         */
+        return 0xff;
+    }
+
+    return s->rx[s->rx_pos++];
+}
+
+static void i2c_dummy_device_init(Object *obj)
+{
+    I2CDummyDeviceState *s = I2C_DUMMY_DEVICE(obj);
+
+    for (int i = 0; i < I2C_DUMMY_ARRAY_SIZE; i++) {
+        g_autofree char *name = g_strdup_printf("tx[%d]", i);
+
+        object_property_add_uint8_ptr(
+            obj, name, &s->tx[i],
+            OBJ_PROP_FLAG_READWRITE);
+    }
+
+    for (int i = 0; i < I2C_DUMMY_ARRAY_SIZE; i++) {
+        g_autofree char *name = g_strdup_printf("rx[%d]", i);
+
+        object_property_add_uint8_ptr(
+            obj, name, &s->rx[i],
+            OBJ_PROP_FLAG_READWRITE);
+    }
+
+    object_property_add_uint8_ptr(
+            obj, "tx-pos", &s->tx_pos,
+            OBJ_PROP_FLAG_READWRITE);
+    object_property_add_uint8_ptr(
+            obj, "rx-pos", &s->rx_pos,
+            OBJ_PROP_FLAG_READWRITE);
+}
+
+static const VMStateDescription vmstate_i2c_dummy_device = {
+    .name = TYPE_I2C_DUMMY_DEVICE,
+    .version_id = 1,
+    .minimum_version_id = 1,
+
+    .fields = (const VMStateField[]) {
+        VMSTATE_I2C_SLAVE(parent_obj, I2CDummyDeviceState),
+
+        VMSTATE_UINT8_ARRAY(tx, I2CDummyDeviceState,
+                            I2C_DUMMY_ARRAY_SIZE),
+
+        VMSTATE_UINT8_ARRAY(rx, I2CDummyDeviceState,
+                            I2C_DUMMY_ARRAY_SIZE),
+
+        VMSTATE_UINT8(tx_pos, I2CDummyDeviceState),
+        VMSTATE_UINT8(rx_pos, I2CDummyDeviceState),
+        VMSTATE_UINT8(event, I2CDummyDeviceState),
+
+        VMSTATE_END_OF_LIST()
+    },
+};
+
+static void i2c_dummy_device_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    I2CSlaveClass *isc = I2C_SLAVE_CLASS(klass);
+
+    device_class_set_legacy_reset(dc, i2c_dummy_device_reset);
+
+    dc->vmsd = &vmstate_i2c_dummy_device;
+
+    isc->event = i2c_dummy_device_event;
+    isc->send = i2c_dummy_device_send;
+    isc->recv = i2c_dummy_device_recv;
+}
+
+static const TypeInfo i2c_dummy_device_info[] = {
+    {
+        .name = TYPE_I2C_DUMMY_DEVICE,
+        .parent = TYPE_I2C_SLAVE,
+        .instance_size = sizeof(I2CDummyDeviceState),
+        .instance_init = i2c_dummy_device_init,
+        .class_init = i2c_dummy_device_class_init,
+    }
+};
+
+DEFINE_TYPES(i2c_dummy_device_info)
diff --git a/hw/i2c/meson.build b/hw/i2c/meson.build
index 88aea35662..4695cbcd28 100644
--- a/hw/i2c/meson.build
+++ b/hw/i2c/meson.build
@@ -1,5 +1,5 @@
 i2c_ss = ss.source_set()
-i2c_ss.add(when: 'CONFIG_I2C', if_true: files('core.c'))
+i2c_ss.add(when: 'CONFIG_I2C', if_true: files('core.c', 'i2c-dummy-device.c'))
 i2c_ss.add(when: 'CONFIG_SMBUS', if_true: files('smbus_slave.c', 
'smbus_master.c'))
 i2c_ss.add(when: 'CONFIG_ACPI_SMBUS', if_true: files('pm_smbus.c'))
 i2c_ss.add(when: 'CONFIG_ACPI_ICH9', if_true: files('smbus_ich9.c'))
@@ -19,4 +19,5 @@ i2c_ss.add(when: 'CONFIG_PPC4XX', if_true: 
files('ppc4xx_i2c.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'))
+i2c_ss.add(when: 'CONFIG_I2C_DUMMY_DEVICE', if_true: 
files('i2c-dummy-device.c'))
 system_ss.add_all(when: 'CONFIG_I2C', if_true: i2c_ss)

-- 
2.53.0


Reply via email to