On 7/15/26 17:50, Mikail Sadic wrote:
Add a PMBus device model for the TI-UCD90320 24-rail power
sequencer. Configures 24 pages with linear vout mode and responds to
the vendor-specific UCD9000_DEVICE_ID, NUM_PAGES, MONITOR_CONFIG, and
MFR_STATUS commands, allowing the Linux ucd9000 driver to bind and
create hwmon sysfs entries.
Signed-off-by: Mikail Sadic <[email protected]>
---
MAINTAINERS | 1 +
docs/specs/index.rst | 1 +
docs/specs/ucd90320.rst | 36 ++++++++++
hw/i2c/aspeed_i2c.c | 7 ++
hw/sensor/ucd90320.c | 144 ++++++++++++++++++++++++++++++++++++++++
hw/arm/Kconfig | 1 +
hw/sensor/Kconfig | 4 ++
hw/sensor/meson.build | 1 +
8 files changed, 195 insertions(+)
create mode 100644 docs/specs/ucd90320.rst
create mode 100644 hw/sensor/ucd90320.c
diff --git a/MAINTAINERS b/MAINTAINERS
index ecb8cfdc41..b2861dfc79 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4059,6 +4059,7 @@ F: hw/i2c/pmbus_device.c
F: hw/sensor/adm1272.c
F: hw/sensor/isl_pmbus_vr.c
F: hw/sensor/max34451.c
+F: hw/sensor/ucd90320.c
F: include/hw/i2c/pmbus_device.h
F: include/hw/sensor/isl_pmbus_vr.h
F: tests/qtest/adm1272-test.c
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index b7909a108a..4de65e2fdf 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -39,4 +39,5 @@ guest hardware that is specific to QEMU.
riscv-iommu
riscv-aia
aspeed-intc
+ ucd90320
iommu-testdev
diff --git a/docs/specs/ucd90320.rst b/docs/specs/ucd90320.rst
new file mode 100644
index 0000000000..fdc0878c75
--- /dev/null
+++ b/docs/specs/ucd90320.rst
@@ -0,0 +1,36 @@
+Texas Instruments UCD90320 Power Sequencer
+==========================================
+
+The UCD90320 is a 24-rail PMBus power sequencer. QEMU models it as a PMBus
+device (``"ucd90320"``) so the Linux ``ucd9000`` driver can bind and create
+``hwmon`` sysfs entries.
+
+The model configures 24 PMBus pages (one per rail) with ``VOUT_MODE = 0x00``
+(linear, exponent 0). ``READ_VOUT`` and ``MFR_STATUS`` return zero on all
+pages.
+
+Vendor-specific commands handled:
+
++------+---------------------------+--------------------------------------------+
+| Code | Name | Response
|
++======+===========================+============================================+
+| 0xD5 | ``UCD9000_MONITOR_CONFIG``| Block: one byte ``0x00``
|
++------+---------------------------+--------------------------------------------+
+| 0xD6 | ``UCD9000_NUM_PAGES`` | Byte: ``24``
|
++------+---------------------------+--------------------------------------------+
+| 0xF3 | ``UCD9000_MFR_STATUS`` | Block: four bytes ``0x00 0x00 0x00 0x00``
|
++------+---------------------------+--------------------------------------------+
+| 0xFD | ``UCD9000_DEVICE_ID`` | Block: ASCII string ``"UCD90320"``
|
++------+---------------------------+--------------------------------------------+
+
+``DEVICE_ID`` uses the vendor-specific code ``0xFD`` rather than the standard
+``PMBUS_IC_DEVICE_ID`` (``0xAD``), matching the Linux ``ucd9000`` probe
+sequence.
+
+The UCD90320 is instantiated automatically in the ``huygens-bmc`` machine on
+I2C bus 5 at address ``0x11``. To instantiate on a different Aspeed machine:
+
+.. code-block:: c
+
+ i2c_slave_create_simple(aspeed_i2c_get_bus(&soc->i2c, 5),
+ "ucd90320", 0x11);
diff --git a/hw/i2c/aspeed_i2c.c b/hw/i2c/aspeed_i2c.c
index 27afcaecee..facb54d27e 100644
--- a/hw/i2c/aspeed_i2c.c
+++ b/hw/i2c/aspeed_i2c.c
@@ -365,6 +365,7 @@ static void aspeed_i2c_bus_recv(AspeedI2CBus *bus)
uint32_t reg_pool_ctrl = aspeed_i2c_bus_pool_ctrl_offset(bus);
uint32_t reg_byte_buf = aspeed_i2c_bus_byte_buf_offset(bus);
uint32_t reg_dma_len = aspeed_i2c_bus_dma_len_offset(bus);
+ bool first_dma_byte;
int pool_rx_count = SHARED_ARRAY_FIELD_EX32(bus->regs, reg_pool_ctrl,
RX_SIZE) + 1;
@@ -391,6 +392,7 @@ static void aspeed_i2c_bus_recv(AspeedI2CBus *bus)
}
aspeed_i2c_set_rx_dma_dram_offset(bus);
+ first_dma_byte = true;
while (bus->regs[reg_dma_len]) {
MemTxResult result;
@@ -407,6 +409,11 @@ static void aspeed_i2c_bus_recv(AspeedI2CBus *bus)
return;
}
+ /* Mirror first byte to reg_byte_buf for I2C_M_RECV_LEN. */
+ if (first_dma_byte) {
+ SHARED_ARRAY_FIELD_DP32(bus->regs, reg_byte_buf, RX_BUF, data);
+ first_dma_byte = false;
+ }
bus->dma_dram_offset++;
bus->regs[reg_dma_len]--;
/* In new mode, keep track of how many bytes we RXed */
The I2C part belongs to another patch.
diff --git a/hw/sensor/ucd90320.c b/hw/sensor/ucd90320.c
new file mode 100644
index 0000000000..f3b2a5d777
--- /dev/null
+++ b/hw/sensor/ucd90320.c
@@ -0,0 +1,144 @@
+/*
+ * Texas Instruments UCD90320 24-Rail PMBus Power Sequencer
+ *
+ * Copyright 2026 IBM Corp.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/i2c/pmbus_device.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+
+#define TYPE_UCD90320 "ucd90320"
+
+/* UCD90320 has 24 sequenced power-rail pages */
+#define UCD90320_NUM_PAGES 24
+
+/* Vendor-specific command codes (not in the standard PMBus register table) */
+#define UCD9000_MONITOR_CONFIG 0xd5
+#define UCD9000_NUM_PAGES 0xd6
+#define UCD9000_MFR_STATUS 0xf3
+#define UCD9000_DEVICE_ID 0xfd
+
+typedef struct UCD90320State {
+ PMBusDevice parent;
+} UCD90320State;
+
+static void ucd90320_send_block(PMBusDevice *pmdev,
+ const uint8_t *data, uint8_t len)
+{
+ int i;
+
+ pmdev->out_buf[len + pmdev->out_buf_len] = len;
+ for (i = len - 1; i >= 0; i--) {
+ pmdev->out_buf[i + pmdev->out_buf_len] = data[len - 1 - i];
+ }
+ pmdev->out_buf_len += len + 1;
+}
+
+static uint8_t ucd90320_read_byte(PMBusDevice *pmdev)
+{
+ switch (pmdev->code) {
+ case UCD9000_DEVICE_ID: {
+ static const uint8_t id[] = "UCD90320";
this is hacky. use device constant reset values or class fields.
Same below.
+ ucd90320_send_block(pmdev, id, sizeof(id) - 1);
+ pmbus_idle(pmdev);
+ return 0;
+ }
+ case UCD9000_NUM_PAGES:
+ pmbus_send8(pmdev, UCD90320_NUM_PAGES);
+ pmbus_idle(pmdev);
+ return 0;
+
+ case UCD9000_MONITOR_CONFIG: {
+ static const uint8_t cfg[] = { 0x00 };
+ ucd90320_send_block(pmdev, cfg, sizeof(cfg));
+ pmbus_idle(pmdev);
+ return 0;
+ }
+ case UCD9000_MFR_STATUS: {
+ static const uint8_t status[] = { 0x00, 0x00, 0x00, 0x00 };
+ ucd90320_send_block(pmdev, status, sizeof(status));
+ pmbus_idle(pmdev);
+ return 0;
+ }
+ default:
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "%s: reading from unsupported register: 0x%02x\n",
+ __func__, pmdev->code);
+ break;
+ }
+ return 0xFF;
+}
+
+static int ucd90320_write_data(PMBusDevice *pmdev, const uint8_t *buf,
+ uint8_t len)
+{
+ if (len == 0) {
+ qemu_log_mask(LOG_GUEST_ERROR, "%s: writing empty data\n", __func__);
+ return -1;
+ }
+
+ pmdev->code = buf[0];
+
+ if (len == 1) {
+ return 0;
+ }
+
+ return 0;
+}
+
+static void ucd90320_exit_reset(Object *obj, ResetType type)
+{
+ PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+
+ pmdev->capability = 0x20; /* PEC supported */
+
+ for (int i = 0; i < UCD90320_NUM_PAGES; i++) {
+ pmdev->pages[i].operation = 0x80; /* on */
+ pmdev->pages[i].on_off_config = 0x1a;
+ pmdev->pages[i].vout_mode = 0x00; /* linear mode, exponent=0 */
+ pmdev->pages[i].read_vout = 0; /* rails off, pgood=0 */
+ }
+}
+
+static void ucd90320_init(Object *obj)
+{
+ PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+ uint64_t flags = PB_HAS_VOUT | PB_HAS_VOUT_MODE |
+ PB_HAS_STATUS_MFR_SPECIFIC;
+
+ for (int i = 0; i < UCD90320_NUM_PAGES; i++) {
+ pmbus_page_config(pmdev, i, flags);
+ }
+}
+
+static void ucd90320_class_init(ObjectClass *klass, const void *data)
+{
+ ResettableClass *rc = RESETTABLE_CLASS(klass);
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ PMBusDeviceClass *k = PMBUS_DEVICE_CLASS(klass);
+
+ dc->desc = "Texas Instruments UCD90320 24-Rail Power Sequencer";
+ k->write_data = ucd90320_write_data;
+ k->receive_byte = ucd90320_read_byte;
+ k->device_num_pages = UCD90320_NUM_PAGES;
+ rc->phases.exit = ucd90320_exit_reset;
+}
+
+static const TypeInfo ucd90320_info = {
+ .name = TYPE_UCD90320,
+ .parent = TYPE_PMBUS_DEVICE,
+ .instance_size = sizeof(UCD90320State),
+ .instance_init = ucd90320_init,
+ .class_init = ucd90320_class_init,
+};
+
+static void ucd90320_register_types(void)
+{
+ type_register_static(&ucd90320_info);
+}
+
+type_init(ucd90320_register_types)
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 82e0bc2e70..ac44740547 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -555,6 +555,7 @@ config ASPEED_SOC
select LED
select PMBUS
select MAX31785
+ select UCD90320
select FSI_APB2OPB_ASPEED
select AT24C
select PCI_EXPRESS
diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
index bc6331b4ab..135ffa7d5d 100644
--- a/hw/sensor/Kconfig
+++ b/hw/sensor/Kconfig
@@ -43,3 +43,7 @@ config ISL_PMBUS_VR
config MAX31785
bool
depends on PMBUS
+
+config UCD90320
+ bool
+ depends on PMBUS
diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
index 420fdc3359..c0583161d3 100644
--- a/hw/sensor/meson.build
+++ b/hw/sensor/meson.build
@@ -8,3 +8,4 @@ system_ss.add(when: 'CONFIG_MAX34451', if_true:
files('max34451.c'))
system_ss.add(when: 'CONFIG_LSM303DLHC_MAG', if_true:
files('lsm303dlhc_mag.c'))
system_ss.add(when: 'CONFIG_ISL_PMBUS_VR', if_true: files('isl_pmbus_vr.c'))
system_ss.add(when: 'CONFIG_MAX31785', if_true: files('max31785.c'))
+system_ss.add(when: 'CONFIG_UCD90320', if_true: files('ucd90320.c'))