[PATCH] hw/sensor: MAX31790 support

Alexander Hansen posted 1 patch 1 month, 1 week ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/qemu tags/patchew/20260421162856.298260-1-alexander.hansen@9elements.com
Maintainers: Paolo Bonzini <pbonzini@redhat.com>, Peter Maydell <peter.maydell@linaro.org>, "Philippe Mathieu-Daudé" <philmd@linaro.org>
hw/arm/Kconfig               |   1 +
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 +
6 files changed, 520 insertions(+)
create mode 100644 hw/sensor/max31790.c
create mode 100644 include/hw/sensor/max31790.h
[PATCH] hw/sensor: MAX31790 support
Posted by Alexander Hansen 1 month, 1 week ago
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.

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: 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>
---
 hw/arm/Kconfig               |   1 +
 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 +
 6 files changed, 520 insertions(+)
 create mode 100644 hw/sensor/max31790.c
 create mode 100644 include/hw/sensor/max31790.h

diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 4e50fb1111..c2483e4c63 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/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..ec6bb394f4
--- /dev/null
+++ b/hw/sensor/max31790.c
@@ -0,0 +1,499 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Maxim MAX31790 PMBus 6-Channel Fan Controller
+ *
+ * Datasheet:
+ * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX31790.pdf
+ *
+ * Copyright(c) 2022 Qualcomm Innovation Center, Inc. All rights reserved.
+ */
+
+#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/max31790.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_realize(DeviceState *dev, Error **errp)
+{
+    MAX31790State *s = MAX31790(dev);
+
+    trace_max31790_realize(s->i2c.address);
+
+    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_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
-- 
2.53.0


Re: [PATCH] hw/sensor: MAX31790 support
Posted by Peter Maydell 1 month ago
On Tue, 21 Apr 2026 at 17:30, Alexander Hansen
<alexander.hansen@9elements.com> 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.

Hi; thanks for this patch.

From our conversation on IRC: you say you want this as part of
modelling the aspeed yosemite4 board. (I've cc'd Cedric as aspeed
maintainer). For upstream QEMU we generally prefer new device
models to be sent as part of the patch series that adds the
board they're part of, so that they have a use upstream.

The code itself looks mostly in good shape; I have a few
first-pass comments below.

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


> Tested: on yosemite 4 qemu
>
> ```

Commit messages aren't markdown, they're plain text.

The commit message also ideally ought to give the rationale
for implementing the device (though this is a bit less
necessary when it's part of a larger series adding board
support).

> 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: 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>
> ---

> +++ b/hw/sensor/max31790.c
> @@ -0,0 +1,499 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Maxim MAX31790 PMBus 6-Channel Fan Controller
> + *
> + * Datasheet:
> + * https://www.analog.com/media/en/technical-documentation/data-sheets/MAX31790.pdf
> + *
> + * Copyright(c) 2022 Qualcomm Innovation Center, Inc. All rights reserved.

Copyright doesn't seem to match your email address, and the
date is very old: is this code that's been in a downstream
fork that you've picked up?

> + */


> +static void max31790_realize(DeviceState *dev, Error **errp)
> +{
> +    MAX31790State *s = MAX31790(dev);
> +
> +    trace_max31790_realize(s->i2c.address);
> +
> +    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;

Reset should be in a reset method, not realize.

> +    }
> +
> +    for (int i = 0; i < MAX31790_NUM_TACHS; i++) {
> +        max31790_set_rpm(s, i, 0x4444);
> +    }
> +}

thanks
-- PMM