All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v5 0/5] Add support for PMBus in QEMU
@ 2021-07-08 17:20 Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 1/5] hw/i2c: add support for PMBus Titus Rwantare
                   ` (4 more replies)
  0 siblings, 5 replies; 7+ messages in thread
From: Titus Rwantare @ 2021-07-08 17:20 UTC (permalink / raw)
  To: Corey Minyard; +Cc: qemu-arm, qemu-devel, Titus Rwantare

Hello,

This patch series adds an interface to start supporting PMBus devices in QEMU.
I’ve included two PMBus devices: MAX34451 and ADM1272.

PMBus is a variant of SMBus meant for digital management of power supplies.
PMBus adds to the SMBus standard by defining a number of constants and commands
used by compliant devices. The specification for PMBus can be found at:

https://pmbus.org/specification-archives/

Currently, the goal for these devices is to emulate basic functionality by
reading and writing registers. Timing, and some logical operation is not
implemented. This implementation supports nearly all available registers for
PMBus including:
   - Voltage inputs and outputs
   - Current inputs and outputs
   - Temperature sensors

Unimplimented registers get passed through to the device model, and device
models can opt out of using the standard registers with flags. The included
devices make use of these fields and illustrate how to interface with the pmbus
class.

Datasheets for sensors:

https://datasheets.maximintegrated.com/en/ds/MAX34451.pdf
https://www.analog.com/media/en/technical-documentation/data-sheets/ADM1272.pdf

Since v4:
- switched to BIT_ULL for flags to enable 32 bit hosts

Since v3:
- added VMState descriptions. Adding PMBusPage info to the vmsd has proven to be tricky, it's a TODO for now.

Since v2:
- bump for feedback
- removed commented out code

Since v1:
- addressed Joel's comments
- split out tests into their own patches

Thanks for reviewing,

Titus Rwantare

Titus Rwantare (5):
  hw/i2c: add support for PMBus
  hw/misc: add ADM1272 device
  tests/qtest: add tests for ADM1272 device model
  hw/misc: add MAX34451 device
  tests/qtest: add tests for MAX34451 device model

 include/hw/i2c/pmbus_device.h |  517 +++++++++++
 hw/i2c/pmbus_device.c         | 1612 +++++++++++++++++++++++++++++++++
 hw/misc/adm1272.c             |  544 +++++++++++
 hw/misc/max34451.c            |  775 ++++++++++++++++
 tests/qtest/adm1272-test.c    |  445 +++++++++
 tests/qtest/max34451-test.c   |  336 +++++++
 hw/arm/Kconfig                |    3 +
 hw/i2c/Kconfig                |    4 +
 hw/i2c/meson.build            |    1 +
 hw/misc/Kconfig               |    8 +
 hw/misc/meson.build           |    2 +
 tests/qtest/meson.build       |    2 +
 12 files changed, 4249 insertions(+)
 create mode 100644 include/hw/i2c/pmbus_device.h
 create mode 100644 hw/i2c/pmbus_device.c
 create mode 100644 hw/misc/adm1272.c
 create mode 100644 hw/misc/max34451.c
 create mode 100644 tests/qtest/adm1272-test.c
 create mode 100644 tests/qtest/max34451-test.c

-- 
2.32.0.93.g670b81a890-goog



^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v5 1/5] hw/i2c: add support for PMBus
  2021-07-08 17:20 [PATCH v5 0/5] Add support for PMBus in QEMU Titus Rwantare
@ 2021-07-08 17:20 ` Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 2/5] hw/misc: add ADM1272 device Titus Rwantare
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 7+ messages in thread
From: Titus Rwantare @ 2021-07-08 17:20 UTC (permalink / raw)
  To: Corey Minyard; +Cc: qemu-arm, qemu-devel, Titus Rwantare, Joel Stanley, Hao Wu

QEMU has support for SMBus devices, and PMBus is a more specific
implementation of SMBus. The additions made in this commit makes it easier to
add new PMBus devices to QEMU.

https://pmbus.org/specification-archives/

Reviewed-by: Joel Stanley <joel@jms.id.au>
Reviewed-by: Hao Wu <wuhaotsh@google.com>
Signed-off-by: Titus Rwantare <titusr@google.com>
---
 include/hw/i2c/pmbus_device.h |  517 +++++++++++
 hw/i2c/pmbus_device.c         | 1612 +++++++++++++++++++++++++++++++++
 hw/arm/Kconfig                |    1 +
 hw/i2c/Kconfig                |    4 +
 hw/i2c/meson.build            |    1 +
 5 files changed, 2135 insertions(+)
 create mode 100644 include/hw/i2c/pmbus_device.h
 create mode 100644 hw/i2c/pmbus_device.c

diff --git a/include/hw/i2c/pmbus_device.h b/include/hw/i2c/pmbus_device.h
new file mode 100644
index 0000000000..62bd38c83f
--- /dev/null
+++ b/include/hw/i2c/pmbus_device.h
@@ -0,0 +1,517 @@
+/*
+ * QEMU PMBus device emulation
+ *
+ * Copyright 2021 Google LLC
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_PMBUS_DEVICE_H
+#define HW_PMBUS_DEVICE_H
+
+#include "qemu/bitops.h"
+#include "hw/i2c/smbus_slave.h"
+
+enum pmbus_registers {
+    PMBUS_PAGE                      = 0x00, /* R/W byte */
+    PMBUS_OPERATION                 = 0x01, /* R/W byte */
+    PMBUS_ON_OFF_CONFIG             = 0x02, /* R/W byte */
+    PMBUS_CLEAR_FAULTS              = 0x03, /* Send Byte */
+    PMBUS_PHASE                     = 0x04, /* R/W byte */
+    PMBUS_PAGE_PLUS_WRITE           = 0x05, /* Block Write-only */
+    PMBUS_PAGE_PLUS_READ            = 0x06, /* Block Read-only */
+    PMBUS_WRITE_PROTECT             = 0x10, /* R/W byte */
+    PMBUS_STORE_DEFAULT_ALL         = 0x11, /* Send Byte */
+    PMBUS_RESTORE_DEFAULT_ALL       = 0x12, /* Send Byte */
+    PMBUS_STORE_DEFAULT_CODE        = 0x13, /* Write-only Byte */
+    PMBUS_RESTORE_DEFAULT_CODE      = 0x14, /* Write-only Byte */
+    PMBUS_STORE_USER_ALL            = 0x15, /* Send Byte */
+    PMBUS_RESTORE_USER_ALL          = 0x16, /* Send Byte */
+    PMBUS_STORE_USER_CODE           = 0x17, /* Write-only Byte */
+    PMBUS_RESTORE_USER_CODE         = 0x18, /* Write-only Byte */
+    PMBUS_CAPABILITY                = 0x19, /* Read-Only byte */
+    PMBUS_QUERY                     = 0x1A, /* Write-Only */
+    PMBUS_SMBALERT_MASK             = 0x1B, /* Block read, Word write */
+    PMBUS_VOUT_MODE                 = 0x20, /* R/W byte */
+    PMBUS_VOUT_COMMAND              = 0x21, /* R/W word */
+    PMBUS_VOUT_TRIM                 = 0x22, /* R/W word */
+    PMBUS_VOUT_CAL_OFFSET           = 0x23, /* R/W word */
+    PMBUS_VOUT_MAX                  = 0x24, /* R/W word */
+    PMBUS_VOUT_MARGIN_HIGH          = 0x25, /* R/W word */
+    PMBUS_VOUT_MARGIN_LOW           = 0x26, /* R/W word */
+    PMBUS_VOUT_TRANSITION_RATE      = 0x27, /* R/W word */
+    PMBUS_VOUT_DROOP                = 0x28, /* R/W word */
+    PMBUS_VOUT_SCALE_LOOP           = 0x29, /* R/W word */
+    PMBUS_VOUT_SCALE_MONITOR        = 0x2A, /* R/W word */
+    PMBUS_COEFFICIENTS              = 0x30, /* Read-only block 5 bytes */
+    PMBUS_POUT_MAX                  = 0x31, /* R/W word */
+    PMBUS_MAX_DUTY                  = 0x32, /* R/W word */
+    PMBUS_FREQUENCY_SWITCH          = 0x33, /* R/W word */
+    PMBUS_VIN_ON                    = 0x35, /* R/W word */
+    PMBUS_VIN_OFF                   = 0x36, /* R/W word */
+    PMBUS_INTERLEAVE                = 0x37, /* R/W word */
+    PMBUS_IOUT_CAL_GAIN             = 0x38, /* R/W word */
+    PMBUS_IOUT_CAL_OFFSET           = 0x39, /* R/W word */
+    PMBUS_FAN_CONFIG_1_2            = 0x3A, /* R/W byte */
+    PMBUS_FAN_COMMAND_1             = 0x3B, /* R/W word */
+    PMBUS_FAN_COMMAND_2             = 0x3C, /* R/W word */
+    PMBUS_FAN_CONFIG_3_4            = 0x3D, /* R/W byte */
+    PMBUS_FAN_COMMAND_3             = 0x3E, /* R/W word */
+    PMBUS_FAN_COMMAND_4             = 0x3F, /* R/W word */
+    PMBUS_VOUT_OV_FAULT_LIMIT       = 0x40, /* R/W word */
+    PMBUS_VOUT_OV_FAULT_RESPONSE    = 0x41, /* R/W byte */
+    PMBUS_VOUT_OV_WARN_LIMIT        = 0x42, /* R/W word */
+    PMBUS_VOUT_UV_WARN_LIMIT        = 0x43, /* R/W word */
+    PMBUS_VOUT_UV_FAULT_LIMIT       = 0x44, /* R/W word */
+    PMBUS_VOUT_UV_FAULT_RESPONSE    = 0x45, /* R/W byte */
+    PMBUS_IOUT_OC_FAULT_LIMIT       = 0x46, /* R/W word */
+    PMBUS_IOUT_OC_FAULT_RESPONSE    = 0x47, /* R/W byte */
+    PMBUS_IOUT_OC_LV_FAULT_LIMIT    = 0x48, /* R/W word */
+    PMBUS_IOUT_OC_LV_FAULT_RESPONSE = 0x49, /* R/W byte */
+    PMBUS_IOUT_OC_WARN_LIMIT        = 0x4A, /* R/W word */
+    PMBUS_IOUT_UC_FAULT_LIMIT       = 0x4B, /* R/W word */
+    PMBUS_IOUT_UC_FAULT_RESPONSE    = 0x4C, /* R/W byte */
+    PMBUS_OT_FAULT_LIMIT            = 0x4F, /* R/W word */
+    PMBUS_OT_FAULT_RESPONSE         = 0x50, /* R/W byte */
+    PMBUS_OT_WARN_LIMIT             = 0x51, /* R/W word */
+    PMBUS_UT_WARN_LIMIT             = 0x52, /* R/W word */
+    PMBUS_UT_FAULT_LIMIT            = 0x53, /* R/W word */
+    PMBUS_UT_FAULT_RESPONSE         = 0x54, /* R/W byte */
+    PMBUS_VIN_OV_FAULT_LIMIT        = 0x55, /* R/W word */
+    PMBUS_VIN_OV_FAULT_RESPONSE     = 0x56, /* R/W byte */
+    PMBUS_VIN_OV_WARN_LIMIT         = 0x57, /* R/W word */
+    PMBUS_VIN_UV_WARN_LIMIT         = 0x58, /* R/W word */
+    PMBUS_VIN_UV_FAULT_LIMIT        = 0x59, /* R/W word */
+    PMBUS_VIN_UV_FAULT_RESPONSE     = 0x5A, /* R/W byte */
+    PMBUS_IIN_OC_FAULT_LIMIT        = 0x5B, /* R/W word */
+    PMBUS_IIN_OC_FAULT_RESPONSE     = 0x5C, /* R/W byte */
+    PMBUS_IIN_OC_WARN_LIMIT         = 0x5D, /* R/W word */
+    PMBUS_POWER_GOOD_ON             = 0x5E, /* R/W word */
+    PMBUS_POWER_GOOD_OFF            = 0x5F, /* R/W word */
+    PMBUS_TON_DELAY                 = 0x60, /* R/W word */
+    PMBUS_TON_RISE                  = 0x61, /* R/W word */
+    PMBUS_TON_MAX_FAULT_LIMIT       = 0x62, /* R/W word */
+    PMBUS_TON_MAX_FAULT_RESPONSE    = 0x63, /* R/W byte */
+    PMBUS_TOFF_DELAY                = 0x64, /* R/W word */
+    PMBUS_TOFF_FALL                 = 0x65, /* R/W word */
+    PMBUS_TOFF_MAX_WARN_LIMIT       = 0x66, /* R/W word */
+    PMBUS_POUT_OP_FAULT_LIMIT       = 0x68, /* R/W word */
+    PMBUS_POUT_OP_FAULT_RESPONSE    = 0x69, /* R/W byte */
+    PMBUS_POUT_OP_WARN_LIMIT        = 0x6A, /* R/W word */
+    PMBUS_PIN_OP_WARN_LIMIT         = 0x6B, /* R/W word */
+    PMBUS_STATUS_BYTE               = 0x78, /* R/W byte */
+    PMBUS_STATUS_WORD               = 0x79, /* R/W word */
+    PMBUS_STATUS_VOUT               = 0x7A, /* R/W byte */
+    PMBUS_STATUS_IOUT               = 0x7B, /* R/W byte */
+    PMBUS_STATUS_INPUT              = 0x7C, /* R/W byte */
+    PMBUS_STATUS_TEMPERATURE        = 0x7D, /* R/W byte */
+    PMBUS_STATUS_CML                = 0x7E, /* R/W byte */
+    PMBUS_STATUS_OTHER              = 0x7F, /* R/W byte */
+    PMBUS_STATUS_MFR_SPECIFIC       = 0x80, /* R/W byte */
+    PMBUS_STATUS_FANS_1_2           = 0x81, /* R/W byte */
+    PMBUS_STATUS_FANS_3_4           = 0x82, /* R/W byte */
+    PMBUS_READ_EIN                  = 0x86, /* Read-Only block 5 bytes */
+    PMBUS_READ_EOUT                 = 0x87, /* Read-Only block 5 bytes */
+    PMBUS_READ_VIN                  = 0x88, /* Read-Only word */
+    PMBUS_READ_IIN                  = 0x89, /* Read-Only word */
+    PMBUS_READ_VCAP                 = 0x8A, /* Read-Only word */
+    PMBUS_READ_VOUT                 = 0x8B, /* Read-Only word */
+    PMBUS_READ_IOUT                 = 0x8C, /* Read-Only word */
+    PMBUS_READ_TEMPERATURE_1        = 0x8D, /* Read-Only word */
+    PMBUS_READ_TEMPERATURE_2        = 0x8E, /* Read-Only word */
+    PMBUS_READ_TEMPERATURE_3        = 0x8F, /* Read-Only word */
+    PMBUS_READ_FAN_SPEED_1          = 0x90, /* Read-Only word */
+    PMBUS_READ_FAN_SPEED_2          = 0x91, /* Read-Only word */
+    PMBUS_READ_FAN_SPEED_3          = 0x92, /* Read-Only word */
+    PMBUS_READ_FAN_SPEED_4          = 0x93, /* Read-Only word */
+    PMBUS_READ_DUTY_CYCLE           = 0x94, /* Read-Only word */
+    PMBUS_READ_FREQUENCY            = 0x95, /* Read-Only word */
+    PMBUS_READ_POUT                 = 0x96, /* Read-Only word */
+    PMBUS_READ_PIN                  = 0x97, /* Read-Only word */
+    PMBUS_REVISION                  = 0x98, /* Read-Only byte */
+    PMBUS_MFR_ID                    = 0x99, /* R/W block */
+    PMBUS_MFR_MODEL                 = 0x9A, /* R/W block */
+    PMBUS_MFR_REVISION              = 0x9B, /* R/W block */
+    PMBUS_MFR_LOCATION              = 0x9C, /* R/W block */
+    PMBUS_MFR_DATE                  = 0x9D, /* R/W block */
+    PMBUS_MFR_SERIAL                = 0x9E, /* R/W block */
+    PMBUS_APP_PROFILE_SUPPORT       = 0x9F, /* Read-Only block-read */
+    PMBUS_MFR_VIN_MIN               = 0xA0, /* Read-Only word */
+    PMBUS_MFR_VIN_MAX               = 0xA1, /* Read-Only word */
+    PMBUS_MFR_IIN_MAX               = 0xA2, /* Read-Only word */
+    PMBUS_MFR_PIN_MAX               = 0xA3, /* Read-Only word */
+    PMBUS_MFR_VOUT_MIN              = 0xA4, /* Read-Only word */
+    PMBUS_MFR_VOUT_MAX              = 0xA5, /* Read-Only word */
+    PMBUS_MFR_IOUT_MAX              = 0xA6, /* Read-Only word */
+    PMBUS_MFR_POUT_MAX              = 0xA7, /* Read-Only word */
+    PMBUS_MFR_TAMBIENT_MAX          = 0xA8, /* Read-Only word */
+    PMBUS_MFR_TAMBIENT_MIN          = 0xA9, /* Read-Only word */
+    PMBUS_MFR_EFFICIENCY_LL         = 0xAA, /* Read-Only block 14 bytes */
+    PMBUS_MFR_EFFICIENCY_HL         = 0xAB, /* Read-Only block 14 bytes */
+    PMBUS_MFR_PIN_ACCURACY          = 0xAC, /* Read-Only byte */
+    PMBUS_IC_DEVICE_ID              = 0xAD, /* Read-Only block-read */
+    PMBUS_IC_DEVICE_REV             = 0xAE, /* Read-Only block-read */
+    PMBUS_MFR_MAX_TEMP_1            = 0xC0, /* R/W word */
+    PMBUS_MFR_MAX_TEMP_2            = 0xC1, /* R/W word */
+    PMBUS_MFR_MAX_TEMP_3            = 0xC2, /* R/W word */
+};
+
+/* STATUS_WORD */
+#define PB_STATUS_VOUT           BIT(15)
+#define PB_STATUS_IOUT_POUT      BIT(14)
+#define PB_STATUS_INPUT          BIT(13)
+#define PB_STATUS_WORD_MFR       BIT(12)
+#define PB_STATUS_POWER_GOOD_N   BIT(11)
+#define PB_STATUS_FAN            BIT(10)
+#define PB_STATUS_OTHER          BIT(9)
+#define PB_STATUS_UNKNOWN        BIT(8)
+/* STATUS_BYTE */
+#define PB_STATUS_BUSY           BIT(7)
+#define PB_STATUS_OFF            BIT(6)
+#define PB_STATUS_VOUT_OV        BIT(5)
+#define PB_STATUS_IOUT_OC        BIT(4)
+#define PB_STATUS_VIN_UV         BIT(3)
+#define PB_STATUS_TEMPERATURE    BIT(2)
+#define PB_STATUS_CML            BIT(1)
+#define PB_STATUS_NONE_ABOVE     BIT(0)
+
+/* STATUS_VOUT */
+#define PB_STATUS_VOUT_OV_FAULT         BIT(7) /* Output Overvoltage Fault */
+#define PB_STATUS_VOUT_OV_WARN          BIT(6) /* Output Overvoltage Warning */
+#define PB_STATUS_VOUT_UV_WARN          BIT(5) /* Output Undervoltage Warning */
+#define PB_STATUS_VOUT_UV_FAULT         BIT(4) /* Output Undervoltage Fault */
+#define PB_STATUS_VOUT_MAX              BIT(3)
+#define PB_STATUS_VOUT_TON_MAX_FAULT    BIT(2)
+#define PB_STATUS_VOUT_TOFF_MAX_WARN    BIT(1)
+
+/* STATUS_IOUT */
+#define PB_STATUS_IOUT_OC_FAULT    BIT(7) /* Output Overcurrent Fault */
+#define PB_STATUS_IOUT_OC_LV_FAULT BIT(6) /* Output OC And Low Voltage Fault */
+#define PB_STATUS_IOUT_OC_WARN     BIT(5) /* Output Overcurrent Warning */
+#define PB_STATUS_IOUT_UC_FAULT    BIT(4) /* Output Undercurrent Fault */
+#define PB_STATUS_CURR_SHARE       BIT(3) /* Current Share Fault */
+#define PB_STATUS_PWR_LIM_MODE     BIT(2) /* In Power Limiting Mode */
+#define PB_STATUS_POUT_OP_FAULT    BIT(1) /* Output Overpower Fault */
+#define PB_STATUS_POUT_OP_WARN     BIT(0) /* Output Overpower Warning */
+
+/* STATUS_INPUT */
+#define PB_STATUS_INPUT_VIN_OV_FAULT    BIT(7) /* Input Overvoltage Fault */
+#define PB_STATUS_INPUT_VIN_OV_WARN     BIT(6) /* Input Overvoltage Warning */
+#define PB_STATUS_INPUT_VIN_UV_WARN     BIT(5) /* Input Undervoltage Warning */
+#define PB_STATUS_INPUT_VIN_UV_FAULT    BIT(4) /* Input Undervoltage Fault */
+#define PB_STATUS_INPUT_IIN_OC_FAULT    BIT(2) /* Input Overcurrent Fault */
+#define PB_STATUS_INPUT_IIN_OC_WARN     BIT(1) /* Input Overcurrent Warning */
+#define PB_STATUS_INPUT_PIN_OP_WARN     BIT(0) /* Input Overpower Warning */
+
+/* STATUS_TEMPERATURE */
+#define PB_STATUS_OT_FAULT              BIT(7) /* Overtemperature Fault */
+#define PB_STATUS_OT_WARN               BIT(6) /* Overtemperature Warning */
+#define PB_STATUS_UT_WARN               BIT(5) /* Undertemperature Warning */
+#define PB_STATUS_UT_FAULT              BIT(4) /* Undertemperature Fault */
+
+/* STATUS_CML */
+#define PB_CML_FAULT_INVALID_CMD     BIT(7) /* Invalid/Unsupported Command */
+#define PB_CML_FAULT_INVALID_DATA    BIT(6) /* Invalid/Unsupported Data  */
+#define PB_CML_FAULT_PEC             BIT(5) /* Packet Error Check Failed */
+#define PB_CML_FAULT_MEMORY          BIT(4) /* Memory Fault Detected */
+#define PB_CML_FAULT_PROCESSOR       BIT(3) /* Processor Fault Detected */
+#define PB_CML_FAULT_OTHER_COMM      BIT(1) /* Other communication fault */
+#define PB_CML_FAULT_OTHER_MEM_LOGIC BIT(0) /* Other Memory Or Logic Fault */
+
+/* OPERATION*/
+#define PB_OP_ON                BIT(7) /* PSU is switched on */
+#define PB_OP_MARGIN_HIGH       BIT(5) /* PSU vout is set to margin high */
+#define PB_OP_MARGIN_LOW        BIT(4) /* PSU vout is set to margin low */
+
+/* PAGES */
+#define PB_MAX_PAGES            0x1F
+#define PB_ALL_PAGES            0xFF
+
+#define TYPE_PMBUS_DEVICE "pmbus-device"
+OBJECT_DECLARE_TYPE(PMBusDevice, PMBusDeviceClass,
+                    PMBUS_DEVICE)
+
+/* flags */
+#define PB_HAS_COEFFICIENTS        BIT_ULL(9)
+#define PB_HAS_VIN                 BIT_ULL(10)
+#define PB_HAS_VOUT                BIT_ULL(11)
+#define PB_HAS_VOUT_MARGIN         BIT_ULL(12)
+#define PB_HAS_VIN_RATING          BIT_ULL(13)
+#define PB_HAS_VOUT_RATING         BIT_ULL(14)
+#define PB_HAS_VOUT_MODE           BIT_ULL(15)
+#define PB_HAS_IOUT                BIT_ULL(21)
+#define PB_HAS_IIN                 BIT_ULL(22)
+#define PB_HAS_IOUT_RATING         BIT_ULL(23)
+#define PB_HAS_IIN_RATING          BIT_ULL(24)
+#define PB_HAS_IOUT_GAIN           BIT_ULL(25)
+#define PB_HAS_POUT                BIT_ULL(30)
+#define PB_HAS_PIN                 BIT_ULL(31)
+#define PB_HAS_EIN                 BIT_ULL(32)
+#define PB_HAS_EOUT                BIT_ULL(33)
+#define PB_HAS_POUT_RATING         BIT_ULL(34)
+#define PB_HAS_PIN_RATING          BIT_ULL(35)
+#define PB_HAS_TEMPERATURE         BIT_ULL(40)
+#define PB_HAS_TEMP2               BIT_ULL(41)
+#define PB_HAS_TEMP3               BIT_ULL(42)
+#define PB_HAS_TEMP_RATING         BIT_ULL(43)
+#define PB_HAS_MFR_INFO            BIT_ULL(50)
+
+struct PMBusDeviceClass {
+    SMBusDeviceClass parent_class;
+    uint8_t device_num_pages;
+
+    /**
+     * Implement quick_cmd, receive byte, and write_data to support non-standard
+     * PMBus functionality
+     */
+    void (*quick_cmd)(PMBusDevice *dev, uint8_t read);
+    int (*write_data)(PMBusDevice *dev, const uint8_t *buf, uint8_t len);
+    uint8_t (*receive_byte)(PMBusDevice *dev);
+};
+
+/*
+ * According to the spec, each page may offer the full range of PMBus commands
+ * available for each output or non-PMBus device.
+ * Therefore, we can't assume that any registers will always be the same across
+ * all pages.
+ * The page 0xFF is intended for writes to all pages
+ */
+typedef struct PMBusPage {
+    uint64_t page_flags;
+
+    uint8_t page;                      /* R/W byte */
+    uint8_t operation;                 /* R/W byte */
+    uint8_t on_off_config;             /* R/W byte */
+    uint8_t write_protect;             /* R/W byte */
+    uint8_t phase;                     /* R/W byte */
+    uint8_t vout_mode;                 /* R/W byte */
+    uint16_t vout_command;             /* R/W word */
+    uint16_t vout_trim;                /* R/W word */
+    uint16_t vout_cal_offset;          /* R/W word */
+    uint16_t vout_max;                 /* R/W word */
+    uint16_t vout_margin_high;         /* R/W word */
+    uint16_t vout_margin_low;          /* R/W word */
+    uint16_t vout_transition_rate;     /* R/W word */
+    uint16_t vout_droop;               /* R/W word */
+    uint16_t vout_scale_loop;          /* R/W word */
+    uint16_t vout_scale_monitor;       /* R/W word */
+    uint8_t coefficients[5];           /* Read-only block 5 bytes */
+    uint16_t pout_max;                 /* R/W word */
+    uint16_t max_duty;                 /* R/W word */
+    uint16_t frequency_switch;         /* R/W word */
+    uint16_t vin_on;                   /* R/W word */
+    uint16_t vin_off;                  /* R/W word */
+    uint16_t iout_cal_gain;            /* R/W word */
+    uint16_t iout_cal_offset;          /* R/W word */
+    uint8_t fan_config_1_2;            /* R/W byte */
+    uint16_t fan_command_1;            /* R/W word */
+    uint16_t fan_command_2;            /* R/W word */
+    uint8_t fan_config_3_4;            /* R/W byte */
+    uint16_t fan_command_3;            /* R/W word */
+    uint16_t fan_command_4;            /* R/W word */
+    uint16_t vout_ov_fault_limit;      /* R/W word */
+    uint8_t vout_ov_fault_response;    /* R/W byte */
+    uint16_t vout_ov_warn_limit;       /* R/W word */
+    uint16_t vout_uv_warn_limit;       /* R/W word */
+    uint16_t vout_uv_fault_limit;      /* R/W word */
+    uint8_t vout_uv_fault_response;    /* R/W byte */
+    uint16_t iout_oc_fault_limit;      /* R/W word */
+    uint8_t iout_oc_fault_response;    /* R/W byte */
+    uint16_t iout_oc_lv_fault_limit;   /* R/W word */
+    uint8_t iout_oc_lv_fault_response; /* R/W byte */
+    uint16_t iout_oc_warn_limit;       /* R/W word */
+    uint16_t iout_uc_fault_limit;      /* R/W word */
+    uint8_t iout_uc_fault_response;    /* R/W byte */
+    uint16_t ot_fault_limit;           /* R/W word */
+    uint8_t ot_fault_response;         /* R/W byte */
+    uint16_t ot_warn_limit;            /* R/W word */
+    uint16_t ut_warn_limit;            /* R/W word */
+    uint16_t ut_fault_limit;           /* R/W word */
+    uint8_t ut_fault_response;         /* R/W byte */
+    uint16_t vin_ov_fault_limit;       /* R/W word */
+    uint8_t vin_ov_fault_response;     /* R/W byte */
+    uint16_t vin_ov_warn_limit;        /* R/W word */
+    uint16_t vin_uv_warn_limit;        /* R/W word */
+    uint16_t vin_uv_fault_limit;       /* R/W word */
+    uint8_t vin_uv_fault_response;     /* R/W byte */
+    uint16_t iin_oc_fault_limit;       /* R/W word */
+    uint8_t iin_oc_fault_response;     /* R/W byte */
+    uint16_t iin_oc_warn_limit;        /* R/W word */
+    uint16_t power_good_on;            /* R/W word */
+    uint16_t power_good_off;           /* R/W word */
+    uint16_t ton_delay;                /* R/W word */
+    uint16_t ton_rise;                 /* R/W word */
+    uint16_t ton_max_fault_limit;      /* R/W word */
+    uint8_t ton_max_fault_response;    /* R/W byte */
+    uint16_t toff_delay;               /* R/W word */
+    uint16_t toff_fall;                /* R/W word */
+    uint16_t toff_max_warn_limit;      /* R/W word */
+    uint16_t pout_op_fault_limit;      /* R/W word */
+    uint8_t pout_op_fault_response;    /* R/W byte */
+    uint16_t pout_op_warn_limit;       /* R/W word */
+    uint16_t pin_op_warn_limit;        /* R/W word */
+    uint16_t status_word;              /* R/W word */
+    uint8_t status_vout;               /* R/W byte */
+    uint8_t status_iout;               /* R/W byte */
+    uint8_t status_input;              /* R/W byte */
+    uint8_t status_temperature;        /* R/W byte */
+    uint8_t status_cml;                /* R/W byte */
+    uint8_t status_other;              /* R/W byte */
+    uint8_t status_mfr_specific;       /* R/W byte */
+    uint8_t status_fans_1_2;           /* R/W byte */
+    uint8_t status_fans_3_4;           /* R/W byte */
+    uint8_t read_ein[5];               /* Read-Only block 5 bytes */
+    uint8_t read_eout[5];              /* Read-Only block 5 bytes */
+    uint16_t read_vin;                 /* Read-Only word */
+    uint16_t read_iin;                 /* Read-Only word */
+    uint16_t read_vcap;                /* Read-Only word */
+    uint16_t read_vout;                /* Read-Only word */
+    uint16_t read_iout;                /* Read-Only word */
+    uint16_t read_temperature_1;       /* Read-Only word */
+    uint16_t read_temperature_2;       /* Read-Only word */
+    uint16_t read_temperature_3;       /* Read-Only word */
+    uint16_t read_fan_speed_1;         /* Read-Only word */
+    uint16_t read_fan_speed_2;         /* Read-Only word */
+    uint16_t read_fan_speed_3;         /* Read-Only word */
+    uint16_t read_fan_speed_4;         /* Read-Only word */
+    uint16_t read_duty_cycle;          /* Read-Only word */
+    uint16_t read_frequency;           /* Read-Only word */
+    uint16_t read_pout;                /* Read-Only word */
+    uint16_t read_pin;                 /* Read-Only word */
+    uint8_t revision;                  /* Read-Only byte */
+    const char *mfr_id;                /* R/W block */
+    const char *mfr_model;             /* R/W block */
+    const char *mfr_revision;          /* R/W block */
+    const char *mfr_location;          /* R/W block */
+    const char *mfr_date;              /* R/W block */
+    const char *mfr_serial;            /* R/W block */
+    const char *app_profile_support;   /* Read-Only block-read */
+    uint16_t mfr_vin_min;              /* Read-Only word */
+    uint16_t mfr_vin_max;              /* Read-Only word */
+    uint16_t mfr_iin_max;              /* Read-Only word */
+    uint16_t mfr_pin_max;              /* Read-Only word */
+    uint16_t mfr_vout_min;             /* Read-Only word */
+    uint16_t mfr_vout_max;             /* Read-Only word */
+    uint16_t mfr_iout_max;             /* Read-Only word */
+    uint16_t mfr_pout_max;             /* Read-Only word */
+    uint16_t mfr_tambient_max;         /* Read-Only word */
+    uint16_t mfr_tambient_min;         /* Read-Only word */
+    uint8_t mfr_efficiency_ll[14];     /* Read-Only block 14 bytes */
+    uint8_t mfr_efficiency_hl[14];     /* Read-Only block 14 bytes */
+    uint8_t mfr_pin_accuracy;          /* Read-Only byte */
+    uint16_t mfr_max_temp_1;           /* R/W word */
+    uint16_t mfr_max_temp_2;           /* R/W word */
+    uint16_t mfr_max_temp_3;           /* R/W word */
+} PMBusPage;
+
+/* State */
+struct PMBusDevice {
+    SMBusDevice smb;
+
+    uint8_t num_pages;
+    uint8_t code;
+    uint8_t page;
+
+    /*
+     * PMBus registers are stored in a PMBusPage structure allocated by
+     * calling pmbus_pages_alloc()
+     */
+    PMBusPage *pages;
+    uint8_t capability;
+
+
+    int32_t in_buf_len;
+    uint8_t *in_buf;
+    int32_t out_buf_len;
+    uint8_t out_buf[SMBUS_DATA_MAX_LEN];
+};
+
+/**
+ * Direct mode coefficients
+ * @var m - mantissa
+ * @var b - offset
+ * @var R - exponent
+ */
+typedef struct PMBusCoefficients {
+    int32_t m;     /* mantissa */
+    int64_t b;     /* offset */
+    int32_t R;     /* exponent */
+} PMBusCoefficients;
+
+/**
+ * Convert sensor values to direct mode format
+ *
+ * Y = (m * x - b) * 10^R
+ *
+ * @return uint32_t
+ */
+uint16_t pmbus_data2direct_mode(PMBusCoefficients c, uint32_t value);
+
+/**
+ * Convert direct mode formatted data into sensor reading
+ *
+ * X = (Y * 10^-R - b) / m
+ *
+ * @return uint32_t
+ */
+uint32_t pmbus_direct_mode2data(PMBusCoefficients c, uint16_t value);
+
+/**
+ * @brief Send a block of data over PMBus
+ * Assumes that the bytes in the block are already ordered correctly,
+ * also assumes the length has been prepended to the block if necessary
+ *     | low_byte | ... | high_byte |
+ * @param state - maintains state of the PMBus device
+ * @param data - byte array to be sent by device
+ * @param len - number
+ */
+void pmbus_send(PMBusDevice *state, const uint8_t *data, uint16_t len);
+void pmbus_send8(PMBusDevice *state, uint8_t data);
+void pmbus_send16(PMBusDevice *state, uint16_t data);
+void pmbus_send32(PMBusDevice *state, uint32_t data);
+void pmbus_send64(PMBusDevice *state, uint64_t data);
+
+/**
+ * @brief Send a string over PMBus with length prepended.
+ * Length is calculated using str_len()
+ */
+void pmbus_send_string(PMBusDevice *state, const char *data);
+
+/**
+ * @brief Receive data over PMBus
+ * These methods help track how much data is being received over PMBus
+ * Log to GUEST_ERROR if too much or too little is sent.
+ */
+uint8_t pmbus_receive8(PMBusDevice *pmdev);
+uint16_t pmbus_receive16(PMBusDevice *pmdev);
+uint32_t pmbus_receive32(PMBusDevice *pmdev);
+uint64_t pmbus_receive64(PMBusDevice *pmdev);
+
+/**
+ * PMBus page config must be called before any page is first used.
+ * It will allocate memory for all the pages if needed.
+ * Passed in flags overwrite existing flags if any.
+ * @param page_index the page to which the flags are applied, setting page_index
+ * to 0xFF applies the passed in flags to all pages.
+ * @param flags
+ */
+int pmbus_page_config(PMBusDevice *pmdev, uint8_t page_index, uint64_t flags);
+
+/**
+ * Update the status registers when sensor values change.
+ * Useful if modifying sensors through qmp, this way status registers get
+ * updated
+ */
+void pmbus_check_limits(PMBusDevice *pmdev);
+
+extern const VMStateDescription vmstate_pmbus_device;
+
+#define VMSTATE_PMBUS_DEVICE(_field, _state) {                       \
+    .name       = (stringify(_field)),                               \
+    .size       = sizeof(PMBusDevice),                               \
+    .vmsd       = &vmstate_pmbus_device,                             \
+    .flags      = VMS_STRUCT,                                        \
+    .offset     = vmstate_offset_value(_state, _field, PMBusDevice), \
+}
+
+#endif
diff --git a/hw/i2c/pmbus_device.c b/hw/i2c/pmbus_device.c
new file mode 100644
index 0000000000..24f8f522d9
--- /dev/null
+++ b/hw/i2c/pmbus_device.c
@@ -0,0 +1,1612 @@
+/*
+ * PMBus wrapper over SMBus
+ *
+ * Copyright 2021 Google LLC
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include <math.h>
+#include <string.h>
+#include "hw/i2c/pmbus_device.h"
+#include "migration/vmstate.h"
+#include "qemu/module.h"
+#include "qemu/log.h"
+
+uint16_t pmbus_data2direct_mode(PMBusCoefficients c, uint32_t value)
+{
+    /* R is usually negative to fit large readings into 16 bits */
+    uint16_t y = (c.m * value + c.b) * pow(10, c.R);
+    return y;
+}
+
+uint32_t pmbus_direct_mode2data(PMBusCoefficients c, uint16_t value)
+{
+    /* X = (Y * 10^-R - b) / m */
+    uint32_t x = (value / pow(10, c.R) - c.b) / c.m;
+    return x;
+}
+
+void pmbus_send(PMBusDevice *pmdev, const uint8_t *data, uint16_t len)
+{
+    if (pmdev->out_buf_len + len > SMBUS_DATA_MAX_LEN) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "PMBus device tried to send too much data");
+        len = 0;
+    }
+
+    for (int i = len - 1; i >= 0; i--) {
+        pmdev->out_buf[i + pmdev->out_buf_len] = data[len - i - 1];
+    }
+    pmdev->out_buf_len += len;
+}
+
+/* Internal only, convert unsigned ints to the little endian bus */
+static void pmbus_send_uint(PMBusDevice *pmdev, uint64_t data, uint8_t size)
+{
+    uint8_t bytes[8];
+    g_assert(size <= 8);
+
+    for (int i = 0; i < size; i++) {
+        bytes[i] = data & 0xFF;
+        data = data >> 8;
+    }
+    pmbus_send(pmdev, bytes, size);
+}
+
+void pmbus_send8(PMBusDevice *pmdev, uint8_t data)
+{
+    pmbus_send_uint(pmdev, data, 1);
+}
+
+void pmbus_send16(PMBusDevice *pmdev, uint16_t data)
+{
+    pmbus_send_uint(pmdev, data, 2);
+}
+
+void pmbus_send32(PMBusDevice *pmdev, uint32_t data)
+{
+    pmbus_send_uint(pmdev, data, 4);
+}
+
+void pmbus_send64(PMBusDevice *pmdev, uint64_t data)
+{
+    pmbus_send_uint(pmdev, data, 8);
+}
+
+void pmbus_send_string(PMBusDevice *pmdev, const char *data)
+{
+    size_t len = strlen(data);
+    g_assert(len > 0);
+    g_assert(len + pmdev->out_buf_len < SMBUS_DATA_MAX_LEN);
+    pmdev->out_buf[len + pmdev->out_buf_len] = len;
+
+    for (int 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 uint64_t pmbus_receive_uint(const uint8_t *buf, uint8_t len)
+{
+    uint64_t ret = 0;
+
+    /* Exclude command code from return value */
+    buf++;
+    len--;
+
+    for (int i = len - 1; i >= 0; i--) {
+        ret = ret << 8 | buf[i];
+    }
+    return ret;
+}
+
+uint8_t pmbus_receive8(PMBusDevice *pmdev)
+{
+    if (pmdev->in_buf_len - 1 != 1) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: length mismatch. Expected 1 byte, got %d bytes\n",
+                      __func__, pmdev->in_buf_len - 1);
+    }
+    return pmbus_receive_uint(pmdev->in_buf, pmdev->in_buf_len);
+}
+
+uint16_t pmbus_receive16(PMBusDevice *pmdev)
+{
+    if (pmdev->in_buf_len - 1 != 2) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: length mismatch. Expected 2 bytes, got %d bytes\n",
+                      __func__, pmdev->in_buf_len - 1);
+    }
+    return pmbus_receive_uint(pmdev->in_buf, pmdev->in_buf_len);
+}
+
+uint32_t pmbus_receive32(PMBusDevice *pmdev)
+{
+    if (pmdev->in_buf_len - 1 != 4) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: length mismatch. Expected 4 bytes, got %d bytes\n",
+                      __func__, pmdev->in_buf_len - 1);
+    }
+    return pmbus_receive_uint(pmdev->in_buf, pmdev->in_buf_len);
+}
+
+uint64_t pmbus_receive64(PMBusDevice *pmdev)
+{
+    if (pmdev->in_buf_len - 1 != 8) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: length mismatch. Expected 8 bytes, got %d bytes\n",
+                      __func__, pmdev->in_buf_len - 1);
+    }
+    return pmbus_receive_uint(pmdev->in_buf, pmdev->in_buf_len);
+}
+
+static uint8_t pmbus_out_buf_pop(PMBusDevice *pmdev)
+{
+    if (pmdev->out_buf_len == 0) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: tried to read from empty buffer",
+                      __func__);
+        return 0xFF;
+    }
+    uint8_t data = pmdev->out_buf[pmdev->out_buf_len - 1];
+    pmdev->out_buf_len--;
+    return data;
+}
+
+static void pmbus_quick_cmd(SMBusDevice *smd, uint8_t read)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(smd);
+    PMBusDeviceClass *pmdc = PMBUS_DEVICE_GET_CLASS(pmdev);
+
+    if (pmdc->quick_cmd) {
+        pmdc->quick_cmd(pmdev, read);
+    }
+}
+
+static void pmbus_pages_alloc(PMBusDevice *pmdev)
+{
+    /* some PMBus devices don't use the PAGE command, so they get 1 page */
+    PMBusDeviceClass *k = PMBUS_DEVICE_GET_CLASS(pmdev);
+    if (k->device_num_pages == 0) {
+        k->device_num_pages = 1;
+    }
+    pmdev->num_pages = k->device_num_pages;
+    pmdev->pages = g_new0(PMBusPage, k->device_num_pages);
+}
+
+void pmbus_check_limits(PMBusDevice *pmdev)
+{
+    for (int i = 0; i < pmdev->num_pages; i++) {
+        if ((pmdev->pages[i].operation & PB_OP_ON) == 0) {
+            continue;   /* don't check powered off devices */
+        }
+
+        if (pmdev->pages[i].read_vout > pmdev->pages[i].vout_ov_fault_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_VOUT;
+            pmdev->pages[i].status_vout |= PB_STATUS_VOUT_OV_FAULT;
+        }
+
+        if (pmdev->pages[i].read_vout > pmdev->pages[i].vout_ov_warn_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_VOUT;
+            pmdev->pages[i].status_vout |= PB_STATUS_VOUT_OV_WARN;
+        }
+
+        if (pmdev->pages[i].read_vout < pmdev->pages[i].vout_uv_warn_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_VOUT;
+            pmdev->pages[i].status_vout |= PB_STATUS_VOUT_UV_WARN;
+        }
+
+        if (pmdev->pages[i].read_vout < pmdev->pages[i].vout_uv_fault_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_VOUT;
+            pmdev->pages[i].status_vout |= PB_STATUS_VOUT_UV_FAULT;
+        }
+
+        if (pmdev->pages[i].read_vin > pmdev->pages[i].vin_ov_warn_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_INPUT;
+            pmdev->pages[i].status_input |= PB_STATUS_INPUT_VIN_OV_WARN;
+        }
+
+        if (pmdev->pages[i].read_vin < pmdev->pages[i].vin_uv_warn_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_INPUT;
+            pmdev->pages[i].status_input |= PB_STATUS_INPUT_VIN_UV_WARN;
+        }
+
+        if (pmdev->pages[i].read_iout > pmdev->pages[i].iout_oc_warn_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_IOUT_POUT;
+            pmdev->pages[i].status_iout |= PB_STATUS_IOUT_OC_WARN;
+        }
+
+        if (pmdev->pages[i].read_iout > pmdev->pages[i].iout_oc_fault_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_IOUT_POUT;
+            pmdev->pages[i].status_iout |= PB_STATUS_IOUT_OC_FAULT;
+        }
+
+        if (pmdev->pages[i].read_pin > pmdev->pages[i].pin_op_warn_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_INPUT;
+            pmdev->pages[i].status_input |= PB_STATUS_INPUT_PIN_OP_WARN;
+        }
+
+        if (pmdev->pages[i].read_temperature_1
+                > pmdev->pages[i].ot_fault_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_TEMPERATURE;
+            pmdev->pages[i].status_temperature |= PB_STATUS_OT_FAULT;
+        }
+
+        if (pmdev->pages[i].read_temperature_1
+                > pmdev->pages[i].ot_warn_limit) {
+            pmdev->pages[i].status_word |= PB_STATUS_TEMPERATURE;
+            pmdev->pages[i].status_temperature |= PB_STATUS_OT_WARN;
+        }
+    }
+}
+
+static uint8_t pmbus_receive_byte(SMBusDevice *smd)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(smd);
+    PMBusDeviceClass *pmdc = PMBUS_DEVICE_GET_CLASS(pmdev);
+    uint8_t ret = 0xFF;
+    uint8_t index = pmdev->page;
+
+    if (pmdev->out_buf_len != 0) {
+        ret = pmbus_out_buf_pop(pmdev);
+        return ret;
+    }
+
+    switch (pmdev->code) {
+    case PMBUS_PAGE:
+        pmbus_send8(pmdev, pmdev->page);
+        break;
+
+    case PMBUS_OPERATION:                 /* R/W byte */
+        pmbus_send8(pmdev, pmdev->pages[index].operation);
+        break;
+
+    case PMBUS_ON_OFF_CONFIG:             /* R/W byte */
+        pmbus_send8(pmdev, pmdev->pages[index].on_off_config);
+        break;
+
+    case PMBUS_PHASE:                     /* R/W byte */
+        pmbus_send8(pmdev, pmdev->pages[index].phase);
+        break;
+
+    case PMBUS_WRITE_PROTECT:             /* R/W byte */
+        pmbus_send8(pmdev, pmdev->pages[index].write_protect);
+        break;
+
+    case PMBUS_CAPABILITY:
+        pmbus_send8(pmdev, pmdev->capability);
+        break;
+
+    case PMBUS_VOUT_MODE:                 /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT_MODE) {
+            pmbus_send8(pmdev, pmdev->pages[index].vout_mode);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_COMMAND:              /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_command);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_TRIM:                 /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_trim);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_CAL_OFFSET:           /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_cal_offset);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_MAX:                  /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_max);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_MARGIN_HIGH:          /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT_MARGIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_margin_high);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_MARGIN_LOW:           /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT_MARGIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_margin_low);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_TRANSITION_RATE:      /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_transition_rate);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_DROOP:                /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_droop);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_SCALE_LOOP:           /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_scale_loop);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_SCALE_MONITOR:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_scale_monitor);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    /* TODO: implement coefficients support */
+
+    case PMBUS_POUT_MAX:                  /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_POUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].pout_max);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VIN_ON:                    /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].vin_on);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VIN_OFF:                   /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].vin_off);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IOUT_CAL_GAIN:             /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT_GAIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].iout_cal_gain);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_OV_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_ov_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_OV_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send8(pmdev, pmdev->pages[index].vout_ov_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_OV_WARN_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_ov_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_UV_WARN_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_uv_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_UV_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].vout_uv_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VOUT_UV_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send8(pmdev, pmdev->pages[index].vout_uv_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].iout_oc_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send8(pmdev, pmdev->pages[index].iout_oc_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_LV_FAULT_LIMIT:    /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].iout_oc_lv_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_LV_FAULT_RESPONSE: /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send8(pmdev, pmdev->pages[index].iout_oc_lv_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_WARN_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].iout_oc_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IOUT_UC_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].iout_uc_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IOUT_UC_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send8(pmdev, pmdev->pages[index].iout_uc_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_OT_FAULT_LIMIT:            /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmbus_send16(pmdev, pmdev->pages[index].ot_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_OT_FAULT_RESPONSE:         /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmbus_send8(pmdev, pmdev->pages[index].ot_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_OT_WARN_LIMIT:             /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmbus_send16(pmdev, pmdev->pages[index].ot_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_UT_WARN_LIMIT:             /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmbus_send16(pmdev, pmdev->pages[index].ut_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_UT_FAULT_LIMIT:            /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmbus_send16(pmdev, pmdev->pages[index].ut_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_UT_FAULT_RESPONSE:         /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmbus_send8(pmdev, pmdev->pages[index].ut_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VIN_OV_FAULT_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].vin_ov_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VIN_OV_FAULT_RESPONSE:     /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send8(pmdev, pmdev->pages[index].vin_ov_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VIN_OV_WARN_LIMIT:         /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].vin_ov_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VIN_UV_WARN_LIMIT:         /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].vin_uv_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VIN_UV_FAULT_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].vin_uv_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_VIN_UV_FAULT_RESPONSE:     /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send8(pmdev, pmdev->pages[index].vin_uv_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IIN_OC_FAULT_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].iin_oc_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IIN_OC_FAULT_RESPONSE:     /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IIN) {
+            pmbus_send8(pmdev, pmdev->pages[index].iin_oc_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_IIN_OC_WARN_LIMIT:         /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].iin_oc_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_POUT_OP_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_POUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].pout_op_fault_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_POUT_OP_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_POUT) {
+            pmbus_send8(pmdev, pmdev->pages[index].pout_op_fault_response);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_POUT_OP_WARN_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_POUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].pout_op_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_PIN_OP_WARN_LIMIT:         /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_PIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].pin_op_warn_limit);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_STATUS_BYTE:               /* R/W byte */
+        pmbus_send8(pmdev, pmdev->pages[index].status_word & 0xFF);
+        break;
+
+    case PMBUS_STATUS_WORD:               /* R/W word */
+        pmbus_send16(pmdev, pmdev->pages[index].status_word);
+        break;
+
+    case PMBUS_STATUS_VOUT:               /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send8(pmdev, pmdev->pages[index].status_vout);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_STATUS_IOUT:               /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send8(pmdev, pmdev->pages[index].status_iout);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_STATUS_INPUT:              /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN ||
+            pmdev->pages[index].page_flags & PB_HAS_IIN ||
+            pmdev->pages[index].page_flags & PB_HAS_PIN) {
+            pmbus_send8(pmdev, pmdev->pages[index].status_input);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_STATUS_TEMPERATURE:        /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmbus_send8(pmdev, pmdev->pages[index].status_temperature);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_STATUS_CML:                /* R/W byte */
+        pmbus_send8(pmdev, pmdev->pages[index].status_cml);
+        break;
+
+    case PMBUS_STATUS_OTHER:              /* R/W byte */
+        pmbus_send8(pmdev, pmdev->pages[index].status_other);
+        break;
+
+    case PMBUS_READ_EIN:                  /* Read-Only block 5 bytes */
+        if (pmdev->pages[index].page_flags & PB_HAS_EIN) {
+            pmbus_send(pmdev, pmdev->pages[index].read_ein, 5);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_EOUT:                 /* Read-Only block 5 bytes */
+        if (pmdev->pages[index].page_flags & PB_HAS_EOUT) {
+            pmbus_send(pmdev, pmdev->pages[index].read_eout, 5);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_VIN:                  /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_vin);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_IIN:                  /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_iin);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_VOUT:                 /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_vout);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_IOUT:                 /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_iout);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_TEMPERATURE_1:        /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_temperature_1);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_TEMPERATURE_2:        /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMP2) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_temperature_2);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_TEMPERATURE_3:        /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMP3) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_temperature_3);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_POUT:                 /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_POUT) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_pout);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_READ_PIN:                  /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_PIN) {
+            pmbus_send16(pmdev, pmdev->pages[index].read_pin);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_REVISION:                  /* Read-Only byte */
+        pmbus_send8(pmdev, pmdev->pages[index].revision);
+        break;
+
+    case PMBUS_MFR_ID:                    /* R/W block */
+        if (pmdev->pages[index].page_flags & PB_HAS_MFR_INFO) {
+            pmbus_send_string(pmdev, pmdev->pages[index].mfr_id);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_MODEL:                 /* R/W block */
+        if (pmdev->pages[index].page_flags & PB_HAS_MFR_INFO) {
+            pmbus_send_string(pmdev, pmdev->pages[index].mfr_model);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_REVISION:              /* R/W block */
+        if (pmdev->pages[index].page_flags & PB_HAS_MFR_INFO) {
+            pmbus_send_string(pmdev, pmdev->pages[index].mfr_revision);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_LOCATION:              /* R/W block */
+        if (pmdev->pages[index].page_flags & PB_HAS_MFR_INFO) {
+            pmbus_send_string(pmdev, pmdev->pages[index].mfr_location);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_VIN_MIN:               /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_vin_min);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_VIN_MAX:               /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_vin_max);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_IIN_MAX:               /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IIN_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_iin_max);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_PIN_MAX:               /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_PIN_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_pin_max);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_VOUT_MIN:              /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_vout_min);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_VOUT_MAX:              /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_vout_max);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_IOUT_MAX:              /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_iout_max);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_POUT_MAX:              /* Read-Only word */
+        if (pmdev->pages[index].page_flags & PB_HAS_POUT_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_pout_max);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_MAX_TEMP_1:            /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMP_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_max_temp_1);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_MAX_TEMP_2:            /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMP_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_max_temp_2);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_MFR_MAX_TEMP_3:            /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMP_RATING) {
+            pmbus_send16(pmdev, pmdev->pages[index].mfr_max_temp_3);
+        } else {
+            goto passthough;
+        }
+        break;
+
+    case PMBUS_CLEAR_FAULTS:              /* Send Byte */
+    case PMBUS_PAGE_PLUS_WRITE:           /* Block Write-only */
+    case PMBUS_STORE_DEFAULT_ALL:         /* Send Byte */
+    case PMBUS_RESTORE_DEFAULT_ALL:       /* Send Byte */
+    case PMBUS_STORE_DEFAULT_CODE:        /* Write-only Byte */
+    case PMBUS_RESTORE_DEFAULT_CODE:      /* Write-only Byte */
+    case PMBUS_STORE_USER_ALL:            /* Send Byte */
+    case PMBUS_RESTORE_USER_ALL:          /* Send Byte */
+    case PMBUS_STORE_USER_CODE:           /* Write-only Byte */
+    case PMBUS_RESTORE_USER_CODE:         /* Write-only Byte */
+    case PMBUS_QUERY:                     /* Write-Only */
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: reading from write only register 0x%02x\n",
+                      __func__, pmdev->code);
+        break;
+
+passthough:
+    default:
+        /* Pass through read request if not handled */
+        if (pmdc->receive_byte) {
+            ret = pmdc->receive_byte(pmdev);
+        }
+        break;
+    }
+
+    if (pmdev->out_buf_len != 0) {
+        ret = pmbus_out_buf_pop(pmdev);
+        return ret;
+    }
+
+    return ret;
+}
+
+/*
+ * PMBus clear faults command applies to all status registers, existing faults
+ * should separately get re-asserted.
+ */
+static void pmbus_clear_faults(PMBusDevice *pmdev)
+{
+    for (uint8_t i = 0; i < pmdev->num_pages; i++) {
+        pmdev->pages[i].status_word = 0;
+        pmdev->pages[i].status_vout = 0;
+        pmdev->pages[i].status_iout = 0;
+        pmdev->pages[i].status_input = 0;
+        pmdev->pages[i].status_temperature = 0;
+        pmdev->pages[i].status_cml = 0;
+        pmdev->pages[i].status_other = 0;
+        pmdev->pages[i].status_mfr_specific = 0;
+        pmdev->pages[i].status_fans_1_2 = 0;
+        pmdev->pages[i].status_fans_3_4 = 0;
+    }
+
+}
+
+/*
+ * PMBus operation is used to turn On and Off PSUs
+ * Therefore, default value for the Operation should be PB_OP_ON or 0x80
+ */
+static void pmbus_operation(PMBusDevice *pmdev)
+{
+    uint8_t index = pmdev->page;
+    if ((pmdev->pages[index].operation & PB_OP_ON) == 0) {
+        pmdev->pages[index].read_vout = 0;
+        pmdev->pages[index].read_iout = 0;
+        pmdev->pages[index].read_pout = 0;
+        return;
+    }
+
+    if (pmdev->pages[index].operation & (PB_OP_ON | PB_OP_MARGIN_HIGH)) {
+        pmdev->pages[index].read_vout = pmdev->pages[index].vout_margin_high;
+    }
+
+    if (pmdev->pages[index].operation & (PB_OP_ON | PB_OP_MARGIN_LOW)) {
+        pmdev->pages[index].read_vout = pmdev->pages[index].vout_margin_low;
+    }
+    pmbus_check_limits(pmdev);
+}
+
+static int pmbus_write_data(SMBusDevice *smd, uint8_t *buf, uint8_t len)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(smd);
+    PMBusDeviceClass *pmdc = PMBUS_DEVICE_GET_CLASS(pmdev);
+    int ret = 0;
+    uint8_t index;
+
+    if (len == 0) {
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: writing empty data\n", __func__);
+        return -1;
+    }
+
+    if (!pmdev->pages) { /* allocate memory for pages on first use */
+        pmbus_pages_alloc(pmdev);
+    }
+
+    pmdev->in_buf_len = len;
+    pmdev->in_buf = buf;
+
+    pmdev->code = buf[0]; /* PMBus command code */
+    if (len == 1) { /* Single length writes are command codes only */
+        return 0;
+    }
+
+    if (pmdev->code == PMBUS_PAGE) {
+        pmdev->page = pmbus_receive8(pmdev);
+        return 0;
+    }
+    /* loop through all the pages when 0xFF is received */
+    if (pmdev->page == PB_ALL_PAGES) {
+        for (int i = 0; i < pmdev->num_pages; i++) {
+            pmdev->page = i;
+            pmbus_write_data(smd, buf, len);
+        }
+        pmdev->page = PB_ALL_PAGES;
+        return 0;
+    }
+
+    index = pmdev->page;
+
+    switch (pmdev->code) {
+    case PMBUS_OPERATION:                 /* R/W byte */
+        pmdev->pages[index].operation = pmbus_receive8(pmdev);
+        pmbus_operation(pmdev);
+        break;
+
+    case PMBUS_ON_OFF_CONFIG:             /* R/W byte */
+        pmdev->pages[index].on_off_config = pmbus_receive8(pmdev);
+        break;
+
+    case PMBUS_CLEAR_FAULTS:              /* Send Byte */
+        pmbus_clear_faults(pmdev);
+        break;
+
+    case PMBUS_PHASE:                     /* R/W byte */
+        pmdev->pages[index].phase = pmbus_receive8(pmdev);
+        break;
+
+    case PMBUS_PAGE_PLUS_WRITE:           /* Block Write-only */
+    case PMBUS_WRITE_PROTECT:             /* R/W byte */
+        pmdev->pages[index].write_protect = pmbus_receive8(pmdev);
+        break;
+
+    case PMBUS_VOUT_MODE:                 /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT_MODE) {
+            pmdev->pages[index].vout_mode = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_COMMAND:              /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_command = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_TRIM:                 /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_trim = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_CAL_OFFSET:           /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_cal_offset = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_MAX:                  /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_max = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_MARGIN_HIGH:          /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT_MARGIN) {
+            pmdev->pages[index].vout_margin_high = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_MARGIN_LOW:           /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT_MARGIN) {
+            pmdev->pages[index].vout_margin_low = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_TRANSITION_RATE:      /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_transition_rate = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_DROOP:                /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_droop = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_SCALE_LOOP:           /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_scale_loop = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_SCALE_MONITOR:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_scale_monitor = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_POUT_MAX:                  /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].pout_max = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VIN_ON:                    /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmdev->pages[index].vin_on = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VIN_OFF:                   /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmdev->pages[index].vin_off = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IOUT_CAL_GAIN:             /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT_GAIN) {
+            pmdev->pages[index].iout_cal_gain = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_OV_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_ov_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_OV_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_ov_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_OV_WARN_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_ov_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_UV_WARN_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_uv_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_UV_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_uv_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VOUT_UV_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].vout_uv_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmdev->pages[index].iout_oc_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmdev->pages[index].iout_oc_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_LV_FAULT_LIMIT:    /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmdev->pages[index].iout_oc_lv_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_LV_FAULT_RESPONSE: /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmdev->pages[index].iout_oc_lv_fault_response
+                = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IOUT_OC_WARN_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmdev->pages[index].iout_oc_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IOUT_UC_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmdev->pages[index].iout_uc_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IOUT_UC_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmdev->pages[index].iout_uc_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_OT_FAULT_LIMIT:            /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmdev->pages[index].ot_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_OT_FAULT_RESPONSE:         /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmdev->pages[index].ot_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_OT_WARN_LIMIT:             /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmdev->pages[index].ot_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_UT_WARN_LIMIT:             /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmdev->pages[index].ut_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_UT_FAULT_LIMIT:            /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmdev->pages[index].ut_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_UT_FAULT_RESPONSE:         /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmdev->pages[index].ut_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VIN_OV_FAULT_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmdev->pages[index].vin_ov_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VIN_OV_FAULT_RESPONSE:     /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmdev->pages[index].vin_ov_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VIN_OV_WARN_LIMIT:         /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmdev->pages[index].vin_ov_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VIN_UV_WARN_LIMIT:         /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmdev->pages[index].vin_uv_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VIN_UV_FAULT_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmdev->pages[index].vin_uv_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_VIN_UV_FAULT_RESPONSE:     /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VIN) {
+            pmdev->pages[index].vin_uv_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IIN_OC_FAULT_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IIN) {
+            pmdev->pages[index].iin_oc_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IIN_OC_FAULT_RESPONSE:     /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IIN) {
+            pmdev->pages[index].iin_oc_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_IIN_OC_WARN_LIMIT:         /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_IIN) {
+            pmdev->pages[index].iin_oc_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_POUT_OP_FAULT_LIMIT:       /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].pout_op_fault_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_POUT_OP_FAULT_RESPONSE:    /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].pout_op_fault_response = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_POUT_OP_WARN_LIMIT:        /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].pout_op_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_PIN_OP_WARN_LIMIT:         /* R/W word */
+        if (pmdev->pages[index].page_flags & PB_HAS_PIN) {
+            pmdev->pages[index].pin_op_warn_limit = pmbus_receive16(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_STATUS_BYTE:               /* R/W byte */
+        pmdev->pages[index].status_word = pmbus_receive8(pmdev);
+        break;
+
+    case PMBUS_STATUS_WORD:               /* R/W word */
+        pmdev->pages[index].status_word = pmbus_receive16(pmdev);
+        break;
+
+    case PMBUS_STATUS_VOUT:               /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_VOUT) {
+            pmdev->pages[index].status_vout = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_STATUS_IOUT:               /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_IOUT) {
+            pmdev->pages[index].status_iout = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_STATUS_INPUT:              /* R/W byte */
+        pmdev->pages[index].status_input = pmbus_receive8(pmdev);
+        break;
+
+    case PMBUS_STATUS_TEMPERATURE:        /* R/W byte */
+        if (pmdev->pages[index].page_flags & PB_HAS_TEMPERATURE) {
+            pmdev->pages[index].status_temperature = pmbus_receive8(pmdev);
+        } else {
+            goto passthrough;
+        }
+        break;
+
+    case PMBUS_STATUS_CML:                /* R/W byte */
+        pmdev->pages[index].status_cml = pmbus_receive8(pmdev);
+        break;
+
+    case PMBUS_STATUS_OTHER:              /* R/W byte */
+        pmdev->pages[index].status_other = pmbus_receive8(pmdev);
+        break;
+
+    case PMBUS_PAGE_PLUS_READ:            /* Block Read-only */
+    case PMBUS_CAPABILITY:                /* Read-Only byte */
+    case PMBUS_COEFFICIENTS:              /* Read-only block 5 bytes */
+    case PMBUS_READ_EIN:                  /* Read-Only block 5 bytes */
+    case PMBUS_READ_EOUT:                 /* Read-Only block 5 bytes */
+    case PMBUS_READ_VIN:                  /* Read-Only word */
+    case PMBUS_READ_IIN:                  /* Read-Only word */
+    case PMBUS_READ_VCAP:                 /* Read-Only word */
+    case PMBUS_READ_VOUT:                 /* Read-Only word */
+    case PMBUS_READ_IOUT:                 /* Read-Only word */
+    case PMBUS_READ_TEMPERATURE_1:        /* Read-Only word */
+    case PMBUS_READ_TEMPERATURE_2:        /* Read-Only word */
+    case PMBUS_READ_TEMPERATURE_3:        /* Read-Only word */
+    case PMBUS_READ_FAN_SPEED_1:          /* Read-Only word */
+    case PMBUS_READ_FAN_SPEED_2:          /* Read-Only word */
+    case PMBUS_READ_FAN_SPEED_3:          /* Read-Only word */
+    case PMBUS_READ_FAN_SPEED_4:          /* Read-Only word */
+    case PMBUS_READ_DUTY_CYCLE:           /* Read-Only word */
+    case PMBUS_READ_FREQUENCY:            /* Read-Only word */
+    case PMBUS_READ_POUT:                 /* Read-Only word */
+    case PMBUS_READ_PIN:                  /* Read-Only word */
+    case PMBUS_REVISION:                  /* Read-Only byte */
+    case PMBUS_APP_PROFILE_SUPPORT:       /* Read-Only block-read */
+    case PMBUS_MFR_VIN_MIN:               /* Read-Only word */
+    case PMBUS_MFR_VIN_MAX:               /* Read-Only word */
+    case PMBUS_MFR_IIN_MAX:               /* Read-Only word */
+    case PMBUS_MFR_PIN_MAX:               /* Read-Only word */
+    case PMBUS_MFR_VOUT_MIN:              /* Read-Only word */
+    case PMBUS_MFR_VOUT_MAX:              /* Read-Only word */
+    case PMBUS_MFR_IOUT_MAX:              /* Read-Only word */
+    case PMBUS_MFR_POUT_MAX:              /* Read-Only word */
+    case PMBUS_MFR_TAMBIENT_MAX:          /* Read-Only word */
+    case PMBUS_MFR_TAMBIENT_MIN:          /* Read-Only word */
+    case PMBUS_MFR_EFFICIENCY_LL:         /* Read-Only block 14 bytes */
+    case PMBUS_MFR_EFFICIENCY_HL:         /* Read-Only block 14 bytes */
+    case PMBUS_MFR_PIN_ACCURACY:          /* Read-Only byte */
+    case PMBUS_IC_DEVICE_ID:              /* Read-Only block-read */
+    case PMBUS_IC_DEVICE_REV:             /* Read-Only block-read */
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: writing to read-only register 0x%02x\n",
+                      __func__, pmdev->code);
+        break;
+
+passthrough:
+    /* Unimplimented registers get passed to the device */
+    default:
+        if (pmdc->write_data) {
+            ret = pmdc->write_data(pmdev, buf, len);
+        }
+        break;
+    }
+    pmbus_check_limits(pmdev);
+    pmdev->in_buf_len = 0;
+    return ret;
+}
+
+int pmbus_page_config(PMBusDevice *pmdev, uint8_t index, uint64_t flags)
+{
+    if (!pmdev->pages) { /* allocate memory for pages on first use */
+        pmbus_pages_alloc(pmdev);
+    }
+
+    /* The 0xFF page is special for commands applying to all pages */
+    if (index == PB_ALL_PAGES) {
+        for (int i = 0; i < pmdev->num_pages; i++) {
+            pmdev->pages[i].page_flags = flags;
+        }
+        return 0;
+    }
+
+    if (index > pmdev->num_pages - 1) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: index %u is out of range\n",
+                      __func__, index);
+        return -1;
+    }
+
+    pmdev->pages[index].page_flags = flags;
+
+    return 0;
+}
+
+/* TODO: include pmbus page info in vmstate */
+const VMStateDescription vmstate_pmbus_device = {
+    .name = TYPE_PMBUS_DEVICE,
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (VMStateField[]) {
+        VMSTATE_SMBUS_DEVICE(smb, PMBusDevice),
+        VMSTATE_UINT8(num_pages, PMBusDevice),
+        VMSTATE_UINT8(code, PMBusDevice),
+        VMSTATE_UINT8(page, PMBusDevice),
+        VMSTATE_UINT8(capability, PMBusDevice),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void pmbus_device_finalize(Object *obj)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+    g_free(pmdev->pages);
+}
+
+static void pmbus_device_class_init(ObjectClass *klass, void *data)
+{
+    SMBusDeviceClass *k = SMBUS_DEVICE_CLASS(klass);
+
+    k->quick_cmd = pmbus_quick_cmd;
+    k->write_data = pmbus_write_data;
+    k->receive_byte = pmbus_receive_byte;
+}
+
+static const TypeInfo pmbus_device_type_info = {
+    .name = TYPE_PMBUS_DEVICE,
+    .parent = TYPE_SMBUS_DEVICE,
+    .instance_size = sizeof(PMBusDevice),
+    .instance_finalize = pmbus_device_finalize,
+    .abstract = true,
+    .class_size = sizeof(PMBusDeviceClass),
+    .class_init = pmbus_device_class_init,
+};
+
+static void pmbus_device_register_types(void)
+{
+    type_register_static(&pmbus_device_type_info);
+}
+
+type_init(pmbus_device_register_types)
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index b887f6a5b1..20bd60f10b 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -374,6 +374,7 @@ config NPCM7XX
     select ARM_GIC
     select AT24C  # EEPROM
     select PL310  # cache controller
+    select PMBUS
     select SERIAL
     select SSI
     select UNIMP
diff --git a/hw/i2c/Kconfig b/hw/i2c/Kconfig
index 8d120a25d5..8217cb5041 100644
--- a/hw/i2c/Kconfig
+++ b/hw/i2c/Kconfig
@@ -32,3 +32,7 @@ config MPC_I2C
 config PCA954X
     bool
     select I2C
+
+config PMBUS
+    bool
+    select SMBUS
diff --git a/hw/i2c/meson.build b/hw/i2c/meson.build
index dd3aef02b2..d3df273251 100644
--- a/hw/i2c/meson.build
+++ b/hw/i2c/meson.build
@@ -15,4 +15,5 @@ i2c_ss.add(when: 'CONFIG_VERSATILE_I2C', if_true: files('versatile_i2c.c'))
 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_PCA954X', if_true: files('i2c_mux_pca954x.c'))
+i2c_ss.add(when: 'CONFIG_PMBUS', if_true: files('pmbus_device.c'))
 softmmu_ss.add_all(when: 'CONFIG_I2C', if_true: i2c_ss)
-- 
2.32.0.93.g670b81a890-goog



^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v5 2/5] hw/misc: add ADM1272 device
  2021-07-08 17:20 [PATCH v5 0/5] Add support for PMBus in QEMU Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 1/5] hw/i2c: add support for PMBus Titus Rwantare
@ 2021-07-08 17:20 ` Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 3/5] tests/qtest: add tests for ADM1272 device model Titus Rwantare
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 7+ messages in thread
From: Titus Rwantare @ 2021-07-08 17:20 UTC (permalink / raw)
  To: Corey Minyard; +Cc: qemu-arm, qemu-devel, Titus Rwantare, Joel Stanley, Hao Wu

The ADM1272 is a PMBus compliant Hot Swap Controller and Digital Power
Monitor by Analog Devices.

This commit adds support for interfacing with it, and support for
setting and monitoring sensor limits.

Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ADM1272.pdf

Reviewed-by: Joel Stanley <joel@jms.id.au>
Reviewed-by: Hao Wu <wuhaotsh@google.com>
Signed-off-by: Titus Rwantare <titusr@google.com>
---
 hw/misc/adm1272.c   | 544 ++++++++++++++++++++++++++++++++++++++++++++
 hw/arm/Kconfig      |   1 +
 hw/misc/Kconfig     |   4 +
 hw/misc/meson.build |   1 +
 4 files changed, 550 insertions(+)
 create mode 100644 hw/misc/adm1272.c

diff --git a/hw/misc/adm1272.c b/hw/misc/adm1272.c
new file mode 100644
index 0000000000..c1f9cf3afd
--- /dev/null
+++ b/hw/misc/adm1272.c
@@ -0,0 +1,544 @@
+/*
+ * Analog Devices ADM1272 High Voltage Positive Hot Swap Controller and Digital
+ * Power Monitor with PMBus
+ *
+ * Copyright 2021 Google LLC
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include <string.h>
+#include "hw/i2c/pmbus_device.h"
+#include "hw/irq.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "trace.h"
+
+#define TYPE_ADM1272 "adm1272"
+#define ADM1272(obj) OBJECT_CHECK(ADM1272State, (obj), TYPE_ADM1272)
+
+#define ADM1272_RESTART_TIME            0xCC
+#define ADM1272_MFR_PEAK_IOUT           0xD0
+#define ADM1272_MFR_PEAK_VIN            0xD1
+#define ADM1272_MFR_PEAK_VOUT           0xD2
+#define ADM1272_MFR_PMON_CONTROL        0xD3
+#define ADM1272_MFR_PMON_CONFIG         0xD4
+#define ADM1272_MFR_ALERT1_CONFIG       0xD5
+#define ADM1272_MFR_ALERT2_CONFIG       0xD6
+#define ADM1272_MFR_PEAK_TEMPERATURE    0xD7
+#define ADM1272_MFR_DEVICE_CONFIG       0xD8
+#define ADM1272_MFR_POWER_CYCLE         0xD9
+#define ADM1272_MFR_PEAK_PIN            0xDA
+#define ADM1272_MFR_READ_PIN_EXT        0xDB
+#define ADM1272_MFR_READ_EIN_EXT        0xDC
+
+#define ADM1272_HYSTERESIS_LOW          0xF2
+#define ADM1272_HYSTERESIS_HIGH         0xF3
+#define ADM1272_STATUS_HYSTERESIS       0xF4
+#define ADM1272_STATUS_GPIO             0xF5
+#define ADM1272_STRT_UP_IOUT_LIM        0xF6
+
+/* Defaults */
+#define ADM1272_OPERATION_DEFAULT       0x80
+#define ADM1272_CAPABILITY_DEFAULT      0xB0
+#define ADM1272_CAPABILITY_NO_PEC       0x30
+#define ADM1272_DIRECT_MODE             0x40
+#define ADM1272_HIGH_LIMIT_DEFAULT      0x0FFF
+#define ADM1272_PIN_OP_DEFAULT          0x7FFF
+#define ADM1272_PMBUS_REVISION_DEFAULT  0x22
+#define ADM1272_MFR_ID_DEFAULT          "ADI"
+#define ADM1272_MODEL_DEFAULT           "ADM1272-A1"
+#define ADM1272_MFR_DEFAULT_REVISION    "25"
+#define ADM1272_DEFAULT_DATE            "160301"
+#define ADM1272_RESTART_TIME_DEFAULT    0x64
+#define ADM1272_PMON_CONTROL_DEFAULT    0x1
+#define ADM1272_PMON_CONFIG_DEFAULT     0x3F35
+#define ADM1272_DEVICE_CONFIG_DEFAULT   0x8
+#define ADM1272_HYSTERESIS_HIGH_DEFAULT     0xFFFF
+#define ADM1272_STRT_UP_IOUT_LIM_DEFAULT    0x000F
+#define ADM1272_VOLT_DEFAULT            12000
+#define ADM1272_IOUT_DEFAULT            25000
+#define ADM1272_PWR_DEFAULT             300  /* 12V 25A */
+#define ADM1272_SHUNT                   300 /* micro-ohms */
+#define ADM1272_VOLTAGE_COEFF_DEFAULT   1
+#define ADM1272_CURRENT_COEFF_DEFAULT   3
+#define ADM1272_PWR_COEFF_DEFAULT       7
+#define ADM1272_IOUT_OFFSET             0x5000
+#define ADM1272_IOUT_OFFSET             0x5000
+
+
+typedef struct ADM1272State {
+    PMBusDevice parent;
+
+    uint64_t ein_ext;
+    uint32_t pin_ext;
+    uint8_t restart_time;
+
+    uint16_t peak_vin;
+    uint16_t peak_vout;
+    uint16_t peak_iout;
+    uint16_t peak_temperature;
+    uint16_t peak_pin;
+
+    uint8_t pmon_control;
+    uint16_t pmon_config;
+    uint16_t alert1_config;
+    uint16_t alert2_config;
+    uint16_t device_config;
+
+    uint16_t hysteresis_low;
+    uint16_t hysteresis_high;
+    uint8_t status_hysteresis;
+    uint8_t status_gpio;
+
+    uint16_t strt_up_iout_lim;
+
+} ADM1272State;
+
+static const PMBusCoefficients adm1272_coefficients[] = {
+    [0] = { 6770, 0, -2 },        /* voltage, vrange 60V */
+    [1] = { 4062, 0, -2 },        /* voltage, vrange 100V */
+    [2] = { 1326, 20480, -1 },    /* current, vsense range 15mV */
+    [3] = { 663, 20480, -1 },     /* current, vsense range 30mV */
+    [4] = { 3512, 0, -2 },        /* power, vrange 60V, irange 15mV */
+    [5] = { 21071, 0, -3 },       /* power, vrange 100V, irange 15mV */
+    [6] = { 17561, 0, -3 },       /* power, vrange 60V, irange 30mV */
+    [7] = { 10535, 0, -3 },       /* power, vrange 100V, irange 30mV */
+    [8] = { 42, 31871, -1 },      /* temperature */
+};
+
+static void adm1272_check_limits(ADM1272State *s)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(s);
+
+    pmbus_check_limits(pmdev);
+
+    if (pmdev->pages[0].read_vout > s->peak_vout) {
+        s->peak_vout = pmdev->pages[0].read_vout;
+    }
+
+    if (pmdev->pages[0].read_vin > s->peak_vin) {
+        s->peak_vin = pmdev->pages[0].read_vin;
+    }
+
+    if (pmdev->pages[0].read_iout > s->peak_iout) {
+        s->peak_iout = pmdev->pages[0].read_iout;
+    }
+
+    if (pmdev->pages[0].read_temperature_1 > s->peak_temperature) {
+        s->peak_temperature = pmdev->pages[0].read_temperature_1;
+    }
+
+    if (pmdev->pages[0].read_pin > s->peak_pin) {
+        s->peak_pin = pmdev->pages[0].read_pin;
+    }
+}
+
+static uint16_t adm1272_millivolts_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_VOLTAGE_COEFF_DEFAULT];
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_millivolts(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_VOLTAGE_COEFF_DEFAULT];
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static uint16_t adm1272_milliamps_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_CURRENT_COEFF_DEFAULT];
+    /* Y = (m * r_sense * x - b) * 10^R */
+    c.m = c.m * ADM1272_SHUNT / 1000; /* micro-ohms */
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_milliamps(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_CURRENT_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static uint16_t adm1272_watts_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_PWR_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_watts(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_PWR_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static void adm1272_exit_reset(Object *obj)
+{
+    ADM1272State *s = ADM1272(obj);
+    PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+
+    pmdev->page = 0;
+    pmdev->pages[0].operation = ADM1272_OPERATION_DEFAULT;
+
+
+    pmdev->capability = ADM1272_CAPABILITY_NO_PEC;
+    pmdev->pages[0].revision = ADM1272_PMBUS_REVISION_DEFAULT;
+    pmdev->pages[0].vout_mode = ADM1272_DIRECT_MODE;
+    pmdev->pages[0].vout_ov_warn_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].vout_uv_warn_limit = 0;
+    pmdev->pages[0].iout_oc_warn_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].ot_fault_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].ot_warn_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].vin_ov_warn_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].vin_uv_warn_limit = 0;
+    pmdev->pages[0].pin_op_warn_limit = ADM1272_PIN_OP_DEFAULT;
+
+    pmdev->pages[0].status_word = 0;
+    pmdev->pages[0].status_vout = 0;
+    pmdev->pages[0].status_iout = 0;
+    pmdev->pages[0].status_input = 0;
+    pmdev->pages[0].status_temperature = 0;
+    pmdev->pages[0].status_mfr_specific = 0;
+
+    pmdev->pages[0].read_vin
+        = adm1272_millivolts_to_direct(ADM1272_VOLT_DEFAULT);
+    pmdev->pages[0].read_vout
+        = adm1272_millivolts_to_direct(ADM1272_VOLT_DEFAULT);
+    pmdev->pages[0].read_iout
+        = adm1272_milliamps_to_direct(ADM1272_IOUT_DEFAULT);
+    pmdev->pages[0].read_temperature_1 = 0;
+    pmdev->pages[0].read_pin = adm1272_watts_to_direct(ADM1272_PWR_DEFAULT);
+    pmdev->pages[0].revision = ADM1272_PMBUS_REVISION_DEFAULT;
+    pmdev->pages[0].mfr_id = ADM1272_MFR_ID_DEFAULT;
+    pmdev->pages[0].mfr_model = ADM1272_MODEL_DEFAULT;
+    pmdev->pages[0].mfr_revision = ADM1272_MFR_DEFAULT_REVISION;
+    pmdev->pages[0].mfr_date = ADM1272_DEFAULT_DATE;
+
+    s->pin_ext = 0;
+    s->ein_ext = 0;
+    s->restart_time = ADM1272_RESTART_TIME_DEFAULT;
+
+    s->peak_vin = 0;
+    s->peak_vout = 0;
+    s->peak_iout = 0;
+    s->peak_temperature = 0;
+    s->peak_pin = 0;
+
+    s->pmon_control = ADM1272_PMON_CONTROL_DEFAULT;
+    s->pmon_config = ADM1272_PMON_CONFIG_DEFAULT;
+    s->alert1_config = 0;
+    s->alert2_config = 0;
+    s->device_config = ADM1272_DEVICE_CONFIG_DEFAULT;
+
+    s->hysteresis_low = 0;
+    s->hysteresis_high = ADM1272_HYSTERESIS_HIGH_DEFAULT;
+    s->status_hysteresis = 0;
+    s->status_gpio = 0;
+
+    s->strt_up_iout_lim = ADM1272_STRT_UP_IOUT_LIM_DEFAULT;
+}
+
+static uint8_t adm1272_read_byte(PMBusDevice *pmdev)
+{
+    ADM1272State *s = ADM1272(pmdev);
+
+    switch (pmdev->code) {
+    case ADM1272_RESTART_TIME:
+        pmbus_send8(pmdev, s->restart_time);
+        break;
+
+    case ADM1272_MFR_PEAK_IOUT:
+        pmbus_send16(pmdev, s->peak_iout);
+        break;
+
+    case ADM1272_MFR_PEAK_VIN:
+        pmbus_send16(pmdev, s->peak_vin);
+        break;
+
+    case ADM1272_MFR_PEAK_VOUT:
+        pmbus_send16(pmdev, s->peak_vout);
+        break;
+
+    case ADM1272_MFR_PMON_CONTROL:
+        pmbus_send8(pmdev, s->pmon_control);
+        break;
+
+    case ADM1272_MFR_PMON_CONFIG:
+        pmbus_send16(pmdev, s->pmon_config);
+        break;
+
+    case ADM1272_MFR_ALERT1_CONFIG:
+        pmbus_send16(pmdev, s->alert1_config);
+        break;
+
+    case ADM1272_MFR_ALERT2_CONFIG:
+        pmbus_send16(pmdev, s->alert2_config);
+        break;
+
+    case ADM1272_MFR_PEAK_TEMPERATURE:
+        pmbus_send16(pmdev, s->peak_temperature);
+        break;
+
+    case ADM1272_MFR_DEVICE_CONFIG:
+        pmbus_send16(pmdev, s->device_config);
+        break;
+
+    case ADM1272_MFR_PEAK_PIN:
+        pmbus_send16(pmdev, s->peak_pin);
+        break;
+
+    case ADM1272_MFR_READ_PIN_EXT:
+        pmbus_send32(pmdev, s->pin_ext);
+        break;
+
+    case ADM1272_MFR_READ_EIN_EXT:
+        pmbus_send64(pmdev, s->ein_ext);
+        break;
+
+    case ADM1272_HYSTERESIS_LOW:
+        pmbus_send16(pmdev, s->hysteresis_low);
+        break;
+
+    case ADM1272_HYSTERESIS_HIGH:
+        pmbus_send16(pmdev, s->hysteresis_high);
+        break;
+
+    case ADM1272_STATUS_HYSTERESIS:
+        pmbus_send16(pmdev, s->status_hysteresis);
+        break;
+
+    case ADM1272_STATUS_GPIO:
+        pmbus_send16(pmdev, s->status_gpio);
+        break;
+
+    case ADM1272_STRT_UP_IOUT_LIM:
+        pmbus_send16(pmdev, s->strt_up_iout_lim);
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: reading from unsupported register: 0x%02x\n",
+                      __func__, pmdev->code);
+        return 0xFF;
+        break;
+    }
+
+    return 0;
+}
+
+static int adm1272_write_data(PMBusDevice *pmdev, const uint8_t *buf,
+                              uint8_t len)
+{
+    ADM1272State *s = ADM1272(pmdev);
+
+    if (len == 0) {
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: writing empty data\n", __func__);
+        return -1;
+    }
+
+    pmdev->code = buf[0]; /* PMBus command code */
+
+    if (len == 1) {
+        return 0;
+    }
+
+    /* Exclude command code from buffer */
+    buf++;
+    len--;
+
+    switch (pmdev->code) {
+
+    case ADM1272_RESTART_TIME:
+        s->restart_time = pmbus_receive8(pmdev);
+        break;
+
+    case ADM1272_MFR_PMON_CONTROL:
+        s->pmon_control = pmbus_receive8(pmdev);
+        break;
+
+    case ADM1272_MFR_PMON_CONFIG:
+        s->pmon_config = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_MFR_ALERT1_CONFIG:
+        s->alert1_config = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_MFR_ALERT2_CONFIG:
+        s->alert2_config = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_MFR_DEVICE_CONFIG:
+        s->device_config = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_MFR_POWER_CYCLE:
+        adm1272_exit_reset((Object *)s);
+        break;
+
+    case ADM1272_HYSTERESIS_LOW:
+        s->hysteresis_low = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_HYSTERESIS_HIGH:
+        s->hysteresis_high = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_STRT_UP_IOUT_LIM:
+        s->strt_up_iout_lim = pmbus_receive16(pmdev);
+        adm1272_check_limits(s);
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: writing to unsupported register: 0x%02x\n",
+                      __func__, pmdev->code);
+        break;
+    }
+    return 0;
+}
+
+static void adm1272_get(Object *obj, Visitor *v, const char *name, void *opaque,
+                        Error **errp)
+{
+    uint16_t value;
+
+    if (strcmp(name, "vin") == 0 || strcmp(name, "vout") == 0) {
+        value = adm1272_direct_to_millivolts(*(uint16_t *)opaque);
+    } else if (strcmp(name, "iout") == 0) {
+        value = adm1272_direct_to_milliamps(*(uint16_t *)opaque);
+    } else if (strcmp(name, "pin") == 0) {
+        value = adm1272_direct_to_watts(*(uint16_t *)opaque);
+    } else {
+        value = *(uint16_t *)opaque;
+    }
+
+    visit_type_uint16(v, name, &value, errp);
+}
+
+static void adm1272_set(Object *obj, Visitor *v, const char *name, void *opaque,
+                        Error **errp)
+{
+    ADM1272State *s = ADM1272(obj);
+    uint16_t *internal = opaque;
+    uint16_t value;
+
+    if (!visit_type_uint16(v, name, &value, errp)) {
+        return;
+    }
+
+    if (strcmp(name, "vin") == 0 || strcmp(name, "vout") == 0) {
+        *internal = adm1272_millivolts_to_direct(value);
+    } else if (strcmp(name, "iout") == 0) {
+        *internal = adm1272_milliamps_to_direct(value);
+    } else if (strcmp(name, "pin") == 0) {
+        *internal = adm1272_watts_to_direct(value);
+    } else {
+        *internal = value;
+    }
+
+    adm1272_check_limits(s);
+}
+
+static const VMStateDescription vmstate_adm1272 = {
+    .name = "ADM1272",
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (VMStateField[]){
+        VMSTATE_PMBUS_DEVICE(parent, ADM1272State),
+        VMSTATE_UINT64(ein_ext, ADM1272State),
+        VMSTATE_UINT32(pin_ext, ADM1272State),
+        VMSTATE_UINT8(restart_time, ADM1272State),
+
+        VMSTATE_UINT16(peak_vin, ADM1272State),
+        VMSTATE_UINT16(peak_vout, ADM1272State),
+        VMSTATE_UINT16(peak_iout, ADM1272State),
+        VMSTATE_UINT16(peak_temperature, ADM1272State),
+        VMSTATE_UINT16(peak_pin, ADM1272State),
+
+        VMSTATE_UINT8(pmon_control, ADM1272State),
+        VMSTATE_UINT16(pmon_config, ADM1272State),
+        VMSTATE_UINT16(alert1_config, ADM1272State),
+        VMSTATE_UINT16(alert2_config, ADM1272State),
+        VMSTATE_UINT16(device_config, ADM1272State),
+
+        VMSTATE_UINT16(hysteresis_low, ADM1272State),
+        VMSTATE_UINT16(hysteresis_high, ADM1272State),
+        VMSTATE_UINT8(status_hysteresis, ADM1272State),
+        VMSTATE_UINT8(status_gpio, ADM1272State),
+
+        VMSTATE_UINT16(strt_up_iout_lim, ADM1272State),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void adm1272_init(Object *obj)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+    uint64_t flags = PB_HAS_VOUT_MODE | PB_HAS_VOUT | PB_HAS_VIN | PB_HAS_IOUT |
+                     PB_HAS_PIN | PB_HAS_TEMPERATURE | PB_HAS_MFR_INFO;
+
+    pmbus_page_config(pmdev, 0, flags);
+
+    object_property_add(obj, "vin", "uint16",
+                        adm1272_get,
+                        adm1272_set, NULL, &pmdev->pages[0].read_vin);
+
+    object_property_add(obj, "vout", "uint16",
+                        adm1272_get,
+                        adm1272_set, NULL, &pmdev->pages[0].read_vout);
+
+    object_property_add(obj, "iout", "uint16",
+                        adm1272_get,
+                        adm1272_set, NULL, &pmdev->pages[0].read_iout);
+
+    object_property_add(obj, "pin", "uint16",
+                        adm1272_get,
+                        adm1272_set, NULL, &pmdev->pages[0].read_pin);
+
+}
+
+static void adm1272_class_init(ObjectClass *klass, void *data)
+{
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    PMBusDeviceClass *k = PMBUS_DEVICE_CLASS(klass);
+
+    dc->desc = "Analog Devices ADM1272 Hot Swap controller";
+    dc->vmsd = &vmstate_adm1272;
+    k->write_data = adm1272_write_data;
+    k->receive_byte = adm1272_read_byte;
+    k->device_num_pages = 1;
+
+    rc->phases.exit = adm1272_exit_reset;
+}
+
+static const TypeInfo adm1272_info = {
+    .name = TYPE_ADM1272,
+    .parent = TYPE_PMBUS_DEVICE,
+    .instance_size = sizeof(ADM1272State),
+    .instance_init = adm1272_init,
+    .class_init = adm1272_class_init,
+};
+
+static void adm1272_register_types(void)
+{
+    type_register_static(&adm1272_info);
+}
+
+type_init(adm1272_register_types)
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 20bd60f10b..e141084a79 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -371,6 +371,7 @@ config XLNX_VERSAL
 config NPCM7XX
     bool
     select A9MPCORE
+    select ADM1272
     select ARM_GIC
     select AT24C  # EEPROM
     select PL310  # cache controller
diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index c71ed25820..0774c1f70c 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -14,6 +14,10 @@ config ARMSSE_CPU_PWRCTRL
 config MAX111X
     bool
 
+config ADM1272
+    bool
+    depends on I2C
+
 config TMP105
     bool
     depends on I2C
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 66e1648533..8666e9225a 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -10,6 +10,7 @@ softmmu_ss.add(when: 'CONFIG_SGA', if_true: files('sga.c'))
 softmmu_ss.add(when: 'CONFIG_TMP105', if_true: files('tmp105.c'))
 softmmu_ss.add(when: 'CONFIG_TMP421', if_true: files('tmp421.c'))
 softmmu_ss.add(when: 'CONFIG_EMC141X', if_true: files('emc141x.c'))
+softmmu_ss.add(when: 'CONFIG_ADM1272', if_true: files('adm1272.c'))
 softmmu_ss.add(when: 'CONFIG_UNIMP', if_true: files('unimp.c'))
 softmmu_ss.add(when: 'CONFIG_EMPTY_SLOT', if_true: files('empty_slot.c'))
 softmmu_ss.add(when: 'CONFIG_LED', if_true: files('led.c'))
-- 
2.32.0.93.g670b81a890-goog



^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v5 3/5] tests/qtest: add tests for ADM1272 device model
  2021-07-08 17:20 [PATCH v5 0/5] Add support for PMBus in QEMU Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 1/5] hw/i2c: add support for PMBus Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 2/5] hw/misc: add ADM1272 device Titus Rwantare
@ 2021-07-08 17:20 ` Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 4/5] hw/misc: add MAX34451 device Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 5/5] tests/qtest: add tests for MAX34451 device model Titus Rwantare
  4 siblings, 0 replies; 7+ messages in thread
From: Titus Rwantare @ 2021-07-08 17:20 UTC (permalink / raw)
  To: Corey Minyard; +Cc: qemu-arm, qemu-devel, Titus Rwantare, Joel Stanley

Signed-off-by: Titus Rwantare <titusr@google.com>
Reviewed-by: Joel Stanley <joel@jms.id.au>
---
 tests/qtest/adm1272-test.c | 445 +++++++++++++++++++++++++++++++++++++
 tests/qtest/meson.build    |   1 +
 2 files changed, 446 insertions(+)
 create mode 100644 tests/qtest/adm1272-test.c

diff --git a/tests/qtest/adm1272-test.c b/tests/qtest/adm1272-test.c
new file mode 100644
index 0000000000..63f8514801
--- /dev/null
+++ b/tests/qtest/adm1272-test.c
@@ -0,0 +1,445 @@
+/*
+ * QTests for the ADM1272 hotswap controller
+ *
+ * Copyright 2021 Google LLC
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include <math.h>
+#include "hw/i2c/pmbus_device.h"
+#include "libqtest-single.h"
+#include "libqos/qgraph.h"
+#include "libqos/i2c.h"
+#include "qapi/qmp/qdict.h"
+#include "qapi/qmp/qnum.h"
+#include "qemu/bitops.h"
+
+#define TEST_ID "adm1272-test"
+#define TEST_ADDR (0x10)
+
+#define ADM1272_RESTART_TIME            0xCC
+#define ADM1272_MFR_PEAK_IOUT           0xD0
+#define ADM1272_MFR_PEAK_VIN            0xD1
+#define ADM1272_MFR_PEAK_VOUT           0xD2
+#define ADM1272_MFR_PMON_CONTROL        0xD3
+#define ADM1272_MFR_PMON_CONFIG         0xD4
+#define ADM1272_MFR_ALERT1_CONFIG       0xD5
+#define ADM1272_MFR_ALERT2_CONFIG       0xD6
+#define ADM1272_MFR_PEAK_TEMPERATURE    0xD7
+#define ADM1272_MFR_DEVICE_CONFIG       0xD8
+#define ADM1272_MFR_POWER_CYCLE         0xD9
+#define ADM1272_MFR_PEAK_PIN            0xDA
+#define ADM1272_MFR_READ_PIN_EXT        0xDB
+#define ADM1272_MFR_READ_EIN_EXT        0xDC
+
+#define ADM1272_HYSTERESIS_LOW          0xF2
+#define ADM1272_HYSTERESIS_HIGH         0xF3
+#define ADM1272_STATUS_HYSTERESIS       0xF4
+#define ADM1272_STATUS_GPIO             0xF5
+#define ADM1272_STRT_UP_IOUT_LIM        0xF6
+
+/* Defaults */
+#define ADM1272_OPERATION_DEFAULT       0x80
+#define ADM1272_CAPABILITY_DEFAULT      0xB0
+#define ADM1272_CAPABILITY_NO_PEC       0x30
+#define ADM1272_DIRECT_MODE             0x40
+#define ADM1272_HIGH_LIMIT_DEFAULT      0x0FFF
+#define ADM1272_PIN_OP_DEFAULT          0x7FFF
+#define ADM1272_PMBUS_REVISION_DEFAULT  0x22
+#define ADM1272_MFR_ID_DEFAULT          "ADI"
+#define ADM1272_MODEL_DEFAULT           "ADM1272-A1"
+#define ADM1272_MFR_DEFAULT_REVISION    "25"
+#define ADM1272_DEFAULT_DATE            "160301"
+#define ADM1272_RESTART_TIME_DEFAULT    0x64
+#define ADM1272_PMON_CONTROL_DEFAULT    0x1
+#define ADM1272_PMON_CONFIG_DEFAULT     0x3F35
+#define ADM1272_DEVICE_CONFIG_DEFAULT   0x8
+#define ADM1272_HYSTERESIS_HIGH_DEFAULT     0xFFFF
+#define ADM1272_STRT_UP_IOUT_LIM_DEFAULT    0x000F
+#define ADM1272_VOLT_DEFAULT            12000
+#define ADM1272_IOUT_DEFAULT            25000
+#define ADM1272_PWR_DEFAULT             300  /* 12V 25A */
+#define ADM1272_SHUNT                   300 /* micro-ohms */
+#define ADM1272_VOLTAGE_COEFF_DEFAULT   1
+#define ADM1272_CURRENT_COEFF_DEFAULT   3
+#define ADM1272_PWR_COEFF_DEFAULT       7
+#define ADM1272_IOUT_OFFSET             0x5000
+#define ADM1272_IOUT_OFFSET             0x5000
+
+static const PMBusCoefficients adm1272_coefficients[] = {
+    [0] = { 6770, 0, -2 },       /* voltage, vrange 60V */
+    [1] = { 4062, 0, -2 },       /* voltage, vrange 100V */
+    [2] = { 1326, 20480, -1 },   /* current, vsense range 15mV */
+    [3] = { 663, 20480, -1 },    /* current, vsense range 30mV */
+    [4] = { 3512, 0, -2 },       /* power, vrange 60V, irange 15mV */
+    [5] = { 21071, 0, -3 },      /* power, vrange 100V, irange 15mV */
+    [6] = { 17561, 0, -3 },      /* power, vrange 60V, irange 30mV */
+    [7] = { 10535, 0, -3 },      /* power, vrange 100V, irange 30mV */
+    [8] = { 42, 31871, -1 },     /* temperature */
+};
+
+uint16_t pmbus_data2direct_mode(PMBusCoefficients c, uint32_t value)
+{
+    /* R is usually negative to fit large readings into 16 bits */
+    uint16_t y = (c.m * value + c.b) * pow(10, c.R);
+    return y;
+}
+
+uint32_t pmbus_direct_mode2data(PMBusCoefficients c, uint16_t value)
+{
+    /* X = (Y * 10^-R - b) / m */
+    uint32_t x = (value / pow(10, c.R) - c.b) / c.m;
+    return x;
+}
+
+
+static uint16_t adm1272_millivolts_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_VOLTAGE_COEFF_DEFAULT];
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_millivolts(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_VOLTAGE_COEFF_DEFAULT];
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static uint16_t adm1272_milliamps_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_CURRENT_COEFF_DEFAULT];
+    /* Y = (m * r_sense * x - b) * 10^R */
+    c.m = c.m * ADM1272_SHUNT / 1000; /* micro-ohms */
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_milliamps(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_CURRENT_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static uint16_t adm1272_watts_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_PWR_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_watts(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_PWR_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static uint16_t qmp_adm1272_get(const char *id, const char *property)
+{
+    QDict *response;
+    uint64_t ret;
+
+    response = qmp("{ 'execute': 'qom-get', 'arguments': { 'path': %s, "
+                   "'property': %s } }", id, property);
+    g_assert(qdict_haskey(response, "return"));
+    ret = qnum_get_uint(qobject_to(QNum, qdict_get(response, "return")));
+    qobject_unref(response);
+    return ret;
+}
+
+static void qmp_adm1272_set(const char *id,
+                            const char *property,
+                            uint16_t value)
+{
+    QDict *response;
+
+    response = qmp("{ 'execute': 'qom-set', 'arguments': { 'path': %s, "
+                   "'property': %s, 'value': %u } }", id, property, value);
+    g_assert(qdict_haskey(response, "return"));
+    qobject_unref(response);
+}
+
+/* PMBus commands are little endian vs i2c_set16 in i2c.h which is big endian */
+static uint16_t adm1272_i2c_get16(QI2CDevice *i2cdev, uint8_t reg)
+{
+    uint8_t resp[2];
+    i2c_read_block(i2cdev, reg, resp, sizeof(resp));
+    return (resp[1] << 8) | resp[0];
+}
+
+/* PMBus commands are little endian vs i2c_set16 in i2c.h which is big endian */
+static void adm1272_i2c_set16(QI2CDevice *i2cdev, uint8_t reg, uint16_t value)
+{
+    uint8_t data[2];
+
+    data[0] = value & 255;
+    data[1] = value >> 8;
+    i2c_write_block(i2cdev, reg, data, sizeof(data));
+}
+
+static void test_defaults(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t value, i2c_value;
+    int16_t err;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+    value = qmp_adm1272_get(TEST_ID, "vout");
+    err = ADM1272_VOLT_DEFAULT - value;
+    g_assert_cmpuint(abs(err), <, ADM1272_VOLT_DEFAULT / 20);
+
+    i2c_value = i2c_get8(i2cdev, PMBUS_OPERATION);
+    g_assert_cmphex(i2c_value, ==, ADM1272_OPERATION_DEFAULT);
+
+    i2c_value = i2c_get8(i2cdev, PMBUS_VOUT_MODE);
+    g_assert_cmphex(i2c_value, ==, ADM1272_DIRECT_MODE);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_VOUT_OV_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, ADM1272_HIGH_LIMIT_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_VOUT_UV_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_IOUT_OC_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, ADM1272_HIGH_LIMIT_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_OT_FAULT_LIMIT);
+    g_assert_cmphex(i2c_value, ==, ADM1272_HIGH_LIMIT_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_OT_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, ADM1272_HIGH_LIMIT_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_VIN_OV_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, ADM1272_HIGH_LIMIT_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_VIN_UV_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_PIN_OP_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, ADM1272_PIN_OP_DEFAULT);
+
+    i2c_value = i2c_get8(i2cdev, PMBUS_REVISION);
+    g_assert_cmphex(i2c_value, ==, ADM1272_PMBUS_REVISION_DEFAULT);
+
+    i2c_value = i2c_get8(i2cdev, ADM1272_MFR_PMON_CONTROL);
+    g_assert_cmphex(i2c_value, ==, ADM1272_PMON_CONTROL_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_MFR_PMON_CONFIG);
+    g_assert_cmphex(i2c_value, ==, ADM1272_PMON_CONFIG_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_MFR_DEVICE_CONFIG);
+    g_assert_cmphex(i2c_value, ==, ADM1272_DEVICE_CONFIG_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_HYSTERESIS_HIGH);
+    g_assert_cmphex(i2c_value, ==, ADM1272_HYSTERESIS_HIGH_DEFAULT);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_STRT_UP_IOUT_LIM);
+    g_assert_cmphex(i2c_value, ==, ADM1272_STRT_UP_IOUT_LIM_DEFAULT);
+}
+
+/* test qmp access */
+static void test_tx_rx(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t i2c_value, value, i2c_voltage, i2c_pwr, lossy_value;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+
+    /* converting to direct mode is lossy - we generate the same loss here */
+    lossy_value =
+        adm1272_direct_to_millivolts(adm1272_millivolts_to_direct(1000));
+    qmp_adm1272_set(TEST_ID, "vin", 1000);
+    value = qmp_adm1272_get(TEST_ID, "vin");
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_VIN);
+    i2c_voltage = adm1272_direct_to_millivolts(i2c_value);
+    g_assert_cmpuint(value, ==, i2c_voltage);
+    g_assert_cmpuint(i2c_voltage, ==, lossy_value);
+
+    lossy_value =
+        adm1272_direct_to_millivolts(adm1272_millivolts_to_direct(1500));
+    qmp_adm1272_set(TEST_ID, "vout", 1500);
+    value = qmp_adm1272_get(TEST_ID, "vout");
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_VOUT);
+    i2c_voltage = adm1272_direct_to_millivolts(i2c_value);
+    g_assert_cmpuint(value, ==, i2c_voltage);
+    g_assert_cmpuint(i2c_voltage, ==, lossy_value);
+
+    lossy_value =
+        adm1272_direct_to_milliamps(adm1272_milliamps_to_direct(1600));
+    qmp_adm1272_set(TEST_ID, "iout", 1600);
+    value = qmp_adm1272_get(TEST_ID, "iout");
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_IOUT);
+    i2c_value = adm1272_direct_to_milliamps(i2c_value);
+    g_assert_cmphex(value, ==, i2c_value);
+    g_assert_cmphex(i2c_value, ==, lossy_value);
+
+    lossy_value =
+        adm1272_direct_to_watts(adm1272_watts_to_direct(320));
+    qmp_adm1272_set(TEST_ID, "pin", 320);
+    value = qmp_adm1272_get(TEST_ID, "pin");
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_PIN);
+    i2c_pwr = adm1272_direct_to_watts(i2c_value);
+    g_assert_cmphex(value, ==, i2c_pwr);
+    g_assert_cmphex(i2c_pwr, ==, lossy_value);
+}
+
+/* test r/w registers */
+static void test_rw_regs(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t i2c_value;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+
+    adm1272_i2c_set16(i2cdev, PMBUS_VOUT_OV_WARN_LIMIT, 0xABCD);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_VOUT_OV_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0xABCD);
+
+    adm1272_i2c_set16(i2cdev, PMBUS_VOUT_UV_WARN_LIMIT, 0xCDEF);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_VOUT_UV_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0xCDEF);
+
+    adm1272_i2c_set16(i2cdev, PMBUS_IOUT_OC_WARN_LIMIT, 0x1234);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_IOUT_OC_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0x1234);
+
+    adm1272_i2c_set16(i2cdev, PMBUS_OT_FAULT_LIMIT, 0x5678);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_OT_FAULT_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0x5678);
+
+    adm1272_i2c_set16(i2cdev, PMBUS_OT_WARN_LIMIT, 0xABDC);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_OT_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0xABDC);
+
+    adm1272_i2c_set16(i2cdev, PMBUS_VIN_OV_WARN_LIMIT, 0xCDEF);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_VIN_OV_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0xCDEF);
+
+    adm1272_i2c_set16(i2cdev, PMBUS_VIN_UV_WARN_LIMIT, 0x2345);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_VIN_UV_WARN_LIMIT);
+    g_assert_cmphex(i2c_value, ==, 0x2345);
+
+    i2c_set8(i2cdev, ADM1272_RESTART_TIME, 0xF8);
+    i2c_value = i2c_get8(i2cdev, ADM1272_RESTART_TIME);
+    g_assert_cmphex(i2c_value, ==, 0xF8);
+
+    i2c_set8(i2cdev, ADM1272_MFR_PMON_CONTROL, 0);
+    i2c_value = i2c_get8(i2cdev, ADM1272_MFR_PMON_CONTROL);
+    g_assert_cmpuint(i2c_value, ==, 0);
+
+    adm1272_i2c_set16(i2cdev, ADM1272_MFR_PMON_CONFIG, 0xDEF0);
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_MFR_PMON_CONFIG);
+    g_assert_cmphex(i2c_value, ==, 0xDEF0);
+
+    adm1272_i2c_set16(i2cdev, ADM1272_MFR_ALERT1_CONFIG, 0x0123);
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_MFR_ALERT1_CONFIG);
+    g_assert_cmphex(i2c_value, ==, 0x0123);
+
+    adm1272_i2c_set16(i2cdev, ADM1272_MFR_ALERT2_CONFIG, 0x9876);
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_MFR_ALERT2_CONFIG);
+    g_assert_cmphex(i2c_value, ==, 0x9876);
+
+    adm1272_i2c_set16(i2cdev, ADM1272_MFR_DEVICE_CONFIG, 0x3456);
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_MFR_DEVICE_CONFIG);
+    g_assert_cmphex(i2c_value, ==, 0x3456);
+
+    adm1272_i2c_set16(i2cdev, ADM1272_HYSTERESIS_LOW, 0xCABA);
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_HYSTERESIS_LOW);
+    g_assert_cmphex(i2c_value, ==, 0xCABA);
+
+    adm1272_i2c_set16(i2cdev, ADM1272_HYSTERESIS_HIGH, 0x6789);
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_HYSTERESIS_HIGH);
+    g_assert_cmphex(i2c_value, ==, 0x6789);
+
+    adm1272_i2c_set16(i2cdev, ADM1272_STRT_UP_IOUT_LIM, 0x9876);
+    i2c_value = adm1272_i2c_get16(i2cdev, ADM1272_STRT_UP_IOUT_LIM);
+    g_assert_cmphex(i2c_value, ==, 0x9876);
+
+    adm1272_i2c_set16(i2cdev, PMBUS_OPERATION, 0xA);
+    i2c_value = i2c_get8(i2cdev, PMBUS_OPERATION);
+    g_assert_cmphex(i2c_value, ==, 0xA);
+}
+
+/* test read-only registers */
+static void test_ro_regs(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t i2c_init_value, i2c_value;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+
+    i2c_init_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_VIN);
+    adm1272_i2c_set16(i2cdev, PMBUS_READ_VIN, 0xBEEF);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_VIN);
+    g_assert_cmphex(i2c_init_value, ==, i2c_value);
+
+    i2c_init_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_VOUT);
+    adm1272_i2c_set16(i2cdev, PMBUS_READ_VOUT, 0x1234);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_VOUT);
+    g_assert_cmphex(i2c_init_value, ==, i2c_value);
+
+    i2c_init_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_IOUT);
+    adm1272_i2c_set16(i2cdev, PMBUS_READ_IOUT, 0x6547);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_IOUT);
+    g_assert_cmphex(i2c_init_value, ==, i2c_value);
+
+    i2c_init_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_TEMPERATURE_1);
+    adm1272_i2c_set16(i2cdev, PMBUS_READ_TEMPERATURE_1, 0x1597);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_TEMPERATURE_1);
+    g_assert_cmphex(i2c_init_value, ==, i2c_value);
+
+    i2c_init_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_PIN);
+    adm1272_i2c_set16(i2cdev, PMBUS_READ_PIN, 0xDEAD);
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_READ_PIN);
+    g_assert_cmphex(i2c_init_value, ==, i2c_value);
+}
+
+/* test voltage fault handling */
+static void test_voltage_faults(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t i2c_value;
+    uint8_t i2c_byte;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+
+    adm1272_i2c_set16(i2cdev, PMBUS_VOUT_OV_WARN_LIMIT,
+                      adm1272_millivolts_to_direct(5000));
+    qmp_adm1272_set(TEST_ID, "vout", 5100);
+
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_STATUS_WORD);
+    i2c_byte = i2c_get8(i2cdev, PMBUS_STATUS_VOUT);
+    g_assert_true((i2c_value & PB_STATUS_VOUT) != 0);
+    g_assert_true((i2c_byte & PB_STATUS_VOUT_OV_WARN) != 0);
+
+    qmp_adm1272_set(TEST_ID, "vout", 4500);
+    i2c_set8(i2cdev, PMBUS_CLEAR_FAULTS, 0);
+    i2c_byte = i2c_get8(i2cdev, PMBUS_STATUS_VOUT);
+    g_assert_true((i2c_byte & PB_STATUS_VOUT_OV_WARN) == 0);
+
+    adm1272_i2c_set16(i2cdev, PMBUS_VOUT_UV_WARN_LIMIT,
+                      adm1272_millivolts_to_direct(4600));
+    i2c_value = adm1272_i2c_get16(i2cdev, PMBUS_STATUS_WORD);
+    i2c_byte = i2c_get8(i2cdev, PMBUS_STATUS_VOUT);
+    g_assert_true((i2c_value & PB_STATUS_VOUT) != 0);
+    g_assert_true((i2c_byte & PB_STATUS_VOUT_UV_WARN) != 0);
+
+}
+
+static void adm1272_register_nodes(void)
+{
+    QOSGraphEdgeOptions opts = {
+        .extra_device_opts = "id=" TEST_ID ",address=0x10"
+    };
+    add_qi2c_address(&opts, &(QI2CAddress) { TEST_ADDR });
+
+    qos_node_create_driver("adm1272", i2c_device_create);
+    qos_node_consumes("adm1272", "i2c-bus", &opts);
+
+    qos_add_test("test_defaults", "adm1272", test_defaults, NULL);
+    qos_add_test("test_tx_rx", "adm1272", test_tx_rx, NULL);
+    qos_add_test("test_rw_regs", "adm1272", test_rw_regs, NULL);
+    qos_add_test("test_ro_regs", "adm1272", test_ro_regs, NULL);
+    qos_add_test("test_ov_faults", "adm1272", test_voltage_faults, NULL);
+}
+libqos_init(adm1272_register_nodes);
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 49de74ff59..99040885fb 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -201,6 +201,7 @@ qtests_s390x = \
 qos_test_ss = ss.source_set()
 qos_test_ss.add(
   'ac97-test.c',
+  'adm1272-test.c',
   'ds1338-test.c',
   'e1000-test.c',
   'e1000e-test.c',
-- 
2.32.0.93.g670b81a890-goog



^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v5 4/5] hw/misc: add MAX34451 device
  2021-07-08 17:20 [PATCH v5 0/5] Add support for PMBus in QEMU Titus Rwantare
                   ` (2 preceding siblings ...)
  2021-07-08 17:20 ` [PATCH v5 3/5] tests/qtest: add tests for ADM1272 device model Titus Rwantare
@ 2021-07-08 17:20 ` Titus Rwantare
  2021-07-08 17:20 ` [PATCH v5 5/5] tests/qtest: add tests for MAX34451 device model Titus Rwantare
  4 siblings, 0 replies; 7+ messages in thread
From: Titus Rwantare @ 2021-07-08 17:20 UTC (permalink / raw)
  To: Corey Minyard; +Cc: qemu-arm, qemu-devel, Titus Rwantare, Joel Stanley, Hao Wu

The MAX34451 is a Maxim power-supply system manager that can monitor up to 16 voltage rails or currents. It also contains a temperature sensor and supports up to four external temperature sensors.

This commit adds support for interfacing with it, and setting limits on the supported sensors.

Reviewed-by: Joel Stanley <joel@jms.id.au>
Reviewed-by: Hao Wu <wuhaotsh@google.com>
Signed-off-by: Titus Rwantare <titusr@google.com>
---
 hw/misc/max34451.c  | 775 ++++++++++++++++++++++++++++++++++++++++++++
 hw/arm/Kconfig      |   1 +
 hw/misc/Kconfig     |   4 +
 hw/misc/meson.build |   1 +
 4 files changed, 781 insertions(+)
 create mode 100644 hw/misc/max34451.c

diff --git a/hw/misc/max34451.c b/hw/misc/max34451.c
new file mode 100644
index 0000000000..1e7c68271e
--- /dev/null
+++ b/hw/misc/max34451.c
@@ -0,0 +1,775 @@
+/*
+ * Maxim MAX34451 PMBus 16-Channel V/I monitor and 12-Channel Sequencer/Marginer
+ *
+ * Copyright 2021 Google LLC
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/i2c/pmbus_device.h"
+#include "hw/irq.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+
+#define TYPE_MAX34451 "max34451"
+#define MAX34451(obj) OBJECT_CHECK(MAX34451State, (obj), TYPE_MAX34451)
+
+#define MAX34451_MFR_MODE               0xD1
+#define MAX34451_MFR_PSEN_CONFIG        0xD2
+#define MAX34451_MFR_VOUT_PEAK          0xD4
+#define MAX34451_MFR_IOUT_PEAK          0xD5
+#define MAX34451_MFR_TEMPERATURE_PEAK   0xD6
+#define MAX34451_MFR_VOUT_MIN           0xD7
+#define MAX34451_MFR_NV_LOG_CONFIG      0xD8
+#define MAX34451_MFR_FAULT_RESPONSE     0xD9
+#define MAX34451_MFR_FAULT_RETRY        0xDA
+#define MAX34451_MFR_NV_FAULT_LOG       0xDC
+#define MAX34451_MFR_TIME_COUNT         0xDD
+#define MAX34451_MFR_MARGIN_CONFIG      0xDF
+#define MAX34451_MFR_FW_SERIAL          0xE0
+#define MAX34451_MFR_IOUT_AVG           0xE2
+#define MAX34451_MFR_CHANNEL_CONFIG     0xE4
+#define MAX34451_MFR_TON_SEQ_MAX        0xE6
+#define MAX34451_MFR_PWM_CONFIG         0xE7
+#define MAX34451_MFR_SEQ_CONFIG         0xE8
+#define MAX34451_MFR_STORE_ALL          0xEE
+#define MAX34451_MFR_RESTORE_ALL        0xEF
+#define MAX34451_MFR_TEMP_SENSOR_CONFIG 0xF0
+#define MAX34451_MFR_STORE_SINGLE       0xFC
+#define MAX34451_MFR_CRC                0xFE
+
+#define MAX34451_NUM_MARGINED_PSU       12
+#define MAX34451_NUM_PWR_DEVICES        16
+#define MAX34451_NUM_TEMP_DEVICES       5
+#define MAX34451_NUM_PAGES              21
+
+#define DEFAULT_OP_ON                   0x80
+#define DEFAULT_CAPABILITY              0x20
+#define DEFAULT_ON_OFF_CONFIG           0x1a
+#define DEFAULT_VOUT_MODE               0x40
+#define DEFAULT_TEMPERATURE             2500
+#define DEFAULT_SCALE                   0x7FFF
+#define DEFAULT_OV_LIMIT                0x7FFF
+#define DEFAULT_OC_LIMIT                0x7FFF
+#define DEFAULT_OT_LIMIT                0x7FFF
+#define DEFAULT_VMIN                    0x7FFF
+#define DEFAULT_TON_FAULT_LIMIT         0xFFFF
+#define DEFAULT_CHANNEL_CONFIG          0x20
+#define DEFAULT_TEXT                    0x3130313031303130
+
+/**
+ * MAX34451State:
+ * @code: The command code received
+ * @page: Each page corresponds to a device monitored by the Max 34451
+ * The page register determines the available commands depending on device
+  ___________________________________________________________________________
+ |   0   |  Power supply monitored by RS0, controlled by PSEN0, and          |
+ |       |  margined with PWM0.                                              |
+ |_______|___________________________________________________________________|
+ |   1   |  Power supply monitored by RS1, controlled by PSEN1, and          |
+ |       |  margined with PWM1.                                              |
+ |_______|___________________________________________________________________|
+ |   2   |  Power supply monitored by RS2, controlled by PSEN2, and          |
+ |       |  margined with PWM2.                                              |
+ |_______|___________________________________________________________________|
+ |   3   |  Power supply monitored by RS3, controlled by PSEN3, and          |
+ |       |  margined with PWM3.                                              |
+ |_______|___________________________________________________________________|
+ |   4   |  Power supply monitored by RS4, controlled by PSEN4, and          |
+ |       |  margined with PWM4.                                              |
+ |_______|___________________________________________________________________|
+ |   5   |  Power supply monitored by RS5, controlled by PSEN5, and          |
+ |       |  margined with PWM5.                                              |
+ |_______|___________________________________________________________________|
+ |   6   |  Power supply monitored by RS6, controlled by PSEN6, and          |
+ |       |  margined with PWM6.                                              |
+ |_______|___________________________________________________________________|
+ |   7   |  Power supply monitored by RS7, controlled by PSEN7, and          |
+ |       |  margined with PWM7.                                              |
+ |_______|___________________________________________________________________|
+ |   8   |  Power supply monitored by RS8, controlled by PSEN8, and          |
+ |       | optionally margined by OUT0 of external DS4424 at I2C address A0h.|
+ |_______|___________________________________________________________________|
+ |   9   |  Power supply monitored by RS9, controlled by PSEN9, and          |
+ |       | optionally margined by OUT1 of external DS4424 at I2C address A0h.|
+ |_______|___________________________________________________________________|
+ |   10  |  Power supply monitored by RS10, controlled by PSEN10, and        |
+ |       | optionally margined by OUT2 of external DS4424 at I2C address A0h.|
+ |_______|___________________________________________________________________|
+ |   11  |  Power supply monitored by RS11, controlled by PSEN11, and        |
+ |       | optionally margined by OUT3 of external DS4424 at I2C address A0h.|
+ |_______|___________________________________________________________________|
+ |   12  |  ADC channel 12 (monitors voltage or current) or GPI.             |
+ |_______|___________________________________________________________________|
+ |   13  |  ADC channel 13 (monitors voltage or current) or GPI.             |
+ |_______|___________________________________________________________________|
+ |   14  |  ADC channel 14 (monitors voltage or current) or GPI.             |
+ |_______|___________________________________________________________________|
+ |   15  |  ADC channel 15 (monitors voltage or current) or GPI.             |
+ |_______|___________________________________________________________________|
+ |   16  |  Internal temperature sensor.                                     |
+ |_______|___________________________________________________________________|
+ |   17  |  External DS75LV temperature sensor with I2C address 90h.         |
+ |_______|___________________________________________________________________|
+ |   18  |  External DS75LV temperature sensor with I2C address 92h.         |
+ |_______|___________________________________________________________________|
+ |   19  |  External DS75LV temperature sensor with I2C address 94h.         |
+ |_______|___________________________________________________________________|
+ |   20  |  External DS75LV temperature sensor with I2C address 96h.         |
+ |_______|___________________________________________________________________|
+ | 21–254|  Reserved.                                                        |
+ |_______|___________________________________________________________________|
+ |   255 |  Applies to all pages.                                            |
+ |_______|___________________________________________________________________|
+ *
+ * @operation: Turn on and off power supplies
+ * @on_off_config: Configure the power supply on and off transition behaviour
+ * @write_protect: protect against changes to the device's memory
+ * @vout_margin_high: the voltage when OPERATION is set to margin high
+ * @vout_margin_low: the voltage when OPERATION is set to margin low
+ * @vout_scale: scale ADC reading to actual device reading if different
+ * @iout_cal_gain: set ratio of the voltage at the ADC input to sensed current
+ */
+typedef struct MAX34451State {
+    PMBusDevice parent;
+
+    uint16_t power_good_on[MAX34451_NUM_PWR_DEVICES];
+    uint16_t power_good_off[MAX34451_NUM_PWR_DEVICES];
+    uint16_t ton_delay[MAX34451_NUM_MARGINED_PSU];
+    uint16_t ton_max_fault_limit[MAX34451_NUM_MARGINED_PSU];
+    uint16_t toff_delay[MAX34451_NUM_MARGINED_PSU];
+    uint8_t status_mfr_specific[MAX34451_NUM_PWR_DEVICES];
+    /* Manufacturer specific function */
+    uint64_t mfr_location;
+    uint64_t mfr_date;
+    uint64_t mfr_serial;
+    uint16_t mfr_mode;
+    uint32_t psen_config[MAX34451_NUM_MARGINED_PSU];
+    uint16_t vout_peak[MAX34451_NUM_PWR_DEVICES];
+    uint16_t iout_peak[MAX34451_NUM_PWR_DEVICES];
+    uint16_t temperature_peak[MAX34451_NUM_TEMP_DEVICES];
+    uint16_t vout_min[MAX34451_NUM_PWR_DEVICES];
+    uint16_t nv_log_config;
+    uint32_t fault_response[MAX34451_NUM_PWR_DEVICES];
+    uint16_t fault_retry;
+    uint32_t fault_log;
+    uint32_t time_count;
+    uint16_t margin_config[MAX34451_NUM_MARGINED_PSU];
+    uint16_t fw_serial;
+    uint16_t iout_avg[MAX34451_NUM_PWR_DEVICES];
+    uint16_t channel_config[MAX34451_NUM_PWR_DEVICES];
+    uint16_t ton_seq_max[MAX34451_NUM_MARGINED_PSU];
+    uint32_t pwm_config[MAX34451_NUM_MARGINED_PSU];
+    uint32_t seq_config[MAX34451_NUM_MARGINED_PSU];
+    uint16_t temp_sensor_config[MAX34451_NUM_TEMP_DEVICES];
+    uint16_t store_single;
+    uint16_t crc;
+} MAX34451State;
+
+
+static void max34451_check_limits(MAX34451State *s)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(s);
+
+    pmbus_check_limits(pmdev);
+
+    for (int i = 0; i < MAX34451_NUM_PWR_DEVICES; i++) {
+        if (pmdev->pages[i].read_vout == 0) { /* PSU disabled */
+            continue;
+        }
+
+        if (pmdev->pages[i].read_vout > s->vout_peak[i]) {
+            s->vout_peak[i] = pmdev->pages[i].read_vout;
+        }
+
+        if (pmdev->pages[i].read_vout < s->vout_min[i]) {
+            s->vout_min[i] = pmdev->pages[i].read_vout;
+        }
+
+        if (pmdev->pages[i].read_iout > s->iout_peak[i]) {
+            s->iout_peak[i] = pmdev->pages[i].read_iout;
+        }
+    }
+
+    for (int i = 0; i < MAX34451_NUM_TEMP_DEVICES; i++) {
+        if (pmdev->pages[i + 16].read_temperature_1 > s->temperature_peak[i]) {
+            s->temperature_peak[i] = pmdev->pages[i + 16].read_temperature_1;
+        }
+    }
+}
+
+static uint8_t max34451_read_byte(PMBusDevice *pmdev)
+{
+    MAX34451State *s = MAX34451(pmdev);
+    switch (pmdev->code) {
+
+    case PMBUS_POWER_GOOD_ON:
+        if (pmdev->page < 16) {
+            pmbus_send16(pmdev, s->power_good_on[pmdev->page]);
+        }
+        break;
+
+    case PMBUS_POWER_GOOD_OFF:
+        if (pmdev->page < 16) {
+            pmbus_send16(pmdev, s->power_good_off[pmdev->page]);
+        }
+        break;
+
+    case PMBUS_TON_DELAY:
+        if (pmdev->page < 12) {
+            pmbus_send16(pmdev, s->ton_delay[pmdev->page]);
+        }
+        break;
+
+    case PMBUS_TON_MAX_FAULT_LIMIT:
+        if (pmdev->page < 12) {
+            pmbus_send16(pmdev, s->ton_max_fault_limit[pmdev->page]);
+        }
+        break;
+
+    case PMBUS_TOFF_DELAY:
+        if (pmdev->page < 12) {
+            pmbus_send16(pmdev, s->toff_delay[pmdev->page]);
+        }
+        break;
+
+    case PMBUS_STATUS_MFR_SPECIFIC:
+        if (pmdev->page < 16) {
+            pmbus_send8(pmdev, s->status_mfr_specific[pmdev->page]);
+        }
+        break;
+
+    case PMBUS_MFR_ID:
+        pmbus_send8(pmdev, 0x4d); /* Maxim */
+        break;
+
+    case PMBUS_MFR_MODEL:
+        pmbus_send8(pmdev, 0x59);
+        break;
+
+    case PMBUS_MFR_LOCATION:
+        pmbus_send64(pmdev, s->mfr_location);
+        break;
+
+    case PMBUS_MFR_DATE:
+        pmbus_send64(pmdev, s->mfr_date);
+        break;
+
+    case PMBUS_MFR_SERIAL:
+        pmbus_send64(pmdev, s->mfr_serial);
+        break;
+
+    case MAX34451_MFR_MODE:
+        pmbus_send16(pmdev, s->mfr_mode);
+        break;
+
+    case MAX34451_MFR_PSEN_CONFIG:
+        if (pmdev->page < 12) {
+            pmbus_send32(pmdev, s->psen_config[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_VOUT_PEAK:
+        if (pmdev->page < 16) {
+            pmbus_send16(pmdev, s->vout_peak[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_IOUT_PEAK:
+        if (pmdev->page < 16) {
+            pmbus_send16(pmdev, s->iout_peak[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_TEMPERATURE_PEAK:
+        if (15 < pmdev->page && pmdev->page < 21) {
+            pmbus_send16(pmdev, s->temperature_peak[pmdev->page % 16]);
+        } else {
+            pmbus_send16(pmdev, s->temperature_peak[0]);
+        }
+        break;
+
+    case MAX34451_MFR_VOUT_MIN:
+        if (pmdev->page < 16) {
+            pmbus_send16(pmdev, s->vout_min[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_NV_LOG_CONFIG:
+        pmbus_send16(pmdev, s->nv_log_config);
+        break;
+
+    case MAX34451_MFR_FAULT_RESPONSE:
+        if (pmdev->page < 16) {
+            pmbus_send32(pmdev, s->fault_response[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_FAULT_RETRY:
+        pmbus_send32(pmdev, s->fault_retry);
+        break;
+
+    case MAX34451_MFR_NV_FAULT_LOG:
+        pmbus_send32(pmdev, s->fault_log);
+        break;
+
+    case MAX34451_MFR_TIME_COUNT:
+        pmbus_send32(pmdev, s->time_count);
+        break;
+
+    case MAX34451_MFR_MARGIN_CONFIG:
+        if (pmdev->page < 12) {
+            pmbus_send16(pmdev, s->margin_config[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_FW_SERIAL:
+        if (pmdev->page == 255) {
+            pmbus_send16(pmdev, 1); /* Firmware revision */
+        }
+        break;
+
+    case MAX34451_MFR_IOUT_AVG:
+        if (pmdev->page < 16) {
+            pmbus_send16(pmdev, s->iout_avg[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_CHANNEL_CONFIG:
+        if (pmdev->page < 16) {
+            pmbus_send16(pmdev, s->channel_config[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_TON_SEQ_MAX:
+        if (pmdev->page < 12) {
+            pmbus_send16(pmdev, s->ton_seq_max[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_PWM_CONFIG:
+        if (pmdev->page < 12) {
+            pmbus_send32(pmdev, s->pwm_config[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_SEQ_CONFIG:
+        if (pmdev->page < 12) {
+            pmbus_send32(pmdev, s->seq_config[pmdev->page]);
+        }
+        break;
+
+    case MAX34451_MFR_TEMP_SENSOR_CONFIG:
+        if (15 < pmdev->page && pmdev->page < 21) {
+            pmbus_send32(pmdev, s->temp_sensor_config[pmdev->page % 16]);
+        }
+        break;
+
+    case MAX34451_MFR_STORE_SINGLE:
+        pmbus_send32(pmdev, s->store_single);
+        break;
+
+    case MAX34451_MFR_CRC:
+        pmbus_send32(pmdev, s->crc);
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: reading from unsupported register: 0x%02x\n",
+                      __func__, pmdev->code);
+        break;
+    }
+    return 0xFF;
+}
+
+static int max34451_write_data(PMBusDevice *pmdev, const uint8_t *buf,
+                               uint8_t len)
+{
+    MAX34451State *s = MAX34451(pmdev);
+
+    if (len == 0) {
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: writing empty data\n", __func__);
+        return -1;
+    }
+
+    pmdev->code = buf[0]; /* PMBus command code */
+
+    if (len == 1) {
+        return 0;
+    }
+
+    /* Exclude command code from buffer */
+    buf++;
+    len--;
+    uint8_t index = pmdev->page;
+
+    switch (pmdev->code) {
+    case MAX34451_MFR_STORE_ALL:
+    case MAX34451_MFR_RESTORE_ALL:
+    case MAX34451_MFR_STORE_SINGLE:
+        /*
+         * TODO: hardware behaviour is to move the contents of volatile
+         * memory to non-volatile memory.
+         */
+        break;
+
+    case PMBUS_POWER_GOOD_ON: /* R/W word */
+        if (pmdev->page < MAX34451_NUM_PWR_DEVICES) {
+            s->power_good_on[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case PMBUS_POWER_GOOD_OFF: /* R/W word */
+        if (pmdev->page < MAX34451_NUM_PWR_DEVICES) {
+            s->power_good_off[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case PMBUS_TON_DELAY: /* R/W word */
+        if (pmdev->page < 12) {
+            s->ton_delay[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case PMBUS_TON_MAX_FAULT_LIMIT: /* R/W word */
+        if (pmdev->page < 12) {
+            s->ton_max_fault_limit[pmdev->page]
+                = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case PMBUS_TOFF_DELAY: /* R/W word */
+        if (pmdev->page < 12) {
+            s->toff_delay[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case PMBUS_MFR_LOCATION: /* R/W 64 */
+        s->mfr_location = pmbus_receive64(pmdev);
+        break;
+
+    case PMBUS_MFR_DATE: /* R/W 64 */
+        s->mfr_date = pmbus_receive64(pmdev);
+        break;
+
+    case PMBUS_MFR_SERIAL: /* R/W 64 */
+        s->mfr_serial = pmbus_receive64(pmdev);
+        break;
+
+    case MAX34451_MFR_MODE: /* R/W word */
+         s->mfr_mode = pmbus_receive16(pmdev);
+        break;
+
+    case MAX34451_MFR_PSEN_CONFIG: /* R/W 32 */
+        if (pmdev->page < 12) {
+            s->psen_config[pmdev->page] = pmbus_receive32(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_VOUT_PEAK: /* R/W word */
+        if (pmdev->page < 16) {
+            s->vout_peak[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_IOUT_PEAK: /* R/W word */
+        if (pmdev->page < 16) {
+            s->iout_peak[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_TEMPERATURE_PEAK: /* R/W word */
+        if (15 < pmdev->page && pmdev->page < 21) {
+            s->temperature_peak[pmdev->page % 16]
+                = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_VOUT_MIN: /* R/W word */
+        if (pmdev->page < 16) {
+            s->vout_min[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_NV_LOG_CONFIG: /* R/W word */
+         s->nv_log_config = pmbus_receive16(pmdev);
+        break;
+
+    case MAX34451_MFR_FAULT_RESPONSE: /* R/W 32 */
+        if (pmdev->page < 16) {
+            s->fault_response[pmdev->page] = pmbus_receive32(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_FAULT_RETRY: /* R/W word */
+        s->fault_retry = pmbus_receive16(pmdev);
+        break;
+
+    case MAX34451_MFR_TIME_COUNT: /* R/W 32 */
+        s->time_count = pmbus_receive32(pmdev);
+        break;
+
+    case MAX34451_MFR_MARGIN_CONFIG: /* R/W word */
+        if (pmdev->page < 12) {
+            s->margin_config[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_CHANNEL_CONFIG: /* R/W word */
+        if (pmdev->page < 16) {
+            s->channel_config[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_TON_SEQ_MAX: /* R/W word */
+        if (pmdev->page < 12) {
+            s->ton_seq_max[pmdev->page] = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_PWM_CONFIG: /* R/W 32 */
+        if (pmdev->page < 12) {
+            s->pwm_config[pmdev->page] = pmbus_receive32(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_SEQ_CONFIG:  /* R/W 32 */
+        if (pmdev->page < 12) {
+            s->seq_config[pmdev->page] = pmbus_receive32(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_TEMP_SENSOR_CONFIG:  /* R/W word */
+        if (15 < pmdev->page && pmdev->page < 21) {
+            s->temp_sensor_config[pmdev->page % 16]
+                = pmbus_receive16(pmdev);
+        }
+        break;
+
+    case MAX34451_MFR_CRC: /* R/W word */
+        s->crc = pmbus_receive16(pmdev);
+        break;
+
+    case MAX34451_MFR_NV_FAULT_LOG:
+    case MAX34451_MFR_FW_SERIAL:
+    case MAX34451_MFR_IOUT_AVG:
+        /* Read only commands */
+        pmdev->pages[index].status_word |= PMBUS_STATUS_CML;
+        pmdev->pages[index].status_cml |= PB_CML_FAULT_INVALID_DATA;
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: writing to read-only register 0x%02x\n",
+                      __func__, pmdev->code);
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: writing to unsupported register: 0x%02x\n",
+                      __func__, pmdev->code);
+        break;
+    }
+
+    return 0;
+}
+
+static void max34451_get(Object *obj, Visitor *v, const char *name,
+                                     void *opaque, Error **errp)
+{
+    visit_type_uint16(v, name, (uint16_t *)opaque, errp);
+}
+
+static void max34451_set(Object *obj, Visitor *v, const char *name,
+                                 void *opaque, Error **errp)
+{
+    MAX34451State *s = MAX34451(obj);
+    uint16_t *internal = opaque;
+    uint16_t value;
+    if (!visit_type_uint16(v, name, &value, errp)) {
+        return;
+    }
+
+    *internal = value;
+    max34451_check_limits(s);
+}
+
+/* used to init uint16_t arrays */
+static inline void *memset_word(void *s, uint16_t c, size_t n)
+{
+    size_t i;
+    uint16_t *p = s;
+
+    for (i = 0; i < n; i++) {
+        p[i] = c;
+    }
+
+    return s;
+}
+
+static void max34451_exit_reset(Object *obj)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+    MAX34451State *s = MAX34451(obj);
+    pmdev->capability = DEFAULT_CAPABILITY;
+
+    for (int i = 0; i < MAX34451_NUM_PAGES; i++) {
+        pmdev->pages[i].operation = DEFAULT_OP_ON;
+        pmdev->pages[i].on_off_config = DEFAULT_ON_OFF_CONFIG;
+        pmdev->pages[i].revision = 0x11;
+        pmdev->pages[i].vout_mode = DEFAULT_VOUT_MODE;
+    }
+
+    for (int i = 0; i < MAX34451_NUM_PWR_DEVICES; i++) {
+        pmdev->pages[i].vout_scale_monitor = DEFAULT_SCALE;
+        pmdev->pages[i].vout_ov_fault_limit = DEFAULT_OV_LIMIT;
+        pmdev->pages[i].vout_ov_warn_limit = DEFAULT_OV_LIMIT;
+        pmdev->pages[i].iout_oc_warn_limit = DEFAULT_OC_LIMIT;
+        pmdev->pages[i].iout_oc_fault_limit = DEFAULT_OC_LIMIT;
+    }
+
+    for (int i = 0; i < MAX34451_NUM_MARGINED_PSU; i++) {
+        pmdev->pages[i].ton_max_fault_limit = DEFAULT_TON_FAULT_LIMIT;
+    }
+
+    for (int i = 16; i < MAX34451_NUM_TEMP_DEVICES + 16; i++) {
+        pmdev->pages[i].read_temperature_1 = DEFAULT_TEMPERATURE;
+        pmdev->pages[i].ot_warn_limit = DEFAULT_OT_LIMIT;
+        pmdev->pages[i].ot_fault_limit = DEFAULT_OT_LIMIT;
+    }
+
+    memset_word(s->ton_max_fault_limit, DEFAULT_TON_FAULT_LIMIT,
+                MAX34451_NUM_MARGINED_PSU);
+    memset_word(s->channel_config, DEFAULT_CHANNEL_CONFIG,
+                MAX34451_NUM_PWR_DEVICES);
+    memset_word(s->vout_min, DEFAULT_VMIN, MAX34451_NUM_PWR_DEVICES);
+
+    s->mfr_location = DEFAULT_TEXT;
+    s->mfr_date = DEFAULT_TEXT;
+    s->mfr_serial = DEFAULT_TEXT;
+}
+
+static const VMStateDescription vmstate_max34451 = {
+    .name = TYPE_MAX34451,
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (VMStateField[]){
+        VMSTATE_PMBUS_DEVICE(parent, MAX34451State),
+        VMSTATE_UINT16_ARRAY(power_good_on, MAX34451State,
+                             MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT16_ARRAY(power_good_off, MAX34451State,
+                             MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT16_ARRAY(ton_delay, MAX34451State,
+                             MAX34451_NUM_MARGINED_PSU),
+        VMSTATE_UINT16_ARRAY(ton_max_fault_limit, MAX34451State,
+                             MAX34451_NUM_MARGINED_PSU),
+        VMSTATE_UINT16_ARRAY(toff_delay, MAX34451State,
+                             MAX34451_NUM_MARGINED_PSU),
+        VMSTATE_UINT8_ARRAY(status_mfr_specific, MAX34451State,
+                             MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT64(mfr_location, MAX34451State),
+        VMSTATE_UINT64(mfr_date, MAX34451State),
+        VMSTATE_UINT64(mfr_serial, MAX34451State),
+        VMSTATE_UINT16(mfr_mode, MAX34451State),
+        VMSTATE_UINT32_ARRAY(psen_config, MAX34451State,
+                             MAX34451_NUM_MARGINED_PSU),
+        VMSTATE_UINT16_ARRAY(vout_peak, MAX34451State,
+                             MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT16_ARRAY(iout_peak, MAX34451State,
+                             MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT16_ARRAY(temperature_peak, MAX34451State,
+                             MAX34451_NUM_TEMP_DEVICES),
+        VMSTATE_UINT16_ARRAY(vout_min, MAX34451State, MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT16(nv_log_config, MAX34451State),
+        VMSTATE_UINT32_ARRAY(fault_response, MAX34451State,
+                             MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT16(fault_retry, MAX34451State),
+        VMSTATE_UINT32(fault_log, MAX34451State),
+        VMSTATE_UINT32(time_count, MAX34451State),
+        VMSTATE_UINT16_ARRAY(margin_config, MAX34451State,
+                             MAX34451_NUM_MARGINED_PSU),
+        VMSTATE_UINT16(fw_serial, MAX34451State),
+        VMSTATE_UINT16_ARRAY(iout_avg, MAX34451State, MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT16_ARRAY(channel_config, MAX34451State,
+                             MAX34451_NUM_PWR_DEVICES),
+        VMSTATE_UINT16_ARRAY(ton_seq_max, MAX34451State,
+                             MAX34451_NUM_MARGINED_PSU),
+        VMSTATE_UINT32_ARRAY(pwm_config, MAX34451State,
+                             MAX34451_NUM_MARGINED_PSU),
+        VMSTATE_UINT32_ARRAY(seq_config, MAX34451State,
+                             MAX34451_NUM_MARGINED_PSU),
+        VMSTATE_UINT16_ARRAY(temp_sensor_config, MAX34451State,
+                             MAX34451_NUM_TEMP_DEVICES),
+        VMSTATE_UINT16(store_single, MAX34451State),
+        VMSTATE_UINT16(crc, MAX34451State),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void max34451_init(Object *obj)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+    uint64_t psu_flags = PB_HAS_VOUT | PB_HAS_IOUT | PB_HAS_VOUT_MODE |
+                         PB_HAS_IOUT_GAIN;
+
+    for (int i = 0; i < MAX34451_NUM_PWR_DEVICES; i++) {
+        pmbus_page_config(pmdev, i, psu_flags);
+    }
+
+    for (int i = 0; i < MAX34451_NUM_MARGINED_PSU; i++) {
+        pmbus_page_config(pmdev, i, psu_flags | PB_HAS_VOUT_MARGIN);
+    }
+
+    for (int i = 16; i < MAX34451_NUM_TEMP_DEVICES + 16; i++) {
+        pmbus_page_config(pmdev, i, PB_HAS_TEMPERATURE | PB_HAS_VOUT_MODE);
+    }
+
+    /* get and set the voltage in millivolts, max is 32767 mV */
+    for (int i = 0; i < MAX34451_NUM_PWR_DEVICES; i++) {
+        object_property_add(obj, "vout[*]", "uint16",
+                            max34451_get,
+                            max34451_set, NULL, &pmdev->pages[i].read_vout);
+    }
+
+    /*
+     * get and set the temperature of the internal temperature sensor in
+     * centidegrees Celcius i.e.: 2500 -> 25.00 C, max is 327.67 C
+     */
+    for (int i = 0; i < MAX34451_NUM_TEMP_DEVICES; i++) {
+        object_property_add(obj, "temperature[*]", "uint16",
+                            max34451_get,
+                            max34451_set,
+                            NULL,
+                            &pmdev->pages[i + 16].read_temperature_1);
+    }
+
+}
+
+static void max34451_class_init(ObjectClass *klass, void *data)
+{
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    PMBusDeviceClass *k = PMBUS_DEVICE_CLASS(klass);
+    dc->desc = "Maxim MAX34451 16-Channel V/I monitor";
+    dc->vmsd = &vmstate_max34451;
+    k->write_data = max34451_write_data;
+    k->receive_byte = max34451_read_byte;
+    k->device_num_pages = MAX34451_NUM_PAGES;
+    rc->phases.exit = max34451_exit_reset;
+}
+
+static const TypeInfo max34451_info = {
+    .name = TYPE_MAX34451,
+    .parent = TYPE_PMBUS_DEVICE,
+    .instance_size = sizeof(MAX34451State),
+    .instance_init = max34451_init,
+    .class_init = max34451_class_init,
+};
+
+static void max34451_register_types(void)
+{
+    type_register_static(&max34451_info);
+}
+
+type_init(max34451_register_types)
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index e141084a79..d689941f64 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -374,6 +374,7 @@ config NPCM7XX
     select ADM1272
     select ARM_GIC
     select AT24C  # EEPROM
+    select MAX34451
     select PL310  # cache controller
     select PMBUS
     select SERIAL
diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index 0774c1f70c..1747667984 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -30,6 +30,10 @@ config EMC141X
     bool
     depends on I2C
 
+config MAX34451
+    bool
+    depends on I2C
+
 config ISA_DEBUG
     bool
     depends on ISA_BUS
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 8666e9225a..b62919e1fe 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -11,6 +11,7 @@ softmmu_ss.add(when: 'CONFIG_TMP105', if_true: files('tmp105.c'))
 softmmu_ss.add(when: 'CONFIG_TMP421', if_true: files('tmp421.c'))
 softmmu_ss.add(when: 'CONFIG_EMC141X', if_true: files('emc141x.c'))
 softmmu_ss.add(when: 'CONFIG_ADM1272', if_true: files('adm1272.c'))
+softmmu_ss.add(when: 'CONFIG_MAX34451', if_true: files('max34451.c'))
 softmmu_ss.add(when: 'CONFIG_UNIMP', if_true: files('unimp.c'))
 softmmu_ss.add(when: 'CONFIG_EMPTY_SLOT', if_true: files('empty_slot.c'))
 softmmu_ss.add(when: 'CONFIG_LED', if_true: files('led.c'))
-- 
2.32.0.93.g670b81a890-goog



^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v5 5/5] tests/qtest: add tests for MAX34451 device model
  2021-07-08 17:20 [PATCH v5 0/5] Add support for PMBus in QEMU Titus Rwantare
                   ` (3 preceding siblings ...)
  2021-07-08 17:20 ` [PATCH v5 4/5] hw/misc: add MAX34451 device Titus Rwantare
@ 2021-07-08 17:20 ` Titus Rwantare
  4 siblings, 0 replies; 7+ messages in thread
From: Titus Rwantare @ 2021-07-08 17:20 UTC (permalink / raw)
  To: Corey Minyard; +Cc: qemu-arm, qemu-devel, Titus Rwantare, Joel Stanley

Signed-off-by: Titus Rwantare <titusr@google.com>
Reviewed-by: Joel Stanley <joel@jms.id.au>
---
 tests/qtest/max34451-test.c | 336 ++++++++++++++++++++++++++++++++++++
 tests/qtest/meson.build     |   1 +
 2 files changed, 337 insertions(+)
 create mode 100644 tests/qtest/max34451-test.c

diff --git a/tests/qtest/max34451-test.c b/tests/qtest/max34451-test.c
new file mode 100644
index 0000000000..0c98d0764c
--- /dev/null
+++ b/tests/qtest/max34451-test.c
@@ -0,0 +1,336 @@
+/*
+ * QTests for the MAX34451 device
+ *
+ * Copyright 2021 Google LLC
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/i2c/pmbus_device.h"
+#include "libqtest-single.h"
+#include "libqos/qgraph.h"
+#include "libqos/i2c.h"
+#include "qapi/qmp/qdict.h"
+#include "qapi/qmp/qnum.h"
+#include "qemu/bitops.h"
+
+#define TEST_ID "max34451-test"
+#define TEST_ADDR (0x4e)
+
+#define MAX34451_MFR_VOUT_PEAK          0xD4
+#define MAX34451_MFR_IOUT_PEAK          0xD5
+#define MAX34451_MFR_TEMPERATURE_PEAK   0xD6
+#define MAX34451_MFR_VOUT_MIN           0xD7
+
+#define DEFAULT_VOUT                    0
+#define DEFAULT_UV_LIMIT                0
+#define DEFAULT_TEMPERATURE             2500
+#define DEFAULT_SCALE                   0x7FFF
+#define DEFAULT_OV_LIMIT                0x7FFF
+#define DEFAULT_OC_LIMIT                0x7FFF
+#define DEFAULT_OT_LIMIT                0x7FFF
+#define DEFAULT_VMIN                    0x7FFF
+#define DEFAULT_TON_FAULT_LIMIT         0xFFFF
+#define DEFAULT_CHANNEL_CONFIG          0x20
+#define DEFAULT_TEXT                    0x20
+
+#define MAX34451_NUM_PWR_DEVICES        16
+#define MAX34451_NUM_TEMP_DEVICES       5
+
+
+static uint16_t qmp_max34451_get(const char *id, const char *property)
+{
+    QDict *response;
+    uint16_t ret;
+    response = qmp("{ 'execute': 'qom-get', 'arguments': { 'path': %s, "
+                   "'property': %s } }", id, property);
+    g_assert(qdict_haskey(response, "return"));
+    ret = qnum_get_uint(qobject_to(QNum, qdict_get(response, "return")));
+    qobject_unref(response);
+    return ret;
+}
+
+static void qmp_max34451_set(const char *id,
+                             const char *property,
+                             uint16_t value)
+{
+    QDict *response;
+
+    response = qmp("{ 'execute': 'qom-set', 'arguments': { 'path': %s, "
+                   "'property': %s, 'value': %u } }",
+                   id, property, value);
+    g_assert(qdict_haskey(response, "return"));
+    qobject_unref(response);
+}
+
+/* PMBus commands are little endian vs i2c_set16 in i2c.h which is big endian */
+static uint16_t max34451_i2c_get16(QI2CDevice *i2cdev, uint8_t reg)
+{
+    uint8_t resp[2];
+    i2c_read_block(i2cdev, reg, resp, sizeof(resp));
+    return (resp[1] << 8) | resp[0];
+}
+
+/* PMBus commands are little endian vs i2c_set16 in i2c.h which is big endian */
+static void max34451_i2c_set16(QI2CDevice *i2cdev, uint8_t reg, uint16_t value)
+{
+    uint8_t data[2];
+
+    data[0] = value & 255;
+    data[1] = value >> 8;
+    i2c_write_block(i2cdev, reg, data, sizeof(data));
+}
+
+/* Test default values */
+static void test_defaults(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t value, i2c_value;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+    char *path;
+
+    /* Default temperatures and temperature fault limits */
+    for (int i = 0; i < MAX34451_NUM_TEMP_DEVICES; i++) {
+        path = g_strdup_printf("temperature[%d]", i);
+        value = qmp_max34451_get(TEST_ID, path);
+        g_assert_cmpuint(value, ==, DEFAULT_TEMPERATURE);
+        g_free(path);
+
+        /* Temperature sensors start on page 16 */
+        i2c_set8(i2cdev, PMBUS_PAGE, i + 16);
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_READ_TEMPERATURE_1);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_TEMPERATURE);
+
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_OT_FAULT_LIMIT);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_OT_LIMIT);
+
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_OT_WARN_LIMIT);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_OT_LIMIT);
+    }
+
+    /* Default voltages and fault limits */
+    for (int i = 0; i < MAX34451_NUM_PWR_DEVICES; i++) {
+        path = g_strdup_printf("vout[%d]", i);
+        value = qmp_max34451_get(TEST_ID, path);
+        g_assert_cmpuint(value, ==, DEFAULT_VOUT);
+        g_free(path);
+
+        i2c_set8(i2cdev, PMBUS_PAGE, i);
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_READ_VOUT);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_VOUT);
+
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_OV_FAULT_LIMIT);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_OV_LIMIT);
+
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_OV_WARN_LIMIT);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_OV_LIMIT);
+
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_UV_WARN_LIMIT);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_UV_LIMIT);
+
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_UV_FAULT_LIMIT);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_UV_LIMIT);
+
+        i2c_value = max34451_i2c_get16(i2cdev, MAX34451_MFR_VOUT_MIN);
+        g_assert_cmpuint(i2c_value, ==, DEFAULT_VMIN);
+    }
+
+    i2c_value = i2c_get8(i2cdev, PMBUS_VOUT_MODE);
+    g_assert_cmphex(i2c_value, ==, 0x40); /* DIRECT mode */
+
+    i2c_value = i2c_get8(i2cdev, PMBUS_REVISION);
+    g_assert_cmphex(i2c_value, ==, 0x11); /* Rev 1.1 */
+}
+
+/* Test setting temperature */
+static void test_temperature(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t value, i2c_value;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+    char *path;
+
+    for (int i = 0; i < MAX34451_NUM_TEMP_DEVICES; i++) {
+        path = g_strdup_printf("temperature[%d]", i);
+        qmp_max34451_set(TEST_ID, path, 0xBE00 + i);
+        value = qmp_max34451_get(TEST_ID, path);
+        g_assert_cmphex(value, ==, 0xBE00 + i);
+        g_free(path);
+    }
+
+    /* compare qmp read with i2c read separately */
+    for (int i = 0; i < MAX34451_NUM_TEMP_DEVICES; i++) {
+        /* temperature[0] is on page 16 */
+        i2c_set8(i2cdev, PMBUS_PAGE, i + 16);
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_READ_TEMPERATURE_1);
+        g_assert_cmphex(i2c_value, ==, 0xBE00 + i);
+
+        i2c_value = max34451_i2c_get16(i2cdev, MAX34451_MFR_TEMPERATURE_PEAK);
+        g_assert_cmphex(i2c_value, ==, 0xBE00 + i);
+    }
+}
+
+/* Test setting voltage */
+static void test_voltage(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t value, i2c_value;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+    char *path;
+
+    for (int i = 0; i < MAX34451_NUM_PWR_DEVICES; i++) {
+        path = g_strdup_printf("vout[%d]", i);
+        qmp_max34451_set(TEST_ID, path, 3000 + i);
+        value = qmp_max34451_get(TEST_ID, path);
+        g_assert_cmpuint(value, ==, 3000 + i);
+        g_free(path);
+    }
+
+    /* compare qmp read with i2c read separately */
+    for (int i = 0; i < MAX34451_NUM_PWR_DEVICES; i++) {
+        i2c_set8(i2cdev, PMBUS_PAGE, i);
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_READ_VOUT);
+        g_assert_cmpuint(i2c_value, ==, 3000 + i);
+
+        i2c_value = max34451_i2c_get16(i2cdev, MAX34451_MFR_VOUT_PEAK);
+        g_assert_cmpuint(i2c_value, ==, 3000 + i);
+
+        i2c_value = max34451_i2c_get16(i2cdev, MAX34451_MFR_VOUT_MIN);
+        g_assert_cmpuint(i2c_value, ==, 3000 + i);
+    }
+}
+
+/* Test setting some read/write registers */
+static void test_rw_regs(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t i2c_value;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+
+    i2c_set8(i2cdev, PMBUS_PAGE, 11);
+    i2c_value = i2c_get8(i2cdev, PMBUS_PAGE);
+    g_assert_cmpuint(i2c_value, ==, 11);
+
+    i2c_set8(i2cdev, PMBUS_OPERATION, 1);
+    i2c_value = i2c_get8(i2cdev, PMBUS_OPERATION);
+    g_assert_cmpuint(i2c_value, ==, 1);
+
+    max34451_i2c_set16(i2cdev, PMBUS_VOUT_MARGIN_HIGH, 5000);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_MARGIN_HIGH);
+    g_assert_cmpuint(i2c_value, ==, 5000);
+
+    max34451_i2c_set16(i2cdev, PMBUS_VOUT_MARGIN_LOW, 4000);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_MARGIN_LOW);
+    g_assert_cmpuint(i2c_value, ==, 4000);
+
+    max34451_i2c_set16(i2cdev, PMBUS_VOUT_OV_FAULT_LIMIT, 5500);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_OV_FAULT_LIMIT);
+    g_assert_cmpuint(i2c_value, ==, 5500);
+
+    max34451_i2c_set16(i2cdev, PMBUS_VOUT_OV_WARN_LIMIT, 5600);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_OV_WARN_LIMIT);
+    g_assert_cmpuint(i2c_value, ==, 5600);
+
+    max34451_i2c_set16(i2cdev, PMBUS_VOUT_UV_FAULT_LIMIT, 5700);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_UV_FAULT_LIMIT);
+    g_assert_cmpuint(i2c_value, ==, 5700);
+
+    max34451_i2c_set16(i2cdev, PMBUS_VOUT_UV_WARN_LIMIT, 5800);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_VOUT_UV_WARN_LIMIT);
+    g_assert_cmpuint(i2c_value, ==, 5800);
+
+    max34451_i2c_set16(i2cdev, PMBUS_POWER_GOOD_ON, 5900);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_POWER_GOOD_ON);
+    g_assert_cmpuint(i2c_value, ==, 5900);
+
+    max34451_i2c_set16(i2cdev, PMBUS_POWER_GOOD_OFF, 6100);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_POWER_GOOD_OFF);
+    g_assert_cmpuint(i2c_value, ==, 6100);
+}
+
+/* Test that Read only registers can't be written */
+static void test_ro_regs(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t i2c_value, i2c_init_value;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+
+    i2c_set8(i2cdev, PMBUS_PAGE, 1); /* move to page 1 */
+    i2c_init_value = i2c_get8(i2cdev, PMBUS_CAPABILITY);
+    i2c_set8(i2cdev, PMBUS_CAPABILITY, 0xF9);
+    i2c_value = i2c_get8(i2cdev, PMBUS_CAPABILITY);
+    g_assert_cmpuint(i2c_init_value, ==, i2c_value);
+
+    i2c_init_value = max34451_i2c_get16(i2cdev, PMBUS_READ_VOUT);
+    max34451_i2c_set16(i2cdev, PMBUS_READ_VOUT, 0xDEAD);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_READ_VOUT);
+    g_assert_cmpuint(i2c_init_value, ==, i2c_value);
+    g_assert_cmphex(i2c_value, !=, 0xDEAD);
+
+    i2c_set8(i2cdev, PMBUS_PAGE, 16); /* move to page 16 */
+    i2c_init_value = max34451_i2c_get16(i2cdev, PMBUS_READ_TEMPERATURE_1);
+    max34451_i2c_set16(i2cdev, PMBUS_READ_TEMPERATURE_1, 0xABBA);
+    i2c_value = max34451_i2c_get16(i2cdev, PMBUS_READ_TEMPERATURE_1);
+    g_assert_cmpuint(i2c_init_value, ==, i2c_value);
+    g_assert_cmphex(i2c_value, !=, 0xABBA);
+}
+
+/* test over voltage faults */
+static void test_ov_faults(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t i2c_value;
+    uint8_t i2c_byte;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+    char *path;
+    /* Test ov fault reporting */
+    for (int i = 0; i < MAX34451_NUM_PWR_DEVICES; i++) {
+        path = g_strdup_printf("vout[%d]", i);
+        i2c_set8(i2cdev, PMBUS_PAGE, i);
+        max34451_i2c_set16(i2cdev, PMBUS_VOUT_OV_FAULT_LIMIT, 5000);
+        qmp_max34451_set(TEST_ID, path, 5100);
+        g_free(path);
+
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_STATUS_WORD);
+        i2c_byte = i2c_get8(i2cdev, PMBUS_STATUS_VOUT);
+        g_assert_true((i2c_value & PB_STATUS_VOUT) != 0);
+        g_assert_true((i2c_byte & PB_STATUS_VOUT_OV_FAULT) != 0);
+    }
+}
+
+/* test over temperature faults */
+static void test_ot_faults(void *obj, void *data, QGuestAllocator *alloc)
+{
+    uint16_t i2c_value;
+    uint8_t i2c_byte;
+    QI2CDevice *i2cdev = (QI2CDevice *)obj;
+    char *path;
+
+    for (int i = 0; i < MAX34451_NUM_TEMP_DEVICES; i++) {
+        path = g_strdup_printf("temperature[%d]", i);
+        i2c_set8(i2cdev, PMBUS_PAGE, i + 16);
+        max34451_i2c_set16(i2cdev, PMBUS_OT_FAULT_LIMIT, 6000);
+        qmp_max34451_set(TEST_ID, path, 6100);
+        g_free(path);
+
+        i2c_value = max34451_i2c_get16(i2cdev, PMBUS_STATUS_WORD);
+        i2c_byte = i2c_get8(i2cdev, PMBUS_STATUS_TEMPERATURE);
+        g_assert_true((i2c_value & PB_STATUS_TEMPERATURE) != 0);
+        g_assert_true((i2c_byte & PB_STATUS_OT_FAULT) != 0);
+    }
+}
+
+static void max34451_register_nodes(void)
+{
+    QOSGraphEdgeOptions opts = {
+        .extra_device_opts = "id=" TEST_ID ",address=0x4e"
+    };
+    add_qi2c_address(&opts, &(QI2CAddress) { TEST_ADDR });
+
+    qos_node_create_driver("max34451", i2c_device_create);
+    qos_node_consumes("max34451", "i2c-bus", &opts);
+
+    qos_add_test("test_defaults", "max34451", test_defaults, NULL);
+    qos_add_test("test_temperature", "max34451", test_temperature, NULL);
+    qos_add_test("test_voltage", "max34451", test_voltage, NULL);
+    qos_add_test("test_rw_regs", "max34451", test_rw_regs, NULL);
+    qos_add_test("test_ro_regs", "max34451", test_ro_regs, NULL);
+    qos_add_test("test_ov_faults", "max34451", test_ov_faults, NULL);
+    qos_add_test("test_ot_faults", "max34451", test_ot_faults, NULL);
+}
+libqos_init(max34451_register_nodes);
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 99040885fb..48eb646889 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -208,6 +208,7 @@ qos_test_ss.add(
   'eepro100-test.c',
   'es1370-test.c',
   'ipoctal232-test.c',
+  'max34451-test.c',
   'megasas-test.c',
   'ne2000-test.c',
   'tulip-test.c',
-- 
2.32.0.93.g670b81a890-goog



^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v5 2/5] hw/misc: add ADM1272 device
  2021-07-08 17:25 [PATCH v5 0/5] Add support for PMBus in QEMU Titus Rwantare
@ 2021-07-08 17:25 ` Titus Rwantare
  0 siblings, 0 replies; 7+ messages in thread
From: Titus Rwantare @ 2021-07-08 17:25 UTC (permalink / raw)
  To: Corey Minyard; +Cc: qemu-arm, qemu-devel, Titus Rwantare, Joel Stanley, Hao Wu

The ADM1272 is a PMBus compliant Hot Swap Controller and Digital Power
Monitor by Analog Devices.

This commit adds support for interfacing with it, and support for
setting and monitoring sensor limits.

Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ADM1272.pdf

Reviewed-by: Joel Stanley <joel@jms.id.au>
Reviewed-by: Hao Wu <wuhaotsh@google.com>
Signed-off-by: Titus Rwantare <titusr@google.com>
---
 hw/misc/adm1272.c   | 544 ++++++++++++++++++++++++++++++++++++++++++++
 hw/arm/Kconfig      |   1 +
 hw/misc/Kconfig     |   4 +
 hw/misc/meson.build |   1 +
 4 files changed, 550 insertions(+)
 create mode 100644 hw/misc/adm1272.c

diff --git a/hw/misc/adm1272.c b/hw/misc/adm1272.c
new file mode 100644
index 0000000000..c1f9cf3afd
--- /dev/null
+++ b/hw/misc/adm1272.c
@@ -0,0 +1,544 @@
+/*
+ * Analog Devices ADM1272 High Voltage Positive Hot Swap Controller and Digital
+ * Power Monitor with PMBus
+ *
+ * Copyright 2021 Google LLC
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include <string.h>
+#include "hw/i2c/pmbus_device.h"
+#include "hw/irq.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "trace.h"
+
+#define TYPE_ADM1272 "adm1272"
+#define ADM1272(obj) OBJECT_CHECK(ADM1272State, (obj), TYPE_ADM1272)
+
+#define ADM1272_RESTART_TIME            0xCC
+#define ADM1272_MFR_PEAK_IOUT           0xD0
+#define ADM1272_MFR_PEAK_VIN            0xD1
+#define ADM1272_MFR_PEAK_VOUT           0xD2
+#define ADM1272_MFR_PMON_CONTROL        0xD3
+#define ADM1272_MFR_PMON_CONFIG         0xD4
+#define ADM1272_MFR_ALERT1_CONFIG       0xD5
+#define ADM1272_MFR_ALERT2_CONFIG       0xD6
+#define ADM1272_MFR_PEAK_TEMPERATURE    0xD7
+#define ADM1272_MFR_DEVICE_CONFIG       0xD8
+#define ADM1272_MFR_POWER_CYCLE         0xD9
+#define ADM1272_MFR_PEAK_PIN            0xDA
+#define ADM1272_MFR_READ_PIN_EXT        0xDB
+#define ADM1272_MFR_READ_EIN_EXT        0xDC
+
+#define ADM1272_HYSTERESIS_LOW          0xF2
+#define ADM1272_HYSTERESIS_HIGH         0xF3
+#define ADM1272_STATUS_HYSTERESIS       0xF4
+#define ADM1272_STATUS_GPIO             0xF5
+#define ADM1272_STRT_UP_IOUT_LIM        0xF6
+
+/* Defaults */
+#define ADM1272_OPERATION_DEFAULT       0x80
+#define ADM1272_CAPABILITY_DEFAULT      0xB0
+#define ADM1272_CAPABILITY_NO_PEC       0x30
+#define ADM1272_DIRECT_MODE             0x40
+#define ADM1272_HIGH_LIMIT_DEFAULT      0x0FFF
+#define ADM1272_PIN_OP_DEFAULT          0x7FFF
+#define ADM1272_PMBUS_REVISION_DEFAULT  0x22
+#define ADM1272_MFR_ID_DEFAULT          "ADI"
+#define ADM1272_MODEL_DEFAULT           "ADM1272-A1"
+#define ADM1272_MFR_DEFAULT_REVISION    "25"
+#define ADM1272_DEFAULT_DATE            "160301"
+#define ADM1272_RESTART_TIME_DEFAULT    0x64
+#define ADM1272_PMON_CONTROL_DEFAULT    0x1
+#define ADM1272_PMON_CONFIG_DEFAULT     0x3F35
+#define ADM1272_DEVICE_CONFIG_DEFAULT   0x8
+#define ADM1272_HYSTERESIS_HIGH_DEFAULT     0xFFFF
+#define ADM1272_STRT_UP_IOUT_LIM_DEFAULT    0x000F
+#define ADM1272_VOLT_DEFAULT            12000
+#define ADM1272_IOUT_DEFAULT            25000
+#define ADM1272_PWR_DEFAULT             300  /* 12V 25A */
+#define ADM1272_SHUNT                   300 /* micro-ohms */
+#define ADM1272_VOLTAGE_COEFF_DEFAULT   1
+#define ADM1272_CURRENT_COEFF_DEFAULT   3
+#define ADM1272_PWR_COEFF_DEFAULT       7
+#define ADM1272_IOUT_OFFSET             0x5000
+#define ADM1272_IOUT_OFFSET             0x5000
+
+
+typedef struct ADM1272State {
+    PMBusDevice parent;
+
+    uint64_t ein_ext;
+    uint32_t pin_ext;
+    uint8_t restart_time;
+
+    uint16_t peak_vin;
+    uint16_t peak_vout;
+    uint16_t peak_iout;
+    uint16_t peak_temperature;
+    uint16_t peak_pin;
+
+    uint8_t pmon_control;
+    uint16_t pmon_config;
+    uint16_t alert1_config;
+    uint16_t alert2_config;
+    uint16_t device_config;
+
+    uint16_t hysteresis_low;
+    uint16_t hysteresis_high;
+    uint8_t status_hysteresis;
+    uint8_t status_gpio;
+
+    uint16_t strt_up_iout_lim;
+
+} ADM1272State;
+
+static const PMBusCoefficients adm1272_coefficients[] = {
+    [0] = { 6770, 0, -2 },        /* voltage, vrange 60V */
+    [1] = { 4062, 0, -2 },        /* voltage, vrange 100V */
+    [2] = { 1326, 20480, -1 },    /* current, vsense range 15mV */
+    [3] = { 663, 20480, -1 },     /* current, vsense range 30mV */
+    [4] = { 3512, 0, -2 },        /* power, vrange 60V, irange 15mV */
+    [5] = { 21071, 0, -3 },       /* power, vrange 100V, irange 15mV */
+    [6] = { 17561, 0, -3 },       /* power, vrange 60V, irange 30mV */
+    [7] = { 10535, 0, -3 },       /* power, vrange 100V, irange 30mV */
+    [8] = { 42, 31871, -1 },      /* temperature */
+};
+
+static void adm1272_check_limits(ADM1272State *s)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(s);
+
+    pmbus_check_limits(pmdev);
+
+    if (pmdev->pages[0].read_vout > s->peak_vout) {
+        s->peak_vout = pmdev->pages[0].read_vout;
+    }
+
+    if (pmdev->pages[0].read_vin > s->peak_vin) {
+        s->peak_vin = pmdev->pages[0].read_vin;
+    }
+
+    if (pmdev->pages[0].read_iout > s->peak_iout) {
+        s->peak_iout = pmdev->pages[0].read_iout;
+    }
+
+    if (pmdev->pages[0].read_temperature_1 > s->peak_temperature) {
+        s->peak_temperature = pmdev->pages[0].read_temperature_1;
+    }
+
+    if (pmdev->pages[0].read_pin > s->peak_pin) {
+        s->peak_pin = pmdev->pages[0].read_pin;
+    }
+}
+
+static uint16_t adm1272_millivolts_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_VOLTAGE_COEFF_DEFAULT];
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_millivolts(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_VOLTAGE_COEFF_DEFAULT];
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static uint16_t adm1272_milliamps_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_CURRENT_COEFF_DEFAULT];
+    /* Y = (m * r_sense * x - b) * 10^R */
+    c.m = c.m * ADM1272_SHUNT / 1000; /* micro-ohms */
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_milliamps(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_CURRENT_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    c.b = c.b * 1000;
+    c.R = c.R - 3;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static uint16_t adm1272_watts_to_direct(uint32_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_PWR_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    return pmbus_data2direct_mode(c, value);
+}
+
+static uint32_t adm1272_direct_to_watts(uint16_t value)
+{
+    PMBusCoefficients c = adm1272_coefficients[ADM1272_PWR_COEFF_DEFAULT];
+    c.m = c.m * ADM1272_SHUNT / 1000;
+    return pmbus_direct_mode2data(c, value);
+}
+
+static void adm1272_exit_reset(Object *obj)
+{
+    ADM1272State *s = ADM1272(obj);
+    PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+
+    pmdev->page = 0;
+    pmdev->pages[0].operation = ADM1272_OPERATION_DEFAULT;
+
+
+    pmdev->capability = ADM1272_CAPABILITY_NO_PEC;
+    pmdev->pages[0].revision = ADM1272_PMBUS_REVISION_DEFAULT;
+    pmdev->pages[0].vout_mode = ADM1272_DIRECT_MODE;
+    pmdev->pages[0].vout_ov_warn_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].vout_uv_warn_limit = 0;
+    pmdev->pages[0].iout_oc_warn_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].ot_fault_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].ot_warn_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].vin_ov_warn_limit = ADM1272_HIGH_LIMIT_DEFAULT;
+    pmdev->pages[0].vin_uv_warn_limit = 0;
+    pmdev->pages[0].pin_op_warn_limit = ADM1272_PIN_OP_DEFAULT;
+
+    pmdev->pages[0].status_word = 0;
+    pmdev->pages[0].status_vout = 0;
+    pmdev->pages[0].status_iout = 0;
+    pmdev->pages[0].status_input = 0;
+    pmdev->pages[0].status_temperature = 0;
+    pmdev->pages[0].status_mfr_specific = 0;
+
+    pmdev->pages[0].read_vin
+        = adm1272_millivolts_to_direct(ADM1272_VOLT_DEFAULT);
+    pmdev->pages[0].read_vout
+        = adm1272_millivolts_to_direct(ADM1272_VOLT_DEFAULT);
+    pmdev->pages[0].read_iout
+        = adm1272_milliamps_to_direct(ADM1272_IOUT_DEFAULT);
+    pmdev->pages[0].read_temperature_1 = 0;
+    pmdev->pages[0].read_pin = adm1272_watts_to_direct(ADM1272_PWR_DEFAULT);
+    pmdev->pages[0].revision = ADM1272_PMBUS_REVISION_DEFAULT;
+    pmdev->pages[0].mfr_id = ADM1272_MFR_ID_DEFAULT;
+    pmdev->pages[0].mfr_model = ADM1272_MODEL_DEFAULT;
+    pmdev->pages[0].mfr_revision = ADM1272_MFR_DEFAULT_REVISION;
+    pmdev->pages[0].mfr_date = ADM1272_DEFAULT_DATE;
+
+    s->pin_ext = 0;
+    s->ein_ext = 0;
+    s->restart_time = ADM1272_RESTART_TIME_DEFAULT;
+
+    s->peak_vin = 0;
+    s->peak_vout = 0;
+    s->peak_iout = 0;
+    s->peak_temperature = 0;
+    s->peak_pin = 0;
+
+    s->pmon_control = ADM1272_PMON_CONTROL_DEFAULT;
+    s->pmon_config = ADM1272_PMON_CONFIG_DEFAULT;
+    s->alert1_config = 0;
+    s->alert2_config = 0;
+    s->device_config = ADM1272_DEVICE_CONFIG_DEFAULT;
+
+    s->hysteresis_low = 0;
+    s->hysteresis_high = ADM1272_HYSTERESIS_HIGH_DEFAULT;
+    s->status_hysteresis = 0;
+    s->status_gpio = 0;
+
+    s->strt_up_iout_lim = ADM1272_STRT_UP_IOUT_LIM_DEFAULT;
+}
+
+static uint8_t adm1272_read_byte(PMBusDevice *pmdev)
+{
+    ADM1272State *s = ADM1272(pmdev);
+
+    switch (pmdev->code) {
+    case ADM1272_RESTART_TIME:
+        pmbus_send8(pmdev, s->restart_time);
+        break;
+
+    case ADM1272_MFR_PEAK_IOUT:
+        pmbus_send16(pmdev, s->peak_iout);
+        break;
+
+    case ADM1272_MFR_PEAK_VIN:
+        pmbus_send16(pmdev, s->peak_vin);
+        break;
+
+    case ADM1272_MFR_PEAK_VOUT:
+        pmbus_send16(pmdev, s->peak_vout);
+        break;
+
+    case ADM1272_MFR_PMON_CONTROL:
+        pmbus_send8(pmdev, s->pmon_control);
+        break;
+
+    case ADM1272_MFR_PMON_CONFIG:
+        pmbus_send16(pmdev, s->pmon_config);
+        break;
+
+    case ADM1272_MFR_ALERT1_CONFIG:
+        pmbus_send16(pmdev, s->alert1_config);
+        break;
+
+    case ADM1272_MFR_ALERT2_CONFIG:
+        pmbus_send16(pmdev, s->alert2_config);
+        break;
+
+    case ADM1272_MFR_PEAK_TEMPERATURE:
+        pmbus_send16(pmdev, s->peak_temperature);
+        break;
+
+    case ADM1272_MFR_DEVICE_CONFIG:
+        pmbus_send16(pmdev, s->device_config);
+        break;
+
+    case ADM1272_MFR_PEAK_PIN:
+        pmbus_send16(pmdev, s->peak_pin);
+        break;
+
+    case ADM1272_MFR_READ_PIN_EXT:
+        pmbus_send32(pmdev, s->pin_ext);
+        break;
+
+    case ADM1272_MFR_READ_EIN_EXT:
+        pmbus_send64(pmdev, s->ein_ext);
+        break;
+
+    case ADM1272_HYSTERESIS_LOW:
+        pmbus_send16(pmdev, s->hysteresis_low);
+        break;
+
+    case ADM1272_HYSTERESIS_HIGH:
+        pmbus_send16(pmdev, s->hysteresis_high);
+        break;
+
+    case ADM1272_STATUS_HYSTERESIS:
+        pmbus_send16(pmdev, s->status_hysteresis);
+        break;
+
+    case ADM1272_STATUS_GPIO:
+        pmbus_send16(pmdev, s->status_gpio);
+        break;
+
+    case ADM1272_STRT_UP_IOUT_LIM:
+        pmbus_send16(pmdev, s->strt_up_iout_lim);
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: reading from unsupported register: 0x%02x\n",
+                      __func__, pmdev->code);
+        return 0xFF;
+        break;
+    }
+
+    return 0;
+}
+
+static int adm1272_write_data(PMBusDevice *pmdev, const uint8_t *buf,
+                              uint8_t len)
+{
+    ADM1272State *s = ADM1272(pmdev);
+
+    if (len == 0) {
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: writing empty data\n", __func__);
+        return -1;
+    }
+
+    pmdev->code = buf[0]; /* PMBus command code */
+
+    if (len == 1) {
+        return 0;
+    }
+
+    /* Exclude command code from buffer */
+    buf++;
+    len--;
+
+    switch (pmdev->code) {
+
+    case ADM1272_RESTART_TIME:
+        s->restart_time = pmbus_receive8(pmdev);
+        break;
+
+    case ADM1272_MFR_PMON_CONTROL:
+        s->pmon_control = pmbus_receive8(pmdev);
+        break;
+
+    case ADM1272_MFR_PMON_CONFIG:
+        s->pmon_config = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_MFR_ALERT1_CONFIG:
+        s->alert1_config = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_MFR_ALERT2_CONFIG:
+        s->alert2_config = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_MFR_DEVICE_CONFIG:
+        s->device_config = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_MFR_POWER_CYCLE:
+        adm1272_exit_reset((Object *)s);
+        break;
+
+    case ADM1272_HYSTERESIS_LOW:
+        s->hysteresis_low = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_HYSTERESIS_HIGH:
+        s->hysteresis_high = pmbus_receive16(pmdev);
+        break;
+
+    case ADM1272_STRT_UP_IOUT_LIM:
+        s->strt_up_iout_lim = pmbus_receive16(pmdev);
+        adm1272_check_limits(s);
+        break;
+
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: writing to unsupported register: 0x%02x\n",
+                      __func__, pmdev->code);
+        break;
+    }
+    return 0;
+}
+
+static void adm1272_get(Object *obj, Visitor *v, const char *name, void *opaque,
+                        Error **errp)
+{
+    uint16_t value;
+
+    if (strcmp(name, "vin") == 0 || strcmp(name, "vout") == 0) {
+        value = adm1272_direct_to_millivolts(*(uint16_t *)opaque);
+    } else if (strcmp(name, "iout") == 0) {
+        value = adm1272_direct_to_milliamps(*(uint16_t *)opaque);
+    } else if (strcmp(name, "pin") == 0) {
+        value = adm1272_direct_to_watts(*(uint16_t *)opaque);
+    } else {
+        value = *(uint16_t *)opaque;
+    }
+
+    visit_type_uint16(v, name, &value, errp);
+}
+
+static void adm1272_set(Object *obj, Visitor *v, const char *name, void *opaque,
+                        Error **errp)
+{
+    ADM1272State *s = ADM1272(obj);
+    uint16_t *internal = opaque;
+    uint16_t value;
+
+    if (!visit_type_uint16(v, name, &value, errp)) {
+        return;
+    }
+
+    if (strcmp(name, "vin") == 0 || strcmp(name, "vout") == 0) {
+        *internal = adm1272_millivolts_to_direct(value);
+    } else if (strcmp(name, "iout") == 0) {
+        *internal = adm1272_milliamps_to_direct(value);
+    } else if (strcmp(name, "pin") == 0) {
+        *internal = adm1272_watts_to_direct(value);
+    } else {
+        *internal = value;
+    }
+
+    adm1272_check_limits(s);
+}
+
+static const VMStateDescription vmstate_adm1272 = {
+    .name = "ADM1272",
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (VMStateField[]){
+        VMSTATE_PMBUS_DEVICE(parent, ADM1272State),
+        VMSTATE_UINT64(ein_ext, ADM1272State),
+        VMSTATE_UINT32(pin_ext, ADM1272State),
+        VMSTATE_UINT8(restart_time, ADM1272State),
+
+        VMSTATE_UINT16(peak_vin, ADM1272State),
+        VMSTATE_UINT16(peak_vout, ADM1272State),
+        VMSTATE_UINT16(peak_iout, ADM1272State),
+        VMSTATE_UINT16(peak_temperature, ADM1272State),
+        VMSTATE_UINT16(peak_pin, ADM1272State),
+
+        VMSTATE_UINT8(pmon_control, ADM1272State),
+        VMSTATE_UINT16(pmon_config, ADM1272State),
+        VMSTATE_UINT16(alert1_config, ADM1272State),
+        VMSTATE_UINT16(alert2_config, ADM1272State),
+        VMSTATE_UINT16(device_config, ADM1272State),
+
+        VMSTATE_UINT16(hysteresis_low, ADM1272State),
+        VMSTATE_UINT16(hysteresis_high, ADM1272State),
+        VMSTATE_UINT8(status_hysteresis, ADM1272State),
+        VMSTATE_UINT8(status_gpio, ADM1272State),
+
+        VMSTATE_UINT16(strt_up_iout_lim, ADM1272State),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void adm1272_init(Object *obj)
+{
+    PMBusDevice *pmdev = PMBUS_DEVICE(obj);
+    uint64_t flags = PB_HAS_VOUT_MODE | PB_HAS_VOUT | PB_HAS_VIN | PB_HAS_IOUT |
+                     PB_HAS_PIN | PB_HAS_TEMPERATURE | PB_HAS_MFR_INFO;
+
+    pmbus_page_config(pmdev, 0, flags);
+
+    object_property_add(obj, "vin", "uint16",
+                        adm1272_get,
+                        adm1272_set, NULL, &pmdev->pages[0].read_vin);
+
+    object_property_add(obj, "vout", "uint16",
+                        adm1272_get,
+                        adm1272_set, NULL, &pmdev->pages[0].read_vout);
+
+    object_property_add(obj, "iout", "uint16",
+                        adm1272_get,
+                        adm1272_set, NULL, &pmdev->pages[0].read_iout);
+
+    object_property_add(obj, "pin", "uint16",
+                        adm1272_get,
+                        adm1272_set, NULL, &pmdev->pages[0].read_pin);
+
+}
+
+static void adm1272_class_init(ObjectClass *klass, void *data)
+{
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    PMBusDeviceClass *k = PMBUS_DEVICE_CLASS(klass);
+
+    dc->desc = "Analog Devices ADM1272 Hot Swap controller";
+    dc->vmsd = &vmstate_adm1272;
+    k->write_data = adm1272_write_data;
+    k->receive_byte = adm1272_read_byte;
+    k->device_num_pages = 1;
+
+    rc->phases.exit = adm1272_exit_reset;
+}
+
+static const TypeInfo adm1272_info = {
+    .name = TYPE_ADM1272,
+    .parent = TYPE_PMBUS_DEVICE,
+    .instance_size = sizeof(ADM1272State),
+    .instance_init = adm1272_init,
+    .class_init = adm1272_class_init,
+};
+
+static void adm1272_register_types(void)
+{
+    type_register_static(&adm1272_info);
+}
+
+type_init(adm1272_register_types)
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 20bd60f10b..e141084a79 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -371,6 +371,7 @@ config XLNX_VERSAL
 config NPCM7XX
     bool
     select A9MPCORE
+    select ADM1272
     select ARM_GIC
     select AT24C  # EEPROM
     select PL310  # cache controller
diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index c71ed25820..0774c1f70c 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -14,6 +14,10 @@ config ARMSSE_CPU_PWRCTRL
 config MAX111X
     bool
 
+config ADM1272
+    bool
+    depends on I2C
+
 config TMP105
     bool
     depends on I2C
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 66e1648533..8666e9225a 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -10,6 +10,7 @@ softmmu_ss.add(when: 'CONFIG_SGA', if_true: files('sga.c'))
 softmmu_ss.add(when: 'CONFIG_TMP105', if_true: files('tmp105.c'))
 softmmu_ss.add(when: 'CONFIG_TMP421', if_true: files('tmp421.c'))
 softmmu_ss.add(when: 'CONFIG_EMC141X', if_true: files('emc141x.c'))
+softmmu_ss.add(when: 'CONFIG_ADM1272', if_true: files('adm1272.c'))
 softmmu_ss.add(when: 'CONFIG_UNIMP', if_true: files('unimp.c'))
 softmmu_ss.add(when: 'CONFIG_EMPTY_SLOT', if_true: files('empty_slot.c'))
 softmmu_ss.add(when: 'CONFIG_LED', if_true: files('led.c'))
-- 
2.32.0.93.g670b81a890-goog



^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2021-07-08 18:14 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-07-08 17:20 [PATCH v5 0/5] Add support for PMBus in QEMU Titus Rwantare
2021-07-08 17:20 ` [PATCH v5 1/5] hw/i2c: add support for PMBus Titus Rwantare
2021-07-08 17:20 ` [PATCH v5 2/5] hw/misc: add ADM1272 device Titus Rwantare
2021-07-08 17:20 ` [PATCH v5 3/5] tests/qtest: add tests for ADM1272 device model Titus Rwantare
2021-07-08 17:20 ` [PATCH v5 4/5] hw/misc: add MAX34451 device Titus Rwantare
2021-07-08 17:20 ` [PATCH v5 5/5] tests/qtest: add tests for MAX34451 device model Titus Rwantare
2021-07-08 17:25 [PATCH v5 0/5] Add support for PMBus in QEMU Titus Rwantare
2021-07-08 17:25 ` [PATCH v5 2/5] hw/misc: add ADM1272 device Titus Rwantare

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.