> ## Content Index
> Fetch the complete content index at: https://mfitzp.ghost.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Driving I2C OLED displays with MicroPython
- URL: https://mfitzp.ghost.io/oled-displays-i2c-micropython/
- Published: 2018-08-25T08:18:00.000Z
- Updated: 2026-09-09T13:15:23.000Z
- Description: Driving I2C monochrome displays with SSD1306 from MicroPython
- Author: Martin Fitzpatrick
- Tags: Electronics, MicroPython, Python, #Import 2026-08-24 14:26

These mini monochrome OLED screens make great displays for projects — perfect for data readout, simple UIs or monochrome games.

| Requirements                                     |  |
| ------------------------------------------------ |  |
| Wemos D1 v2.2+ or good imitations.</td           |  |
| 0.91in OLED Screen 128x32 pixels, I2c interface. |  |
| Breadboard Any size will do.                     |  |
| Wires Loose ends, or jumper leads.               |  |

## Setting up

MicroPython provides some built-in support for these displays, and a simple framebuffer which can be used as a drawing surface. However, we still need a driver to interface the buffer to the display.

There is a Python *ssd1306* module for OLED displays available [in the MicroPython repository](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py?ref=mfitzp.ghost.io). Click *Raw* format and save the file with a `.py` extension.

You can then use the [ampy](https://github.com/adafruit/ampy?ref=mfitzp.ghost.io) tool (or the WebREPL) to upload the file to your device's filesystem:

```Bash
ampy --port /dev/tty.wchusbserial141120 put ssd1306.py

```

With the `ssd1306.py` file on your Wemos D1, you can import it as any other Python module. Connect to your device, and then in the REPL enter:

```Python
from machine import I2C, Pin
import ssd1306

```

Both I2C and SPI-controlled displays are available — these instructions will only work for I2C displays.

If the `import ssd1306` succeeds, the package is correctly uploaded and you're good to go. Wire up the OLED display, connecting pins `D1` to `SCL` and `D2` to `SDA`. Provide power from `G` and `5V`.

![The circuit](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/display-wemos-circuit.jpg)

## Using the interface

🤘

The following instructions are based on a 0.91in monochrome 128x32 OLED, but other displays using the same chipset can be used with this library.

To use the display you first need to create an I2C interface. In MicroPython I2C is [via a software implementation](https://www.martinfitzpatrick.com/wemos-d1-pins-micropython/?ref=mfitzp.ghost.io) so you can put it on any GPIO pins you like.

```Python
i2c = I2C(-1, Pin(5), Pin(4))
display = ssd1306.SSD1306_I2C(128, 32, i2c)

```

The SSD1306 module makes use of the MicroPython `framebuf` frame buffer, an efficient in-memory buffer for working with a simple graphics view. The methods for drawing text and primitives are from this framebuffer implementation. For a complete overview of what's available, [check the MicroPython documentation](http://docs.micropython.org/en/latest/wipy/library/framebuf.html?ref=mfitzp.ghost.io).

The setup of the framebuffer format (monochrome, bit ordering, etc.) is also handled by the SSD1306 library. Check the [framebuf documentation](http://docs.micropython.org/en/latest/wipy/library/framebuf.html?ref=mfitzp.ghost.io) for more info on available options for other displays.

To test your I2C connection to the display, fill the display in solid colour.

```Python
display.fill(1)  # Fill the entire display with 1="on"
display.show()

```

You need to call `display.show()` to actually send the current `framebuf` to the device.

![The display, filled](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0004.jpg)

## Drawing primitives

The `.fill()` method can be used to fill the entire display with a specified colour. Note that this changes all pixels to the given colour, and does not perform a flood fill of matching regions.

```Python
# Fill the entire display with colour 0
display.fill(0)

```

![The display, filled with blank](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0005.jpg)

For filling specific regions of the display see `.fill_rect()` below.

Setting individual pixels can be accomplished using `.pixel()`. This is only a good idea when you area setting relatively few pixels as it is much slower than using other methods.

```Python
# Set the pixel at 3, 4 (x, y) to 1
# .pixel(x, y, c)
display.pixel(3, 4, 1)  # 3rd param is the colour
display.show()

```

![A single pixel](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0006.jpg)

If you don't provide a color via parameter *c*, this method *returns* the colour of the pixel at the specified coordinates.

```Python
# Return the value at 3, 4 (x, y)
# .pixel(x, y)
c = display.pixel(3, 4)

```

This is not a particularly nice API. In other cases (e.g. text) omitting the optional `c` will use 1 as a default.

Horizontal and vertical lines can be drawn with `.hline()` and `.vline()` respectively, providing a starting *x,y* location and line *length* and *colour*.

```Python
# Draw a horizontal line, starting from 2, 3 (x, y), 4 pixels wide
# .hline(x, y, w, c)
display.hline(2, 3, 25, 1)
display.show()

```

![A horizontal line](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0007.jpg)

```Python
# Draw a vertical line, starting from 5, 0 (x, y), 6 pixels high
# .vline(x, y, h, c)
display.vline(5, 0, 15, 1)
display.show()

```

![A vertical line](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0008.jpg)

For diagonal lines, the `.line()` method can be used to draw lines between between two sets of points *x1,y1* and *x2,y2* specified in order. The parameter *c* controls the colour of the line drawn.

```Python
# Draw a short diagonal line down to the right.
# .line(x1, y1, x2, y2, c)
display.line(0, 0, 50, 25, 1)
display.show()

```

![A diagonal line](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0009.jpg)

There is no *antialiasing*, so diagonal lines will look pretty jaggy.

The `.draw_rect()` method allows you to draw a *unfilled* rectangle, starting at *x,y* and with a specified width *w* and height *h*. The specified colour *c* is used to draw the boundary of the rectangle, but it is not filled.

```Python
# Draw an unfilled rectangle of 8, 5 pixels, starting at 1,1 in colour 1.
# .rect(x, y, w, h, c)
display.rect(5, 5, 100, 20, 1)
display.show()

```

![An empty rectangle](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0010.jpg)

You can also draw filled rectangles, using `.fill_rect()`. The parameters are the same as for `.rect()` but all pixels within the boundary will be set.

```Python
# Draw a filled rectangle of 10x5 pixels, starting at 3,3 in colour 1
# .fill_rect(x, y, w, h, c)
display.fill_rect(9, 9, 25, 25, 1)
display.show()

```

![An filled rectangle](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0011.jpg)

## Writing text

The framebuffer class provides support for writing text using a simple 8x8 bitmap font. The pixel *x,y* positions are relative to the top-left of the 8x8 character, so positioning at *0,0* will give the absolute top left of the screen.

```Python
# Print "Hello world!" 1 pixel from top left, in colour 1 (on)
# .text(text, x, y, c)
display.text("Hello world!", 1, 1, 1)
display.show()

```

![Hello world](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0014.jpg)

The `.text()` method takes an optional 4th parameter *color*, which gives the colour to draw text in. On a mono screen this can be either `0` (off) or `1` (on).

```Python
# Print "Hello world!" at 2,2 in colour 0 (off)
display.text("Hello world!", 2, 2, 0)
display.show()

```

![Hello world, black over](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0015.jpg)

```Python
# Print "Hello world!" at 3,3 in colour 1 (on)
display.text("Hello world!", 3, 3, 1)
display.show()

```

![Hello world, white over black](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0016.jpg)

## Scrolling

You can shift the contents of the entire framebuffer around using `.scroll`, which takes `x` and `y` parameters to specify the scroll (positive/negative) in each dimension.

```Python
# .scroll(x, y)
display.scroll(-10, 0)

```

![Scrolled left](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0018.jpg)

There is no support for scrolling a portion of the framebuffer. You can however keep a separate framebuffer which you scroll and then blit to your main display — see the next section.

## Pixel graphics

For very simple graphics that do not update often you can get away with writing bitmap graphics to the framebuffer pixel by pixel. For example, in the following code block we draw an icon from a list-of-lists of binary colour data, iterating over with simple loops:

```Python
ICON = [
    [ 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [ 0, 1, 1, 0, 0, 0, 1, 1, 0],
    [ 1, 1, 1, 1, 0, 1, 1, 1, 1],
    [ 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [ 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [ 0, 1, 1, 1, 1, 1, 1, 1, 0],
    [ 0, 0, 1, 1, 1, 1, 1, 0, 0],
    [ 0, 0, 0, 1, 1, 1, 0, 0, 0],
    [ 0, 0, 0, 0, 1, 0, 0, 0, 0],
]

display.fill(0) # Clear the display
for y, row in enumerate(ICON):
    for x, c in enumerate(row):
        display.pixel(x, y, c)

display.show()

```

![Bitmap heart graphic](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0019.jpg)

Using `urandom` we can scatter images over the display, like in the following example:

```Python
import urandom

def random_heart():
    xofs = urandom.getrandbits(8)
    yofs = urandom.getrandbits(5)
    for y, row in enumerate(ICON):
        for x, c in enumerate(row):
            display.pixel(x + xofs, y + yofs, c)

for n in range(100):
    random_heart()

display.show()

```

![Scattered heart graphics](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0020.jpg)

You'll notice that there is a black square around each heart, visible when a heart is drawn over the top of another. You can avoid this by adding `if c` around the `display.pixel` call. This will only draw pixels which are on, effectively masking the image.

For bigger graphics or where you need faster updates, you can instead *blit* your bitmap images from one framebuffer to another. Pass the `framebuf` to blit, and the coordinates `x` and `y` to blit at.

```Python
# Blit a framebuffer at the pixel position 1, 1.
# display.blit(fbuf, x, y)
display.blit(fbuf, 1, 1)

```

The `blit` method takes an optional 4th parameter *key* which is a colour to considered 'transparent' when blitting. Pixels with this value won't be copied over onto the target display.

```Python
# Blit except pixels with value 0 (i.e. only pixels with value 1)
# .blit(fbuf, x, y, key)
display.blit(fbuf, 1, 1, key=0)

```

See [this follow up](https://www.martinfitzpatrick.com/article/displaying-images-oled-displays/?ref=mfitzp.ghost.io) for details on how to display images on OLED screens, including animated graphics.

## Display control

The drawing methods so far are inherited from the the MicroPython `framebuf` object, which is written to the display on `.update()`. But the `ssd1306` object itself also provides methods for direct control of the display component.

The display can be turned on and off using `display.poweron()` and `display.poweroff()` respectively.

You can set the contrast for the display using `.contrast()` passing in parameter *c* which is a value between `0` and `255`. This controls the contrast between the foreground, active colour `1` and the background `0`.

```Python
# Set the display contrast to half (127/255 = 0.5)
# .contrast(c)
display.contrast(50)

```

![Contrast 50](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0023.jpg)

To invert the display, switching foreground and background colours, call `.invert(1)`.

```Python
display.invert(1)

```

![Inverted](https://storage.ghost.io/c/50/3e/503e92e5-9187-4058-a6fd-d37b3deb22ec/content/images/2026/09/IMG_0025.jpg)

Calling `.invert(0)` will return the display to how it was originally. You can use this for display-flashing visual effects:

```Python
import time
while True:
    display.invert(0)
    time.sleep(0.01)
    display.invert(1)
    time.sleep(0.01)
```