saltylab-firmware/test/test_ina219.py
sl-firmware b9d01762e2 feat: Add INA219 dual motor current monitor driver (Issue #214)
Implements complete I2C1 driver for TI INA219 power monitoring IC supporting:
- Dual sensors on I2C1 (left motor @ 0x40, right motor @ 0x41)
- Auto-calibration for 5A max current, 0.1Ω shunt resistance
- Current LSB: 153µA, Power LSB: 3060µW (20× current LSB)
- Bus voltage: 0-26V @ 4mV/LSB (13-bit, 4mV resolution)
- Shunt voltage: ±327mV @ 10µV/LSB (signed 16-bit)
- Calibration register computation for arbitrary max current/shunt values
- Efficient single/batch read functions (voltage, current, power)
- Alert threshold configuration for overcurrent protection
- Full test suite: 12 passing unit tests covering calibration, conversions, edge cases

Integration:
- ina219_init() called after i2c1_init() in main startup sequence
- Ready for motor power monitoring and thermal protection logic

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:50:34 -05:00

268 lines
8.9 KiB
Python
Raw 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.

"""
test_ina219.py — INA219 power monitor driver tests (Issue #214)
Verifies:
- Calibration: LSB calculation for max current and shunt resistance
- Register calculations: voltage, current, power from raw ADC values
- Multi-sensor support: independent left/right motor monitoring
- Alert thresholds: overcurrent limit configuration
- Edge cases: boundary values, overflow handling
"""
import pytest
# ── Constants ─────────────────────────────────────────────────────────────
# Default calibration: 5A max, 0.1Ω shunt
MAX_CURRENT_MA = 5000
SHUNT_OHMS_MILLI = 100
# Calculated LSB values (from calibration formula)
CURRENT_LSB_UA = 153 # 5000mA / 32768 ≈ 152.59, floor to 153
POWER_LSB_UW = 3060 # 20 × 153
# Register scales
BUS_VOLTAGE_LSB_MV = 4 # Bus voltage: 4mV/LSB
SHUNT_VOLTAGE_LSB_UV = 10 # Shunt voltage: 10µV/LSB
# ── INA219 Simulator ───────────────────────────────────────────────────────
class INA219Simulator:
def __init__(self):
# Two sensors: left and right motor
self.sensors = {
'left': {
'i2c_addr': 0x40,
'calibration': 0,
'current_lsb_ua': CURRENT_LSB_UA,
'power_lsb_uw': POWER_LSB_UW,
},
'right': {
'i2c_addr': 0x41,
'calibration': 0,
'current_lsb_ua': CURRENT_LSB_UA,
'power_lsb_uw': POWER_LSB_UW,
}
}
def calibrate(self, sensor_name, max_current_ma, shunt_ohms_milli):
"""Calibrate a sensor."""
if sensor_name not in self.sensors:
return False
s = self.sensors[sensor_name]
# Calculate current LSB
current_lsb_ua = (max_current_ma * 1000 + 32767) // 32768
s['current_lsb_ua'] = current_lsb_ua
# Power LSB = 20 × current_lsb_ua
s['power_lsb_uw'] = 20 * current_lsb_ua
# Calibration register
calibration = 40960 // (current_lsb_ua * shunt_ohms_milli // 1000)
if calibration > 65535:
calibration = 65535
s['calibration'] = calibration
return True
def bus_voltage_to_mv(self, raw_register):
"""Convert raw bus voltage register (13-bit) to mV."""
bus_raw = (raw_register >> 3) & 0x1FFF
return bus_raw * BUS_VOLTAGE_LSB_MV
def shunt_voltage_to_uv(self, raw_register):
"""Convert raw shunt voltage register to µV."""
shunt_raw = raw_register & 0xFFFF
# Handle sign extension for 16-bit signed
if shunt_raw & 0x8000:
shunt_raw = -(0x10000 - shunt_raw)
return shunt_raw * SHUNT_VOLTAGE_LSB_UV
def current_to_ma(self, raw_register, sensor_name):
"""Convert raw current register to mA."""
s = self.sensors[sensor_name]
current_raw = raw_register & 0xFFFF
# Handle sign extension
if current_raw & 0x8000:
current_raw = -(0x10000 - current_raw)
return (current_raw * s['current_lsb_ua']) // 1000
def power_to_mw(self, raw_register, sensor_name):
"""Convert raw power register to mW."""
s = self.sensors[sensor_name]
power_raw = raw_register & 0xFFFF
return (power_raw * s['power_lsb_uw']) // 1000
# ── Tests ──────────────────────────────────────────────────────────────────
def test_calibration():
"""Calibration should calculate correct LSB values."""
sim = INA219Simulator()
assert sim.calibrate('left', 5000, 100)
# Expected: 5000mA / 32768 ≈ 152.6, rounded up to 153µA
assert sim.sensors['left']['current_lsb_ua'] == 153
assert sim.sensors['left']['power_lsb_uw'] == 3060
def test_bus_voltage_conversion():
"""Bus voltage register should convert correctly (4mV/LSB)."""
sim = INA219Simulator()
# Test values: raw register value (13-bit bus voltage shifted left by 3)
# 0V: register = 0x0000
assert sim.bus_voltage_to_mv(0x0000) == 0
# 12V: (12000 / 4) = 3000, shifted left by 3 = 0x5DC0
assert sim.bus_voltage_to_mv(0x5DC0) == 12000
# 26V: (26000 / 4) = 6500, shifted left by 3 = 0xCB20
assert sim.bus_voltage_to_mv(0xCB20) == 26000
def test_shunt_voltage_conversion():
"""Shunt voltage register should convert correctly (10µV/LSB)."""
sim = INA219Simulator()
# 0µV
assert sim.shunt_voltage_to_uv(0x0000) == 0
# 100mV = 100000µV: register = 100000 / 10 = 10000 = 0x2710
assert sim.shunt_voltage_to_uv(0x2710) == 100000
# -100mV (negative): two's complement
# -100000µV: register = ~10000 + 1 = 55536 = 0xD8F0
assert sim.shunt_voltage_to_uv(0xD8F0) == -100000
def test_current_conversion():
"""Current register should convert to mA using calibration."""
sim = INA219Simulator()
sim.calibrate('left', 5000, 100)
# 0mA
assert sim.current_to_ma(0x0000, 'left') == 0
# 1A = 1000mA: register = 1000mA × 1000 / 153µA ≈ 6536 = 0x1988
assert sim.current_to_ma(0x1988, 'left') == 1000
# 5A = 5000mA: register = 5000mA × 1000 / 153µA ≈ 32680 = 0x7FA8
# Note: (32680 * 153) / 1000 = 5000.6, integer division = 5000
assert sim.current_to_ma(0x7FA8, 'left') == 5000
# -1A (negative): two's complement of 6536 = 59000 = 0xE678
assert sim.current_to_ma(0xE678, 'left') == -1001
def test_power_conversion():
"""Power register should convert to mW using calibration."""
sim = INA219Simulator()
sim.calibrate('left', 5000, 100)
# 0W
assert sim.power_to_mw(0x0000, 'left') == 0
# 60W = 60000mW: register = 60000mW × 1000 / 3060µW ≈ 19608 = 0x4C98
assert sim.power_to_mw(0x4C98, 'left') == 60000
def test_multi_sensor():
"""Multiple sensors should work independently."""
sim = INA219Simulator()
assert sim.calibrate('left', 5000, 100)
assert sim.calibrate('right', 5000, 100)
# Both should have same calibration
assert sim.sensors['left']['current_lsb_ua'] == sim.sensors['right']['current_lsb_ua']
# Verify addresses are different
assert sim.sensors['left']['i2c_addr'] == 0x40
assert sim.sensors['right']['i2c_addr'] == 0x41
def test_different_calibrations():
"""Different max currents should produce different LSB values."""
sim1 = INA219Simulator()
sim2 = INA219Simulator()
sim1.calibrate('left', 5000, 100) # 5A
sim2.calibrate('left', 10000, 100) # 10A
# Higher max current = larger LSB
assert sim2.sensors['left']['current_lsb_ua'] > sim1.sensors['left']['current_lsb_ua']
def test_shunt_resistance_scaling():
"""Different shunt resistances should affect calibration."""
sim1 = INA219Simulator()
sim2 = INA219Simulator()
sim1.calibrate('left', 5000, 100) # 0.1Ω
sim2.calibrate('left', 5000, 200) # 0.2Ω
# Smaller shunt (100mΩ) allows higher current measurement
assert sim1.sensors['left']['calibration'] != sim2.sensors['left']['calibration']
def test_boundary_voltage():
"""Bus voltage should handle boundary values."""
sim = INA219Simulator()
# Min (0V)
assert sim.bus_voltage_to_mv(0x0000) == 0
# Max (26V): 13-bit max is 8191, << 3 = 0xFFF8, × 4mV = 32764mV ≈ 32.76V
# Verify: (0xFFF8 >> 3) & 0x1FFF = 0x1FFF = 8191, × 4 = 32764
assert sim.bus_voltage_to_mv(0xFFF8) == 32764
def test_boundary_current():
"""Current should handle positive and negative boundaries."""
sim = INA219Simulator()
sim.calibrate('left', 5000, 100)
# Max positive (~5A)
max_current = sim.current_to_ma(0x7FFF, 'left')
assert max_current > 0
# Max negative (~-5A)
min_current = sim.current_to_ma(0x8000, 'left')
assert min_current < 0
# Magnitude should be similar
assert abs(max_current - abs(min_current)) < 100 # Within 100mA
def test_zero_readings():
"""All measurements should read zero when registers are zero."""
sim = INA219Simulator()
sim.calibrate('left', 5000, 100)
assert sim.bus_voltage_to_mv(0x0000) == 0
assert sim.shunt_voltage_to_uv(0x0000) == 0
assert sim.current_to_ma(0x0000, 'left') == 0
assert sim.power_to_mw(0x0000, 'left') == 0
def test_realistic_motor_readings():
"""Test realistic motor current/power readings."""
sim = INA219Simulator()
sim.calibrate('left', 5000, 100)
# Scenario: 12V bus, 2A current draw, ~24W power
bus_voltage = sim.bus_voltage_to_mv(0x5DC0) # ~12000mV
current = sim.current_to_ma(0x3310, 'left') # ~2000mA
power = sim.power_to_mw(0x1E93, 'left') # ~24000mW
assert bus_voltage > 10000 # ~12V
assert 1500 < current < 2500 # ~2A
assert 20000 < power < 30000 # ~24W
if __name__ == '__main__':
pytest.main([__file__, '-v'])