All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3 0/5] initial support for yosemite v4
@ 2026-05-12 10:20 Alexander Hansen
  2026-05-12 10:20 ` [PATCH v3 1/5] ast2600: yosemite4 initial support Alexander Hansen
                   ` (4 more replies)
  0 siblings, 5 replies; 12+ messages in thread
From: Alexander Hansen @ 2026-05-12 10:20 UTC (permalink / raw)
  To: qemu-devel; +Cc: Alexander Hansen

Add initial and incomplete support for yosemite v4 BMC-side emulation.

As part of this effort 3 new sensor ICs are added.

Alexander Hansen (5):
  ast2600: yosemite4 initial support
  ast2600: yosemite4 functional test
  hw/sensor: MAX31790 support
  hw/sensor: support MAX11615
  hw/sensor: support Texas Instruments ADC128D818

 MAINTAINERS                              |   3 +
 hw/arm/Kconfig                           |   3 +
 hw/arm/aspeed_ast2600_fby4.c             | 281 +++++++++++++
 hw/arm/meson.build                       |   1 +
 hw/sensor/Kconfig                        |  12 +
 hw/sensor/adc128d818.c                   | 414 +++++++++++++++++++
 hw/sensor/max11615.c                     | 202 +++++++++
 hw/sensor/max31790.c                     | 499 +++++++++++++++++++++++
 hw/sensor/meson.build                    |   3 +
 hw/sensor/trace-events                   |  24 ++
 include/hw/sensor/adc128d818.h           |  20 +
 include/hw/sensor/max11615.h             |  20 +
 include/hw/sensor/max31790.h             |   7 +
 tests/functional/arm/meson.build         |   2 +
 tests/functional/arm/test_aspeed_fby4.py |  80 ++++
 15 files changed, 1571 insertions(+)
 create mode 100644 hw/arm/aspeed_ast2600_fby4.c
 create mode 100644 hw/sensor/adc128d818.c
 create mode 100644 hw/sensor/max11615.c
 create mode 100644 hw/sensor/max31790.c
 create mode 100644 include/hw/sensor/adc128d818.h
 create mode 100644 include/hw/sensor/max11615.h
 create mode 100644 include/hw/sensor/max31790.h
 create mode 100755 tests/functional/arm/test_aspeed_fby4.py

-- 
2.54.0



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

* [PATCH v3 1/5] ast2600: yosemite4 initial support
  2026-05-12 10:20 [PATCH v3 0/5] initial support for yosemite v4 Alexander Hansen
@ 2026-05-12 10:20 ` Alexander Hansen
  2026-05-12 16:13   ` Cédric Le Goater
  2026-05-12 10:20 ` [PATCH v3 2/5] ast2600: yosemite4 functional test Alexander Hansen
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 12+ messages in thread
From: Alexander Hansen @ 2026-05-12 10:20 UTC (permalink / raw)
  To: qemu-devel
  Cc: Alexander Hansen, Titus Rwantare, Cédric Le Goater,
	Peter Maydell, Steven Lee, Troy Lee, Jamin Lin, Kane Chen,
	Andrew Jeffery, Joel Stanley, qemu-arm

Initial patch based on [1] to support yosemite v4 bmc emulation.

The goal of this machine support is to support OpenBMC development.

Reference linux devicetree from openbmc linux is [2].

Status:
- Enclosure FRU:         showing up
- Management Board FRU:  showing up
- Blade Board FRU:       showing up
- Blade Board sensors:   not implemented
- Blade Chassis FRU:     showing up
- Fan Board FRU:         both showing up
- Fan Board sensors:     supported

Overall the emulation is incomplete but already helpful in development
and testing.

The focus of this initial support is on the FRU eeproms and fanboard
sensors.

Tested: booted an OpenBMC image for yosemite4 target.

References:
[1] https://github.com/9elements/qemu/commit/32139f913c2bd0ebe4bd26c46765861f6f9f2d49
[2] arch/arm/boot/dts/aspeed/aspeed-bmc-facebook-yosemite4.dts
[3] https://github.com/legoater/qemu-aspeed-boot/pull/5

Cc: Titus Rwantare <titusr@google.com>
Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
Cc: Peter Maydell <peter.maydell@linaro.org> (maintainer:ASPEED BMCs)
Cc: Steven Lee <steven_lee@aspeedtech.com> (reviewer:ASPEED BMCs)
Cc: Troy Lee <leetroy@gmail.com> (reviewer:ASPEED BMCs)
Cc: Jamin Lin <jamin_lin@aspeedtech.com> (reviewer:ASPEED BMCs)
Cc: Kane Chen <kane_chen@aspeedtech.com> (reviewer:ASPEED BMCs)
Cc: Andrew Jeffery <andrew@codeconstruct.com.au> (reviewer:ASPEED BMCs)
Cc: Joel Stanley <joel@jms.id.au> (reviewer:ASPEED BMCs)
Cc: qemu-arm@nongnu.org (open list:ASPEED BMCs)
Cc: qemu-devel@nongnu.org (open list:All patches CC here)
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
---
 hw/arm/aspeed_ast2600_fby4.c | 267 +++++++++++++++++++++++++++++++++++
 hw/arm/meson.build           |   1 +
 2 files changed, 268 insertions(+)
 create mode 100644 hw/arm/aspeed_ast2600_fby4.c

diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
new file mode 100644
index 0000000000..d79a88e2fe
--- /dev/null
+++ b/hw/arm/aspeed_ast2600_fby4.c
@@ -0,0 +1,267 @@
+/*
+ * Yosemite V4
+ *
+ * Copyright 2026 9elements.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/arm/machines-qom.h"
+#include "hw/arm/aspeed.h"
+#include "hw/arm/aspeed_soc.h"
+#include "hw/nvram/eeprom_at24c.h"
+#include "hw/i2c/i2c_mux_pca954x.h"
+#include "hw/gpio/pca9552.h"
+
+#define FBY4_BMC_RAM_SIZE ASPEED_RAM_SIZE(2 * GiB)
+
+/* START OF EEPROM CONTENTS */
+
+/*
+ *./frugen -s board.mfg="Wiwynn" \
+ * --set board.pname="Fan Board FSC-MAX ADC-TI LED-NXP EFUSE-MAX" \
+ * --set text:board.pn="BRD-PN-345" \
+ * --board-date "10/1/2017 12:58:00" \
+ * --set board.serial="123456" \
+ * --set product.pname="Yosemite V4" \
+ * --set product.mfg="Wiwynn" \
+ * --set product.ver="v1.1" \
+ * --set product.serial="123456" \
+ * --set product.atag="PLACEHOLDER" \
+ * --set product.custom="Fanboard Custom1" \
+ * --set product.custom.1="Fanboard Custom2" \
+ * --set text:product.pn="PN-345" \
+ * fru-yv4-fanboard.bin
+ */
+/* EM Config: yosemite4_fanboard_fsc_max_adc_ti_led_nxp_ons_efuse_max.json */
+/* Yosemite4 fan board */
+static const uint8_t fru_yv4_fanboard_bin[] = {
+  0x01, 0x00, 0x00, 0x01, 0x0b, 0x00, 0x00, 0xf3, 0x01, 0x0a, 0x19, 0xce,
+  0xc2, 0xa8, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xea, 0x46, 0x61,
+  0x6e, 0x20, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x20, 0x46, 0x53, 0x43, 0x2d,
+  0x4d, 0x41, 0x58, 0x20, 0x41, 0x44, 0x43, 0x2d, 0x54, 0x49, 0x20, 0x4c,
+  0x45, 0x44, 0x2d, 0x4e, 0x58, 0x50, 0x20, 0x45, 0x46, 0x55, 0x53, 0x45,
+  0x2d, 0x4d, 0x41, 0x58, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05, 0xca, 0x42,
+  0x52, 0x44, 0x2d, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc0, 0xc1, 0x00,
+  0x00, 0x00, 0x00, 0xdb, 0x01, 0x09, 0x19, 0xc6, 0x57, 0x69, 0x77, 0x79,
+  0x6e, 0x6e, 0xcb, 0x59, 0x6f, 0x73, 0x65, 0x6d, 0x69, 0x74, 0x65, 0x20,
+  0x56, 0x34, 0xc6, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc4, 0x76, 0x31,
+  0x2e, 0x31, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05, 0x89, 0x30, 0x1b, 0x8e,
+  0x25, 0xfa, 0xb2, 0x64, 0x29, 0x03, 0xc0, 0xd0, 0x46, 0x61, 0x6e, 0x62,
+  0x6f, 0x61, 0x72, 0x64, 0x20, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x32,
+  0xc1, 0x00, 0x00, 0x9d
+};
+static const size_t fru_yv4_fanboard_bin_len = sizeof(fru_yv4_fanboard_bin);
+
+/*
+ *./frugen -s board.mfg="Wiwynn" \
+ * --set board.pname="Sentinel Dome without Retimer" \
+ * --set text:board.pn="BRD-PN-345" \
+ * --board-date "10/1/2017 12:58:00" \
+ * --set board.serial="123456" \
+ * --set product.pname="Yosemite V4" \
+ * --set product.mfg="Wiwynn" \
+ * --set product.ver="v1.1" \
+ * --set product.serial="123456" \
+ * --set product.atag="PLACEHOLDER" \
+ * --set product.custom="Yosemite V4 T1" \
+ * --set product.custom.1="Yosemite V4 T1" \
+ * --set text:product.pn="PN-345" \
+ * fru-yv4-sentineldome-board.bin
+ */
+/* product.cust maps to PRODUCT_INFO_AM2 */
+/* EM Config: yosemite4_sentineldome_t1.json */
+/* Yosemite4 Sentinel Dome without Retimer Boards */
+static const uint8_t fru_yv4_sentineldome_board_bin[] = {
+  0x01, 0x00, 0x00, 0x01, 0x09, 0x00, 0x00, 0xf5, 0x01, 0x08, 0x19, 0xce,
+  0xc2, 0xa8, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xdd, 0x53, 0x65,
+  0x6e, 0x74, 0x69, 0x6e, 0x65, 0x6c, 0x20, 0x44, 0x6f, 0x6d, 0x65, 0x20,
+  0x77, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x20, 0x52, 0x65, 0x74, 0x69,
+  0x6d, 0x65, 0x72, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05, 0xca, 0x42, 0x52,
+  0x44, 0x2d, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc0, 0xc1, 0x00, 0x78,
+  0x01, 0x09, 0x19, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xcb, 0x59,
+  0x6f, 0x73, 0x65, 0x6d, 0x69, 0x74, 0x65, 0x20, 0x56, 0x34, 0xc6, 0x50,
+  0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc4, 0x76, 0x31, 0x2e, 0x31, 0x85, 0x91,
+  0x34, 0x51, 0x95, 0x05, 0x89, 0x30, 0x1b, 0x8e, 0x25, 0xfa, 0xb2, 0x64,
+  0x29, 0x03, 0xc0, 0xce, 0x59, 0x6f, 0x73, 0x65, 0x6d, 0x69, 0x74, 0x65,
+  0x20, 0x56, 0x34, 0x20, 0x54, 0x31, 0xc1, 0x00, 0x00, 0x00, 0x00, 0xeb
+};
+static const size_t fru_yv4_sentineldome_board_bin_len =
+    sizeof(fru_yv4_sentineldome_board_bin);
+
+/*
+ *./frugen -s board.mfg="Wiwynn" \
+ * --set board.pname="Sentinel Dome" \
+ * --set text:board.pn="BRD-PN-345" \
+ * --board-date "10/1/2017 12:58:00" \
+ * --set board.serial="123456" \
+ * --set product.pname="Yosemite V4" \
+ * --set product.mfg="Wiwynn" \
+ * --set product.ver="v1.1" \
+ * --set product.serial="123456" \
+ * --set product.atag="PLACEHOLDER" \
+ * --set text:product.pn="PN-345" \
+ * fru-yv4-sentineldome-chassis.bin
+ */
+/* EM Config: yosemite4_sentineldome_chassis.json */
+/* Yosemite 4 Sentinel Dome Chassis FRU */
+static const uint8_t fru_yv4_sentineldome_chassis_bin[] = {
+  0x01, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0xf7, 0x01, 0x06, 0x19, 0xce,
+  0xc2, 0xa8, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xcd, 0x53, 0x65,
+  0x6e, 0x74, 0x69, 0x6e, 0x65, 0x6c, 0x20, 0x44, 0x6f, 0x6d, 0x65, 0x85,
+  0x91, 0x34, 0x51, 0x95, 0x05, 0xca, 0x42, 0x52, 0x44, 0x2d, 0x50, 0x4e,
+  0x2d, 0x33, 0x34, 0x35, 0xc0, 0xc1, 0x00, 0xb6, 0x01, 0x07, 0x19, 0xc6,
+  0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xcb, 0x59, 0x6f, 0x73, 0x65, 0x6d,
+  0x69, 0x74, 0x65, 0x20, 0x56, 0x34, 0xc6, 0x50, 0x4e, 0x2d, 0x33, 0x34,
+  0x35, 0xc4, 0x76, 0x31, 0x2e, 0x31, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05,
+  0x89, 0x30, 0x1b, 0x8e, 0x25, 0xfa, 0xb2, 0x64, 0x29, 0x03, 0xc0, 0xc1,
+  0x00, 0x00, 0x00, 0x59
+};
+static const size_t fru_yv4_sentineldome_chassis_bin_len =
+    sizeof(fru_yv4_sentineldome_chassis_bin);
+
+/*
+ *./frugen -s board.mfg="Wiwynn" \
+ * --set board.pname="Management Board wBMC" \
+ * --set text:board.pn="BRD-PN-345" \
+ * --board-date "10/1/2017 12:58:00" \
+ * --set board.serial="123456" \
+ * --set product.pname="Yosemite V4" \
+ * --set product.mfg="Wiwynn" \
+ * --set product.ver="v1.1" \
+ * --set product.serial="123456" \
+ * --set product.atag="PLACEHOLDER" \
+ * --set text:product.pn="PN-345" \
+ * fru-yv4-eclosure.bin
+ */
+/* EM Config: yosemite4_chassis.json, yosemite4.json */
+/* Yosemite 4 Sentinel Dome Enclosure FRU */
+static const uint8_t fru_yv4_eclosure_bin[] = {
+  0x01, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0xf6, 0x01, 0x07, 0x19, 0xce,
+  0xc2, 0xa8, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xd5, 0x4d, 0x61,
+  0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x42, 0x6f, 0x61,
+  0x72, 0x64, 0x20, 0x77, 0x42, 0x4d, 0x43, 0x85, 0x91, 0x34, 0x51, 0x95,
+  0x05, 0xca, 0x42, 0x52, 0x44, 0x2d, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35,
+  0xc0, 0xc1, 0x00, 0x26, 0x01, 0x07, 0x19, 0xc6, 0x57, 0x69, 0x77, 0x79,
+  0x6e, 0x6e, 0xcb, 0x59, 0x6f, 0x73, 0x65, 0x6d, 0x69, 0x74, 0x65, 0x20,
+  0x56, 0x34, 0xc6, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc4, 0x76, 0x31,
+  0x2e, 0x31, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05, 0x89, 0x30, 0x1b, 0x8e,
+  0x25, 0xfa, 0xb2, 0x64, 0x29, 0x03, 0xc0, 0xc1, 0x00, 0x00, 0x00, 0x59
+};
+static const size_t fru_yv4_eclosure_bin_len = sizeof(fru_yv4_eclosure_bin);
+
+/* END OF EEPROM CONTENTS */
+
+static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
+{
+    /* 2 fan boards */
+    for (int i = 0; i <= 1; i++) {
+        /* downstream bus */
+        I2CBus *bus = pca954x_i2c_get_bus(fan_mux, i);
+
+        /* ti,adc128d818 @ 0x1f    (adc) */
+        /* TODO */
+
+        /* maxim,max31790 @ 0x20   (pwm) */
+        /* TODO */
+
+        /*
+         * ti,tca6424 @ 0x22       (gpio)
+         * linux handles tca6424 with PCA953X_TYPE, same as pca9535
+         * { "pca9535", 16 | PCA953X_TYPE | PCA_INT, },
+         * { "tca6424", 24 | PCA953X_TYPE | PCA_INT, },
+         * so we _could_ be fine here unless more than 16 gpios are used
+         */
+        i2c_slave_create_simple(bus, TYPE_PCA9535, 0x22);
+
+        /*
+         * NOTE: above works and could use for gpio presence:
+         * $ gpioget gpiochip2 2
+         * 1
+         */
+
+        /* maxim,max31790 @ 0x2f   (pwm) */
+        /* TODO */
+
+        /* maxim,max11615 @ 0x33   (adc) */
+        /* TODO */
+
+        at24c_eeprom_init_rom(
+            bus, 0x52, eepromSize,
+            fru_yv4_fanboard_bin,
+            fru_yv4_fanboard_bin_len);
+
+        /* LED blink driver / gpio expander */
+        /* nxp,pca9552 @ 0x61   (gpio) */
+        i2c_slave_create_simple(bus, TYPE_PCA9552, 0x61);
+    }
+}
+
+static void fby4_i2c_init_blade_chassis(I2CBus *bus, size_t eepromSize)
+{
+    /* Sentinel Dome Blade EEPROMS */
+
+    /* Board */
+    at24c_eeprom_init_rom(bus, 0x54, eepromSize,
+        fru_yv4_sentineldome_board_bin, fru_yv4_sentineldome_board_bin_len);
+
+    /* Chassis */
+   at24c_eeprom_init_rom(bus, 0x55, eepromSize,
+       fru_yv4_sentineldome_chassis_bin, fru_yv4_sentineldome_chassis_bin_len);
+}
+
+static void fby4_i2c_init_multiple_blade_chassis(I2CBus **i2c,
+    size_t eepromSize)
+{
+    /* there is 8 blade chassis, but we only emulate 2 for performance reason */
+    for (int bus = 1; bus <= (8 / 4); bus++) {
+        fby4_i2c_init_blade_chassis(i2c[bus], eepromSize);
+    }
+}
+
+static void fby4_i2c_init(AspeedMachineState *bmc)
+{
+    AspeedSoCState *soc = bmc->soc;
+    I2CBus *i2c[16];
+
+    for (int i = 0; i < 16; i++) {
+        i2c[i] = aspeed_i2c_get_bus(&soc->i2c, i);
+    }
+
+    /* Enclosure (EM Config: yosemite4_chassis.json, yosemite4.json) */
+    at24c_eeprom_init_rom(i2c[1], 0x51, 128 * KiB,
+        fru_yv4_eclosure_bin, fru_yv4_eclosure_bin_len);
+
+    fby4_i2c_init_multiple_blade_chassis(i2c, 128 * KiB);
+
+    /* Yv4 fanboard connection */
+    I2CSlave *fan_mux = i2c_slave_create_simple(i2c[14], TYPE_PCA9546, 0x74);
+
+    fby4_i2c_init_fanboard(fan_mux, 128 * KiB);
+}
+
+static void aspeed_machine_fby4_class_init(ObjectClass *oc, const void *data)
+{
+    MachineClass *mc = MACHINE_CLASS(oc);
+    AspeedMachineClass *amc = ASPEED_MACHINE_CLASS(oc);
+
+    mc->desc       = "Facebook fby4 BMC (Cortex-A7)";
+    amc->fmc_model = "mx66l1g45g";
+    amc->num_cs    = 2;
+    amc->macs_mask = ASPEED_MAC3_ON;
+    amc->i2c_init  = fby4_i2c_init;
+    mc->default_ram_size = FBY4_BMC_RAM_SIZE;
+    aspeed_machine_class_init_cpus_defaults(mc);
+}
+
+static const TypeInfo aspeed_ast2600_fby4_types[] = {
+    {
+        .name          = MACHINE_TYPE_NAME("fby4-bmc"),
+        .parent        = MACHINE_TYPE_NAME("ast2600-evb"),
+        .class_init    = aspeed_machine_fby4_class_init,
+        .interfaces    = arm_machine_interfaces,
+    }
+};
+
+DEFINE_TYPES(aspeed_ast2600_fby4_types)
diff --git a/hw/arm/meson.build b/hw/arm/meson.build
index 84b8ec5fb5..ccbc23e549 100644
--- a/hw/arm/meson.build
+++ b/hw/arm/meson.build
@@ -60,6 +60,7 @@ arm_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: files(
   'aspeed_ast2600_catalina.c',
   'aspeed_ast2600_evb.c',
   'aspeed_ast2600_fby35.c',
+  'aspeed_ast2600_fby4.c',
   'aspeed_ast2600_fuji.c',
   'aspeed_ast2600_gb200nvl.c',
   'aspeed_ast2600_qcom-dc-scm-v1.c',
-- 
2.54.0



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

* [PATCH v3 2/5] ast2600: yosemite4 functional test
  2026-05-12 10:20 [PATCH v3 0/5] initial support for yosemite v4 Alexander Hansen
  2026-05-12 10:20 ` [PATCH v3 1/5] ast2600: yosemite4 initial support Alexander Hansen
@ 2026-05-12 10:20 ` Alexander Hansen
  2026-05-12 16:34   ` Cédric Le Goater
  2026-05-12 10:20 ` [PATCH v3 3/5] hw/sensor: MAX31790 support Alexander Hansen
                   ` (2 subsequent siblings)
  4 siblings, 1 reply; 12+ messages in thread
From: Alexander Hansen @ 2026-05-12 10:20 UTC (permalink / raw)
  To: qemu-devel
  Cc: Alexander Hansen, Titus Rwantare, Cédric Le Goater,
	Peter Maydell, Steven Lee, Troy Lee, Jamin Lin, Kane Chen,
	Andrew Jeffery, Joel Stanley, qemu-arm

Tested: functional test passed.

export QEMU_TEST_QEMU_BINARY=qemu-system-arm
./build/run tests/functional/arm/test_aspeed_fby4.py
TAP version 13
ok 1 test_aspeed_fby4.YosemiteV4Machine.test_arm_ast2600_yosemitev4_openbmc
1..1

Cc: Titus Rwantare <titusr@google.com>
Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
Cc: Peter Maydell <peter.maydell@linaro.org> (maintainer:ASPEED BMCs)
Cc: Steven Lee <steven_lee@aspeedtech.com> (reviewer:ASPEED BMCs)
Cc: Troy Lee <leetroy@gmail.com> (reviewer:ASPEED BMCs)
Cc: Jamin Lin <jamin_lin@aspeedtech.com> (reviewer:ASPEED BMCs)
Cc: Kane Chen <kane_chen@aspeedtech.com> (reviewer:ASPEED BMCs)
Cc: Andrew Jeffery <andrew@codeconstruct.com.au> (reviewer:ASPEED BMCs)
Cc: Joel Stanley <joel@jms.id.au> (reviewer:ASPEED BMCs)
Cc: qemu-arm@nongnu.org (open list:ASPEED BMCs)
Cc: qemu-devel@nongnu.org (open list:All patches CC here)
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
---
 tests/functional/arm/meson.build         |  2 ++
 tests/functional/arm/test_aspeed_fby4.py | 44 ++++++++++++++++++++++++
 2 files changed, 46 insertions(+)
 create mode 100755 tests/functional/arm/test_aspeed_fby4.py

diff --git a/tests/functional/arm/meson.build b/tests/functional/arm/meson.build
index 2f538f29a2..10c0006f22 100644
--- a/tests/functional/arm/meson.build
+++ b/tests/functional/arm/meson.build
@@ -14,6 +14,7 @@ test_arm_timeouts = {
   'aspeed_ast2600_sdk_otp' : 720,
   'aspeed_bletchley' : 480,
   'aspeed_catalina' : 480,
+  'aspeed_fby4': 480,
   'aspeed_gb200nvl_bmc' : 480,
   'aspeed_rainier' : 480,
   'bpim2u' : 500,
@@ -47,6 +48,7 @@ tests_arm_system_thorough = [
   'aspeed_ast2600_sdk_otp',
   'aspeed_bletchley',
   'aspeed_catalina',
+  'aspeed_fby4',
   'aspeed_gb200nvl_bmc',
   'aspeed_rainier',
   'bpim2u',
diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
new file mode 100755
index 0000000000..a3124c240f
--- /dev/null
+++ b/tests/functional/arm/test_aspeed_fby4.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+#
+# Functional test that boots the ASPEED machines
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+from qemu_test import Asset
+from aspeed import AspeedTest
+
+
+class YosemiteV4Machine(AspeedTest):
+
+    ASSET_YOSEMITE_V4_FLASH = Asset(
+        'https://github.com/legoater/qemu-aspeed-boot/raw/refs/heads/master/images/yosemite4-bmc/openbmc-20260505132843/obmc-phosphor-image-yosemite4-20260505132843.static.mtd.xz',
+        'dff6946363b41f952b15cfc3156482b89fcfc1b0ecfc3ec8b3ed496a5f001ef9')
+
+    def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
+                                   cpu_id, soc):
+
+        self.set_machine(machine)
+        self.vm.set_console()
+        self.vm.add_args('-drive', f'file={image},if=mtd,format=raw',
+                         '-snapshot')
+        self.vm.launch()
+
+        self.wait_for_console_pattern(f'U-Boot {uboot}')
+        self.wait_for_console_pattern('## Loading kernel from FIT Image')
+        self.wait_for_console_pattern('Starting kernel ...')
+        self.wait_for_console_pattern(f'Booting Linux on physical CPU {cpu_id}')
+        self.wait_for_console_pattern(f'ASPEED {soc}')
+        self.wait_for_console_pattern('/init as init process')
+        # yosemite v4 does not emit the hostname log which is
+        # different from the other machines.
+        self.wait_for_console_pattern('yosemite4 login:')
+
+    def test_arm_ast2600_yosemitev4_openbmc(self):
+        image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
+
+        self.do_test_arm_aspeed_openbmc_no_network('fby4-bmc', image=image_path,
+                                        uboot='2019.04', cpu_id='0xf00',
+                                        soc='AST2600 rev A3')
+
+if __name__ == '__main__':
+    AspeedTest.main()
-- 
2.54.0



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

* [PATCH v3 3/5] hw/sensor: MAX31790 support
  2026-05-12 10:20 [PATCH v3 0/5] initial support for yosemite v4 Alexander Hansen
  2026-05-12 10:20 ` [PATCH v3 1/5] ast2600: yosemite4 initial support Alexander Hansen
  2026-05-12 10:20 ` [PATCH v3 2/5] ast2600: yosemite4 functional test Alexander Hansen
@ 2026-05-12 10:20 ` Alexander Hansen
  2026-05-12 16:51   ` Cédric Le Goater
  2026-05-12 10:20 ` [PATCH v3 4/5] hw/sensor: support MAX11615 Alexander Hansen
  2026-05-12 10:20 ` [PATCH v3 5/5] hw/sensor: support Texas Instruments ADC128D818 Alexander Hansen
  4 siblings, 1 reply; 12+ messages in thread
From: Alexander Hansen @ 2026-05-12 10:20 UTC (permalink / raw)
  To: qemu-devel
  Cc: Alexander Hansen, Titus Rwantare, Cédric Le Goater,
	Paolo Bonzini, Peter Maydell, Philippe Mathieu-Daudé,
	qemu-arm, Steven Lee, Troy Lee, Jamin Lin, Kane Chen,
	Andrew Jeffery, Joel Stanley

Product: [1]
Datasheet: [2]

MAX31790 Support:
- fan inputs are reading
- tach reading propertional to pwm setting from linux driver
- fans do not show any fault
- 6 PWM registers influence 6 TACH registers

There is intentional stub behavior in some places and various functions
of the device are currently unsupported.

MAX31790 currently unsupported:
- slave address restriction
- fan dynamics
- spin-up configuration
- fault state / failure possibility
- rate-of-change control
- tach mode
- mixed layouts where number of fans != number of tachs
- see Figure 5.9 in [2] for example of mixed layout

Anyone could expand it in the future for more accurate emulation.

The reason for adding this device is to support Yosemite V4 emulation.

Tested: on yosemite 4 qemu

root@yosemite4:~# ls /sys/class/hwmon/hwmon2/
device	    fan2_fault   fan3_target  fan5_fault   fan6_target  pwm2	     pwm5
fan1_enable  fan2_input   fan4_enable  fan5_input   name	pwm2_enable  pwm5_enable
fan1_fault   fan2_target  fan4_fault   fan5_target  of_node	pwm3	     pwm6
fan1_input   fan3_enable  fan4_input   fan6_enable  power	pwm3_enable  pwm6_enable
fan1_target  fan3_fault   fan4_target  fan6_fault   pwm1	pwm4	     subsystem
fan2_enable  fan3_input   fan5_enable  fan6_input   pwm1_enable  pwm4_enable  uevent
root@yosemite4:~# cat /sys/class/hwmon/hwmon2/fan1_input
4551
root@yosemite4:~# cat /sys/class/hwmon/hwmon2/fan1_enable
1
root@yosemite4:~# cat /sys/class/hwmon/hwmon2/fan1_fault
0
root@yosemite4:~# cat /sys/class/hwmon/hwmon2/fan1_target
2048
root@yosemite4:~# cat /sys/class/hwmon/hwmon2/pwm1
178
root@yosemite4:~# cat /sys/class/hwmon/hwmon2/name
max31790

Trace output:
max31790_realize i2c_addr: 0x20
max31790_realize i2c_addr: 0x2f
max31790_realize i2c_addr: 0x20
max31790_realize i2c_addr: 0x2f
max31790_event i2c_addr: 0x20, event: 0x01
max31790_send i2c_addr: 0x20, data: 0x02
max31790_event i2c_addr: 0x20, event: 0x00
max31790_recv i2c_addr: 0x20, reg_addr: 0x02
max31790_recv_return i2c_addr: 0x20, returns: 0x08
...

References:
[1] https://www.analog.com/en/products/MAX31790.html
[2] https://www.analog.com/media/en/technical-documentation/data-sheets/MAX31790.pdf

Cc: Titus Rwantare <titusr@google.com>
Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
Cc: Paolo Bonzini <pbonzini@redhat.com>
Cc: Peter Maydell <peter.maydell@linaro.org>
Cc: "Philippe Mathieu-Daudé" <philmd@linaro.org>
Cc: qemu-arm@nongnu.org
Cc: qemu-devel@nongnu.org
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
---
 MAINTAINERS                              |   1 +
 hw/arm/Kconfig                           |   1 +
 hw/arm/aspeed_ast2600_fby4.c             |   5 +-
 hw/sensor/Kconfig                        |   4 +
 hw/sensor/max31790.c                     | 499 +++++++++++++++++++++++
 hw/sensor/meson.build                    |   1 +
 hw/sensor/trace-events                   |   8 +
 include/hw/sensor/max31790.h             |   7 +
 tests/functional/arm/test_aspeed_fby4.py |  18 +
 9 files changed, 542 insertions(+), 2 deletions(-)
 create mode 100644 hw/sensor/max31790.c
 create mode 100644 include/hw/sensor/max31790.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 93a1e4e482..9c991f8e70 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3981,6 +3981,7 @@ S: Maintained
 F: hw/i2c/pmbus_device.c
 F: hw/sensor/adm1272.c
 F: hw/sensor/isl_pmbus_vr.c
+F: hw/sensor/max31790.c
 F: hw/sensor/max34451.c
 F: include/hw/i2c/pmbus_device.h
 F: include/hw/sensor/isl_pmbus_vr.h
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 5b198402d5..99864eb878 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -552,6 +552,7 @@ config ASPEED_SOC
     select LED
     select PMBUS
     select MAX31785
+    select MAX31790
     select FSI_APB2OPB_ASPEED
     select AT24C
     select PCI_EXPRESS
diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
index d79a88e2fe..a7c2dc09ea 100644
--- a/hw/arm/aspeed_ast2600_fby4.c
+++ b/hw/arm/aspeed_ast2600_fby4.c
@@ -11,6 +11,7 @@
 #include "hw/arm/aspeed.h"
 #include "hw/arm/aspeed_soc.h"
 #include "hw/nvram/eeprom_at24c.h"
+#include "hw/sensor/max31790.h"
 #include "hw/i2c/i2c_mux_pca954x.h"
 #include "hw/gpio/pca9552.h"
 
@@ -164,7 +165,7 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
         /* TODO */
 
         /* maxim,max31790 @ 0x20   (pwm) */
-        /* TODO */
+        i2c_slave_create_simple(bus, TYPE_MAX31790, 0x20);
 
         /*
          * ti,tca6424 @ 0x22       (gpio)
@@ -182,7 +183,7 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
          */
 
         /* maxim,max31790 @ 0x2f   (pwm) */
-        /* TODO */
+        i2c_slave_create_simple(bus, TYPE_MAX31790, 0x2f);
 
         /* maxim,max11615 @ 0x33   (adc) */
         /* TODO */
diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
index bc6331b4ab..ece2f2b167 100644
--- a/hw/sensor/Kconfig
+++ b/hw/sensor/Kconfig
@@ -43,3 +43,7 @@ config ISL_PMBUS_VR
 config MAX31785
     bool
     depends on PMBUS
+
+config MAX31790
+    bool
+    depends on PMBUS
diff --git a/hw/sensor/max31790.c b/hw/sensor/max31790.c
new file mode 100644
index 0000000000..16525cba9b
--- /dev/null
+++ b/hw/sensor/max31790.c
@@ -0,0 +1,499 @@
+/*
+ * Maxim MAX31790 PMBus 6-Channel Fan Controller
+ *
+ * Datasheet:
+ * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX31790.pdf
+ *
+ * Copyright 2026 9elements
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/sensor/max31790.h"
+#include "hw/i2c/i2c.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "qom/object.h"
+#include "trace.h"
+
+#define MAX31790_NUM_FANS 6
+#define MAX31790_NUM_TACHS 12
+
+#define MAX31790_REG_GLOBAL_CONFIG 0x00
+#define MAX31790_REG_PWM_FREQ 0x01
+
+/* 0x02 to 0x07: N = 0 .. 5 */
+#define MAX31790_REG_FAN_CONFIG(N) (0x02 + N)
+
+/* 0x08 to 0x0d: N = 0 .. 5 */
+#define MAX31790_REG_FAN_DYNAMICS(N) (0x08 + N)
+
+#define MAX31790_REG_FAN_FAULT_STATUS_2 0x10
+#define MAX31790_REG_FAN_FAULT_STATUS_1 0x11
+#define MAX31790_REG_FAN_FAULT_MASK_2 0x12
+#define MAX31790_REG_FAN_FAULT_MASK_1 0x13
+#define MAX31790_REG_FAILED_FAN_OPT 0x14
+
+/* 0x18 to 0x2f: N = 0 .. 11 */
+#define MAX31790_REG_TACH_COUNT_MSB(N) (0x18 + 2 * N)
+#define MAX31790_REG_TACH_COUNT_LSB(N) (0x19 + 2 * N)
+
+/* 0x30 to 0x3b: N = 0 .. 5 */
+#define MAX31790_REG_PWM_DUTY_CYCLE_MSB(N) (0x30 + 2 * N)
+#define MAX31790_REG_PWM_DUTY_CYCLE_LSB(N) (0x31 + 2 * N)
+
+/* .. reserved registers ... */
+
+/* 0x40 to 0x4b: N = 0 .. 5 */
+#define MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(N) (0x40 + 2 * N)
+#define MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(N) (0x41 + 2 * N)
+
+/* ... 'User Byte' registers ... */
+
+/* 0x50 to 0x5b: N = 0 .. 5 */
+#define MAX31790_REG_TACH_TARGET_COUNT_MSB(N) (0x50 + 2 * N)
+#define MAX31790_REG_TACH_TARGET_COUNT_LSB(N) (0x51 + 2 * N)
+
+struct MAX31790State {
+  I2CSlave i2c;
+
+  uint8_t fan_config[MAX31790_NUM_FANS];
+  uint8_t fan_dynamics[MAX31790_NUM_FANS];
+
+  uint16_t pwm[MAX31790_NUM_FANS];
+  uint16_t tach_target[MAX31790_NUM_FANS];
+  uint16_t rpm[MAX31790_NUM_TACHS];
+
+  /* command buffer */
+  uint8_t len;
+  uint8_t buf[2];
+
+  /* output buffer */
+  uint8_t outlen;
+  uint8_t outbuf[2];
+
+  /* selected register for read/write operation */
+  uint8_t pointer;
+};
+
+struct MAX31790Class {
+  I2CSlaveClass parent_class;
+};
+
+OBJECT_DECLARE_TYPE(MAX31790State, MAX31790Class, MAX31790)
+
+static void max31790_read(MAX31790State *s)
+{
+  size_t index = 0;
+  uint8_t out0 = 0;
+  uint8_t out1 = 0;
+
+  switch (s->pointer) {
+  case MAX31790_REG_FAN_CONFIG(0):
+  case MAX31790_REG_FAN_CONFIG(1):
+  case MAX31790_REG_FAN_CONFIG(2):
+  case MAX31790_REG_FAN_CONFIG(3):
+  case MAX31790_REG_FAN_CONFIG(4):
+  case MAX31790_REG_FAN_CONFIG(5):
+    out0 = s->fan_config[s->pointer - MAX31790_REG_FAN_CONFIG(0)];
+    break;
+  case MAX31790_REG_FAN_DYNAMICS(0):
+  case MAX31790_REG_FAN_DYNAMICS(1):
+  case MAX31790_REG_FAN_DYNAMICS(2):
+  case MAX31790_REG_FAN_DYNAMICS(3):
+  case MAX31790_REG_FAN_DYNAMICS(4):
+  case MAX31790_REG_FAN_DYNAMICS(5):
+    out0 = s->fan_dynamics[s->pointer - MAX31790_REG_FAN_DYNAMICS(0)];
+    break;
+  case MAX31790_REG_FAN_FAULT_STATUS_1:
+  case MAX31790_REG_FAN_FAULT_STATUS_2:
+    /* we do not have any fan fault */
+    out0 = 0x00;
+    out1 = 0x00;
+    break;
+  case MAX31790_REG_TACH_COUNT_MSB(0):
+  case MAX31790_REG_TACH_COUNT_MSB(1):
+  case MAX31790_REG_TACH_COUNT_MSB(2):
+  case MAX31790_REG_TACH_COUNT_MSB(3):
+  case MAX31790_REG_TACH_COUNT_MSB(4):
+  case MAX31790_REG_TACH_COUNT_MSB(5):
+  case MAX31790_REG_TACH_COUNT_MSB(6):
+  case MAX31790_REG_TACH_COUNT_MSB(7):
+  case MAX31790_REG_TACH_COUNT_MSB(8):
+  case MAX31790_REG_TACH_COUNT_MSB(9):
+  case MAX31790_REG_TACH_COUNT_MSB(10):
+  case MAX31790_REG_TACH_COUNT_MSB(11):
+    index = (s->pointer - MAX31790_REG_TACH_COUNT_MSB(0)) / 2;
+    out0 = (s->rpm[index] >> 8) & 0xff;
+    out1 = s->rpm[index] & 0xff;
+    break;
+
+  case MAX31790_REG_TACH_COUNT_LSB(0):
+  case MAX31790_REG_TACH_COUNT_LSB(1):
+  case MAX31790_REG_TACH_COUNT_LSB(2):
+  case MAX31790_REG_TACH_COUNT_LSB(3):
+  case MAX31790_REG_TACH_COUNT_LSB(4):
+  case MAX31790_REG_TACH_COUNT_LSB(5):
+  case MAX31790_REG_TACH_COUNT_LSB(6):
+  case MAX31790_REG_TACH_COUNT_LSB(7):
+  case MAX31790_REG_TACH_COUNT_LSB(8):
+  case MAX31790_REG_TACH_COUNT_LSB(9):
+  case MAX31790_REG_TACH_COUNT_LSB(10):
+  case MAX31790_REG_TACH_COUNT_LSB(11):
+    index = (s->pointer - MAX31790_REG_TACH_COUNT_LSB(0)) / 2;
+    out0 = s->rpm[index] & 0xff;
+    break;
+
+  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(0):
+  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(1):
+  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(2):
+  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(3):
+  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(4):
+  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(5):
+    index = (s->pointer - MAX31790_REG_PWM_DUTY_CYCLE_MSB(0)) / 2;
+    out0 = (s->pwm[index] >> 8) & 0xff;
+    out1 = s->pwm[index] & 0xff;
+    break;
+  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(0):
+  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(1):
+  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(2):
+  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(3):
+  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(4):
+  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(5):
+    index = (s->pointer - MAX31790_REG_PWM_DUTY_CYCLE_LSB(0)) / 2;
+    out0 = s->pwm[index] & 0xff;
+    break;
+
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(1):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(2):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(3):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(4):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(5):
+    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0)) / 2;
+    out0 = (s->pwm[index] >> 8) & 0xff;
+    out1 = s->pwm[index] & 0xff;
+    break;
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(0):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(1):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(2):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(3):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(4):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(5):
+    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(0)) / 2;
+    out0 = s->pwm[index] & 0xff;
+    break;
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(0):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(1):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(2):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(3):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(4):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(5):
+    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_MSB(0)) / 2;
+    out0 = (s->tach_target[index] >> 8) & 0xff;
+    out1 = s->tach_target[index] & 0xff;
+    break;
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(0):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(1):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(2):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(3):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(4):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(5):
+    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_LSB(0)) / 2;
+    out0 = s->tach_target[index] & 0xff;
+    break;
+  default:
+    qemu_log_mask(LOG_UNIMP, "%s: read of register %d", __func__, s->pointer);
+    break;
+  }
+
+  s->outbuf[0] = out0;
+  s->outbuf[1] = out1;
+}
+
+static void max31790_set_rpm(MAX31790State *s, size_t index, uint16_t rpm)
+{
+  /* datasheet: lowest 5 bits are 0 */
+  s->rpm[index] = rpm & ~0b11111;
+}
+
+static void max31790_pwm_write(MAX31790State *s, size_t index, uint16_t value)
+{
+  trace_max31790_pwm_write(s->i2c.address, index, value);
+
+  s->pwm[index] = value;
+
+  /* change rpm based on pwm input */
+  const uint16_t pwm_no_reserve = s->pwm[index] >> 7;
+
+  /*
+   * This formula has magic values which model the relationship
+   * of PWM input to a fan. Not derived from datasheet.
+   */
+  max31790_set_rpm(s, index, 0x1000 + (pwm_no_reserve << 3));
+}
+
+static void max31790_write_2_byte(MAX31790State *s)
+{
+  size_t index = 0;
+  const uint8_t value0 = s->buf[0];
+  const uint8_t value1 = s->buf[1];
+  switch (s->pointer) {
+  case MAX31790_REG_FAN_CONFIG(0):
+  case MAX31790_REG_FAN_CONFIG(1):
+  case MAX31790_REG_FAN_CONFIG(2):
+  case MAX31790_REG_FAN_CONFIG(3):
+  case MAX31790_REG_FAN_CONFIG(4):
+  case MAX31790_REG_FAN_CONFIG(5):
+    break; /* handled by one byte write */
+  case MAX31790_REG_FAN_DYNAMICS(0):
+  case MAX31790_REG_FAN_DYNAMICS(1):
+  case MAX31790_REG_FAN_DYNAMICS(2):
+  case MAX31790_REG_FAN_DYNAMICS(3):
+  case MAX31790_REG_FAN_DYNAMICS(4):
+  case MAX31790_REG_FAN_DYNAMICS(5):
+    break; /* handled by one byte write */
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(1):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(2):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(3):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(4):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(5):
+    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0)) / 2;
+    max31790_pwm_write(s, index, value0 << 8 | value1);
+    break;
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(0):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(1):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(2):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(3):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(4):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(5):
+    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_MSB(0)) / 2;
+    s->tach_target[index] = (value0 << 8) | value1;
+    break;
+  default:
+    qemu_log_mask(LOG_UNIMP, "%s: write to register %d", __func__, s->pointer);
+    break;
+  }
+}
+
+static void max31790_write_1_byte(MAX31790State *s)
+{
+
+  size_t index = 0;
+  uint16_t pwm = 0;
+  const uint8_t value = s->buf[0];
+  switch (s->pointer) {
+  case MAX31790_REG_FAN_CONFIG(0):
+  case MAX31790_REG_FAN_CONFIG(1):
+  case MAX31790_REG_FAN_CONFIG(2):
+  case MAX31790_REG_FAN_CONFIG(3):
+  case MAX31790_REG_FAN_CONFIG(4):
+  case MAX31790_REG_FAN_CONFIG(5):
+    s->fan_config[s->pointer - MAX31790_REG_FAN_CONFIG(0)] = value;
+    break;
+  case MAX31790_REG_FAN_DYNAMICS(0):
+  case MAX31790_REG_FAN_DYNAMICS(1):
+  case MAX31790_REG_FAN_DYNAMICS(2):
+  case MAX31790_REG_FAN_DYNAMICS(3):
+  case MAX31790_REG_FAN_DYNAMICS(4):
+  case MAX31790_REG_FAN_DYNAMICS(5):
+    s->fan_dynamics[s->pointer - MAX31790_REG_FAN_DYNAMICS(0)] = value;
+    break;
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(1):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(2):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(3):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(4):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(5):
+    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0)) / 2;
+    pwm = (value << 8) | (s->pwm[index] & 0x00ff);
+    max31790_pwm_write(s, index, pwm);
+    break;
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(0):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(1):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(2):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(3):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(4):
+  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(5):
+    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(0)) / 2;
+    pwm = (s->pwm[index] & 0xff00) | (value & 0x00ff);
+    max31790_pwm_write(s, index, pwm);
+    break;
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(0):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(1):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(2):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(3):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(4):
+  case MAX31790_REG_TACH_TARGET_COUNT_MSB(5):
+    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_MSB(0)) / 2;
+    s->tach_target[index] = (s->tach_target[index] & 0x00ff) | (value << 8);
+    break;
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(0):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(1):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(2):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(3):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(4):
+  case MAX31790_REG_TACH_TARGET_COUNT_LSB(5):
+    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_LSB(0)) / 2;
+    s->tach_target[index] = (s->tach_target[index] & 0xff00) | value;
+    break;
+  default:
+    qemu_log_mask(LOG_UNIMP, "%s: write to register %d", __func__, s->pointer);
+    break;
+  }
+}
+
+static int max31790_send(I2CSlave *i2c, uint8_t data)
+{
+  MAX31790State *s = MAX31790(i2c);
+
+  trace_max31790_send(s->i2c.address, data);
+
+  if (s->len == 0) {
+    /* first byte is the register pointer for a read / write operation */
+    s->pointer = data;
+    s->len++;
+    return 0;
+  }
+
+  if (s->len > 2) {
+    qemu_log_mask(LOG_GUEST_ERROR, "%s: write too many bytes", __func__);
+    return 1; /* NAK */
+  }
+
+  /* second / third byte is the data to write */
+  s->buf[s->len - 1] = data;
+  s->len++;
+
+  if (s->len == 2) {
+    max31790_write_1_byte(s);
+  } else if (s->len == 3) {
+    max31790_write_2_byte(s);
+  }
+
+  return 0;
+}
+
+static uint8_t max31790_recv(I2CSlave *i2c)
+{
+  MAX31790State *s = MAX31790(i2c);
+  trace_max31790_recv(s->i2c.address, s->pointer);
+
+  max31790_read(s);
+  s->len = 0;
+
+  if (s->outlen >= 2) {
+    /* error */
+    s->outlen = 0;
+  }
+
+  const uint8_t data = s->outbuf[s->outlen++];
+
+  trace_max31790_recv_return(s->i2c.address, data);
+  return data;
+}
+
+static int max31790_event(I2CSlave *i2c, enum i2c_event event)
+{
+  MAX31790State *s = MAX31790(i2c);
+
+  trace_max31790_event(s->i2c.address, event);
+
+  switch (event) {
+  case I2C_START_RECV:
+    s->outlen = 0;
+    break;
+  case I2C_START_SEND:
+    s->len = 0;
+    break;
+  default:
+    break;
+  }
+
+  return 0;
+}
+
+static const VMStateDescription vmstate_max31790 = {
+    .name = TYPE_MAX31790,
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (const VMStateField[]){
+        VMSTATE_UINT8(len, MAX31790State),
+        VMSTATE_UINT8_ARRAY(fan_config, MAX31790State, MAX31790_NUM_FANS),
+        VMSTATE_UINT8_ARRAY(fan_dynamics, MAX31790State, MAX31790_NUM_FANS),
+        VMSTATE_UINT16_ARRAY(pwm, MAX31790State, MAX31790_NUM_FANS),
+        VMSTATE_UINT16_ARRAY(tach_target, MAX31790State, MAX31790_NUM_FANS),
+        VMSTATE_UINT16_ARRAY(rpm, MAX31790State, MAX31790_NUM_TACHS),
+        VMSTATE_UINT8_ARRAY(buf, MAX31790State, 2),
+        VMSTATE_UINT8(outlen, MAX31790State),
+        VMSTATE_UINT8_ARRAY(outbuf, MAX31790State, 2),
+        VMSTATE_UINT8(pointer, MAX31790State),
+        VMSTATE_I2C_SLAVE(i2c, MAX31790State), VMSTATE_END_OF_LIST()}
+};
+
+static void max31790_init(Object *obj) { /* Nothing to do */ }
+
+static void max31790_reset(I2CSlave *i2c)
+{
+  MAX31790State *s = MAX31790(i2c);
+
+  for (int i = 0; i < MAX31790_NUM_FANS; i++) {
+    /* POR-State 0b 0XX0 0000 */
+    s->fan_config[i] = 0b00000000;
+
+    /* same as POR-State */
+    s->tach_target[i] = 0b0011110000000000;
+
+    /* same as POR-State */
+    s->fan_dynamics[i] = 0b01001100;
+
+    s->pwm[i] = 0;
+  }
+
+  for (int i = 0; i < MAX31790_NUM_TACHS; i++) {
+    max31790_set_rpm(s, i, 0x4444);
+  }
+}
+
+static void max31790_realize(DeviceState *dev, Error **errp)
+{
+  MAX31790State *s = MAX31790(dev);
+
+  trace_max31790_realize(s->i2c.address);
+
+  max31790_reset(&s->i2c);
+}
+
+static void max31790_class_init(ObjectClass *klass, const void *data)
+{
+  DeviceClass *dc = DEVICE_CLASS(klass);
+  I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
+
+  dc->realize = max31790_realize;
+  dc->desc = "Maxim MAX31790 6-Channel Fan Controller";
+  dc->vmsd = &vmstate_max31790;
+  k->event = max31790_event;
+  k->recv = max31790_recv;
+  k->send = max31790_send;
+}
+
+static const TypeInfo max31790_info = {
+    .name = TYPE_MAX31790,
+    .parent = TYPE_I2C_SLAVE,
+    .instance_size = sizeof(MAX31790State),
+    .class_size = sizeof(MAX31790Class),
+    .instance_init = max31790_init,
+    .class_init = max31790_class_init,
+};
+
+static void max31790_register_types(void)
+{
+  type_register_static(&max31790_info);
+}
+
+type_init(max31790_register_types)
diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
index 420fdc3359..4987c3b253 100644
--- a/hw/sensor/meson.build
+++ b/hw/sensor/meson.build
@@ -8,3 +8,4 @@ system_ss.add(when: 'CONFIG_MAX34451', if_true: files('max34451.c'))
 system_ss.add(when: 'CONFIG_LSM303DLHC_MAG', if_true: files('lsm303dlhc_mag.c'))
 system_ss.add(when: 'CONFIG_ISL_PMBUS_VR', if_true: files('isl_pmbus_vr.c'))
 system_ss.add(when: 'CONFIG_MAX31785', if_true: files('max31785.c'))
+system_ss.add(when: 'CONFIG_MAX31790', if_true: files('max31790.c'))
diff --git a/hw/sensor/trace-events b/hw/sensor/trace-events
index a3fe54fa6d..c15c0a7e93 100644
--- a/hw/sensor/trace-events
+++ b/hw/sensor/trace-events
@@ -4,3 +4,11 @@
 tmp105_read(uint8_t dev, uint8_t addr) "device: 0x%02x, addr: 0x%02x"
 tmp105_write(uint8_t dev, uint8_t addr) "device: 0x%02x, addr 0x%02x"
 tmp105_write_shutdown(uint8_t dev) "device: 0x%02x"
+
+# max31790.c
+max31790_send(uint8_t i2c_addr, uint8_t send) "i2c_addr: 0x%02x, data: 0x%02x"
+max31790_recv(uint8_t i2c_addr, uint8_t reg_addr) "i2c_addr: 0x%02x, reg_addr: 0x%02x"
+max31790_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
+max31790_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
+max31790_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
+max31790_pwm_write(uint8_t i2c_addr, size_t index, uint16_t value) "i2c_addr: 0x%02x, index: %zu, value: 0x%04x"
diff --git a/include/hw/sensor/max31790.h b/include/hw/sensor/max31790.h
new file mode 100644
index 0000000000..7ead420926
--- /dev/null
+++ b/include/hw/sensor/max31790.h
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef QEMU_MAX31790_H
+#define QEMU_MAX31790_H
+
+#define TYPE_MAX31790 "max31790"
+
+#endif
diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
index a3124c240f..d29423add7 100755
--- a/tests/functional/arm/test_aspeed_fby4.py
+++ b/tests/functional/arm/test_aspeed_fby4.py
@@ -7,6 +7,8 @@
 from qemu_test import Asset
 from aspeed import AspeedTest
 
+from qemu_test import wait_for_console_pattern, exec_command
+from qemu_test import exec_command_and_wait_for_pattern
 
 class YosemiteV4Machine(AspeedTest):
 
@@ -33,6 +35,22 @@ def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
         # different from the other machines.
         self.wait_for_console_pattern('yosemite4 login:')
 
+        # perform login
+        exec_command_and_wait_for_pattern(self,
+                                          "root", "Password:");
+
+        exec_command_and_wait_for_pattern(self, "0penBmc", "#");
+
+        # MAX31790 test
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/class/hwmon/hwmon2/name", "max31790");
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/class/hwmon/hwmon2/fan1_input", "4530");
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/class/hwmon/hwmon2/fan1_enable", "1");
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/class/hwmon/hwmon2/fan1_fault", "0");
+
     def test_arm_ast2600_yosemitev4_openbmc(self):
         image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
 
-- 
2.54.0



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

* [PATCH v3 4/5] hw/sensor: support MAX11615
  2026-05-12 10:20 [PATCH v3 0/5] initial support for yosemite v4 Alexander Hansen
                   ` (2 preceding siblings ...)
  2026-05-12 10:20 ` [PATCH v3 3/5] hw/sensor: MAX31790 support Alexander Hansen
@ 2026-05-12 10:20 ` Alexander Hansen
  2026-05-12 16:55   ` Cédric Le Goater
  2026-05-12 16:58   ` Cédric Le Goater
  2026-05-12 10:20 ` [PATCH v3 5/5] hw/sensor: support Texas Instruments ADC128D818 Alexander Hansen
  4 siblings, 2 replies; 12+ messages in thread
From: Alexander Hansen @ 2026-05-12 10:20 UTC (permalink / raw)
  To: qemu-devel
  Cc: Alexander Hansen, Titus Rwantare, Cédric Le Goater,
	Philippe Mathieu-Daudé, Paolo Bonzini, Peter Maydell,
	qemu-arm, Steven Lee, Troy Lee, Jamin Lin, Kane Chen,
	Andrew Jeffery, Joel Stanley

Product: [1]
Datasheet: [2]

Sensor readings can be provided upon creation of the device. In case no
readings are provided the ADC reads a pre-defined arbitrary value.

root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/name
max11615
root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/in_voltage0_raw
1922
root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/in_voltage_scale
0.500000000

trace:
less /tmp/qemu-trace.log | grep -i max116
max11615_realize i2c_addr: 0x33
max11615_realize i2c_addr: 0x33
max11615_event i2c_addr: 0x33, event: 0x01
max11615_write_setup i2c_addr: 0x33, data: 0xd2
max11615_write_config i2c_addr: 0x33, data: 0x0f
max11615_event i2c_addr: 0x33, event: 0x03
max11615_event i2c_addr: 0x33, event: 0x01
max11615_write_setup i2c_addr: 0x33, data: 0xd2
max11615_write_config i2c_addr: 0x33, data: 0x0f
max11615_event i2c_addr: 0x33, event: 0x03
max11615_event i2c_addr: 0x33, event: 0x01
max11615_write_setup i2c_addr: 0x33, data: 0xd2
max11615_write_config i2c_addr: 0x33, data: 0x61
max11615_event i2c_addr: 0x33, event: 0x03
max11615_event i2c_addr: 0x33, event: 0x00
max11615_recv i2c_addr: 0x33, reg_addr: 0x00
max11615_recv_return i2c_addr: 0x33, returns: 0xfa
max11615_recv i2c_addr: 0x33, reg_addr: 0x00
max11615_recv_return i2c_addr: 0x33, returns: 0xd2
max11615_event i2c_addr: 0x33, event: 0x04
max11615_event i2c_addr: 0x33, event: 0x03

References:
[1] https://www.analog.com/en/products/MAX11615.html
[2] https://www.analog.com/media/en/technical-documentation/data-sheets/MAX11612-MAX11617.pdf

Cc: Titus Rwantare <titusr@google.com>
Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
Cc: "Philippe Mathieu-Daudé" <philmd@linaro.org> (odd fixer:Overall sensors)
Cc: Paolo Bonzini <pbonzini@redhat.com> (maintainer:Kconfig)
Cc: Peter Maydell <peter.maydell@linaro.org> (supporter:ARM TCG CPUs)
Cc: qemu-devel@nongnu.org (open list:All patches CC here)
Cc: qemu-arm@nongnu.org (open list:ARM TCG CPUs)
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
---
 MAINTAINERS                              |   1 +
 hw/arm/Kconfig                           |   1 +
 hw/arm/aspeed_ast2600_fby4.c             |   8 +-
 hw/sensor/Kconfig                        |   4 +
 hw/sensor/max11615.c                     | 202 +++++++++++++++++++++++
 hw/sensor/meson.build                    |   1 +
 hw/sensor/trace-events                   |   8 +
 include/hw/sensor/max11615.h             |  20 +++
 tests/functional/arm/test_aspeed_fby4.py |   8 +
 9 files changed, 252 insertions(+), 1 deletion(-)
 create mode 100644 hw/sensor/max11615.c
 create mode 100644 include/hw/sensor/max11615.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 9c991f8e70..a9c88996a2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3981,6 +3981,7 @@ S: Maintained
 F: hw/i2c/pmbus_device.c
 F: hw/sensor/adm1272.c
 F: hw/sensor/isl_pmbus_vr.c
+F: hw/sensor/max11615.c
 F: hw/sensor/max31790.c
 F: hw/sensor/max34451.c
 F: include/hw/i2c/pmbus_device.h
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 99864eb878..76a7d327a9 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -553,6 +553,7 @@ config ASPEED_SOC
     select PMBUS
     select MAX31785
     select MAX31790
+    select MAX11615
     select FSI_APB2OPB_ASPEED
     select AT24C
     select PCI_EXPRESS
diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
index a7c2dc09ea..036f5b9e38 100644
--- a/hw/arm/aspeed_ast2600_fby4.c
+++ b/hw/arm/aspeed_ast2600_fby4.c
@@ -12,6 +12,7 @@
 #include "hw/arm/aspeed_soc.h"
 #include "hw/nvram/eeprom_at24c.h"
 #include "hw/sensor/max31790.h"
+#include "hw/sensor/max11615.h"
 #include "hw/i2c/i2c_mux_pca954x.h"
 #include "hw/gpio/pca9552.h"
 
@@ -186,7 +187,12 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
         i2c_slave_create_simple(bus, TYPE_MAX31790, 0x2f);
 
         /* maxim,max11615 @ 0x33   (adc) */
-        /* TODO */
+        static const uint16_t adc_values[8] = {
+            0b011110000010, 0b010100011000,
+            0b001000110100, 0b100000101001,
+            0b011110000010, 0b010100011000,
+            0b001000110100, 0b100000101001};
+        max11615_init_with_values(bus, 0x33, adc_values, 8);
 
         at24c_eeprom_init_rom(
             bus, 0x52, eepromSize,
diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
index ece2f2b167..84eede9d84 100644
--- a/hw/sensor/Kconfig
+++ b/hw/sensor/Kconfig
@@ -47,3 +47,7 @@ config MAX31785
 config MAX31790
     bool
     depends on PMBUS
+
+config MAX11615
+    bool
+    depends on I2C
diff --git a/hw/sensor/max11615.c b/hw/sensor/max11615.c
new file mode 100644
index 0000000000..7950e00e33
--- /dev/null
+++ b/hw/sensor/max11615.c
@@ -0,0 +1,202 @@
+/*
+ * Maxim MAX11615 Low-Power 12 bit ADC
+ * Models MAX11612,MAX11613,MAX11614,MAX11615,MAX11616,MAX11617
+ *
+ * Datasheet:
+ * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX11612-MAX11617.pdf
+ *
+ * Copyright 2026 9elements
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/i2c/i2c.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "trace.h"
+#include "hw/sensor/max11615.h"
+
+#define MAX11615_NUM_CHANNELS 8
+
+struct MAX11615State {
+    I2CSlave i2c;
+
+    uint16_t channels[MAX11615_NUM_CHANNELS];
+
+    /* output buffer */
+    uint8_t outlen;
+    uint8_t outbuf[2];
+
+    /* selected channel for read/write operation */
+    uint8_t pointer;
+};
+
+struct MAX11615Class {
+    I2CSlaveClass parent_class;
+};
+
+OBJECT_DECLARE_TYPE(MAX11615State, MAX11615Class, MAX11615)
+
+static void max11615_read(MAX11615State *s)
+{
+    /* read an ADC channel, first 4 bits must be high */
+    uint8_t msb = s->channels[s->pointer] >> 8;
+    uint8_t lsb = s->channels[s->pointer] & 0xff;
+    s->outbuf[0] = 0b11110000 | (msb & 0b00001111);
+    s->outbuf[1] = lsb;
+}
+
+static void max11615_write_config_byte(MAX11615State *s, uint8_t data)
+{
+    trace_max11615_write_config(s->i2c.address, data);
+
+    uint8_t channelSelect = (data >> 1) & 0b1111;
+
+    /* Table 3. Channel Selection (AIN0 ... AIN11) */
+    if (channelSelect > 11) {
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: invalid channel select", __func__);
+        channelSelect = 11;
+    }
+    s->pointer = channelSelect;
+}
+
+static void max11615_write_setup_byte(MAX11615State *s, uint8_t data)
+{
+    trace_max11615_write_setup(s->i2c.address, data);
+    /* we ignore the setup byte, not implemented */
+}
+
+static int max11615_send(I2CSlave *i2c, uint8_t data)
+{
+    MAX11615State *s = MAX11615(i2c);
+    const uint8_t msb = (data >> 7) & 0b1;
+
+    if (msb) {
+        max11615_write_setup_byte(s, data);
+    } else {
+        max11615_write_config_byte(s, data);
+    }
+
+    s->outlen = 0;
+    return 0;
+}
+
+static uint8_t max11615_recv(I2CSlave *i2c)
+{
+    MAX11615State *s = MAX11615(i2c);
+    trace_max11615_recv(s->i2c.address, s->pointer);
+
+    max11615_read(s);
+
+    if (s->outlen >= 2) {
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: too many bytes read", __func__);
+        s->outlen = 0;
+    }
+
+    const uint8_t data =  s->outbuf[s->outlen++];
+
+    trace_max11615_recv_return(s->i2c.address, data);
+    return data;
+}
+
+static int max11615_event(I2CSlave *i2c, enum i2c_event event)
+{
+    MAX11615State *s = MAX11615(i2c);
+
+    trace_max11615_event(s->i2c.address, event);
+
+    switch (event) {
+    case I2C_START_RECV:
+        s->outlen = 0;
+        break;
+    default:
+        break;
+    }
+
+    return 0;
+}
+
+static const VMStateDescription vmstate_max11615 = {
+    .name = TYPE_MAX11615,
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (const VMStateField[]){
+        VMSTATE_UINT16_ARRAY(channels, MAX11615State, MAX11615_NUM_CHANNELS),
+        VMSTATE_UINT8(outlen, MAX11615State),
+        VMSTATE_UINT8_ARRAY(outbuf, MAX11615State, 2),
+        VMSTATE_UINT8(pointer, MAX11615State),
+        VMSTATE_I2C_SLAVE(i2c, MAX11615State),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void max11615_init(Object *obj)
+{
+    /* Nothing to do */
+}
+
+I2CSlave *max11615_init_with_values(I2CBus *bus, uint8_t address,
+    const uint16_t *init_values, uint32_t init_values_size)
+{
+    MAX11615State *s;
+
+    s = MAX11615(i2c_slave_new(TYPE_MAX11615, address));
+
+    for (int i = 0; i < MAX11615_NUM_CHANNELS && i < init_values_size; i++) {
+
+        /* arbitrary value */
+        uint16_t value = 0b0000101011010010;
+
+        if (i < init_values_size) {
+            value = init_values[i];
+        }
+        s->channels[i] = value;
+    }
+
+    i2c_slave_realize_and_unref(I2C_SLAVE(s), bus, &error_abort);
+
+    return I2C_SLAVE(s);
+}
+
+static void max11615_realize(DeviceState *dev, Error **errp)
+{
+    MAX11615State *s = MAX11615(dev);
+
+    trace_max11615_realize(s->i2c.address);
+
+    s->pointer = 0;
+    s->outlen = 0;
+}
+
+static void max11615_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
+
+    dc->realize = max11615_realize;
+    dc->desc = "Maxim MAX11615 12-bit ADC";
+    dc->vmsd = &vmstate_max11615;
+    k->event = max11615_event;
+    k->recv = max11615_recv;
+    k->send = max11615_send;
+}
+
+static const TypeInfo max31790_info = {
+    .name = TYPE_MAX11615,
+    .parent = TYPE_I2C_SLAVE,
+    .instance_size = sizeof(MAX11615State),
+    .class_size = sizeof(MAX11615Class),
+    .instance_init = max11615_init,
+    .class_init = max11615_class_init,
+};
+
+static void max31790_register_types(void)
+{
+    type_register_static(&max31790_info);
+}
+
+type_init(max31790_register_types)
diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
index 4987c3b253..a1e26604fa 100644
--- a/hw/sensor/meson.build
+++ b/hw/sensor/meson.build
@@ -9,3 +9,4 @@ system_ss.add(when: 'CONFIG_LSM303DLHC_MAG', if_true: files('lsm303dlhc_mag.c'))
 system_ss.add(when: 'CONFIG_ISL_PMBUS_VR', if_true: files('isl_pmbus_vr.c'))
 system_ss.add(when: 'CONFIG_MAX31785', if_true: files('max31785.c'))
 system_ss.add(when: 'CONFIG_MAX31790', if_true: files('max31790.c'))
+system_ss.add(when: 'CONFIG_MAX11615', if_true: files('max11615.c'))
diff --git a/hw/sensor/trace-events b/hw/sensor/trace-events
index c15c0a7e93..3fed979e85 100644
--- a/hw/sensor/trace-events
+++ b/hw/sensor/trace-events
@@ -12,3 +12,11 @@ max31790_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns:
 max31790_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
 max31790_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
 max31790_pwm_write(uint8_t i2c_addr, size_t index, uint16_t value) "i2c_addr: 0x%02x, index: %zu, value: 0x%04x"
+
+# max11615.c
+max11615_write_setup(uint8_t i2c_addr, uint8_t send) "i2c_addr: 0x%02x, data: 0x%02x"
+max11615_write_config(uint8_t i2c_addr, uint8_t send) "i2c_addr: 0x%02x, data: 0x%02x"
+max11615_recv(uint8_t i2c_addr, uint8_t reg_addr) "i2c_addr: 0x%02x, reg_addr: 0x%02x"
+max11615_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
+max11615_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
+max11615_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
diff --git a/include/hw/sensor/max11615.h b/include/hw/sensor/max11615.h
new file mode 100644
index 0000000000..ccc0c9bc2e
--- /dev/null
+++ b/include/hw/sensor/max11615.h
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef QEMU_MAX11615_H
+#define QEMU_MAX11615_H
+
+#include <stdint.h>
+#include "hw/i2c/i2c.h"
+
+#define TYPE_MAX11615 "max11615"
+
+/*
+ * Create and realize a MAX11615 ADC with constant caller-supplied readings
+ * @bus: I2C bus to put it on
+ * @address: I2C address
+ * @init_values: array of readings for each ADC channel
+ * @init_values_size: Size of @init_values, can be less than the number of channels
+ */
+I2CSlave *max11615_init_with_values(I2CBus *bus, uint8_t address,
+                                    const uint16_t *init_values, uint32_t init_values_size);
+
+#endif
diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
index d29423add7..319f1a7672 100755
--- a/tests/functional/arm/test_aspeed_fby4.py
+++ b/tests/functional/arm/test_aspeed_fby4.py
@@ -51,6 +51,14 @@ def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
         exec_command_and_wait_for_pattern(self,
             "cat /sys/class/hwmon/hwmon2/fan1_fault", "0");
 
+        # MAX11615 test
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/bus/iio/devices/iio:device2/name", "max11615");
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/bus/iio/devices/iio:device2/in_voltage0_raw", "1922");
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/bus/iio/devices/iio:device2/in_voltage_scale", "0.500000000");
+
     def test_arm_ast2600_yosemitev4_openbmc(self):
         image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
 
-- 
2.54.0



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

* [PATCH v3 5/5] hw/sensor: support Texas Instruments ADC128D818
  2026-05-12 10:20 [PATCH v3 0/5] initial support for yosemite v4 Alexander Hansen
                   ` (3 preceding siblings ...)
  2026-05-12 10:20 ` [PATCH v3 4/5] hw/sensor: support MAX11615 Alexander Hansen
@ 2026-05-12 10:20 ` Alexander Hansen
  2026-05-12 16:57   ` Cédric Le Goater
  4 siblings, 1 reply; 12+ messages in thread
From: Alexander Hansen @ 2026-05-12 10:20 UTC (permalink / raw)
  To: qemu-devel
  Cc: Alexander Hansen, Titus Rwantare, Cédric Le Goater,
	Paolo Bonzini, Peter Maydell, Philippe Mathieu-Daudé,
	qemu-arm, Steven Lee, Troy Lee, Jamin Lin, Kane Chen,
	Andrew Jeffery, Joel Stanley

Product: [1]
Datasheet: [2]

ADC128D818 Support:
- channel readings from pre-set values
- driver can read and write most configuration registers

ADC128D818 currently unsupported:
- slave address restriction
- startup sequence and realistic busy register emulation
- external VREF
- conversion rate
- interrupts
- deep shutdown mode
- individual channel shutdown
- selection between Mode 0,1,2,3
- pseudo-differential input

Anyone could expand it in the future for more accurate emulation.

The reason for adding this device is to support Yosemite V4 emulation.

Tested: on yosemite v4 qemu

initialize the device:
// ti,adc128d818 @ 0x1f    (adc)
// the driver will throw away the last 4 bits, set them 0
uint16_t adc_values1[8] = {
    0b011110000000, 0b010100010000,
    0b001000110000, 0b100000100000,
    0b011110000000, 0b010100010000,
    0b001000110000, 0b100000100000};
adc128d818_init_with_values(bus, 0x1f, adc_values1, 8);

Trace outputs directly after initialization:
adc128d818_realize i2c_addr: 0x1f
adc128d818_realize i2c_addr: 0x1f
adc128d818_event i2c_addr: 0x1f, event: 0x01
adc128d818_send i2c_addr: 0x1f, data: 0x00
adc128d818_send i2c_addr: 0x1f, data: 0x80
adc128d818_write i2c_addr: 0x1f, reg: 0x00 data: 0x80
adc128d818_event i2c_addr: 0x1f, event: 0x03
adc128d818_event i2c_addr: 0x1f, event: 0x01
...

read the values
root@yosemite4:/sys/bus/i2c/devices/30-001f# cat hwmon/hwmon0/in0_min
0
root@yosemite4:/sys/bus/i2c/devices/30-001f# cat hwmon/hwmon0/in0_max
0
root@yosemite4:/sys/bus/i2c/devices/30-001f# cat hwmon/hwmon0/in0_input
75
root@yosemite4:/sys/bus/i2c/devices/30-001f# cat hwmon/hwmon0/name
adc128d818

We initially configured 0b011110000000 for the first channel.
The driver throws away the last 4 bits and does calculation similar to
below:

val = DIV_ROUND_CLOSEST(data->in[index][nr] * data->vref, 4095);

We can check that the calculation is as expected given our configured
value.

((0b011110000000 >> 4) * 2560) / 4095
75.01831501831502

References:
[1] https://www.ti.com/product/ADC128D818
[2] https://www.ti.com/lit/gpn/adc128d818

Cc: Titus Rwantare <titusr@google.com>
Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
Cc: Paolo Bonzini <pbonzini@redhat.com> (maintainer:Kconfig)
Cc: Peter Maydell <peter.maydell@linaro.org> (supporter:ARM TCG CPUs)
Cc: "Philippe Mathieu-Daudé" <philmd@linaro.org> (odd fixer:Overall sensors)
Cc: qemu-arm@nongnu.org (open list:ARM TCG CPUs)
Cc: qemu-devel@nongnu.org (open list:All patches CC here)
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
---
 MAINTAINERS                              |   1 +
 hw/arm/Kconfig                           |   1 +
 hw/arm/aspeed_ast2600_fby4.c             |   9 +-
 hw/sensor/Kconfig                        |   4 +
 hw/sensor/adc128d818.c                   | 414 +++++++++++++++++++++++
 hw/sensor/meson.build                    |   1 +
 hw/sensor/trace-events                   |   8 +
 include/hw/sensor/adc128d818.h           |  20 ++
 tests/functional/arm/test_aspeed_fby4.py |  10 +
 9 files changed, 467 insertions(+), 1 deletion(-)
 create mode 100644 hw/sensor/adc128d818.c
 create mode 100644 include/hw/sensor/adc128d818.h

diff --git a/MAINTAINERS b/MAINTAINERS
index a9c88996a2..43831b67c4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3979,6 +3979,7 @@ PMBus
 M: Titus Rwantare <titusr@google.com>
 S: Maintained
 F: hw/i2c/pmbus_device.c
+F: hw/sensor/adc128d818.c
 F: hw/sensor/adm1272.c
 F: hw/sensor/isl_pmbus_vr.c
 F: hw/sensor/max11615.c
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 76a7d327a9..cc89c65e3f 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -554,6 +554,7 @@ config ASPEED_SOC
     select MAX31785
     select MAX31790
     select MAX11615
+    select ADC128D818
     select FSI_APB2OPB_ASPEED
     select AT24C
     select PCI_EXPRESS
diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
index 036f5b9e38..4fd3a713e2 100644
--- a/hw/arm/aspeed_ast2600_fby4.c
+++ b/hw/arm/aspeed_ast2600_fby4.c
@@ -13,6 +13,7 @@
 #include "hw/nvram/eeprom_at24c.h"
 #include "hw/sensor/max31790.h"
 #include "hw/sensor/max11615.h"
+#include "hw/sensor/adc128d818.h"
 #include "hw/i2c/i2c_mux_pca954x.h"
 #include "hw/gpio/pca9552.h"
 
@@ -163,7 +164,13 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
         I2CBus *bus = pca954x_i2c_get_bus(fan_mux, i);
 
         /* ti,adc128d818 @ 0x1f    (adc) */
-        /* TODO */
+        /* the driver will throw away the last 4 bits, set them 0 */
+        static const uint16_t adc_values1[8] = {
+            0b011110000000, 0b010100010000,
+            0b001000110000, 0b100000100000,
+            0b011110000000, 0b010100010000,
+            0b001000110000, 0b100000100000};
+        adc128d818_init_with_values(bus, 0x1f, adc_values1, 8);
 
         /* maxim,max31790 @ 0x20   (pwm) */
         i2c_slave_create_simple(bus, TYPE_MAX31790, 0x20);
diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
index 84eede9d84..c9190ff780 100644
--- a/hw/sensor/Kconfig
+++ b/hw/sensor/Kconfig
@@ -51,3 +51,7 @@ config MAX31790
 config MAX11615
     bool
     depends on I2C
+
+config ADC128D818
+    bool
+    depends on I2C
diff --git a/hw/sensor/adc128d818.c b/hw/sensor/adc128d818.c
new file mode 100644
index 0000000000..83a4d43846
--- /dev/null
+++ b/hw/sensor/adc128d818.c
@@ -0,0 +1,414 @@
+/*
+ * Texas Instruments ADC128D818 12 bit ADC with temperature sensor
+ * Models ADC128D818
+ *
+ * Product: https://www.ti.com/product/ADC128D818
+ * Datasheet: https://www.ti.com/lit/gpn/adc128d818
+ *
+ * Copyright 2026 9elements
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/i2c/i2c.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "trace.h"
+#include "hw/sensor/adc128d818.h"
+
+/* 8 bit, r/w */
+#define REG_CONFIG 0x00
+
+/* 8 bit, readonly */
+#define REG_INTERRUPT_STATUS 0x01
+
+/* 8 bit, r/w */
+#define REG_INTERRUPT_MASK 0x03
+
+/* 8 bit, r/w */
+#define REG_CONVERSION_RATE 0x07
+
+/* 8 bit, r/w */
+#define REG_CHANNEL_DISABLE 0x08
+
+/* 8 bit, write-only */
+#define REG_ONE_SHOT 0x09
+
+/* 8 bit, r/w */
+#define REG_DEEP_SHUTDOWN 0x0a
+
+/* 8 bit, r/w */
+#define REG_ADVANCED_CONFIG 0x0b
+
+/* 8 bit, readonly */
+#define REG_BUSY_STATUS 0x0c
+
+/* 16 bit registers, N = 0..7, readonly */
+#define REG_CHANNEL_READING(N) (0x20 + N)
+
+/* 8 bit registers N = 0..15, r/w */
+#define REG_LIMIT(N) (0x2a + N)
+
+/* 8 bit register, readonly */
+#define REG_MANUFACTURER_ID 0x3e
+
+/* 8 bit register, readonly */
+#define REG_REVISION_ID 0x3f
+
+#define ADC128D818_NUM_CHANNELS 8
+
+struct ADC128D818State {
+    I2CSlave i2c;
+
+    uint8_t config;
+    uint8_t interrupt_mask;
+    uint8_t conversion_rate;
+    uint8_t channel_disable;
+    bool deep_shutdown;
+    uint8_t advanced_config;
+
+    /* channel reading registers, 2 bytes each */
+    uint16_t channels[ADC128D818_NUM_CHANNELS];
+
+    /* high and low limit registers 0x2a - 0x39, one byte each */
+    uint8_t limit[ADC128D818_NUM_CHANNELS * 2];
+
+    /* input buffer */
+    uint8_t len;
+    uint8_t buf[2];
+
+    /* output buffer */
+    uint8_t outlen;
+    uint8_t outbuf[2];
+
+    /* selected channel for read/write operation */
+    uint8_t pointer;
+};
+
+struct ADC128D818Class {
+    I2CSlaveClass parent_class;
+};
+
+OBJECT_DECLARE_TYPE(ADC128D818State, ADC128D818Class, ADC128D818)
+
+static void adc128d818_read(ADC128D818State *s)
+{
+    uint8_t ch_num = 0;
+    switch (s->pointer) {
+    case REG_CONFIG:
+        s->outbuf[0] = s->config;
+        break;
+    case REG_INTERRUPT_STATUS:
+        s->outbuf[0] = 0x0; /* POR State */
+        break;
+    case REG_INTERRUPT_MASK:
+        s->outbuf[0] = s->interrupt_mask;
+        break;
+    case REG_CONVERSION_RATE:
+        s->outbuf[0] = s->conversion_rate;
+        break;
+    case REG_CHANNEL_DISABLE:
+        s->outbuf[0] = s->channel_disable;
+        break;
+    case REG_ONE_SHOT:
+        /* not marked as readable */
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: read of register 0x%02x\n",
+            __func__, s->pointer);
+        s->outbuf[0] = 0x0;
+        break;
+    case REG_DEEP_SHUTDOWN:
+        s->outbuf[0] = s->deep_shutdown ? 0x1 : 0x0;
+        break;
+    case REG_ADVANCED_CONFIG:
+        s->outbuf[0] = s->advanced_config & 0b111;
+        break;
+    case REG_BUSY_STATUS:
+        /* not implemented */
+        s->outbuf[0] = 0b00000010; /* POR State */
+        break;
+    case REG_CHANNEL_READING(0):
+    case REG_CHANNEL_READING(1):
+    case REG_CHANNEL_READING(2):
+    case REG_CHANNEL_READING(3):
+    case REG_CHANNEL_READING(4):
+    case REG_CHANNEL_READING(5):
+    case REG_CHANNEL_READING(6):
+    case REG_CHANNEL_READING(7):
+        ch_num = s->pointer - REG_CHANNEL_READING(0);
+        /* high byte comes first, driver reads swapped */
+        s->outbuf[0] = (s->channels[ch_num] >> 8) & 0xff;
+        s->outbuf[1] = s->channels[ch_num] & 0xff;
+        break;
+    case REG_LIMIT(0):
+    case REG_LIMIT(1):
+    case REG_LIMIT(2):
+    case REG_LIMIT(3):
+    case REG_LIMIT(4):
+    case REG_LIMIT(5):
+    case REG_LIMIT(6):
+    case REG_LIMIT(7):
+    case REG_LIMIT(8):
+    case REG_LIMIT(9):
+    case REG_LIMIT(10):
+    case REG_LIMIT(11):
+    case REG_LIMIT(12):
+    case REG_LIMIT(13):
+    case REG_LIMIT(14):
+    case REG_LIMIT(15):
+        s->outbuf[0] = s->limit[s->pointer - REG_LIMIT(0)];
+        break;
+    case REG_MANUFACTURER_ID:
+        s->outbuf[0] = 0x1; /* readonly */
+        break;
+    case REG_REVISION_ID:
+        s->outbuf[0] = 0b00001001; /* readonly */
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: read of register 0x%02x\n",
+            __func__, s->pointer);
+        break;
+    }
+}
+
+static void adc128d818_write_advanced_config(ADC128D818State *s, uint8_t data)
+{
+    /*
+     * Note: Whenever the Advanced Configuration Register is programmed,
+     * all of the values in the Channel Reading Registers and
+     * Interrupt Status Registers will return to their default values.
+     */
+
+    s->advanced_config = (data & 0b111);
+}
+
+static void adc128d818_write(ADC128D818State *s, uint8_t data)
+{
+    trace_adc128d818_write(s->i2c.address, s->pointer, data);
+
+    /* which bits in config register are writable */
+    const uint8_t config_w_mask = 0b10001011;
+    const uint8_t config_ro_mask = (uint8_t)~config_w_mask;
+
+    switch (s->pointer) {
+    case REG_CONFIG:
+        s->config = (s->config & config_ro_mask) | (data & config_w_mask);
+        break;
+    case REG_INTERRUPT_MASK:
+        s->interrupt_mask = data;
+        break;
+    case REG_CONVERSION_RATE:
+        s->conversion_rate = data;
+        break;
+    case REG_CHANNEL_DISABLE:
+        s->channel_disable = data;
+        break;
+    case REG_ONE_SHOT:
+        /*
+         * Initiate a single conversion and comparison cycle when
+         * the device is in shutdown mode or deep shutdown mode, after
+         * which the device returns to the respective mode that it was in
+         *
+         */
+        break;
+    case REG_DEEP_SHUTDOWN:
+        s->deep_shutdown = (data & 0x1) != 0;
+        break;
+    case REG_ADVANCED_CONFIG:
+        adc128d818_write_advanced_config(s, data);
+        break;
+    case REG_LIMIT(0):
+    case REG_LIMIT(1):
+    case REG_LIMIT(2):
+    case REG_LIMIT(3):
+    case REG_LIMIT(4):
+    case REG_LIMIT(5):
+    case REG_LIMIT(6):
+    case REG_LIMIT(7):
+    case REG_LIMIT(8):
+    case REG_LIMIT(9):
+    case REG_LIMIT(10):
+    case REG_LIMIT(11):
+    case REG_LIMIT(12):
+    case REG_LIMIT(13):
+    case REG_LIMIT(14):
+    case REG_LIMIT(15):
+        s->limit[s->pointer - REG_LIMIT(0)] = data;
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: write of register 0x%02x\n",
+            __func__, s->pointer);
+        break;
+    }
+}
+
+static int adc128d818_send(I2CSlave *i2c, uint8_t data)
+{
+    ADC128D818State *s = ADC128D818(i2c);
+    trace_adc128d818_send(s->i2c.address, data);
+
+    s->outlen = 0;
+    s->buf[s->len] = data;
+
+    if (s->len == 0) {
+        s->pointer = data;
+    } else if (s->len == 1) {
+        adc128d818_write(s, data);
+    }
+
+    s->len++;
+    return 0;
+}
+
+static uint8_t adc128d818_recv(I2CSlave *i2c)
+{
+    ADC128D818State *s = ADC128D818(i2c);
+    trace_adc128d818_recv(s->i2c.address, s->pointer);
+
+    adc128d818_read(s);
+
+    if (s->outlen >= 2) {
+        qemu_log_mask(LOG_GUEST_ERROR, "%s: too many bytes read\n", __func__);
+        s->outlen = 0;
+    }
+
+    const uint8_t data =  s->outbuf[s->outlen++];
+
+    trace_adc128d818_recv_return(s->i2c.address, data);
+    return data;
+}
+
+static int adc128d818_event(I2CSlave *i2c, enum i2c_event event)
+{
+    ADC128D818State *s = ADC128D818(i2c);
+
+    trace_adc128d818_event(s->i2c.address, event);
+
+    switch (event) {
+    case I2C_START_RECV:
+        s->outlen = 0;
+        break;
+    case I2C_START_SEND:
+        s->len = 0;
+        break;
+    default:
+        break;
+    }
+
+    return 0;
+}
+
+static const VMStateDescription vmstate_adc128d818 = {
+    .name = TYPE_ADC128D818,
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields = (const VMStateField[]){
+        VMSTATE_UINT8(config, ADC128D818State),
+        VMSTATE_UINT8(interrupt_mask, ADC128D818State),
+        VMSTATE_UINT8(conversion_rate, ADC128D818State),
+        VMSTATE_UINT8(channel_disable, ADC128D818State),
+        VMSTATE_BOOL(deep_shutdown, ADC128D818State),
+        VMSTATE_UINT8(advanced_config, ADC128D818State),
+        VMSTATE_UINT16_ARRAY(channels, ADC128D818State,
+            ADC128D818_NUM_CHANNELS),
+        VMSTATE_UINT8_ARRAY(limit, ADC128D818State,
+            ADC128D818_NUM_CHANNELS * 2),
+        VMSTATE_UINT8(len, ADC128D818State),
+        VMSTATE_UINT8_ARRAY(buf, ADC128D818State, 2),
+        VMSTATE_UINT8(outlen, ADC128D818State),
+        VMSTATE_UINT8_ARRAY(outbuf, ADC128D818State, 2),
+        VMSTATE_UINT8(pointer, ADC128D818State),
+        VMSTATE_I2C_SLAVE(i2c, ADC128D818State),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void adc128d818_init(Object *obj)
+{
+    /* Nothing to do */
+}
+
+I2CSlave *adc128d818_init_with_values(I2CBus *bus, uint8_t address,
+     const uint16_t *init_values, uint32_t init_values_size)
+{
+    ADC128D818State *s;
+
+    s = ADC128D818(i2c_slave_new(TYPE_ADC128D818, address));
+
+    for (int i = 0; i < ADC128D818_NUM_CHANNELS; i++) {
+
+        /* arbitrary value */
+        uint16_t value = 0b0000101011010010;
+
+        if (i < init_values_size) {
+            value = init_values[i];
+        }
+        s->channels[i] = value;
+    }
+
+    i2c_slave_realize_and_unref(I2C_SLAVE(s), bus, &error_abort);
+
+    return I2C_SLAVE(s);
+}
+
+static void adc128d818_reset(I2CSlave *i2c)
+{
+    ADC128D818State *s = ADC128D818(i2c);
+
+    s->pointer = 0;
+    s->outlen = 0;
+
+    /* POR-State */
+    s->config = 0b00001000;
+    s->interrupt_mask = 0;
+    s->conversion_rate = 0;
+    s->channel_disable = 0;
+    s->deep_shutdown = 0;
+    s->advanced_config = 0;
+
+    /* No POR-State defined in datasheet */
+    for (int i = 0; i < ADC128D818_NUM_CHANNELS * 2; i++) {
+        s->limit[i] = 0;
+    }
+}
+
+static void adc128d818_realize(DeviceState *dev, Error **errp)
+{
+    ADC128D818State *s = ADC128D818(dev);
+
+    trace_adc128d818_realize(s->i2c.address);
+
+    adc128d818_reset(&s->i2c);
+}
+
+static void adc128d818_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
+
+    dc->realize = adc128d818_realize;
+    dc->desc = "Texas Intstruments ADC128D818 12-bit ADC with temp sensor";
+    dc->vmsd = &vmstate_adc128d818;
+    k->event = adc128d818_event;
+    k->recv = adc128d818_recv;
+    k->send = adc128d818_send;
+}
+
+static const TypeInfo adc128d818_info = {
+    .name = TYPE_ADC128D818,
+    .parent = TYPE_I2C_SLAVE,
+    .instance_size = sizeof(ADC128D818State),
+    .class_size = sizeof(ADC128D818Class),
+    .instance_init = adc128d818_init,
+    .class_init = adc128d818_class_init,
+};
+
+static void adc128d818_register_types(void)
+{
+    type_register_static(&adc128d818_info);
+}
+
+type_init(adc128d818_register_types)
diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
index a1e26604fa..defd7647e7 100644
--- a/hw/sensor/meson.build
+++ b/hw/sensor/meson.build
@@ -10,3 +10,4 @@ system_ss.add(when: 'CONFIG_ISL_PMBUS_VR', if_true: files('isl_pmbus_vr.c'))
 system_ss.add(when: 'CONFIG_MAX31785', if_true: files('max31785.c'))
 system_ss.add(when: 'CONFIG_MAX31790', if_true: files('max31790.c'))
 system_ss.add(when: 'CONFIG_MAX11615', if_true: files('max11615.c'))
+system_ss.add(when: 'CONFIG_ADC128D818', if_true: files('adc128d818.c'))
diff --git a/hw/sensor/trace-events b/hw/sensor/trace-events
index 3fed979e85..4853f1944d 100644
--- a/hw/sensor/trace-events
+++ b/hw/sensor/trace-events
@@ -20,3 +20,11 @@ max11615_recv(uint8_t i2c_addr, uint8_t reg_addr) "i2c_addr: 0x%02x, reg_addr: 0
 max11615_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
 max11615_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
 max11615_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
+
+# adc128d818.c
+adc128d818_send(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, data: 0x%02x"
+adc128d818_write(uint8_t i2c_addr, uint8_t reg, uint8_t data) "i2c_addr: 0x%02x, reg: 0x%02x data: 0x%02x"
+adc128d818_recv(uint8_t i2c_addr, uint8_t reg) "i2c_addr: 0x%02x, reg: 0x%02x"
+adc128d818_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
+adc128d818_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
+adc128d818_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
diff --git a/include/hw/sensor/adc128d818.h b/include/hw/sensor/adc128d818.h
new file mode 100644
index 0000000000..e2bdc47590
--- /dev/null
+++ b/include/hw/sensor/adc128d818.h
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef QEMU_ADC128D818_H
+#define QEMU_ADC128D818_H
+
+#include <stdint.h>
+#include "hw/i2c/i2c.h"
+
+#define TYPE_ADC128D818 "adc128d818"
+
+/*
+ * Create and realize a adc128d818 ADC with constant caller-supplied readings
+ * @bus: I2C bus to put it on
+ * @address: I2C address
+ * @init_values: array of readings for each ADC channel
+ * @init_values_size: Size of @init_values, can be less than the number of channels
+ */
+I2CSlave *adc128d818_init_with_values(I2CBus *bus, uint8_t address,
+                                    const uint16_t *init_values, uint32_t init_values_size);
+
+#endif
diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
index 319f1a7672..883e058832 100755
--- a/tests/functional/arm/test_aspeed_fby4.py
+++ b/tests/functional/arm/test_aspeed_fby4.py
@@ -59,6 +59,16 @@ def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
         exec_command_and_wait_for_pattern(self,
             "cat /sys/bus/iio/devices/iio:device2/in_voltage_scale", "0.500000000");
 
+        # ADC128D818 test
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/bus/i2c/devices/30-001f/hwmon/hwmon0/name", "adc128d818");
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/bus/i2c/devices/30-001f/hwmon/hwmon0/in0_input", "75");
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/bus/i2c/devices/30-001f/hwmon/hwmon0/in0_min", "0");
+        exec_command_and_wait_for_pattern(self,
+            "cat /sys/bus/i2c/devices/30-001f/hwmon/hwmon0/in0_max", "0");
+
     def test_arm_ast2600_yosemitev4_openbmc(self):
         image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
 
-- 
2.54.0



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

* Re: [PATCH v3 1/5] ast2600: yosemite4 initial support
  2026-05-12 10:20 ` [PATCH v3 1/5] ast2600: yosemite4 initial support Alexander Hansen
@ 2026-05-12 16:13   ` Cédric Le Goater
  0 siblings, 0 replies; 12+ messages in thread
From: Cédric Le Goater @ 2026-05-12 16:13 UTC (permalink / raw)
  To: Alexander Hansen, qemu-devel
  Cc: Titus Rwantare, Peter Maydell, Steven Lee, Troy Lee, Jamin Lin,
	Kane Chen, Andrew Jeffery, Joel Stanley, qemu-arm

On 5/12/26 12:20, Alexander Hansen wrote:
> Initial patch based on [1] to support yosemite v4 bmc emulation.
> 
> The goal of this machine support is to support OpenBMC development.

    Linux version 6.18.26-80ec78d

Nice. this is a commit coming from Andrew.

> 
> Reference linux devicetree from openbmc linux is [2].
> 
> Status:
> - Enclosure FRU:         showing up
> - Management Board FRU:  showing up
> - Blade Board FRU:       showing up
> - Blade Board sensors:   not implemented
> - Blade Chassis FRU:     showing up
> - Fan Board FRU:         both showing up
> - Fan Board sensors:     supported
> 
> Overall the emulation is incomplete but already helpful in development
> and testing.
> 
> The focus of this initial support is on the FRU eeproms and fanboard
> sensors.
> 
> Tested: booted an OpenBMC image for yosemite4 target.
> 
> References:
> [1] https://github.com/9elements/qemu/commit/32139f913c2bd0ebe4bd26c46765861f6f9f2d49
> [2] arch/arm/boot/dts/aspeed/aspeed-bmc-facebook-yosemite4.dts
> [3] https://github.com/legoater/qemu-aspeed-boot/pull/5
> 
> Cc: Titus Rwantare <titusr@google.com>
> Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
> Cc: Peter Maydell <peter.maydell@linaro.org> (maintainer:ASPEED BMCs)
> Cc: Steven Lee <steven_lee@aspeedtech.com> (reviewer:ASPEED BMCs)
> Cc: Troy Lee <leetroy@gmail.com> (reviewer:ASPEED BMCs)
> Cc: Jamin Lin <jamin_lin@aspeedtech.com> (reviewer:ASPEED BMCs)
> Cc: Kane Chen <kane_chen@aspeedtech.com> (reviewer:ASPEED BMCs)
> Cc: Andrew Jeffery <andrew@codeconstruct.com.au> (reviewer:ASPEED BMCs)
> Cc: Joel Stanley <joel@jms.id.au> (reviewer:ASPEED BMCs)
> Cc: qemu-arm@nongnu.org (open list:ASPEED BMCs)
> Cc: qemu-devel@nongnu.org (open list:All patches CC here)
> Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
> ---
>   hw/arm/aspeed_ast2600_fby4.c | 267 +++++++++++++++++++++++++++++++++++
>   hw/arm/meson.build           |   1 +
>   2 files changed, 268 insertions(+)
>   create mode 100644 hw/arm/aspeed_ast2600_fby4.c
> 
> diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
> new file mode 100644
> index 0000000000..d79a88e2fe
> --- /dev/null
> +++ b/hw/arm/aspeed_ast2600_fby4.c
> @@ -0,0 +1,267 @@
> +/*
> + * Yosemite V4

"Facebook Yosemite V4" may be ?

> + *
> + * Copyright 2026 9elements.
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + */
> +
> +#include "qemu/osdep.h"
> +#include "hw/arm/machines-qom.h"
> +#include "hw/arm/aspeed.h"
> +#include "hw/arm/aspeed_soc.h"
> +#include "hw/nvram/eeprom_at24c.h"
> +#include "hw/i2c/i2c_mux_pca954x.h"
> +#include "hw/gpio/pca9552.h"
> +
> +#define FBY4_BMC_RAM_SIZE ASPEED_RAM_SIZE(2 * GiB)
> +
> +/* START OF EEPROM CONTENTS */
> +
> +/*
> + *./frugen -s board.mfg="Wiwynn" \
> + * --set board.pname="Fan Board FSC-MAX ADC-TI LED-NXP EFUSE-MAX" \
> + * --set text:board.pn="BRD-PN-345" \
> + * --board-date "10/1/2017 12:58:00" \
> + * --set board.serial="123456" \
> + * --set product.pname="Yosemite V4" \
> + * --set product.mfg="Wiwynn" \
> + * --set product.ver="v1.1" \
> + * --set product.serial="123456" \
> + * --set product.atag="PLACEHOLDER" \
> + * --set product.custom="Fanboard Custom1" \
> + * --set product.custom.1="Fanboard Custom2" \
> + * --set text:product.pn="PN-345" \
> + * fru-yv4-fanboard.bin
> + */
> +/* EM Config: yosemite4_fanboard_fsc_max_adc_ti_led_nxp_ons_efuse_max.json */
> +/* Yosemite4 fan board */
> +static const uint8_t fru_yv4_fanboard_bin[] = {
> +  0x01, 0x00, 0x00, 0x01, 0x0b, 0x00, 0x00, 0xf3, 0x01, 0x0a, 0x19, 0xce,
> +  0xc2, 0xa8, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xea, 0x46, 0x61,
> +  0x6e, 0x20, 0x42, 0x6f, 0x61, 0x72, 0x64, 0x20, 0x46, 0x53, 0x43, 0x2d,
> +  0x4d, 0x41, 0x58, 0x20, 0x41, 0x44, 0x43, 0x2d, 0x54, 0x49, 0x20, 0x4c,
> +  0x45, 0x44, 0x2d, 0x4e, 0x58, 0x50, 0x20, 0x45, 0x46, 0x55, 0x53, 0x45,
> +  0x2d, 0x4d, 0x41, 0x58, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05, 0xca, 0x42,
> +  0x52, 0x44, 0x2d, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc0, 0xc1, 0x00,
> +  0x00, 0x00, 0x00, 0xdb, 0x01, 0x09, 0x19, 0xc6, 0x57, 0x69, 0x77, 0x79,
> +  0x6e, 0x6e, 0xcb, 0x59, 0x6f, 0x73, 0x65, 0x6d, 0x69, 0x74, 0x65, 0x20,
> +  0x56, 0x34, 0xc6, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc4, 0x76, 0x31,
> +  0x2e, 0x31, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05, 0x89, 0x30, 0x1b, 0x8e,
> +  0x25, 0xfa, 0xb2, 0x64, 0x29, 0x03, 0xc0, 0xd0, 0x46, 0x61, 0x6e, 0x62,
> +  0x6f, 0x61, 0x72, 0x64, 0x20, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x32,
> +  0xc1, 0x00, 0x00, 0x9d
> +};
> +static const size_t fru_yv4_fanboard_bin_len = sizeof(fru_yv4_fanboard_bin);
> +
> +/*
> + *./frugen -s board.mfg="Wiwynn" \
> + * --set board.pname="Sentinel Dome without Retimer" \
> + * --set text:board.pn="BRD-PN-345" \
> + * --board-date "10/1/2017 12:58:00" \
> + * --set board.serial="123456" \
> + * --set product.pname="Yosemite V4" \
> + * --set product.mfg="Wiwynn" \
> + * --set product.ver="v1.1" \
> + * --set product.serial="123456" \
> + * --set product.atag="PLACEHOLDER" \
> + * --set product.custom="Yosemite V4 T1" \
> + * --set product.custom.1="Yosemite V4 T1" \
> + * --set text:product.pn="PN-345" \
> + * fru-yv4-sentineldome-board.bin
> + */
> +/* product.cust maps to PRODUCT_INFO_AM2 */
> +/* EM Config: yosemite4_sentineldome_t1.json */
> +/* Yosemite4 Sentinel Dome without Retimer Boards */
> +static const uint8_t fru_yv4_sentineldome_board_bin[] = {
> +  0x01, 0x00, 0x00, 0x01, 0x09, 0x00, 0x00, 0xf5, 0x01, 0x08, 0x19, 0xce,
> +  0xc2, 0xa8, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xdd, 0x53, 0x65,
> +  0x6e, 0x74, 0x69, 0x6e, 0x65, 0x6c, 0x20, 0x44, 0x6f, 0x6d, 0x65, 0x20,
> +  0x77, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x20, 0x52, 0x65, 0x74, 0x69,
> +  0x6d, 0x65, 0x72, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05, 0xca, 0x42, 0x52,
> +  0x44, 0x2d, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc0, 0xc1, 0x00, 0x78,
> +  0x01, 0x09, 0x19, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xcb, 0x59,
> +  0x6f, 0x73, 0x65, 0x6d, 0x69, 0x74, 0x65, 0x20, 0x56, 0x34, 0xc6, 0x50,
> +  0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc4, 0x76, 0x31, 0x2e, 0x31, 0x85, 0x91,
> +  0x34, 0x51, 0x95, 0x05, 0x89, 0x30, 0x1b, 0x8e, 0x25, 0xfa, 0xb2, 0x64,
> +  0x29, 0x03, 0xc0, 0xce, 0x59, 0x6f, 0x73, 0x65, 0x6d, 0x69, 0x74, 0x65,
> +  0x20, 0x56, 0x34, 0x20, 0x54, 0x31, 0xc1, 0x00, 0x00, 0x00, 0x00, 0xeb
> +};
> +static const size_t fru_yv4_sentineldome_board_bin_len =
> +    sizeof(fru_yv4_sentineldome_board_bin);
> +
> +/*
> + *./frugen -s board.mfg="Wiwynn" \
> + * --set board.pname="Sentinel Dome" \
> + * --set text:board.pn="BRD-PN-345" \
> + * --board-date "10/1/2017 12:58:00" \
> + * --set board.serial="123456" \
> + * --set product.pname="Yosemite V4" \
> + * --set product.mfg="Wiwynn" \
> + * --set product.ver="v1.1" \
> + * --set product.serial="123456" \
> + * --set product.atag="PLACEHOLDER" \
> + * --set text:product.pn="PN-345" \
> + * fru-yv4-sentineldome-chassis.bin
> + */
> +/* EM Config: yosemite4_sentineldome_chassis.json */
> +/* Yosemite 4 Sentinel Dome Chassis FRU */
> +static const uint8_t fru_yv4_sentineldome_chassis_bin[] = {
> +  0x01, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0xf7, 0x01, 0x06, 0x19, 0xce,
> +  0xc2, 0xa8, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xcd, 0x53, 0x65,
> +  0x6e, 0x74, 0x69, 0x6e, 0x65, 0x6c, 0x20, 0x44, 0x6f, 0x6d, 0x65, 0x85,
> +  0x91, 0x34, 0x51, 0x95, 0x05, 0xca, 0x42, 0x52, 0x44, 0x2d, 0x50, 0x4e,
> +  0x2d, 0x33, 0x34, 0x35, 0xc0, 0xc1, 0x00, 0xb6, 0x01, 0x07, 0x19, 0xc6,
> +  0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xcb, 0x59, 0x6f, 0x73, 0x65, 0x6d,
> +  0x69, 0x74, 0x65, 0x20, 0x56, 0x34, 0xc6, 0x50, 0x4e, 0x2d, 0x33, 0x34,
> +  0x35, 0xc4, 0x76, 0x31, 0x2e, 0x31, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05,
> +  0x89, 0x30, 0x1b, 0x8e, 0x25, 0xfa, 0xb2, 0x64, 0x29, 0x03, 0xc0, 0xc1,
> +  0x00, 0x00, 0x00, 0x59
> +};
> +static const size_t fru_yv4_sentineldome_chassis_bin_len =
> +    sizeof(fru_yv4_sentineldome_chassis_bin);
> +
> +/*
> + *./frugen -s board.mfg="Wiwynn" \
> + * --set board.pname="Management Board wBMC" \
> + * --set text:board.pn="BRD-PN-345" \
> + * --board-date "10/1/2017 12:58:00" \
> + * --set board.serial="123456" \
> + * --set product.pname="Yosemite V4" \
> + * --set product.mfg="Wiwynn" \
> + * --set product.ver="v1.1" \
> + * --set product.serial="123456" \
> + * --set product.atag="PLACEHOLDER" \
> + * --set text:product.pn="PN-345" \
> + * fru-yv4-eclosure.bin
> + */
> +/* EM Config: yosemite4_chassis.json, yosemite4.json */
> +/* Yosemite 4 Sentinel Dome Enclosure FRU */
> +static const uint8_t fru_yv4_eclosure_bin[] = {
> +  0x01, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0xf6, 0x01, 0x07, 0x19, 0xce,
> +  0xc2, 0xa8, 0xc6, 0x57, 0x69, 0x77, 0x79, 0x6e, 0x6e, 0xd5, 0x4d, 0x61,
> +  0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x42, 0x6f, 0x61,
> +  0x72, 0x64, 0x20, 0x77, 0x42, 0x4d, 0x43, 0x85, 0x91, 0x34, 0x51, 0x95,
> +  0x05, 0xca, 0x42, 0x52, 0x44, 0x2d, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35,
> +  0xc0, 0xc1, 0x00, 0x26, 0x01, 0x07, 0x19, 0xc6, 0x57, 0x69, 0x77, 0x79,
> +  0x6e, 0x6e, 0xcb, 0x59, 0x6f, 0x73, 0x65, 0x6d, 0x69, 0x74, 0x65, 0x20,
> +  0x56, 0x34, 0xc6, 0x50, 0x4e, 0x2d, 0x33, 0x34, 0x35, 0xc4, 0x76, 0x31,
> +  0x2e, 0x31, 0x85, 0x91, 0x34, 0x51, 0x95, 0x05, 0x89, 0x30, 0x1b, 0x8e,
> +  0x25, 0xfa, 0xb2, 0x64, 0x29, 0x03, 0xc0, 0xc1, 0x00, 0x00, 0x00, 0x59
> +};
> +static const size_t fru_yv4_eclosure_bin_len = sizeof(fru_yv4_eclosure_bin);
> +
> +/* END OF EEPROM CONTENTS */
> +
> +static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
> +{
> +    /* 2 fan boards */
> +    for (int i = 0; i <= 1; i++) {
> +        /* downstream bus */
> +        I2CBus *bus = pca954x_i2c_get_bus(fan_mux, i);
> +
> +        /* ti,adc128d818 @ 0x1f    (adc) */
> +        /* TODO */
> +
> +        /* maxim,max31790 @ 0x20   (pwm) */
> +        /* TODO */
> +
> +        /*
> +         * ti,tca6424 @ 0x22       (gpio)
> +         * linux handles tca6424 with PCA953X_TYPE, same as pca9535
> +         * { "pca9535", 16 | PCA953X_TYPE | PCA_INT, },
> +         * { "tca6424", 24 | PCA953X_TYPE | PCA_INT, },
> +         * so we _could_ be fine here unless more than 16 gpios are used
> +         */
> +        i2c_slave_create_simple(bus, TYPE_PCA9535, 0x22);
> +
> +        /*
> +         * NOTE: above works and could use for gpio presence:
> +         * $ gpioget gpiochip2 2
> +         * 1
> +         */
> +
> +        /* maxim,max31790 @ 0x2f   (pwm) */
> +        /* TODO */
> +
> +        /* maxim,max11615 @ 0x33   (adc) */
> +        /* TODO */
> +
> +        at24c_eeprom_init_rom(
> +            bus, 0x52, eepromSize,
> +            fru_yv4_fanboard_bin,
> +            fru_yv4_fanboard_bin_len);
> +
> +        /* LED blink driver / gpio expander */
> +        /* nxp,pca9552 @ 0x61   (gpio) */
> +        i2c_slave_create_simple(bus, TYPE_PCA9552, 0x61);
> +    }
> +}
> +
> +static void fby4_i2c_init_blade_chassis(I2CBus *bus, size_t eepromSize)
> +{
> +    /* Sentinel Dome Blade EEPROMS */
> +
> +    /* Board */
> +    at24c_eeprom_init_rom(bus, 0x54, eepromSize,
> +        fru_yv4_sentineldome_board_bin, fru_yv4_sentineldome_board_bin_len);
> +
> +    /* Chassis */
> +   at24c_eeprom_init_rom(bus, 0x55, eepromSize,
> +       fru_yv4_sentineldome_chassis_bin, fru_yv4_sentineldome_chassis_bin_len);
> +}
> +
> +static void fby4_i2c_init_multiple_blade_chassis(I2CBus **i2c,
> +    size_t eepromSize)
> +{
> +    /* there is 8 blade chassis, but we only emulate 2 for performance reason */
> +    for (int bus = 1; bus <= (8 / 4); bus++) {
> +        fby4_i2c_init_blade_chassis(i2c[bus], eepromSize);
> +    }
> +}
> +
> +static void fby4_i2c_init(AspeedMachineState *bmc)
> +{
> +    AspeedSoCState *soc = bmc->soc;
> +    I2CBus *i2c[16];
> +
> +    for (int i = 0; i < 16; i++) {
> +        i2c[i] = aspeed_i2c_get_bus(&soc->i2c, i);
> +    }
> +
> +    /* Enclosure (EM Config: yosemite4_chassis.json, yosemite4.json) */
> +    at24c_eeprom_init_rom(i2c[1], 0x51, 128 * KiB,
> +        fru_yv4_eclosure_bin, fru_yv4_eclosure_bin_len);
> +
> +    fby4_i2c_init_multiple_blade_chassis(i2c, 128 * KiB);
> +
> +    /* Yv4 fanboard connection */
> +    I2CSlave *fan_mux = i2c_slave_create_simple(i2c[14], TYPE_PCA9546, 0x74);
> +
> +    fby4_i2c_init_fanboard(fan_mux, 128 * KiB);
> +}
> +
> +static void aspeed_machine_fby4_class_init(ObjectClass *oc, const void *data)
> +{
> +    MachineClass *mc = MACHINE_CLASS(oc);
> +    AspeedMachineClass *amc = ASPEED_MACHINE_CLASS(oc);
> +
> +    mc->desc       = "Facebook fby4 BMC (Cortex-A7)";
> +    amc->fmc_model = "mx66l1g45g";
> +    amc->num_cs    = 2;
> +    amc->macs_mask = ASPEED_MAC3_ON;
> +    amc->i2c_init  = fby4_i2c_init;
> +    mc->default_ram_size = FBY4_BMC_RAM_SIZE;
> +    aspeed_machine_class_init_cpus_defaults(mc);
> +}
> +
> +static const TypeInfo aspeed_ast2600_fby4_types[] = {
> +    {
> +        .name          = MACHINE_TYPE_NAME("fby4-bmc"),
> +        .parent        = MACHINE_TYPE_NAME("ast2600-evb"),
> +        .class_init    = aspeed_machine_fby4_class_init,
> +        .interfaces    = arm_machine_interfaces,
> +    }
> +};
> +
> +DEFINE_TYPES(aspeed_ast2600_fby4_types)
> diff --git a/hw/arm/meson.build b/hw/arm/meson.build
> index 84b8ec5fb5..ccbc23e549 100644
> --- a/hw/arm/meson.build
> +++ b/hw/arm/meson.build
> @@ -60,6 +60,7 @@ arm_ss.add(when: 'CONFIG_ASPEED_SOC', if_true: files(
>     'aspeed_ast2600_catalina.c',
>     'aspeed_ast2600_evb.c',
>     'aspeed_ast2600_fby35.c',
> +  'aspeed_ast2600_fby4.c',
>     'aspeed_ast2600_fuji.c',
>     'aspeed_ast2600_gb200nvl.c',
>     'aspeed_ast2600_qcom-dc-scm-v1.c',


Reviewed-by: Cédric Le Goater <clg@redhat.com>

Thanks,

C.





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

* Re: [PATCH v3 2/5] ast2600: yosemite4 functional test
  2026-05-12 10:20 ` [PATCH v3 2/5] ast2600: yosemite4 functional test Alexander Hansen
@ 2026-05-12 16:34   ` Cédric Le Goater
  0 siblings, 0 replies; 12+ messages in thread
From: Cédric Le Goater @ 2026-05-12 16:34 UTC (permalink / raw)
  To: Alexander Hansen, qemu-devel
  Cc: Titus Rwantare, Peter Maydell, Steven Lee, Troy Lee, Jamin Lin,
	Kane Chen, Andrew Jeffery, Joel Stanley, qemu-arm

On 5/12/26 12:20, Alexander Hansen wrote:
> Tested: functional test passed.
> 
> export QEMU_TEST_QEMU_BINARY=qemu-system-arm
> ./build/run tests/functional/arm/test_aspeed_fby4.py
> TAP version 13
> ok 1 test_aspeed_fby4.YosemiteV4Machine.test_arm_ast2600_yosemitev4_openbmc
> 1..1

The above is not useful information for the reader, developer,
maintainer.

As a commit log, we generally provide some info on what is the test,
what the test does, where the FW images come from, what is excluded
if so, etc.

Something like :

   Add a functional test for the Yosemite V4 (fby4-bmc) machine
   that validates:
     - U-Boot initialization and kernel boot sequence
   The test uses an OpenBMC image built for the yosemite4 board and
   validates console output through the boot process.

The rest looks good. I like adding network but this is up to you.

Thanks,

C.



> Cc: Titus Rwantare <titusr@google.com>
> Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
> Cc: Peter Maydell <peter.maydell@linaro.org> (maintainer:ASPEED BMCs)
> Cc: Steven Lee <steven_lee@aspeedtech.com> (reviewer:ASPEED BMCs)
> Cc: Troy Lee <leetroy@gmail.com> (reviewer:ASPEED BMCs)
> Cc: Jamin Lin <jamin_lin@aspeedtech.com> (reviewer:ASPEED BMCs)
> Cc: Kane Chen <kane_chen@aspeedtech.com> (reviewer:ASPEED BMCs)
> Cc: Andrew Jeffery <andrew@codeconstruct.com.au> (reviewer:ASPEED BMCs)
> Cc: Joel Stanley <joel@jms.id.au> (reviewer:ASPEED BMCs)
> Cc: qemu-arm@nongnu.org (open list:ASPEED BMCs)
> Cc: qemu-devel@nongnu.org (open list:All patches CC here)
> Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
> ---
>   tests/functional/arm/meson.build         |  2 ++
>   tests/functional/arm/test_aspeed_fby4.py | 44 ++++++++++++++++++++++++
>   2 files changed, 46 insertions(+)
>   create mode 100755 tests/functional/arm/test_aspeed_fby4.py
> 
> diff --git a/tests/functional/arm/meson.build b/tests/functional/arm/meson.build
> index 2f538f29a2..10c0006f22 100644
> --- a/tests/functional/arm/meson.build
> +++ b/tests/functional/arm/meson.build
> @@ -14,6 +14,7 @@ test_arm_timeouts = {
>     'aspeed_ast2600_sdk_otp' : 720,
>     'aspeed_bletchley' : 480,
>     'aspeed_catalina' : 480,
> +  'aspeed_fby4': 480,
>     'aspeed_gb200nvl_bmc' : 480,
>     'aspeed_rainier' : 480,
>     'bpim2u' : 500,
> @@ -47,6 +48,7 @@ tests_arm_system_thorough = [
>     'aspeed_ast2600_sdk_otp',
>     'aspeed_bletchley',
>     'aspeed_catalina',
> +  'aspeed_fby4',
>     'aspeed_gb200nvl_bmc',
>     'aspeed_rainier',
>     'bpim2u',
> diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
> new file mode 100755
> index 0000000000..a3124c240f
> --- /dev/null
> +++ b/tests/functional/arm/test_aspeed_fby4.py
> @@ -0,0 +1,44 @@
> +#!/usr/bin/env python3
> +#
> +# Functional test that boots the ASPEED machines
> +#
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +
> +from qemu_test import Asset
> +from aspeed import AspeedTest
> +
> +
> +class YosemiteV4Machine(AspeedTest):
> +
> +    ASSET_YOSEMITE_V4_FLASH = Asset(
> +        'https://github.com/legoater/qemu-aspeed-boot/raw/refs/heads/master/images/yosemite4-bmc/openbmc-20260505132843/obmc-phosphor-image-yosemite4-20260505132843.static.mtd.xz',
> +        'dff6946363b41f952b15cfc3156482b89fcfc1b0ecfc3ec8b3ed496a5f001ef9')
> +
> +    def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
> +                                   cpu_id, soc):
> +
> +        self.set_machine(machine)
> +        self.vm.set_console()
> +        self.vm.add_args('-drive', f'file={image},if=mtd,format=raw',
> +                         '-snapshot')
> +        self.vm.launch()
> +
> +        self.wait_for_console_pattern(f'U-Boot {uboot}')
> +        self.wait_for_console_pattern('## Loading kernel from FIT Image')
> +        self.wait_for_console_pattern('Starting kernel ...')
> +        self.wait_for_console_pattern(f'Booting Linux on physical CPU {cpu_id}')
> +        self.wait_for_console_pattern(f'ASPEED {soc}')
> +        self.wait_for_console_pattern('/init as init process')
> +        # yosemite v4 does not emit the hostname log which is
> +        # different from the other machines.
> +        self.wait_for_console_pattern('yosemite4 login:')
> +
> +    def test_arm_ast2600_yosemitev4_openbmc(self):
> +        image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
> +
> +        self.do_test_arm_aspeed_openbmc_no_network('fby4-bmc', image=image_path,
> +                                        uboot='2019.04', cpu_id='0xf00',
> +                                        soc='AST2600 rev A3')
> +
> +if __name__ == '__main__':
> +    AspeedTest.main()



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

* Re: [PATCH v3 3/5] hw/sensor: MAX31790 support
  2026-05-12 10:20 ` [PATCH v3 3/5] hw/sensor: MAX31790 support Alexander Hansen
@ 2026-05-12 16:51   ` Cédric Le Goater
  0 siblings, 0 replies; 12+ messages in thread
From: Cédric Le Goater @ 2026-05-12 16:51 UTC (permalink / raw)
  To: Alexander Hansen, qemu-devel
  Cc: Titus Rwantare, Paolo Bonzini, Peter Maydell,
	Philippe Mathieu-Daudé, qemu-arm, Steven Lee, Troy Lee,
	Jamin Lin, Kane Chen, Andrew Jeffery, Joel Stanley

On 5/12/26 12:20, Alexander Hansen wrote:
> Product: [1]
> Datasheet: [2]
> 
> MAX31790 Support:
> - fan inputs are reading
> - tach reading propertional to pwm setting from linux driver
> - fans do not show any fault
> - 6 PWM registers influence 6 TACH registers
> 
> There is intentional stub behavior in some places and various functions
> of the device are currently unsupported.
> 
> MAX31790 currently unsupported:
> - slave address restriction
> - fan dynamics
> - spin-up configuration
> - fault state / failure possibility
> - rate-of-change control
> - tach mode
> - mixed layouts where number of fans != number of tachs
> - see Figure 5.9 in [2] for example of mixed layout
> 
> Anyone could expand it in the future for more accurate emulation.
> 
> The reason for adding this device is to support Yosemite V4 emulation.
> 
> Tested: on yosemite 4 qemu
> 
> root@yosemite4:~# ls /sys/class/hwmon/hwmon2/
> device	    fan2_fault   fan3_target  fan5_fault   fan6_target  pwm2	     pwm5
> fan1_enable  fan2_input   fan4_enable  fan5_input   name	pwm2_enable  pwm5_enable
> fan1_fault   fan2_target  fan4_fault   fan5_target  of_node	pwm3	     pwm6
> fan1_input   fan3_enable  fan4_input   fan6_enable  power	pwm3_enable  pwm6_enable
> fan1_target  fan3_fault   fan4_target  fan6_fault   pwm1	pwm4	     subsystem
> fan2_enable  fan3_input   fan5_enable  fan6_input   pwm1_enable  pwm4_enable  uevent
> root@yosemite4:~# cat /sys/class/hwmon/hwmon2/fan1_input
> 4551
> root@yosemite4:~# cat /sys/class/hwmon/hwmon2/fan1_enable
> 1
> root@yosemite4:~# cat /sys/class/hwmon/hwmon2/fan1_fault
> 0
> root@yosemite4:~# cat /sys/class/hwmon/hwmon2/fan1_target
> 2048
> root@yosemite4:~# cat /sys/class/hwmon/hwmon2/pwm1
> 178
> root@yosemite4:~# cat /sys/class/hwmon/hwmon2/name
> max31790
> 
> Trace output:
> max31790_realize i2c_addr: 0x20
> max31790_realize i2c_addr: 0x2f
> max31790_realize i2c_addr: 0x20
> max31790_realize i2c_addr: 0x2f
> max31790_event i2c_addr: 0x20, event: 0x01
> max31790_send i2c_addr: 0x20, data: 0x02
> max31790_event i2c_addr: 0x20, event: 0x00
> max31790_recv i2c_addr: 0x20, reg_addr: 0x02
> max31790_recv_return i2c_addr: 0x20, returns: 0x08
> ...
> 
> References:
> [1] https://www.analog.com/en/products/MAX31790.html
> [2] https://www.analog.com/media/en/technical-documentation/data-sheets/MAX31790.pdf
> 
> Cc: Titus Rwantare <titusr@google.com>
> Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
> Cc: Paolo Bonzini <pbonzini@redhat.com>
> Cc: Peter Maydell <peter.maydell@linaro.org>
> Cc: "Philippe Mathieu-Daudé" <philmd@linaro.org>
> Cc: qemu-arm@nongnu.org
> Cc: qemu-devel@nongnu.org
> Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
> ---
>   MAINTAINERS                              |   1 +
>   hw/arm/Kconfig                           |   1 +
>   hw/arm/aspeed_ast2600_fby4.c             |   5 +-
>   hw/sensor/Kconfig                        |   4 +
>   hw/sensor/max31790.c                     | 499 +++++++++++++++++++++++
>   hw/sensor/meson.build                    |   1 +
>   hw/sensor/trace-events                   |   8 +
>   include/hw/sensor/max31790.h             |   7 +

Please add to your .gitconfig :

   [diff]
	orderFile = /path/to/qemu/scripts/git.orderfile


>   tests/functional/arm/test_aspeed_fby4.py |  18 +
>   9 files changed, 542 insertions(+), 2 deletions(-)
>   create mode 100644 hw/sensor/max31790.c
>   create mode 100644 include/hw/sensor/max31790.h
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 93a1e4e482..9c991f8e70 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -3981,6 +3981,7 @@ S: Maintained
>   F: hw/i2c/pmbus_device.c
>   F: hw/sensor/adm1272.c
>   F: hw/sensor/isl_pmbus_vr.c
> +F: hw/sensor/max31790.c
>   F: hw/sensor/max34451.c
>   F: include/hw/i2c/pmbus_device.h
>   F: include/hw/sensor/isl_pmbus_vr.h
> diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
> index 5b198402d5..99864eb878 100644
> --- a/hw/arm/Kconfig
> +++ b/hw/arm/Kconfig
> @@ -552,6 +552,7 @@ config ASPEED_SOC
>       select LED
>       select PMBUS
>       select MAX31785
> +    select MAX31790
>       select FSI_APB2OPB_ASPEED
>       select AT24C
>       select PCI_EXPRESS
> diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
> index d79a88e2fe..a7c2dc09ea 100644
> --- a/hw/arm/aspeed_ast2600_fby4.c
> +++ b/hw/arm/aspeed_ast2600_fby4.c
> @@ -11,6 +11,7 @@
>   #include "hw/arm/aspeed.h"
>   #include "hw/arm/aspeed_soc.h"
>   #include "hw/nvram/eeprom_at24c.h"
> +#include "hw/sensor/max31790.h"
>   #include "hw/i2c/i2c_mux_pca954x.h"
>   #include "hw/gpio/pca9552.h"
>   
> @@ -164,7 +165,7 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
>           /* TODO */
>   
>           /* maxim,max31790 @ 0x20   (pwm) */
> -        /* TODO */
> +        i2c_slave_create_simple(bus, TYPE_MAX31790, 0x20);
>   
>           /*
>            * ti,tca6424 @ 0x22       (gpio)
> @@ -182,7 +183,7 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
>            */
>   
>           /* maxim,max31790 @ 0x2f   (pwm) */
> -        /* TODO */
> +        i2c_slave_create_simple(bus, TYPE_MAX31790, 0x2f);
>   
>           /* maxim,max11615 @ 0x33   (adc) */
>           /* TODO */
> diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
> index bc6331b4ab..ece2f2b167 100644
> --- a/hw/sensor/Kconfig
> +++ b/hw/sensor/Kconfig
> @@ -43,3 +43,7 @@ config ISL_PMBUS_VR
>   config MAX31785
>       bool
>       depends on PMBUS
> +
> +config MAX31790
> +    bool
> +    depends on PMBUS
> diff --git a/hw/sensor/max31790.c b/hw/sensor/max31790.c
> new file mode 100644
> index 0000000000..16525cba9b
> --- /dev/null
> +++ b/hw/sensor/max31790.c
> @@ -0,0 +1,499 @@
> +/*
> + * Maxim MAX31790 PMBus 6-Channel Fan Controller
> + *
> + * Datasheet:
> + * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX31790.pdf
> + *
> + * Copyright 2026 9elements
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + */
> +
> +#include "qemu/osdep.h"
> +#include "hw/sensor/max31790.h"
> +#include "hw/i2c/i2c.h"
> +#include "migration/vmstate.h"
> +#include "qapi/error.h"
> +#include "qapi/visitor.h"
> +#include "qemu/log.h"
> +#include "qemu/module.h"
> +#include "qom/object.h"
> +#include "trace.h"
> +
> +#define MAX31790_NUM_FANS 6
> +#define MAX31790_NUM_TACHS 12
> +
> +#define MAX31790_REG_GLOBAL_CONFIG 0x00
> +#define MAX31790_REG_PWM_FREQ 0x01
> +
> +/* 0x02 to 0x07: N = 0 .. 5 */
> +#define MAX31790_REG_FAN_CONFIG(N) (0x02 + N)
> +
> +/* 0x08 to 0x0d: N = 0 .. 5 */
> +#define MAX31790_REG_FAN_DYNAMICS(N) (0x08 + N)
> +
> +#define MAX31790_REG_FAN_FAULT_STATUS_2 0x10
> +#define MAX31790_REG_FAN_FAULT_STATUS_1 0x11
> +#define MAX31790_REG_FAN_FAULT_MASK_2 0x12
> +#define MAX31790_REG_FAN_FAULT_MASK_1 0x13
> +#define MAX31790_REG_FAILED_FAN_OPT 0x14
> +
> +/* 0x18 to 0x2f: N = 0 .. 11 */
> +#define MAX31790_REG_TACH_COUNT_MSB(N) (0x18 + 2 * N)
> +#define MAX31790_REG_TACH_COUNT_LSB(N) (0x19 + 2 * N)
> +
> +/* 0x30 to 0x3b: N = 0 .. 5 */
> +#define MAX31790_REG_PWM_DUTY_CYCLE_MSB(N) (0x30 + 2 * N)
> +#define MAX31790_REG_PWM_DUTY_CYCLE_LSB(N) (0x31 + 2 * N)
> +
> +/* .. reserved registers ... */
> +
> +/* 0x40 to 0x4b: N = 0 .. 5 */
> +#define MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(N) (0x40 + 2 * N)
> +#define MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(N) (0x41 + 2 * N)
> +
> +/* ... 'User Byte' registers ... */
> +
> +/* 0x50 to 0x5b: N = 0 .. 5 */
> +#define MAX31790_REG_TACH_TARGET_COUNT_MSB(N) (0x50 + 2 * N)
> +#define MAX31790_REG_TACH_TARGET_COUNT_LSB(N) (0x51 + 2 * N)
> +
> +struct MAX31790State {
> +  I2CSlave i2c;

indent is wrong : 2-space instead of 4-space

Check docs/devel/style.rst.

> +
> +  uint8_t fan_config[MAX31790_NUM_FANS];
> +  uint8_t fan_dynamics[MAX31790_NUM_FANS];
> +
> +  uint16_t pwm[MAX31790_NUM_FANS];
> +  uint16_t tach_target[MAX31790_NUM_FANS];
> +  uint16_t rpm[MAX31790_NUM_TACHS];
> +
> +  /* command buffer */
> +  uint8_t len;
> +  uint8_t buf[2];
> +
> +  /* output buffer */
> +  uint8_t outlen;
> +  uint8_t outbuf[2];
> +
> +  /* selected register for read/write operation */
> +  uint8_t pointer;
> +};
> +
> +struct MAX31790Class {
> +  I2CSlaveClass parent_class;
> +};
> +
> +OBJECT_DECLARE_TYPE(MAX31790State, MAX31790Class, MAX31790)
> +
> +static void max31790_read(MAX31790State *s)
> +{
> +  size_t index = 0;
> +  uint8_t out0 = 0;
> +  uint8_t out1 = 0;
> +
> +  switch (s->pointer) {
> +  case MAX31790_REG_FAN_CONFIG(0):
> +  case MAX31790_REG_FAN_CONFIG(1):
> +  case MAX31790_REG_FAN_CONFIG(2):
> +  case MAX31790_REG_FAN_CONFIG(3):
> +  case MAX31790_REG_FAN_CONFIG(4):
> +  case MAX31790_REG_FAN_CONFIG(5):
> +    out0 = s->fan_config[s->pointer - MAX31790_REG_FAN_CONFIG(0)];
> +    break;
> +  case MAX31790_REG_FAN_DYNAMICS(0):
> +  case MAX31790_REG_FAN_DYNAMICS(1):
> +  case MAX31790_REG_FAN_DYNAMICS(2):
> +  case MAX31790_REG_FAN_DYNAMICS(3):
> +  case MAX31790_REG_FAN_DYNAMICS(4):
> +  case MAX31790_REG_FAN_DYNAMICS(5):
> +    out0 = s->fan_dynamics[s->pointer - MAX31790_REG_FAN_DYNAMICS(0)];
> +    break;
> +  case MAX31790_REG_FAN_FAULT_STATUS_1:
> +  case MAX31790_REG_FAN_FAULT_STATUS_2:
> +    /* we do not have any fan fault */
> +    out0 = 0x00;
> +    out1 = 0x00;
> +    break;
> +  case MAX31790_REG_TACH_COUNT_MSB(0):
> +  case MAX31790_REG_TACH_COUNT_MSB(1):
> +  case MAX31790_REG_TACH_COUNT_MSB(2):
> +  case MAX31790_REG_TACH_COUNT_MSB(3):
> +  case MAX31790_REG_TACH_COUNT_MSB(4):
> +  case MAX31790_REG_TACH_COUNT_MSB(5):
> +  case MAX31790_REG_TACH_COUNT_MSB(6):
> +  case MAX31790_REG_TACH_COUNT_MSB(7):
> +  case MAX31790_REG_TACH_COUNT_MSB(8):
> +  case MAX31790_REG_TACH_COUNT_MSB(9):
> +  case MAX31790_REG_TACH_COUNT_MSB(10):
> +  case MAX31790_REG_TACH_COUNT_MSB(11):
> +    index = (s->pointer - MAX31790_REG_TACH_COUNT_MSB(0)) / 2;
> +    out0 = (s->rpm[index] >> 8) & 0xff;
> +    out1 = s->rpm[index] & 0xff;
> +    break;
> +
> +  case MAX31790_REG_TACH_COUNT_LSB(0):
> +  case MAX31790_REG_TACH_COUNT_LSB(1):
> +  case MAX31790_REG_TACH_COUNT_LSB(2):
> +  case MAX31790_REG_TACH_COUNT_LSB(3):
> +  case MAX31790_REG_TACH_COUNT_LSB(4):
> +  case MAX31790_REG_TACH_COUNT_LSB(5):
> +  case MAX31790_REG_TACH_COUNT_LSB(6):
> +  case MAX31790_REG_TACH_COUNT_LSB(7):
> +  case MAX31790_REG_TACH_COUNT_LSB(8):
> +  case MAX31790_REG_TACH_COUNT_LSB(9):
> +  case MAX31790_REG_TACH_COUNT_LSB(10):
> +  case MAX31790_REG_TACH_COUNT_LSB(11):
> +    index = (s->pointer - MAX31790_REG_TACH_COUNT_LSB(0)) / 2;
> +    out0 = s->rpm[index] & 0xff;
> +    break;
> +
> +  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(0):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(1):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(2):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(3):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(4):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_MSB(5):
> +    index = (s->pointer - MAX31790_REG_PWM_DUTY_CYCLE_MSB(0)) / 2;
> +    out0 = (s->pwm[index] >> 8) & 0xff;
> +    out1 = s->pwm[index] & 0xff;
> +    break;
> +  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(0):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(1):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(2):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(3):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(4):
> +  case MAX31790_REG_PWM_DUTY_CYCLE_LSB(5):
> +    index = (s->pointer - MAX31790_REG_PWM_DUTY_CYCLE_LSB(0)) / 2;
> +    out0 = s->pwm[index] & 0xff;
> +    break;
> +
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(1):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(2):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(3):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(4):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(5):
> +    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0)) / 2;
> +    out0 = (s->pwm[index] >> 8) & 0xff;
> +    out1 = s->pwm[index] & 0xff;
> +    break;
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(0):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(1):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(2):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(3):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(4):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(5):
> +    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(0)) / 2;
> +    out0 = s->pwm[index] & 0xff;
> +    break;
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(0):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(1):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(2):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(3):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(4):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(5):
> +    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_MSB(0)) / 2;
> +    out0 = (s->tach_target[index] >> 8) & 0xff;
> +    out1 = s->tach_target[index] & 0xff;
> +    break;
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(0):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(1):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(2):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(3):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(4):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(5):
> +    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_LSB(0)) / 2;
> +    out0 = s->tach_target[index] & 0xff;
> +    break;
> +  default:
> +    qemu_log_mask(LOG_UNIMP, "%s: read of register %d", __func__, s->pointer);
> +    break;
> +  }
> +
> +  s->outbuf[0] = out0;
> +  s->outbuf[1] = out1;
> +}
> +
> +static void max31790_set_rpm(MAX31790State *s, size_t index, uint16_t rpm)
> +{
> +  /* datasheet: lowest 5 bits are 0 */
> +  s->rpm[index] = rpm & ~0b11111;

ok. binary literals are not that common. Nothing wrong though.

> +}
> +
> +static void max31790_pwm_write(MAX31790State *s, size_t index, uint16_t value)
> +{
> +  trace_max31790_pwm_write(s->i2c.address, index, value);
> +
> +  s->pwm[index] = value;
> +
> +  /* change rpm based on pwm input */
> +  const uint16_t pwm_no_reserve = s->pwm[index] >> 7;
> +
> +  /*
> +   * This formula has magic values which model the relationship
> +   * of PWM input to a fan. Not derived from datasheet.
> +   */
> +  max31790_set_rpm(s, index, 0x1000 + (pwm_no_reserve << 3));
> +}
> +
> +static void max31790_write_2_byte(MAX31790State *s)
> +{
> +  size_t index = 0;
> +  const uint8_t value0 = s->buf[0];
> +  const uint8_t value1 = s->buf[1];

a newline to delimit the variables from the code is nice to have.

> +  switch (s->pointer) {
> +  case MAX31790_REG_FAN_CONFIG(0):
> +  case MAX31790_REG_FAN_CONFIG(1):
> +  case MAX31790_REG_FAN_CONFIG(2):
> +  case MAX31790_REG_FAN_CONFIG(3):
> +  case MAX31790_REG_FAN_CONFIG(4):
> +  case MAX31790_REG_FAN_CONFIG(5):
> +    break; /* handled by one byte write */
> +  case MAX31790_REG_FAN_DYNAMICS(0):
> +  case MAX31790_REG_FAN_DYNAMICS(1):
> +  case MAX31790_REG_FAN_DYNAMICS(2):
> +  case MAX31790_REG_FAN_DYNAMICS(3):
> +  case MAX31790_REG_FAN_DYNAMICS(4):
> +  case MAX31790_REG_FAN_DYNAMICS(5):
> +    break; /* handled by one byte write */
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(1):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(2):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(3):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(4):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(5):
> +    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0)) / 2;
> +    max31790_pwm_write(s, index, value0 << 8 | value1);
> +    break;
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(0):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(1):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(2):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(3):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(4):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(5):
> +    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_MSB(0)) / 2;
> +    s->tach_target[index] = (value0 << 8) | value1;
> +    break;
> +  default:
> +    qemu_log_mask(LOG_UNIMP, "%s: write to register %d", __func__, s->pointer);
> +    break;
> +  }
> +}
> +
> +static void max31790_write_1_byte(MAX31790State *s)
> +{
> +
> +  size_t index = 0;
> +  uint16_t pwm = 0;
> +  const uint8_t value = s->buf[0];
> +  switch (s->pointer) {
> +  case MAX31790_REG_FAN_CONFIG(0):
> +  case MAX31790_REG_FAN_CONFIG(1):
> +  case MAX31790_REG_FAN_CONFIG(2):
> +  case MAX31790_REG_FAN_CONFIG(3):
> +  case MAX31790_REG_FAN_CONFIG(4):
> +  case MAX31790_REG_FAN_CONFIG(5):
> +    s->fan_config[s->pointer - MAX31790_REG_FAN_CONFIG(0)] = value;
> +    break;
> +  case MAX31790_REG_FAN_DYNAMICS(0):
> +  case MAX31790_REG_FAN_DYNAMICS(1):
> +  case MAX31790_REG_FAN_DYNAMICS(2):
> +  case MAX31790_REG_FAN_DYNAMICS(3):
> +  case MAX31790_REG_FAN_DYNAMICS(4):
> +  case MAX31790_REG_FAN_DYNAMICS(5):
> +    s->fan_dynamics[s->pointer - MAX31790_REG_FAN_DYNAMICS(0)] = value;
> +    break;
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(1):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(2):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(3):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(4):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(5):
> +    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_MSB(0)) / 2;
> +    pwm = (value << 8) | (s->pwm[index] & 0x00ff);
> +    max31790_pwm_write(s, index, pwm);
> +    break;
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(0):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(1):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(2):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(3):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(4):
> +  case MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(5):
> +    index = (s->pointer - MAX31790_REG_PWM_TARGET_DUTY_CYCLE_LSB(0)) / 2;
> +    pwm = (s->pwm[index] & 0xff00) | (value & 0x00ff);
> +    max31790_pwm_write(s, index, pwm);
> +    break;
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(0):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(1):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(2):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(3):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(4):
> +  case MAX31790_REG_TACH_TARGET_COUNT_MSB(5):
> +    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_MSB(0)) / 2;
> +    s->tach_target[index] = (s->tach_target[index] & 0x00ff) | (value << 8);
> +    break;
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(0):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(1):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(2):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(3):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(4):
> +  case MAX31790_REG_TACH_TARGET_COUNT_LSB(5):
> +    index = (s->pointer - MAX31790_REG_TACH_TARGET_COUNT_LSB(0)) / 2;
> +    s->tach_target[index] = (s->tach_target[index] & 0xff00) | value;
> +    break;
> +  default:
> +    qemu_log_mask(LOG_UNIMP, "%s: write to register %d", __func__, s->pointer);
> +    break;
> +  }
> +}
> +
> +static int max31790_send(I2CSlave *i2c, uint8_t data)
> +{
> +  MAX31790State *s = MAX31790(i2c);
> +
> +  trace_max31790_send(s->i2c.address, data);
> +
> +  if (s->len == 0) {
> +    /* first byte is the register pointer for a read / write operation */
> +    s->pointer = data;
> +    s->len++;
> +    return 0;
> +  }
> +
> +  if (s->len > 2) {
> +    qemu_log_mask(LOG_GUEST_ERROR, "%s: write too many bytes", __func__);
> +    return 1; /* NAK */
> +  }
> +
> +  /* second / third byte is the data to write */
> +  s->buf[s->len - 1] = data;
> +  s->len++;
> +
> +  if (s->len == 2) {
> +    max31790_write_1_byte(s);
> +  } else if (s->len == 3) {
> +    max31790_write_2_byte(s);
> +  }
> +
> +  return 0;
> +}
> +
> +static uint8_t max31790_recv(I2CSlave *i2c)
> +{
> +  MAX31790State *s = MAX31790(i2c);
> +  trace_max31790_recv(s->i2c.address, s->pointer);
> +
> +  max31790_read(s);
> +  s->len = 0;
> +
> +  if (s->outlen >= 2) {
> +    /* error */
> +    s->outlen = 0;
> +  }
> +
> +  const uint8_t data = s->outbuf[s->outlen++];
> +
> +  trace_max31790_recv_return(s->i2c.address, data);
> +  return data;
> +}
> +
> +static int max31790_event(I2CSlave *i2c, enum i2c_event event)
> +{
> +  MAX31790State *s = MAX31790(i2c);
> +
> +  trace_max31790_event(s->i2c.address, event);
> +
> +  switch (event) {
> +  case I2C_START_RECV:
> +    s->outlen = 0;
> +    break;
> +  case I2C_START_SEND:
> +    s->len = 0;
> +    break;
> +  default:
> +    break;
> +  }
> +
> +  return 0;
> +}
> +
> +static const VMStateDescription vmstate_max31790 = {
> +    .name = TYPE_MAX31790,
> +    .version_id = 0,
> +    .minimum_version_id = 0,
> +    .fields = (const VMStateField[]){
> +        VMSTATE_UINT8(len, MAX31790State),
> +        VMSTATE_UINT8_ARRAY(fan_config, MAX31790State, MAX31790_NUM_FANS),
> +        VMSTATE_UINT8_ARRAY(fan_dynamics, MAX31790State, MAX31790_NUM_FANS),
> +        VMSTATE_UINT16_ARRAY(pwm, MAX31790State, MAX31790_NUM_FANS),
> +        VMSTATE_UINT16_ARRAY(tach_target, MAX31790State, MAX31790_NUM_FANS),
> +        VMSTATE_UINT16_ARRAY(rpm, MAX31790State, MAX31790_NUM_TACHS),
> +        VMSTATE_UINT8_ARRAY(buf, MAX31790State, 2),
> +        VMSTATE_UINT8(outlen, MAX31790State),
> +        VMSTATE_UINT8_ARRAY(outbuf, MAX31790State, 2),
> +        VMSTATE_UINT8(pointer, MAX31790State),
> +        VMSTATE_I2C_SLAVE(i2c, MAX31790State), VMSTATE_END_OF_LIST()}
> +};
> +
> +static void max31790_init(Object *obj) { /* Nothing to do */ }

So don't define max31790_init then.
> +
> +static void max31790_reset(I2CSlave *i2c)
> +{
> +  MAX31790State *s = MAX31790(i2c);
> +
> +  for (int i = 0; i < MAX31790_NUM_FANS; i++) {
> +    /* POR-State 0b 0XX0 0000 */
> +    s->fan_config[i] = 0b00000000;
> +
> +    /* same as POR-State */
> +    s->tach_target[i] = 0b0011110000000000;
> +
> +    /* same as POR-State */
> +    s->fan_dynamics[i] = 0b01001100;
> +
> +    s->pwm[i] = 0;
> +  }
> +
> +  for (int i = 0; i < MAX31790_NUM_TACHS; i++) {
> +    max31790_set_rpm(s, i, 0x4444);
> +  }
> +}
> +
> +static void max31790_realize(DeviceState *dev, Error **errp)
> +{
> +  MAX31790State *s = MAX31790(dev);
> +
> +  trace_max31790_realize(s->i2c.address);
> +
> +  max31790_reset(&s->i2c);

calling reset from realize is incorrect. Use dc->reset instead.


> +}
> +
> +static void max31790_class_init(ObjectClass *klass, const void *data)
> +{
> +  DeviceClass *dc = DEVICE_CLASS(klass);
> +  I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
> +
> +  dc->realize = max31790_realize;
> +  dc->desc = "Maxim MAX31790 6-Channel Fan Controller";
> +  dc->vmsd = &vmstate_max31790;
> +  k->event = max31790_event;
> +  k->recv = max31790_recv;
> +  k->send = max31790_send;
> +}
> +
> +static const TypeInfo max31790_info = {
> +    .name = TYPE_MAX31790,
> +    .parent = TYPE_I2C_SLAVE,
> +    .instance_size = sizeof(MAX31790State),
> +    .class_size = sizeof(MAX31790Class),
> +    .instance_init = max31790_init,
> +    .class_init = max31790_class_init,
> +};
> +
> +static void max31790_register_types(void)
> +{
> +  type_register_static(&max31790_info);
> +}
> +
> +type_init(max31790_register_types)
> diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
> index 420fdc3359..4987c3b253 100644
> --- a/hw/sensor/meson.build
> +++ b/hw/sensor/meson.build
> @@ -8,3 +8,4 @@ system_ss.add(when: 'CONFIG_MAX34451', if_true: files('max34451.c'))
>   system_ss.add(when: 'CONFIG_LSM303DLHC_MAG', if_true: files('lsm303dlhc_mag.c'))
>   system_ss.add(when: 'CONFIG_ISL_PMBUS_VR', if_true: files('isl_pmbus_vr.c'))
>   system_ss.add(when: 'CONFIG_MAX31785', if_true: files('max31785.c'))
> +system_ss.add(when: 'CONFIG_MAX31790', if_true: files('max31790.c'))
> diff --git a/hw/sensor/trace-events b/hw/sensor/trace-events
> index a3fe54fa6d..c15c0a7e93 100644
> --- a/hw/sensor/trace-events
> +++ b/hw/sensor/trace-events
> @@ -4,3 +4,11 @@
>   tmp105_read(uint8_t dev, uint8_t addr) "device: 0x%02x, addr: 0x%02x"
>   tmp105_write(uint8_t dev, uint8_t addr) "device: 0x%02x, addr 0x%02x"
>   tmp105_write_shutdown(uint8_t dev) "device: 0x%02x"
> +
> +# max31790.c
> +max31790_send(uint8_t i2c_addr, uint8_t send) "i2c_addr: 0x%02x, data: 0x%02x"
> +max31790_recv(uint8_t i2c_addr, uint8_t reg_addr) "i2c_addr: 0x%02x, reg_addr: 0x%02x"
> +max31790_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
> +max31790_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
> +max31790_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
> +max31790_pwm_write(uint8_t i2c_addr, size_t index, uint16_t value) "i2c_addr: 0x%02x, index: %zu, value: 0x%04x"
> diff --git a/include/hw/sensor/max31790.h b/include/hw/sensor/max31790.h
> new file mode 100644
> index 0000000000..7ead420926
> --- /dev/null
> +++ b/include/hw/sensor/max31790.h
> @@ -0,0 +1,7 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +#ifndef QEMU_MAX31790_H
> +#define QEMU_MAX31790_H

the header guard definition does not match the path of the file.

> +
> +#define TYPE_MAX31790 "max31790"


I'd use the "max31790" type directly in the machine. As you wish.


Thanks,

C.

> +
> +#endif
> diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
> index a3124c240f..d29423add7 100755
> --- a/tests/functional/arm/test_aspeed_fby4.py
> +++ b/tests/functional/arm/test_aspeed_fby4.py
> @@ -7,6 +7,8 @@
>   from qemu_test import Asset
>   from aspeed import AspeedTest
>   
> +from qemu_test import wait_for_console_pattern, exec_command
> +from qemu_test import exec_command_and_wait_for_pattern
>   
>   class YosemiteV4Machine(AspeedTest):
>   
> @@ -33,6 +35,22 @@ def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
>           # different from the other machines.
>           self.wait_for_console_pattern('yosemite4 login:')
>   
> +        # perform login
> +        exec_command_and_wait_for_pattern(self,
> +                                          "root", "Password:");
> +
> +        exec_command_and_wait_for_pattern(self, "0penBmc", "#");
> +
> +        # MAX31790 test
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/class/hwmon/hwmon2/name", "max31790");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/class/hwmon/hwmon2/fan1_input", "4530");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/class/hwmon/hwmon2/fan1_enable", "1");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/class/hwmon/hwmon2/fan1_fault", "0");
> +
>       def test_arm_ast2600_yosemitev4_openbmc(self):
>           image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
>   



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

* Re: [PATCH v3 4/5] hw/sensor: support MAX11615
  2026-05-12 10:20 ` [PATCH v3 4/5] hw/sensor: support MAX11615 Alexander Hansen
@ 2026-05-12 16:55   ` Cédric Le Goater
  2026-05-12 16:58   ` Cédric Le Goater
  1 sibling, 0 replies; 12+ messages in thread
From: Cédric Le Goater @ 2026-05-12 16:55 UTC (permalink / raw)
  To: Alexander Hansen, qemu-devel
  Cc: Titus Rwantare, Philippe Mathieu-Daudé, Paolo Bonzini,
	Peter Maydell, qemu-arm, Steven Lee, Troy Lee, Jamin Lin,
	Kane Chen, Andrew Jeffery, Joel Stanley

On 5/12/26 12:20, Alexander Hansen wrote:
> Product: [1]
> Datasheet: [2]
> 
> Sensor readings can be provided upon creation of the device. In case no
> readings are provided the ADC reads a pre-defined arbitrary value.
> 
> root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/name
> max11615
> root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/in_voltage0_raw
> 1922
> root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/in_voltage_scale
> 0.500000000
> 
> trace:
> less /tmp/qemu-trace.log | grep -i max116
> max11615_realize i2c_addr: 0x33
> max11615_realize i2c_addr: 0x33
> max11615_event i2c_addr: 0x33, event: 0x01
> max11615_write_setup i2c_addr: 0x33, data: 0xd2
> max11615_write_config i2c_addr: 0x33, data: 0x0f
> max11615_event i2c_addr: 0x33, event: 0x03
> max11615_event i2c_addr: 0x33, event: 0x01
> max11615_write_setup i2c_addr: 0x33, data: 0xd2
> max11615_write_config i2c_addr: 0x33, data: 0x0f
> max11615_event i2c_addr: 0x33, event: 0x03
> max11615_event i2c_addr: 0x33, event: 0x01
> max11615_write_setup i2c_addr: 0x33, data: 0xd2
> max11615_write_config i2c_addr: 0x33, data: 0x61
> max11615_event i2c_addr: 0x33, event: 0x03
> max11615_event i2c_addr: 0x33, event: 0x00
> max11615_recv i2c_addr: 0x33, reg_addr: 0x00
> max11615_recv_return i2c_addr: 0x33, returns: 0xfa
> max11615_recv i2c_addr: 0x33, reg_addr: 0x00
> max11615_recv_return i2c_addr: 0x33, returns: 0xd2
> max11615_event i2c_addr: 0x33, event: 0x04
> max11615_event i2c_addr: 0x33, event: 0x03
> 
> References:
> [1] https://www.analog.com/en/products/MAX11615.html
> [2] https://www.analog.com/media/en/technical-documentation/data-sheets/MAX11612-MAX11617.pdf
> 
> Cc: Titus Rwantare <titusr@google.com>
> Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
> Cc: "Philippe Mathieu-Daudé" <philmd@linaro.org> (odd fixer:Overall sensors)
> Cc: Paolo Bonzini <pbonzini@redhat.com> (maintainer:Kconfig)
> Cc: Peter Maydell <peter.maydell@linaro.org> (supporter:ARM TCG CPUs)
> Cc: qemu-devel@nongnu.org (open list:All patches CC here)
> Cc: qemu-arm@nongnu.org (open list:ARM TCG CPUs)
> Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
> ---
>   MAINTAINERS                              |   1 +
>   hw/arm/Kconfig                           |   1 +
>   hw/arm/aspeed_ast2600_fby4.c             |   8 +-
>   hw/sensor/Kconfig                        |   4 +
>   hw/sensor/max11615.c                     | 202 +++++++++++++++++++++++
>   hw/sensor/meson.build                    |   1 +
>   hw/sensor/trace-events                   |   8 +
>   include/hw/sensor/max11615.h             |  20 +++
>   tests/functional/arm/test_aspeed_fby4.py |   8 +
>   9 files changed, 252 insertions(+), 1 deletion(-)
>   create mode 100644 hw/sensor/max11615.c
>   create mode 100644 include/hw/sensor/max11615.h
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 9c991f8e70..a9c88996a2 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -3981,6 +3981,7 @@ S: Maintained
>   F: hw/i2c/pmbus_device.c
>   F: hw/sensor/adm1272.c
>   F: hw/sensor/isl_pmbus_vr.c
> +F: hw/sensor/max11615.c
>   F: hw/sensor/max31790.c
>   F: hw/sensor/max34451.c
>   F: include/hw/i2c/pmbus_device.h
> diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
> index 99864eb878..76a7d327a9 100644
> --- a/hw/arm/Kconfig
> +++ b/hw/arm/Kconfig
> @@ -553,6 +553,7 @@ config ASPEED_SOC
>       select PMBUS
>       select MAX31785
>       select MAX31790
> +    select MAX11615
>       select FSI_APB2OPB_ASPEED
>       select AT24C
>       select PCI_EXPRESS
> diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
> index a7c2dc09ea..036f5b9e38 100644
> --- a/hw/arm/aspeed_ast2600_fby4.c
> +++ b/hw/arm/aspeed_ast2600_fby4.c
> @@ -12,6 +12,7 @@
>   #include "hw/arm/aspeed_soc.h"
>   #include "hw/nvram/eeprom_at24c.h"
>   #include "hw/sensor/max31790.h"
> +#include "hw/sensor/max11615.h"
>   #include "hw/i2c/i2c_mux_pca954x.h"
>   #include "hw/gpio/pca9552.h"
>   
> @@ -186,7 +187,12 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
>           i2c_slave_create_simple(bus, TYPE_MAX31790, 0x2f);
>   
>           /* maxim,max11615 @ 0x33   (adc) */
> -        /* TODO */
> +        static const uint16_t adc_values[8] = {
> +            0b011110000010, 0b010100011000,
> +            0b001000110100, 0b100000101001,
> +            0b011110000010, 0b010100011000,
> +            0b001000110100, 0b100000101001};
> +        max11615_init_with_values(bus, 0x33, adc_values, 8);
>   
>           at24c_eeprom_init_rom(
>               bus, 0x52, eepromSize,
> diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
> index ece2f2b167..84eede9d84 100644
> --- a/hw/sensor/Kconfig
> +++ b/hw/sensor/Kconfig
> @@ -47,3 +47,7 @@ config MAX31785
>   config MAX31790
>       bool
>       depends on PMBUS
> +
> +config MAX11615
> +    bool
> +    depends on I2C
> diff --git a/hw/sensor/max11615.c b/hw/sensor/max11615.c
> new file mode 100644
> index 0000000000..7950e00e33
> --- /dev/null
> +++ b/hw/sensor/max11615.c
> @@ -0,0 +1,202 @@
> +/*
> + * Maxim MAX11615 Low-Power 12 bit ADC
> + * Models MAX11612,MAX11613,MAX11614,MAX11615,MAX11616,MAX11617
> + *
> + * Datasheet:
> + * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX11612-MAX11617.pdf
> + *
> + * Copyright 2026 9elements
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + */
> +
> +#include "qemu/osdep.h"
> +#include "hw/i2c/i2c.h"
> +#include "migration/vmstate.h"
> +#include "qapi/error.h"
> +#include "qapi/visitor.h"
> +#include "qemu/log.h"
> +#include "qemu/module.h"
> +#include "trace.h"
> +#include "hw/sensor/max11615.h"
> +
> +#define MAX11615_NUM_CHANNELS 8
> +
> +struct MAX11615State {
> +    I2CSlave i2c;
> +
> +    uint16_t channels[MAX11615_NUM_CHANNELS];
> +
> +    /* output buffer */
> +    uint8_t outlen;
> +    uint8_t outbuf[2];
> +
> +    /* selected channel for read/write operation */
> +    uint8_t pointer;
> +};
> +
> +struct MAX11615Class {
> +    I2CSlaveClass parent_class;
> +};
> +
> +OBJECT_DECLARE_TYPE(MAX11615State, MAX11615Class, MAX11615)
> +
> +static void max11615_read(MAX11615State *s)
> +{
> +    /* read an ADC channel, first 4 bits must be high */
> +    uint8_t msb = s->channels[s->pointer] >> 8;
> +    uint8_t lsb = s->channels[s->pointer] & 0xff;
> +    s->outbuf[0] = 0b11110000 | (msb & 0b00001111);
> +    s->outbuf[1] = lsb;
> +}
> +
> +static void max11615_write_config_byte(MAX11615State *s, uint8_t data)
> +{
> +    trace_max11615_write_config(s->i2c.address, data);
> +
> +    uint8_t channelSelect = (data >> 1) & 0b1111;

channel_select

> +
> +    /* Table 3. Channel Selection (AIN0 ... AIN11) */
> +    if (channelSelect > 11) {
> +        qemu_log_mask(LOG_GUEST_ERROR, "%s: invalid channel select", __func__);
> +        channelSelect = 11;
> +    }
> +    s->pointer = channelSelect;
> +}
> +
> +static void max11615_write_setup_byte(MAX11615State *s, uint8_t data)
> +{
> +    trace_max11615_write_setup(s->i2c.address, data);
> +    /* we ignore the setup byte, not implemented */
> +}
> +
> +static int max11615_send(I2CSlave *i2c, uint8_t data)
> +{
> +    MAX11615State *s = MAX11615(i2c);
> +    const uint8_t msb = (data >> 7) & 0b1;
> +
> +    if (msb) {
> +        max11615_write_setup_byte(s, data);
> +    } else {
> +        max11615_write_config_byte(s, data);
> +    }
> +
> +    s->outlen = 0;
> +    return 0;
> +}
> +
> +static uint8_t max11615_recv(I2CSlave *i2c)
> +{
> +    MAX11615State *s = MAX11615(i2c);
> +    trace_max11615_recv(s->i2c.address, s->pointer);
> +
> +    max11615_read(s);
> +
> +    if (s->outlen >= 2) {
> +        qemu_log_mask(LOG_GUEST_ERROR, "%s: too many bytes read", __func__);
> +        s->outlen = 0;
> +    }
> +
> +    const uint8_t data =  s->outbuf[s->outlen++];
> +
> +    trace_max11615_recv_return(s->i2c.address, data);
> +    return data;
> +}
> +
> +static int max11615_event(I2CSlave *i2c, enum i2c_event event)
> +{
> +    MAX11615State *s = MAX11615(i2c);
> +
> +    trace_max11615_event(s->i2c.address, event);
> +
> +    switch (event) {
> +    case I2C_START_RECV:
> +        s->outlen = 0;
> +        break;
> +    default:
> +        break;
> +    }
> +
> +    return 0;
> +}
> +
> +static const VMStateDescription vmstate_max11615 = {
> +    .name = TYPE_MAX11615,
> +    .version_id = 0,
> +    .minimum_version_id = 0,
> +    .fields = (const VMStateField[]){
> +        VMSTATE_UINT16_ARRAY(channels, MAX11615State, MAX11615_NUM_CHANNELS),
> +        VMSTATE_UINT8(outlen, MAX11615State),
> +        VMSTATE_UINT8_ARRAY(outbuf, MAX11615State, 2),
> +        VMSTATE_UINT8(pointer, MAX11615State),
> +        VMSTATE_I2C_SLAVE(i2c, MAX11615State),
> +        VMSTATE_END_OF_LIST()
> +    }
> +};
> +
> +static void max11615_init(Object *obj)
> +{
> +    /* Nothing to do */
> +}
> +
> +I2CSlave *max11615_init_with_values(I2CBus *bus, uint8_t address,
> +    const uint16_t *init_values, uint32_t init_values_size)
> +{
> +    MAX11615State *s;
> +
> +    s = MAX11615(i2c_slave_new(TYPE_MAX11615, address));
> +
> +    for (int i = 0; i < MAX11615_NUM_CHANNELS && i < init_values_size; i++) {
> +
> +        /* arbitrary value */
> +        uint16_t value = 0b0000101011010010;
> +
> +        if (i < init_values_size) {
> +            value = init_values[i];
> +        }
> +        s->channels[i] = value;
> +    }
> +
> +    i2c_slave_realize_and_unref(I2C_SLAVE(s), bus, &error_abort);
> +
> +    return I2C_SLAVE(s);
> +}
> +
> +static void max11615_realize(DeviceState *dev, Error **errp)
> +{
> +    MAX11615State *s = MAX11615(dev);
> +
> +    trace_max11615_realize(s->i2c.address);
> +
> +    s->pointer = 0;
> +    s->outlen = 0;
> +}
> +
> +static void max11615_class_init(ObjectClass *klass, const void *data)
> +{
> +    DeviceClass *dc = DEVICE_CLASS(klass);
> +    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
> +
> +    dc->realize = max11615_realize;
> +    dc->desc = "Maxim MAX11615 12-bit ADC";
> +    dc->vmsd = &vmstate_max11615;
> +    k->event = max11615_event;
> +    k->recv = max11615_recv;
> +    k->send = max11615_send;
> +}
> +
> +static const TypeInfo max31790_info = {

wrong name.

Thanks,

C.

> +    .name = TYPE_MAX11615,
> +    .parent = TYPE_I2C_SLAVE,
> +    .instance_size = sizeof(MAX11615State),
> +    .class_size = sizeof(MAX11615Class),
> +    .instance_init = max11615_init,
> +    .class_init = max11615_class_init,
> +};
> +
> +static void max31790_register_types(void)
> +{
> +    type_register_static(&max31790_info);
> +}
> +
> +type_init(max31790_register_types)
> diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
> index 4987c3b253..a1e26604fa 100644
> --- a/hw/sensor/meson.build
> +++ b/hw/sensor/meson.build
> @@ -9,3 +9,4 @@ system_ss.add(when: 'CONFIG_LSM303DLHC_MAG', if_true: files('lsm303dlhc_mag.c'))
>   system_ss.add(when: 'CONFIG_ISL_PMBUS_VR', if_true: files('isl_pmbus_vr.c'))
>   system_ss.add(when: 'CONFIG_MAX31785', if_true: files('max31785.c'))
>   system_ss.add(when: 'CONFIG_MAX31790', if_true: files('max31790.c'))
> +system_ss.add(when: 'CONFIG_MAX11615', if_true: files('max11615.c'))
> diff --git a/hw/sensor/trace-events b/hw/sensor/trace-events
> index c15c0a7e93..3fed979e85 100644
> --- a/hw/sensor/trace-events
> +++ b/hw/sensor/trace-events
> @@ -12,3 +12,11 @@ max31790_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns:
>   max31790_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
>   max31790_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
>   max31790_pwm_write(uint8_t i2c_addr, size_t index, uint16_t value) "i2c_addr: 0x%02x, index: %zu, value: 0x%04x"
> +
> +# max11615.c
> +max11615_write_setup(uint8_t i2c_addr, uint8_t send) "i2c_addr: 0x%02x, data: 0x%02x"
> +max11615_write_config(uint8_t i2c_addr, uint8_t send) "i2c_addr: 0x%02x, data: 0x%02x"
> +max11615_recv(uint8_t i2c_addr, uint8_t reg_addr) "i2c_addr: 0x%02x, reg_addr: 0x%02x"
> +max11615_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
> +max11615_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
> +max11615_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
> diff --git a/include/hw/sensor/max11615.h b/include/hw/sensor/max11615.h
> new file mode 100644
> index 0000000000..ccc0c9bc2e
> --- /dev/null
> +++ b/include/hw/sensor/max11615.h
> @@ -0,0 +1,20 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +#ifndef QEMU_MAX11615_H
> +#define QEMU_MAX11615_H
> +
> +#include <stdint.h>
> +#include "hw/i2c/i2c.h"
> +
> +#define TYPE_MAX11615 "max11615"
> +
> +/*
> + * Create and realize a MAX11615 ADC with constant caller-supplied readings
> + * @bus: I2C bus to put it on
> + * @address: I2C address
> + * @init_values: array of readings for each ADC channel
> + * @init_values_size: Size of @init_values, can be less than the number of channels
> + */
> +I2CSlave *max11615_init_with_values(I2CBus *bus, uint8_t address,
> +                                    const uint16_t *init_values, uint32_t init_values_size);
> +
> +#endif
> diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
> index d29423add7..319f1a7672 100755
> --- a/tests/functional/arm/test_aspeed_fby4.py
> +++ b/tests/functional/arm/test_aspeed_fby4.py
> @@ -51,6 +51,14 @@ def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
>           exec_command_and_wait_for_pattern(self,
>               "cat /sys/class/hwmon/hwmon2/fan1_fault", "0");
>   
> +        # MAX11615 test
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/iio/devices/iio:device2/name", "max11615");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/iio/devices/iio:device2/in_voltage0_raw", "1922");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/iio/devices/iio:device2/in_voltage_scale", "0.500000000");
> +
>       def test_arm_ast2600_yosemitev4_openbmc(self):
>           image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
>   



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

* Re: [PATCH v3 5/5] hw/sensor: support Texas Instruments ADC128D818
  2026-05-12 10:20 ` [PATCH v3 5/5] hw/sensor: support Texas Instruments ADC128D818 Alexander Hansen
@ 2026-05-12 16:57   ` Cédric Le Goater
  0 siblings, 0 replies; 12+ messages in thread
From: Cédric Le Goater @ 2026-05-12 16:57 UTC (permalink / raw)
  To: Alexander Hansen, qemu-devel
  Cc: Titus Rwantare, Paolo Bonzini, Peter Maydell,
	Philippe Mathieu-Daudé, qemu-arm, Steven Lee, Troy Lee,
	Jamin Lin, Kane Chen, Andrew Jeffery, Joel Stanley

On 5/12/26 12:20, Alexander Hansen wrote:
> Product: [1]
> Datasheet: [2]
> 
> ADC128D818 Support:
> - channel readings from pre-set values
> - driver can read and write most configuration registers
> 
> ADC128D818 currently unsupported:
> - slave address restriction
> - startup sequence and realistic busy register emulation
> - external VREF
> - conversion rate
> - interrupts
> - deep shutdown mode
> - individual channel shutdown
> - selection between Mode 0,1,2,3
> - pseudo-differential input
> 
> Anyone could expand it in the future for more accurate emulation.
> 
> The reason for adding this device is to support Yosemite V4 emulation.
> 
> Tested: on yosemite v4 qemu
> 
> initialize the device:
> // ti,adc128d818 @ 0x1f    (adc)
> // the driver will throw away the last 4 bits, set them 0
> uint16_t adc_values1[8] = {
>      0b011110000000, 0b010100010000,
>      0b001000110000, 0b100000100000,
>      0b011110000000, 0b010100010000,
>      0b001000110000, 0b100000100000};
> adc128d818_init_with_values(bus, 0x1f, adc_values1, 8);
> 
> Trace outputs directly after initialization:
> adc128d818_realize i2c_addr: 0x1f
> adc128d818_realize i2c_addr: 0x1f
> adc128d818_event i2c_addr: 0x1f, event: 0x01
> adc128d818_send i2c_addr: 0x1f, data: 0x00
> adc128d818_send i2c_addr: 0x1f, data: 0x80
> adc128d818_write i2c_addr: 0x1f, reg: 0x00 data: 0x80
> adc128d818_event i2c_addr: 0x1f, event: 0x03
> adc128d818_event i2c_addr: 0x1f, event: 0x01
> ...
> 
> read the values
> root@yosemite4:/sys/bus/i2c/devices/30-001f# cat hwmon/hwmon0/in0_min
> 0
> root@yosemite4:/sys/bus/i2c/devices/30-001f# cat hwmon/hwmon0/in0_max
> 0
> root@yosemite4:/sys/bus/i2c/devices/30-001f# cat hwmon/hwmon0/in0_input
> 75
> root@yosemite4:/sys/bus/i2c/devices/30-001f# cat hwmon/hwmon0/name
> adc128d818
> 
> We initially configured 0b011110000000 for the first channel.
> The driver throws away the last 4 bits and does calculation similar to
> below:
> 
> val = DIV_ROUND_CLOSEST(data->in[index][nr] * data->vref, 4095);
> 
> We can check that the calculation is as expected given our configured
> value.
> 
> ((0b011110000000 >> 4) * 2560) / 4095
> 75.01831501831502
> 
> References:
> [1] https://www.ti.com/product/ADC128D818
> [2] https://www.ti.com/lit/gpn/adc128d818
> 
> Cc: Titus Rwantare <titusr@google.com>
> Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
> Cc: Paolo Bonzini <pbonzini@redhat.com> (maintainer:Kconfig)
> Cc: Peter Maydell <peter.maydell@linaro.org> (supporter:ARM TCG CPUs)
> Cc: "Philippe Mathieu-Daudé" <philmd@linaro.org> (odd fixer:Overall sensors)
> Cc: qemu-arm@nongnu.org (open list:ARM TCG CPUs)
> Cc: qemu-devel@nongnu.org (open list:All patches CC here)
> Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
> ---
>   MAINTAINERS                              |   1 +
>   hw/arm/Kconfig                           |   1 +
>   hw/arm/aspeed_ast2600_fby4.c             |   9 +-
>   hw/sensor/Kconfig                        |   4 +
>   hw/sensor/adc128d818.c                   | 414 +++++++++++++++++++++++
>   hw/sensor/meson.build                    |   1 +
>   hw/sensor/trace-events                   |   8 +
>   include/hw/sensor/adc128d818.h           |  20 ++
>   tests/functional/arm/test_aspeed_fby4.py |  10 +
>   9 files changed, 467 insertions(+), 1 deletion(-)
>   create mode 100644 hw/sensor/adc128d818.c
>   create mode 100644 include/hw/sensor/adc128d818.h
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index a9c88996a2..43831b67c4 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -3979,6 +3979,7 @@ PMBus
>   M: Titus Rwantare <titusr@google.com>
>   S: Maintained
>   F: hw/i2c/pmbus_device.c
> +F: hw/sensor/adc128d818.c
>   F: hw/sensor/adm1272.c
>   F: hw/sensor/isl_pmbus_vr.c
>   F: hw/sensor/max11615.c
> diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
> index 76a7d327a9..cc89c65e3f 100644
> --- a/hw/arm/Kconfig
> +++ b/hw/arm/Kconfig
> @@ -554,6 +554,7 @@ config ASPEED_SOC
>       select MAX31785
>       select MAX31790
>       select MAX11615
> +    select ADC128D818
>       select FSI_APB2OPB_ASPEED
>       select AT24C
>       select PCI_EXPRESS
> diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
> index 036f5b9e38..4fd3a713e2 100644
> --- a/hw/arm/aspeed_ast2600_fby4.c
> +++ b/hw/arm/aspeed_ast2600_fby4.c
> @@ -13,6 +13,7 @@
>   #include "hw/nvram/eeprom_at24c.h"
>   #include "hw/sensor/max31790.h"
>   #include "hw/sensor/max11615.h"
> +#include "hw/sensor/adc128d818.h"
>   #include "hw/i2c/i2c_mux_pca954x.h"
>   #include "hw/gpio/pca9552.h"
>   
> @@ -163,7 +164,13 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
>           I2CBus *bus = pca954x_i2c_get_bus(fan_mux, i);
>   
>           /* ti,adc128d818 @ 0x1f    (adc) */
> -        /* TODO */
> +        /* the driver will throw away the last 4 bits, set them 0 */
> +        static const uint16_t adc_values1[8] = {
> +            0b011110000000, 0b010100010000,
> +            0b001000110000, 0b100000100000,
> +            0b011110000000, 0b010100010000,
> +            0b001000110000, 0b100000100000};
> +        adc128d818_init_with_values(bus, 0x1f, adc_values1, 8);
>   
>           /* maxim,max31790 @ 0x20   (pwm) */
>           i2c_slave_create_simple(bus, TYPE_MAX31790, 0x20);
> diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
> index 84eede9d84..c9190ff780 100644
> --- a/hw/sensor/Kconfig
> +++ b/hw/sensor/Kconfig
> @@ -51,3 +51,7 @@ config MAX31790
>   config MAX11615
>       bool
>       depends on I2C
> +
> +config ADC128D818
> +    bool
> +    depends on I2C
> diff --git a/hw/sensor/adc128d818.c b/hw/sensor/adc128d818.c
> new file mode 100644
> index 0000000000..83a4d43846
> --- /dev/null
> +++ b/hw/sensor/adc128d818.c
> @@ -0,0 +1,414 @@
> +/*
> + * Texas Instruments ADC128D818 12 bit ADC with temperature sensor
> + * Models ADC128D818
> + *
> + * Product: https://www.ti.com/product/ADC128D818
> + * Datasheet: https://www.ti.com/lit/gpn/adc128d818
> + *
> + * Copyright 2026 9elements
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + */
> +
> +#include "qemu/osdep.h"
> +#include "hw/i2c/i2c.h"
> +#include "migration/vmstate.h"
> +#include "qapi/error.h"
> +#include "qapi/visitor.h"
> +#include "qemu/log.h"
> +#include "qemu/module.h"
> +#include "trace.h"
> +#include "hw/sensor/adc128d818.h"
> +
> +/* 8 bit, r/w */
> +#define REG_CONFIG 0x00
> +
> +/* 8 bit, readonly */
> +#define REG_INTERRUPT_STATUS 0x01
> +
> +/* 8 bit, r/w */
> +#define REG_INTERRUPT_MASK 0x03
> +
> +/* 8 bit, r/w */
> +#define REG_CONVERSION_RATE 0x07
> +
> +/* 8 bit, r/w */
> +#define REG_CHANNEL_DISABLE 0x08
> +
> +/* 8 bit, write-only */
> +#define REG_ONE_SHOT 0x09
> +
> +/* 8 bit, r/w */
> +#define REG_DEEP_SHUTDOWN 0x0a
> +
> +/* 8 bit, r/w */
> +#define REG_ADVANCED_CONFIG 0x0b
> +
> +/* 8 bit, readonly */
> +#define REG_BUSY_STATUS 0x0c
> +
> +/* 16 bit registers, N = 0..7, readonly */
> +#define REG_CHANNEL_READING(N) (0x20 + N)
> +
> +/* 8 bit registers N = 0..15, r/w */
> +#define REG_LIMIT(N) (0x2a + N)
> +
> +/* 8 bit register, readonly */
> +#define REG_MANUFACTURER_ID 0x3e
> +
> +/* 8 bit register, readonly */
> +#define REG_REVISION_ID 0x3f
> +
> +#define ADC128D818_NUM_CHANNELS 8
> +
> +struct ADC128D818State {
> +    I2CSlave i2c;
> +
> +    uint8_t config;
> +    uint8_t interrupt_mask;
> +    uint8_t conversion_rate;
> +    uint8_t channel_disable;
> +    bool deep_shutdown;
> +    uint8_t advanced_config;
> +
> +    /* channel reading registers, 2 bytes each */
> +    uint16_t channels[ADC128D818_NUM_CHANNELS];
> +
> +    /* high and low limit registers 0x2a - 0x39, one byte each */
> +    uint8_t limit[ADC128D818_NUM_CHANNELS * 2];
> +
> +    /* input buffer */
> +    uint8_t len;
> +    uint8_t buf[2];
> +
> +    /* output buffer */
> +    uint8_t outlen;
> +    uint8_t outbuf[2];
> +
> +    /* selected channel for read/write operation */
> +    uint8_t pointer;
> +};
> +
> +struct ADC128D818Class {
> +    I2CSlaveClass parent_class;
> +};
> +
> +OBJECT_DECLARE_TYPE(ADC128D818State, ADC128D818Class, ADC128D818)
> +
> +static void adc128d818_read(ADC128D818State *s)
> +{
> +    uint8_t ch_num = 0;
> +    switch (s->pointer) {
> +    case REG_CONFIG:
> +        s->outbuf[0] = s->config;
> +        break;
> +    case REG_INTERRUPT_STATUS:
> +        s->outbuf[0] = 0x0; /* POR State */
> +        break;
> +    case REG_INTERRUPT_MASK:
> +        s->outbuf[0] = s->interrupt_mask;
> +        break;
> +    case REG_CONVERSION_RATE:
> +        s->outbuf[0] = s->conversion_rate;
> +        break;
> +    case REG_CHANNEL_DISABLE:
> +        s->outbuf[0] = s->channel_disable;
> +        break;
> +    case REG_ONE_SHOT:
> +        /* not marked as readable */
> +        qemu_log_mask(LOG_GUEST_ERROR, "%s: read of register 0x%02x\n",
> +            __func__, s->pointer);
> +        s->outbuf[0] = 0x0;
> +        break;
> +    case REG_DEEP_SHUTDOWN:
> +        s->outbuf[0] = s->deep_shutdown ? 0x1 : 0x0;
> +        break;
> +    case REG_ADVANCED_CONFIG:
> +        s->outbuf[0] = s->advanced_config & 0b111;
> +        break;
> +    case REG_BUSY_STATUS:
> +        /* not implemented */
> +        s->outbuf[0] = 0b00000010; /* POR State */
> +        break;
> +    case REG_CHANNEL_READING(0):
> +    case REG_CHANNEL_READING(1):
> +    case REG_CHANNEL_READING(2):
> +    case REG_CHANNEL_READING(3):
> +    case REG_CHANNEL_READING(4):
> +    case REG_CHANNEL_READING(5):
> +    case REG_CHANNEL_READING(6):
> +    case REG_CHANNEL_READING(7):
> +        ch_num = s->pointer - REG_CHANNEL_READING(0);
> +        /* high byte comes first, driver reads swapped */
> +        s->outbuf[0] = (s->channels[ch_num] >> 8) & 0xff;
> +        s->outbuf[1] = s->channels[ch_num] & 0xff;
> +        break;
> +    case REG_LIMIT(0):
> +    case REG_LIMIT(1):
> +    case REG_LIMIT(2):
> +    case REG_LIMIT(3):
> +    case REG_LIMIT(4):
> +    case REG_LIMIT(5):
> +    case REG_LIMIT(6):
> +    case REG_LIMIT(7):
> +    case REG_LIMIT(8):
> +    case REG_LIMIT(9):
> +    case REG_LIMIT(10):
> +    case REG_LIMIT(11):
> +    case REG_LIMIT(12):
> +    case REG_LIMIT(13):
> +    case REG_LIMIT(14):
> +    case REG_LIMIT(15):
> +        s->outbuf[0] = s->limit[s->pointer - REG_LIMIT(0)];
> +        break;
> +    case REG_MANUFACTURER_ID:
> +        s->outbuf[0] = 0x1; /* readonly */
> +        break;
> +    case REG_REVISION_ID:
> +        s->outbuf[0] = 0b00001001; /* readonly */
> +        break;
> +    default:
> +        qemu_log_mask(LOG_GUEST_ERROR, "%s: read of register 0x%02x\n",
> +            __func__, s->pointer);
> +        break;
> +    }
> +}
> +
> +static void adc128d818_write_advanced_config(ADC128D818State *s, uint8_t data)
> +{
> +    /*
> +     * Note: Whenever the Advanced Configuration Register is programmed,
> +     * all of the values in the Channel Reading Registers and
> +     * Interrupt Status Registers will return to their default values.
> +     */
> +
> +    s->advanced_config = (data & 0b111);
> +}
> +
> +static void adc128d818_write(ADC128D818State *s, uint8_t data)
> +{
> +    trace_adc128d818_write(s->i2c.address, s->pointer, data);
> +
> +    /* which bits in config register are writable */
> +    const uint8_t config_w_mask = 0b10001011;
> +    const uint8_t config_ro_mask = (uint8_t)~config_w_mask;
> +
> +    switch (s->pointer) {
> +    case REG_CONFIG:
> +        s->config = (s->config & config_ro_mask) | (data & config_w_mask);
> +        break;
> +    case REG_INTERRUPT_MASK:
> +        s->interrupt_mask = data;
> +        break;
> +    case REG_CONVERSION_RATE:
> +        s->conversion_rate = data;
> +        break;
> +    case REG_CHANNEL_DISABLE:
> +        s->channel_disable = data;
> +        break;
> +    case REG_ONE_SHOT:
> +        /*
> +         * Initiate a single conversion and comparison cycle when
> +         * the device is in shutdown mode or deep shutdown mode, after
> +         * which the device returns to the respective mode that it was in
> +         *
> +         */
> +        break;
> +    case REG_DEEP_SHUTDOWN:
> +        s->deep_shutdown = (data & 0x1) != 0;
> +        break;
> +    case REG_ADVANCED_CONFIG:
> +        adc128d818_write_advanced_config(s, data);
> +        break;
> +    case REG_LIMIT(0):
> +    case REG_LIMIT(1):
> +    case REG_LIMIT(2):
> +    case REG_LIMIT(3):
> +    case REG_LIMIT(4):
> +    case REG_LIMIT(5):
> +    case REG_LIMIT(6):
> +    case REG_LIMIT(7):
> +    case REG_LIMIT(8):
> +    case REG_LIMIT(9):
> +    case REG_LIMIT(10):
> +    case REG_LIMIT(11):
> +    case REG_LIMIT(12):
> +    case REG_LIMIT(13):
> +    case REG_LIMIT(14):
> +    case REG_LIMIT(15):
> +        s->limit[s->pointer - REG_LIMIT(0)] = data;
> +        break;
> +    default:
> +        qemu_log_mask(LOG_GUEST_ERROR, "%s: write of register 0x%02x\n",
> +            __func__, s->pointer);
> +        break;
> +    }
> +}
> +
> +static int adc128d818_send(I2CSlave *i2c, uint8_t data)
> +{
> +    ADC128D818State *s = ADC128D818(i2c);
> +    trace_adc128d818_send(s->i2c.address, data);
> +
> +    s->outlen = 0;
> +    s->buf[s->len] = data;
> +
> +    if (s->len == 0) {
> +        s->pointer = data;
> +    } else if (s->len == 1) {
> +        adc128d818_write(s, data);
> +    }
> +
> +    s->len++;
> +    return 0;
> +}
> +
> +static uint8_t adc128d818_recv(I2CSlave *i2c)
> +{
> +    ADC128D818State *s = ADC128D818(i2c);
> +    trace_adc128d818_recv(s->i2c.address, s->pointer);
> +
> +    adc128d818_read(s);
> +
> +    if (s->outlen >= 2) {
> +        qemu_log_mask(LOG_GUEST_ERROR, "%s: too many bytes read\n", __func__);
> +        s->outlen = 0;
> +    }
> +
> +    const uint8_t data =  s->outbuf[s->outlen++];
> +
> +    trace_adc128d818_recv_return(s->i2c.address, data);
> +    return data;
> +}
> +
> +static int adc128d818_event(I2CSlave *i2c, enum i2c_event event)
> +{
> +    ADC128D818State *s = ADC128D818(i2c);
> +
> +    trace_adc128d818_event(s->i2c.address, event);
> +
> +    switch (event) {
> +    case I2C_START_RECV:
> +        s->outlen = 0;
> +        break;
> +    case I2C_START_SEND:
> +        s->len = 0;
> +        break;
> +    default:
> +        break;
> +    }
> +
> +    return 0;
> +}
> +
> +static const VMStateDescription vmstate_adc128d818 = {
> +    .name = TYPE_ADC128D818,
> +    .version_id = 0,
> +    .minimum_version_id = 0,
> +    .fields = (const VMStateField[]){
> +        VMSTATE_UINT8(config, ADC128D818State),
> +        VMSTATE_UINT8(interrupt_mask, ADC128D818State),
> +        VMSTATE_UINT8(conversion_rate, ADC128D818State),
> +        VMSTATE_UINT8(channel_disable, ADC128D818State),
> +        VMSTATE_BOOL(deep_shutdown, ADC128D818State),
> +        VMSTATE_UINT8(advanced_config, ADC128D818State),
> +        VMSTATE_UINT16_ARRAY(channels, ADC128D818State,
> +            ADC128D818_NUM_CHANNELS),
> +        VMSTATE_UINT8_ARRAY(limit, ADC128D818State,
> +            ADC128D818_NUM_CHANNELS * 2),
> +        VMSTATE_UINT8(len, ADC128D818State),
> +        VMSTATE_UINT8_ARRAY(buf, ADC128D818State, 2),
> +        VMSTATE_UINT8(outlen, ADC128D818State),
> +        VMSTATE_UINT8_ARRAY(outbuf, ADC128D818State, 2),
> +        VMSTATE_UINT8(pointer, ADC128D818State),
> +        VMSTATE_I2C_SLAVE(i2c, ADC128D818State),
> +        VMSTATE_END_OF_LIST()
> +    }
> +};
> +
> +static void adc128d818_init(Object *obj)
> +{
> +    /* Nothing to do */
> +}
> +
> +I2CSlave *adc128d818_init_with_values(I2CBus *bus, uint8_t address,
> +     const uint16_t *init_values, uint32_t init_values_size)
> +{
> +    ADC128D818State *s;
> +
> +    s = ADC128D818(i2c_slave_new(TYPE_ADC128D818, address));
> +
> +    for (int i = 0; i < ADC128D818_NUM_CHANNELS; i++) {
> +
> +        /* arbitrary value */
> +        uint16_t value = 0b0000101011010010;
> +
> +        if (i < init_values_size) {
> +            value = init_values[i];
> +        }
> +        s->channels[i] = value;
> +    }
> +
> +    i2c_slave_realize_and_unref(I2C_SLAVE(s), bus, &error_abort);
> +
> +    return I2C_SLAVE(s);
> +}
> +
> +static void adc128d818_reset(I2CSlave *i2c)
> +{
> +    ADC128D818State *s = ADC128D818(i2c);
> +
> +    s->pointer = 0;
> +    s->outlen = 0;
> +
> +    /* POR-State */
> +    s->config = 0b00001000;
> +    s->interrupt_mask = 0;
> +    s->conversion_rate = 0;
> +    s->channel_disable = 0;
> +    s->deep_shutdown = 0;
> +    s->advanced_config = 0;
> +
> +    /* No POR-State defined in datasheet */
> +    for (int i = 0; i < ADC128D818_NUM_CHANNELS * 2; i++) {
> +        s->limit[i] = 0;
> +    }
> +}
> +
> +static void adc128d818_realize(DeviceState *dev, Error **errp)
> +{
> +    ADC128D818State *s = ADC128D818(dev);
> +
> +    trace_adc128d818_realize(s->i2c.address);
> +
> +    adc128d818_reset(&s->i2c);

use dc->reset.

> +}
> +
> +static void adc128d818_class_init(ObjectClass *klass, const void *data)
> +{
> +    DeviceClass *dc = DEVICE_CLASS(klass);
> +    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
> +
> +    dc->realize = adc128d818_realize;
> +    dc->desc = "Texas Intstruments ADC128D818 12-bit ADC with temp sensor";

Intstruments

> +    dc->vmsd = &vmstate_adc128d818;
> +    k->event = adc128d818_event;
> +    k->recv = adc128d818_recv;
> +    k->send = adc128d818_send;
> +}
> +
> +static const TypeInfo adc128d818_info = {
> +    .name = TYPE_ADC128D818,
> +    .parent = TYPE_I2C_SLAVE,
> +    .instance_size = sizeof(ADC128D818State),
> +    .class_size = sizeof(ADC128D818Class),
> +    .instance_init = adc128d818_init,
> +    .class_init = adc128d818_class_init,
> +};
> +
> +static void adc128d818_register_types(void)
> +{
> +    type_register_static(&adc128d818_info);
> +}
> +
> +type_init(adc128d818_register_types)
> diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
> index a1e26604fa..defd7647e7 100644
> --- a/hw/sensor/meson.build
> +++ b/hw/sensor/meson.build
> @@ -10,3 +10,4 @@ system_ss.add(when: 'CONFIG_ISL_PMBUS_VR', if_true: files('isl_pmbus_vr.c'))
>   system_ss.add(when: 'CONFIG_MAX31785', if_true: files('max31785.c'))
>   system_ss.add(when: 'CONFIG_MAX31790', if_true: files('max31790.c'))
>   system_ss.add(when: 'CONFIG_MAX11615', if_true: files('max11615.c'))
> +system_ss.add(when: 'CONFIG_ADC128D818', if_true: files('adc128d818.c'))
> diff --git a/hw/sensor/trace-events b/hw/sensor/trace-events
> index 3fed979e85..4853f1944d 100644
> --- a/hw/sensor/trace-events
> +++ b/hw/sensor/trace-events
> @@ -20,3 +20,11 @@ max11615_recv(uint8_t i2c_addr, uint8_t reg_addr) "i2c_addr: 0x%02x, reg_addr: 0
>   max11615_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
>   max11615_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
>   max11615_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
> +
> +# adc128d818.c
> +adc128d818_send(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, data: 0x%02x"
> +adc128d818_write(uint8_t i2c_addr, uint8_t reg, uint8_t data) "i2c_addr: 0x%02x, reg: 0x%02x data: 0x%02x"
> +adc128d818_recv(uint8_t i2c_addr, uint8_t reg) "i2c_addr: 0x%02x, reg: 0x%02x"
> +adc128d818_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
> +adc128d818_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
> +adc128d818_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
> diff --git a/include/hw/sensor/adc128d818.h b/include/hw/sensor/adc128d818.h
> new file mode 100644
> index 0000000000..e2bdc47590
> --- /dev/null
> +++ b/include/hw/sensor/adc128d818.h
> @@ -0,0 +1,20 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +#ifndef QEMU_ADC128D818_H
> +#define QEMU_ADC128D818_H
> +
> +#include <stdint.h>
> +#include "hw/i2c/i2c.h"
> +
> +#define TYPE_ADC128D818 "adc128d818"
> +
> +/*
> + * Create and realize a adc128d818 ADC with constant caller-supplied readings
> + * @bus: I2C bus to put it on
> + * @address: I2C address
> + * @init_values: array of readings for each ADC channel
> + * @init_values_size: Size of @init_values, can be less than the number of channels
> + */
> +I2CSlave *adc128d818_init_with_values(I2CBus *bus, uint8_t address,
> +                                    const uint16_t *init_values, uint32_t init_values_size);
> +
> +#endif
> diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
> index 319f1a7672..883e058832 100755
> --- a/tests/functional/arm/test_aspeed_fby4.py
> +++ b/tests/functional/arm/test_aspeed_fby4.py
> @@ -59,6 +59,16 @@ def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
>           exec_command_and_wait_for_pattern(self,
>               "cat /sys/bus/iio/devices/iio:device2/in_voltage_scale", "0.500000000");
>   
> +        # ADC128D818 test
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/i2c/devices/30-001f/hwmon/hwmon0/name", "adc128d818");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/i2c/devices/30-001f/hwmon/hwmon0/in0_input", "75");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/i2c/devices/30-001f/hwmon/hwmon0/in0_min", "0");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/i2c/devices/30-001f/hwmon/hwmon0/in0_max", "0");
> +
>       def test_arm_ast2600_yosemitev4_openbmc(self):
>           image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
>   

Thanks,

C.



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

* Re: [PATCH v3 4/5] hw/sensor: support MAX11615
  2026-05-12 10:20 ` [PATCH v3 4/5] hw/sensor: support MAX11615 Alexander Hansen
  2026-05-12 16:55   ` Cédric Le Goater
@ 2026-05-12 16:58   ` Cédric Le Goater
  1 sibling, 0 replies; 12+ messages in thread
From: Cédric Le Goater @ 2026-05-12 16:58 UTC (permalink / raw)
  To: Alexander Hansen, qemu-devel
  Cc: Titus Rwantare, Philippe Mathieu-Daudé, Paolo Bonzini,
	Peter Maydell, qemu-arm, Steven Lee, Troy Lee, Jamin Lin,
	Kane Chen, Andrew Jeffery, Joel Stanley

On 5/12/26 12:20, Alexander Hansen wrote:
> Product: [1]
> Datasheet: [2]
> 
> Sensor readings can be provided upon creation of the device. In case no
> readings are provided the ADC reads a pre-defined arbitrary value.
> 
> root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/name
> max11615
> root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/in_voltage0_raw
> 1922
> root@yosemite4:~# cat /sys/bus/iio/devices/iio:device2/in_voltage_scale
> 0.500000000
> 
> trace:
> less /tmp/qemu-trace.log | grep -i max116
> max11615_realize i2c_addr: 0x33
> max11615_realize i2c_addr: 0x33
> max11615_event i2c_addr: 0x33, event: 0x01
> max11615_write_setup i2c_addr: 0x33, data: 0xd2
> max11615_write_config i2c_addr: 0x33, data: 0x0f
> max11615_event i2c_addr: 0x33, event: 0x03
> max11615_event i2c_addr: 0x33, event: 0x01
> max11615_write_setup i2c_addr: 0x33, data: 0xd2
> max11615_write_config i2c_addr: 0x33, data: 0x0f
> max11615_event i2c_addr: 0x33, event: 0x03
> max11615_event i2c_addr: 0x33, event: 0x01
> max11615_write_setup i2c_addr: 0x33, data: 0xd2
> max11615_write_config i2c_addr: 0x33, data: 0x61
> max11615_event i2c_addr: 0x33, event: 0x03
> max11615_event i2c_addr: 0x33, event: 0x00
> max11615_recv i2c_addr: 0x33, reg_addr: 0x00
> max11615_recv_return i2c_addr: 0x33, returns: 0xfa
> max11615_recv i2c_addr: 0x33, reg_addr: 0x00
> max11615_recv_return i2c_addr: 0x33, returns: 0xd2
> max11615_event i2c_addr: 0x33, event: 0x04
> max11615_event i2c_addr: 0x33, event: 0x03
> 
> References:
> [1] https://www.analog.com/en/products/MAX11615.html
> [2] https://www.analog.com/media/en/technical-documentation/data-sheets/MAX11612-MAX11617.pdf
> 
> Cc: Titus Rwantare <titusr@google.com>
> Cc: "Cédric Le Goater" <clg@kaod.org> (maintainer:ASPEED BMCs)
> Cc: "Philippe Mathieu-Daudé" <philmd@linaro.org> (odd fixer:Overall sensors)
> Cc: Paolo Bonzini <pbonzini@redhat.com> (maintainer:Kconfig)
> Cc: Peter Maydell <peter.maydell@linaro.org> (supporter:ARM TCG CPUs)
> Cc: qemu-devel@nongnu.org (open list:All patches CC here)
> Cc: qemu-arm@nongnu.org (open list:ARM TCG CPUs)
> Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
> ---
>   MAINTAINERS                              |   1 +
>   hw/arm/Kconfig                           |   1 +
>   hw/arm/aspeed_ast2600_fby4.c             |   8 +-
>   hw/sensor/Kconfig                        |   4 +
>   hw/sensor/max11615.c                     | 202 +++++++++++++++++++++++
>   hw/sensor/meson.build                    |   1 +
>   hw/sensor/trace-events                   |   8 +
>   include/hw/sensor/max11615.h             |  20 +++
>   tests/functional/arm/test_aspeed_fby4.py |   8 +
>   9 files changed, 252 insertions(+), 1 deletion(-)
>   create mode 100644 hw/sensor/max11615.c
>   create mode 100644 include/hw/sensor/max11615.h
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 9c991f8e70..a9c88996a2 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -3981,6 +3981,7 @@ S: Maintained
>   F: hw/i2c/pmbus_device.c
>   F: hw/sensor/adm1272.c
>   F: hw/sensor/isl_pmbus_vr.c
> +F: hw/sensor/max11615.c
>   F: hw/sensor/max31790.c
>   F: hw/sensor/max34451.c
>   F: include/hw/i2c/pmbus_device.h
> diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
> index 99864eb878..76a7d327a9 100644
> --- a/hw/arm/Kconfig
> +++ b/hw/arm/Kconfig
> @@ -553,6 +553,7 @@ config ASPEED_SOC
>       select PMBUS
>       select MAX31785
>       select MAX31790
> +    select MAX11615
>       select FSI_APB2OPB_ASPEED
>       select AT24C
>       select PCI_EXPRESS
> diff --git a/hw/arm/aspeed_ast2600_fby4.c b/hw/arm/aspeed_ast2600_fby4.c
> index a7c2dc09ea..036f5b9e38 100644
> --- a/hw/arm/aspeed_ast2600_fby4.c
> +++ b/hw/arm/aspeed_ast2600_fby4.c
> @@ -12,6 +12,7 @@
>   #include "hw/arm/aspeed_soc.h"
>   #include "hw/nvram/eeprom_at24c.h"
>   #include "hw/sensor/max31790.h"
> +#include "hw/sensor/max11615.h"
>   #include "hw/i2c/i2c_mux_pca954x.h"
>   #include "hw/gpio/pca9552.h"
>   
> @@ -186,7 +187,12 @@ static void fby4_i2c_init_fanboard(I2CSlave *fan_mux, size_t eepromSize)
>           i2c_slave_create_simple(bus, TYPE_MAX31790, 0x2f);
>   
>           /* maxim,max11615 @ 0x33   (adc) */
> -        /* TODO */
> +        static const uint16_t adc_values[8] = {
> +            0b011110000010, 0b010100011000,
> +            0b001000110100, 0b100000101001,
> +            0b011110000010, 0b010100011000,
> +            0b001000110100, 0b100000101001};
> +        max11615_init_with_values(bus, 0x33, adc_values, 8);
>   
>           at24c_eeprom_init_rom(
>               bus, 0x52, eepromSize,
> diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
> index ece2f2b167..84eede9d84 100644
> --- a/hw/sensor/Kconfig
> +++ b/hw/sensor/Kconfig
> @@ -47,3 +47,7 @@ config MAX31785
>   config MAX31790
>       bool
>       depends on PMBUS
> +
> +config MAX11615
> +    bool
> +    depends on I2C
> diff --git a/hw/sensor/max11615.c b/hw/sensor/max11615.c
> new file mode 100644
> index 0000000000..7950e00e33
> --- /dev/null
> +++ b/hw/sensor/max11615.c
> @@ -0,0 +1,202 @@
> +/*
> + * Maxim MAX11615 Low-Power 12 bit ADC
> + * Models MAX11612,MAX11613,MAX11614,MAX11615,MAX11616,MAX11617
> + *
> + * Datasheet:
> + * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX11612-MAX11617.pdf
> + *
> + * Copyright 2026 9elements
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + */
> +
> +#include "qemu/osdep.h"
> +#include "hw/i2c/i2c.h"
> +#include "migration/vmstate.h"
> +#include "qapi/error.h"
> +#include "qapi/visitor.h"
> +#include "qemu/log.h"
> +#include "qemu/module.h"
> +#include "trace.h"
> +#include "hw/sensor/max11615.h"
> +
> +#define MAX11615_NUM_CHANNELS 8
> +
> +struct MAX11615State {
> +    I2CSlave i2c;
> +
> +    uint16_t channels[MAX11615_NUM_CHANNELS];
> +
> +    /* output buffer */
> +    uint8_t outlen;
> +    uint8_t outbuf[2];
> +
> +    /* selected channel for read/write operation */
> +    uint8_t pointer;
> +};
> +
> +struct MAX11615Class {
> +    I2CSlaveClass parent_class;
> +};
> +
> +OBJECT_DECLARE_TYPE(MAX11615State, MAX11615Class, MAX11615)
> +
> +static void max11615_read(MAX11615State *s)
> +{
> +    /* read an ADC channel, first 4 bits must be high */
> +    uint8_t msb = s->channels[s->pointer] >> 8;
> +    uint8_t lsb = s->channels[s->pointer] & 0xff;
> +    s->outbuf[0] = 0b11110000 | (msb & 0b00001111);
> +    s->outbuf[1] = lsb;
> +}
> +
> +static void max11615_write_config_byte(MAX11615State *s, uint8_t data)
> +{
> +    trace_max11615_write_config(s->i2c.address, data);
> +
> +    uint8_t channelSelect = (data >> 1) & 0b1111;
> +
> +    /* Table 3. Channel Selection (AIN0 ... AIN11) */
> +    if (channelSelect > 11) {
> +        qemu_log_mask(LOG_GUEST_ERROR, "%s: invalid channel select", __func__);
> +        channelSelect = 11;
> +    }
> +    s->pointer = channelSelect;
> +}
> +
> +static void max11615_write_setup_byte(MAX11615State *s, uint8_t data)
> +{
> +    trace_max11615_write_setup(s->i2c.address, data);
> +    /* we ignore the setup byte, not implemented */
> +}
> +
> +static int max11615_send(I2CSlave *i2c, uint8_t data)
> +{
> +    MAX11615State *s = MAX11615(i2c);
> +    const uint8_t msb = (data >> 7) & 0b1;
> +
> +    if (msb) {
> +        max11615_write_setup_byte(s, data);
> +    } else {
> +        max11615_write_config_byte(s, data);
> +    }
> +
> +    s->outlen = 0;
> +    return 0;
> +}
> +
> +static uint8_t max11615_recv(I2CSlave *i2c)
> +{
> +    MAX11615State *s = MAX11615(i2c);
> +    trace_max11615_recv(s->i2c.address, s->pointer);
> +
> +    max11615_read(s);
> +
> +    if (s->outlen >= 2) {
> +        qemu_log_mask(LOG_GUEST_ERROR, "%s: too many bytes read", __func__);
> +        s->outlen = 0;
> +    }
> +
> +    const uint8_t data =  s->outbuf[s->outlen++];
> +
> +    trace_max11615_recv_return(s->i2c.address, data);
> +    return data;
> +}
> +
> +static int max11615_event(I2CSlave *i2c, enum i2c_event event)
> +{
> +    MAX11615State *s = MAX11615(i2c);
> +
> +    trace_max11615_event(s->i2c.address, event);
> +
> +    switch (event) {
> +    case I2C_START_RECV:
> +        s->outlen = 0;
> +        break;
> +    default:
> +        break;
> +    }
> +
> +    return 0;
> +}
> +
> +static const VMStateDescription vmstate_max11615 = {
> +    .name = TYPE_MAX11615,
> +    .version_id = 0,
> +    .minimum_version_id = 0,
> +    .fields = (const VMStateField[]){
> +        VMSTATE_UINT16_ARRAY(channels, MAX11615State, MAX11615_NUM_CHANNELS),
> +        VMSTATE_UINT8(outlen, MAX11615State),
> +        VMSTATE_UINT8_ARRAY(outbuf, MAX11615State, 2),
> +        VMSTATE_UINT8(pointer, MAX11615State),
> +        VMSTATE_I2C_SLAVE(i2c, MAX11615State),
> +        VMSTATE_END_OF_LIST()
> +    }
> +};
> +
> +static void max11615_init(Object *obj)
> +{
> +    /* Nothing to do */
> +}
> +
> +I2CSlave *max11615_init_with_values(I2CBus *bus, uint8_t address,
> +    const uint16_t *init_values, uint32_t init_values_size)
> +{
> +    MAX11615State *s;
> +
> +    s = MAX11615(i2c_slave_new(TYPE_MAX11615, address));
> +
> +    for (int i = 0; i < MAX11615_NUM_CHANNELS && i < init_values_size; i++) {
> +
> +        /* arbitrary value */
> +        uint16_t value = 0b0000101011010010;
> +
> +        if (i < init_values_size) {
> +            value = init_values[i];
> +        }
> +        s->channels[i] = value;
> +    }
> +
> +    i2c_slave_realize_and_unref(I2C_SLAVE(s), bus, &error_abort);
> +
> +    return I2C_SLAVE(s);
> +}
> +
> +static void max11615_realize(DeviceState *dev, Error **errp)
> +{
> +    MAX11615State *s = MAX11615(dev);
> +
> +    trace_max11615_realize(s->i2c.address);
> +
> +    s->pointer = 0;
> +    s->outlen = 0;

These should be in a reset handler.

Thanks,

C.

> +}
> +
> +static void max11615_class_init(ObjectClass *klass, const void *data)
> +{
> +    DeviceClass *dc = DEVICE_CLASS(klass);
> +    I2CSlaveClass *k = I2C_SLAVE_CLASS(klass);
> +
> +    dc->realize = max11615_realize;
> +    dc->desc = "Maxim MAX11615 12-bit ADC";
> +    dc->vmsd = &vmstate_max11615;
> +    k->event = max11615_event;
> +    k->recv = max11615_recv;
> +    k->send = max11615_send;
> +}
> +
> +static const TypeInfo max31790_info = {
> +    .name = TYPE_MAX11615,
> +    .parent = TYPE_I2C_SLAVE,
> +    .instance_size = sizeof(MAX11615State),
> +    .class_size = sizeof(MAX11615Class),
> +    .instance_init = max11615_init,
> +    .class_init = max11615_class_init,
> +};
> +
> +static void max31790_register_types(void)
> +{
> +    type_register_static(&max31790_info);
> +}
> +
> +type_init(max31790_register_types)
> diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
> index 4987c3b253..a1e26604fa 100644
> --- a/hw/sensor/meson.build
> +++ b/hw/sensor/meson.build
> @@ -9,3 +9,4 @@ system_ss.add(when: 'CONFIG_LSM303DLHC_MAG', if_true: files('lsm303dlhc_mag.c'))
>   system_ss.add(when: 'CONFIG_ISL_PMBUS_VR', if_true: files('isl_pmbus_vr.c'))
>   system_ss.add(when: 'CONFIG_MAX31785', if_true: files('max31785.c'))
>   system_ss.add(when: 'CONFIG_MAX31790', if_true: files('max31790.c'))
> +system_ss.add(when: 'CONFIG_MAX11615', if_true: files('max11615.c'))
> diff --git a/hw/sensor/trace-events b/hw/sensor/trace-events
> index c15c0a7e93..3fed979e85 100644
> --- a/hw/sensor/trace-events
> +++ b/hw/sensor/trace-events
> @@ -12,3 +12,11 @@ max31790_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns:
>   max31790_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
>   max31790_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
>   max31790_pwm_write(uint8_t i2c_addr, size_t index, uint16_t value) "i2c_addr: 0x%02x, index: %zu, value: 0x%04x"
> +
> +# max11615.c
> +max11615_write_setup(uint8_t i2c_addr, uint8_t send) "i2c_addr: 0x%02x, data: 0x%02x"
> +max11615_write_config(uint8_t i2c_addr, uint8_t send) "i2c_addr: 0x%02x, data: 0x%02x"
> +max11615_recv(uint8_t i2c_addr, uint8_t reg_addr) "i2c_addr: 0x%02x, reg_addr: 0x%02x"
> +max11615_recv_return(uint8_t i2c_addr, uint8_t data) "i2c_addr: 0x%02x, returns: 0x%02x"
> +max11615_event(uint8_t i2c_addr, uint8_t event) "i2c_addr: 0x%02x, event: 0x%02x"
> +max11615_realize(uint8_t i2c_addr) "i2c_addr: 0x%02x"
> diff --git a/include/hw/sensor/max11615.h b/include/hw/sensor/max11615.h
> new file mode 100644
> index 0000000000..ccc0c9bc2e
> --- /dev/null
> +++ b/include/hw/sensor/max11615.h
> @@ -0,0 +1,20 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +#ifndef QEMU_MAX11615_H
> +#define QEMU_MAX11615_H
> +
> +#include <stdint.h>
> +#include "hw/i2c/i2c.h"
> +
> +#define TYPE_MAX11615 "max11615"
> +
> +/*
> + * Create and realize a MAX11615 ADC with constant caller-supplied readings
> + * @bus: I2C bus to put it on
> + * @address: I2C address
> + * @init_values: array of readings for each ADC channel
> + * @init_values_size: Size of @init_values, can be less than the number of channels
> + */
> +I2CSlave *max11615_init_with_values(I2CBus *bus, uint8_t address,
> +                                    const uint16_t *init_values, uint32_t init_values_size);
> +
> +#endif
> diff --git a/tests/functional/arm/test_aspeed_fby4.py b/tests/functional/arm/test_aspeed_fby4.py
> index d29423add7..319f1a7672 100755
> --- a/tests/functional/arm/test_aspeed_fby4.py
> +++ b/tests/functional/arm/test_aspeed_fby4.py
> @@ -51,6 +51,14 @@ def do_test_arm_aspeed_openbmc_no_network(self, machine, image, uboot,
>           exec_command_and_wait_for_pattern(self,
>               "cat /sys/class/hwmon/hwmon2/fan1_fault", "0");
>   
> +        # MAX11615 test
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/iio/devices/iio:device2/name", "max11615");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/iio/devices/iio:device2/in_voltage0_raw", "1922");
> +        exec_command_and_wait_for_pattern(self,
> +            "cat /sys/bus/iio/devices/iio:device2/in_voltage_scale", "0.500000000");
> +
>       def test_arm_ast2600_yosemitev4_openbmc(self):
>           image_path = self.uncompress(self.ASSET_YOSEMITE_V4_FLASH)
>   



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

end of thread, other threads:[~2026-05-12 16:58 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-12 10:20 [PATCH v3 0/5] initial support for yosemite v4 Alexander Hansen
2026-05-12 10:20 ` [PATCH v3 1/5] ast2600: yosemite4 initial support Alexander Hansen
2026-05-12 16:13   ` Cédric Le Goater
2026-05-12 10:20 ` [PATCH v3 2/5] ast2600: yosemite4 functional test Alexander Hansen
2026-05-12 16:34   ` Cédric Le Goater
2026-05-12 10:20 ` [PATCH v3 3/5] hw/sensor: MAX31790 support Alexander Hansen
2026-05-12 16:51   ` Cédric Le Goater
2026-05-12 10:20 ` [PATCH v3 4/5] hw/sensor: support MAX11615 Alexander Hansen
2026-05-12 16:55   ` Cédric Le Goater
2026-05-12 16:58   ` Cédric Le Goater
2026-05-12 10:20 ` [PATCH v3 5/5] hw/sensor: support Texas Instruments ADC128D818 Alexander Hansen
2026-05-12 16:57   ` Cédric Le Goater

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.