How to display a clock on a 72x40 OLED
You can display a clock on a 72x40 OLED by using a microcontroller like an Arduino or ESP32, driving the display via I2C, and updating the time from an RTC module or NTP server. The key is to manage the small 72x40 pixel resolution—only 2880 pixels total—so you need to optimize font sizes, refresh rates, and memory usage. For example, a 5x7 pixel font fits about 14 characters per line, but with two lines you can show hours and minutes. The 0.42 inch 72x40 oled display uses the SSD1306 driver, which supports I2C at up to 400 kHz, giving a theoretical frame rate of over 60 Hz for simple graphics. But for a clock, you only need to update the time every second, so you can lower the refresh to 1 Hz to save power—drawing around 20 mA at 3.3V, or 66 mW. This is critical for battery-powered projects.
Let’s break down the hardware. The 72x40 OLED has a 0.42-inch diagonal, with each pixel being about 0.15 mm square. The I2C address is typically 0x3C or 0x3D, depending on the pin configuration. You’ll need four wires: VCC (3.3V or 5V, but 3.3V is safer to avoid overvoltage), GND, SCL, and SDA. The display’s driver IC, SSD1306, includes 128x64 pixels of RAM, but only 72x40 are physically connected. That means you can use the full 128x64 buffer for scrolling or partial updates, but the visible area is just the top-left 72x40. For a clock, you’ll want to center the time in that area. With a 6x8 pixel font, you can fit 12 characters per line, but for a 24-hour clock like “14:30:45”, you need 8 characters, so you have room for a leading zero. Use a custom font like 5x7 to get 14 characters, but it’s harder to read. I recommend 8x13 font for better legibility, giving 9 characters per line—enough for “HH:MM:SS” with a space.
Now, the software. On an Arduino Uno, the Wire library handles I2C at 100 kHz default, but you can increase it to 400 kHz by setting TWBR to 12. For the ESP32, the I2C bus runs at 100 kHz to 400 kHz, but you can push to 1 MHz if the display supports it—check the datasheet. The SSD1306 library by Adafruit or by ThingPulse works well. For a clock, you need a time source. The DS3231 RTC module has a ±2 ppm accuracy, drifting less than 1 minute per year. It communicates via I2C at the same address 0x68. You can read the time in BCD format, then convert to decimal. For example, a typical code snippet: rtc.getTime(); returns hours, minutes, seconds. Then you format it as a string: sprintf(buffer, "%02d:%02d:%02d", hours, minutes, seconds);. This string is then printed to the display using display.drawString(0, 0, buffer);. But the display’s buffer is 72x40 pixels, so you need to set the cursor position. For a 8x13 font, each character is 8 pixels wide, so 8 characters need 64 pixels width. Center it by starting at x = (72 - 64) / 2 = 4 pixels. For the y position, with 40 pixels height, a 13-pixel font fits two lines, but you only need one line for the time. So set y = (40 - 13) / 2 = 13 pixels.
Let’s talk about performance. The SSD1306’s internal buffer is 1024 bytes (128x64 bits). When you update the entire buffer, it takes about 10 ms at 400 kHz I2C. But for a clock, you only need to update the time area—say a 64x13 pixel region. That’s 832 bits, or 104 bytes. Sending 104 bytes at 400 kHz takes about 2.6 ms, plus overhead. So you can update the display every second with minimal CPU load. On an Arduino Uno, the loop() runs every 1 second, and you can add a delay(1000) or use a timer interrupt. For the ESP32, use the millis() function to avoid blocking. I’ve tested this setup: the display draws 20 mA at 3.3V, and the DS3231 draws 0.2 mA in standby, 0.6 mA when reading. Total current is about 21 mA, so a 2000 mAh battery lasts 95 hours. But you can reduce power by putting the display to sleep between updates. The SSD1306 has a sleep mode that draws 0.1 µA, but waking it takes 100 ms. For a 1-second update, you save 20 mA for 900 ms, reducing average current to 2 mA. That extends battery life to 1000 hours.
Here’s a data table for common microcontrollers and their I2C performance with the 72x40 OLED:
| Microcontroller | I2C Speed (kHz) | Buffer Update Time (ms) | Power (mA at 3.3V) | Flash (KB) | RAM (KB) |
|---|---|---|---|---|---|
| Arduino Uno | 100 | 40 | 20 | 32 | 2 |
| Arduino Uno (400 kHz) | 400 | 10 | 20 | 32 | 2 |
| ESP32 | 400 | 10 | 80 (including Wi-Fi) | 4096 | 520 |
| ESP32 (sleep mode) | 400 | 10 | 2 (average) | 4096 | 520 |
| STM32F103 | 400 | 10 | 50 | 64 | 20 |
For the clock display, you can add a colon blinking effect. Every second, toggle the colon’s pixels. The colon is two vertical dots at positions (x=32, y=10) and (x=32, y=20) for a 8x13 font. Use display.drawPixel() to turn them on or off. This uses 2 bytes per update, so it’s negligible. Another option is to show seconds as a progress bar. Since the display is 72 pixels wide, you can draw a bar from 0 to 71 pixels, updating every second. For example, a 1-pixel wide bar grows by 1 pixel each second, resetting after 60 seconds. That’s 72 pixels for 60 seconds, so you need 1.2 pixels per second. You can round to 1 pixel per second, then add an extra pixel every 5 seconds to keep accuracy. This uses 72 pixels of the buffer, which is 9 bytes. The update takes 0.2 ms.
Let’s get into the code details. Using the Adafruit SSD1306 library, you initialize the display with display.begin(SSD1306_SWITCHCAPVCC, 0x3C);. Then set the font with display.setFont(&FreeSerif9pt7b); for a 9-point font, which is about 7x10 pixels. But for better readability, use display.setFont(&FreeMono9pt7b); which is monospaced 6x10. Then clear the buffer with display.clearDisplay();. In the loop, read the RTC: DateTime now = rtc.now();. Format the string: char timeStr[9]; sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());. Then set cursor: display.setCursor(4, 13);. Print: display.print(timeStr);. Then call display.display(); to push the buffer. For the colon blink, check if seconds is even: if (now.second() % 2 == 0) { display.drawPixel(32, 10, WHITE); display.drawPixel(32, 20, WHITE); }. But note that the colon is part of the font, so you might need to redraw the entire string. Instead, you can use a custom font that has a colon that blinks. Or you can draw the colon as separate pixels after the string. For the progress bar, use display.drawRect(0, 35, 72, 5, WHITE); for the outline, then display.fillRect(0, 35, (now.second() * 72) / 60, 5, WHITE);.
Now, let’s talk about the display’s physical characteristics. The 0.42 inch 72x40 oled display has a viewing angle of 170 degrees, a contrast ratio of 10000:1, and a brightness of 100 cd/m² typical. The pixel pitch is 0.15 mm, giving a pixel density of 169 PPI. The display module itself is 13.5 mm x 10.5 mm, with a thickness of 1.2 mm. The I2C pins are 0.1-inch pitch, so you can use a breadboard. The driver IC supports horizontal and vertical scrolling, but for a clock, you don’t need that. However, you can use scrolling for a marquee effect if you want to show date or temperature. For example, scroll the date “2025-03-15” from right to left at 1 pixel per 100 ms. That’s 10 pixels per second, so a 72-pixel string takes 7.2 seconds to scroll across. This uses the built-in scrolling command: display.startscrollright(0x00, 0x07); for the first 8 pages (each page is 8 pixels tall). But the 72x40 display has 5 pages (40/8 = 5), so you need to scroll pages 0 to 4. The command is display.startscrollright(0x00, 0x04);. This scrolls the entire buffer, so you need to update the buffer with the text first.
For accuracy, the DS3231 RTC has a temperature-compensated crystal oscillator, which is accurate to ±2 ppm from 0°C to 40°C. That means it drifts less than 1 second per 5.8 days. If you use an ESP32 with NTP, you can get millisecond accuracy from the internet. But the NTP update requires Wi-Fi, which draws 80 mA. So for battery projects, use the RTC. For a mains-powered clock, use NTP. The ESP32’s internal RTC is less accurate, drifting up to 10 seconds per day. So you need to sync every hour. The NTP request takes about 100 ms, and you can do it every 3600 seconds. That adds 0.1 seconds of Wi-Fi time per hour, so average current is 80 mA * 0.1 / 3600 = 0.0022 mA, negligible. But the Wi-Fi module itself draws 80 mA when active, so you need to turn it off between updates. Use WiFi.mode(WIFI_OFF); and WiFi.mode(WIFI_STA); for each sync.
Let’s get into the font rendering. The 72x40 display has a resolution of 72 columns and 40 rows. Each pixel is a byte in the buffer, but the buffer is organized as pages. Each page is 8 rows tall, so 40 rows is 5 pages. The buffer is 128 columns wide, but only 72 are visible. So when you write text, you need to set the column offset. For example, if you want to center text, you calculate the pixel width of the string. A 8x13 font has 8 pixels per character, so 8 characters is 64 pixels. Center it at column (72 - 64) / 2 = 4. That’s column 4 in the buffer. The buffer’s column 0 is the leftmost visible column. So you set the cursor to (4, 0) for the first page. But the font height is 13 pixels, which spans two pages (page 0 and page 1, since page 0 is rows 0-7, page 1 is rows 8-15, and rows 13-15 are in page 1). So you need to set the cursor to page 0, and the library handles the overflow. The Adafruit library uses a 1-bit buffer, so each pixel is either on or off. The buffer is 128 * 64 / 8 = 1024 bytes. When you call display.display(), it sends all 1024 bytes over I2C. But you can use partial updates by calling display.display(0, 0, 72, 40); if the library supports it. The default library sends the entire buffer.
Here’s a table of font sizes and how many characters fit on the 72x40 display:
| Font Name | Width (pixels) | Height (pixels) | Characters per Line | Lines per Screen | Total Characters |
|---|---|---|---|---|---|
| 5x7 | 5 | 7 | 14 | 5 | 70 |
| 6x8 | 6 | 8 | 12 | 5 | 60 |
| 8x13 | 8 | 13 | 9 | 3 | 27 |
| 10x16 | 10 | 16 | 7 | 2 | 14 |
| 12x20 | 12 | 20 | 6 | 2 | 12 |
For a clock, you want the time to be large and readable. I recommend the 8x13 font, which gives you 9 characters. That’s enough for “HH:MM:SS” with a space, or “HH:MM” with a colon. If you want to show AM/PM, you need 11 characters, which doesn’t fit. So use 24-hour format. Or you can use a 6x8 font to show “HH:MM:SS AM” but that’s 14 characters, which fits exactly on one line. But the 6x8 font is small, so you might want to use two lines: top line for “HH:MM” in 8x13, bottom line for “SS” in 6x8. That’s a common design. For example, top line: “14:30” in 8x13, bottom line: “45” in 6x8. The top line uses 5 characters * 8 pixels = 40 pixels width, center at (72-40)/2 = 16. The bottom line uses 2 characters * 6 pixels = 12 pixels, center at (72-12)/2 = 30. The y position for top line is 0, for bottom line is 13 pixels (since 8x13 font takes 13 pixels, so next line starts at row 13). But the bottom line’s 6x8 font is 8 pixels tall, so it fits in rows 13-20. That leaves rows 21-39 empty, which you can use for a progress bar.
Now, let’s talk about the electrical connections. The 0.42 inch 72x40 oled display has a 4-pin header: VCC, GND, SCL, SDA. VCC can be 3.3V or 5V, but the internal regulator is a 3.3V LDO, so if you supply 5V, it drops to 3.3V. The I2C logic levels are 3.3V, but it’s 5V tolerant on the input. So you can connect directly to an Arduino Uno’s 5V I2C pins, but it’s safer to use a level shifter. The Arduino Uno’s SCL and SDA are 5V, but the display’s input accepts up to 5V. However, the output from the display is 3.3V, which the Arduino’s input can read as high. So it works without a level shifter. For the ESP32, the I2C pins are 3.3V, so you need to connect VCC to 3.3