Solar battery monitor with an INA3221 on an Arduino Uno
One INA3221 reads a solar panel, an 18650 and its load on three channels, and a 20x4 LCD shows volts, currents and whether the cell is charging. The whole build, an Arduino Uno and 6 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Intermediate About 45 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $43. On Mokxi they're free.
What you'll learn
- Measuring current with a shunt resistor
- Reading several I2C devices on one bus
- Signed current: charging versus discharging
- Why a battery sits between a charger and a load
- Formatting numbers for a character LCD
- Difficulty: Intermediate
- Time: About 45 minutes
- Board: Arduino Uno R3
- Parts: 8
How it works
An INA3221 measures three places in a small solar power system: the panel, the battery and the lamp. Each channel has a shunt resistor. The chip measures the tiny voltage across it and the library turns that into current.
Power flows from the panel through a TP4056 charger to the battery and the lamp. In sun, the panel runs the lamp and charges the cell with the rest. In the dark, the cell runs the lamp alone.
A 20x4 LCD shows volts and milliamps for each channel. The last line says Charging, On battery or Idle, plus the panel's power in watts. Serial prints the same numbers once a second.
Parts list
8 parts, plus the jumper wires. They retail for about $43; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Three-channel current monitor, INA3221
- 1 × Solar panel, 6 V 1 W
- 1 × Li-ion charger, TP4056
- 1 × Battery: AA, coin cell, LiPoIn Handheld Console Kit, Laser Sword Kit.
- 1 × Filament lamp
- 1 × Character LCD, 20x4, with I2C backpackIn Spaceship Panel Kit, Escape Box Kit.
- 1 × Power, 5 V
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Character LCD, 20x4, with I2C backpack pin SCL | Arduino Uno R3 pin SCL |
| Character LCD, 20x4, with I2C backpack pin SDA | Arduino Uno R3 pin SDA |
| Arduino Uno R3 pin GND | Ground |
| Three-channel current monitor, INA3221 pin VCC | Arduino Uno R3 pin 5V |
| Three-channel current monitor, INA3221 pin GND | Arduino Uno R3 pin GND |
| Three-channel current monitor, INA3221 pin SDA | Arduino Uno R3 pin A4 |
| Three-channel current monitor, INA3221 pin SCL | Arduino Uno R3 pin A5 |
| Solar panel, 6 V 1 W pin + | Three-channel current monitor, INA3221 pin IN1+ |
| Li-ion charger, TP4056 pin IN+ | Three-channel current monitor, INA3221 pin IN1- |
| Three-channel current monitor, INA3221 pin IN3+ | Three-channel current monitor, INA3221 pin IN2+ |
| Li-ion charger, TP4056 pin BAT+ | Three-channel current monitor, INA3221 pin IN2+ |
| Battery: AA, coin cell, LiPo pin V+ | Three-channel current monitor, INA3221 pin IN2- |
| Filament lamp pin 1 | Three-channel current monitor, INA3221 pin IN3- |
| Solar panel, 6 V 1 W pin - | Ground |
| Li-ion charger, TP4056 pin IN- | Ground |
| Li-ion charger, TP4056 pin BAT- | Ground |
| Battery: AA, coin cell, LiPo pin V- | Ground |
| Filament lamp pin 2 | Ground |
| Character LCD, 20x4, with I2C backpack pin GND | Ground |
| Character LCD, 20x4, with I2C backpack pin VCC | 5 V |
The note on the canvas
Word for word, as it sits beside the circuit in the editor.
Try it
1. Press Run. In full sun the panel gives about 0.2 A: the lamp takes its share and the rest charges the 18650, so Batt reads plus and the last line says Charging.
2. Drag the sun slider on the panel down to 0. Solar falls to 0 mA, Batt turns minus and the last line says On battery: the cell now runs the lamp.
3. Halfway, at 500 W/m2, the panel gives about half the current.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Solar battery monitor: one INA3221 watches three places in a small solar
// power system at once, the panel, the battery and the load, and a 20x4 I2C
// LCD shows each one's volts and current and whether the battery is charging.
//
// INA3221 VCC 5V, GND, SDA A4, SCL A5, address 0x40 (A0 to GND)
// channel 1 the panel: panel + -> IN1+, IN1- -> TP4056 IN+
// channel 2 the battery: TP4056 BAT+ -> IN2+, IN2- -> the 18650's +
// channel 3 the load: IN2+ jumpered to IN3+, IN3- -> the lamp
// LCD 20x4 with an I2C backpack at 0x27: SDA, SCL, VCC 5V, GND
// Every - and GND joins: the panel, the TP4056, the cell, the lamp and the Uno.
//
// HOW IT WORKS
// Each channel has a 0.1 ohm shunt in series. The INA3221 measures the few
// millivolts across it (40 uV a step) and the voltage on its IN- side to
// ground (8 mV a step); the library divides the shunt voltage by the shunt
// resistance for the current. Channel 2 is signed: plus while the TP4056
// charges the cell, minus while the cell carries the load.
//
// The TP4056's output feeds both the cell and the load, so in sunshine the
// panel runs the lamp and charges the cell with what is left, and in the
// dark the cell runs the lamp on its own.
//
// WHAT TO CHANGE
// SHUNT_OHMS 0.1 on the common purple and CJMCU boards; some have 0.01
// IDLE_MA below this the battery counts as neither charging nor in use
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "INA3221.h"
INA3221 ina(0x40);
LiquidCrystal_I2C lcd(0x27, 20, 4);
const float SHUNT_OHMS = 0.1;
const float IDLE_MA = 10;
const uint8_t SOLAR = 0; // the library numbers the channels 0 to 2
const uint8_t BATTERY = 1;
const uint8_t LOAD = 2;
void showChannel(uint8_t row, const char *name, float volts, float mA, bool sign) {
char v[8];
char line[21];
dtostrf(volts, 5, 2, v);
int ma = (int)(mA + (mA < 0 ? -0.5 : 0.5));
if (sign) snprintf(line, sizeof line, "%-6s%sV %+5dmA ", name, v, ma);
else snprintf(line, sizeof line, "%-6s%sV %5dmA ", name, v, ma);
lcd.setCursor(0, row);
lcd.print(line);
}
void setup() {
Serial.begin(115200);
Wire.begin();
lcd.init();
lcd.backlight();
if (!ina.begin()) {
Serial.println("INA3221 not found at 0x40: check SDA, SCL and VCC");
lcd.print("No INA3221 at 0x40");
while (1);
}
for (uint8_t ch = 0; ch < 3; ch++) ina.setShuntR(ch, SHUNT_OHMS);
Serial.println("solar monitor ready");
}
void loop() {
float solarV = ina.getBusVoltage(SOLAR);
float solarMa = ina.getCurrent_mA(SOLAR);
float battV = ina.getBusVoltage(BATTERY);
float battMa = ina.getCurrent_mA(BATTERY);
float loadV = ina.getBusVoltage(LOAD);
float loadMa = ina.getCurrent_mA(LOAD);
showChannel(0, "Solar", solarV, solarMa, false);
showChannel(1, "Batt", battV, battMa, true);
showChannel(2, "Load", loadV, loadMa, false);
const char *state = "Idle";
if (battMa > IDLE_MA) state = "Charging";
else if (battMa < -IDLE_MA) state = "On battery";
char watts[8];
dtostrf(solarV * solarMa / 1000.0, 4, 2, watts);
char line[21];
snprintf(line, sizeof line, "%-10s Sun%sW", state, watts);
lcd.setCursor(0, 3);
lcd.print(line);
Serial.print("solar ");
Serial.print(solarV, 2);
Serial.print(" V ");
Serial.print(solarMa, 0);
Serial.print(" mA battery ");
Serial.print(battV, 2);
Serial.print(" V ");
Serial.print(battMa, 0);
Serial.print(" mA load ");
Serial.print(loadV, 2);
Serial.print(" V ");
Serial.print(loadMa, 0);
Serial.print(" mA ");
Serial.println(state);
delay(1000);
}
Code walkthrough
The key functions of the sketch, and what each one does.
showChannel sketch.ino
Prints one line on the LCD: a name, volts and milliamps, padded to 20 characters. It rounds the current to a whole number. The sign flag adds a plus or minus, which only the battery row uses.
void showChannel(uint8_t row, const char *name, float volts, float mA, bool sign) {
char v[8];
char line[21];
dtostrf(volts, 5, 2, v);
int ma = (int)(mA + (mA < 0 ? -0.5 : 0.5));
if (sign) snprintf(line, sizeof line, "%-6s%sV %+5dmA ", name, v, ma);
else snprintf(line, sizeof line, "%-6s%sV %5dmA ", name, v, ma);
lcd.setCursor(0, row);
lcd.print(line);
}setup sketch.ino
Starts serial, I2C and the LCD. If the INA3221 does not answer at 0x40, it shows an error and stops. Otherwise it gives each channel the SHUNT_OHMS value, 0.1 ohm, so currents come out right.
void setup() {
Serial.begin(115200);
Wire.begin();
lcd.init();
lcd.backlight();
if (!ina.begin()) {
Serial.println("INA3221 not found at 0x40: check SDA, SCL and VCC");
lcd.print("No INA3221 at 0x40");
while (1);
}
for (uint8_t ch = 0; ch < 3; ch++) ina.setShuntR(ch, SHUNT_OHMS);
Serial.println("solar monitor ready");
}loop sketch.ino
Reads volts and current for the solar, battery and load channels and shows them. The battery current decides the status: above IDLE_MA (10) is Charging, below minus 10 is On battery, otherwise Idle. Then it waits one second.
void loop() {
float solarV = ina.getBusVoltage(SOLAR);
float solarMa = ina.getCurrent_mA(SOLAR);
float battV = ina.getBusVoltage(BATTERY);
float battMa = ina.getCurrent_mA(BATTERY);
float loadV = ina.getBusVoltage(LOAD);
float loadMa = ina.getCurrent_mA(LOAD);
showChannel(0, "Solar", solarV, solarMa, false);
showChannel(1, "Batt", battV, battMa, true);
showChannel(2, "Load", loadV, loadMa, false);
const char *state = "Idle";
if (battMa > IDLE_MA) state = "Charging";
else if (battMa < -IDLE_MA) state = "On battery";
char watts[8];
dtostrf(solarV * solarMa / 1000.0, 4, 2, watts);
char line[21];
snprintf(line, sizeof line, "%-10s Sun%sW", state, watts);
lcd.setCursor(0, 3);
lcd.print(line);
Serial.print("solar ");
Serial.print(solarV, 2);
Serial.print(" V ");
Serial.print(solarMa, 0);
Serial.print(" mA battery ");
Serial.print(battV, 2);
// ... 11 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- The LCD says No INA3221 at 0x40. This project
Check the INA3221's SDA goes to A4, SCL to A5, and it has 5V and GND. - Currents read ten times too big or too small. This project
ChangeSHUNT_OHMS. Most boards use 0.1 ohm, but some use 0.01. - The battery row never goes negative. This project
Channel 2 is signed. Check IN2+ is on the charger side and IN2- on the battery's plus, not the other way around. - Current reads zero or is wrong. Three-channel current monitor
The shunt must sit in series with the load. Connect IN+ on the supply side and IN- on the load side. Reading is signed, so swapping them flips the sign. - The chip does not answer, or two boards clash. Three-channel current monitor
Set the address with the A0 pin: GND is 0x40, VS is 0x41, SDA is 0x42, SCL is 0x43. Max bus voltage is 26 V. Use a shared ground. - Measured voltage is far lower than the panel's rating. Solar panel
A panel is a current source with a limit. Under load its voltage falls, and without full sun the current drops. Open circuit voltage is higher (about 7.2 V) than the working voltage. - A battery drains back through the panel at night. Solar panel
Put a diode in series so current only flows toward the battery. Use a charge controller for batteries. Do not charge a lithium cell straight from a panel. - The cell does not charge or gets hot. Li-ion charger
Use one lithium-ion cell only, never two in series. Set the charge current with the programming resistor (1.2 kohm is 1 A). Use a protected cell.
Questions and answers
Why does the battery current go negative?
The INA3221 measures its shunt voltage with a sign. Channel 2 sits between the charger and the cell, so current into the cell reads plus, and current out of it, when the cell runs the lamp in the dark, reads minus.
Why is the panel only at about 3.9 V in full sun?
The TP4056 is a linear charger and the panel is close to a current source, so the panel's voltage falls to just above the cell's while it gives its whole 0.2 A. With nothing drawing on it the panel reads about 7.2 V.
What shunt does my INA3221 board have?
The common boards have 0.1 ohm on each channel, which is what setShuntR() is given here: 40 uV steps and about 1.6 A full scale. Set SHUNT_OHMS to match if yours has 0.01 ohm.
Can I measure more than three channels?
Up to four INA3221s share one I2C bus at 0x40 to 0x43, set by the A0 pin. Give each one its own INA3221 object.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.