A compact, open-source battery monitoring system for 48V battery banks. Built around the ESP32-C6 Mini, it reads four voltage taps, bidirectional current, and up to four temperature sensors — displaying live data on a 0.96" OLED and flagging faults through status LEDs and serial output.
- Voltage monitoring — measures four independent taps (12V, 32V, 48V, 64V) via a 12-bit SPI ADC
- Bidirectional current — charge and discharge detection with configurable sensitivity
- Temperature — up to 4× DS18B20 sensors on a single OneWire bus, fully bit-banged (no external library)
- Live OLED display — two alternating pages: voltages/current and temperatures
- Fault detection — overvoltage, undervoltage, overcurrent, overtemperature with hysteresis to prevent flicker
- Status LEDs — green = healthy, red = any active fault
- Non-blocking firmware — temperature conversion runs asynchronously; no
delay()in the main loop - Hardware watchdog — 5-second timeout reboots the system if any sensor stalls the loop
- Serial debug output — live readings at 115200 baud, all in a single batched line per cycle
- Custom PCB — KiCad hardware design files included
pbMonitorv2/
├── src/
│ └── pbMonitor/
│ └── pbMonitor.ino Main firmware (Arduino sketch)
├── hardware/
│ ├── *.kicad_pro KiCad project file
│ ├── *.kicad_sch Schematic source
│ ├── *.kicad_pcb PCB layout source
│ ├── schematic.pdf Exported schematic (human-readable)
│ └── BOM.csv Bill of materials
└── README.md
Full component values and footprints are in
hardware/BOM.csv. Schematic PDF:hardware/schematic.pdf.
Battery tap ──► Resistor divider ──► TLV9004 op-amp ──► TLA2518 ADC ──► ESP32-C6
The op-amp buffers the resistor divider output and keeps the ADC input within the 0–3.3V safe range regardless of load on the divider.
| Signal | ESP32-C6 GPIO |
|---|---|
| TLA2518 CS | 14 |
| TLA2518 SCLK | 21 |
| TLA2518 SDO (MISO) | 22 |
| TLA2518 SDI (MOSI) | 20 |
| DS18B20 OneWire | 2 |
| LED Green | 1 |
| LED Red | 3 |
| OLED SDA / SCL | Default Wire pins |
| Channel | Signal |
|---|---|
| CH0 | Bidirectional current sensor (0A = 1.65V = VREF/2) |
| CH1 | 64V tap — full pack positive terminal |
| CH2 | 48V tap |
| CH3 | 32V tap |
| CH4 | 12V tap |
| CH5–7 | Spare / unconnected |
- Arduino IDE 2.x or arduino-cli
- ESP32 board package by Espressif
Add the ESP32 board package URL in Arduino IDE preferences:
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
Then install: Boards Manager → esp32 by Espressif Systems
Install via Arduino Library Manager:
| Library | Version |
|---|---|
| Adafruit SSD1306 | ≥ 2.5 |
| Adafruit GFX Library | ≥ 1.11 |
OneWire and DallasTemperature are not required. The DS18B20 driver is built into the sketch.
# Compile
arduino-cli compile --fqbn esp32:esp32:esp32c6 src/pbMonitor/pbMonitor.ino
# Upload (replace /dev/ttyUSB0 with your port)
arduino-cli upload --fqbn esp32:esp32:esp32c6 --port /dev/ttyUSB0 src/pbMonitor/pbMonitor.ino- Open
src/pbMonitor/pbMonitor.ino - Select board: Tools → Board → ESP32C6 Dev Module
- Set the calibration constants (see below)
- Click Upload
All calibration constants are grouped at the top of pbMonitor.ino. No other changes are needed.
#define RATIO_64V 19.4f
#define RATIO_48V 14.6f
#define RATIO_32V 9.7f
#define RATIO_12V 3.7fCalculate the ratio from your actual resistor values:
RATIO = (R_top + R_bottom) / R_bottom
Example: 180 kΩ top + 10 kΩ bottom → RATIO = 19.0
Measure the actual battery tap voltage with a multimeter and the corresponding ADC voltage at the op-amp output to verify or fine-tune.
#define CURRENT_SENSITIVITY 0.066f // V/A — adjust for your sensorCommon values:
| Sensor | Sensitivity |
|---|---|
| ACS758-50B | 0.060 V/A |
| ACS712-20A | 0.100 V/A |
| ACS712-5A | 0.185 V/A |
The sensor must output VREF/2 = 1.65V at 0A. Positive current = charging, negative = discharging.
#define PACK_OVERVOLTAGE 58.0f // V (~14.5V/cell × 4)
#define PACK_UNDERVOLTAGE 42.0f // V (~10.5V/cell × 4)
#define PACK_CONNECTED_MIN 5.0f // V below this = pack disconnected, no UV fault
#define OVERCURRENT_A 50.0f // A peak discharge limit
#define OVERTEMP_C 55.0f // °C battery thermal limitFault thresholds include a built-in hysteresis deadband (FAULT_HYSTERESIS_V = 0.5V, FAULT_HYSTERESIS_A = 1A) so fault flags do not oscillate when a measurement hovers near a trip point.
The display alternates between two pages every 2 seconds.
Page 1 — Voltages & Current
┌──────────────────────────────┐
│ pbMonitor v2 [ OK ] │
├──────────────────────────────┤
│ 64V: 54.2V 48V: 48.1V │
│ 32V: 32.1V 12V: 12.8V │
├──────────────────────────────┤
│ I: +12.50 A │
│ ▲ CHARGING │
├──────────────────────────────┤
│ System Normal │
└──────────────────────────────┘
Page 2 — Temperatures
┌──────────────────────────────┐
│ pbMonitor v2 Temps │
├──────────────────────────────┤
│ T1: 32.5C T2: 33.1C │
│ T3: 31.8C T4: 34.0C │
├──────────────────────────────┤
│ Max: 34.0C │
└──────────────────────────────┘
Connect at 115200 baud. One line is printed per ADC cycle (500ms), followed by temperature lines when sensors are present:
64V:54.20 48V:48.10 32V:32.05 12V:12.80 I:+12.50A [CHARGING]
T1: 32.5C
T2: 33.1C
Active faults are appended inline:
64V:59.10 48V:52.30 32V:35.00 12V:13.10 I:+0.20A [IDLE] !!OV
| Code | Condition | Default Trip | Notes |
|---|---|---|---|
| OV | Overvoltage | Pack > 58.0V | |
| UV | Undervoltage | Pack < 42.0V | Suppressed if pack voltage < 5V (disconnected) |
| OC | Overcurrent | |I| > 50A | Absolute value — triggers on charge or discharge |
| OT | Overtemperature | Any sensor > 55°C |
When any fault is active, the red LED turns on and the green LED turns off. The OLED status bar shows the active fault codes (e.g. OV OT). All faults clear automatically once the measured value recovers past the threshold plus the hysteresis deadband.
The main loop is fully non-blocking and timer-driven:
| Task | Interval | Notes |
|---|---|---|
| ADC read + fault check | 500ms | Reads all 5 channels, updates LEDs |
| Temperature conversion | 2000ms | Phase 1: send convert command |
| Temperature read | 2000ms + 750ms | Phase 2: read results after conversion window |
| OLED refresh | 2000ms | Alternates between page 1 and page 2 |
| Watchdog reset | Every loop | 5s hardware WDT reboots on hang |
The DS18B20 conversion window (750ms) runs concurrently with ADC reads and display updates — there is no delay() in the main loop.
The hardware/ directory contains the complete KiCad project for the pbMonitor V2 PCB:
| File | Description |
|---|---|
*.kicad_pro |
KiCad project |
*.kicad_sch |
Schematic source (open in KiCad 7+) |
*.kicad_pcb |
PCB layout source |
schematic.pdf |
Exported schematic for viewing without KiCad |
BOM.csv |
Bill of materials with component values and footprints |
Designed in KiCad 7. Open the
.kicad_profile to load the full project.
MIT License — free to use, modify, and distribute with attribution. See LICENSE for full text.