Skip to content

Commit

Permalink
Add 8-bit gc9a01 driver.
Browse files Browse the repository at this point in the history
  • Loading branch information
peterhinch committed May 23, 2024
1 parent 2fdabc5 commit 108f3d9
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 1 deletion.
1 change: 0 additions & 1 deletion drivers/gc9a01/gc9a01.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ def _wcd(self, command, data):
self._spi.write(data)
self._cs(1)

# @micropython.native # Made almost no difference to timing
def show(self): # Physical display is in portrait mode
clut = GC9A01.lut
lb = self._linebuf
Expand Down
216 changes: 216 additions & 0 deletions drivers/gc9a01/gc9a01_8_bit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# gc9a01_8_bit.py nano-gui driver for gc9a01 displays using 8 bit pixels
# Default args are for a 240*240 (typically circular) display. This will result
# in a 57,600 byte frame buffer.

# Copyright (c) Peter Hinch 2024
# Released under the MIT license see LICENSE

from time import sleep_ms
import gc
import framebuf
import asyncio
from drivers.boolpalette import BoolPalette

# Initialisation ported from Russ Hughes' C driver
# https://github.com/russhughes/gc9a01_mpy/
# Based on a ST7789 C driver: https://github.com/devbis/st7789_mpy
# Many registers are undocumented. Lines initialising them are commented "?"
# in cases where initialising them seems to have no effect.

# Datasheet 7.3.4 allows scl <= 100MHz
# Waveshare touch board https://www.waveshare.com/wiki/1.28inch_Touch_LCD has CST816S touch controller
# Touch controller uses I2C

# g4 g3 g2 b7 b6 b5 b4 b3 r7 r6 r5 r4 r3 g7 g6 g5
@micropython.viper
def _lcopy(dest: ptr16, source: ptr8, length: int, gscale: bool):
# rgb565 - 16bit/pixel
n: int = 0
while length:
c = source[n]
if gscale: # Source byte holds 8-bit greyscale
# dest rrrr rggg gggb bbbb
dest[n] = (c & 0xF1) | (c >> 5) | ((c & 0x1C) << 11) | ((c & 0xF1) << 5)
else: # Source byte holds 8-bit rrrgggbb
# dest 000b b000 rrr0 0ggg
dest[n] = (c & 0xE0) | ((c & 0x1C) >> 2) | ((c & 0x03) << 11)
n += 1
length -= 1


class GC9A01(framebuf.FrameBuffer):
# Convert r, g, b in range 0-255 to an 8 bit colour value
# rrrgggbb. Converted to 16 bit on the fly.
# GC9A01 expects RGB order.
@staticmethod
def rgb(r, g, b):
return (r & 0xE0) | ((g >> 3) & 0x1C) | (b >> 6)

def __init__(
self,
spi,
cs,
dc,
rst,
height=240,
width=240,
lscape=False,
usd=False,
mirror=False,
init_spi=False,
):
self._spi = spi
self._cs = cs
self._dc = dc
self._rst = rst
self.height = height # Logical dimensions for GUIs
self.width = width
self._spi_init = init_spi
self._gscale = False # Interpret buffer as rrrgggbb color
mode = framebuf.GS8 # Use 8bit greyscale for 8 bit color.
self.palette = BoolPalette(mode)
gc.collect()
buf = bytearray(height * width) # Frame buffer
self._mvb = memoryview(buf)
super().__init__(buf, width, height, mode)
self._linebuf = bytearray(width * 2) # Line buffer (16-bit colors)

# Hardware reset
self._rst(0)
sleep_ms(50)
self._rst(1)
sleep_ms(50)
if self._spi_init: # A callback was passed
self._spi_init(spi) # Bus may be shared
self._lock = asyncio.Lock() # Prevent concurrent refreshes.
sleep_ms(100)
self._wcd(b"\x2a", int.to_bytes(width - 1, 4, "big"))
# Default page address start == 0 end == 0xEF (239)
self._wcd(b"\x2b", int.to_bytes(height - 1, 4, "big")) # SET_PAGE ht
# **** Start of opaque chip setup ****
self._wcmd(b"\xEF") # Inter register enable 2
self._wcd(b"\xEB", b"\x14") # ?
self._wcmd(b"\xFE") # Inter register enable 1
self._wcmd(b"\xEF") # Inter register enable 2
self._wcd(b"\xEB", b"\x14") # ?
self._wcd(b"\x84", b"\x40") # ?
self._wcd(b"\x85", b"\xFF") # ?
self._wcd(b"\x87", b"\xFF") # ?
self._wcd(b"\x86", b"\xFF") # ?
self._wcd(b"\x88", b"\x0A") # ?
self._wcd(b"\x89", b"\x21") # ?
self._wcd(b"\x8A", b"\x00") # ?
self._wcd(b"\x8B", b"\x80") # ?
self._wcd(b"\x8C", b"\x01") # ?
self._wcd(b"\x8D", b"\x01") # ?
self._wcd(b"\x8E", b"\xFF") # ?
self._wcd(b"\x8F", b"\xFF") # ?
self._wcd(b"\xB6", b"\x00\x00") # Display function control
self._wcd(b"\x3A", b"\x55") # COLMOD
self._wcd(b"\x90", b"\x08\x08\x08\x08") # ?
self._wcd(b"\xBD", b"\x06") # ?
self._wcd(b"\xBC", b"\x00") # ?
self._wcd(b"\xFF", b"\x60\x01\x04") # ?
self._wcd(b"\xC3", b"\x13") # Vreg1a voltage Control
self._wcd(b"\xC4", b"\x13") # Vreg1b voltage Control
self._wcd(b"\xC9", b"\x22") # Vreg2a voltage Control
self._wcd(b"\xBE", b"\x11") # ?
self._wcd(b"\xE1", b"\x10\x0E") # ?
self._wcd(b"\xDF", b"\x21\x0c\x02") # ?
self._wcd(b"\xF0", b"\x45\x09\x08\x08\x26\x2A") # Gamma
self._wcd(b"\xF1", b"\x43\x70\x72\x36\x37\x6F") # Gamma
self._wcd(b"\xF2", b"\x45\x09\x08\x08\x26\x2A") # Gamma
self._wcd(b"\xF3", b"\x43\x70\x72\x36\x37\x6F") # Gamma
self._wcd(b"\xED", b"\x1B\x0B") # ?
self._wcd(b"\xAE", b"\x77") # ?
self._wcd(b"\xCD", b"\x63") # ?
self._wcd(b"\x70", b"\x07\x07\x04\x0E\x0F\x09\x07\x08\x03") # ?
self._wcd(b"\xE8", b"\x34") # Frame rate / dot inversion
self._wcd(b"\x62", b"\x18\x0D\x71\xED\x70\x70\x18\x0F\x71\xEF\x70\x70") # ?
self._wcd(b"\x63", b"\x18\x11\x71\xF1\x70\x70\x18\x13\x71\xF3\x70\x70") # ?
self._wcd(b"\x64", b"\x28\x29\xF1\x01\xF1\x00\x07") # ?
self._wcd(b"\x66", b"\x3C\x00\xCD\x67\x45\x45\x10\x00\x00\x00") # Undoc but needed
self._wcd(b"\x67", b"\x00\x3C\x00\x00\x00\x01\x54\x10\x32\x98") # Undoc but needed
self._wcd(b"\x74", b"\x10\x85\x80\x00\x00\x4E\x00") # ?
self._wcd(b"\x98", b"\x3e\x07") # ?
self._wcmd(b"\x35") # Tearing effect line on
self._wcmd(b"\x21") # Display inversion on ???
self._wcmd(b"\x11")
sleep_ms(120)
# *************************

# madctl reg 0x36 p127 6.2.18. b0-2 == 0. b3: color output BGR RGB/
# b4 == 0
# d5 row/col exchange
# d6 col address order
# d7 row address order
if lscape:
madctl = 0x28 if usd else 0xE8 # RGB landscape mode
else:
madctl = 0x48 if usd else 0x88 # RGB portrait mode
if mirror:
madctl ^= 0x80
self._wcd(b"\x36", madctl.to_bytes(1, "big")) # MADCTL: RGB portrait mode
self._wcmd(b"\x29") # display on

# Write a command.
def _wcmd(self, command):
self._dc(0)
self._cs(0)
self._spi.write(command)
self._cs(1)

# Write a command followed by a data arg.
def _wcd(self, command, data):
self._dc(0)
self._cs(0)
self._spi.write(command)
self._cs(1)
self._dc(1)
self._cs(0)
self._spi.write(data)
self._cs(1)

def greyscale(self, gs=None):
if gs is not None:
self._gscale = gs
return self._gscale

def show(self): # Physical display is in portrait mode
lb = self._linebuf
buf = self._mvb
if self._spi_init: # A callback was passed
self._spi_init(self._spi) # Bus may be shared
self._wcmd(b"\x2c") # WRITE_RAM
self._dc(1)
self._cs(0)
wd = self.width
ht = self.height
cm = self._gscale # color False, greyscale True
for start in range(0, wd * ht, wd): # For each line
_lcopy(lb, buf[start:], wd, cm) # Copy and map colors
self._spi.write(lb)
self._cs(1)

async def do_refresh(self, split=4):
async with self._lock:
lines, mod = divmod(self.height, split) # Lines per segment
if mod:
raise ValueError("Invalid do_refresh arg.")
lb = self._linebuf
buf = self._mvb
self._wcmd(b"\x2c") # WRITE_RAM
self._dc(1)
wd = self.width
cm = self._gscale # color False, greyscale True
line = 0
for _ in range(split): # For each segment
if self._spi_init: # A callback was passed
self._spi_init(self._spi) # Bus may be shared
self._cs(0)
for start in range(wd * line, wd * (line + lines), wd): # For each line
_lcopy(lb, buf[start:], wd, cm) # Copy and map colors
self._spi.write(lb)
line += lines
self._cs(1) # Allow other tasks to use bus
await asyncio.sleep_ms(0)
8 changes: 8 additions & 0 deletions drivers/gc9a01/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"urls": [
["drivers/gc9a01/gc9a01.py", "github:peterhinch/micropython-nano-gui/drivers/gc9a01/gc9a01.py"],
["drivers/gc9a01/gc9a01_8_bit.py", "github:peterhinch/micropython-nano-gui/drivers/gc9a01/gc9a01_8_bit.py"],
["drivers/boolpalette.py", "github:peterhinch/micropython-nano-gui/drivers/boolpalette.py"]
],
"version": "0.1"
}
89 changes: 89 additions & 0 deletions gui/demos/round.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# round.py Test/demo of scale widget for nano-gui on round gc9a01 screen

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

# Usage:
# import gui.demos.round

# Initialise hardware and framebuf before importing modules.
# Uses asyncio and also the asynchronous do_refresh method if the driver
# supports it.

from color_setup import ssd # Create a display instance

from gui.core.nanogui import refresh
from gui.core.writer import CWriter

import asyncio
from gui.core.colors import *
import gui.fonts.arial10 as arial10
from gui.widgets.label import Label
from gui.widgets.scale import Scale

# COROUTINES
async def radio(scale):
cv = 88.0 # Current value
val = 108.0 # Target value
while True:
v1, v2 = val, cv
steps = 200
delta = (val - cv) / steps
for _ in range(steps):
cv += delta
# Map user variable to -1.0..+1.0
scale.value(2 * (cv - 88) / (108 - 88) - 1)
await asyncio.sleep_ms(200)
val, cv = v2, v1


async def default(scale, lbl):
cv = -1.0 # Current
val = 1.0
while True:
v1, v2 = val, cv
steps = 400
delta = (val - cv) / steps
for _ in range(steps):
cv += delta
scale.value(cv)
lbl.value("{:4.3f}".format(cv))
if hasattr(ssd, "do_refresh"):
# Option to reduce asyncio latency
await ssd.do_refresh()
else:
# Normal synchronous call
refresh(ssd)
await asyncio.sleep_ms(250)
val, cv = v2, v1


def test():
def tickcb(f, c):
if f > 0.8:
return RED
if f < -0.8:
return BLUE
return c

def legendcb(f):
return "{:2.0f}".format(88 + ((f + 1) / 2) * (108 - 88))

refresh(ssd, True) # Initialise and clear display.
CWriter.set_textpos(ssd, 0, 0) # In case previous tests have altered it
wri = CWriter(ssd, arial10, GREEN, BLACK, verbose=False)
wri.set_clip(True, True, False)
scale1 = Scale(wri, 64, 64, width=124, legendcb=legendcb, pointercolor=RED, fontcolor=YELLOW)
asyncio.create_task(radio(scale1))

lbl = Label(wri, 180, 64, 50, bgcolor=DARKGREEN, bdcolor=RED, fgcolor=WHITE)
# do_refresh is called with arg 4. In landscape mode this splits screen
# into segments of 240/4=60 lines. Here we ensure a scale straddles
# this boundary
scale = Scale(
wri, 140, 64, width=124, tickcb=tickcb, pointercolor=RED, fontcolor=YELLOW, bdcolor=CYAN
)
asyncio.run(default(scale, lbl))


test()
38 changes: 38 additions & 0 deletions setup_examples/gc9a01_pico.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# color_setup.py Customise for your hardware config

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

# As written, supports:
# gc9a01 240x240 circular display on Pi Pico
# 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.

# WIRING
# Pico Display
# GPIO Pin
# 3v3 36 Vin
# IO6 9 CLK Hardware SPI0
# IO7 10 DATA (AKA SI MOSI)
# IO8 11 DC
# IO9 12 Rst
# Gnd 13 Gnd
# IO10 14 CS

from machine import Pin, SPI
import gc
from drivers.gc9a01.gc9a01 import GC9A01 as SSD

# from drivers.gc9a01.gc9a01_8_bit import GC9A01 as SSD

pdc = Pin(8, Pin.OUT, value=0) # Arbitrary pins
prst = Pin(9, Pin.OUT, value=1)
pcs = Pin(10, Pin.OUT, value=1)

gc.collect() # Precaution before instantiating framebuf
# See DRIVERS.md
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4), baudrate=33_000_000)
ssd = SSD(spi, dc=pdc, cs=pcs, rst=prst, lscape=False, usd=False, mirror=False)

0 comments on commit 108f3d9

Please sign in to comment.