add sh1106 driver and update docs and create config sample

pull/48/head
Joe Freeman 2023-02-12 15:36:05 -05:00
rodzic 112bd48c72
commit 41d695b4d7
4 zmienionych plików z 396 dodań i 23 usunięć

Wyświetl plik

@ -48,6 +48,13 @@ I2C or SPI interfaces. An example is [Adafruit 938][4m]. The SSD1306 driver in
this repo has a minor addition to enable it to support demos intended for color this repo has a minor addition to enable it to support demos intended for color
displays. displays.
Monochrome OLED displays based on the SH1106 chip are supported via the
[unofficial driver](https://github.com/robert-hh/SH1106).
Displays are available from various sources and can use I2C or SPI interfaces.
An exmaple is the [Inland IIC SPI 1.3" 128x64 OLED V2.0](https://www.microcenter.com/product/643965/inland-iic-spi-13-128x64-oled-v20-graphic-display-module-for-arduino-uno-r3)
AKA [KeyStudio](https://wiki.keyestudio.com/Ks0056_keyestudio_1.3%22_128x64_OLED_Graphic_Display).
Nokia 5110 (PCD8544) displays. [This driver](https://github.com/mcauser/micropython-pcd8544.git) Nokia 5110 (PCD8544) displays. [This driver](https://github.com/mcauser/micropython-pcd8544.git)
is compatible. is compatible.

Wyświetl plik

@ -113,6 +113,7 @@ my GUI's employ the American spelling of `color`.
## 1.1 Change log ## 1.1 Change log
12 Feb 2023 Add support for sh1106 driver.
5 Sep 2022 Add support for additional Pico displays. 5 Sep 2022 Add support for additional Pico displays.
8 Aug 2022 Typo and grammar fixes from @bfiics. 8 Aug 2022 Typo and grammar fixes from @bfiics.
10 May 2022 Support Waveshare Pi Pico displays. 10 May 2022 Support Waveshare Pi Pico displays.
@ -252,12 +253,12 @@ The `gui/core` directory contains the GUI core and its principal dependencies:
The `gui/demos` directory contains test/demo scripts. The `gui/demos` directory contains test/demo scripts.
Demos for small displays: Demos for small displays:
* `mono_test.py` Tests/demos using the official SSD1306 driver for a * `mono_test.py` Tests/demos using the official SSD1306 or SH1106 driver for
monochrome 128*64 OLED display. monochrome 128*64 OLED displays.
* `color96.py` Tests/demos for the Adafruit 0.96 inch color OLED. * `color96.py` Tests/demos for the Adafruit 0.96 inch color OLED.
Demos for larger displays. Demos for larger displays.
* `color15.py` Demonstrates a variety of widgets. Cross platform. * `color15.py` Demonstrates a variety of widgets. Cross platform.
* `aclock.py` Analog clock demo. Cross platform. * `aclock.py` Analog clock demo. Cross platform.
* `alevel.py` Spirit level using Pyboard accelerometer. * `alevel.py` Spirit level using Pyboard accelerometer.
@ -267,7 +268,7 @@ Demos for larger displays.
* `asnano.py` Could readily be adapted for other targets. * `asnano.py` Could readily be adapted for other targets.
* `tbox.py` Demo `Textbox` class. Cross-platform. * `tbox.py` Demo `Textbox` class. Cross-platform.
Demos for ePaper displays: Demos for ePaper displays:
* `epd_async.py` Demo of asynchronous code on an eInk display. Needs a large display. * `epd_async.py` Demo of asynchronous code on an eInk display. Needs a large display.
* `epd29_sync.py` Demo for Adafruit 2.9" eInk display: emulates a seismograph. * `epd29_sync.py` Demo for Adafruit 2.9" eInk display: emulates a seismograph.
* `epd29_async.py` Asynchronous demo for Adafruit 2.9" eInk display. * `epd29_async.py` Asynchronous demo for Adafruit 2.9" eInk display.
@ -275,7 +276,7 @@ Demos for ePaper displays:
[Micropower use](./DRIVERS.md#715-micropower-use) should be read before [Micropower use](./DRIVERS.md#715-micropower-use) should be read before
attempting to run this. attempting to run this.
Demos for Sharp displays: Demos for Sharp displays:
* `sharptest.py` Basic functionality check. * `sharptest.py` Basic functionality check.
* `clocktest.py` Digital and analog clock demo. * `clocktest.py` Digital and analog clock demo.
* `clock_batt.py` Low power demo of battery operated clock. * `clock_batt.py` Low power demo of battery operated clock.
@ -317,6 +318,8 @@ copied to the hardware root as `color_setup.py`. Example files:
* `ssd1306_pyb.py` Setup file for monochrome displays using the official * `ssd1306_pyb.py` Setup file for monochrome displays using the official
driver. Supports hard or soft SPI or I2C connections. driver. Supports hard or soft SPI or I2C connections.
* `ssd1106_spi_pico.py` Setup file for monochrome displays.
Supports hard or soft SPI or I2C connections.
* `ssd1351_esp32.py` As written supports an ESP32 connected to a 128x128 SSD1351 * `ssd1351_esp32.py` As written supports an ESP32 connected to a 128x128 SSD1351
display. After editing to match the display and wiring, it should be copied to display. After editing to match the display and wiring, it should be copied to
the target as `/pyboard/color_setup.py`. the target as `/pyboard/color_setup.py`.
@ -342,11 +345,15 @@ to check for newer versions:
Provides text rendering of Python font files. Provides text rendering of Python font files.
A copy of the official driver for OLED displays using the SSD1306 chip is A copy of the official driver for OLED displays using the SSD1306 chip is
provided. The official file is here: provided. The official file is here:
* [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). * [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py).
A copy of the unofficial driver for OLED displays using the SH1106 chip is
provided. The unofficial file is here:
* [Sh1106 driver](https://github.com/robert-hh/SH1106).
Displays based on the Nokia 5110 (PCD8544 chip) require this driver. It is not Displays based on the Nokia 5110 (PCD8544 chip) require this driver. It is not
in this repo but may be found here: in this repo but may be found here:
* [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git) * [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git)
###### [Contents](./README.md#contents) ###### [Contents](./README.md#contents)
@ -404,11 +411,12 @@ from gui.widgets.label import Label # Import any widgets you plan to use
from gui.widgets.dial import Dial, Pointer from gui.widgets.dial import Dial, Pointer
refresh(ssd, True) # Initialise and clear display. refresh(ssd, True) # Initialise and clear display.
``` ```
Initialisation of text display follows. For each font a `CWriter` instance
is created (for monochrome displays a `Writer` is used): Initialisation of color text display follows. For each font a `CWriter` instance
is created:
```python ```python
from gui.core.writer import CWriter # Renders color text from gui.core.writer import CWriter # Renders color text
import gui.fonts.arial10 # A Python Font from gui.fonts import arial10 # A Python Font
from gui.core.colors import * # Standard color constants from gui.core.colors import * # Standard color constants
CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it
@ -417,6 +425,19 @@ wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False) # Colors are defaults
# wri = Writer(ssd, arial10, verbose=False) # Monochrome display uses Writer # wri = Writer(ssd, arial10, verbose=False) # Monochrome display uses Writer
wri.set_clip(True, True, False) wri.set_clip(True, True, False)
``` ```
Initialisation of monochorome text display follows. For each font a `Writer` instance
is created:
```python
from gui.core.writer import Writer # Renders color text
from gui.fonts import arial10
Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it
# Instantiate any Writers to be used (one for each font)
wri = Writer(ssd, arial10, verbose=False) # Monochrome display uses Writer
wri.set_clip(True, True, False)
```
Calling `nanogui.refresh` on startup sets up and clears the display. The method Calling `nanogui.refresh` on startup sets up and clears the display. The method
will subsequently be called whenever a refresh is required. It takes two args: will subsequently be called whenever a refresh is required. It takes two args:
1. `device` The display instance (the GUI supports multiple displays). 1. `device` The display instance (the GUI supports multiple displays).
@ -488,7 +509,7 @@ Colors are handled flexibly. By default the colors used are those of the
to warn of overrange or underrange values. The `color15.py` demo illustrates to warn of overrange or underrange values. The `color15.py` demo illustrates
this. this.
Constructor args: Constructor args:
1. `writer` The `Writer` instance (font and screen) to use. 1. `writer` The `Writer` instance (font and screen) to use.
2. `row` Location on screen. 2. `row` Location on screen.
3. `col` 3. `col`
@ -507,7 +528,7 @@ Constructor args:
The constructor displays the string at the required location. The constructor displays the string at the required location.
Methods: Methods:
1. `value` Redraws the label. This takes the following args: 1. `value` Redraws the label. This takes the following args:
* `text=None` The text to display. If `None` displays the last value. * `text=None` The text to display. If `None` displays the last value.
* ` invert=False` If true, show inverse text. * ` invert=False` If true, show inverse text.
@ -520,7 +541,7 @@ Methods:
Returns the current text string. Returns the current text string.
2. `show` No args. (Re)draws the label. Primarily for internal use by GUI. 2. `show` No args. (Re)draws the label. Primarily for internal use by GUI.
Module Constants: Module Constants:
* `ALIGN_LEFT=0` * `ALIGN_LEFT=0`
* `ALIGN_RIGHT=1` * `ALIGN_RIGHT=1`
* `ALIGN_CENTER=2` * `ALIGN_CENTER=2`
@ -579,22 +600,22 @@ Keyword only args:
`('0.0', '0.5', '1.0')` `('0.0', '0.5', '1.0')`
14. `value=None` Initial value. If `None` the meter will not be drawn until 14. `value=None` Initial value. If `None` the meter will not be drawn until
its `value()` method is called. its `value()` method is called.
Methods: Methods:
1. `value` Args: `n=None, color=None`. 1. `value` Args: `n=None, color=None`.
* `n` should be a float in range 0 to 1.0. Causes the meter to be updated. * `n` should be a float in range 0 to 1.0. Causes the meter to be updated.
Out of range values are constrained. If `None` is passed the meter is not Out of range values are constrained. If `None` is passed the meter is not
updated. updated.
* `color` Updates the color of the bar or line if a value is also passed. * `color` Updates the color of the bar or line if a value is also passed.
`None` causes no change. `None` causes no change.
Returns the current value. Returns the current value.
2. `text` Updates the label if present (otherwise throws a `ValueError`). Args: 2. `text` Updates the label if present (otherwise throws a `ValueError`). Args:
* `text=None` The text to display. If `None` displays the last value. * `text=None` The text to display. If `None` displays the last value.
* ` invert=False` If true, show inverse text. * ` invert=False` If true, show inverse text.
* `fgcolor=None` Foreground color: if `None` the `Writer` default is used. * `fgcolor=None` Foreground color: if `None` the `Writer` default is used.
* `bgcolor=None` Background color, as per foreground. * `bgcolor=None` Background color, as per foreground.
* `bdcolor=None` Border color. As per above except that if `False` is * `bdcolor=None` Border color. As per above except that if `False` is
passed, no border is displayed. This clears a previously drawn border. passed, no border is displayed. This clears a previously drawn border.
3. `show` No args. (Re)draws the meter. Primarily for internal use by GUI. 3. `show` No args. (Re)draws the meter. Primarily for internal use by GUI.
###### [Contents](./README.md#contents) ###### [Contents](./README.md#contents)
@ -603,7 +624,7 @@ Methods:
This is a virtual LED whose color may be altered dynamically. This is a virtual LED whose color may be altered dynamically.
Constructor positional args: Constructor positional args:
1. `writer` The `Writer` instance (font and screen) to use. 1. `writer` The `Writer` instance (font and screen) to use.
2. `row` Location on screen. 2. `row` Location on screen.
3. `col` 3. `col`
@ -627,7 +648,7 @@ Methods:
* `fgcolor=None` Foreground color: if `None` the `Writer` default is used. * `fgcolor=None` Foreground color: if `None` the `Writer` default is used.
* `bgcolor=None` Background color, as per foreground. * `bgcolor=None` Background color, as per foreground.
* `bdcolor=None` Border color. As per above except that if `False` is * `bdcolor=None` Border color. As per above except that if `False` is
passed, no border is displayed. This clears a previously drawn border. passed, no border is displayed. This clears a previously drawn border.
3. `show` No args. (Re)draws the LED. Primarily for internal use by GUI. 3. `show` No args. (Re)draws the LED. Primarily for internal use by GUI.
###### [Contents](./README.md#contents) ###### [Contents](./README.md#contents)
@ -652,7 +673,7 @@ Pointer values are complex numbers.
### Dial class ### Dial class
Constructor positional args: Constructor positional args:
1. `writer` The `Writer` instance (font and screen) to use. 1. `writer` The `Writer` instance (font and screen) to use.
2. `row` Location on screen. 2. `row` Location on screen.
3. `col` 3. `col`
@ -684,12 +705,12 @@ Constructor arg:
1. `dial` The `Dial` instance on which it is to be displayed. 1. `dial` The `Dial` instance on which it is to be displayed.
Methods: Methods:
1. `value` Args: 1. `value` Args:
* `v=None` The value is a complex number. A magnitude exceeding unity is * `v=None` The value is a complex number. A magnitude exceeding unity is
reduced (preserving phase) to constrain the `Pointer` within the unit reduced (preserving phase) to constrain the `Pointer` within the unit
circle. circle.
* `color=None` By default the pointer is rendered in the foreground color * `color=None` By default the pointer is rendered in the foreground color
of the parent `Dial`. Otherwise the passed color is used. of the parent `Dial`. Otherwise the passed color is used.
Returns the current value. Returns the current value.
2. `show` No args. (Re)draws the control. Primarily for internal use by GUI. 2. `show` No args. (Re)draws the control. Primarily for internal use by GUI.

Wyświetl plik

@ -0,0 +1,330 @@
# Copied from https://github.com/robert-hh/SH1106
#
# 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)
# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SH1106(framebuf.FrameBuffer):
@staticmethod
def rgb(r, g, b):
return int((r > 127) or (g > 127) or (b > 127))
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)
self.pages_to_update = 0
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.show()
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)
if self.delay:
time.sleep_ms(self.delay)
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(True) # full update
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, full_update=False):
# 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]
if full_update:
pages_to_update = (1 << self.pages) - 1
else:
pages_to_update = self.pages_to_update
# print("Updating pages: {:08b}".format(pages_to_update))
for page in range(self.pages):
if pages_to_update & (1 << page):
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)])
self.pages_to_update = 0
def pixel(self, x, y, color=None):
if color is None:
return super().pixel(x, y)
else:
super().pixel(x, y, color)
page = y // 8
self.pages_to_update |= 1 << page
def text(self, text, x, y, color=1):
super().text(text, x, y, color)
self.register_updates(y, y + 7)
def line(self, x0, y0, x1, y1, color):
super().line(x0, y0, x1, y1, color)
self.register_updates(y0, y1)
def hline(self, x, y, w, color):
super().hline(x, y, w, color)
self.register_updates(y)
def vline(self, x, y, h, color):
super().vline(x, y, h, color)
self.register_updates(y, y + h - 1)
def fill(self, color):
super().fill(color)
self.pages_to_update = (1 << self.pages) - 1
def blit(self, fbuf, x, y, key=-1, palette=None):
super().blit(fbuf, x, y, key, palette)
self.register_updates(y, y + self.height)
def scroll(self, x, y):
# my understanding is that scroll() does a full screen change
super().scroll(x, y)
self.pages_to_update = (1 << self.pages) - 1
def fill_rect(self, x, y, w, h, color):
super().fill_rect(x, y, w, h, color)
self.register_updates(y, y + h - 1)
def rect(self, x, y, w, h, color):
super().rect(x, y, w, h, color)
self.register_updates(y, y + h - 1)
def register_updates(self, y0, y1=None):
# this function takes the top and optional bottom address of the changes made
# and updates the pages_to_change list with any changed pages
# that are not yet on the list
start_page = max(0, y0 // 8)
end_page = max(0, y1 // 8) if y1 is not None else start_page
# rearrange start_page and end_page if coordinates were given from bottom to top
if start_page > end_page:
start_page, end_page = end_page, start_page
for page in range(start_page, end_page + 1):
self.pages_to_update |= 1 << page
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,
delay=0,
):
self.i2c = i2c
self.addr = addr
self.res = res
self.temp = bytearray(2)
self.delay = delay
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,
delay=0,
):
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
self.delay = delay
super().__init__(width, height, external_vcc, rotate)
def write_cmd(self, cmd):
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):
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)

Wyświetl plik

@ -0,0 +1,15 @@
from machine import Pin, SPI
from drivers.sh1106.sh1106 import SH1106_SPI as SSD
oled_width = 128
oled_height = 64
# Incorporating the Pico pin names into the variable names
sck_clk = Pin(14)
tx_mosi = Pin(15)
rx_miso_dc = Pin(12)
csn_cs = Pin(13)
oled_spi = SPI(1, sck=sck_clk, mosi=tx_mosi, miso=rx_miso_dc)
ssd = SSD(oled_width, oled_height, oled_spi, dc=rx_miso_dc, cs=csn_cs)