The if else statement Arduino construct is the fundamental mechanism for evaluating boolean conditions and branching logic in embedded C++. While beginners use it for simple button presses, professional firmware relies on nested if/else if/else chains to build state machines, enforce safety cutoffs, and manage multi-stage physical processes. When managing high-energy systems like lithium-ion batteries, a poorly constructed conditional block can lead to relay chatter, overcharging, or bricked microcontrollers.
This guide moves past abstract syntax. We will build a multi-stage Li-Ion battery charge monitor and cutoff controller. You will see exactly how to map physical voltage thresholds into robust if/else logic, wire the hardware, and debug the three most common compiler and runtime traps that plague embedded conditionals.
Project Overview & Hardware BOM
This project targets the Arduino Nano v3 (ATmega328P, 5V/16MHz). We chose the Nano for its compact footprint and direct 5V logic, which interfaces cleanly with standard I2C displays and 5V relay coils without needing logic level shifters.
Bill of Materials
- Microcontroller: Arduino Nano v3 (ATmega328P variant, pre-soldered headers)
- Sensor: INA219 Bi-directional Current/Power Sensor Breakout (Adafruit 904 or equivalent generic)
- Display: 1602 I2C LCD with PCF8574T backpack (5V tolerant)
- Actuator: 5V Relay Module (Songle SRD-05VDC-SL-C) with optocoupler isolation
- Power: 18650 Li-Ion cell holder with BMS, or a bench power supply for testing
- Wiring: 22 AWG solid core hook-up wire, breadboard
Difficulty Rating: Intermediate. Requires basic I2C soldering/wiring and an understanding of floating-point math in C++.
Threshold Logic: Mapping Physics to If/Else Conditions
Before writing a single line of code, you must define the physical boundaries of your system. A lithium-ion (NMC) cell requires a specific charging profile: Pre-charge, Constant Current (Bulk), Constant Voltage (Absorption), and Cutoff. If your if else statement Arduino logic does not account for these distinct stages, the system will fail.
Below is the data-dense threshold table that directly dictates our conditional branching. Note the inclusion of a hysteresis deadband—a critical real-world requirement to prevent the relay from rapidly switching on and off (chatter) when the voltage hovers exactly on the threshold boundary.
| Charging Stage | Voltage Threshold (V) | Current Condition (mA) | if/else Logic Block | Hardware Action |
|---|---|---|---|---|
| Pre-Charge / Trickle | < 2.8V | N/A | if (voltage < 2.8) |
Relay OFF (Fault/Protect) |
| Bulk (Constant Current) | 2.8V to 4.1V | > 100mA | else if (voltage < 4.1) |
Relay ON (Charge Active) |
| Absorption (Constant Voltage) | 4.1V to 4.2V | Tapering | else if (voltage < 4.25) |
Relay ON (Top-off) |
| Float / Cutoff | ≥ 4.2V (with hysteresis) | < 50mA | else (Catch-all) |
Relay OFF (Charge Complete) |
By structuring the logic with else if and a final else catch-all, we ensure that only one hardware state can be active at any given millisecond, preventing conflicting relay commands.
Pin Mapping & Wiring Procedure
Correct I2C addressing and relay isolation are critical. The INA219 and the LCD share the I2C bus, so ensure their addresses do not conflict (default INA219 is 0x40, PCF8574T LCD is usually 0x27).
| Component | Component Pin | Arduino Nano v3 Pin | Notes |
|---|---|---|---|
| INA219 Sensor | VCC | 5V | Do not use 3.3V; logic levels need 5V. |
| INA219 Sensor | GND | GND | Common ground with Nano and Relay. |
| INA219 Sensor | SCL | A5 | I2C Clock line. |
| INA219 Sensor | SDA | A4 | I2C Data line. |
| I2C LCD 1602 | VCC | 5V | Draws ~20mA with backlight on. |
| I2C LCD 1602 | GND | GND | |
| I2C LCD 1602 | SCL | A5 | Shared with INA219. |
| I2C LCD 1602 | SDA | A4 | Shared with INA219. |
| Relay Module | VCC | 5V | Power the optocoupler and coil. |
| Relay Module | GND | GND | |
| Relay Module | IN (Signal) | D8 | Digital output to trigger relay. |
Wiring Steps
- De-energize the system. Disconnect the Li-Ion cell or bench power supply before touching any wires.
- Wire the I2C Bus. Connect the SDA (A4) and SCL (A5) pins from the Nano to both the INA219 and the LCD backpack. Daisy-chain the 5V and GND lines.
- Wire the Load Path. Connect your power source positive to the INA219
VIN+. Connect INA219VIN-to the battery positive. Connect the battery negative to the power source negative (completing the shunt circuit). - Wire the Relay. Connect the Nano D8 pin to the Relay
INterminal. Wire your charging circuit's positive supply line through the relay'sCOM(Common) andNO(Normally Open) terminals. - Verify connections. Use a multimeter in continuity mode to verify no shorts exist between 5V and GND before applying power.
Complete Compilable Code with Error Handling
The following code is fully compilable for the Arduino Nano v3. It utilizes the Adafruit_INA219 and LiquidCrystal_I2C libraries. Notice how the if/else block explicitly handles sensor initialization failures and implements a 0.05V hysteresis deadband to protect the relay contacts.
#include <Wire.h>
#include <Adafruit_INA219.h>
#include <LiquidCrystal_I2C.h>
// --- PIN DEFINITIONS ---
#define RELAY_PIN 8
#define I2C_LCD_ADDR 0x27
// --- THRESHOLD CONSTANTS ---
const float V_PRECHARGE = 2.80;
const float V_BULK = 4.10;
const float V_ABSORPTION = 4.20;
const float HYSTERESIS = 0.05; // Prevents relay chatter
// --- GLOBAL OBJECTS ---
Adafruit_INA219 ina219;
LiquidCrystal_I2C lcd(I2C_LCD_ADDR, 16, 2);
bool systemFault = false;
bool chargeComplete = false;
void setup() {
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // HIGH = Relay OFF (Active LOW module)
Serial.begin(115200);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("System Booting..");
// --- ERROR HANDLING: Sensor Initialization ---
if (!ina219.begin()) {
systemFault = true;
lcd.clear();
lcd.print("FATAL: INA219");
lcd.setCursor(0, 1);
lcd.print("I2C Not Found!");
Serial.println(F("Failed to find INA219 chip. Check wiring."));
while (1) { delay(1000); } // Halt execution safely
}
// Calibrate for lower current, higher resolution (optional but recommended)
ina219.setCalibration_16V_400mA();
lcd.clear();
}
void loop() {
if (systemFault) return; // Safety bail-out
float shuntvoltage = ina219.getShuntVoltage_mV();
float busvoltage = ina219.getBusVoltage_V();
float current_mA = ina219.getCurrent_mA();
float loadvoltage = busvoltage + (shuntvoltage / 1000);
// --- CORE IF/ELSE STATEMENT ARDUINO LOGIC ---
if (loadvoltage < V_PRECHARGE) {
// Stage 1: Pre-charge / Deep Discharge Fault
digitalWrite(RELAY_PIN, HIGH); // OFF
updateLCD("FAULT: LOW V", loadvoltage, current_mA);
chargeComplete = false;
}
else if (loadvoltage < V_BULK) {
// Stage 2: Bulk Charge
digitalWrite(RELAY_PIN, LOW); // ON
updateLCD("BULK CHARGE", loadvoltage, current_mA);
chargeComplete = false;
}
else if (loadvoltage < (V_ABSORPTION + HYSTERESIS)) {
// Stage 3: Absorption / Top-off
digitalWrite(RELAY_PIN, LOW); // ON
updateLCD("ABSORPTION", loadvoltage, current_mA);
chargeComplete = false;
}
else {
// Stage 4: Cutoff / Float (Catch-all)
// Implemented with a latch to prevent re-triggering if voltage sags slightly under load removal
if (!chargeComplete) {
digitalWrite(RELAY_PIN, HIGH); // OFF
chargeComplete = true;
}
updateLCD("CHARGE DONE", loadvoltage, current_mA);
}
delay(500); // Sample rate limiting
}
void updateLCD(const char* stage, float v, float ma) {
lcd.setCursor(0, 0);
lcd.print(stage);
lcd.print(" "); // Clear trailing chars
lcd.setCursor(0, 1);
lcd.print(v, 2);
lcd.print("V ");
lcd.print(ma, 0);
lcd.print("mA ");
}
Debugging If/Else Logic Traps: First 3 Things to Check
When your if else statement Arduino logic fails to compile or behaves erratically on the bench, avoid rewriting the whole sketch. Follow this ranked diagnostic path.
1. The Stray Semicolon Syntax Error
Exact Error String: error: expected primary-expression before 'else'
Cause: You placed a semicolon immediately after the if condition. Example: if (loadvoltage < 4.2); { ... }. The compiler interprets the semicolon as an empty statement, making the subsequent else orphaned and illegal.
Fix: Remove the semicolon. Ensure the opening curly brace { immediately follows the closing parenthesis of the condition.
2. The Assignment vs. Equality Trap
Exact Error String: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
Cause: Using a single equals sign = (assignment) instead of a double equals sign == (comparison) inside the condition. Example: if (chargeComplete = true). This assigns the value and always evaluates to true, bypassing your logic branches.
Fix: Change = to ==. Better yet, for booleans, just write if (chargeComplete). For floats, never use ==; always use <, >, or check if the absolute difference is within an epsilon tolerance.
3. Runtime Relay Chatter (Missing Hysteresis)
Symptom: The code compiles fine, but the relay clicks on and off rapidly when the battery hits exactly 4.2V.
Cause: The sensor reads 4.20V. The else block triggers, turning the relay off. Without the charger connected, the battery voltage instantly sags to 4.18V. The next loop iteration hits the else if (loadvoltage < V_ABSORPTION) block, turning the relay back on. The voltage jumps back to 4.20V, and the cycle repeats at 500ms intervals, destroying the relay contacts.
Fix: Implement a hysteresis deadband (as shown in the code above with HYSTERESIS = 0.05) or use a boolean state latch (chargeComplete) to lock the system out of the charging branches once the final threshold is crossed.
Extending and Simplifying the Build
While the if/else if/else chain is perfect for linear threshold checks, it becomes unmaintainable if your project grows to include user inputs, temperature derating, or fault resets.
When to Simplify: Switch/Case
If your logic depends on discrete integer states (e.g., systemState = 1 for Bulk, 2 for Absorption) rather than raw floating-point sensor readings, refactor the inner loop to use a switch/case statement. It compiles to a more efficient jump table and is easier to read. However, switch cannot evaluate ranges (like voltage < 4.2), so you must use if/else to determine the state first, then switch to execute the state's behavior.
When to Extend: Finite State Machines (FSM)
For advanced battery management, move away from blocking delay() calls and nested conditionals. Implement a non-blocking Finite State Machine using the millis() timer. Libraries like Arduino-StateMachine allow you to define entry, state, and exit functions for each charging phase, isolating your if/else logic into modular, testable blocks.
Mastering the if else statement Arduino construct is not just about syntax; it is about translating physical realities—voltage sag, sensor noise, and mechanical relay limits—into bulletproof software logic. Always map your thresholds on paper first, build in hysteresis, and let the compiler warnings guide your debugging.






