Skip to content
Creative Capitalism · Field Notes

How to display a battery level on a 0.96 inch 128x64 OLED?

aBy admin

To display a battery level on a 0.96 inch 128x64 OLED, you need to interface the display with a microcontroller (like an Arduino, ESP32, or STM32) via SPI or I2C, read the battery voltage using an ADC pin, map that voltage to a percentage, and then draw a battery icon and fill it proportionally on the OLED screen. The 0.96 inch 128x64 spi i2c oled display is a common choice because it offers a 128x64 pixel resolution, supports both SPI and I2C protocols, and uses the SSD1306 driver chip, which is well-documented and has mature libraries. The key is to handle voltage division, ADC resolution, and the OLED’s buffer memory properly to get a smooth, real-time battery indicator.

Hardware Setup and Voltage Reading

First, you need to connect the OLED to your microcontroller. For SPI, you use MOSI, SCK, CS, DC, and RST pins. For I2C, you only need SDA and SCL, plus a pull-up resistor (typically 4.7kΩ on each line). The SSD1306 driver operates at 3.3V logic, but most 5V microcontrollers have 3.3V tolerant pins or you can use a level shifter. The OLED draws about 20mA when all pixels are on, so a 3.3V regulator with at least 100mA capacity is recommended. To read the battery voltage, you need a voltage divider if the battery voltage exceeds the microcontroller’s ADC reference voltage (usually 3.3V or 5V). For a single-cell LiPo battery (3.7V nominal, 4.2V fully charged), use two resistors: say 100kΩ and 47kΩ. This gives a voltage divider ratio of 47/(100+47) ≈ 0.32. When the battery is at 4.2V, the ADC pin sees 4.2 * 0.32 ≈ 1.34V, which is safe for a 3.3V ADC. For a 12V lead-acid battery, use a 10kΩ and 3.3kΩ divider (ratio 0.248) to bring 12V down to 2.98V. The ADC resolution on an Arduino Uno (10-bit) gives 1024 steps from 0 to 5V, so each step is about 4.88mV. With a 0.32 divider, the battery voltage resolution is 4.88mV / 0.32 ≈ 15.25mV per step. That’s fine for a battery gauge, but for higher precision, use a 12-bit ADC (ESP32) which gives 4096 steps, or an external ADC like the ADS1115 (16-bit).

Mapping ADC Readings to Battery Percentage

Battery voltage is not linear with capacity, especially for Li-ion and LiPo chemistries. The discharge curve has a flat region between 3.7V and 3.4V, then drops sharply below 3.3V. A simple linear mapping from voltage to percentage will be inaccurate. Instead, use a lookup table with at least 10 points. For a typical LiPo cell, the table looks like this:

Voltage (V)Percentage (%)
4.20100
4.1090
4.0080
3.9070
3.8060
3.7050
3.6040
3.5030
3.4020
3.3010
3.205
3.000

Interpolate between these points in your code. For example, if the ADC returns 3.75V, the percentage is between 50% and 60%. Linear interpolation gives 50 + (3.75-3.70)/(3.80-3.70) * 10 = 55%. For a 12V lead-acid battery, the curve is different: 12.6V is 100%, 12.0V is 50%, 11.5V is 0%. Use a similar table with 0.1V steps. The ADC reading also needs averaging to filter noise. Take 10 to 20 samples and average them, or use a moving average filter with a window of 5 samples. This prevents the battery level from jumping around on the OLED.

Drawing the Battery Icon on the OLED

The OLED has a 128x64 pixel grid. The SSD1306 driver uses a page-addressing mode where the display is divided into 8 pages of 8 pixels each (since each byte represents 8 vertical pixels). To draw a battery icon, you allocate a small area in the top-right corner, say from x=80 to x=127 and y=0 to y=15. The icon consists of a rectangle (the battery body) and a small terminal on the right. The body is 30 pixels wide and 12 pixels tall, with a 2-pixel border. The terminal is 4 pixels wide and 6 pixels tall, attached to the right side. To fill the battery based on percentage, calculate the fill width: fillWidth = (percentage / 100) * (bodyWidth - 2*border). For example, at 55%, fillWidth = 0.55 * 26 = 14.3 pixels, rounded to 14 pixels. Then draw a filled rectangle from x=82 to x=82+14, y=2 to y=13. Use the Adafruit_SSD1306 library or the u8g2 library. The Adafruit library uses a buffer of 1024 bytes (128*64/8). You call display.clearDisplay(), then draw the battery outline, then draw the fill, then display.display(). The u8g2 library is faster and supports more fonts, but the buffer management is similar. For a 128x64 display, the buffer size is 1KB, which fits in the RAM of most microcontrollers. The drawing speed is about 10-20 frames per second for simple shapes, which is plenty for a battery indicator.

Handling Different Battery Chemistries and Cell Counts

If you are using multiple cells in series, the voltage divider ratio must be adjusted. For a 2S LiPo (8.4V max), use a divider like 100kΩ and 22kΩ (ratio 0.18) to bring 8.4V down to 1.51V. For 3S (12.6V), use 100kΩ and 15kΩ (ratio 0.13). The lookup table must be scaled accordingly. For a 6-cell NiMH pack (7.2V nominal, 8.4V peak), the voltage range is 6.0V to 8.4V, and the discharge curve is flatter. Use a 10-point table from 8.4V to 6.0V. For alkaline batteries in series (e.g., 4x AA = 6V), the voltage drops from 6.0V to 4.0V, and the curve is more linear. You can use a simple linear mapping: percentage = (voltage - 4.0) / (6.0 - 4.0) * 100, but cap it at 100% and 0%. The OLED’s power consumption is also a factor. If the battery is powering the OLED and the microcontroller, the system’s own current draw will affect the battery voltage reading. For example, an ESP32 drawing 80mA and the OLED drawing 20mA will cause a voltage drop of about 0.1V on a 100mΩ battery internal resistance. This drop is negligible for most applications, but for high-current devices, measure the voltage when the system is in sleep mode (low current) to get a more accurate reading. Use a MOSFET to switch off the OLED when not in use, reducing power consumption to near zero.

Code Implementation Details

Here is a practical code snippet for an Arduino Uno with the OLED on I2C (address 0x3C). The ADC reads analog pin A0, which is connected to the voltage divider output. The code uses the Adafruit_SSD1306 library and the voltage lookup table. First, include the libraries: Wire.h, Adafruit_GFX.h, Adafruit_SSD1306.h. Define the display object with width 128, height 64, and reset pin -1 (for I2C). In setup(), initialize the display with SSD1306_SWITCHCAPVCC and display.begin(SSD1306_SWITCHCAPVCC, 0x3C). Then clear the display. In loop(), read the analog pin 10 times and average. Convert the ADC value to voltage: voltage = (averageADC * 5.0) / 1024.0 / voltageDividerRatio. Then use the lookup table to find the percentage. For interpolation, use a for loop to find the two closest voltages in the table. Then draw the battery icon: use display.drawRect() for the outline, display.fillRect() for the fill, and display.drawRect() for the terminal. Use display.setTextSize(1) and display.setCursor() to print the percentage number next to the icon. Call display.display() to update the screen. Add a delay of 1000ms to avoid flickering. The code size is about 8KB, and the RAM usage is about 1.5KB (including the display buffer). For an ESP32, use the same library but with different pin definitions for I2C (SDA=21, SCL=22). The ADC on ESP32 is 12-bit, so the voltage calculation changes: voltage = (averageADC * 3.3) / 4095.0 / voltageDividerRatio. The ESP32 also has two ADCs, each with 18 channels, so you can monitor multiple batteries. The OLED refresh rate can be increased to 30 FPS by using the u8g2 library with hardware acceleration, but for a battery indicator, 1 FPS is sufficient.

Optimizing the Display for Readability

The 0.96 inch OLED has a small physical size (about 2.5cm x 1.5cm), so the battery icon should be large enough to see. A 40x20 pixel icon is about 1/3 of the screen width, which is good. Use a 2-pixel border for the outline, and a 1-pixel gap between the fill and the border. The terminal should be 6x8 pixels. For the percentage text, use a 5x7 font (default in Adafruit library) at size 1, which gives 6x8 pixels per character. Place the text to the left of the icon, at x=70, y=4. If you want to show the voltage as well, use a second line at y=16. The OLED’s contrast can be adjusted with display.ssd1306_command(SSD1306_SETCONTRAST) and a value from 0 to 255. A value of 128 is good for indoor use, 200 for outdoor. The OLED’s viewing angle is 160 degrees, so it’s readable from the side. The refresh rate of the SSD1306 is about 100Hz for the internal driver, but the library limits it to the SPI/I2C speed. I2C at 400kHz can update the full screen in about 10ms, so you can update the battery icon every 100ms without issues. For SPI, using 8MHz clock, the update time is about 2ms. This allows for smooth animations, like a charging animation where the battery fill pulses.

Dealing with Noise and Accuracy

ADC readings are noisy due to digital switching on the microcontroller. To reduce noise, use a 100nF capacitor between the ADC pin and ground. Also, use a 1kΩ resistor in series with the ADC pin to limit current. Take multiple readings and discard the highest and lowest before averaging. For example, take 15 readings, sort them, discard the top 2 and bottom 2, then average the remaining 11. This gives a robust estimate. The voltage divider resistors should have 1% tolerance or better to ensure accuracy. A 5% resistor can cause a 5% error in voltage reading, which translates to a 10% error in battery percentage at the flat region. Use a precision reference voltage for the ADC, like the internal 1.1V reference on the ATmega328P, which is more stable than the VCC reference. Change the analogReference() to INTERNAL for 1.1V, and adjust the voltage divider ratio to bring the battery voltage down to 1.1V max. For a 4.2V battery, use a divider ratio of 1.1/4.2 ≈ 0.262, so resistors like 100kΩ and 35.7kΩ (or 100kΩ and 36kΩ). This gives better resolution because the ADC uses the full 1.1V range instead of 5V. The step size becomes 1.1V/1024 ≈ 1.07mV, which with the divider gives 1.07mV/0.262 ≈ 4.1mV per step. That’s 4 times better than the 5V reference. For the ESP32, use the internal 2.5V reference (if available) or an external ADS1115 with 16-bit resolution for 0.1% accuracy.

Charging State Detection

If the battery is being charged, the voltage will be higher than the nominal max. For a LiPo, the charger applies 4.2V constant voltage until the current drops to 0.1C. During charging, the battery voltage will be at 4.2V even if the battery is only 80% full. To detect charging, you can monitor the current with a shunt resistor (e.g., 0.1Ω) and an op-amp. Or, if the charger has a status pin (like the TP4056’s CHRG pin), connect it to a digital input. When CHRG is low, the battery is charging. In your code, if charging is detected, show a charging animation: a lightning bolt symbol or a pulsing fill. The lightning bolt can be drawn with a few lines using display.drawLine() and display.drawPixel(). Or, use a small bitmap of 16x16 pixels stored in PROGMEM. The charging animation can be a simple blink of the fill every 500ms. When the battery is fully charged, the CHRG pin goes high, and the STDBY pin goes low. You can use both pins to show “FULL” on the OLED. For a 0.96 inch OLED, you can display the text “CHG” or “FULL” in the center of the screen, using a larger font like the 8x13 font from the u8g2 library. The font data is stored in flash memory, so it doesn’t consume RAM.

Low Power Considerations

If the device is battery-powered, the OLED itself consumes about 20mA, which is significant for a small battery. You can reduce power by turning off the OLED when not in use. Use the display.ssd1306_command(SSD1306_DISPLAYOFF) command to put the display into sleep mode, drawing only 10µA. Then wake it up with SSD1306_DISPLAYON. You can also reduce the brightness by setting the contrast to a low value, like 10, which reduces current to about 5mA. Another trick is to update the display only when the battery percentage changes by more than 1%. This reduces the number of SPI/I2C transactions. For example, if the battery is stable, the display updates once per minute. If the battery is discharging quickly (like under load), update every second. The microcontroller can also go into sleep mode between readings. For an Arduino Uno, use the LowPower library to put the MCU into sleep mode for 1 second, waking up to take a reading and update the display. This extends battery life from a few hours to several days. For an ESP32, use deep sleep with a timer wake-up, consuming only 10µA in sleep mode. The OLED can be powered via a GPIO pin, so you can turn it off completely during sleep. Use a MOSFET to switch the OLED’s VCC, controlled by a digital pin. In sleep mode, set the pin low to cut power to the OLED, saving the 20mA. When waking, set the pin high, wait 10ms for the OLED to initialize, then update the display.

Testing and Calibration

To calibrate the battery level, use a known good battery and a multimeter. Measure the voltage at the battery terminals while the system is running. Compare the voltage read by the ADC with the multimeter reading. Adjust the voltage divider ratio in the code to match. For example, if the multimeter shows 3.80V but the ADC reads 3.75V, the divider ratio is off by 1.3%. Correct the ratio by multiplying by 3.80/3.75 = 1.0133. Then test at different charge levels: full, 50%, and empty. Use a battery analyzer or a simple resistor load to discharge the battery at a constant current (e.g., 100mA) and record the voltage and percentage every 10 minutes. Plot the curve and adjust the lookup table accordingly. The OLED’s pixel response time is about 30ms, so there is no visible lag. The display’s temperature range is -40°C to +85°C, so it works in most environments. The SSD1306 driver has a built-in charge pump for the OLED voltage (7V to 15V), so no

§ § §
a
About the author admin is part of the 23-person investment team at Creative Capitalism. The firm reviews 11,400+ founder intro calls a year out of its Brooklyn office at 55 Washington Street.
Fund III · Open for Series A–C

If this thesis resonates with what you're building, we'd like to hear from you.