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'))


Reply via email to