# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries # # SPDX-License-Identifier: MIT """ `adafruit_ina228` ================================================================================ CircuitPython driver for the INA228 I2C 85V, 20-bit High or Low Side Power Monitor * Author(s): Liz Clark Implementation Notes -------------------- **Hardware:** * `Adafruit INA228 High Side Current and Power Monitor `_ **Software and Dependencies:** * Adafruit CircuitPython firmware for the supported boards: https://circuitpython.org/downloads * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register """ import time from adafruit_bus_device.i2c_device import I2CDevice from adafruit_register.i2c_bit import RWBit from adafruit_register.i2c_bits import RWBits from adafruit_register.i2c_struct import UnaryStruct from micropython import const try: import typing from busio import I2C except ImportError: pass __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_INA228.git" # Register addresses _CONFIG = const(0x00) # Configuration Register _ADC_CONFIG = const(0x01) # ADC Configuration Register _SHUNT_CAL = const(0x02) # Shunt Calibration Register _SHUNT_TEMPCO = const(0x03) # Shunt Temperature Coefficient Register _VSHUNT = const(0x04) # Shunt Voltage Measurement _VBUS = const(0x05) # Bus Voltage Measurement _DIETEMP = const(0x06) # Temperature Measurement _CURRENT = const(0x07) # Current Result _POWER = const(0x08) # Power Result _ENERGY = const(0x09) # Energy Result _CHARGE = const(0x0A) # Charge Result _DIAG_ALRT = const(0x0B) # Diagnostic Flags and Alert _SOVL = const(0x0C) # Shunt Overvoltage Threshold _SUVL = const(0x0D) # Shunt Undervoltage Threshold _BOVL = const(0x0E) # Bus Overvoltage Threshold _BUVL = const(0x0F) # Bus Undervoltage Threshold _TEMP_LIMIT = const(0x10) # Temperature Over-Limit Threshold _PWR_LIMIT = const(0x11) # Power Over-Limit Threshold _MFG_ID = const(0x3E) # Manufacturer ID _DEVICE_ID = const(0x3F) # Device ID class Mode: """Constants for operating modes""" SHUTDOWN = 0x00 TRIGGERED_BUS = 0x01 TRIGGERED_SHUNT = 0x02 TRIGGERED_BUS_SHUNT = 0x03 TRIGGERED_TEMP = 0x04 TRIGGERED_TEMP_BUS = 0x05 TRIGGERED_TEMP_SHUNT = 0x06 TRIGGERED_ALL = 0x07 SHUTDOWN2 = 0x08 CONTINUOUS_BUS = 0x09 CONTINUOUS_SHUNT = 0x0A CONTINUOUS_BUS_SHUNT = 0x0B CONTINUOUS_TEMP = 0x0C CONTINUOUS_TEMP_BUS = 0x0D CONTINUOUS_TEMP_SHUNT = 0x0E CONTINUOUS_ALL = 0x0F class INA228: # noqa: PLR0904 """Driver for the INA228 power and current sensor""" _config = UnaryStruct(_CONFIG, ">H") _adc_config = UnaryStruct(_ADC_CONFIG, ">H") _shunt_cal = UnaryStruct(_SHUNT_CAL, ">H") _diag_alrt = UnaryStruct(_DIAG_ALRT, ">H") _adc_range = RWBit(_CONFIG, 4, register_width=2) """Operating mode""" mode = RWBits(4, _ADC_CONFIG, 12, register_width=2) _vbus_ct = RWBits(3, _ADC_CONFIG, 9, register_width=2) _vshunt_ct = RWBits(3, _ADC_CONFIG, 6, register_width=2) _temper_ct = RWBits(3, _ADC_CONFIG, 3, register_width=2) _avg_count = RWBits(3, _ADC_CONFIG, 0, register_width=2) _device_id = UnaryStruct(_DEVICE_ID, ">H") _temperature = UnaryStruct(_DIETEMP, ">h") _sovl = UnaryStruct(_SOVL, ">H") # Shunt overvoltage _suvl = UnaryStruct(_SUVL, ">H") # Shunt undervoltage _bovl = UnaryStruct(_BOVL, ">H") # Bus overvoltage _buvl = UnaryStruct(_BUVL, ">H") # Bus undervoltage _temp_limit = UnaryStruct(_TEMP_LIMIT, ">H") # Temperature limit _pwr_limit = UnaryStruct(_PWR_LIMIT, ">H") # Power limit _shunt_tempco = UnaryStruct(_SHUNT_TEMPCO, ">H") """Manufacturer ID""" manufacturer_id = UnaryStruct(_MFG_ID, ">H") def __init__(self, i2c_bus, addr=0x40): self.i2c_device = I2CDevice(i2c_bus, addr) self.buf3 = bytearray(3) # Buffer for 24-bit registers self.buf5 = bytearray(5) # Buffer for 40-bit registers # Verify device ID dev_id = (self._device_id >> 4) & 0xFFF # Get 12-bit device ID if dev_id != 0x228: raise RuntimeError(f"Failed to find INA228 - check your wiring! (Got ID: 0x{dev_id:X})") self._current_lsb = 0 self._shunt_res = 0 self.reset() self.mode = Mode.CONTINUOUS_ALL self.set_shunt(0.1, 2.0) self.conversion_time_bus = 150 self.conversion_time_shunt = 280 self.averaging_count = 16 def reset(self) -> None: """Reset the INA228""" self._config = 0x8000 def _reg24(self, reg): """Read 24-bit register""" with self.i2c_device as i2c: i2c.write_then_readinto(bytes([reg]), self.buf3) result = (self.buf3[0] << 16) | (self.buf3[1] << 8) | self.buf3[2] return result def _reg40(self, reg): """Read 40-bit register""" with self.i2c_device as i2c: i2c.write_then_readinto(bytes([reg]), self.buf5) result = 0 for b in self.buf5: result = (result << 8) | b return result def reset_accumulators(self) -> None: """Reset the energy and charge accumulators""" self._config = 1 << 14 @property def conversion_time_bus(self) -> int: """ Bus voltage conversion time in microseconds. Valid values are: 50, 84, 150, 280, 540, 1052, 2074, 4120. """ times = [50, 84, 150, 280, 540, 1052, 2074, 4120] return times[self._vbus_ct] @conversion_time_bus.setter def conversion_time_bus(self, usec: int): times = [50, 84, 150, 280, 540, 1052, 2074, 4120] if usec not in times: raise ValueError( f"Invalid conversion time: {usec}. Valid values are: {', '.join(map(str, times))}." ) self._vbus_ct = times.index(usec) @property def conversion_time_shunt(self) -> int: """ Shunt voltage conversion time in microseconds. Valid values are: 50, 84, 150, 280, 540, 1052, 2074, 4120. """ times = [50, 84, 150, 280, 540, 1052, 2074, 4120] return times[self._vshunt_ct] @conversion_time_shunt.setter def conversion_time_shunt(self, usec: int): times = [50, 84, 150, 280, 540, 1052, 2074, 4120] if usec not in times: raise ValueError( f"Invalid conversion time: {usec}. Valid values are: {', '.join(map(str, times))}." ) self._vshunt_ct = times.index(usec) @property def averaging_count(self) -> int: """ Number of samples to average. Returns actual count. Valid values are: 1, 4, 16, 64, 128, 256, 512, 1024. """ counts = [1, 4, 16, 64, 128, 256, 512, 1024] return counts[self._avg_count] @averaging_count.setter def averaging_count(self, count: int): counts = [1, 4, 16, 64, 128, 256, 512, 1024] if count not in counts: raise ValueError( "Invalid averaging count: " + str(count) + ". " + "Valid values are: " + ", ".join(map(str, counts)) + "." ) self._avg_count = counts.index(count) def set_shunt(self, shunt_res: float, max_current: float) -> None: """Configure shunt resistor value and maximum expected current""" self._shunt_res = shunt_res self._current_lsb = max_current / (1 << 19) self._update_calibration() time.sleep(0.001) def _update_calibration(self): """Update the calibration register based on shunt and current settings""" scale = 4 if self._adc_range else 1 cal_value = int(13107.2 * 1000000.0 * self._shunt_res * self._current_lsb * scale) self._shunt_cal = cal_value read_cal = self._shunt_cal if read_cal != cal_value: raise ValueError(" Warning: Calibration readback mismatch!") def set_calibration_32V_2A(self) -> None: """Configure for 32V and up to 2A measurements""" self._mode = Mode.CONTINUOUS_ALL time.sleep(0.001) self.set_shunt(0.1, 2.0) self._vbus_ct = 5 self._vshunt_ct = 5 self._temper_ct = 5 self._avg_count = 0 def set_calibration_32V_1A(self) -> None: """Configure for 32V and up to 1A measurements""" self.set_shunt(0.1, 1.0) def set_calibration_16V_400mA(self) -> None: """Configure for 16V and up to 400mA measurements""" self.set_shunt(0.1, 0.4) @property def conversion_ready(self) -> bool: """Check if conversion is ready""" return bool(self._diag_alrt & (1 << 1)) @property def shunt_voltage(self) -> float: """Shunt voltage in V""" raw = self._reg24(_VSHUNT) if raw & 0x800000: raw -= 0x1000000 scale = 78.125e-9 if self._adc_range else 312.5e-9 return (raw / 16.0) * scale @property def voltage(self) -> float: """Bus voltage measurement in V""" raw = self._reg24(_VBUS) value = (raw >> 4) * 195.3125e-6 return value @property def power(self) -> float: """Power measurement in mW""" raw = self._reg24(_POWER) value = raw * 3.2 * self._current_lsb * 1000 return value @property def energy(self) -> float: """Energy measurement in Joules""" raw = self._reg40(_ENERGY) value = raw * 16.0 * 3.2 * self._current_lsb return value @property def current(self) -> float: """Current measurement in mA""" raw = self._reg24(_CURRENT) if raw & 0x800000: raw -= 0x1000000 value = (raw / 16.0) * self._current_lsb * 1000.0 return value @property def charge(self) -> float: """Accumulated charge in coulombs""" raw = self._reg40(_CHARGE) return raw * self._current_lsb @property def temperature(self) -> float: """Die temperature in celsius""" return self._temperature * 7.8125e-3 @property def shunt_tempco(self) -> int: """Shunt temperature coefficient in ppm/°C""" return self._shunt_tempco @shunt_tempco.setter def shunt_tempco(self, value: int): self._shunt_tempco = value @property def conversion_time_temperature(self) -> int: """ Temperature conversion time in microseconds. Valid values are: 50, 84, 150, 280, 540, 1052, 2074, 4120. """ times = [50, 84, 150, 280, 540, 1052, 2074, 4120] return times[self._temper_ct] @conversion_time_temperature.setter def conversion_time_temperature(self, usec: int): times = [50, 84, 150, 280, 540, 1052, 2074, 4120] if usec not in times: raise ValueError( f"Invalid conversion time: {usec}. Valid values are: {', '.join(map(str, times))}." ) self._temper_ct = times.index(usec) @property def alert_latch(self) -> bool: """Alert latch setting. True=latched, False=transparent""" return bool(self._diag_alrt & (1 << 15)) @alert_latch.setter def alert_latch(self, value: bool): if value: self._diag_alrt |= 1 << 15 else: self._diag_alrt &= ~(1 << 15) @property def alert_polarity(self) -> bool: """Alert polarity. True=inverted, False=normal""" return bool(self._diag_alrt & (1 << 12)) @alert_polarity.setter def alert_polarity(self, value: bool): if value: self._diag_alrt |= 1 << 12 else: self._diag_alrt &= ~(1 << 12) @property def shunt_voltage_overlimit(self) -> float: """Shunt voltage overlimit threshold in volts""" return self._sovl * (78.125e-6 if self._adc_range else 312.5e-6) @shunt_voltage_overlimit.setter def shunt_voltage_overlimit(self, value: float): scale = 78.125e-6 if self._adc_range else 312.5e-6 self._sovl = int(value / scale) @property def alert_flags(self) -> dict: """ Get all diagnostic and alert flags Returns a dictionary with the status of each flag: 'ENERGYOF': bool, # Energy overflow 'CHARGEOF': bool, # Charge overflow 'MATHOF': bool, # Math overflow 'TMPOL': bool, # Temperature overlimit 'SHNTOL': bool, # Shunt voltage overlimit 'SHNTUL': bool, # Shunt voltage underlimit 'BUSOL': bool, # Bus voltage overlimit 'BUSUL': bool, # Bus voltage underlimit 'POL': bool, # Power overlimit 'CNVRF': bool, # Conversion ready 'MEMSTAT': bool, # ADC conversion status """ flags = self._diag_alrt return { "ENERGYOF": bool(flags & (1 << 11)), "CHARGEOF": bool(flags & (1 << 10)), "MATHOF": bool(flags & (1 << 9)), "TMPOL": bool(flags & (1 << 7)), "SHNTOL": bool(flags & (1 << 6)), "SHNTUL": bool(flags & (1 << 5)), "BUSOL": bool(flags & (1 << 4)), "BUSUL": bool(flags & (1 << 3)), "POL": bool(flags & (1 << 2)), "CNVRF": bool(flags & (1 << 1)), "MEMSTAT": bool(flags & (1 << 0)), } def trigger_measurement(self) -> None: """Trigger a one-shot measurement when in triggered mode""" current_mode = self.mode if current_mode < Mode.SHUTDOWN2: self.mode = current_mode def clear_overflow_flags(self) -> None: """Clear energy, charge, and math overflow flags""" flags = self._diag_alrt self._diag_alrt = flags & ~((1 << 11) | (1 << 10) | (1 << 9))