OLED Driver Class (Solution)
A class for the OLED display
from time import sleep
import framebuf
##################################################################
class OLED :
##################################################################
def __init__( self, i2c, i2c_adr, buf = None ):
'''We have 2 possibilities to use this class. If we do not
provide a buffer (which needs to be a byte-array) then the
constructor creates a framebuffer with the associated buffer.
the framebuffer can then be retrieved by the application
with the "getFramebuffer" call and then the application can
use the api of the framebuffer to draw things.
The second way to use the class is by providing the buffer
to the constructor. Then no framebuffer is created and the
class assumes that the user has created the framebuffer
himself.
The framebuffer itself is not needed by this class. (It is
only created as a convenience for the user with parameters
which match the display). The buffer, however, is used when
the contents is copied into the memory of the display. The
corresponding method ('copyFramebuffer') has an optional
parameter "buf" which can be used by the user to provide
a buffer from the appliction. If no buffer is provided the
buffer provided by the user in this constructor is used or
the one which was create by the constructor.
This architecture should be convenient for the user and in
addition allows the user to play with multiple framebuffers
if he wants to.'''
if buf:
self.buf = buf
self.framebuffer = None
else:
self.buf = bytearray( 128*16)
self.framebuffer = framebuf.FrameBuffer( self.buf, 128,128, framebuf.MONO_VLSB )
self.i2c = i2c
self.oled_adr = i2c_adr
self.landscape = False
self.bufr = None
def _wr_cmd( self, cmd ):
'''This function writes a single command byte to the display controller.
(There are one byte and two byte commands in for the controller (see
datasheet. Both bytes are considered "command bytes" here. Hence this
function can be used for both types of command bytes.
Try to understand from the data sheet what has to be transferred to the
display in order that it is interpreted as a command byte. (Hint: it will
be two bytes in total which you will need to transfer)
It is important to note tht the i2c.writeto function expects the python
type 'bytearray' and NOT a series of integers. Fortunately there is a
function in python which we can use to convert an integer (which in python
is an object) to a series of bytes: the function is called ".to_bytes(...)"
and is called directly on the variable holding the integer we want to convert.
You can look up the documentation of the function in the python documentation.
The signature is "int.to_bytes(length=1, byteorder='big')" (with default parameters).
Btw: the esp32 is "little endian" computer (but this is not important here.)'''
self.i2c.writeto( self.oled_adr, b'\x80'+ cmd.to_bytes(1,'little') )
def init(self):
'''Initialize the display. To do this we need to write configuration
parameters to some registers in the controller chip. What we need to write
depends on the design of our display. We need to study the data sheet to
retrieve the correct values. We write the values with a sequence of commands.
The sequence is very similar for many different controller chips on the
market. Hence if you get one working it is easy to adapt to another display.
Sometimes some offsets have to be adjusted since this depends on how the
display is soldered to the controller chip.
Btw: most of the parameters are correctly set after a Power On Reset (POR)
Hence many of the commands below are strictly speaking not necessary.
To have this a bit readable and compact we define an array with the
commands and then pass this array to the loop which writes one byte
after the other to the controller. As you can see in the data sheet
some commands have 2 bytes and others only one byte.'''
commands = [ 0xae, # Turn display off during initialisation
0x00, # Set column address to '0' (lower 4 bits)
0x10, # Set column address to '0' (higher 3 bits)
0xb0, # Set page address to '0'
0xdc,0x00, # Set display start-line to '0'
0x81,0x20, # Set the contrast to 32
0x20, # Address mode: page addressing
0xa0, # Scan direction (horizontal mirroring)
0xc0, # Vertical mirroring (c0 - c8)
0xa4, # Display RAM contents (not all white)
0xa6, # Display not inverted
0xa8,0x3f, # Multiplex ratio to 0x3f
0xd3,0x60, # Display offset (start line)
0xd5,0x41, # Sets oscillator freq and clock divide ratio
0xd5,0x50, # Sets oscillator freq and clock divide ratio (set to default)
0xd9,0x22, # Pre and dis-charge period (set to default)
0xdb,0x35, # Sets some display voltage (set to default)
0xad,0x80, # Disable the internal DC/DC converter
# (There is probably an external one)
0xaf # Turn on the display
]
for cmd in commands:
self._wr_cmd( cmd )
sleep(0.1) # the data sheet wants this.
def getFramebuffer(self):
return self.framebuffer
def setLandscape( self, ls = True ):
if ls:
if not self.bufr:
self.bufr = bytearray( 128*16)
self.landscape = True
else:
self.landscape = False
def rotateFramebuffer( self, buf ):
'''The SH1107 controller chip has no methods to rotate the screen.
This is why we have to do this ourselves if we want to use the display
in landscape mode (which for most of the applications is nicer...)
This is pretty horrible spaghetti code here...but it is fast. If you
have a more elegant solution, try it out! (Matrix multiplication may be?)
'''
bufr = self.bufr
for ip in range(0,16):
for ic in range(0,8):
bufr[ip*128+ic*8+7] = ((buf[ic*128+120-ip*8]&0x80) ) + ((buf[ic*128+121-ip*8]&0x80)>>1) + ((buf[ic*128+122-ip*8]&0x80)>>2) + ((buf[ic*128+123-ip*8]&0x80)>>3) + ((buf[ic*128+124-ip*8]&0x80)>>4) + ((buf[ic*128+125-ip*8]&0x80)>>5) + ((buf[ic*128+126-ip*8]&0x80)>>6) + ((buf[ic*128+127-ip*8]&0x80)>>7)
bufr[ip*128+ic*8+6] = ((buf[ic*128+120-ip*8]&0x40)<<1) + ((buf[ic*128+121-ip*8]&0x40) ) + ((buf[ic*128+122-ip*8]&0x40)>>1) + ((buf[ic*128+123-ip*8]&0x40)>>2) + ((buf[ic*128+124-ip*8]&0x40)>>3) + ((buf[ic*128+125-ip*8]&0x40)>>4) + ((buf[ic*128+126-ip*8]&0x40)>>5) + ((buf[ic*128+127-ip*8]&0x40)>>6)
bufr[ip*128+ic*8+5] = ((buf[ic*128+120-ip*8]&0x20)<<2) + ((buf[ic*128+121-ip*8]&0x20)<<1) + ((buf[ic*128+122-ip*8]&0x20) ) + ((buf[ic*128+123-ip*8]&0x20)>>1) + ((buf[ic*128+124-ip*8]&0x20)>>2) + ((buf[ic*128+125-ip*8]&0x20)>>3) + ((buf[ic*128+126-ip*8]&0x20)>>4) + ((buf[ic*128+127-ip*8]&0x20)>>5)
bufr[ip*128+ic*8+4] = ((buf[ic*128+120-ip*8]&0x10)<<3) + ((buf[ic*128+121-ip*8]&0x10)<<2) + ((buf[ic*128+122-ip*8]&0x10)<<1) + ((buf[ic*128+123-ip*8]&0x10) ) + ((buf[ic*128+124-ip*8]&0x10)>>1) + ((buf[ic*128+125-ip*8]&0x10)>>2) + ((buf[ic*128+126-ip*8]&0x10)>>3) + ((buf[ic*128+127-ip*8]&0x10)>>4)
bufr[ip*128+ic*8+3] = ((buf[ic*128+120-ip*8]&0x08)<<4) + ((buf[ic*128+121-ip*8]&0x08)<<3) + ((buf[ic*128+122-ip*8]&0x08)<<2) + ((buf[ic*128+123-ip*8]&0x08)<<1) + ((buf[ic*128+124-ip*8]&0x08) ) + ((buf[ic*128+125-ip*8]&0x08)>>1) + ((buf[ic*128+126-ip*8]&0x08)>>2) + ((buf[ic*128+127-ip*8]&0x08)>>3)
bufr[ip*128+ic*8+2] = ((buf[ic*128+120-ip*8]&0x04)<<5) + ((buf[ic*128+121-ip*8]&0x04)<<4) + ((buf[ic*128+122-ip*8]&0x04)<<3) + ((buf[ic*128+123-ip*8]&0x04)<<2) + ((buf[ic*128+124-ip*8]&0x04)<<1) + ((buf[ic*128+125-ip*8]&0x04) ) + ((buf[ic*128+126-ip*8]&0x04)>>1) + ((buf[ic*128+127-ip*8]&0x04)>>2)
bufr[ip*128+ic*8+1] = ((buf[ic*128+120-ip*8]&0x02)<<6) + ((buf[ic*128+121-ip*8]&0x02)<<5) + ((buf[ic*128+122-ip*8]&0x02)<<4) + ((buf[ic*128+123-ip*8]&0x02)<<3) + ((buf[ic*128+124-ip*8]&0x02)<<2) + ((buf[ic*128+125-ip*8]&0x02)<<1) + ((buf[ic*128+126-ip*8]&0x02) ) + ((buf[ic*128+127-ip*8]&0x02)>>1)
bufr[ip*128+ic*8+0] = ((buf[ic*128+120-ip*8]&0x01)<<7) + ((buf[ic*128+121-ip*8]&0x01)<<6) + ((buf[ic*128+122-ip*8]&0x01)<<5) + ((buf[ic*128+123-ip*8]&0x01)<<4) + ((buf[ic*128+124-ip*8]&0x01)<<3) + ((buf[ic*128+125-ip*8]&0x01)<<2) + ((buf[ic*128+126-ip*8]&0x01)<<1) + ((buf[ic*128+127-ip*8]&0x01) )
def copyFramebuf(self, buf = None):
'''copies a framebuffer to the Display so that the
contents gets displayed.'''
if not buf:
buf = self.buf
if self.landscape:
self.rotateFramebuffer( buf )
buf = self.bufr
# We need to write the data page by page (there are 16 pages for 16x8=128 pixels vertically)
# For every page we need to set the page and the start column address. Since we always transfer
# the entire display we set the column address to '0' (see data sheet for the commands used)
for ip in range(0, 16):
self._wr_cmd(0xB0 + ip) # page addr
self._wr_cmd(0x00) # col addr low
self._wr_cmd(0x10) # col addr high
# We copy the page data into a temporary buffer but the first byte will be 0x40
# This will indicate to the controller that all following bytes are Pixel data.
# (We should use a memoryview to avoid the copying of the framebuffer every time
# this would be more elegant...)
tmp = bytearray( 129 )
tmp[0] = 0x40
for i in range(0,128):
tmp[i+1] = buf[ip*128+i]
self.i2c.writeto( self.oled_adr, tmp )
def invert( self, inv ):
if inv:
self._wr_cmd( 0xA7 )
else:
self._wr_cmd( 0xA6 )