diff --git a/README.rst b/README.rst index 48719c9..9f4c65f 100644 --- a/README.rst +++ b/README.rst @@ -92,8 +92,23 @@ Usage Example ============= .. code-block:: python + + import time + import board import adafruit_ina228 + i2c = board.I2C() + ina228 = adafruit_ina228.INA228(i2c) + + while True: + print(f"Current: {ina228.current:.2f} mA") + print(f"Bus Voltage: {ina228.voltage:.2f} V") + print(f"Shunt Voltage: {ina228.shunt_voltage*1000:.2f} mV") + print(f"Power: {ina228.power:.2f} mW") + print(f"Energy: {ina228.energy:.2f} J") + print(f"Temperature: {ina228.temperature:.2f} °C") + time.sleep(1) + Documentation ============= API documentation for this library can be found on `Read the Docs `_. diff --git a/adafruit_ina228.py b/adafruit_ina228.py index fec9c25..57e55bb 100644 --- a/adafruit_ina228.py +++ b/adafruit_ina228.py @@ -1,4 +1,3 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries # # SPDX-License-Identifier: MIT @@ -16,22 +15,405 @@ **Hardware:** -.. todo:: Add links to any specific hardware product page(s), or category page(s). - Use unordered list & hyperlink rST inline format: "* `Link Text `_" +* `Adafruit INA228 High Side Current and Power Monitor `_ **Software and Dependencies:** -* Adafruit CircuitPython firmware for the supported boards: - https://circuitpython.org/downloads +* Adafruit CircuitPython firmware for the supported boards: https://circuitpython.org/downloads -.. todo:: Uncomment or remove the Bus Device and/or the Register library dependencies - based on the library's use of either. - -# * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice -# * Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register +* Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +* Adafruit's Register library: https://github.com/adafruit/Adafruit_CircuitPython_Register """ -# imports +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)) diff --git a/docs/conf.py b/docs/conf.py index b77fc29..01a025a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ # Uncomment the below if you use native CircuitPython modules such as # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. -# autodoc_mock_imports = ["digitalio", "busio"] +autodoc_mock_imports = ["digitalio", "busio", "adafruit_register", "adafruit_bus_device"] autodoc_preserve_defaults = True diff --git a/docs/index.rst b/docs/index.rst index 941a66c..6c188a6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,14 +24,12 @@ Table of Contents .. toctree:: :caption: Tutorials -.. todo:: Add any Learn guide links here. If there are none, then simply delete this todo and leave - the toctree above for use later. + Adafruit Learn Guide .. toctree:: :caption: Related Products -.. todo:: Add any product links here. If there are none, then simply delete this todo and leave - the toctree above for use later. + Adafruit INA228 - I2C 85V, 20-bit High or Low Side Power Monitor - STEMMA QT / Qwiic .. toctree:: :caption: Other Links diff --git a/examples/ina228_simpletest.py b/examples/ina228_simpletest.py index 42772ff..279c8d6 100644 --- a/examples/ina228_simpletest.py +++ b/examples/ina228_simpletest.py @@ -1,4 +1,27 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries # -# SPDX-License-Identifier: Unlicense +# SPDX-License-Identifier: MIT + +import time + +import board + +import adafruit_ina228 + +i2c = board.I2C() +ina228 = adafruit_ina228.INA228(i2c) +print("Adafruit INA228 Test") + +print(f"Bus conversion time: {ina228.conversion_time_bus} microseconds") +print(f"Shunt conversion time: {ina228.conversion_time_shunt} microseconds") +print(f"Samples averaged: {ina228.averaging_count}") + +while True: + print("\nCurrent Measurements:") + print(f"Current: {ina228.current:.2f} mA") + print(f"Bus Voltage: {ina228.voltage:.2f} V") + print(f"Shunt Voltage: {ina228.shunt_voltage*1000:.2f} mV") + print(f"Power: {ina228.power:.2f} mW") + print(f"Energy: {ina228.energy:.2f} J") + print(f"Temperature: {ina228.temperature:.2f} °C") + time.sleep(1) diff --git a/requirements.txt b/requirements.txt index c1ad2a0..7284723 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ Adafruit-Blinka adafruit-circuitpython-busdevice +adafruit-circuitpython-register