diff --git a/adafruit_ina228.py b/adafruit_ina228.py index 731da7c..2e895aa 100644 --- a/adafruit_ina228.py +++ b/adafruit_ina228.py @@ -15,7 +15,7 @@ **Hardware:** -* `Adafruit INA228 High Side Current and Power Monitor `_ +* `Adafruit INA228 High or Low Side Current and Power Monitor `_ **Software and Dependencies:** @@ -28,9 +28,9 @@ 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 adafruit_register.i2c_bit import ROBit, RWBit +from adafruit_register.i2c_bits import ROBits, RWBits +from adafruit_register.i2c_struct import ROUnaryStruct, UnaryStruct from micropython import const try: @@ -43,61 +43,150 @@ __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 +# Common register addresses for INA2XX family +_CONFIG = const(0x00) +_ADCCFG = const(0x01) +_SHUNTCAL = const(0x02) +_VSHUNT = const(0x04) +_VBUS = const(0x05) +_DIETEMP = const(0x06) +_CURRENT = const(0x07) +_POWER = const(0x08) +_DIAGALRT = const(0x0B) +_MFG_UID = const(0x3E) +_DVC_UID = const(0x3F) + +# INA228-specific registers +_SHUNT_TEMPCO = const(0x03) +_ENERGY = const(0x09) +_CHARGE = const(0x0A) +_SOVL = const(0x0C) +_SUVL = const(0x0D) +_BOVL = const(0x0E) +_BUVL = const(0x0F) +_TEMP_LIMIT = const(0x10) +_PWR_LIMIT = const(0x11) + +# Constants +_INA2XX_DEFAULT_ADDR = const(0x40) +_TEXAS_INSTRUMENTS_ID = const(0x5449) +_INA228_DEVICE_ID = const(0x228) 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 + """Operating mode constants for INA2XX""" + + SHUTDOWN = const(0x00) + TRIG_BUS = const(0x01) + TRIG_SHUNT = const(0x02) + TRIG_BUS_SHUNT = const(0x03) + TRIG_TEMP = const(0x04) + TRIG_TEMP_BUS = const(0x05) + TRIG_TEMP_SHUNT = const(0x06) + TRIG_TEMP_BUS_SHUNT = const(0x07) + CONT_BUS = const(0x09) + CONT_SHUNT = const(0x0A) + CONT_BUS_SHUNT = const(0x0B) + CONT_TEMP = const(0x0C) + CONT_TEMP_BUS = const(0x0D) + CONT_TEMP_SHUNT = const(0x0E) + CONT_TEMP_BUS_SHUNT = const(0x0F) + + # Convenience aliases + TRIGGERED = TRIG_TEMP_BUS_SHUNT + CONTINUOUS = CONT_TEMP_BUS_SHUNT + + # Valid modes set for validation + _VALID_MODES = { + SHUTDOWN, + TRIG_BUS, + TRIG_SHUNT, + TRIG_BUS_SHUNT, + TRIG_TEMP, + TRIG_TEMP_BUS, + TRIG_TEMP_SHUNT, + TRIG_TEMP_BUS_SHUNT, + CONT_BUS, + CONT_SHUNT, + CONT_BUS_SHUNT, + CONT_TEMP, + CONT_TEMP_BUS, + CONT_TEMP_SHUNT, + CONT_TEMP_BUS_SHUNT, + } + + +class ConversionTime: + """Conversion time constants for INA2XX""" + + TIME_50_US = const(0) + TIME_84_US = const(1) + TIME_150_US = const(2) + TIME_280_US = const(3) + TIME_540_US = const(4) + TIME_1052_US = const(5) + TIME_2074_US = const(6) + TIME_4120_US = const(7) + + _VALID_TIMES = { + TIME_50_US, + TIME_84_US, + TIME_150_US, + TIME_280_US, + TIME_540_US, + TIME_1052_US, + TIME_2074_US, + TIME_4120_US, + } + + # Microsecond values for each setting + _VALUES = [50, 84, 150, 280, 540, 1052, 2074, 4120] + + +class AveragingCount: + """Averaging count constants for INA2XX""" + + COUNT_1 = const(0) + COUNT_4 = const(1) + COUNT_16 = const(2) + COUNT_64 = const(3) + COUNT_128 = const(4) + COUNT_256 = const(5) + COUNT_512 = const(6) + COUNT_1024 = const(7) + + _VALID_COUNTS = { + COUNT_1, + COUNT_4, + COUNT_16, + COUNT_64, + COUNT_128, + COUNT_256, + COUNT_512, + COUNT_1024, + } + + # Actual count values for each setting + _VALUES = [1, 4, 16, 64, 128, 256, 512, 1024] class AlertType: - """Constants for alert type settings""" - - NONE = 0x00 - CONVERSION_READY = 0x01 - OVERPOWER = 0x02 - UNDERVOLTAGE = 0x04 - OVERVOLTAGE = 0x08 - UNDERCURRENT = 0x10 - OVERCURRENT = 0x20 - _VALID_VALUES = [ + """Alert type constants for INA2XX""" + + NONE = const(0x0) + CONVERSION_READY = const(0x1) + OVERPOWER = const(0x2) + UNDERVOLTAGE = const(0x4) + OVERVOLTAGE = const(0x8) + UNDERCURRENT = const(0x10) + OVERCURRENT = const(0x20) + + # INA237/238 specific + OVERTEMPERATURE = const(0x2) + UNDERSHUNT = const(0x20) + OVERSHUNT = const(0x40) + + _VALID_TYPES = { NONE, CONVERSION_READY, OVERPOWER, @@ -105,281 +194,302 @@ class AlertType: OVERVOLTAGE, UNDERCURRENT, OVERCURRENT, - ] - _MAX_COMBINED = 0x3F + OVERTEMPERATURE, + UNDERSHUNT, + OVERSHUNT, + } -class INA228: # noqa: PLR0904 - """Driver for the INA228 power and current sensor""" +class INA2XX: # noqa: PLR0904 + """Base driver for INA2XX series power and current sensors. - _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) - _alert_type = RWBits(6, _DIAG_ALRT, 8, register_width=2) - _alert_polarity_bit = RWBit(_DIAG_ALRT, 12, register_width=2) - _alert_latch_bit = RWBit(_DIAG_ALRT, 15, register_width=2) - _reset_bit = RWBit(_CONFIG, 15, register_width=2) - _reset_accumulators_bit = RWBit(_CONFIG, 14, register_width=2) - """Operating mode""" - mode = RWBits(4, _ADC_CONFIG, 12, register_width=2) - _alert_conv_bit = RWBit(_DIAG_ALRT, 14, 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") + :param ~busio.I2C i2c_bus: The I2C bus the INA2XX is connected to. + :param int address: The I2C device address. Defaults to :const:`0x40` + :param bool skip_reset: Skip resetting the device on init. Defaults to False. + """ - 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 manufacturer ID (should be 0x5449 for Texas Instruments) - if self.manufacturer_id != 0x5449: - raise RuntimeError( - f"Invalid manufacturer ID: 0x{self.manufacturer_id:04X} (expected 0x5449)" - ) - # Verify device ID - dev_id = (self._device_id >> 4) & 0xFFF - 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.015, 10.0) - self.conversion_time_bus = 150 - self.conversion_time_shunt = 280 - self.averaging_count = 16 + # Configuration register bits + _reset = RWBit(_CONFIG, 15, register_width=2, lsb_first=False) + _adc_range = RWBit(_CONFIG, 4, register_width=2, lsb_first=False) - def reset(self) -> None: - """Reset the INA228 (all registers to default values)""" - self._reset_bit = True - self._alert_conv_bit = True - self.mode = Mode.CONTINUOUS_ALL - time.sleep(0.002) + # ADC Configuration register bits + _mode = RWBits(4, _ADCCFG, 12, register_width=2, lsb_first=False) + _vbus_conv_time = RWBits(3, _ADCCFG, 9, register_width=2, lsb_first=False) + _vshunt_conv_time = RWBits(3, _ADCCFG, 6, register_width=2, lsb_first=False) + _temp_conv_time = RWBits(3, _ADCCFG, 3, register_width=2, lsb_first=False) + _avg_count = RWBits(3, _ADCCFG, 0, register_width=2, lsb_first=False) - 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 + # Diagnostic/Alert register bits + _alert_latch = RWBit(_DIAGALRT, 15, register_width=2, lsb_first=False) + _alert_conv = RWBit(_DIAGALRT, 14, register_width=2, lsb_first=False) + _alert_polarity = RWBit(_DIAGALRT, 12, register_width=2, lsb_first=False) - 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 + # Measurement registers + _raw_dietemp = ROUnaryStruct(_DIETEMP, ">h") + _raw_vbus = ROUnaryStruct(_VBUS, ">H") - def reset_accumulators(self) -> None: - """Reset the energy and charge accumulators""" - self._reset_accumulators_bit = True + # Calibration register + _shunt_cal = UnaryStruct(_SHUNTCAL, ">H") + + # ID registers + _manufacturer_id = ROUnaryStruct(_MFG_UID, ">H") + _device_id = ROUnaryStruct(_DVC_UID, ">H") + + def __init__( + self, i2c_bus: I2C, address: int = _INA2XX_DEFAULT_ADDR, skip_reset: bool = False + ) -> None: + self.i2c_device = I2CDevice(i2c_bus, address) + + # Verify manufacturer ID + if self._manufacturer_id != _TEXAS_INSTRUMENTS_ID: + raise ValueError("Failed to find INA2XX - incorrect manufacturer ID") + + self._shunt_res = 0.1 # Default shunt resistance + self._current_lsb = 0.0 + + if not skip_reset: + self.reset() + time.sleep(0.002) # 2ms delay for first measurement + + # Set defaults + self.averaging_count = AveragingCount.COUNT_16 + self.bus_voltage_conv_time = ConversionTime.TIME_150_US + self.shunt_voltage_conv_time = ConversionTime.TIME_280_US + self.mode = Mode.CONTINUOUS + + def reset(self) -> None: + """Reset the sensor to default configuration.""" + self._reset = True + self._alert_conv = True + self.mode = Mode.CONTINUOUS @property - def adc_range(self) -> int: - """ - ADC range. - 0 = ±163.84 mV - 1 = ±40.96 mV + def device_id(self) -> int: + """Device ID""" + return (self._device_id >> 4) & 0xFFF - When using the ±40.96 mV range, the shunt calibration value is automatically scaled by 4. - """ + @property + def shunt_resistance(self) -> float: + """The shunt resistance in ohms.""" + return self._shunt_res + + @property + def adc_range(self) -> int: + """ADC range setting. 0 = ±163.84mV, 1 = ±40.96mV""" return self._adc_range @adc_range.setter - def adc_range(self, value: int): + def adc_range(self, value: int) -> None: if value not in {0, 1}: - raise ValueError("ADC range must be 0 (±163.84 mV) or 1 (±40.96 mV)") + raise ValueError("ADC range must be 0 or 1") self._adc_range = value - self._update_calibration() + self._update_shunt_cal() @property - def alert_type(self) -> int: - """ - The alert trigger type. Use AlertType constants. - Can be OR'd together for multiple triggers. + def mode(self) -> int: + """Operating mode of the sensor.""" + return self._mode - Example: - # Single alert type - sensor.alert_type = AlertType.OVERPOWER + @mode.setter + def mode(self, value: int) -> None: + if value not in Mode._VALID_MODES: + raise ValueError(f"Invalid mode 0x{value:02X}. Must be one of the Mode.* constants") + self._mode = value - # Multiple alert types - sensor.alert_type = AlertType.OVERPOWER | AlertType.OVERVOLTAGE - """ - return self._alert_type - - @alert_type.setter - def alert_type(self, value: int): - # Check if it's a valid combination of alert types - if value < 0 or value > AlertType._MAX_COMBINED: - raise ValueError(f"Invalid alert type value: {value}. Must be 0-0x3F") - - # Optional: Validate that only valid bits are set - valid_mask = ( - AlertType.CONVERSION_READY - | AlertType.OVERPOWER - | AlertType.UNDERVOLTAGE - | AlertType.OVERVOLTAGE - | AlertType.UNDERCURRENT - | AlertType.OVERCURRENT - ) - - if value & ~valid_mask: - raise ValueError(f"Invalid alert type bits set: 0x{value:02X}") + @property + def averaging_count(self) -> int: + """Number of samples to average.""" + return self._avg_count - self._alert_type = value + @averaging_count.setter + def averaging_count(self, value: int) -> None: + if value not in AveragingCount._VALID_COUNTS: + raise ValueError( + f"Invalid averaging count {value}. Must be one of the AveragingCount.* constants" + ) + self._avg_count = value @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] + def bus_voltage_conv_time(self) -> int: + """Bus voltage conversion time setting.""" + return self._vbus_conv_time - @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: + @bus_voltage_conv_time.setter + def bus_voltage_conv_time(self, value: int) -> None: + if value not in ConversionTime._VALID_TIMES: raise ValueError( - f"Invalid conversion time: {usec}. Valid values are: {', '.join(map(str, times))}." + f"Invalid conversion time {value}. Must be one of the ConversionTime.* constants" ) - self._vbus_ct = times.index(usec) + self._vbus_conv_time = value @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] + def shunt_voltage_conv_time(self) -> int: + """Shunt voltage conversion time setting.""" + return self._vshunt_conv_time - @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: + @shunt_voltage_conv_time.setter + def shunt_voltage_conv_time(self, value: int) -> None: + if value not in ConversionTime._VALID_TIMES: raise ValueError( - f"Invalid conversion time: {usec}. Valid values are: {', '.join(map(str, times))}." + f"Invalid conversion time {value}. Must be one of the ConversionTime.* constants" ) - self._vshunt_ct = times.index(usec) + self._vshunt_conv_time = value @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] + def temp_conv_time(self) -> int: + """Temperature conversion time setting.""" + return self._temp_conv_time - @averaging_count.setter - def averaging_count(self, count: int): - counts = [1, 4, 16, 64, 128, 256, 512, 1024] - if count not in counts: + @temp_conv_time.setter + def temp_conv_time(self, value: int) -> None: + if value not in ConversionTime._VALID_TIMES: raise ValueError( - "Invalid averaging count: " - + str(count) - + ". " - + "Valid values are: " - + ", ".join(map(str, counts)) - + "." + f"Invalid conversion time {value}. Must be one of the ConversionTime.* constants" ) - self._avg_count = counts.index(count) + self._temp_conv_time = value + + @property + def alert_polarity(self) -> int: + """Alert pin polarity. 0 = active high, 1 = active low.""" + return self._alert_polarity + + @alert_polarity.setter + def alert_polarity(self, value: int) -> None: + if value not in {0, 1}: + raise ValueError("Alert polarity must be 0 or 1") + self._alert_polarity = value + + @property + def alert_latch(self) -> int: + """Alert latch enable. 0 = transparent, 1 = latched.""" + return self._alert_latch + + @alert_latch.setter + def alert_latch(self, value: int) -> None: + if value not in {0, 1}: + raise ValueError("Alert latch must be 0 or 1") + self._alert_latch = value + + +class INA228(INA2XX): # noqa: PLR0904 + """Driver for the INA228 power and current sensor""" + + # Additional registers for INA228 + _reset_accumulators_bit = RWBit(_CONFIG, 14, register_width=2, lsb_first=False) + _alert_type = RWBits(6, _DIAGALRT, 8, register_width=2, lsb_first=False) + _alert_flags = ROUnaryStruct(_DIAGALRT, ">H") + + _shunt_tempco = UnaryStruct(_SHUNT_TEMPCO, ">H") + _sovl = UnaryStruct(_SOVL, ">H") + _suvl = UnaryStruct(_SUVL, ">H") + _bovl = UnaryStruct(_BOVL, ">H") + _buvl = UnaryStruct(_BUVL, ">H") + _temp_limit = UnaryStruct(_TEMP_LIMIT, ">H") + _pwr_limit = UnaryStruct(_PWR_LIMIT, ">H") - def set_shunt(self, shunt_res: float, max_current: float) -> None: - """Configure shunt resistor value and maximum expected current""" + _raw_vshunt = ROUnaryStruct(_VSHUNT, ">h") + _raw_current = ROUnaryStruct(_CURRENT, ">h") + _raw_power = ROUnaryStruct(_POWER, ">H") + + _conversion_ready = ROBit(_DIAGALRT, 1, register_width=2, lsb_first=False) + + def __init__(self, i2c_bus: I2C, address: int = 0x40, skip_reset: bool = False) -> None: + # Initialize 24-bit and 40-bit register buffers + self.buf3 = bytearray(3) # Buffer for 24-bit registers + self.buf5 = bytearray(5) # Buffer for 40-bit registers + + super().__init__(i2c_bus, address, skip_reset) + + # Verify device ID + dev_id = self.device_id + if dev_id != _INA228_DEVICE_ID: + raise RuntimeError(f"Failed to find INA228 - check your wiring! (Got ID: 0x{dev_id:X})") + + # Set INA228 defaults + self.set_calibration(0.015, 10.0) + + 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._reset_accumulators_bit = True + + def set_calibration(self, shunt_res: float = 0.015, max_current: float = 10.0) -> None: + """Set the calibration based on shunt resistance and maximum expected current. + + :param float shunt_res: Shunt resistance in ohms + :param float max_current: Maximum expected current in amperes + """ self._shunt_res = shunt_res + # INA228 uses 2^19 as divisor self._current_lsb = max_current / (1 << 19) - self._update_calibration() + self._update_shunt_cal() time.sleep(0.001) - def _update_calibration(self): - """Update the calibration register based on shunt and current settings""" + def _update_shunt_cal(self) -> None: + """Update the shunt calibration register.""" scale = 4 if self._adc_range else 1 + # INA228 formula: SHUNT_CAL = 13107.2 × 10^6 × CURRENT_LSB × RSHUNT × scale 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.015, 10.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 die_temperature(self) -> float: + """Die temperature in degrees Celsius.""" + # INA228 uses 7.8125 m°C/LSB + return self._raw_dietemp * 7.8125e-3 @property - def conversion_ready(self) -> bool: - """Check if conversion is ready""" - return bool(self._diag_alrt & (1 << 1)) + def bus_voltage(self) -> float: + """Bus voltage in volts.""" + raw = self._reg24(_VBUS) + # INA228 uses 195.3125 µV/LSB + return (raw >> 4) * 195.3125e-6 @property def shunt_voltage(self) -> float: - """Shunt voltage in V""" + """Shunt voltage in volts.""" raw = self._reg24(_VSHUNT) if raw & 0x800000: raw -= 0x1000000 + # Scale depends on ADC range 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 + def current(self) -> float: + """Current in amperes.""" + raw = self._reg24(_CURRENT) + if raw & 0x800000: + raw -= 0x1000000 + return (raw / 16.0) * self._current_lsb @property def power(self) -> float: - """Power measurement in mW""" + """Power in watts.""" raw = self._reg24(_POWER) - value = raw * 3.2 * self._current_lsb * 1000 - return value + # INA228 power LSB = 3.2 × current_lsb + return raw * 3.2 * self._current_lsb @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 + return raw * 16.0 * 3.2 * self._current_lsb @property def charge(self) -> float: @@ -390,54 +500,73 @@ def charge(self) -> float: 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 + def conversion_ready(self) -> bool: + """Check if conversion is complete.""" + return bool(self._conversion_ready) @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] + def alert_type(self) -> int: + """Alert type configuration.""" + return self._alert_type - @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: + @alert_type.setter + def alert_type(self, value: int) -> None: + # Alert type can be a combination of flags + valid_mask = ( + AlertType.CONVERSION_READY + | AlertType.OVERPOWER + | AlertType.UNDERVOLTAGE + | AlertType.OVERVOLTAGE + | AlertType.UNDERCURRENT + | AlertType.OVERCURRENT + ) + if value & ~valid_mask: raise ValueError( - f"Invalid conversion time: {usec}. Valid values are: {', '.join(map(str, times))}." + f"Invalid alert type 0x{value:02X}. Must be a combination of AlertType.* constants" ) - self._temper_ct = times.index(usec) + self._alert_type = value @property - def alert_latch(self) -> bool: - """Alert latch setting. True=latched, False=transparent""" - return bool(self._alert_latch_bit) + def alert_flags(self) -> dict: + """ + All diagnostic and alert flags - @alert_latch.setter - def alert_latch(self, value: bool): - self._alert_latch_bit = value + 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._alert_flags + 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)), + } @property - def alert_polarity(self) -> bool: - """Alert polarity. True=inverted, False=normal""" - return bool(self._alert_polarity_bit) + def shunt_tempco(self) -> int: + """Shunt temperature coefficient in ppm/°C""" + return self._shunt_tempco - @alert_polarity.setter - def alert_polarity(self, value: bool): - self._alert_polarity_bit = value + @shunt_tempco.setter + def shunt_tempco(self, value: int): + self._shunt_tempco = value @property def shunt_voltage_overlimit(self) -> float: @@ -450,56 +579,80 @@ def shunt_voltage_overlimit(self, value: float): self._sovl = int(value / scale) @property - def alert_flags(self) -> dict: - """ - All diagnostic and alert flags - - Returns a dictionary with the status of each flag: + def shunt_voltage_underlimit(self) -> float: + """Shunt voltage underlimit threshold in volts""" + return self._suvl * (78.125e-6 if self._adc_range else 312.5e-6) - 'ENERGYOF': bool, # Energy overflow - - 'CHARGEOF': bool, # Charge overflow - - 'MATHOF': bool, # Math overflow + @shunt_voltage_underlimit.setter + def shunt_voltage_underlimit(self, value: float): + scale = 78.125e-6 if self._adc_range else 312.5e-6 + self._suvl = int(value / scale) - 'TMPOL': bool, # Temperature overlimit + @property + def bus_voltage_overlimit(self) -> float: + """Bus voltage overlimit threshold in volts""" + return self._bovl * 3.125e-3 - 'SHNTOL': bool, # Shunt voltage overlimit + @bus_voltage_overlimit.setter + def bus_voltage_overlimit(self, value: float): + self._bovl = int(value / 3.125e-3) - 'SHNTUL': bool, # Shunt voltage underlimit + @property + def bus_voltage_underlimit(self) -> float: + """Bus voltage underlimit threshold in volts""" + return self._buvl * 3.125e-3 - 'BUSOL': bool, # Bus voltage overlimit + @bus_voltage_underlimit.setter + def bus_voltage_underlimit(self, value: float): + self._buvl = int(value / 3.125e-3) - 'BUSUL': bool, # Bus voltage underlimit + @property + def temperature_limit(self) -> float: + """Temperature overlimit threshold in degrees Celsius""" + return self._temp_limit * 7.8125e-3 - 'POL': bool, # Power overlimit + @temperature_limit.setter + def temperature_limit(self, value: float): + self._temp_limit = int(value / 7.8125e-3) - 'CNVRF': bool, # Conversion ready + @property + def power_limit(self) -> float: + """Power overlimit threshold in watts""" + # Power limit LSB = 256 × current_lsb × 3.2 + return self._pwr_limit * 256 * self._current_lsb * 3.2 - '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)), - } + @power_limit.setter + def power_limit(self, value: float): + self._pwr_limit = int(value / (256 * self._current_lsb * 3.2)) def trigger_measurement(self) -> None: """Trigger a one-shot measurement when in triggered mode""" current_mode = self.mode - if current_mode < Mode.SHUTDOWN2: + if current_mode <= Mode.TRIG_TEMP_BUS_SHUNT: + # Re-write the same mode to trigger measurement 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)) + # Read-modify-write to clear only overflow flags + flags = self._alert_flags + self._alert_flags = flags & ~((1 << 11) | (1 << 10) | (1 << 9)) + + # Convenience calibration methods + def set_calibration_32V_2A(self) -> None: + """Configure for 32V and up to 2A measurements""" + self.mode = Mode.CONTINUOUS + time.sleep(0.001) + self.set_calibration(0.015, 10.0) + self.bus_voltage_conv_time = ConversionTime.TIME_1052_US + self.shunt_voltage_conv_time = ConversionTime.TIME_1052_US + self.temp_conv_time = ConversionTime.TIME_1052_US + self.averaging_count = AveragingCount.COUNT_1 + + def set_calibration_32V_1A(self) -> None: + """Configure for 32V and up to 1A measurements""" + self.set_calibration(0.1, 1.0) + + def set_calibration_16V_400mA(self) -> None: + """Configure for 16V and up to 400mA measurements""" + self.set_calibration(0.1, 0.4)