Raspberry Pi, Python and OLED display

Yey, another day with LCD. This time an OLED 0.96″ display with an SSD1306 chipset. Like with previous display, we will focus on learning how this works, how to initialize LCD and how to display something.
My OLED is a popular display, you can see it everywhere. It exists in a few variants most popular are with i2c bus or with SPI interface. It also varies in size, from 64×32 to 128×64.
My unit is 128×64 with SPI on the SSD1306 chipset. There are some fakes with an SH1106, almost same but with 132 pixels in width.
Now it is fascinating, this screen is twice bigger in the pixels that the one with NJU! And is much smaller! Woot technology.
I have chosen this LCD because it has SPI interface and OLED technology.



The plan is similar to last time. Wire a display, initialize it and display something. We need to learn some basics about this kind of display.


What a relief, only 6 pins:)

LCD             Raspberry Pi
GND   ----------- GND
+3.3V ----------- +3.3V
SCL   ----------- G11
SDA   ----------- G10
RST   ----------- G13
D/C   ----------- G6

We will use 3.3V but it should work with 5V. We are using SPI interface, do not forget to enable it @ RaspberryPi!
LCD got its power and.. nothing. My first thought was that something is wrong. But it is OLED! No backlight 🙂 so no sign that it works:

And that’s why I have this LED 🙂 It shows activity.


Like usually let’s start from pin setup, cmd and data:

import RPi.GPIO as GPIO
import time
import spidev
import random

class Oled(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.pins = {
            'RST': 13,
            'DC': 6,

        for pin in self.pins:
            GPIO.setup(self.pins[pin], GPIO.OUT)
            GPIO.output(self.pins[pin], 0)

        spi = spidev.SpiDev()
        spi.max_speed_hz = 8000000
        spi.mode = 0
        self.spi = spi

    def cmd(self, data):
        GPIO.output(self.pins['DC'], 0)

    def data(self, data):
        GPIO.output(self.pins['DC'], 1)
        GPIO.output(self.pins['DC'], 0)

We do not have a function send because we are using SPI and sending is just one line.
Combining the power of google with documentation and we have a nice effect: an init function.

    def init(self):
        GPIO.output(self.pins['RST'], 1)
        GPIO.output(self.pins['RST'], 0)
        GPIO.output(self.pins['RST'], 1)
        self.cmd(0xae)  # turn off panel
        self.cmd(0x00)  # set low column address
        self.cmd(0x10)  # set high column address
        self.cmd(0x40)  # set start line address

        self.cmd(0x20)  # addr mode
        self.cmd(0x00)  # horizontal

        self.cmd(0xb0)  # set page address
        self.cmd(0x81)  # set contrast control register
        self.cmd(0xa0)  # a0/a1 set segment re-map 127 to 0   a0:0 to seg127
        self.cmd(0xc0)  # c8/c0 set com(N-1)to com0  c0:com0 to com(N-1)
        self.cmd(0xa6)  # set normal display, a6 - normal, a7 - inverted

        self.cmd(0xa8)  # set multiplex ratio(16to63)
        self.cmd(0x3f)  # 1/64 duty

        self.cmd(0xd3)  # set display offset
        self.cmd(0x00)  # not offset

        self.cmd(0xd5)  # set display clock divide ratio/oscillator frequency
        self.cmd(0x80)  # set divide ratio

        self.cmd(0xd9)  # set pre-charge period
        self.cmd(0xda)  # set com pins hardware configuration

        self.cmd(0xdb)  # set vcomh

        self.cmd(0x8d)  # charge pump
        self.cmd(0x14)  # enable charge pump
        self.cmd(0xaf)  # turn on panel

There are two interesting setting:

self.cmd(0xa0)  # a0/a1 set segment re-map 127 to 0   a0:0 to seg127
self.cmd(0xc0)  # c8/c0 set com(N-1)to com0  c0:com0 to com(N-1)

With them, we can reverse x or y-axis.
Do not be afraid of random dots after initialization.


Like with NJU6450 we work on pages and columns. From what I read it can be changed but I think this is nice and efficient.
Seems that writing to column increase column pointer. We will use this nice feature to write fill function:

    def set_area(self, x1, y1, x2, y2):
        self.cmd(0xb0 + y1)
        self.cmd(0xb0 + y2)

    def fill(self, c=0xff):
        for j in range(0, self.height//8):
            self.set_area(0, j, self.width-1, j+1)
            for i in range(0, self.width):

How to use our functions?

o = Oled(128, 64)
o.fill(random.randint(0, 255))

And a small problem, I thought that function set_area would, like with TFT set an area and increase page and column pointer. But to my surprise, it works only with the column. That is why we set area in first for loop.

I was wondering about it and checked the documentation.  There I found very interesting information, there are 3 different memory addressing modes. During initialization, we set the page addressing mode (0xB0). And it behaves exactly like that, increases column but not the page.
To have the effect we want, set memory addressing mode to horizontal (or vertical). It increases column (or page) and after reaching the end, reset column and increase page ( or reset page and increase column 🙂 ). We will play with modes a little bit in next chapter.

Time for pixel/page drawing in page addressing mode:

    def draw_pixels(self, x, y, c=0xff):
        """draw a pixel /line"""
        j = y//8
        self.set_area(x, j, x+1, j+1)

And another surprise. When we draw like that:

o.draw_pixels(30, 50)
o.draw_pixels(30, 10)

We see only one line! What the heck? But when we change to:

o.draw_pixels(30, 50)
o.draw_pixels(31, 10)

both lines appear. Looks like we need to change the column if we want to change the page. Very unfriendly.

Write modes

Quick summaries of each mode. With each write data to screen:
Page addressing mode – column increase, page not. Column resets at the end of row
Horizontal addressing mode – column increase, after reach end of row, column resets and page increase
Vertical addressing mode – page increase, after reaching end of column, page resets and column increase

Let’s see how horizontal mode works. Add lines to init function:

        self.cmd(0x10)  # set high column address
        self.cmd(0x40)  # set start line address

        self.cmd(0x20)  # addr mode
        self.cmd(0x00)  # horizontal

        self.cmd(0xb0)  # set page address
        self.cmd(0x81)  # set contrast control register

First 0x20 and next 0x00 sets required mode. When I run code nothing changed. After removing set_are from fill function it worked.
So when we use set_xy it changes the mode to the page. Hmm seems horizontal and vertical mode is good only to fully refresh an LCD. We cannot draw on a part, only on a full area. It may be good for streaming but not for us. Back to page address mode.


This is amazing, screen is small in size but big in pixels 🙂
See that it and NJU work in a similar way, they operate on pages and columns. This is good information because we can encapsulate them in the same abstract class.
There are some functions that we skipped, like contrast control. We do not need them for now.
Also, the problem with drawing pixels, a line on the same column but a different page will hit us later.
We gathered all required data to take the next step and we can start work on GfxLCD module.


One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s