* [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.