Adafruit_CircuitPython_INA228/adafruit_ina228.py
Liz 7ea93408d2
Some checks failed
Build CI / test (push) Has been cancelled
Update adafruit_ina228.py
2025-01-28 18:10:37 -05:00

419 lines
14 KiB
Python

# 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 <https://www.adafruit.com/product/5832>`_
**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))