[Solved] Larger fonts with OLED SH1106..?

Discussion about programs, libraries and tools that work with MicroPython. Mostly these are provided by a third party.
Target audience: All users and developers of MicroPython.
Post Reply
FeK9
Posts: 33
Joined: Sun Jan 20, 2019 8:19 am

[Solved] Larger fonts with OLED SH1106..?

Post by FeK9 » Sun Nov 07, 2021 12:38 pm

Hi All :)

I had a few SH1106 OLED's and I thought I'll try one with my Teensy 4.0.

Did a 'Hello World' with normal built-in font of micropythonl, it worked as expected
but found the 8x8 font a bit hard to see with these aging eyes... :)

Then I discovered 'MicroPython font handling' by @pythoncoder, but was not sure
if it would work with SH1106 OLED driver by @robert-hh and @deshipu...

This near novice to MicroPython did a hack/bodge and came up with the code shown below...

Code: Select all

import machine
from writer import Writer
from sh1106 import SH1106_I2C

# Font
import freesans20

WIDTH = const(128)
HEIGHT = const(64)

# Create the I2C interface.
i2c = machine.I2C(0)
ssd = SH1106_I2C(WIDTH, HEIGHT, i2c)
print("I2C Address      : "+hex(i2c.scan()[0]).upper()) # Display device address
print("I2C Configuration: "+str(i2c))                   # Display I2C config

wri = Writer(ssd, freesans20)
Writer.set_textpos(ssd, 0, 0)  # verbose = False to suppress console output
wri.printstring('Hello World\n')
ssd.show()
I have four files in the root of the Teensy file system, freesans20.py sh1106.py writer.py and
writer_demo.py file as shown above...

Below is the is the result, did a Ctrl D via Thonny IDE 3.3.14 console before executing
writer_demo.py ...

Code: Select all

MPY: soft reboot
MicroPython v1.17-135-gc9c55032d on 2021-11-05; Teensy 4.0 with MIMXRT1062DVJ6A
Type "help()" for more information.

>>> %Run -c $EDITOR_CONTENT

I2C Address      : 0X3C
I2C Configuration: I2C(0, freq=400000)
Traceback (most recent call last):
  File "<stdin>", line 17, in <module>
  File "writer.py", line 64, in __init__
  File "writer.py", line 39, in _get_id
ValueError: Device must be derived from FrameBuffer.

>>> 
Three errors showed, the 1st 'line 17' refers to my hack/bodge code shown above...

Code: Select all

wri = Writer(ssd, freesans20)
The next two come from writer.py, 'line 64' and 'line 39' are shown below...

Code: Select all

.....
        self.devid = _get_id(device)
.....
        raise ValueError('Device must be derived from FrameBuffer.')
.....
Is my bodge code above faulty, or the sh1106 driver unable to work with
'MicroPython font handling' by @pythoncoder for the moment...? :)
Last edited by FeK9 on Tue Nov 09, 2021 3:15 am, edited 2 times in total.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: Larger fonts with OLED SH1106..?

Post by pythoncoder » Sun Nov 07, 2021 12:53 pm

The Writer class will work with any display driver if it is designed such that the display class is a subclass of framebuf.FrameBuffer.

It is also possible to render my fonts with other display drivers, but the driver has to be designed to suit.

Unfortunately it sounds as if the SH1106 driver meets neither of those criteria.
Peter Hinch
Index to my micropython libraries.

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Larger fonts with OLED SH1106..?

Post by Roberthh » Sun Nov 07, 2021 1:02 pm

Unfortunately it sounds as if the SH1106 driver meets neither of those criteria.
That's indeed the fact. Could be changed.

User avatar
Roberthh
Posts: 3667
Joined: Sat May 09, 2015 4:13 pm
Location: Rhineland, Europe

Re: Larger fonts with OLED SH1106..?

Post by Roberthh » Sun Nov 07, 2021 1:45 pm

Below a copy of the changed driver. Tested with a sh1106 display and a Teensy 4.0. Do not forget to add pull-up resistors to SDC and SDA.

Code: Select all

#
# MicroPython SH1106 OLED driver, I2C and SPI interfaces
#
# The MIT License (MIT)
#
# Copyright (c) 2016 Radomir Dopieralski (@deshipu),
#               2017-2021 Robert Hammelrath (@robert-hh)
#               2021 Tim Weber (@scy)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Sample code sections for ESP8266 pin assignments
# ------------ SPI ------------------
# Pin Map SPI
#   - 3v - xxxxxx   - Vcc
#   - G  - xxxxxx   - Gnd
#   - D7 - GPIO 13  - Din / MOSI fixed
#   - D5 - GPIO 14  - Clk / Sck fixed
#   - D8 - GPIO 4   - CS (optional, if the only connected device)
#   - D2 - GPIO 5   - D/C
#   - D1 - GPIO 2   - Res
#
# for CS, D/C and Res other ports may be chosen.
#
# from machine import Pin, SPI
# import sh1106

# spi = SPI(1, baudrate=1000000)
# display = sh1106.SH1106_SPI(128, 64, spi, Pin(5), Pin(2), Pin(4))
# display.sleep(False)
# display.fill(0)
# display.text('Testing 1', 0, 0, 1)
# display.show()
#
# --------------- I2C ------------------
#
# Pin Map I2C
#   - 3v - xxxxxx   - Vcc
#   - G  - xxxxxx   - Gnd
#   - D2 - GPIO 5   - SCK / SCL
#   - D1 - GPIO 4   - DIN / SDA
#   - D0 - GPIO 16  - Res
#   - G  - xxxxxx     CS
#   - G  - xxxxxx     D/C
#
# Pin's for I2C can be set almost arbitrary
#
# from machine import Pin, I2C
# import sh1106
#
# i2c = I2C(scl=Pin(5), sda=Pin(4), freq=400000)
# display = sh1106.SH1106_I2C(128, 64, i2c, Pin(16), 0x3c)
# display.sleep(False)
# display.fill(0)
# display.text('Testing 1', 0, 0, 1)
# display.show()

from micropython import const
import utime as time
import framebuf


# a few register definitions
_SET_CONTRAST        = const(0x81)
_SET_NORM_INV        = const(0xa6)
_SET_DISP            = const(0xae)
_SET_SCAN_DIR        = const(0xc0)
_SET_SEG_REMAP       = const(0xa0)
_LOW_COLUMN_ADDRESS  = const(0x00)
_HIGH_COLUMN_ADDRESS = const(0x10)
_SET_PAGE_ADDRESS    = const(0xB0)


class SH1106(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc, rotate=0):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.flip_en = rotate == 180 or rotate == 270
        self.rotate90 = rotate == 90 or rotate == 270
        self.pages = self.height // 8
        self.bufsize = self.pages * self.width
        self.renderbuf = bytearray(self.bufsize)
        if self.rotate90:
            self.displaybuf = bytearray(self.bufsize)
            # HMSB is required to keep the bit order in the render buffer
            # compatible with byte-for-byte remapping to the display buffer,
            # which is in VLSB. Else we'd have to copy bit-by-bit!
            super().__init__(self.renderbuf, self.height, self.width,
                             framebuf.MONO_HMSB)
        else:
            self.displaybuf = self.renderbuf
            super().__init__(self.renderbuf, self.width, self.height,
                             framebuf.MONO_VLSB)

        # flip() was called rotate() once, provide backwards compatibility.
        self.rotate = self.flip
        self.init_display()

    def init_display(self):
        self.reset()
        self.fill(0)
        self.poweron()
        # rotate90 requires a call to flip() for setting up.
        self.flip(self.flip_en)

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

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

    def flip(self, flag=None, update=True):
        if flag is None:
            flag = not self.flip_en
        mir_v = flag ^ self.rotate90
        mir_h = flag
        self.write_cmd(_SET_SEG_REMAP | (0x01 if mir_v else 0x00))
        self.write_cmd(_SET_SCAN_DIR | (0x08 if mir_h else 0x00))
        self.flip_en = flag
        if update:
            self.show()

    def sleep(self, value):
        self.write_cmd(_SET_DISP | (not value))

    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):
        # self.* lookups in loops take significant time (~4fps).
        (w, p, db, rb) = (self.width, self.pages,
                          self.displaybuf, self.renderbuf)
        if self.rotate90:
            for i in range(self.bufsize):
                db[w * (i % p) + (i // p)] = rb[i]
        for page in range(self.height // 8):
            self.write_cmd(_SET_PAGE_ADDRESS | page)
            self.write_cmd(_LOW_COLUMN_ADDRESS | 2)
            self.write_cmd(_HIGH_COLUMN_ADDRESS | 0)
            self.write_data(db[(w*page):(w*page+w)])

    def reset(self, res):
        if res is not None:
            res(1)
            time.sleep_ms(1)
            res(0)
            time.sleep_ms(20)
            res(1)
            time.sleep_ms(20)


class SH1106_I2C(SH1106):
    def __init__(self, width, height, i2c, res=None, addr=0x3c,
                 rotate=0, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.res = res
        self.temp = bytearray(2)
        if res is not None:
            res.init(res.OUT, value=1)
        super().__init__(width, height, external_vcc, rotate)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_data(self, buf):
        self.i2c.writeto(self.addr, b'\x40'+buf)

    def reset(self):
        super().reset(self.res)


class SH1106_SPI(SH1106):
    def __init__(self, width, height, spi, dc, res=None, cs=None,
                 rotate=0, external_vcc=False):
        self.rate = 10 * 1000 * 1000
        dc.init(dc.OUT, value=0)
        if res is not None:
            res.init(res.OUT, value=0)
        if cs is not None:
            cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs
        super().__init__(width, height, external_vcc, rotate)

    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        if self.cs is not None:
            self.cs(1)
            self.dc(0)
            self.cs(0)
            self.spi.write(bytearray([cmd]))
            self.cs(1)
        else:
            self.dc(0)
            self.spi.write(bytearray([cmd]))

    def write_data(self, buf):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        if self.cs is not None:
            self.cs(1)
            self.dc(1)
            self.cs(0)
            self.spi.write(buf)
            self.cs(1)
        else:
            self.dc(1)
            self.spi.write(buf)

    def reset(self):
        super().reset(self.res)

FeK9
Posts: 33
Joined: Sun Jan 20, 2019 8:19 am

Re: Larger fonts with OLED SH1106..?

Post by FeK9 » Tue Nov 09, 2021 3:05 am

Thanks @Roberthh :)

The update driver you posted above works like a charm with
@peterhinch font-to-py Writer class...

I'll can now experiment tinker a little with nano-gui and micro-gui
libraries, out curiosity... :)

Talking of pull-resisters, I'm not using any... I need to take a closure
look at the back of the i2c only sh1106 OLED display, maybe a pair are
there...

Thanks again Guys... :)

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: [Solved] Larger fonts with OLED SH1106..?

Post by pythoncoder » Sun Nov 21, 2021 10:23 am

Did you have success with nano-gui or micro-gui?
Peter Hinch
Index to my micropython libraries.

FeK9
Posts: 33
Joined: Sun Jan 20, 2019 8:19 am

Re: [Solved] Larger fonts with OLED SH1106..?

Post by FeK9 » Wed Nov 24, 2021 10:12 am

pythoncoder wrote:
Sun Nov 21, 2021 10:23 am
Did you have success with nano-gui or micro-gui?
Hi Peter

Been distracted with your Writer class and font_to_py, been converting fonts that I find
and resizing them finding what I like.

Using them with the Writer class and the SH1106 i2c with a BMP180 & DHT22
sensors, font_to_py works like a charm... :)

To answer your question yes, I did manage to get the 'mono_test.py' example
to work with the SH1106 driver by Roberthh. Had to mod the color_setup.py
file, and renamed it. I also comment out the line 12 "from gui.core.colors import *"
found in the nanogui.py file.

I've moved to using a Pi Pico for the moment, will try it on the Teensy 4.0 later. :)

Here's my setup.py...

Code: Select all

# color_setup.py Customise for your hardware config

# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2020 Peter Hinch

# As written, supports:
# Adafruit 1.5" 128*128 OLED display: https://www.adafruit.com/product/1431
# Adafruit 1.27" 128*96 display https://www.adafruit.com/product/1673
# Edit the driver import for other displays.

# Demo of initialisation procedure designed to minimise risk of memory fail
# when instantiating the frame buffer. The aim is to do this as early as
# possible before importing other modules.

import machine
import gc

from drivers.sh1106.sh1106 import SH1106_I2C
WIDTH = const(128)
HEIGHT = const(64)

gc.collect()  # Precaution before instantiating framebuf

sda=machine.Pin(0)
scl=machine.Pin(1)
i2c=machine.I2C(0,sda=sda, scl=scl, freq=400000)
ssd = SH1106_I2C(WIDTH, HEIGHT, i2c)
ssd.rotate(True)
ssd.fill(0)

Post Reply