INA3221 mit Arduino & ESP32: Drei Stromkreise gleichzeitig überwachen – Strom, Spannung & Verbrauch per I2C
Ein Solarpanel lädt einen Akku, der Akku versorgt einen ESP32, und irgendwo dazwischen hängt noch eine Pumpe. Drei Stromkreise, drei Fragen – und mit einem einzelnen Stromsensor drei Messgeräte. Der INA3221 erledigt alle drei Kanäle gleichzeitig, misst highside bis 26 V, liefert Strom und Spannung als fertige Digitalwerte über I2C und braucht dafür genau zwei Pins am Mikrocontroller. In diesem Tutorial verkabelst du ihn richtig, verstehst die Shunt-Rechnung dahinter, baust ein Energiemessgerät mit mAh-Zähler und richtest Überstrom-Warnungen ein, die der Chip selbstständig auslöst.
Highside, Lowside – und warum das der ganze Punkt ist
Strom messen heißt: einen kleinen Widerstand in den Stromkreis legen und die Spannung darüber messen. Die Frage ist nur, wo dieser Shunt sitzt.
Lowside bedeutet zwischen Last und Masse. Das ist einfach, weil eine Seite des Shunts auf 0 V liegt – aber es hebt die Masse deiner Last um einige Millivolt an, und wenn die Last noch anderweitig geerdet ist (Gehäuse, USB-Kabel, zweites Netzteil), fließt der Strom an deinem Shunt vorbei und die Messung ist wertlos.
Highside bedeutet zwischen Quelle und Last. Die Masse bleibt sauber durchverbunden, aber jetzt muss dein Messverstärker eine winzige Differenzspannung erkennen, die auf einem Gleichtaktpotenzial von vielleicht 24 V schwimmt. Genau das kann der INA3221 – dreimal parallel.
| Merkmal | ACS712 (Hall) | INA219 (1 Kanal) | INA3221 (3 Kanäle) |
|---|---|---|---|
| Messprinzip | Magnetfeld, galvanisch getrennt | Shunt, highside | Shunt, highside |
| Kanäle | 1 | 1 | 3 |
| Schnittstelle | analog (ADC nötig) | I2C | I2C |
| Busspannung | bis 5 V Logik | bis 26 V | bis 26 V |
| Auflösung Shunt-ADC | – | 12 Bit | 13 Bit (40 µV/LSB) |
| Typischer Messbereich | ±5 A | ±3,2 A (0,1 Ω) | ±1,6 A je Kanal (0,1 Ω) |
| Auflösung bei kleinen Strömen | ca. 20 mA | ca. 0,1 mA | ca. 0,4 mA |
| Wechselstrom | ja | nein | nein |
| Warnschwellen in Hardware | nein | nein | ja (Critical / Warning) |
💡 Der ACS712 und der INA3221 konkurrieren kaum miteinander. Der ACS712 ist galvanisch getrennt und kann Wechselstrom – er ist das Mittel der Wahl für Netzspannung oder große Motorströme. Der INA3221 gewinnt überall dort, wo es um kleine Gleichströme und Genauigkeit geht: Ein ESP32 im Deep Sleep zieht 20 µA, das sieht ein Hall-Sensor prinzipbedingt nie. Mehr zum ACS712 im Artikel ACS712 Stromsensor mit Arduino auslesen.
Das brauchst du
1Verkabelung: sechs Schraubklemmen, zwei Signalpins
Das Modul hat pro Kanal zwei Anschlüsse, die im Datenblatt VIN+ und VIN− heißen. Dazwischen sitzt der Shunt. Merksatz: Der Strom fließt von der Quelle in VIN+ hinein und aus VIN− zur Last heraus.
| INA3221 | Bedeutung | Anschluss |
|---|---|---|
| VS | Versorgung des Chips | 3,3 V (ESP32) oder 5 V (Nano) |
| GND | Masse – gemeinsam mit der Last | GND |
| SDA | I2C Daten | GPIO21 (ESP32) / A4 (Nano) |
| SCL | I2C Takt | GPIO22 (ESP32) / A5 (Nano) |
| CH1 VIN+ / VIN− | Kanal 1 Shunt | Quelle 1 → Last 1 |
| CH2 VIN+ / VIN− | Kanal 2 Shunt | Quelle 2 → Last 2 |
| CH3 VIN+ / VIN− | Kanal 3 Shunt | Quelle 3 → Last 3 |
| CRIT / WARN | Alarm-Ausgänge (Open-Drain) | optional auf freie GPIOs |
Die I2C-Adresse stellst du über den A0-Pin ein:
| A0 verbunden mit | I2C-Adresse |
|---|---|
| GND (Standard) | 0x40 |
| VS | 0x41 |
| SDA | 0x42 |
| SCL | 0x43 |
⚠️ Alle drei Kanäle teilen sich eine Masse. Der INA3221 misst die Busspannung immer gegen seinen eigenen GND. Du kannst also drei Verbraucher an derselben Versorgung überwachen – aber keine drei galvanisch getrennten Kreise. Wer das braucht, nimmt drei INA219 oder drei ACS712.
⚠️ 26 V sind das absolute Maximum, und zwar auf VIN+ und VIN−. Ein 24-V-Netzteil liegt im Leerlauf gerne bei 25,5 V – das ist noch in Ordnung, aber eine 6S-LiPo mit 25,2 V voll geladen hat keine Reserve mehr. Bei allem oberhalb von 20 V nominal solltest du dir die Spannungsspitzen genau ansehen.
⚠️ Den Laststrom nicht übers Breadboard führen. Die Kontaktfedern eines Steckbretts machen bei 1 A schon ein paar Zehntel Ohm Übergangswiderstand – mehr als der Shunt selbst. Das verfälscht nicht die Messung (die passiert ja direkt am Shunt), erzeugt aber Spannungsabfall und Wärme. Nimm 22AWG-Kabel direkt in die Schraubklemmen.
2Ohne Bibliothek: die Register direkt lesen
Der INA3221 hat nur eine Handvoll Register, und wer sie einmal von Hand angefasst hat, versteht danach jede Bibliothek. Der Aufbau ist erfreulich regelmäßig: Ab Register 0x01 folgen abwechselnd Shunt- und Busspannung für Kanal 1, 2 und 3.
#include <Wire.h>
#define INA3221_ADDR 0x40
#define REG_CONFIG 0x00
#define REG_CH1_SHUNT 0x01
#define REG_CH1_BUS 0x02
#define REG_MANUF_ID 0xFE
#define REG_DIE_ID 0xFF
// Shunt-Widerstand auf dem Modul, in Ohm
const float SHUNT_OHM[3] = { 0.1f, 0.1f, 0.1f };
void schreibeRegister(uint8_t reg, uint16_t wert) {
Wire.beginTransmission(INA3221_ADDR);
Wire.write(reg);
Wire.write((uint8_t)(wert >> 8));
Wire.write((uint8_t)(wert & 0xFF));
Wire.endTransmission();
}
int16_t leseRegister(uint8_t reg) {
Wire.beginTransmission(INA3221_ADDR);
Wire.write(reg);
if (Wire.endTransmission() != 0) return 0;
Wire.requestFrom((uint8_t)INA3221_ADDR, (uint8_t)2);
if (Wire.available() < 2) return 0;
uint16_t hi = Wire.read();
uint16_t lo = Wire.read();
return (int16_t)((hi << 8) | lo);
}
// Shunt-Spannung in Millivolt. 1 LSB = 40 uV, Wert steht in Bit 15..3
float leseShuntMV(uint8_t kanal) {
uint8_t reg = REG_CH1_SHUNT + kanal * 2;
int16_t roh = leseRegister(reg);
return (roh >> 3) * 0.04f; // 0,04 mV pro Schritt
}
// Busspannung in Volt. 1 LSB = 8 mV, Wert steht in Bit 15..3
float leseBusV(uint8_t kanal) {
uint8_t reg = REG_CH1_BUS + kanal * 2;
int16_t roh = leseRegister(reg);
return (roh >> 3) * 0.008f; // 8 mV pro Schritt
}
// Strom in Milliampere: I = U_shunt / R_shunt
float leseStromMA(uint8_t kanal) {
return leseShuntMV(kanal) / SHUNT_OHM[kanal];
}
void setup() {
Serial.begin(115200);
Wire.begin();
Wire.setClock(400000);
delay(100);
// Sanity-Check: Hersteller-ID muss 0x5449 ("TI") sein
uint16_t id = (uint16_t)leseRegister(REG_MANUF_ID);
Serial.print(F("Manufacturer-ID: 0x"));
Serial.println(id, HEX);
if (id != 0x5449) {
Serial.println(F("Kein INA3221 gefunden - Adresse oder Verkabelung pruefen!"));
}
// Config: alle 3 Kanaele an, Mittelung ueber 16 Werte,
// 1,1 ms Wandlungszeit fuer Bus und Shunt, Dauerbetrieb
// Bit 14..12 = Kanal-Enable (111)
// Bit 11..9 = Averaging (010 = 16 Samples)
// Bit 8..6 = Bus Conversion Time (100 = 1,1 ms)
// Bit 5..3 = Shunt Conversion Time (100 = 1,1 ms)
// Bit 2..0 = Mode (111 = Shunt+Bus, continuous)
uint16_t config = 0;
config |= (0b111 << 12);
config |= (0b010 << 9);
config |= (0b100 << 6);
config |= (0b100 << 3);
config |= 0b111;
schreibeRegister(REG_CONFIG, config);
Serial.println(F("INA3221 konfiguriert."));
}
void loop() {
for (uint8_t k = 0; k < 3; k++) {
float u = leseBusV(k);
float i = leseStromMA(k);
float p = u * i; // Leistung in Milliwatt
Serial.print(F("CH")); Serial.print(k + 1);
Serial.print(F(": ")); Serial.print(u, 3); Serial.print(F(" V "));
Serial.print(i, 1); Serial.print(F(" mA "));
Serial.print(p, 1); Serial.print(F(" mW "));
}
Serial.println();
delay(500);
}
💡 Die drei Nachkommastellen im Shunt-Register kommen nicht von ungefähr. Der ADC liefert 13 Bit, die im 16-Bit-Register linksbündig sitzen – die untersten drei Bit sind immer null. Deshalb >> 3 vor der Multiplikation. Wer das vergisst, misst den achtfachen Wert und wundert sich, warum die LED angeblich 800 mA zieht.
ℹ️ Was die Mittelung bringt. Mit Averaging = 16 und 1,1 ms Wandlungszeit dauert ein kompletter Durchlauf über alle drei Kanäle rund 106 ms – dafür ist das Ergebnis deutlich ruhiger als der Einzelwert. Willst du kurze Stromspitzen sehen (etwa den Anlaufstrom eines Motors), setz Averaging auf 1 und die Wandlungszeit auf 140 µs. Dann misst du alle 0,9 ms, siehst aber auch jedes bisschen Rauschen.
3Energiemessgerät mit mAh- und mWh-Zähler
Ein Momentanwert beantwortet nicht die eigentliche Frage: „Wie lange hält der Akku?" Dafür brauchst du das Integral über die Zeit – und genau das macht dieser Sketch, sauber über millis() statt über gezählte Schleifendurchläufe:
#include <Wire.h>
#define INA3221_ADDR 0x40
float leseBusV(uint8_t kanal); // wie in Schritt 2
float leseStromMA(uint8_t kanal); // wie in Schritt 2
void ina3221Init(); // Config-Teil aus Schritt 2
struct KanalStatistik {
const char* name;
double ladungMAh; // aufsummierte Ladung
double energieMWh; // aufsummierte Energie
float stromMax;
float spannungMin;
float spannungMax;
};
KanalStatistik kanal[3] = {
{ "Solar ", 0, 0, 0, 99, 0 },
{ "Akku ", 0, 0, 0, 99, 0 },
{ "Last ", 0, 0, 0, 99, 0 }
};
uint32_t letzteMessung = 0;
uint32_t letzteAusgabe = 0;
const uint16_t MESS_INTERVALL_MS = 200;
const uint16_t AUSGABE_INTERVALL_MS = 2000;
void setup() {
Serial.begin(115200);
Wire.begin();
Wire.setClock(400000);
delay(100);
ina3221Init();
letzteMessung = millis();
Serial.println(F("Messung laeuft. 'r' im Serial Monitor setzt zurueck."));
}
void loop() {
uint32_t jetzt = millis();
// --- Messen und integrieren ---
if (jetzt - letzteMessung >= MESS_INTERVALL_MS) {
// Echte vergangene Zeit verwenden, nicht das Soll-Intervall
double stunden = (jetzt - letzteMessung) / 3600000.0;
letzteMessung = jetzt;
for (uint8_t k = 0; k < 3; k++) {
float u = leseBusV(k);
float i = leseStromMA(k);
kanal[k].ladungMAh += (double)i * stunden;
kanal[k].energieMWh += (double)i * u * stunden;
if (i > kanal[k].stromMax) kanal[k].stromMax = i;
if (u < kanal[k].spannungMin) kanal[k].spannungMin = u;
if (u > kanal[k].spannungMax) kanal[k].spannungMax = u;
}
}
// --- Ausgabe als Tabelle ---
if (jetzt - letzteAusgabe >= AUSGABE_INTERVALL_MS) {
letzteAusgabe = jetzt;
Serial.println(F("----------------------------------------------------"));
Serial.print(F("Laufzeit: "));
Serial.print(jetzt / 1000);
Serial.println(F(" s"));
Serial.println(F("Kanal U[V] I[mA] mAh mWh Imax[mA]"));
for (uint8_t k = 0; k < 3; k++) {
float u = leseBusV(k);
float i = leseStromMA(k);
char zeile[80];
snprintf(zeile, sizeof(zeile),
"%s %6.3f %8.1f %8.2f %8.2f %8.1f",
kanal[k].name, u, i,
(float)kanal[k].ladungMAh,
(float)kanal[k].energieMWh,
kanal[k].stromMax);
Serial.println(zeile);
}
}
// --- Zuruecksetzen per Tastendruck ---
if (Serial.available() && Serial.read() == 'r') {
for (uint8_t k = 0; k < 3; k++) {
kanal[k].ladungMAh = 0;
kanal[k].energieMWh = 0;
kanal[k].stromMax = 0;
kanal[k].spannungMin = 99;
kanal[k].spannungMax = 0;
}
Serial.println(F("Zaehler zurueckgesetzt."));
}
}
💡 Die echte Zeitdifferenz statt des Soll-Intervalls zu verwenden, ist kein Detail. Sobald irgendwo im Sketch ein WLAN-Reconnect, ein SD-Schreibvorgang oder ein Displayupdate dazwischenfunkt, wird aus dem 200-ms-Takt schnell ein 340-ms-Takt. Wer stur mit 0,2 Sekunden rechnet, verliert damit ein Drittel der gemessenen Ladung. Das Prinzip dahinter erklärt der Artikel Arduino millis() statt delay().
ℹ️ Warum double für die Summen? Auf dem ESP32 sind float und double beide 64 Bit – dort ist es egal. Auf dem Arduino Nano ist double aber identisch mit float (32 Bit, ca. 7 Stellen). Wenn du dort tagelang mAh aufsummierst, verschluckt die Gleitkomma-Addition irgendwann die kleinen Beiträge. Abhilfe: als uint32_t in Mikro-Amperestunden zählen und erst bei der Ausgabe umrechnen.
4Überstrom-Warnung, die der Chip selbst überwacht
Das eigentliche Alleinstellungsmerkmal des INA3221 kommt jetzt: Er kann pro Kanal zwei Schwellwerte in Hardware überwachen und meldet sich über die Pins CRIT und WARN. Dein Sketch muss nicht pollen – er wird geweckt.
#include <Wire.h>
#define INA3221_ADDR 0x40
#define REG_CH1_CRIT 0x07 // dann 0x09, 0x0B
#define REG_CH1_WARN 0x08 // dann 0x0A, 0x0C
#define REG_MASK_ENABLE 0x0F
#define PIN_CRIT 25
#define PIN_WARN 26
#define PIN_LASTABWURF 27
const float SHUNT_OHM = 0.1f;
void schreibeRegister(uint8_t reg, uint16_t wert); // wie in Schritt 2
int16_t leseRegister(uint8_t reg); // wie in Schritt 2
float leseStromMA(uint8_t kanal); // wie in Schritt 2
void ina3221Init(); // wie in Schritt 2
// Grenzwert in mA in das Registerformat umrechnen
void setzeGrenzwert(uint8_t reg, float grenzeMA) {
float shuntMV = grenzeMA * SHUNT_OHM; // U = I * R
int16_t schritte = (int16_t)(shuntMV / 0.04f);
schreibeRegister(reg, (uint16_t)(schritte << 3));
}
volatile bool critAusgeloest = false;
volatile bool warnAusgeloest = false;
void IRAM_ATTR critIsr() { critAusgeloest = true; }
void IRAM_ATTR warnIsr() { warnAusgeloest = true; }
void setup() {
Serial.begin(115200);
Wire.begin();
Wire.setClock(400000);
delay(100);
ina3221Init();
pinMode(PIN_LASTABWURF, OUTPUT);
digitalWrite(PIN_LASTABWURF, HIGH); // Last zunaechst eingeschaltet
// CRIT und WARN sind Open-Drain -> Pullup noetig
pinMode(PIN_CRIT, INPUT_PULLUP);
pinMode(PIN_WARN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_CRIT), critIsr, FALLING);
attachInterrupt(digitalPinToInterrupt(PIN_WARN), warnIsr, FALLING);
// Kanal 3 (Last): Warnung ab 800 mA, Notabschaltung ab 1200 mA
setzeGrenzwert(REG_CH1_WARN + 4, 800.0f); // 0x0C
setzeGrenzwert(REG_CH1_CRIT + 4, 1200.0f); // 0x0B
// Mask/Enable: Summation aus, CRIT und WARN fuer Kanal 3 aktiv
schreibeRegister(REG_MASK_ENABLE, 0x0000);
Serial.println(F("Schutz aktiv: Warnung 800 mA, Abschaltung 1200 mA"));
}
void loop() {
if (critAusgeloest) {
critAusgeloest = false;
digitalWrite(PIN_LASTABWURF, LOW); // sofort abschalten
Serial.print(F("!! KRITISCH - Last abgeworfen bei "));
Serial.print(leseStromMA(2), 0);
Serial.println(F(" mA"));
// Das Mask/Enable-Register lesen loescht die Flags im Chip
leseRegister(REG_MASK_ENABLE);
delay(3000); // Abkuehlpause
digitalWrite(PIN_LASTABWURF, HIGH);
Serial.println(F("Last wieder zugeschaltet."));
}
if (warnAusgeloest) {
warnAusgeloest = false;
Serial.print(F("Warnung: "));
Serial.print(leseStromMA(2), 0);
Serial.println(F(" mA ueberschritten"));
leseRegister(REG_MASK_ENABLE);
}
delay(10);
}
⚠️ CRIT und WARN sind Open-Drain-Ausgänge. Ohne Pullup – intern per INPUT_PULLUP oder extern mit 10 kΩ aus dem Widerstands-Sortiment – hängt der Pin in der Luft und der Interrupt feuert zufällig. Die Flanke ist FALLING, weil der Chip im Alarmfall gegen Masse zieht.
⚠️ Die Flags müssen gelöscht werden. Ein Lesezugriff auf das Mask/Enable-Register 0x0F setzt die Alarmbits zurück. Vergisst du das, bleibt der Ausgang auf LOW und du bekommst nie wieder einen neuen Interrupt.
ℹ️ Wie schnell reagiert das? Der Vergleich passiert nach jeder Wandlung – mit 140 µs Wandlungszeit und Averaging 1 also nach spätestens rund 0,4 ms. Das ist schneller als jede Software-Abfrage, aber langsam gegenüber einem echten Kurzschluss. Als Ersatz für eine Sicherung taugt das nicht – als Schutz gegen „Motor blockiert und zieht dauerhaft zu viel" dagegen sehr gut.
5Alle drei Kanäle auf dem OLED
Drei Zeilen, drei Kanäle, jeweils Spannung, Strom und ein Balken für die relative Auslastung. Auf dem 0,96 Zoll OLED passt das exakt – und weil beide Bausteine am selben I2C-Bus hängen, kommt kein weiteres Kabel dazu:
#include <Wire.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
float leseBusV(uint8_t kanal); // wie in Schritt 2
float leseStromMA(uint8_t kanal); // wie in Schritt 2
void ina3221Init(); // wie in Schritt 2
const char* NAMEN[3] = { "SOL", "BAT", "LST" };
const float SKALA_MA[3] = { 1000, 2000, 1500 }; // Vollausschlag je Kanal
double ladungMAh[3] = { 0, 0, 0 };
uint32_t letzteMessung = 0;
uint8_t ansicht = 0; // 0 = Live, 1 = Summen
uint32_t letzterWechsel = 0;
void setup() {
Wire.begin();
Wire.setClock(400000);
u8g2.begin();
u8g2.enableUTF8Print();
ina3221Init();
letzteMessung = millis();
}
void zeichneLive() {
u8g2.setFont(u8g2_font_5x7_tf);
u8g2.drawStr(0, 7, "KANAL VOLT mA");
u8g2.drawHLine(0, 9, 128);
for (uint8_t k = 0; k < 3; k++) {
uint8_t y = 20 + k * 15;
float u = leseBusV(k);
float i = leseStromMA(k);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, y, NAMEN[k]);
char t[16];
snprintf(t, sizeof(t), "%5.2fV", u);
u8g2.drawStr(24, y, t);
snprintf(t, sizeof(t), "%6.0f", i);
u8g2.drawStr(68, y, t);
// Balken unter der Zeile
float anteil = i / SKALA_MA[k];
if (anteil < 0) anteil = 0;
if (anteil > 1) anteil = 1;
uint8_t b = (uint8_t)(anteil * 126);
u8g2.drawFrame(0, y + 2, 128, 3);
if (b > 0) u8g2.drawBox(1, y + 3, b, 1);
}
}
void zeichneSummen() {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(0, 8, "Verbrauch gesamt");
u8g2.drawHLine(0, 11, 128);
u8g2.setFont(u8g2_font_6x10_tf);
for (uint8_t k = 0; k < 3; k++) {
char t[24];
snprintf(t, sizeof(t), "%s %8.2f mAh", NAMEN[k], (float)ladungMAh[k]);
u8g2.drawStr(0, 24 + k * 12, t);
}
u8g2.setFont(u8g2_font_5x7_tf);
char t[24];
snprintf(t, sizeof(t), "Laufzeit %lu min", millis() / 60000UL);
u8g2.drawStr(0, 62, t);
}
void loop() {
uint32_t jetzt = millis();
double stunden = (jetzt - letzteMessung) / 3600000.0;
letzteMessung = jetzt;
for (uint8_t k = 0; k < 3; k++) {
ladungMAh[k] += (double)leseStromMA(k) * stunden;
}
// Alle 5 Sekunden zwischen Live und Summen umschalten
if (jetzt - letzterWechsel > 5000) {
letzterWechsel = jetzt;
ansicht = 1 - ansicht;
}
u8g2.clearBuffer();
if (ansicht == 0) zeichneLive();
else zeichneSummen();
u8g2.sendBuffer();
delay(150);
}
💡 Ein eigener Vollausschlag pro Kanal macht die Balken erst brauchbar. Ein Solarpanel liefert vielleicht 400 mA, ein Motor zieht 1,5 A – auf einer gemeinsamen Skala ist der Solar-Balken immer ein Strich. Mit SKALA_MA[] bekommt jeder Kanal seinen eigenen Maßstab, und alle drei Balken bewegen sich sichtbar.
Troubleshooting
⚠️ Der I2C-Scanner findet nichts: Prüfe, ob VS versorgt ist. Der INA3221 braucht eine eigene Logikversorgung – Spannung an den Messkanälen allein reicht nicht. Wie du systematisch suchst, steht im Artikel I2C-Bus mit Arduino verstehen.
⚠️ Der Strom ist genau achtmal zu groß: Das >> 3 fehlt. Die 13 ADC-Bits sitzen linksbündig im 16-Bit-Register.
⚠️ Der Strom ist negativ, obwohl er fließt: VIN+ und VIN− sind vertauscht. Entweder die Klemmen tauschen oder im Code das Vorzeichen drehen – der Chip misst vorzeichenrichtig und beide Richtungen sind zulässig.
⚠️ Die Busspannung zeigt 0 V, der Strom stimmt aber: Du hast nur den Shunt in Reihe gelegt, aber die Last hängt an einer anderen Masse. Die Busspannung wird gegen den GND des Moduls gemessen – ohne gemeinsame Masse ist dieser Wert bedeutungslos.
⚠️ Bei kleinen Strömen schwanken die Werte um ±2 mA: Das ist die Auflösungsgrenze. 40 µV an 0,1 Ω sind 0,4 mA pro Schritt, dazu kommt Offsetrauschen. Erhöhe das Averaging auf 128, oder löte für kleine Ströme einen größeren Shunt (1 Ω statt 0,1 Ω) auf – dann misst du zehnmal feiner, aber nur noch bis 160 mA.
⚠️ Die Messwerte hängen davon ab, ob ein Motor läuft: Der Spannungsabfall auf der gemeinsamen Masseleitung koppelt in die Messung. Sternförmig verdrahten: alle GND-Leitungen an einem Punkt, nicht in einer Kette.
⚠️ Das Modul wird warm: Bei 1,6 A durch 0,1 Ω fallen 160 mV ab, das sind 0,26 W am Shunt. Das ist normal. Wird es heiß, liegst du deutlich über dem Nennstrom – der SMD-Shunt verträgt typischerweise 1 W dauerhaft.
Praxistipps
- Nutze alle drei Kanäle sinnvoll. Der Klassiker in Solarprojekten: Kanal 1 Panel, Kanal 2 Akku, Kanal 3 Verbraucher. Aus der Differenz siehst du sofort, ob der Akku gerade geladen oder entladen wird – ohne einen einzigen zusätzlichen Sensor.
-
Shunt tauschen verändert den Messbereich. Die Formel ist simpel: max. Strom = 163,8 mV / R. Bei 0,1 Ω sind das 1,6 A, bei 0,01 Ω sind es 16 A (dann aber nur noch 4 mA Auflösung), bei 1 Ω nur 160 mA (dafür 40 µA Auflösung). Trag den neuen Wert in
SHUNT_OHM[]ein, sonst stimmt gar nichts mehr. -
Der Summationskanal spart Code. Der INA3221 kann im Register
0x0Ddie Shunt-Spannungen ausgewählter Kanäle addieren und dafür einen eigenen Grenzwert überwachen. Praktisch, wenn nicht ein Kanal, sondern die Summe aller Verbraucher ein Limit hat. -
Powerdown zwischen den Messungen. Modus
000im Config-Register schaltet den Chip auf unter 2 µA. In batteriebetriebenen Loggern, die nur einmal pro Minute messen, ist das der Unterschied zwischen Wochen und Monaten Laufzeit – siehe ESP32 Deep Sleep Tutorial. - Vier Module an einem Bus. Mit den Adressen 0x40 bis 0x43 überwachst du zwölf Kanäle über dieselben zwei Leitungen – genug für einen kompletten Laborplatz.
- Daten mitschreiben statt nur anschauen. Mit dem SD-Modul und einem Zeitstempel vom DS3231 hast du in einer Nacht ein vollständiges Verbrauchsprofil. Wie das Logging aufgebaut wird, zeigt der Artikel Arduino Datenlogger mit Micro-SD-Karte.
- Leitungen dick genug wählen. Ein Dupont-Kabel ist etwa AWG28 und macht bei 1 A schon spürbar Spannungsabfall. Für die Leistungsseite gehört 22AWG-Kabel in die Schraubklemme, gern mit JST-XH-Steckern zum Trennen.
🚀 Bauteile für dein Energiemonitoring
- INA3221 3-Kanal Strom- & Spannungssensor – drei Kanäle, ein I2C-Bus
- ACS712 Hall-Stromsensor – die Ergänzung für Wechselstrom und große Ströme
- ESP32 DEV KIT WROOM32 – genug Rechenleistung fürs Integrieren und Loggen
- 0,96 Zoll OLED Display SSD1306 – drei Kanäle auf einen Blick
- TP4056 Lademodul (3er Set) – der Ladekanal im Solaraufbau
- MT3608 Step-Up Modul – Wirkungsgrad endlich mal wirklich messen
- Micro-SD-Karten-Modul – für Verbrauchsprofile über Tage
- 22AWG Kabel 2-adrig und JST-XH Stecker-Sortiment
Fazit
Der INA3221 ist einer der Sensoren, die ein Projekt nicht nur ergänzen, sondern erklären. Solange du nur die Spannung siehst, rätst du – sobald du Strom, Spannung und aufsummierte Ladung auf drei Kanälen gleichzeitig hast, siehst du sofort, wohin die Energie verschwindet. Und weil alles über I2C läuft, kostet die Erkenntnis genau zwei Pins.
Die zwei Stolpersteine sind schnell benannt: das >> 3 beim Registerlesen und die gemeinsame Masse aller drei Kanäle. Wer beides beachtet, hat mit dem Code aus diesem Artikel ein Messgerät, das im Alltag mehr taugt als so manches gekaufte USB-Wattmeter – inklusive mAh-Zähler, Überstromschutz in Hardware und einer Anzeige, die genau das zeigt, was du sehen willst.