-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathadafruit_ina228.py
419 lines (349 loc) · 13.7 KB
/
adafruit_ina228.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# 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.015, 10.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))