pulkin / micropython

MicroPython implementation on Ai-Thinker GPRS module A9 (RDA8955)
https://micropython.org
MIT License
102 stars 30 forks source link

SSD1306 i2c oled display library #47

Open iemmeti opened 4 years ago

iemmeti commented 4 years ago

I slightly modified adafruit-ssd1306.py oled display driver library for use with A9G pudding and non-standard I2C library.

I did some tests using hw i2c ports 2 and 3 and had no issues.

The main difference is how to init display:

oled = ssd1306_a9g.SSD1306_I2C(width, height, i2c_id, i2c_freq, addr)

where i2c_id is 2 or 3 and i2c_freq = clk frequency in kHz i.e oled = ssd1306_a9g.SSD1306_I2C(128, 64, 3, 400)

Hoping it could be useful to somebody.

# MicroPython SSD1306 OLED driver, I2C and SPI interfaces created by Adafruit
# ported to A9G pudding by iemmeti

import framebuf
import i2c
import time

# register definitions
SET_CONTRAST        = const(0x81)
SET_ENTIRE_ON       = const(0xa4)
SET_NORM_INV        = const(0xa6)
SET_DISP            = const(0xae)
SET_MEM_ADDR        = const(0x20)
SET_COL_ADDR        = const(0x21)
SET_PAGE_ADDR       = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP       = const(0xa0)
SET_MUX_RATIO       = const(0xa8)
SET_COM_OUT_DIR     = const(0xc0)
SET_DISP_OFFSET     = const(0xd3)
SET_COM_PIN_CFG     = const(0xda)
SET_DISP_CLK_DIV    = const(0xd5)
SET_PRECHARGE       = const(0xd9)
SET_VCOM_DESEL      = const(0xdb)
SET_CHARGE_PUMP     = const(0x8d)

# Timings (after some try and error)
CMD_TIMEOUT = 20
FB_TIMEOUT = 100

class SSD1306:
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        # Note the subclass must initialize self.framebuf to a framebuffer.
        # This is necessary because the underlying data buffer is different
        # between I2C and SPI implementations (I2C needs an extra byte).
        self.poweron()
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP | 0x00, # off
            # address setting
            SET_MEM_ADDR, 0x00, # horizontal
            # resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
            SET_MUX_RATIO, self.height - 1,
            SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
            SET_DISP_OFFSET, 0x00,
            SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV, 0x80,
            SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
            SET_VCOM_DESEL, 0x30, # 0.83*Vcc
            # display
            SET_CONTRAST, 0xff, # maximum
            SET_ENTIRE_ON, # output follows RAM contents
            SET_NORM_INV, # not inverted
            # charge pump
            SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01): # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)

    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        x0 = 0
        x1 = self.width - 1
        if self.width == 64:
            # displays with width of 64 pixels are shifted by 32
            x0 += 32
            x1 += 32
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_framebuf()

    def fill(self, col):
        self.framebuf.fill(col)

    def pixel(self, x, y, col):
        self.framebuf.pixel(x, y, col)

    def scroll(self, dx, dy):
        self.framebuf.scroll(dx, dy)

    def text(self, string, x, y, col=1):
        self.framebuf.text(string, x, y, col)

class SSD1306_I2C(SSD1306):
    # modified for A9G i2c library
    def __init__(self, width, height, i2c_id=3, i2c_freq = 100, addr=0x3C, external_vcc=False):
        self.i2c_id = i2c_id
        self.i2c_freq = i2c_freq
        i2c.init(i2c_id, i2c_freq)
        self.addr = addr
        self.temp = bytearray(2)
        # Add an extra byte to the data buffer to hold an I2C data/command byte
        # to use hardware-compatible I2C transactions.  A memoryview of the
        # buffer is used to mask this byte from the framebuffer operations
        # (without a major memory hit as memoryview doesn't copy to a separate
        # buffer).
        self.buffer = bytearray(((height // 8) * width) + 1)
        self.buffer[0] = 0x40  # Set first byte of data buffer to Co=0, D/C=1
        self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80 # Co=1, D/C#=0
        self.temp[1] = cmd
        # modified for A9G i2c library
        i2c.transmit(self.i2c_id, self.addr, self.temp, CMD_TIMEOUT)

    def write_framebuf(self):
        # Blast out the frame buffer using a single I2C transaction to support
        # hardware I2C interfaces.
        # modified for A9G i2c library
        i2c.transmit(self.i2c_id, self.addr, self.buffer, FB_TIMEOUT)

    def poweron(self):
        pass
pulkin commented 4 years ago

Awesome, thanks. Once #9 is fixed the library will be usable as-is. Are you interested in fixing it? An easy way would be to make i2c 🠞 _i2c and then to make a frozen wrapper i2c.py implementing the API.

iemmeti commented 4 years ago

I suspect to be not enough skilled for that, but I'll try for sure. At the moment I'm testing a GY-521 i2c accelerometer to investigate mem_transmit / mem_receive functions. When I'll feel comfortable with them, I'll start.

ens4dz commented 4 years ago

I managed to make it works with 16x2 Character LCD by your tips guys ^__^ https://imgur.com/e1kd8xs I changed some lines in esp8622 port code: https://github.com/ens4dz/python_lcd/commit/87d5072f5fb55478e07f013bdb59185be112b8db