How to set up a 0.96 inch 128x64 OLED with MicroPython?
How to Set Up a 0.96 Inch 128x64 OLED with MicroPython
To set up a 0.96 inch 128x64 OLED with MicroPython, you need to connect the display to your microcontroller via I2C or SPI, flash the correct firmware, and write a driver script. The most common approach is using I2C because it only requires two wires (SDA and SCL) plus power, which simplifies wiring. For example, with an ESP32 or Raspberry Pi Pico, you connect VCC to 3.3V, GND to ground, SCL to GPIO 22 (on ESP32) or GPIO 1 (on Pico), and SDA to GPIO 21 (ESP32) or GPIO 0 (Pico). If you’re using SPI, you’ll need four wires: CS, DC, MOSI, and SCK, plus a reset pin. The display itself is a monochrome OLED with a resolution of 128x64 pixels, driven by the SSD1306 controller, which is widely supported in MicroPython libraries. For a reliable hardware reference, check the 0.96 inch 128x64 spi i2c oled display—it’s a solid choice for prototyping because it works with both interfaces and has a 1.27mm pin pitch.
Hardware Requirements and Wiring Details
Before diving into code, let’s nail down the hardware. You’ll need a microcontroller board (like ESP32, Raspberry Pi Pico, or STM32), a 0.96 inch OLED with SSD1306, and jumper wires. The OLED operates at 3.3V logic, but some boards tolerate 5V on the I2C lines—check your datasheet. For I2C, the default address is 0x3C or 0x3D, depending on the manufacturer. On the Pico, the I2C0 pins are GPIO 0 (SDA) and GPIO 1 (SCL), while I2C1 uses GPIO 2 (SDA) and GPIO 3 (SCL). For ESP32, I2C0 typically uses GPIO 21 (SDA) and GPIO 22 (SCL), but you can reassign them in software. Here’s a quick wiring table for I2C:
I2C Wiring Table
| OLED Pin | ESP32 Pin | Raspberry Pi Pico Pin |
|----------|-----------|-----------------------|
| VCC | 3.3V | 3.3V (Pin 36) |
| GND | GND | GND (Pin 38) |
| SCL | GPIO 22 | GPIO 1 (Pin 2) |
| SDA | GPIO 21 | GPIO 0 (Pin 1) |
For SPI, the pinout is more complex. You need CS (Chip Select), DC (Data/Command), MOSI (Master Out Slave In), and SCK (Serial Clock). On the Pico, you can use SPI0 with GPIO 16 (CS), GPIO 17 (DC), GPIO 19 (MOSI), and GPIO 18 (SCK). The OLED’s reset pin is optional but recommended—connect it to a GPIO like GPIO 20. The SPI frequency can go up to 10 MHz, but 4 MHz is stable for most cases. The display’s power consumption is around 20 mA when fully lit, so it’s fine for battery projects if you use sleep modes.
Flashing MicroPython Firmware
You need MicroPython firmware that includes the SSD1306 driver. Most official builds from micropython.org include it, but if you’re using a custom board, compile it with the “micropython-ssd1306” module. For the Raspberry Pi Pico, download the UF2 file from the official site, hold the BOOTSEL button, plug it in, and drag the file to the RPI-RP2 drive. For ESP32, use esptool.py to erase and flash: “esptool.py --chip esp32 --port /dev/ttyUSB0 erase_flash” followed by “esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash -z 0x1000 firmware.bin”. The firmware size is about 1.5 MB for ESP32, and the Pico’s firmware is around 1 MB. After flashing, connect via a serial terminal (like PuTTY or Thonny) at 115200 baud to verify the REPL works.
Writing the MicroPython Code
The core driver is the “ssd1306.py” file from the MicroPython repository. You can copy it to your board using a tool like ampy or rshell. For I2C, the initialization is straightforward:
```python
from machine import Pin, I2C
import ssd1306
i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
```
For SPI, you need to define the pins and create an SPI object:
```python
from machine import Pin, SPI
import ssd1306
spi = SPI(0, baudrate=4000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(19))
dc = Pin(17, Pin.OUT)
cs = Pin(16, Pin.OUT)
rst = Pin(20, Pin.OUT)
oled = ssd1306.SSD1306_SPI(128, 64, spi, dc, cs, rst)
```
The I2C frequency is set to 400 kHz (fast mode), but 100 kHz also works. SPI baudrate can go up to 10 MHz, but 4 MHz is a safe starting point. The OLED buffer is 1024 bytes (128x64 bits, so 128*64/8 = 1024). The driver stores the frame buffer in RAM, which uses about 1 KB on the microcontroller. For complex graphics, you can use the “framebuf” module to draw shapes, text, and images.
Drawing Text and Graphics
Once the OLED object is created, you can use methods like “oled.text()”, “oled.pixel()”, “oled.line()”, “oled.rect()”, and “oled.fill_rect()”. The default font is 8x8 pixels, so you can display 16 characters per row (128/8 = 16) and 8 rows (64/8 = 8). To display “Hello World” at the top-left corner:
```python
oled.fill(0)
oled.text("Hello World", 0, 0, 1)
oled.show()
```
The “fill(0)” clears the screen (0 = off, 1 = on). The “text()” method takes the string, x position, y position, and color (1 = white, 0 = black). The “show()” method transfers the buffer to the display. You can also invert the display with “oled.invert(True)” or set contrast with “oled.contrast(128)” (range 0 to 255). For scrolling, use “oled.scroll(dx, dy)” where dx and dy are pixel offsets. The display refresh rate is about 60 Hz when using I2C, but SPI can push 100 Hz for simple updates.
Performance Benchmarks
Let’s get into the numbers. With I2C at 400 kHz, a full screen update (1024 bytes) takes about 20 ms, giving you 50 FPS. With SPI at 4 MHz, the same update takes 2 ms, yielding 500 FPS theoretically, but the microcontroller’s overhead limits it to around 200 FPS. For text updates, I2C can update a single row in 1.5 ms, while SPI does it in 0.15 ms. The frame buffer is stored in RAM, so if you’re on a Pico with 264 KB, you have plenty of space. On an ESP32 with 520 KB, you can even double-buffer for smooth animations. Power consumption: the display draws 20 mA when all pixels are on, but with typical usage (text and simple graphics), it’s around 10 mA. In sleep mode, the SSD1306 can be powered down by pulling the reset pin low, reducing current to 1 µA.
Common Pitfalls and Fixes
A frequent issue is the I2C address being wrong. Use “i2c.scan()” to find the address—it returns a list like [60] (0x3C) or [61] (0x3D). If the scan returns nothing, check wiring and pull-up resistors. The OLED module usually has built-in 4.7kΩ pull-ups on SDA and SCL, but if you’re using long wires (over 10 cm), add external 2.2kΩ resistors. Another problem is the display staying blank after “oled.show()”. This often happens because the buffer is not initialized—always call “oled.fill(0)” first. For SPI, ensure the CS pin is low during communication and that the DC pin is set correctly (0 for command, 1 for data). Some clone displays have a different pinout—verify with a multimeter that VCC is 3.3V and not 5V, as 5V can damage the SSD1306.
Advanced Features: Bitmaps and Fonts
You can display custom bitmaps by converting images to byte arrays. For a 128x64 monochrome bitmap, you need 1024 bytes. Use a tool like “img2bytearray” or “LCD Image Converter” to generate the data. Then load it into the frame buffer:
```python
bitmap = bytearray([0xFF, 0x00, ...]) # 1024 bytes
oled.blit(bitmap, 0, 0, 128, 64)
oled.show()
```
For custom fonts, use the “writer” library from Peter Hinch (available on GitHub). It supports proportional fonts and can store them in flash to save RAM. A 12-point font uses about 2 KB, while a 16-point font uses 4 KB. The library handles character spacing and line wrapping automatically. You can also use the “framebuf” module to draw circles, ellipses, and polygons with “framebuf.circle()” and “framebuf.poly()”.
Interfacing with Sensors
A common use case is displaying sensor data. For example, reading a DHT22 temperature and humidity sensor and showing it on the OLED:
```python
import dht
from machine import Pin
dht22 = dht.DHT22(Pin(4))
while True:
dht22.measure()
temp = dht22.temperature()
hum = dht22.humidity()
oled.fill(0)
oled.text("Temp: {}C".format(temp), 0, 0, 1)
oled.text("Hum: {}%".format(hum), 0, 16, 1)
oled.show()
time.sleep(2)
```
The DHT22 reading takes 250 ms, so the OLED update rate is about 0.5 Hz. For faster sensors like an MPU6050 accelerometer, you can update at 50 Hz. The I2C bus can handle multiple devices—just use different addresses. The MPU6050 uses address 0x68, so you can share the same SDA and SCL lines as the OLED.
Power Optimization for Battery Projects
If you’re running on a battery, you need to minimize power draw. The OLED’s SSD1306 has a sleep mode: send command 0xAE to turn off the display, and 0xAF to turn it on. You can also reduce the display’s contrast to 0x00 to save power, but it will be barely visible. The microcontroller’s deep sleep mode is key: on the ESP32, you can wake up from a timer every 10 seconds, read a sensor, update the OLED, and then go back to sleep. The current during deep sleep is 5 µA for the ESP32, plus 1 µA for the OLED in sleep mode, totaling 6 µA. With a 2000 mAh battery, you get over 300,000 hours of standby—but that’s theoretical, as the wake-up cycles consume more. A practical setup with a 10-second update interval drains about 2 mA average, giving 1000 hours of runtime.
Debugging with the REPL
When things go wrong, use the MicroPython REPL to test step by step. First, check the I2C bus: “i2c = I2C(0, scl=Pin(1), sda=Pin(0))” then “i2c.scan()”. If it returns an empty list, your wiring is off. For SPI, verify the pins with “spi = SPI(0)” and check the frequency. You can also read the OLED’s status register: send command 0x00 and read back a byte—it should return 0x08 for the SSD1306. If you get a timeout error, reduce the I2C frequency to 100 kHz. Another trick: use a logic analyzer to capture the I2C or SPI signals. The Saleae Logic software is free for up to 1 MHz, and it shows you exactly what’s being sent. For example, the initialization sequence for the SSD1306 includes commands like 0xAE (display off), 0xD5 (display clock divide), 0x80 (default), 0xA8 (multiplex ratio), 0x3F (64 rows), 0xD3 (display offset), 0x00, 0x40 (start line), 0x8D (charge pump), 0x14 (enable), 0x20 (memory addressing mode), 0x00 (horizontal), 0xA1 (segment remap), 0xC8 (COM scan direction), 0xDA (COM pins), 0x12, 0x81 (contrast), 0xCF, 0xD9 (pre-charge period), 0xF1, 0xDB (VCOMH deselect), 0x40, 0xA4 (display on resume), 0xA6 (normal display), 0x2E (deactivate scroll), and 0xAF (display on). If any of these are missing, the display won’t work.
Real-World Project Example
Let’s build a weather station with an ESP32, a BME280 sensor, and the OLED. The BME280 measures temperature, humidity, and pressure over I2C. The code reads the sensor every 5 seconds and updates the display. The OLED shows three lines of text: “Temp: 25.3C”, “Hum: 60%”, “Press: 1013 hPa”. The BME280’s I2C address is 0x76 (or 0x77), so the scan should return [60, 118] for the OLED and sensor. The total current draw is 30 mA (20 mA for OLED, 10 mA for BME280 and ESP32). With a 3.7V 18650 battery, you get about 20 hours of continuous operation. To extend battery life, put the ESP32 into deep sleep for 5 seconds, wake up, read the sensor, update the OLED, and sleep again. The OLED stays on during sleep, but you can turn it off with 0xAE before sleeping and on with 0xAF after waking. The wake-up time for the ESP32 is about 100 ms, so the average current is: (100 ms * 30 mA + 4900 ms * 1 µA) / 5000 ms = 0.6 mA, giving 3000 hours from a 2000 mAh battery.
Code Optimization Tips
To speed up display updates, avoid calling “oled.show()” after every pixel change. Instead, batch all drawing commands and call “show()” once. For animations, use double buffering: keep two bytearrays in RAM, draw to one, then swap and show. The “framebuf” module supports this with “framebuf.FrameBuffer”. You can also use the SSD1306’s horizontal scrolling feature: send command 0x26 for right scroll, 0x27 for left scroll, with parameters for start page, end page, and speed. This is hardware-accelerated and doesn’t require CPU intervention. For example, “oled.scroll(0, 0)” disables scrolling, while “oled.scroll(1, 0)” scrolls right by 1 pixel. The speed is set by the frame rate—at 60 Hz, a 1-pixel scroll per frame gives a smooth 60 pixels per second. This is useful for marquee text or status bars.
Compatibility with Different Microcontrollers
The SSD1306 driver works on any MicroPython board, but pin assignments vary. On the STM32F4 Discovery, I2C1 uses PB6 (SCL) and PB7 (SDA). On the Pyboard, I2C1 uses X9 (SCL) and X10 (SDA). On the ESP8266, I2C uses GPIO 4 (SDA) and GPIO 5 (SCL). The driver is written in pure Python, so it’s slower than C but
Have a jacket in mind?
Send us your sketch, logo, or a photo of a style you love. Our in-house design team will return a free, unlimited-revision mockup within 24 hours.
Get a Free Design Mockup & Quote