Adafruit_CircuitPython_INA23x/adafruit_ina23x.py
2025-06-09 16:03:37 -04:00

169 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPDX-FileCopyrightText: Copyright (c) 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_ina23x`
================================================================================
CircuitPython driver for the INA237 and INA238 DC Current Voltage Power Monitors
* Author(s): Liz Clark
Implementation Notes
--------------------
**Hardware:**
* `Adafruit INA237 Breakout <https://www.adafruit.com/product/6340>`_
* `Adafruit INA238 Breakout <https://www.adafruit.com/product/6349>`_
**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
* Adafruit CircuitPython INA228 library: https://github.com/adafruit/Adafruit_CircuitPython_INA228
"""
import time
from adafruit_ina228 import INA2XX, AlertType
from adafruit_register.i2c_bit import ROBit
from adafruit_register.i2c_bits import ROBits, RWBits
from adafruit_register.i2c_struct import ROUnaryStruct
from micropython import const
try:
import typing # pylint: disable=unused-import
from busio import I2C
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_INA23x.git"
_SOVL = const(0x0C) # Shunt Overvoltage Threshold
_SUVL = const(0x0D) # Shunt Undervoltage Threshold
_BOVL = const(0x0E) # Bus Overvoltage Threshold
_BUVL = const(0x0F) # Bus Undervoltage Threshold
_TEMPLIMIT = const(0x10) # Temperature Over-Limit Threshold
_PWRLIMIT = const(0x11) # Power Over-Limit Threshold
# Constants
_INA237_DEVICE_ID = const(0x237)
_INA238_DEVICE_ID = const(0x238)
class INA23X(INA2XX): # noqa: PLR0904
"""Driver for the INA237/INA238 current and power sensor.
:param ~busio.I2C i2c_bus: The I2C bus the INA23X 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.
"""
# INA23X-specific register bits
_alert_type = RWBits(7, 0x0B, 5, register_width=2, lsb_first=False)
_conversion_ready = ROBit(0x0B, 1, register_width=2, lsb_first=False)
_alert_flags = ROBits(12, 0x0B, 0, register_width=2, lsb_first=False)
_raw_vshunt = ROUnaryStruct(0x04, ">h")
_raw_current = ROUnaryStruct(0x07, ">h")
_raw_power = ROUnaryStruct(0x08, ">H")
def __init__(self, i2c_bus: I2C, address: int = 0x40, skip_reset: bool = False) -> None:
super().__init__(i2c_bus, address, skip_reset)
# Verify device ID (both INA237 and INA238 use compatible IDs)
if self.device_id not in {_INA237_DEVICE_ID, _INA238_DEVICE_ID}:
raise ValueError("Failed to find INA237/INA238 - incorrect device ID")
# Set INA23X defaults
self.set_calibration(0.015, 10.0)
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
# INA237/238 uses 2^15 as divisor
self._current_lsb = max_current / (1 << 15)
self._update_shunt_cal()
def _update_shunt_cal(self) -> None:
"""Update the shunt calibration register."""
# Scale factor based on ADC range
scale = 4 if self._adc_range else 1
# INA237/238 formula: SHUNT_CAL = 819.2 × 10^6 × CURRENT_LSB × RSHUNT × scale
shunt_cal = int(819.2e6 * self._current_lsb * self._shunt_res * scale)
self._shunt_cal = min(shunt_cal, 0xFFFF)
@property
def die_temperature(self) -> float:
"""Die temperature in degrees Celsius."""
# INA237/238 uses 12 bits (15:4) with 125 m°C/LSB
return (self._raw_dietemp >> 4) * 0.125
@property
def bus_voltage(self) -> float:
"""Bus voltage in volts."""
# INA237/238 uses 3.125 mV/LSB
return self._raw_vbus * 0.003125
@property
def shunt_voltage(self) -> float:
"""Shunt voltage in volts."""
# Scale depends on ADC range
scale = 1.25e-6 if self._adc_range else 5.0e-6 # µV/LSB
return self._raw_vshunt * scale
@property
def current(self) -> float:
"""Current in amperes."""
return self._raw_current * self._current_lsb
@property
def power(self) -> float:
"""Power in watts."""
# INA237/238 power LSB = 20 × current_lsb
return self._raw_power * 20.0 * self._current_lsb
@property
def conversion_ready(self) -> bool:
"""Check if conversion is complete."""
return bool(self._conversion_ready)
@property
def alert_type(self) -> int:
"""Alert type configuration."""
return self._alert_type
@alert_type.setter
def alert_type(self, value: int) -> None:
# Alert type can be a combination of flags, so we check if all bits are valid
valid_mask = (
AlertType.CONVERSION_READY
| AlertType.OVERTEMPERATURE
| AlertType.OVERPOWER
| AlertType.UNDERVOLTAGE
| AlertType.OVERVOLTAGE
| AlertType.UNDERSHUNT
| AlertType.OVERSHUNT
)
if value & ~valid_mask:
raise ValueError(
f"Invalid alert type 0x{value:02X}. Must be a combination of AlertType.* constants"
)
self._alert_type = value
@property
def alert_flags(self) -> int:
"""Current alert flags."""
return self._alert_flags