Setting Up a 0.95 Inch 96x64 OLED with CircuitPython: A Practical Guide
To get a 0.95 inch 96x64 color oled display running with CircuitPython, you need to wire it up, install the right libraries, and write a few lines of code. This display uses an SSD1331 controller, which is a common driver for small color OLEDs, and it communicates over SPI. The resolution is 96x64 pixels, and it supports 65k colors, making it decent for small graphics, text, or sensor readouts. I’ll walk you through the hardware connections, software setup, and code examples, with real data and pinouts, so you can get it working without guesswork. The display operates at 3.3V logic, but it can handle 5V power if you’re careful—check the datasheet for your specific module. The SPI interface uses four pins: MOSI, SCLK, CS, and DC (data/command), plus a RST pin for reset. The typical power draw is around 20-30 mA at full brightness, which is low enough for battery-powered projects. For the wiring, connect the VCC to 3.3V or 5V (depending on your board), GND to ground, MOSI to your board’s MOSI (e.g., GPIO 11 on a Raspberry Pi Pico), SCLK to SCK (GPIO 10), CS to any GPIO (e.g., GPIO 9), DC to another GPIO (e.g., GPIO 8), and RST to a third GPIO (e.g., GPIO 12). If your display has a backlight pin, you can leave it unconnected or tie it to VCC for full brightness. The SSD1331 supports a SPI clock frequency up to 10 MHz, but starting at 4 MHz is safe to avoid signal issues. I’ll use a Raspberry Pi Pico as the example board, but the same approach works with any CircuitPython-compatible board like the Adafruit Feather or ESP32-S2.
The first step is to install CircuitPython on your board. Download the latest .uf2 file from the official CircuitPython site for your specific board, then hold the BOOTSEL button on the Pico while plugging it into USB, and drag the file onto the RPI-RP2 drive. Once it reboots, you’ll see a CIRCUITPY drive. Next, you need the SSD1331 library. The Adafruit CircuitPython SSD1331 library is the standard one, but it depends on the Adafruit CircuitPython DisplayIO and BusDevice libraries. Download the latest releases from the Adafruit CircuitPython Bundle (version 8.x or later) from GitHub, and copy the following folders to the lib folder on your CIRCUITPY drive: adafruit_ssd1331.mpy, adafruit_displayio_ssd1331.mpy (if using displayio), and adafruit_bus_device. The total library size is about 50 KB, so it fits easily on most boards. If you’re using a board with limited flash (like the Pico with 2 MB), you can also use the raw SPI library to save space, but the displayio method is more efficient for graphics. The displayio library uses a framebuffer in RAM, which for a 96x64 display at 16-bit color takes 96 * 64 * 2 = 12,288 bytes of RAM. That’s fine for the Pico (which has 264 KB), but on a smaller board like the Adafruit Trinket M0 (with 32 KB RAM), you might need to use the raw SPI approach to avoid memory issues. For the raw SPI method, you can use the adafruit_ssd1331 library directly, which writes pixels one by one and uses less RAM, but it’s slower for full-screen updates. The displayio library, on the other hand, supports hardware acceleration and double buffering, which is smoother for animations.
Now, let’s write the code. Open a new file in your editor (like Mu or Thonny) and save it as code.py on the CIRCUITPY drive. Here’s a basic setup using displayio:
```python
import board
import displayio
import adafruit_displayio_ssd1331
# Release any previously configured displays
displayio.release_displays()
# Define SPI bus and pins
spi = board.SPI()
tft_cs = board.GP9 # Chip select
tft_dc = board.GP8 # Data/command
tft_rst = board.GP12 # Reset
# Create display bus
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst)
# Initialize the display (width=96, height=64, color depth=16-bit)
display = adafruit_displayio_ssd1331.SSD1331(display_bus, width=96, height=64)
# Create a group to hold shapes
splash = displayio.Group()
display.show(splash)
# Add a bitmap (e.g., a red rectangle)
color_bitmap = displayio.Bitmap(96, 64, 1) # 1 color
color_palette = displayio.Palette(1)
color_palette[0] = 0xFF0000 # Red in RGB565
bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0)
splash.append(bg_sprite)
while True:
pass
```
This code initializes the display and fills it with red. The color format is RGB565, where each color component (red, green, blue) uses 5 or 6 bits. For example, 0xFF0000 is red (bits 15-11: 11111 for red, 10-5: 000000 for green, 4-0: 00000 for blue). The display supports 65,536 colors, but the actual visible range depends on the OLED’s gamma. The SSD1331 datasheet specifies a typical contrast ratio of 10,000:1 and a brightness of 100 cd/m², which is bright enough for indoor use. The refresh rate is around 100 Hz, but the SPI bus speed limits the actual frame rate. At 4 MHz SPI clock, a full-screen update takes about 96 * 64 * 2 * 8 / 4,000,000 = 0.0246 seconds, or about 40 frames per second. That’s smooth for most applications, but you can increase the SPI clock to 8 MHz for faster updates. The display also has a built-in charge pump for the OLED voltage, so you don’t need external components. The power consumption is around 20 mA at full brightness, but you can reduce it by lowering the brightness via the SSD1331’s contrast register (command 0x81). To set brightness, send the command 0x81 followed by a value from 0 to 255, where 255 is max. For example, to set 50% brightness, send 0x81, 0x7F. You can do this with the raw SPI library by writing to the display’s registers.
For text rendering, you need a font. The displayio library includes built-in terminal fonts, but they’re pixelated at small sizes. A better option is the adafruit_bitmap_font library, which supports TrueType fonts. Download a .bdf font file (like the 5x7 or 8x12 from the Adafruit font library) and place it in the fonts folder on your CIRCUITPY drive. Then use this code:
```python
import board
import displayio
import adafruit_displayio_ssd1331
from adafruit_bitmap_font import bitmap_font
from displayio import Group, Label
displayio.release_displays()
spi = board.SPI()
tft_cs = board.GP9
tft_dc = board.GP8
tft_rst = board.GP12
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst)
display = adafruit_displayio_ssd1331.SSD1331(display_bus, width=96, height=64)
# Load a font
font = bitmap_font.load_font("/fonts/5x7.bdf")
# Create a group
group = Group()
display.show(group)
# Add text
text = "Hello World"
text_area = Label(font, text=text, color=0x00FF00, x=5, y=10)
group.append(text_area)
while True:
pass
```
This renders green text at position (5, 10). The 5x7 font is small enough to fit about 13 characters per line (96 pixels / 7 pixels per char = 13.7), and you can fit about 8 lines (64 pixels / 8 pixels per line including spacing). For larger fonts, you’ll have fewer lines. The display’s pixel pitch is 0.21 mm, so the physical size is about 20.16 mm x 13.44 mm, which is tiny but readable from a few inches away. The viewing angle is 160 degrees, typical for OLEDs, so it’s good for wearable or handheld devices. The display also supports partial updates, which can save power if you only change a small area. To do a partial update, you need to send a window command (0x15 for column start/end, 0x75 for row start/end) followed by pixel data. The displayio library handles this automatically, but if you’re using the raw SPI library, you can do it manually. For example, to update only a 20x20 pixel area, send 0x15, 0x10, 0x23 (columns 16 to 35), then 0x75, 0x10, 0x23 (rows 16 to 35), then the pixel data for 400 pixels. This reduces the data transfer from 12,288 bytes to 800 bytes, which speeds up the update and reduces power.
One common issue is the display not initializing due to incorrect wiring or timing. The SSD1331 requires a reset pulse after power-up. The library handles this by toggling the RST pin, but if you’re using a custom setup, you need to hold RST low for at least 10 µs, then high for 10 µs before sending commands. The initialization sequence from the datasheet includes commands like 0xAE (display off), 0xA0 (set remap), 0x81 (set contrast), 0x82 (set brightness), 0x87 (set master current), 0x8A (set pre-charge speed), 0x8B (set pre-charge voltage), 0x8C (set VCOMH), 0xB0 (set power save), 0xB1 (set phase 1 and 2 periods), 0xB3 (set display clock divide ratio), 0xB4 (set segment low voltage), 0xB6 (set second pre-charge period), 0xBE (set VSL), 0xE0 (set gamma correction), 0xE3 (set gamma correction), and 0xAF (display on). The default values in the library are usually fine, but you can tweak them for better color accuracy or lower power. For example, the default contrast is 0x7F, but you can increase it to 0xFF for brighter colors, though this might reduce the OLED lifespan. The OLED lifespan is typically 10,000 hours at full brightness, but it can be extended by dimming the display when not in use. The display also has a sleep mode (command 0xAE) that reduces power to less than 1 µA, which is useful for battery-powered projects.
For advanced graphics, you can draw shapes using the adafruit_display_shapes library. Install it by copying the adafruit_display_shapes.mpy file to the lib folder. Then you can draw rectangles, circles, lines, and polygons. Here’s an example that draws a circle and a line:
```python
import board
import displayio
import adafruit_displayio_ssd1331
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.line import Line
displayio.release_displays()
spi = board.SPI()
tft_cs = board.GP9
tft_dc = board.GP8
tft_rst = board.GP12
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst)
display = adafruit_displayio_ssd1331.SSD1331(display_bus, width=96, height=64)
group = displayio.Group()
display.show(group)
# Draw a blue circle at (48, 32) with radius 20
circle = Circle(48, 32, 20, fill=0x0000FF, outline=0xFFFFFF)
group.append(circle)
# Draw a red line from (0, 0) to (95, 63)
line = Line(0, 0, 95, 63, color=0xFF0000)
group.append(line)
while True:
pass
```
This creates a blue circle with a white outline and a red diagonal line. The fill and outline colors are optional. The shapes library uses the displayio framebuffer, so it’s efficient for static graphics. For animations, you can update the group in a loop, but be careful not to overload the SPI bus. A simple animation like a moving dot can be done by changing the x and y coordinates of a circle in a loop, but you need to clear the previous position by redrawing the background. A better approach is to use a displayio.TileGrid with a bitmap that you modify in place. For example, create a 96x64 bitmap and set pixels individually, then refresh the display. The bitmap is stored in RAM, so you can modify it quickly. Here’s a simple animation that draws a bouncing ball:
```python
import board
import displayio
import adafruit_displayio_ssd1331
import time
displayio.release_displays()
spi = board.SPI()
tft_cs = board.GP9
tft_dc = board.GP8
tft_rst = board.GP12
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst)
display = adafruit_displayio_ssd1331.SSD1331(display_bus, width=96, height=64)
# Create a bitmap for the background
bitmap = displayio.Bitmap(96, 64, 2) # 2 colors
palette = displayio.Palette(2)
palette[0] = 0x000000 # Black background
palette[1] = 0xFFFFFF # White ball
tile_grid = displayio.TileGrid(bitmap, pixel_shader=palette)
group = displayio.Group()
group.append(tile_grid)
display.show(group)
# Ball position and velocity
x, y = 48, 32
vx, vy = 2, 1
while True:
# Clear previous ball
bitmap[x, y] = 0
# Update position
x += vx
y += vy
# Bounce off edges
if x <= 0 or x >= 95:
vx = -vx
if y <= 0 or y >= 63:
vy = -vy
# Draw new ball
bitmap[x, y] = 1
time.sleep(0.02)
```
This creates a bouncing white ball on a black background. The bitmap is modified directly, and the display updates automatically via displayio’s refresh mechanism. The frame rate is limited by the sleep time, but you can remove the sleep for maximum speed. The displayio library refreshes the display at the end of each loop iteration, so the ball moves smoothly. The actual refresh rate depends on the SPI speed and the number of pixels changed. For a single pixel, the update is fast, but for larger areas, it’s slower. The SSD1331 also supports hardware scrolling, which can be used for smooth text scrolling. Command 0x2A (set horizontal scroll) and 0x2B (set vertical scroll) allow you to scroll the entire display or a window. This is useful for ticker-style text. To enable horizontal scrolling, send 0x2A, 0x00 (direction), 0x00 (start column), 0x5F (end column), 0x00 (start row), 0x3F (end row), 0x00 (scroll speed), then 0x2F (activate scroll). The scroll speed is in frames, so a value of 0x00 means 1 frame per step, which is fast. You can adjust it for slower scrolling.
Another practical aspect is power management. The display’s current consumption is about 20 mA at full brightness, but it can spike to 30 mA during initialization due to the charge pump. If you’re using a battery, you can put the display to sleep