Add a model for the Texas Instruments INA238, an 85V, 16-bit I2C
current-shunt, power and temperature monitor.

The device measures a differential shunt voltage, a bus voltage and its
own die temperature; the current and power registers are not measured
directly but derived by the device from the shunt voltage and the
guest-programmed Shunt Calibration register. The alert is modelled.

Shunt and bus voltages and the die temperature can be injected through
the "shunt-voltage", "bus-voltage" and "die-temperature" QOM properties.

Note: Conversion is instantaneous: the averaging and conversion-time
fields are stored but have no timing effect.

Signed-off-by: Emmanuel Blot <[email protected]>
---
 hw/arm/Kconfig             |   1 +
 hw/sensor/Kconfig          |   4 +
 hw/sensor/ina238.c         | 802 +++++++++++++++++++++++++++++++++++++++++++++
 hw/sensor/meson.build      |   1 +
 hw/sensor/trace-events     |   7 +
 include/hw/sensor/ina238.h |  14 +
 6 files changed, 829 insertions(+)

diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index 1b33877d5c..97c97aeef8 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -539,6 +539,7 @@ config ASPEED_SOC
     select TMP421
     select EMC141X
     select INA230
+    select INA238
     select OR_IRQ
     select UNIMP
     select LED
diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig
index da519159ac..9b7c76fcf5 100644
--- a/hw/sensor/Kconfig
+++ b/hw/sensor/Kconfig
@@ -6,6 +6,10 @@ config INA230
     bool
     depends on I2C
 
+config INA238
+    bool
+    depends on I2C
+
 config TMP105
     bool
     depends on I2C
diff --git a/hw/sensor/ina238.c b/hw/sensor/ina238.c
new file mode 100644
index 0000000000..e4033771b0
--- /dev/null
+++ b/hw/sensor/ina238.c
@@ -0,0 +1,802 @@
+/*
+ * Texas Instruments INA238 85V, 16-bit current/voltage/power monitor
+ * with an I2C interface.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * The INA238 measures a differential shunt voltage, a bus voltage and its own
+ * die temperature. Current and power are not measured directly: they are
+ * derived by the device from the measured shunt voltage and the programmable
+ * Shunt Calibration register. The INA238 exposes six independent limit
+ * registers, all monitored simultaneously (multi-alert), a die-temperature
+ * sensor, a 24-bit power register and an ADCRANGE-dependent shunt LSB.
+ *
+ * Limitations (not modelled):
+ * - Conversion timing is instantaneous. Conversions run synchronously when an
+ *   input, the calibration, the ADC range or the ADC configuration changes; 
the
+ *   ADC conversion times (VBUSCT/VSHCT/VTCT), the averaging count (AVG) and 
the
+ *   CONVDLY start delay are not modelled, and continuous mode does not
+ *   re-convert on a timer. A read always returns the value derived from the
+ *   current inputs and CNVRF is set once a conversion has run.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/log.h"
+#include "qapi/error.h"
+#include "qapi/visitor.h"
+#include "qom/object.h"
+#include "hw/sensor/ina238.h"
+#include "hw/core/irq.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/registerfields.h"
+#include "hw/i2c/i2c.h"
+#include "migration/vmstate.h"
+#include "trace.h"
+
+#define INA238_REG_CONFIG          0x00
+#define INA238_REG_ADC_CONFIG      0x01
+#define INA238_REG_SHUNT_CAL       0x02
+#define INA238_REG_VSHUNT          0x04
+#define INA238_REG_VBUS            0x05
+#define INA238_REG_DIETEMP         0x06
+#define INA238_REG_CURRENT         0x07
+#define INA238_REG_POWER           0x08
+#define INA238_REG_DIAG_ALRT       0x0B
+#define INA238_REG_SOVL            0x0C
+#define INA238_REG_SUVL            0x0D
+#define INA238_REG_BOVL            0x0E
+#define INA238_REG_BUVL            0x0F
+#define INA238_REG_TEMP_LIMIT      0x10
+#define INA238_REG_PWR_LIMIT       0x11
+#define INA238_REG_MANUFACTURER_ID 0x3E
+#define INA238_REG_DEVICE_ID       0x3F
+
+FIELD(INA238_CONFIG, ADCRANGE, 4, 1)
+FIELD(INA238_CONFIG, CONVDLY,  6, 8)
+FIELD(INA238_CONFIG, RST,     15, 1)
+
+#define INA238_CONFIG_POR       0x0000
+#define INA238_CONFIG_WMASK     (R_INA238_CONFIG_ADCRANGE_MASK | \
+                                 R_INA238_CONFIG_CONVDLY_MASK)
+
+FIELD(INA238_ADC_CONFIG, AVG,     0, 3)
+FIELD(INA238_ADC_CONFIG, VTCT,    3, 3)
+FIELD(INA238_ADC_CONFIG, VSHCT,   6, 3)
+FIELD(INA238_ADC_CONFIG, VBUSCT,  9, 3)
+FIELD(INA238_ADC_CONFIG, MODE,   12, 4)
+
+#define INA238_ADC_CONFIG_POR   0xFB68
+
+#define INA238_SHUNT_CAL_POR    0x1000
+#define INA238_SHUNT_CAL_MASK   0x7FFF   /* bit 15 reserved */
+
+FIELD(INA238_DIAG_ALRT, MEMSTAT,   0, 1)  /* memory checksum ok (always 1) */
+FIELD(INA238_DIAG_ALRT, CNVRF,     1, 1)  /* conversion ready flag */
+FIELD(INA238_DIAG_ALRT, POL,       2, 1)  /* power over-limit */
+FIELD(INA238_DIAG_ALRT, BUSUL,     3, 1)  /* bus under-limit */
+FIELD(INA238_DIAG_ALRT, BUSOL,     4, 1)  /* bus over-limit */
+FIELD(INA238_DIAG_ALRT, SHNTUL,    5, 1)  /* shunt under-limit */
+FIELD(INA238_DIAG_ALRT, SHNTOL,    6, 1)  /* shunt over-limit */
+FIELD(INA238_DIAG_ALRT, TMPOL,     7, 1)  /* over temperature */
+FIELD(INA238_DIAG_ALRT, MATHOF,    9, 1)  /* arithmetic overflow */
+FIELD(INA238_DIAG_ALRT, APOL,     12, 1)  /* alert polarity */
+FIELD(INA238_DIAG_ALRT, SLOWALERT, 13, 1)  /* alert on averaged value */
+FIELD(INA238_DIAG_ALRT, CNVR,     14, 1)  /* conversion-ready on ALERT pin */
+FIELD(INA238_DIAG_ALRT, ALATCH,   15, 1)  /* alert latch enable */
+
+#define INA238_DIAG_ALRT_POR    0x0001    /* MEMSTAT = 1 */
+
+/* Control (writable) bits */
+#define INA238_DIAG_CTRL_MASK   (R_INA238_DIAG_ALRT_ALATCH_MASK | \
+                                 R_INA238_DIAG_ALRT_CNVR_MASK | \
+                                 R_INA238_DIAG_ALRT_SLOWALERT_MASK | \
+                                 R_INA238_DIAG_ALRT_APOL_MASK)
+/* Read-only status bits (preserved across a control-bit write) */
+#define INA238_DIAG_STATUS_MASK (R_INA238_DIAG_ALRT_MATHOF_MASK | \
+                                 R_INA238_DIAG_ALRT_TMPOL_MASK | \
+                                 R_INA238_DIAG_ALRT_SHNTOL_MASK | \
+                                 R_INA238_DIAG_ALRT_SHNTUL_MASK | \
+                                 R_INA238_DIAG_ALRT_BUSOL_MASK | \
+                                 R_INA238_DIAG_ALRT_BUSUL_MASK | \
+                                 R_INA238_DIAG_ALRT_POL_MASK | \
+                                 R_INA238_DIAG_ALRT_CNVRF_MASK | \
+                                 R_INA238_DIAG_ALRT_MEMSTAT_MASK)
+/* Threshold flags cleared when the DIAG_ALRT register is read */
+#define INA238_DIAG_LATCH_MASK  (R_INA238_DIAG_ALRT_TMPOL_MASK | \
+                                 R_INA238_DIAG_ALRT_SHNTOL_MASK | \
+                                 R_INA238_DIAG_ALRT_SHNTUL_MASK | \
+                                 R_INA238_DIAG_ALRT_BUSOL_MASK | \
+                                 R_INA238_DIAG_ALRT_BUSUL_MASK | \
+                                 R_INA238_DIAG_ALRT_POL_MASK)
+
+#define INA238_SOVL_POR         0x7FFF
+#define INA238_SUVL_POR         0x8000
+#define INA238_BOVL_POR         0x7FFF
+#define INA238_BUVL_POR         0x0000
+/*
+ * The datasheet is self-inconsistent on the TEMP_LIMIT reset value.
+ * Writes mask with INA238_TEMP_LIMIT_MASK and the alert comparison shifts both
+ * operands >>4.
+ */
+#define INA238_TEMP_LIMIT_POR   0x7FF0
+#define INA238_PWR_LIMIT_POR    0xFFFF
+
+#define INA238_BUS_LIMIT_MASK   0x7FFF    /* BOVL/BUVL are 15-bit, positive */
+#define INA238_TEMP_LIMIT_MASK  0xFFF0    /* TOL occupies bits [15:4] */
+
+#define INA238_MANUFACTURER_ID_VAL 0x5449 /* "TI" */
+#define INA238_DEVICE_ID_VAL       0x2381 /* DIEID 0x238, REV 0x1 */
+
+/* Shunt voltage LSB depends on ADCRANGE; bus and temperature are fixed */
+#define INA238_SHUNT_LSB_NV_R0  5000     /* 5 uV per LSB (ADCRANGE = 0) */
+#define INA238_SHUNT_LSB_NV_R1  1250     /* 1.25 uV per LSB (ADCRANGE = 1) */
+#define INA238_BUS_LSB_UV       3125     /* 3.125 mV per LSB */
+#define INA238_TEMP_LSB_MC      125      /* 125 m-degC per LSB */
+
+/*
+ * current = shunt_reg * K / SHUNT_CAL. K derives from the fixed 819.2e6
+ * internal constant and the ADCRANGE=0 shunt LSB: 5e-6 * 819.2e6 = 4096.
+ * power = current * bus / 64 (0.2 / 3.125mV expressed in the register domain).
+ */
+#define INA238_CURRENT_K        4096
+#define INA238_POWER_DIV        64
+#define INA238_POWER_MAX        0xFFFFFF /* POWER is a 24-bit register */
+
+/* Injection property ranges */
+#define INA238_SHUNT_MIN_NV     (-163840000)  /* -32768 * 5 uV */
+#define INA238_SHUNT_MAX_NV     163835000      /*  32767 * 5 uV */
+#define INA238_BUS_MAX_UV       85000000       /* 85 V full common-mode range 
*/
+#define INA238_TEMP_MIN_MC      (-40000)       /* package limited */
+#define INA238_TEMP_MAX_MC      125000
+
+OBJECT_DECLARE_SIMPLE_TYPE(INA238State, INA238)
+
+struct INA238State {
+    I2CSlave parent_obj;
+
+    qemu_irq alert; /* ALERT open-drain output */
+
+    /* I2C transfer state */
+    uint8_t ptr;      /* register pointer */
+    uint8_t len;      /* bytes received in current transfer */
+    uint8_t wr_hi;    /* pending write MSB */
+    uint8_t rx_byte;  /* index of the next byte to return within rx_val */
+    uint8_t rx_width; /* width in bytes of the register being read (2 or 3) */
+    uint32_t rx_val;  /* value latched at the start of a read */
+
+    /* Register file */
+    uint16_t config;
+    uint16_t adc_config;
+    uint16_t shunt_cal;
+    uint16_t diag_alrt;
+    uint16_t sovl;
+    uint16_t suvl;
+    uint16_t bovl;
+    uint16_t buvl;
+    uint16_t temp_limit;
+    uint16_t pwr_limit;
+
+    /* Derived measurement registers */
+    uint16_t shunt_reg;
+    uint16_t bus_reg;
+    uint16_t dietemp_reg;
+    uint16_t current_reg;
+    uint32_t power_reg;   /* 24-bit */
+
+    /* Injected physical inputs */
+    int32_t shunt_nv;  /* shunt voltage, nanovolts (signed: bidirectional) */
+    uint32_t bus_uv;   /* bus voltage, microvolts (never negative) */
+    int32_t temp_mc;   /* die temperature, millidegrees Celsius (signed) */
+
+    char *description;
+};
+
+/*
+ * Update the _field_ limit flag in _diag_ from the current comparison _cond_.
+ * In latch mode (_latch_) a flag that is already set stays set until the
+ * register is read; in transparent mode it tracks _cond_ directly. A flag
+ * whose measurement register was not refreshed (_fresh_) keeps its value.
+ */
+#define INA238_UPD_FLAG(_diag_, _latch_, _fresh_, _field_, _cond_)            \
+    do {                                                                      \
+        if (_fresh_) {                                                        \
+            uint16_t _old_ = FIELD_EX16(_diag_, INA238_DIAG_ALRT, _field_);   \
+            uint16_t _new_ = (uint16_t)((_cond_) || ((_latch_) && _old_));    \
+            (_diag_) = FIELD_DP16(_diag_, INA238_DIAG_ALRT, _field_, _new_);  \
+        }                                                                     \
+    } while (0)
+
+static void ina238_refresh_alert(INA238State *s)
+{
+    uint16_t diag = s->diag_alrt;
+    bool active;
+    bool cnvr;
+    int level;
+
+    /* MATHOF is a diagnostic; it does not drive the ALERT pin. */
+    active = FIELD_EX16(diag, INA238_DIAG_ALRT, SHNTOL) ||
+             FIELD_EX16(diag, INA238_DIAG_ALRT, SHNTUL) ||
+             FIELD_EX16(diag, INA238_DIAG_ALRT, BUSOL) ||
+             FIELD_EX16(diag, INA238_DIAG_ALRT, BUSUL) ||
+             FIELD_EX16(diag, INA238_DIAG_ALRT, TMPOL) ||
+             FIELD_EX16(diag, INA238_DIAG_ALRT, POL);
+
+    cnvr = FIELD_EX16(diag, INA238_DIAG_ALRT, CNVR) &&
+           FIELD_EX16(diag, INA238_DIAG_ALRT, CNVRF);
+
+    active = active || cnvr;
+    /* APOL: 0 = active-low (default), 1 = active-high */
+    level = FIELD_EX16(diag, INA238_DIAG_ALRT, APOL) ? active : !active;
+
+    trace_ina238_alert(s->description, level);
+    qemu_set_irq(s->alert, level);
+}
+
+/*
+ * @shunt_fresh, @bus_fresh and @temp_fresh tell which measurement registers
+ * hold a fresh value: after a partial conversion the flags fed by the other
+ * channels must not be re-evaluated against their stale registers.
+ */
+static void ina238_update_alert(INA238State *s, bool shunt_fresh,
+                                bool bus_fresh, bool temp_fresh,
+                                bool power_fresh)
+{
+    bool latch = FIELD_EX16(s->diag_alrt, INA238_DIAG_ALRT, ALATCH);
+    int16_t shunt = (int16_t)s->shunt_reg;
+    int16_t temp = (int16_t)s->dietemp_reg >> 4;      /* sign-extend [15:4] */
+    int16_t temp_lim = (int16_t)s->temp_limit >> 4;
+    uint16_t diag = s->diag_alrt;
+
+    INA238_UPD_FLAG(diag, latch, shunt_fresh, SHNTOL, shunt > 
(int16_t)s->sovl);
+    INA238_UPD_FLAG(diag, latch, shunt_fresh, SHNTUL, shunt < 
(int16_t)s->suvl);
+    INA238_UPD_FLAG(diag, latch, bus_fresh, BUSOL,
+                    s->bus_reg > (s->bovl & INA238_BUS_LIMIT_MASK));
+    INA238_UPD_FLAG(diag, latch, bus_fresh, BUSUL,
+                    s->bus_reg < (s->buvl & INA238_BUS_LIMIT_MASK));
+    INA238_UPD_FLAG(diag, latch, temp_fresh, TMPOL, temp > temp_lim);
+    INA238_UPD_FLAG(diag, latch, power_fresh, POL,
+                    s->power_reg > ((uint32_t)s->pwr_limit << 8));
+
+    s->diag_alrt = diag;
+
+    ina238_refresh_alert(s);
+}
+
+static void ina238_convert(INA238State *s)
+{
+    uint8_t mode = FIELD_EX16(s->adc_config, INA238_ADC_CONFIG, MODE);
+    bool bus_en = mode & 0x1;
+    bool shunt_en = mode & 0x2;
+    bool temp_en = mode & 0x4;
+    bool adcrange = FIELD_EX16(s->config, INA238_CONFIG, ADCRANGE);
+    int shunt_lsb = adcrange ? INA238_SHUNT_LSB_NV_R1 : INA238_SHUNT_LSB_NV_R0;
+    int64_t sr, br, tr, cur, pwr;
+    uint16_t mathof = 0;
+    bool power_fresh = bus_en;
+
+    if (!bus_en && !shunt_en && !temp_en) {
+        return;
+    }
+
+    if (shunt_en) {
+        sr = s->shunt_nv / shunt_lsb;
+        sr = MAX(INT16_MIN, MIN(INT16_MAX, sr));
+        s->shunt_reg = (uint16_t)(int16_t)sr;
+
+        if (s->shunt_cal == 0) {
+            cur = 0;
+        } else {
+            cur = (int64_t)(int16_t)s->shunt_reg * INA238_CURRENT_K /
+                  s->shunt_cal;
+        }
+        if (cur > INT16_MAX || cur < INT16_MIN) {
+            mathof = 1;
+            cur = MAX(INT16_MIN, MIN(INT16_MAX, cur));
+        }
+        s->current_reg = (uint16_t)(int16_t)cur;
+    }
+
+    if (bus_en) {
+        br = s->bus_uv / INA238_BUS_LSB_UV;
+        br = MAX(0, MIN(INT16_MAX, br));
+        s->bus_reg = (uint16_t)br;
+    }
+
+    if (temp_en) {
+        /* Die temperature: 12-bit signed value stored in bits [15:4] */
+        tr = s->temp_mc / INA238_TEMP_LSB_MC;
+        tr = MAX(-2048, MIN(2047, tr));
+        s->dietemp_reg = ((uint16_t)(int16_t)tr << 4) & INA238_TEMP_LIMIT_MASK;
+    }
+
+    /*
+     * In bus-only mode the current register is retained from the last shunt
+     * conversion rather than measured anew, but the power register is still
+     * refreshed.
+     */
+    if (bus_en) {
+        cur = (int16_t)s->current_reg;
+        if (cur < 0) {
+            cur = -cur;
+        }
+        pwr = cur * (int64_t)s->bus_reg / INA238_POWER_DIV;
+        if (pwr > INA238_POWER_MAX) {
+            mathof = 1;
+            pwr = INA238_POWER_MAX;
+        }
+        s->power_reg = (uint32_t)pwr;
+    }
+
+    /* Without a calibration the device reports neither current nor power. */
+    if (s->shunt_cal == 0) {
+        s->current_reg = 0;
+        s->power_reg = 0;
+        power_fresh = true;
+    }
+
+    s->diag_alrt = FIELD_DP16(s->diag_alrt, INA238_DIAG_ALRT, MATHOF, mathof);
+    s->diag_alrt = FIELD_DP16(s->diag_alrt, INA238_DIAG_ALRT, CNVRF, 1);
+
+    trace_ina238_convert(s->description, s->shunt_reg, s->bus_reg,
+                         s->dietemp_reg, s->current_reg, s->power_reg);
+
+    ina238_update_alert(s, shunt_en, bus_en, temp_en, power_fresh);
+}
+
+/*
+ * A physical input, the calibration or the ADC range changed.
+ * - in continuous mode the device keeps converting and immediately tracks
+ *   the new value
+ * - in a triggered mode it performs exactly one conversion per trigger
+ */
+static void ina238_input_update(INA238State *s)
+{
+    if (FIELD_EX16(s->adc_config, INA238_ADC_CONFIG, MODE) & 0x8) {
+        ina238_convert(s);
+    }
+}
+
+static void ina238_reset_regs(INA238State *s)
+{
+    s->config = INA238_CONFIG_POR;
+    s->adc_config = INA238_ADC_CONFIG_POR;
+    s->shunt_cal = INA238_SHUNT_CAL_POR;
+    s->diag_alrt = INA238_DIAG_ALRT_POR;
+    s->sovl = INA238_SOVL_POR;
+    s->suvl = INA238_SUVL_POR;
+    s->bovl = INA238_BOVL_POR;
+    s->buvl = INA238_BUVL_POR;
+    s->temp_limit = INA238_TEMP_LIMIT_POR;
+    s->pwr_limit = INA238_PWR_LIMIT_POR;
+
+    s->ptr = 0;
+    s->len = 0;
+    s->wr_hi = 0;
+    s->rx_byte = 0;
+    s->rx_width = 0;
+    s->rx_val = 0;
+
+    /*
+     * The measurement registers power up at zero and stay zero until the next
+     * conversion. The injected physical inputs are retained, but no conversion
+     * is performed here, so a software reset does not resurrect a previous
+     * measurement.
+     */
+    s->shunt_reg = 0;
+    s->bus_reg = 0;
+    s->dietemp_reg = 0;
+    s->current_reg = 0;
+    s->power_reg = 0;
+
+    ina238_update_alert(s, true, true, true, true);
+}
+
+static uint32_t ina238_read_reg(INA238State *s, uint8_t reg)
+{
+    uint32_t val;
+
+    switch (reg) {
+    case INA238_REG_CONFIG:
+        val = s->config;
+        break;
+    case INA238_REG_ADC_CONFIG:
+        val = s->adc_config;
+        break;
+    case INA238_REG_SHUNT_CAL:
+        val = s->shunt_cal;
+        break;
+    case INA238_REG_VSHUNT:
+        val = s->shunt_reg;
+        break;
+    case INA238_REG_VBUS:
+        val = s->bus_reg;
+        break;
+    case INA238_REG_DIETEMP:
+        val = s->dietemp_reg;
+        break;
+    case INA238_REG_CURRENT:
+        val = s->current_reg;
+        break;
+    case INA238_REG_POWER:
+        val = s->power_reg;
+        break;
+    case INA238_REG_DIAG_ALRT:
+        val = s->diag_alrt;
+        /*
+         * A read always clears CNVRF; the latched limit flags clear too, but
+         * only in latch mode.
+         */
+        s->diag_alrt &= ~R_INA238_DIAG_ALRT_CNVRF_MASK;
+        if (FIELD_EX16(s->diag_alrt, INA238_DIAG_ALRT, ALATCH)) {
+            s->diag_alrt &= ~INA238_DIAG_LATCH_MASK;
+        }
+        ina238_refresh_alert(s);
+        break;
+    case INA238_REG_SOVL:
+        val = s->sovl;
+        break;
+    case INA238_REG_SUVL:
+        val = s->suvl;
+        break;
+    case INA238_REG_BOVL:
+        val = s->bovl;
+        break;
+    case INA238_REG_BUVL:
+        val = s->buvl;
+        break;
+    case INA238_REG_TEMP_LIMIT:
+        val = s->temp_limit;
+        break;
+    case INA238_REG_PWR_LIMIT:
+        val = s->pwr_limit;
+        break;
+    case INA238_REG_MANUFACTURER_ID:
+        val = INA238_MANUFACTURER_ID_VAL;
+        break;
+    case INA238_REG_DEVICE_ID:
+        val = INA238_DEVICE_ID_VAL;
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: %s: read from undefined register 0x%02x\n",
+                      __func__, s->description, reg);
+        val = 0;
+        break;
+    }
+
+    trace_ina238_read(s->description, reg, val);
+    return val;
+}
+
+static void ina238_write_reg(INA238State *s, uint8_t reg, uint16_t val)
+{
+    trace_ina238_write(s->description, reg, val);
+
+    switch (reg) {
+    case INA238_REG_CONFIG:
+        if (val & R_INA238_CONFIG_RST_MASK) {
+            trace_ina238_reset(s->description, "reg");
+            ina238_reset_regs(s);
+            break;
+        }
+        s->config = val & INA238_CONFIG_WMASK;
+        ina238_input_update(s);
+        break;
+    case INA238_REG_ADC_CONFIG:
+        s->adc_config = val;
+        /* Writing the MODE bits (re)starts a conversion: it is the trigger. */
+        ina238_convert(s);
+        break;
+    case INA238_REG_SHUNT_CAL:
+        s->shunt_cal = val & INA238_SHUNT_CAL_MASK;
+        ina238_input_update(s);
+        break;
+    case INA238_REG_DIAG_ALRT: {
+        uint16_t status = s->diag_alrt & INA238_DIAG_STATUS_MASK;
+        /*
+         * In latch mode a write clears the latched limit flags and the
+         * conversion-ready flag. In transparent mode the flags track the live
+         * condition and are preserved. The pin is only refreshed, so a cleared
+         * flag re-asserts on the next conversion rather than immediately.
+         */
+        if (FIELD_EX16(s->diag_alrt, INA238_DIAG_ALRT, ALATCH)) {
+            status &= ~(R_INA238_DIAG_ALRT_CNVRF_MASK | 
INA238_DIAG_LATCH_MASK);
+        }
+        s->diag_alrt = (val & INA238_DIAG_CTRL_MASK) | status;
+        ina238_refresh_alert(s);
+        break;
+    }
+    case INA238_REG_SOVL:
+        s->sovl = val;
+        ina238_update_alert(s, true, true, true, true);
+        break;
+    case INA238_REG_SUVL:
+        s->suvl = val;
+        ina238_update_alert(s, true, true, true, true);
+        break;
+    case INA238_REG_BOVL:
+        s->bovl = val & INA238_BUS_LIMIT_MASK;
+        ina238_update_alert(s, true, true, true, true);
+        break;
+    case INA238_REG_BUVL:
+        s->buvl = val & INA238_BUS_LIMIT_MASK;
+        ina238_update_alert(s, true, true, true, true);
+        break;
+    case INA238_REG_TEMP_LIMIT:
+        s->temp_limit = val & INA238_TEMP_LIMIT_MASK;
+        ina238_update_alert(s, true, true, true, true);
+        break;
+    case INA238_REG_PWR_LIMIT:
+        s->pwr_limit = val;
+        ina238_update_alert(s, true, true, true, true);
+        break;
+    case INA238_REG_VSHUNT:
+    case INA238_REG_VBUS:
+    case INA238_REG_DIETEMP:
+    case INA238_REG_CURRENT:
+    case INA238_REG_POWER:
+    case INA238_REG_MANUFACTURER_ID:
+    case INA238_REG_DEVICE_ID:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: %s: write to read-only register 0x%02x\n",
+                      __func__, s->description, reg);
+        break;
+    default:
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: %s: write to undefined register 0x%02x\n",
+                      __func__, s->description, reg);
+        break;
+    }
+}
+
+static uint8_t ina238_recv(I2CSlave *i2c)
+{
+    INA238State *s = INA238(i2c);
+    uint8_t byte;
+
+    if (s->rx_byte == 0) {
+        s->rx_val = ina238_read_reg(s, s->ptr);
+        s->rx_width = (s->ptr == INA238_REG_POWER) ? 3 : 2;
+    }
+
+    byte = (s->rx_val >> (8 * (s->rx_width - 1 - s->rx_byte))) & 0xff;
+    s->rx_byte++;
+    if (s->rx_byte >= s->rx_width) {
+        s->rx_byte = 0;
+    }
+
+    return byte;
+}
+
+static int ina238_send(I2CSlave *i2c, uint8_t data)
+{
+    INA238State *s = INA238(i2c);
+
+    if (s->len == 0) {
+        s->ptr = data;
+    } else if ((s->len & 1) == 1) {
+        s->wr_hi = data;
+    } else {
+        ina238_write_reg(s, s->ptr, ((uint16_t)s->wr_hi << 8) | data);
+    }
+    s->len++;
+
+    return 0;
+}
+
+static int ina238_event(I2CSlave *i2c, enum i2c_event event)
+{
+    INA238State *s = INA238(i2c);
+
+    s->len = 0;
+    s->rx_byte = 0;
+
+    return 0;
+}
+
+static void ina238_get_shunt(Object *obj, Visitor *v, const char *name,
+                             void *opaque, Error **errp)
+{
+    INA238State *s = INA238(obj);
+    int64_t value = s->shunt_nv;
+
+    visit_type_int(v, name, &value, errp);
+}
+
+static void ina238_set_shunt(Object *obj, Visitor *v, const char *name,
+                             void *opaque, Error **errp)
+{
+    INA238State *s = INA238(obj);
+    int64_t value;
+
+    if (!visit_type_int(v, name, &value, errp)) {
+        return;
+    }
+
+    if (value < INA238_SHUNT_MIN_NV || value > INA238_SHUNT_MAX_NV) {
+        error_setg(errp,
+                   "%s: shunt-voltage %" PRId64 " out of range (%d..%d nV)",
+                   s->description, value,
+                   INA238_SHUNT_MIN_NV, INA238_SHUNT_MAX_NV);
+        return;
+    }
+
+    s->shunt_nv = (int32_t)value;
+    ina238_input_update(s);
+}
+
+static void ina238_get_bus(Object *obj, Visitor *v, const char *name,
+                           void *opaque, Error **errp)
+{
+    INA238State *s = INA238(obj);
+    int64_t value = s->bus_uv;
+
+    visit_type_int(v, name, &value, errp);
+}
+
+static void ina238_set_bus(Object *obj, Visitor *v, const char *name,
+                           void *opaque, Error **errp)
+{
+    INA238State *s = INA238(obj);
+    int64_t value;
+
+    if (!visit_type_int(v, name, &value, errp)) {
+        return;
+    }
+
+    if (value < 0 || value > INA238_BUS_MAX_UV) {
+        error_setg(errp,
+                   "%s: bus-voltage %" PRId64 " out of range (0..%d uV)",
+                   s->description, value, INA238_BUS_MAX_UV);
+        return;
+    }
+
+    s->bus_uv = (uint32_t)value;
+    ina238_input_update(s);
+}
+
+static void ina238_get_temp(Object *obj, Visitor *v, const char *name,
+                            void *opaque, Error **errp)
+{
+    INA238State *s = INA238(obj);
+    int64_t value = s->temp_mc;
+
+    visit_type_int(v, name, &value, errp);
+}
+
+static void ina238_set_temp(Object *obj, Visitor *v, const char *name,
+                            void *opaque, Error **errp)
+{
+    INA238State *s = INA238(obj);
+    int64_t value;
+
+    if (!visit_type_int(v, name, &value, errp)) {
+        return;
+    }
+
+    if (value < INA238_TEMP_MIN_MC || value > INA238_TEMP_MAX_MC) {
+        error_setg(errp,
+                   "%s: die-temperature %" PRId64
+                   " out of range (%d..%d m-degC)",
+                   s->description, value,
+                   INA238_TEMP_MIN_MC, INA238_TEMP_MAX_MC);
+        return;
+    }
+
+    s->temp_mc = (int32_t)value;
+    ina238_input_update(s);
+}
+
+static void ina238_reset_hold(Object *obj, ResetType type)
+{
+    INA238State *s = INA238(obj);
+
+    trace_ina238_reset(s->description, "hw");
+    ina238_reset_regs(s);
+}
+
+static int ina238_post_load(void *opaque, int version_id)
+{
+    INA238State *s = opaque;
+
+    ina238_refresh_alert(s);
+
+    return 0;
+}
+
+static const VMStateDescription ina238_vmstate = {
+    .name = "INA238",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .post_load = ina238_post_load,
+    .fields = (const VMStateField[]) {
+        VMSTATE_UINT8(ptr, INA238State),
+        VMSTATE_UINT8(len, INA238State),
+        VMSTATE_UINT8(wr_hi, INA238State),
+        VMSTATE_UINT8(rx_byte, INA238State),
+        VMSTATE_UINT8(rx_width, INA238State),
+        VMSTATE_UINT32(rx_val, INA238State),
+        VMSTATE_UINT16(config, INA238State),
+        VMSTATE_UINT16(adc_config, INA238State),
+        VMSTATE_UINT16(shunt_cal, INA238State),
+        VMSTATE_UINT16(diag_alrt, INA238State),
+        VMSTATE_UINT16(sovl, INA238State),
+        VMSTATE_UINT16(suvl, INA238State),
+        VMSTATE_UINT16(bovl, INA238State),
+        VMSTATE_UINT16(buvl, INA238State),
+        VMSTATE_UINT16(temp_limit, INA238State),
+        VMSTATE_UINT16(pwr_limit, INA238State),
+        VMSTATE_UINT16(shunt_reg, INA238State),
+        VMSTATE_UINT16(bus_reg, INA238State),
+        VMSTATE_UINT16(dietemp_reg, INA238State),
+        VMSTATE_UINT16(current_reg, INA238State),
+        VMSTATE_UINT32(power_reg, INA238State),
+        VMSTATE_INT32(shunt_nv, INA238State),
+        VMSTATE_UINT32(bus_uv, INA238State),
+        VMSTATE_INT32(temp_mc, INA238State),
+        VMSTATE_I2C_SLAVE(parent_obj, INA238State),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void ina238_initfn(Object *obj)
+{
+    object_property_add(obj, "shunt-voltage", "int", ina238_get_shunt,
+                        ina238_set_shunt, NULL, NULL);
+    object_property_set_description(obj, "shunt-voltage",
+                                    "Injected shunt (differential) voltage, "
+                                    "in nanovolts");
+    object_property_add(obj, "bus-voltage", "int", ina238_get_bus,
+                        ina238_set_bus, NULL, NULL);
+    object_property_set_description(obj, "bus-voltage",
+                                    "Injected bus voltage, in microvolts");
+    object_property_add(obj, "die-temperature", "int", ina238_get_temp,
+                        ina238_set_temp, NULL, NULL);
+    object_property_set_description(obj, "die-temperature",
+                                    "Injected die temperature, "
+                                    "in millidegrees Celsius");
+}
+
+static void ina238_realize(DeviceState *dev, Error **errp)
+{
+    INA238State *s = INA238(dev);
+
+    if (!s->description) {
+        s->description = g_strdup(object_get_typename(OBJECT(dev)));
+    }
+
+    qdev_init_gpio_out(dev, &s->alert, 1);
+}
+
+static const Property ina238_properties[] = {
+    DEFINE_PROP_STRING("description", INA238State, description),
+};
+
+static void ina238_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    I2CSlaveClass *ic = I2C_SLAVE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    ic->event = ina238_event;
+    ic->recv = ina238_recv;
+    ic->send = ina238_send;
+    dc->realize = ina238_realize;
+    rc->phases.hold = ina238_reset_hold;
+    dc->vmsd = &ina238_vmstate;
+    device_class_set_props(dc, ina238_properties);
+}
+
+static const TypeInfo ina238_types[] = {
+    {
+        .name          = TYPE_INA238,
+        .parent        = TYPE_I2C_SLAVE,
+        .instance_init = ina238_initfn,
+        .instance_size = sizeof(INA238State),
+        .class_init    = ina238_class_init,
+    },
+};
+
+DEFINE_TYPES(ina238_types)
diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build
index b81f001fdf..7e6fd64488 100644
--- a/hw/sensor/meson.build
+++ b/hw/sensor/meson.build
@@ -1,5 +1,6 @@
 system_ss.add(when: 'CONFIG_ADC128D818', if_true: files('adc128d818.c'))
 system_ss.add(when: 'CONFIG_INA230', if_true: files('ina230.c'))
+system_ss.add(when: 'CONFIG_INA238', if_true: files('ina238.c'))
 system_ss.add(when: 'CONFIG_TMP105', if_true: files('tmp105.c'))
 system_ss.add(when: 'CONFIG_TMP421', if_true: files('tmp421.c'))
 system_ss.add(when: 'CONFIG_DPS310', if_true: files('dps310.c'))
diff --git a/hw/sensor/trace-events b/hw/sensor/trace-events
index f809b6b506..98158e37c6 100644
--- a/hw/sensor/trace-events
+++ b/hw/sensor/trace-events
@@ -15,6 +15,13 @@ ina230_convert(const char *id, uint16_t shunt, uint16_t bus, 
uint16_t current, u
 ina230_alert(const char *id, bool level) "%s level %u"
 ina230_reset(const char *id, const char *source) "%s %s"
 
+# ina238.c
+ina238_read(const char *id, uint8_t reg, uint32_t value) "%s reg 0x%02x val 
0x%06x"
+ina238_write(const char *id, uint8_t reg, uint16_t value) "%s reg 0x%02x val 
0x%04x"
+ina238_convert(const char *id, uint16_t shunt, uint16_t bus, uint16_t dietemp, 
uint16_t current, uint32_t power) "%s shunt 0x%04x bus 0x%04x dietemp 0x%04x 
current 0x%04x power 0x%06x"
+ina238_alert(const char *id, bool level) "%s level %u"
+ina238_reset(const char *id, const char *source) "%s %s"
+
 # tmp105.c
 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"
diff --git a/include/hw/sensor/ina238.h b/include/hw/sensor/ina238.h
new file mode 100644
index 0000000000..3bc3c02073
--- /dev/null
+++ b/include/hw/sensor/ina238.h
@@ -0,0 +1,14 @@
+/*
+ * Texas Instruments INA238 current/voltage/power monitor with I2C interface
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_SENSOR_INA238_H
+#define HW_SENSOR_INA238_H
+
+#define TYPE_INA238 "ina238"
+
+#endif

-- 
2.50.1


Reply via email to