Raspberry Pi and drawing with 8 bit page – part one

When we played with NJU and SSD LCDs we were operating on a page rather than pixels. It is performance wise because we can toggle as much as 8 pixels in one write. But drawing requires us to get current page status and mix it with a current operation.

Normally we would read data from the screen but there is one catch, they operate on 5V and this can kill Raspi.
So we will do it a different way. We will use a buffer to keep current state and we will work on it. When we are ready we will flush content to display. It may be more effective because we are skipping reads.

Code @ GitHub

Planning

We have two LCDs with two sizes 122×32 and 128×64. We will create a universal class but before we write any line of code we will do some math.
First, we need to divide a height by 8 because the page has 8 bits. It also requires our LCDs to have the height that can be divided by 8 without rest. And this with width gives us buffer size.
I choose to implement a draw_pixel and maybe a draw_line. They are enough to create a page drawing algorithm.
Drawing is in a buffer and after that, it is flushed to the device.
So we need to recalculate pixel position to column and page.

Preparing

Our long-term goal is to have a separated classes for a different kind of drawing algorithms and different drivers. Additionally, drivers consist of connection class and chip class.
That’s why we need a new package drawing and in it first file page.py. In it, we define a page drawing algorithm.
Next, we need a driver package and subpackages for SSD and NJU chipsets. The connection can be anything, i2c, SPI, GPIO and it is used by Chip class.
How to connect all this?
Like with CharLCD, the driver is only responsible for communication with device. The chip class should know how to handle LCD with commands. And drawing class should know how to calculate pixels and colors for architecture. And on top of it is GfxLCD that provide a unified interface.
So it would be nice if we could pass chip driver with communication driver and drawing method to Gfx. But it is not friendly to a user, so maybe some factories will come handy. But this latter.
Ok, time for separate SSD1306 logic from device communication and see if our structure has any sense.

Decoupling

Our proof of concept for ssd1306 is in oled.py file. From this one file, we gonna make 2 or more:)
And ‘by accident’ let’s create abstract classes for driver and chipset so we would have a defined common interface.
After some tweaks, I have a driver module with two abstract classes. In it, we will create submodules for chipsets and there classes for connection and handling. It may sound complicated but look at the structure:

drawing
    page.py
driver
    ssd1306
        spi.py
        ssd1306.py
    chip.py
    driver.py

We define module ssd1306 and in it, we implement chip logic. And side by side we have spi.py which implements a connection driver.
The driver takes care of sending data to display and chip knows what to send to get required result.
Driver’s interface:

import abc


class Driver(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def init(self):
        """initialize a device"""
        return

    @abc.abstractmethod
    def reset(self):
        """resets a device"""
        return

    @abc.abstractmethod
    def cmd(self, data):
        """sends command to device"""
        return

    @abc.abstractmethod
    def data(self, data):
        """sends data to device"""
        return

Our interface forces us to implement four functions, to initialize, reset, send a command to an LCD and send a data to an LCD.
And chip needs to know about initialization and drawing functions:

import abc


class Chip(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def init(self):
        """init a chipset"""
        return

    @abc.abstractmethod
    def draw_pixel(self, x, y):
        """draw a pixel at x,y"""
        return

    @abc.abstractmethod
    def draw_line(self, x1, y1, x2, y2):
        """draw a line from point x1,y1 to x2,y2"""
        return

    @abc.abstractmethod
    def draw_rect(self, x1, y1, x2, y2):
        """draw a rectangle"""
        return

    @abc.abstractmethod
    def draw_circle(self, x, y, r):
        """draw a circle"""
        return

    @abc.abstractmethod
    def draw_arc(self, x, y, radius, start, end):
        """draw an arc"""
        return

    @abc.abstractmethod
    def fill_rect(self, x1, y1, x2, y2):
        """draw a filled rectangle"""
        return

Yes, we will implement so many drawing functions…one day 😀 And fun fact, we won’t implement them in chip class but in a class from drawing package.

Abstractions and multi-inheritance

This is a class that we will focus on: page.py, it is a goal of this post. It is responsible for translating abstract draw functions to understandable by the chip.
We are operating on a page and we are using a buffer. We need the buffer to know what is currently displayed and merge old state with a new state.
Class Page in page.py file:

class Page(object):
    def __init__(self):
        self.buffer = []

    def init(self):
        self.buffer = [[0] * (self.height // 8) for x in range(self.width)]

All pre-requirements are on place. So back to the ssd1306.py from ssd1306 module.

import sys
sys.path.append("../")
from drawing.page import Page
from driver.chip import Chip


class SSD1306(Chip, Page):
    def __init__(self, width, height, driver):
        super().__init__()
        self.width = width
        self.height = height
        self.driver = driver

    def init(self):
        self.driver.init()
        super().init()
        self.driver.reset()
        self.driver.cmd(0xae)  # turn off panel
        ...

There is a long list of cmd instructions but we already know them.

See how nicely everything meets here. We create an SSD1306 class that implements Chip and takes drawing functions from Page.
In constructor, we pass size and driver (SPI in our case)
So how to use this?
Back to oled.py in root dir and replace content with:

from driver.ssd1306.spi import SPI
from driver.ssd1306.ssd1306 import SSD1306

drv = SPI()
o = SSD1306(128, 64, drv)

o.init()

It is nice and clean. We initialize the driver and pass it to our LCD class.
The code will fail of course because we have lots of unimplemented methods. Back to page.py and implement them all with simple pass statement:)
And fail.
W need to swap:

class SSD1306(Chip, Page)

to

class SSD1306(Page, Chip)

And it initializes properly.

Yeh, we somehow managed to split everything. Let’s implement drawing!

Drawing a pixel

We pass x and y as coordinated for a pixel. First, we need to find page corresponding with y and bit that needs to be set. If we divide y via 8 we have a page.  And bit as a modulo operation. For example if we have a (6, 13), 13//8 = 1 and 13 mod 8 = 5.
So it should be column 6, page 1 and bit 5.

def draw_pixel(self, x, y):
        """draw a pixel at x,y"""
        self.buffer[x][y//8] |= 1 << (y % 8)

Thanks to bitwise operations we can do everything in one line.

But we haven’t got this pixel on the screen. We need some kind of flush function. But let’s think a little. If we flush everytime pixel is draw we would be doing unnecessary writes. We should flush buffer at the end of a drawing. But what is the end? I think we should add auto_flush option. With this, we may control and set what we need.
Define options dictionary in __init__ @ chip class:

        self.options = {
            'auto_flush': True,
        }

and flush function:

    def flush(self, force=None):
        if force is None:
            force = self.options['auto_flush']

        if force:
            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):
                    self.driver.data(self.get_page_value(i, j))

How to display those pixels?

from driver.ssd1306.spi import SPI
from driver.ssd1306.ssd1306 import SSD1306

drv = SPI()
o = SSD1306(128, 64, drv)

o.init()
o.draw_pixel(5, 0)
o.draw_pixel(6, 1)
o.draw_pixel(7, 2)
o.draw_pixel(5, 3)
o.draw_pixel(6, 13)
o.draw_pixel(7, 15)
o.draw_pixel(6, 18)

 

Summary

We began refactoring our proof of concepts into class for multiple LCDs. With separation connection class from driver class, we may easily change how our screen is wired.
We provide an interface for initializing and drawing. Our first drawing implementation operates on column and page.
This is necessary to support two kinds of LCDS: SSD1306 and NJU.
We also implemented pixel drawing and the page buffer. I was aiming to implement draw_line but this in next part:)

Advertisements

4 comments

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