From 356848d7dbffad64920520295e8fb1065516a0f6 Mon Sep 17 00:00:00 2001 From: Richard Marko Date: Thu, 14 Jan 2016 02:31:00 +0100 Subject: [PATCH] Support SSD1306 over I2C Closes #11 --- gaugette/ssd1306.py | 196 +++++++++++------- .../{ssd1306_test.py => ssd1306_i2c_test.py} | 2 +- samples/ssd1306_spi_test.py | 53 +++++ 3 files changed, 179 insertions(+), 72 deletions(-) rename samples/{ssd1306_test.py => ssd1306_i2c_test.py} (92%) create mode 100755 samples/ssd1306_spi_test.py diff --git a/gaugette/ssd1306.py b/gaugette/ssd1306.py index 22fd442..972c597 100755 --- a/gaugette/ssd1306.py +++ b/gaugette/ssd1306.py @@ -2,7 +2,7 @@ # ssd1306.py from https://github.com/guyc/py-gaugette # ported by Guy Carpenter, Clearwater Software # -# This library works with +# This library works with # Adafruit's 128x32 SPI monochrome OLED http://www.adafruit.com/products/661 # Adafruit's 128x64 SPI monochrome OLED http://www.adafruit.com/products/326 # it should work with other SSD1306-based displays. @@ -27,14 +27,14 @@ # data in this case refers only to the display memory buffer. # keep D/C LOW for the command byte including any following argument bytes. # Pull D/C HIGH only when writting to the display memory buffer. -# +# # SPI and GPIO calls are made through an abstraction library that calls # the appropriate library for the platform. # For the RaspberryPi: # wiring2 # spidev # For the BeagleBone Black: -# Adafruit_BBIO.SPI +# Adafruit_BBIO.SPI # Adafruit_BBIO.GPIO # # - The pin connections between the BeagleBone Black SPI0 and OLED module are: @@ -51,13 +51,12 @@ #---------------------------------------------------------------------- import gaugette.platform -import gaugette.gpio import gaugette.spi -import gaugette.font5x8 +from gaugette.font5x8 import Font5x8 import time import sys -class SSD1306: +class SSD1306(object): # Class constants are externally accessible as gaugette.ssd1306.SSD1306.CONST # or my_instance.CONST @@ -66,7 +65,7 @@ class SSD1306: EXTERNAL_VCC = 0x1 SWITCH_CAP_VCC = 0x2 - + SET_LOW_COLUMN = 0x00 SET_HIGH_COLUMN = 0x10 SET_MEMORY_MODE = 0x20 @@ -102,53 +101,29 @@ class SSD1306: MEMORY_MODE_VERT = 0x01 MEMORY_MODE_PAGE = 0x02 - # Device name will be /dev/spidev-{bus}.{device} - # dc_pin is the data/commmand pin. This line is HIGH for data, LOW for command. - # We will keep d/c low and bump it high only for commands with data - # reset is normally HIGH, and pulled LOW to reset the display - - def __init__(self, bus=0, device=0, dc_pin="P9_15", reset_pin="P9_13", buffer_rows=64, buffer_cols=128, rows=32, cols=128): + def __init__(self, reset_pin="P9_13", buffer_rows=64, buffer_cols=128, rows=32, cols=128): self.cols = cols self.rows = rows self.buffer_rows = buffer_rows self.mem_bytes = self.buffer_rows * self.cols / 8 # total bytes in SSD1306 display ram - self.dc_pin = dc_pin self.reset_pin = reset_pin - self.spi = gaugette.spi.SPI(bus, device) - self.gpio = gaugette.gpio.GPIO() - self.gpio.setup(self.reset_pin, self.gpio.OUT) - self.gpio.output(self.reset_pin, self.gpio.HIGH) - self.gpio.setup(self.dc_pin, self.gpio.OUT) - self.gpio.output(self.dc_pin, self.gpio.LOW) - self.font = gaugette.font5x8.Font5x8 + if self.reset_pin: + import gaugette.gpio + self.gpio = gaugette.gpio.GPIO() + self.gpio.setup(self.reset_pin, self.gpio.OUT) + self.gpio.output(self.reset_pin, self.gpio.HIGH) + + self.font = Font5x8 self.col_offset = 0 self.bitmap = self.Bitmap(buffer_cols, buffer_rows) self.flipped = False def reset(self): - self.gpio.output(self.reset_pin, self.gpio.LOW) - time.sleep(0.010) # 10ms - self.gpio.output(self.reset_pin, self.gpio.HIGH) - - def command(self, *bytes): - # already low - # self.gpio.output(self.dc_pin, self.gpio.LOW) - self.spi.writebytes(list(bytes)) + if self.reset_pin: + self.gpio.output(self.reset_pin, self.gpio.LOW) + time.sleep(0.010) # 10ms + self.gpio.output(self.reset_pin, self.gpio.HIGH) - def data(self, bytes): - self.gpio.output(self.dc_pin, self.gpio.HIGH) - # chunk data to work around 255 byte limitation in adafruit implementation of writebytes - # revisit - change to 1024 when Adafruit_BBIO is fixed. - max_xfer = 255 if gaugette.platform.isBeagleBoneBlack else 1024 - start = 0 - remaining = len(bytes) - while remaining>0: - count = remaining if remaining <= max_xfer else max_xfer - remaining -= count - self.spi.writebytes(bytes[start:start+count]) - start += count - self.gpio.output(self.dc_pin, self.gpio.LOW) - def begin(self, vcc_state = SWITCH_CAP_VCC): time.sleep(0.001) # 1ms self.reset() @@ -157,12 +132,12 @@ def begin(self, vcc_state = SWITCH_CAP_VCC): # support for 128x32 and 128x64 line models if self.rows == 64: - self.command(self.SET_MULTIPLEX, 0x3F) + self.command(self.SET_MULTIPLEX, 0x3F) self.command(self.SET_COM_PINS, 0x12) else: self.command(self.SET_MULTIPLEX, 0x1F) self.command(self.SET_COM_PINS, 0x02) - + self.command(self.SET_DISPLAY_OFFSET, 0x00) self.command(self.SET_START_LINE | 0x00) if (vcc_state == self.EXTERNAL_VCC): @@ -181,7 +156,7 @@ def begin(self, vcc_state = SWITCH_CAP_VCC): self.command(self.DISPLAY_ALL_ON_RESUME) self.command(self.NORMAL_DISPLAY) self.command(self.DISPLAY_ON) - + def clear_display(self): self.bitmap.clear() @@ -220,7 +195,7 @@ def display_cols(self, start_col, count): # col: Starting col to write to. # col_count: Number of cols to write. # col_offset: column offset in buffer to write from - # + # def display_block(self, bitmap, row, col, col_count, col_offset=0): page_count = bitmap.rows >> 3 page_start = row >> 3 @@ -234,13 +209,13 @@ def display_block(self, bitmap, row, col, col_count, col_offset=0): length = col_count * page_count self.data(bitmap.data[start:start+length]) - # Diagnostic print of the memory buffer to stdout + # Diagnostic print of the memory buffer to stdout def dump_buffer(self): self.bitmap.dump() def draw_pixel(self, x, y, on=True): self.bitmap.draw_pixel(x,y,on) - + def draw_text(self, x, y, string): font_bytes = self.font.bytes font_rows = self.font.rows @@ -278,7 +253,7 @@ def draw_text2(self, x, y, string, size=2, space=1): def clear_block(self, x0,y0,dx,dy): self.bitmap.clear_block(x0,y0,dx,dy) - + def draw_text3(self, x, y, string, font): return self.bitmap.draw_text(x,y,string,font) @@ -286,22 +261,22 @@ def text_width(self, string, font): return self.bitmap.text_width(string, font) class Bitmap: - + # Pixels are stored in column-major order! # This makes it easy to reference a vertical slice of the display buffer - # and we use the to achieve reasonable performance vertical scrolling + # and we use the to achieve reasonable performance vertical scrolling # without hardware support. def __init__(self, cols, rows): self.rows = rows self.cols = cols self.bytes_per_col = rows / 8 self.data = [0] * (self.cols * self.bytes_per_col) - + def clear(self): for i in range(0,len(self.data)): self.data[i] = 0 - # Diagnostic print of the memory buffer to stdout + # Diagnostic print of the memory buffer to stdout def dump(self): for y in range(0, self.rows): mem_row = y/8 @@ -315,7 +290,7 @@ def dump(self): else: line += ' ' print('|'+line+'|') - + def draw_pixel(self, x, y, on=True): if (x<0 or x>=self.cols or y<0 or y>=self.rows): return @@ -323,12 +298,12 @@ def draw_pixel(self, x, y, on=True): mem_row = y / 8 bit_mask = 1 << (y % 8) offset = mem_row + self.rows/8 * mem_col - + if on: self.data[offset] |= bit_mask else: self.data[offset] &= (0xFF - bit_mask) - + def clear_block(self, x0,y0,dx,dy): for x in range(x0,x0+dx): for y in range(y0,y0+dy): @@ -350,16 +325,16 @@ def text_width(self, string, font): x += font.kerning[prev_char][pos] + font.gap_width prev_char = pos prev_width = width - + if prev_char != None: x += prev_width - + return x - + def draw_text(self, x, y, string, font): height = font.char_height prev_char = None - + for c in string: if (cfont.end_char): if prev_char != None: @@ -372,7 +347,7 @@ def draw_text(self, x, y, string, font): x += font.kerning[prev_char][pos] + font.gap_width prev_char = pos prev_width = width - + bytes_per_row = (width + 7) / 8 for row in range(0,height): py = y + row @@ -387,10 +362,10 @@ def draw_text(self, x, y, string, font): mask = 0x80 p+=1 offset += bytes_per_row - + if prev_char != None: x += prev_width - + return x # This is a helper class to display a scrollable list of text lines. @@ -418,10 +393,10 @@ def __init__(self, ssd1306, list, font): text_bitmap = ssd1306.Bitmap(width+15, self.rows) text_bitmap.draw_text(0,downset,text,font) self.bitmaps.append(text_bitmap) - + # display the first word in the first position self.ssd1306.display_block(self.bitmaps[0], 0, 0, self.cols) - + # how many steps to the nearest home position def align_offset(self): pos = self.position % self.rows @@ -439,12 +414,12 @@ def align(self, delay=0.005): time.sleep(delay) self.scroll(sign) return self.position / self.rows - + # scroll up or down. Does multiple one-pixel scrolls if delta is not >1 or <-1 def scroll(self, delta): if delta == 0: return - + count = len(self.list) step = cmp(delta, 0) for i in range(0,delta, step): @@ -460,7 +435,7 @@ def scroll(self, delta): self.ssd1306.command(self.ssd1306.SET_START_LINE | self.offset) max_position = count * self.rows self.position = (self.position + max_position + step) % max_position - + # pans the current row back and forth repeatedly. # Note that this currently only works if we are at a home position. def auto_pan(self): @@ -468,7 +443,7 @@ def auto_pan(self): if n != self.pan_row: self.pan_row = n self.pan_offset = 0 - + text_bitmap = self.bitmaps[n] if text_bitmap.cols > self.cols: row = self.offset # this only works if we are at a home position @@ -483,4 +458,83 @@ def auto_pan(self): else: self.pan_direction = 1 self.ssd1306.display_block(text_bitmap, row, 0, self.cols, self.pan_offset) - + + +class SSD1306_SPI(SSD1306): + # Device name will be /dev/spidev-{bus}.{device} + # dc_pin is the data/commmand pin. This line is HIGH for data, LOW for command. + # We will keep d/c low and bump it high only for commands with data. + # reset is normally HIGH, and pulled LOW to reset the display + + def __init__(self, bus=0, device=0, dc_pin=1, reset_pin=2, + buffer_rows=64, buffer_cols=128, + rows=32, cols=128): + self.dc_pin = dc_pin + self.spi = gaugette.spi.SPI(bus, device) + + super(SSD1306_SPI, self).__init__(reset_pin, + buffer_rows, buffer_cols, + rows, cols) + self.gpio.pinMode(self.dc_pin, self.gpio.OUTPUT) + self.gpio.digitalWrite(self.dc_pin, self.gpio.LOW) + + def command(self, *bytes): + # already low + # self.gpio.digitalWrite(self.dc_pin, self.gpio.LOW) + self.spi.writebytes(list(bytes)) + + def data(self, bytes): + self.gpio.digitalWrite(self.dc_pin, self.gpio.HIGH) + self.spi.writebytes(bytes) + self.gpio.digitalWrite(self.dc_pin, self.gpio.LOW) + + """ + def data(self, bytes): + self.gpio.output(self.dc_pin, self.gpio.HIGH) + # chunk data to work around 255 byte limitation in adafruit implementation of writebytes + # revisit - change to 1024 when Adafruit_BBIO is fixed. + max_xfer = 255 if gaugette.platform == 'beaglebone' else 1024 + start = 0 + remaining = len(bytes) + while remaining>0: + count = remaining if remaining <= max_xfer else max_xfer + remaining -= count + self.spi.writebytes(bytes[start:start+count]) + start += count + self.gpio.output(self.dc_pin, self.gpio.LOW) + """ + + +class SSD1306_I2C(SSD1306): + I2C_CONTROL = 0x00 + I2C_DATA = 0x40 + + I2C_BLOCKSIZE = 32 + + # Device name will be /dev/i2c-{bus}.{device} + # bus number is being ignored as Adafruit_I2C auto-detects correct bus + + def __init__(self, bus=0, address=0x3c, reset_pin=None, + buffer_rows=64, buffer_cols=128, + rows=32, cols=128): + + super(SSD1306_I2C, self).__init__(reset_pin, + buffer_rows, buffer_cols, + rows, cols) + + import smbus + self.i2c = smbus.SMBus(bus) + self.address = address + + def command(self, *bytes): + self.i2c.write_i2c_block_data(self.address, self.I2C_CONTROL, + list(bytes)) + + def data(self, bytes): + # not more than 32 bytes at a time + for chunk in zip(*[iter(list(bytes))] * self.I2C_BLOCKSIZE): + self.i2c.write_i2c_block_data(self.address, self.I2C_DATA, + list(chunk)) + # remaining bytes + rest = bytes[len(bytes) - len(bytes) % self.I2C_BLOCKSIZE:] + self.i2c.write_i2c_block_data(self.address, self.I2C_DATA, rest) diff --git a/samples/ssd1306_test.py b/samples/ssd1306_i2c_test.py similarity index 92% rename from samples/ssd1306_test.py rename to samples/ssd1306_i2c_test.py index d7fccee..404408c 100755 --- a/samples/ssd1306_test.py +++ b/samples/ssd1306_i2c_test.py @@ -12,7 +12,7 @@ DC_PIN = "P9_13" print("init") -led = gaugette.ssd1306.SSD1306(reset_pin=RESET_PIN, dc_pin=DC_PIN, rows=ROWS, cols=128) +led = gaugette.ssd1306.SSD1306_I2C(reset_pin=None, rows=ROWS, cols=128) print("begin") led.begin() print("clear") diff --git a/samples/ssd1306_spi_test.py b/samples/ssd1306_spi_test.py new file mode 100755 index 0000000..f010400 --- /dev/null +++ b/samples/ssd1306_spi_test.py @@ -0,0 +1,53 @@ +import gaugette.ssd1306 +import time +import sys + +ROWS = 32 + +if gaugette.platform.isRaspberryPi: + RESET_PIN = 15 + DC_PIN = 16 +else: # beagebone + RESET_PIN = "P9_15" + DC_PIN = "P9_13" + +print("init") +led = gaugette.ssd1306.SSD1306_SPI(reset_pin=RESET_PIN, dc_pin=DC_PIN, rows=ROWS, cols=128) +print("begin") +led.begin() +print("clear") +led.clear_display() +led.display() + +led.invert_display() +time.sleep(0.5) +led.normal_display() +time.sleep(0.5) + +offset = 0 # flips between 0 and 32 for double buffering + +while True: + + # write the current time to the display on every other cycle + if offset == 0: + text = time.strftime("%A") + print("draw") + led.draw_text2(0,0,text,2) + text = time.strftime("%e %b %Y") + print("draw") + led.draw_text2(0,16,text,2) + text = time.strftime("%X") + print("draw") + led.draw_text2(0,32+4,text,3) + print("display") + led.display() + time.sleep(0.2) + else: + time.sleep(0.5) + + # vertically scroll to switch between buffers + print("scroll") + for i in range(0,32): + offset = (offset + 1) % 64 + led.command(led.SET_START_LINE | offset) + time.sleep(0.01)