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

  1. De-energize the system. Disconnect the Li-Ion cell or bench power supply before touching any wires.
  2. 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.
  3. Wire the Load Path. Connect your power source positive to the INA219 VIN+. Connect INA219 VIN- to the battery positive. Connect the battery negative to the power source negative (completing the shunt circuit).
  4. Wire the Relay. Connect the Nano D8 pin to the Relay IN terminal. Wire your charging circuit's positive supply line through the relay's COM (Common) and NO (Normally Open) terminals.
  5. 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.