Reviewed-by: Glenn Miles <[email protected]> Thanks,
Glenn On Mon, 2026-10-12 at 16:32 +0200, Emmanuel Blot via qemu development wrote: > 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 116d1c64ac..3abe249ed5 100644 > --- a/hw/arm/Kconfig > +++ b/hw/arm/Kconfig > @@ -540,6 +540,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 >
