How to display a battery level on a 1.54 inch 128x64 OLED?
How to Display a Battery Level on a 1.54 inch 128x64 OLED
To display a battery level on a 1.54 inch 128x64 OLED, you need to read the voltage from your battery, map it to a percentage, and then render a graphical battery icon with a fill level on the OLED screen. The core process involves three hardware steps: connecting the OLED via SPI or I2C, setting up a voltage divider for the battery (if using a LiPo cell above 3.3V), and writing firmware that updates the display in real time. For this guide, we’ll assume you’re using a 1.54 inch 128x64 oled display with SPI interface, which gives you faster refresh rates—critical for smooth battery level animations. The display itself has a resolution of 128 pixels horizontally and 64 pixels vertically, each pixel individually addressable. The driver chip is typically an SSD1309 or SH1106, both of which support SPI at clock speeds up to 10 MHz. You’ll also need a microcontroller like an ESP32, STM32, or Arduino Uno. The ESP32 is preferred because it has built-in ADC (analog-to-digital converter) with 12-bit resolution, giving you 4096 steps for voltage measurement. A standard LiPo battery, say a 3.7V 18650 cell, outputs between 3.0V (empty) and 4.2V (full). To read this with a 3.3V microcontroller, you must use a voltage divider—two resistors, like 100kΩ and 47kΩ, to bring the max voltage down to about 3.0V at the ADC pin. That gives you a safe margin. The formula is Vout = Vin * (R2 / (R1 + R2)). With R1 = 100k and R2 = 47k, the divider ratio is 0.32. So at 4.2V, Vout is 1.34V, well within the ESP32’s 3.3V range. You then read the ADC value, convert it back to the actual battery voltage, and map it to a percentage using a lookup table or a linear approximation. But linear mapping isn’t accurate for LiPo batteries because the discharge curve is not linear. Instead, use a piecewise linear model with at least five points: 4.2V = 100%, 4.0V = 80%, 3.8V = 50%, 3.6V = 20%, 3.0V = 0%. You can interpolate between these points in your code. For the OLED graphics, you’ll draw a battery outline—a rectangle with a small tab on the right. The outline dimensions can be 40 pixels wide and 20 pixels tall, centered on the 128x64 screen. The fill level is a smaller rectangle inside, whose width changes based on the battery percentage. For example, if the battery is at 50%, the fill rectangle width is 20 pixels (half of 40). You also need to handle the case where the battery voltage drops below 3.0V—display a low battery warning, like a blinking icon or a red outline. The refresh rate should be around 10 Hz to avoid flicker. Using SPI, you can send a full frame buffer (128 * 64 / 8 = 1024 bytes) in about 1 millisecond at 10 MHz, so the display update is nearly instant. Power consumption is another factor. The OLED itself draws about 20 mA when all pixels are on, but with a battery icon that only lights up a small portion, it’s closer to 5-10 mA. If you’re running off the same battery, this matters. You can reduce power by putting the OLED into sleep mode between updates, using the SSD1309’s display off command (0xAE). Wake it up with 0xAF, update the frame, then go back to sleep. This can cut average current to under 1 mA if you update every second. For the voltage reading, use the ADC with oversampling and averaging—take 16 samples and average them to reduce noise. The ESP32’s ADC is known to be nonlinear near the extremes, so calibrate it with a known voltage using a multimeter. Store the calibration offset in EEPROM or flash. If you’re using an Arduino Uno, its ADC is 10-bit (1024 steps), which gives you about 4.9 mV resolution. That’s fine for battery monitoring because a 0.1V change corresponds to roughly 8% capacity in the linear region. But the Uno runs at 5V, so you’ll need a different voltage divider ratio. For a 4.2V max battery, use R1 = 100k and R2 = 100k to get Vout = 2.1V at 4.2V, which is safe for the 5V ADC. The math is the same. Now, let’s talk about the graphical design. A simple battery icon has a rectangular body and a small tab on the right. On a 128x64 display, you have room to show additional info: voltage, percentage as text, and a small graph of recent readings. For the text, use a 5x7 pixel font. You can fit about 25 characters per line. Display the percentage as a number, like “72%”, and the voltage as “3.85V”. Use the OLED’s built-in font or generate your own bitmap. The font data for ASCII characters takes about 95 bytes per character, but you can store a subset. For the battery level bar, use a filled rectangle with a color that changes based on level: green for >60%, yellow for 20-60%, red for <20%. This is easy with the SSD1309 because it supports monochrome only, but you can simulate colors by using different patterns—hatching for yellow, solid for green, and blinking for red. The blink rate should be around 500 ms on, 500 ms off. To implement this, you need a timer interrupt that toggles a flag. In your main loop, check the flag and either draw the fill or skip it. For the voltage reading, use a moving average filter with a window of 10 samples to smooth out sudden drops caused by load changes. For example, when a motor starts, the battery voltage might dip 0.2V temporarily. The moving average prevents the display from jumping around. You can also add a low-pass filter in software: new_value = 0.9 * old_value + 0.1 * raw_value. This gives a time constant of about 10 samples at 100 ms intervals. If you’re using an ESP32, you can also use the hall effect sensor or touch pins for additional features, but that’s overkill. For the OLED initialization, the sequence for SSD1309 over SPI is: send 0xAE (display off), 0xD5 (set display clock divide ratio), 0x80 (default), 0xA8 (set multiplex ratio), 0x3F (for 64 rows), 0xD3 (set display offset), 0x00, 0x40 (set start line), 0x8D (charge pump), 0x14 (enable), 0x20 (set memory addressing mode), 0x00 (horizontal), 0xA1 (set segment remap), 0xC8 (set COM output scan direction), 0xDA (set COM pins), 0x12, 0x81 (set contrast), 0xCF (medium contrast), 0xD9 (set pre-charge period), 0xF1, 0xDB (set VCOMH deselect level), 0x40, 0xA4 (display on resume), 0xA6 (normal display), 0xAF (display on). This is standard and works with most libraries. If you’re using the Adafruit SSD1306 library, it handles this automatically. But for a custom implementation, you need to send these commands via SPI. The SPI pins are: CS (chip select), DC (data/command), RES (reset), SCLK (clock), MOSI (data). On an ESP32, typical pins are: CS = 5, DC = 17, RES = 16, SCLK = 18, MOSI = 23. You can change these as long as they don’t conflict with other peripherals. For the battery voltage measurement, use ADC1 channel 0 (GPIO 36) on the ESP32. The ADC has 12-bit resolution, so the raw value ranges from 0 to 4095. The reference voltage is 1.1V internally, but the ADC scales the input based on attenuation. Set the attenuation to 11 dB to measure up to 3.3V. The formula for actual voltage is: V_actual = (raw_value / 4095.0) * 3.3. Then multiply by the voltage divider ratio (1 / 0.32 = 3.125) to get the battery voltage. So V_battery = V_actual * 3.125. But this is approximate. Use a multimeter to measure the actual battery voltage and adjust the ratio in code. For example, if the multimeter shows 3.85V and your calculated value is 3.80V, multiply by a correction factor of 3.85 / 3.80 = 1.013. Store this in EEPROM. Now, for the battery level mapping, use a lookup table with voltage thresholds. Here’s a table based on typical LiPo discharge curves at 25°C and 0.5C discharge rate:
Battery Voltage (V) - Percentage (%)
4.20 - 100
4.10 - 90
4.00 - 80
3.90 - 65
3.80 - 50
3.70 - 35
3.60 - 20
3.50 - 10
3.40 - 5
3.30 - 2
3.00 - 0
Interpolate linearly between these points. For example, if the voltage is 3.85V, it’s between 3.80V (50%) and 3.90V (65%). The percentage is 50 + (3.85 - 3.80) / (3.90 - 3.80) * (65 - 50) = 50 + 0.5 * 15 = 57.5%. Round to 58%. This is more accurate than a linear map from 3.0V to 4.2V. You can also use a polynomial fit, but the lookup table is simpler and faster. For the OLED display, the battery icon should be drawn at a fixed location, say (x=44, y=22) for a 40x20 icon. The outline is a rectangle from (44,22) to (84,42). The tab is a small rectangle from (84,27) to (88,37). The fill is a rectangle from (46,24) to (46 + fill_width, 40), where fill_width = (percentage / 100) * 36. The 36 comes from the inner width (40 pixels minus 2 pixels border on each side). So at 50%, fill_width = 18 pixels. For the text, use coordinates (10, 10) for the voltage and (10, 50) for the percentage. The font is 5x7, so each character is 5 pixels wide and 7 pixels tall. You can use a library like Adafruit_GFX to print text. But if you’re writing your own, you need a font array. For example, the character ‘0’ is defined as: 0x3E, 0x51, 0x49, 0x45, 0x3E. That’s 5 bytes per character. For a full ASCII set, you need 95 characters * 5 bytes = 475 bytes, plus the bitmap for each row. The SSD1309’s memory is organized in pages of 8 rows. So a 128x64 display has 8 pages (0 to 7) and 128 columns. To draw a pixel, you set the corresponding bit in the page. For the battery fill, you need to set multiple bits in a rectangle. Use a loop to set the bits in the fill area. For example, for the fill rectangle from (46,24) to (82,40), the rows are from page 3 (rows 24-31) and page 4 (rows 32-39). For each column from 46 to 82, set the bits in page 3 for rows 24-31 and page 4 for rows 32-39. This is straightforward but requires careful bit manipulation. If you’re using a library, it handles this. But if you want low-level control, you can write a function that takes a rectangle and fills it. The refresh rate should be at least 10 Hz to avoid visible flicker. The human eye can perceive flicker up to about 60 Hz, but for static icons, 10 Hz is fine. For the blinking low battery warning, use a timer that toggles a flag every 500 ms. In the main loop, check the flag and draw the icon with or without the fill. The blink pattern can be: on for 500 ms, off for 500 ms. This is standard for warning indicators. For the voltage reading, use a digital filter to remove noise. The ADC on the ESP32 is noisy, especially when Wi-Fi is enabled. To mitigate this, take 64 samples and average them. This reduces noise by a factor of 8 (sqrt(64)). Also, use a median filter to remove outliers. For example, take 5 samples, sort them, and take the middle value. Then average the last 10 median values. This gives a stable reading. The update interval should be 100 ms to 1 second. If you update too fast, the display will flicker and consume more power. If you update too slow, the user won’t see real-time changes. A 500 ms update interval is a good compromise. For the OLED, you can use the SSD1309’s “display on” and “display off” commands to save power. When the display is off, it draws about 1 µA. When on, it draws 10-20 mA. So if you only update every second and turn off the display between updates, you can save significant power. But for a battery level display, you usually want it always on. In that case, use the contrast control to reduce brightness. The default contrast is 0xCF (207). You can reduce it to 0x10 (16) for low power, which still shows the icon clearly. The trade-off is visibility in bright light. For outdoor use, you might need higher contrast. Another option is to use the OLED’s “inverse display” mode to invert the colors, which can be easier to read in some conditions. For the battery level, you can also add a numerical value in the center of the icon. For example, draw the number “58” inside the fill area. This requires a small font, like 3x5 pixels. But with a 40x20 icon, you have limited space. A better approach is to show the percentage as text below the icon. This gives a clear, unambiguous reading. For the voltage, display it as “3.85V” with two decimal places. This is useful for debugging and for users who want precise data. The code structure should be: initialize OLED, initialize ADC, set up a timer for the blink, then in the main loop: read ADC, convert to voltage, map to percentage, draw battery icon with fill, draw text, delay 500 ms. Use non-blocking delays with millis() to avoid freezing the loop. For example, check if 500 ms have passed since the last update. If yes, update. This allows other tasks to run, like reading a button or sending data over serial. If you’re using an ESP32, you can also send the battery data over Wi-Fi to a dashboard. But that’s beyond the scope of this guide. For the hardware, make sure the voltage divider resistors are 1% tolerance for accuracy. The capacitors on the battery input can also help filter noise. A 10 µF capacitor between the battery positive and ground reduces ripple. For the OLED, use a 10 µF capacitor between VCC and GND to decouple the power supply. This prevents the OLED from resetting when the battery voltage dips. The SPI wiring should be as short as possible to avoid signal degradation. Use a ground plane if you’re designing a PCB. For a breadboard, keep wires under 10 cm. The OLED’s logic level is 3.3V, so if you’re using a 5V microcontroller like Arduino Uno, you need a level shifter for the SPI lines. The MOSI and SCLK lines can be driven with a voltage divider (e.g., 1kΩ and 2kΩ) to drop 5V to 3.3V. The CS and DC lines can also use dividers. The RES line can be connected directly to a 3.3V pin if you don’t need reset. Alternatively, use a 74LVC245 level shifter. For the ADC, the ESP32’s input is 3.3V tolerant, so no level shifting is needed. But if you’re using a 5V Arduino, the ADC input is 5V tolerant, so the voltage divider output must be below 5V. With a 4.2V battery and 100k/100k divider, Vout is 2.1V, safe. For the battery connector, use a JST PH 2.0 connector for LiPo packs. This is standard. The ground connection should be thick wire to handle current. The battery positive goes through the voltage divider to the ADC pin. The divider’s output also goes to the ADC. The divider’s ground goes to the common ground. For the OLED, the SPI pins are: CS to GPIO5, DC to GPIO17, RES to GPIO16, SCLK to GPIO18, MOSI to GPIO23, VCC to 3.3V, GND to GND. On an Arduino Uno, the SPI pins are: CS to pin 10, DC to pin 9, RES to pin 8, SCLK to pin 13, MOSI to pin 11, VCC to 5V (if using a level shifter) or 3.3V (if using a 3.3V regulator). The Uno’s 3.3V pin can supply up to 150 mA, enough for the OLED. But if