Add a model for the Texas Instruments INA230, a 36V, 16-bit I2C current-shunt and power monitor.
The device measures a differential shunt voltage and a bus voltage; the current and power registers are not measured directly but derived by the device from the shunt voltage and the guest-programmed Calibration register. The alert is modelled. Shunt and bus voltages can be injected through the "shunt-voltage" and "bus-voltage" QOM properties (in nV and uV respectively). 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/ina230.c | 597 +++++++++++++++++++++++++++++++++++++++++++++ hw/sensor/meson.build | 1 + hw/sensor/trace-events | 7 + include/hw/sensor/ina230.h | 14 ++ 6 files changed, 624 insertions(+) diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig index eae06369b0..1b33877d5c 100644 --- a/hw/arm/Kconfig +++ b/hw/arm/Kconfig @@ -538,6 +538,7 @@ config ASPEED_SOC select TMP105 select TMP421 select EMC141X + select INA230 select OR_IRQ select UNIMP select LED diff --git a/hw/sensor/Kconfig b/hw/sensor/Kconfig index b459ac2240..da519159ac 100644 --- a/hw/sensor/Kconfig +++ b/hw/sensor/Kconfig @@ -2,6 +2,10 @@ config ADC128D818 bool depends on I2C +config INA230 + bool + depends on I2C + config TMP105 bool depends on I2C diff --git a/hw/sensor/ina230.c b/hw/sensor/ina230.c new file mode 100644 index 0000000000..988d12a6c5 --- /dev/null +++ b/hw/sensor/ina230.c @@ -0,0 +1,597 @@ +/* + * Texas Instruments INA230 36V, 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 INA230 measures a differential shunt voltage and a bus voltage. + * Current and power are not measured directly: they are derived by the + * device from the measured shunt voltage. + * + * Limitations: + * - Conversion timing is instantaneous. A conversion runs synchronously when an + * input, the calibration or the configuration changes; the per-channel + * conversion times (VSH_CT/VBUS_CT), the averaging count (AVG) and continuous + * re-conversion on a timer are not modelled. A read always returns the value + * derived from the current inputs and CVRF 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/ina230.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 INA230_REG_CONFIG 0x00 +#define INA230_REG_SHUNT 0x01 +#define INA230_REG_BUS 0x02 +#define INA230_REG_POWER 0x03 +#define INA230_REG_CURRENT 0x04 +#define INA230_REG_CALIBRATION 0x05 +#define INA230_REG_MASK_ENABLE 0x06 +#define INA230_REG_ALERT_LIMIT 0x07 +#define INA230_REG_DIE_ID 0xFF + +FIELD(INA230_CONFIG, MODE, 0, 3) +FIELD(INA230_CONFIG, VSH_CT, 3, 3) +FIELD(INA230_CONFIG, VBUS_CT, 6, 3) +FIELD(INA230_CONFIG, AVG, 9, 3) +FIELD(INA230_CONFIG, RST, 15, 1) + +#define INA230_CONFIG_POR 0x4127 + +FIELD(INA230_MASK_ENABLE, LEN, 0, 1) /* alert latch enable */ +FIELD(INA230_MASK_ENABLE, APOL, 1, 1) /* alert polarity */ +FIELD(INA230_MASK_ENABLE, OVF, 2, 1) /* math overflow flag (status) */ +FIELD(INA230_MASK_ENABLE, CVRF, 3, 1) /* conversion ready flag (status) */ +FIELD(INA230_MASK_ENABLE, AFF, 4, 1) /* alert function flag (status) */ +FIELD(INA230_MASK_ENABLE, CNVR, 10, 1) /* conversion ready alert */ +FIELD(INA230_MASK_ENABLE, POL, 11, 1) /* power over-limit */ +FIELD(INA230_MASK_ENABLE, BUL, 12, 1) /* bus under-limit */ +FIELD(INA230_MASK_ENABLE, BOL, 13, 1) /* bus over-limit */ +FIELD(INA230_MASK_ENABLE, SUL, 14, 1) /* shunt under-limit */ +FIELD(INA230_MASK_ENABLE, SOL, 15, 1) /* shunt over-limit */ + +/* Writable Mask/Enable bits: alert-function selects + APOL + LEN */ +#define INA230_ME_WMASK (R_INA230_MASK_ENABLE_SOL_MASK | \ + R_INA230_MASK_ENABLE_SUL_MASK | \ + R_INA230_MASK_ENABLE_BOL_MASK | \ + R_INA230_MASK_ENABLE_BUL_MASK | \ + R_INA230_MASK_ENABLE_POL_MASK | \ + R_INA230_MASK_ENABLE_CNVR_MASK | \ + R_INA230_MASK_ENABLE_APOL_MASK | \ + R_INA230_MASK_ENABLE_LEN_MASK) +/* Read-only status bits: AFF, CVRF, OVF */ +#define INA230_ME_STATUS_MASK (R_INA230_MASK_ENABLE_AFF_MASK | \ + R_INA230_MASK_ENABLE_CVRF_MASK | \ + R_INA230_MASK_ENABLE_OVF_MASK) + +FIELD(INA230_CALIBRATION, FS, 0, 15) +#define INA230_CAL_MASK R_INA230_CALIBRATION_FS_MASK + +#define INA230_SHUNT_LSB_NV 2500 /* 2.5 uV per LSB */ +#define INA230_BUS_LSB_UV 1250 /* 1.25 mV per LSB */ +#define INA230_SHUNT_MIN_NV (-81920000) /* -32768 * 2.5 uV */ +#define INA230_SHUNT_MAX_NV 81917500 /* 32767 * 2.5 uV */ +/* + * A real rail should not drive the pin past 36 V, so bus codes above 0x7080 + * are intentionally unreachable in the model. + */ +#define INA230_BUS_MAX_UV 36000000 /* bus pin rated 0..36 V */ + +#define INA230_CURRENT_DIV 2048 +#define INA230_POWER_DIV 20000 + +#define INA230_DIE_ID_VAL 0x2310 + +OBJECT_DECLARE_SIMPLE_TYPE(INA230State, INA230) + +struct INA230State { + 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; /* 0: return MSB next, 1: return LSB next */ + uint16_t rx_word; /* value latched at the start of a read */ + + /* Register file */ + uint16_t config; + uint16_t calibration; + uint16_t mask_enable; + uint16_t alert_limit; + + /* Derived measurement registers */ + uint16_t shunt_reg; + uint16_t bus_reg; + uint16_t current_reg; + uint16_t power_reg; + + /* Injected physical inputs */ + int32_t shunt_nv; /* shunt voltage, nanovolts (signed: bidirectional) */ + uint32_t bus_uv; /* bus voltage, microvolts (never negative) */ + + /* + * In latch mode AFF and the ALERT pin stay asserted until the + * Configuration register is written or the Mask/Enable register is read + * (datasheet 7.3.3). + */ + bool alert_latched; + + char *description; +}; + +/* + * Evaluate the selected alert function. @shunt_fresh and @bus_fresh tell which + * of the measurement registers hold a fresh value; a function fed by a channel + * that did not convert cannot assert. + */ +static bool ina230_limit_alert(const INA230State *s, bool shunt_fresh, + bool bus_fresh) +{ + uint16_t me = s->mask_enable; + int16_t shunt = (int16_t)s->shunt_reg; + int16_t limit_s = (int16_t)s->alert_limit; + uint16_t limit_u = s->alert_limit; + + if (FIELD_EX16(me, INA230_MASK_ENABLE, SOL)) { + return shunt_fresh && shunt > limit_s; + } + if (FIELD_EX16(me, INA230_MASK_ENABLE, SUL)) { + return shunt_fresh && shunt < limit_s; + } + if (FIELD_EX16(me, INA230_MASK_ENABLE, BOL)) { + return bus_fresh && s->bus_reg > limit_u; + } + if (FIELD_EX16(me, INA230_MASK_ENABLE, BUL)) { + return bus_fresh && s->bus_reg < limit_u; + } + if (FIELD_EX16(me, INA230_MASK_ENABLE, POL)) { + return bus_fresh && s->power_reg > limit_u; + } + return false; +} + +static void ina230_refresh_alert(INA230State *s) +{ + bool latch = FIELD_EX16(s->mask_enable, INA230_MASK_ENABLE, LEN); + bool aff = latch ? s->alert_latched : ina230_limit_alert(s, true, true); + bool cnvr; + bool active; + int level; + + s->mask_enable = FIELD_DP16(s->mask_enable, INA230_MASK_ENABLE, AFF, aff); + + cnvr = FIELD_EX16(s->mask_enable, INA230_MASK_ENABLE, CNVR) && + FIELD_EX16(s->mask_enable, INA230_MASK_ENABLE, CVRF); + + active = aff || cnvr; + + level = FIELD_EX16(s->mask_enable, INA230_MASK_ENABLE, APOL) ? + active : !active; + + trace_ina230_alert(s->description, level); + qemu_set_irq(s->alert, level); +} + +static void ina230_sample_alert(INA230State *s, bool shunt_fresh, + bool bus_fresh) +{ + if (FIELD_EX16(s->mask_enable, INA230_MASK_ENABLE, LEN) && + ina230_limit_alert(s, shunt_fresh, bus_fresh)) { + s->alert_latched = true; + } + ina230_refresh_alert(s); +} + +static void ina230_convert(INA230State *s) +{ + uint8_t mode = FIELD_EX16(s->config, INA230_CONFIG, MODE); + bool shunt_en = mode & 0x1; + bool bus_en = mode & 0x2; + int64_t sr, br, cur, pwr; + bool ovf = false; + + if (!shunt_en && !bus_en) { + return; + } + + if (shunt_en) { + sr = s->shunt_nv / INA230_SHUNT_LSB_NV; + sr = MAX(INT16_MIN, MIN(INT16_MAX, sr)); + s->shunt_reg = (uint16_t)(int16_t)sr; + + cur = (int64_t)(int16_t)s->shunt_reg * s->calibration / + INA230_CURRENT_DIV; + if (cur > INT16_MAX || cur < INT16_MIN) { + ovf = true; + cur = MAX(INT16_MIN, MIN(INT16_MAX, cur)); + } + s->current_reg = (uint16_t)(int16_t)cur; + } + + if (bus_en) { + br = s->bus_uv / INA230_BUS_LSB_UV; + br = MAX(0, MIN(INT16_MAX, br)); + s->bus_reg = (uint16_t)br; + } + + /* + * 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) { + pwr = (int64_t)(int16_t)s->current_reg * s->bus_reg / INA230_POWER_DIV; + /* POWER is an unsigned, positive-only register (datasheet). */ + if (pwr < 0) { + pwr = 0; + } else if (pwr > UINT16_MAX) { + ovf = true; + pwr = UINT16_MAX; + } + s->power_reg = (uint16_t)pwr; + } + + /* Without a calibration the device reports neither current nor power. */ + if (s->calibration == 0) { + s->current_reg = 0; + s->power_reg = 0; + } + + s->mask_enable = FIELD_DP16(s->mask_enable, INA230_MASK_ENABLE, OVF, ovf); + s->mask_enable = FIELD_DP16(s->mask_enable, INA230_MASK_ENABLE, CVRF, 1); + + trace_ina230_convert(s->description, s->shunt_reg, s->bus_reg, + s->current_reg, s->power_reg); + + ina230_sample_alert(s, shunt_en, bus_en); +} + +/* + * A physical input or the calibration 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 ina230_input_update(INA230State *s) +{ + if (FIELD_EX16(s->config, INA230_CONFIG, MODE) & 0x4) { + ina230_convert(s); + } +} + +static void ina230_reset_regs(INA230State *s) +{ + s->config = INA230_CONFIG_POR; + s->calibration = 0; + s->mask_enable = 0; + s->alert_limit = 0; + s->alert_latched = false; + + s->ptr = 0; + s->len = 0; + s->wr_hi = 0; + s->rx_byte = 0; + s->rx_word = 0; + + s->shunt_reg = 0; + s->bus_reg = 0; + s->current_reg = 0; + s->power_reg = 0; + + ina230_refresh_alert(s); +} + +static uint16_t ina230_read_reg(INA230State *s, uint8_t reg) +{ + uint16_t val; + + switch (reg) { + case INA230_REG_CONFIG: + val = s->config; + break; + case INA230_REG_SHUNT: + val = s->shunt_reg; + break; + case INA230_REG_BUS: + val = s->bus_reg; + break; + case INA230_REG_POWER: + val = s->power_reg; + break; + case INA230_REG_CURRENT: + val = s->current_reg; + break; + case INA230_REG_CALIBRATION: + val = s->calibration; + break; + case INA230_REG_MASK_ENABLE: + val = s->mask_enable; + s->mask_enable = FIELD_DP16(s->mask_enable, INA230_MASK_ENABLE, + CVRF, 0); + s->alert_latched = false; + ina230_refresh_alert(s); + break; + case INA230_REG_ALERT_LIMIT: + val = s->alert_limit; + break; + case INA230_REG_DIE_ID: + val = INA230_DIE_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_ina230_read(s->description, reg, val); + return val; +} + +static void ina230_write_reg(INA230State *s, uint8_t reg, uint16_t val) +{ + trace_ina230_write(s->description, reg, val); + + switch (reg) { + case INA230_REG_CONFIG: + if (val & R_INA230_CONFIG_RST_MASK) { + trace_ina230_reset(s->description, "reg"); + ina230_reset_regs(s); + break; + } + s->config = val & ~R_INA230_CONFIG_RST_MASK; + /* + * The write itself releases a latched alert, so the release reaches + * the ALERT pin before the conversion it triggers can relatch it. + */ + s->alert_latched = false; + ina230_refresh_alert(s); + ina230_convert(s); + break; + case INA230_REG_CALIBRATION: + s->calibration = val & INA230_CAL_MASK; + ina230_input_update(s); + break; + case INA230_REG_MASK_ENABLE: + s->mask_enable = (val & INA230_ME_WMASK) | + (s->mask_enable & INA230_ME_STATUS_MASK); + ina230_refresh_alert(s); + break; + case INA230_REG_ALERT_LIMIT: + s->alert_limit = val; + ina230_refresh_alert(s); + break; + case INA230_REG_SHUNT: + case INA230_REG_BUS: + case INA230_REG_POWER: + case INA230_REG_CURRENT: + case INA230_REG_DIE_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 ina230_recv(I2CSlave *i2c) +{ + INA230State *s = INA230(i2c); + uint8_t byte; + + if (s->rx_byte == 0) { + s->rx_word = ina230_read_reg(s, s->ptr); + byte = s->rx_word >> 8; + s->rx_byte = 1; + } else { + byte = s->rx_word & 0xff; + s->rx_byte = 0; + } + + return byte; +} + +static int ina230_send(I2CSlave *i2c, uint8_t data) +{ + INA230State *s = INA230(i2c); + + if (s->len == 0) { + s->ptr = data; + } else if ((s->len & 1) == 1) { + s->wr_hi = data; + } else { + ina230_write_reg(s, s->ptr, ((uint16_t)s->wr_hi << 8) | data); + } + s->len++; + + return 0; +} + +static int ina230_event(I2CSlave *i2c, enum i2c_event event) +{ + INA230State *s = INA230(i2c); + + s->len = 0; + s->rx_byte = 0; + + return 0; +} + +static void ina230_get_shunt(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + INA230State *s = INA230(obj); + int64_t value = s->shunt_nv; + + visit_type_int(v, name, &value, errp); +} + +static void ina230_set_shunt(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + INA230State *s = INA230(obj); + int64_t value; + + if (!visit_type_int(v, name, &value, errp)) { + return; + } + + if (value < INA230_SHUNT_MIN_NV || value > INA230_SHUNT_MAX_NV) { + error_setg(errp, + "%s: shunt-voltage %" PRId64 " out of range (%d..%d nV)", + s->description, value, + INA230_SHUNT_MIN_NV, INA230_SHUNT_MAX_NV); + return; + } + + s->shunt_nv = (int32_t)value; + ina230_input_update(s); +} + +static void ina230_get_bus(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + INA230State *s = INA230(obj); + int64_t value = s->bus_uv; + + visit_type_int(v, name, &value, errp); +} + +static void ina230_set_bus(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + INA230State *s = INA230(obj); + int64_t value; + + if (!visit_type_int(v, name, &value, errp)) { + return; + } + + if (value < 0 || value > INA230_BUS_MAX_UV) { + error_setg(errp, + "%s: bus-voltage %" PRId64 " out of range (0..%d uV)", + s->description, value, INA230_BUS_MAX_UV); + return; + } + + s->bus_uv = (uint32_t)value; + ina230_input_update(s); +} + +static void ina230_reset_hold(Object *obj, ResetType type) +{ + INA230State *s = INA230(obj); + + trace_ina230_reset(s->description, "hw"); + ina230_reset_regs(s); +} + +static int ina230_post_load(void *opaque, int version_id) +{ + INA230State *s = opaque; + + ina230_refresh_alert(s); + + return 0; +} + +static const VMStateDescription ina230_vmstate = { + .name = "INA230", + .version_id = 1, + .minimum_version_id = 1, + .post_load = ina230_post_load, + .fields = (const VMStateField[]) { + VMSTATE_UINT8(ptr, INA230State), + VMSTATE_UINT8(len, INA230State), + VMSTATE_UINT8(wr_hi, INA230State), + VMSTATE_UINT8(rx_byte, INA230State), + VMSTATE_UINT16(rx_word, INA230State), + VMSTATE_UINT16(config, INA230State), + VMSTATE_UINT16(calibration, INA230State), + VMSTATE_UINT16(mask_enable, INA230State), + VMSTATE_UINT16(alert_limit, INA230State), + VMSTATE_UINT16(shunt_reg, INA230State), + VMSTATE_UINT16(bus_reg, INA230State), + VMSTATE_UINT16(current_reg, INA230State), + VMSTATE_UINT16(power_reg, INA230State), + VMSTATE_INT32(shunt_nv, INA230State), + VMSTATE_UINT32(bus_uv, INA230State), + VMSTATE_BOOL(alert_latched, INA230State), + VMSTATE_I2C_SLAVE(parent_obj, INA230State), + VMSTATE_END_OF_LIST() + } +}; + +static void ina230_initfn(Object *obj) +{ + object_property_add(obj, "shunt-voltage", "int", ina230_get_shunt, + ina230_set_shunt, NULL, NULL); + object_property_set_description(obj, "shunt-voltage", + "Injected shunt (differential) voltage, " + "in nanovolts"); + object_property_add(obj, "bus-voltage", "int", ina230_get_bus, + ina230_set_bus, NULL, NULL); + object_property_set_description(obj, "bus-voltage", + "Injected bus voltage, in microvolts"); +} + +static void ina230_realize(DeviceState *dev, Error **errp) +{ + INA230State *s = INA230(dev); + + if (!s->description) { + s->description = g_strdup(object_get_typename(OBJECT(dev))); + } + + qdev_init_gpio_out(dev, &s->alert, 1); +} + +static const Property ina230_properties[] = { + DEFINE_PROP_STRING("description", INA230State, description), +}; + +static void ina230_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 = ina230_event; + ic->recv = ina230_recv; + ic->send = ina230_send; + dc->realize = ina230_realize; + rc->phases.hold = ina230_reset_hold; + dc->vmsd = &ina230_vmstate; + device_class_set_props(dc, ina230_properties); +} + +static const TypeInfo ina230_types[] = { + { + .name = TYPE_INA230, + .parent = TYPE_I2C_SLAVE, + .instance_init = ina230_initfn, + .instance_size = sizeof(INA230State), + .class_init = ina230_class_init, + }, +}; + +DEFINE_TYPES(ina230_types) diff --git a/hw/sensor/meson.build b/hw/sensor/meson.build index fe36c9ef91..b81f001fdf 100644 --- a/hw/sensor/meson.build +++ b/hw/sensor/meson.build @@ -1,4 +1,5 @@ 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_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 5a3630f7bb..f809b6b506 100644 --- a/hw/sensor/trace-events +++ b/hw/sensor/trace-events @@ -8,6 +8,13 @@ adc128d818_convert(const char *id, uint8_t channel, uint16_t value) "%s ch %u va adc128d818_irq(const char *id, bool level) "%s level %u" adc128d818_reset(const char *id, const char *source) "%s %s" +# ina230.c +ina230_read(const char *id, uint8_t reg, uint16_t value) "%s reg 0x%02x val 0x%04x" +ina230_write(const char *id, uint8_t reg, uint16_t value) "%s reg 0x%02x val 0x%04x" +ina230_convert(const char *id, uint16_t shunt, uint16_t bus, uint16_t current, uint16_t power) "%s shunt 0x%04x bus 0x%04x current 0x%04x power 0x%04x" +ina230_alert(const char *id, bool level) "%s level %u" +ina230_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/ina230.h b/include/hw/sensor/ina230.h new file mode 100644 index 0000000000..4394727d47 --- /dev/null +++ b/include/hw/sensor/ina230.h @@ -0,0 +1,14 @@ +/* + * Texas Instruments INA230 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_INA230_H +#define HW_SENSOR_INA230_H + +#define TYPE_INA230 "ina230" + +#endif -- 2.50.1
