Try this, I think it does what you want. The code was originally written for a Waveshare 240x240 display. I have had to adjust the MADCTR register (0x36) to rotate the display by 90 degrees. Page 215 of the very long manual ST7789VW_SPEC_V1.0 (waveshare.com)
I’ve moved the SPI pins to those hard wired on the Explorer.
The driver for the ST7789 is included in the code and does not need a Pimoroni version of the UF2
It contains Text additions and many Graphics routines (including filled circles and Triangles) which are very easy to call.
Please let me know how you get on.
# Introduction to Computer Graphics with Crib Code help
# Raspberry Pi Pico microcontroller
# Originally for the WaveShare Pico LCD 1.3 inch 240x240 screen with Joystick and Buttons
# Tony Goodhew - 9th Nov 2021
# This version has been modifed for the Pimoroni Pico Explorer - 27 June 2023
# This is an educational demonstration with many comments
# MADCTR register has been changed to rotate the display
# Import the libraries needed
from machine import Pin,SPI,PWM
import framebuf
import utime
import os
import math
import random
# ============ Start of Drive Code ================
# == Copy and paste into your code ==
BL = 13 # Pins used for Pimoroni Pico Display
DC = 16
RST = 12
MOSI = 19
SCK = 18
CS = 17
class LCD_1inch3(framebuf.FrameBuffer):
def __init__(self):
self.width = 240
self.height = 240
self.cs = Pin(CS,Pin.OUT)
self.rst = Pin(RST,Pin.OUT)
self.cs(1)
self.spi = SPI(0)
self.spi = SPI(0,1000_000)
self.spi = SPI(0,100000_000,polarity=0, phase=0,sck=Pin(SCK),mosi=Pin(MOSI),miso=None)
self.dc = Pin(DC,Pin.OUT)
self.dc(1)
self.buffer = bytearray(self.height * self.width * 2)
super().__init__(self.buffer, self.width, self.height, framebuf.RGB565)
self.init_display()
self.red = 0x07E0 # Pre-defined colours
self.green = 0x001f # Probably easier to use colour(r,g,b) defined below
self.blue = 0xf800
self.white = 0xffff
def write_cmd(self, cmd):
self.cs(1)
self.dc(0)
self.cs(0)
self.spi.write(bytearray([cmd]))
self.cs(1)
def write_data(self, buf):
self.cs(1)
self.dc(1)
self.cs(0)
self.spi.write(bytearray([buf]))
self.cs(1)
def init_display(self):
"""Initialize display"""
self.rst(1)
self.rst(0)
self.rst(1)
self.write_cmd(0x36)
self.write_data(0x10)########### 0x70 Adjusted for Explorer
self.write_cmd(0x3A)
self.write_data(0x05)
self.write_cmd(0xB2)
self.write_data(0x0C)
self.write_data(0x0C)
self.write_data(0x00)
self.write_data(0x33)
self.write_data(0x33)
self.write_cmd(0xB7)
self.write_data(0x35)
self.write_cmd(0xBB)
self.write_data(0x19)
self.write_cmd(0xC0)
self.write_data(0x2C)
self.write_cmd(0xC2)
self.write_data(0x01)
self.write_cmd(0xC3)
self.write_data(0x12)
self.write_cmd(0xC4)
self.write_data(0x20)
self.write_cmd(0xC6)
self.write_data(0x0F)
self.write_cmd(0xD0)
self.write_data(0xA4)
self.write_data(0xA1)
self.write_cmd(0xE0)
self.write_data(0xD0)
self.write_data(0x04)
self.write_data(0x0D)
self.write_data(0x11)
self.write_data(0x13)
self.write_data(0x2B)
self.write_data(0x3F)
self.write_data(0x54)
self.write_data(0x4C)
self.write_data(0x18)
self.write_data(0x0D)
self.write_data(0x0B)
self.write_data(0x1F)
self.write_data(0x23)
self.write_cmd(0xE1)
self.write_data(0xD0)
self.write_data(0x04)
self.write_data(0x0C)
self.write_data(0x11)
self.write_data(0x13)
self.write_data(0x2C)
self.write_data(0x3F)
self.write_data(0x44)
self.write_data(0x51)
self.write_data(0x2F)
self.write_data(0x1F)
self.write_data(0x1F)
self.write_data(0x20)
self.write_data(0x23)
self.write_cmd(0x21)
self.write_cmd(0x11)
self.write_cmd(0x29)
def show(self):
self.write_cmd(0x2A)
self.write_data(0x00)
self.write_data(0x00)
self.write_data(0x00)
self.write_data(0xef)
self.write_cmd(0x2B)
self.write_data(0x00)
self.write_data(0x00)
self.write_data(0x00)
self.write_data(0xEF)
self.write_cmd(0x2C)
self.cs(1)
self.dc(1)
self.cs(0)
self.spi.write(self.buffer)
self.cs(1)
# ========= End of Driver ===========
# ==== Start of Improved Text system - 3 sizes of text ===========
#ASCII Character Set
cmap = ['00000000000000000000000000000000000', #Space
'00100001000010000100001000000000100', #!
'01010010100000000000000000000000000', #"
'01010010101101100000110110101001010', ##
'00100011111000001110000011111000100', #$
'11001110010001000100010001001110011', #%
'01000101001010001000101011001001101', #&
'10000100001000000000000000000000000', #'
'00100010001000010000100000100000100', #(
'00100000100000100001000010001000100', #)
'00000001001010101110101010010000000', #*
'00000001000010011111001000010000000', #+
'000000000000000000000000000000110000100010000', #,
'00000000000000011111000000000000000', #-
'00000000000000000000000001100011000', #.
'00001000010001000100010001000010000', #/
'01110100011000110101100011000101110', #0
'00100011000010000100001000010001110', #1
'01110100010000101110100001000011111', #2
'01110100010000101110000011000101110', #3
'00010001100101011111000100001000010', #4
'11111100001111000001000011000101110', #5
'01110100001000011110100011000101110', #6
'11111000010001000100010001000010000', #7
'01110100011000101110100011000101110', #8
'01110100011000101111000010000101110', #9
'00000011000110000000011000110000000', #:
'01100011000000001100011000010001000', #;
'00010001000100010000010000010000010', #<
'00000000001111100000111110000000000', #=
'01000001000001000001000100010001000', #>
'01100100100001000100001000000000100', #?
'01110100010000101101101011010101110', #@
'00100010101000110001111111000110001', #A
'11110010010100111110010010100111110', #B
'01110100011000010000100001000101110', #C
'11110010010100101001010010100111110', #D
'11111100001000011100100001000011111', #E
'11111100001000011100100001000010000', #F
'01110100011000010111100011000101110', #G
'10001100011000111111100011000110001', #H
'01110001000010000100001000010001110', #I
'00111000100001000010000101001001100', #J
'10001100101010011000101001001010001', #K
'10000100001000010000100001000011111', #L
'10001110111010110101100011000110001', #M
'10001110011010110011100011000110001', #N
'01110100011000110001100011000101110', #O
'11110100011000111110100001000010000', #P
'01110100011000110001101011001001101', #Q
'11110100011000111110101001001010001', #R
'01110100011000001110000011000101110', #S
'11111001000010000100001000010000100', #T
'10001100011000110001100011000101110', #U
'10001100011000101010010100010000100', #V
'10001100011000110101101011101110001', #W
'10001100010101000100010101000110001', #X
'10001100010101000100001000010000100', #Y
'11111000010001000100010001000011111', #Z
'01110010000100001000010000100001110', #[
'10000100000100000100000100000100001', #\
'00111000010000100001000010000100111', #]
'00100010101000100000000000000000000', #^
'00000000000000000000000000000011111', #_
'11000110001000001000000000000000000', #`
'00000000000111000001011111000101110', #a
'10000100001011011001100011100110110', #b
'00000000000011101000010000100000111', #c
'00001000010110110011100011001101101', #d
'00000000000111010001111111000001110', #e
'00110010010100011110010000100001000', #f
'000000000001110100011000110001011110000101110', #g
'10000100001011011001100011000110001', #h
'00100000000110000100001000010001110', #i
'0001000000001100001000010000101001001100', #j
'10000100001001010100110001010010010', #k
'01100001000010000100001000010001110', #l
'00000000001101010101101011010110101', #m
'00000000001011011001100011000110001', #n
'00000000000111010001100011000101110', #o
'000000000001110100011000110001111101000010000', #p
'000000000001110100011000110001011110000100001', #q
'00000000001011011001100001000010000', #r
'00000000000111110000011100000111110', #s
'00100001000111100100001000010000111', #t
'00000000001000110001100011001101101', #u
'00000000001000110001100010101000100', #v
'00000000001000110001101011010101010', #w
'00000000001000101010001000101010001', #x
'000000000010001100011000110001011110000101110', #y
'00000000001111100010001000100011111', #z
'00010001000010001000001000010000010', #{
'00100001000010000000001000010000100', #|
'01000001000010000010001000010001000', #}
'01000101010001000000000000000000000' #}~
]
def printchar(letter,xpos,ypos,size,c):
origin = xpos
charval = ord(letter)
#print(charval)
index = charval-32 #start code, 32 or space
#print(index)
character = cmap[index] #this is our char...
rows = [character[i:i+5] for i in range(0,len(character),5)]
#print(rows)
for row in rows:
#print(row)
for bit in row:
#print(bit)
if bit == '1':
lcd.pixel(xpos,ypos,c)
if size==2:
lcd.pixel(xpos,ypos+1,c)
lcd.pixel(xpos+1,ypos,c)
lcd.pixel(xpos+1,ypos+1,c)
if size == 3:
lcd.pixel(xpos,ypos+1,c)
lcd.pixel(xpos,ypos+2,c)
lcd.pixel(xpos+1,ypos,c)
lcd.pixel(xpos+1,ypos+1,c)
lcd.pixel(xpos+1,ypos+2,c)
lcd.pixel(xpos+2,ypos,c)
lcd.pixel(xpos+2,ypos+1,c)
lcd.pixel(xpos+2,ypos+2,c)
xpos+=size
xpos=origin
ypos+=size
def delchar(xpos,ypos,size,):
if size == 1:
charwidth = 5
charheight = 9
if size == 2:
charwidth = 10
charheight = 18
if size == 3:
charwidth = 15
charheight = 27
c=colour(40,40,40)
lcd.fill_rect(xpos,ypos,charwidth,charheight,0) #xywh
def printstring(string,xpos,ypos,size,c):
if size == 1:
spacing = 8
if size == 2:
spacing = 14
if size == 3:
spacing = 18
for i in string:
printchar(i,xpos,ypos,size,c)
xpos+=spacing
def centrestring(string,ypos,size,c):
if size == 1:
spacing = 8
if size == 2:
spacing = 14
if size == 3:
spacing = 18
used = len(string)*spacing
xpos = int((240-used)/2)
printstring(string,xpos,ypos,size,c)
# ========= End of Improved text system ==================
# ========== Start of Graphics routines =================
# Colour Mixing Routine
def colour(R,G,B): # Compact method!
mix1 = ((R&0xF8)*256) + ((G&0xFC)*8) + ((B&0xF8)>>3)
return (mix1 & 0xFF) *256 + int((mix1 & 0xFF00) /256) # low nibble first
# ========== Start of Triangles code =============
# Modified from https://github.com/SpiderMaf/PiPicoDsply/blob/main/filled-triangles.py
# To work on WaveShare Pi Pico displays
class Point:
def __init__(self,x,y):
self.X=x
self.Y=y
def __str__(self):
return "Point(%s,%s)"%(self.X,self.Y)
class Triangle:
def __init__(self,p1,p2,p3):
self.P1=p1
self.P2=p2
self.P3=p3
def __str__(self):
return "Triangle(%s,%s,%s)"%(self.P1,self.P2,self.P3)
def draw(self):
print("I should draw now")
self.fillTri()
# Filled triangle routines ported from http://www.sunshine2k.de/coding/java/TriangleRasterization/TriangleRasterization.html
def sortVerticesAscendingByY(self):
if self.P1.Y > self.P2.Y:
vTmp = self.P1
self.P1 = self.P2
self.P2 = vTmp
if self.P1.Y > self.P3.Y:
vTmp = self.P1
self.P1 = self.P3
self.P3 = vTmp
if self.P2.Y > self.P3.Y:
vTmp = self.P2
self.P2 = self.P3
self.P3 = vTmp
def fillTri(self):
self.sortVerticesAscendingByY()
if self.P2.Y == self.P3.Y:
fillBottomFlatTriangle(self.P1, self.P2, self.P3)
else:
if self.P1.Y == self.P2.Y:
fillTopFlatTriangle(self.P1, self.P2, self.P3)
else:
newx = int(self.P1.X + (float(self.P2.Y - self.P1.Y) / float(self.P3.Y - self.P1.Y)) * (self.P3.X - self.P1.X))
newy = self.P2.Y
pTmp = Point( newx,newy )
fillBottomFlatTriangle(self.P1, self.P2, pTmp)
fillTopFlatTriangle(self.P2, pTmp, self.P3)
def fillBottomFlatTriangle(p1,p2,p3):
slope1 = float(p2.X - p1.X) / float (p2.Y - p1.Y)
slope2 = float(p3.X - p1.X) / float (p3.Y - p1.Y)
x1 = p1.X
x2 = p1.X + 0.5
for scanlineY in range(p1.Y,p2.Y):
# lcd.pixel_span(int(x1), scanlineY, int(x2)-int(x1)) # Switch pixel_span() to hline() / Pimoroni to WS
lcd.hline(int(x1),scanlineY, int(x2)-int(x1),c)
x1 += slope1
x2 += slope2
def fillTopFlatTriangle(p1,p2,p3):
slope1 = float(p3.X - p1.X) / float(p3.Y - p1.Y)
slope2 = float(p3.X - p2.X) / float(p3.Y - p2.Y)
x1 = p3.X
x2 = p3.X + 0.5
for scanlineY in range (p3.Y,p1.Y-1,-1):
# lcd.pixel_span(int(x1), scanlineY, int(x2)-int(x1)) # Switch pixel_span() to hline() / Pimoroni to WS
lcd.hline(int(x1),scanlineY, int(x2)-int(x1),c)
x1 -= slope1
x2 -= slope2
# ============== End of Triangles Code ===============
# ============== GFX Routines ============
def ring_old(cx,cy,r,cc): # Draws a circle - with centre (x,y), radius, colour
for angle in range(181):
y3=int(r*math.sin(math.radians(angle/2))) # Uses Trigonometry
x3=int(r*math.cos(math.radians(angle/2)))
lcd.pixel(cx-x3,cy+y3,cc) # 4 quadrants
lcd.pixel(cx-x3,cy-y3,cc)
lcd.pixel(cx+x3,cy+y3,cc)
lcd.pixel(cx+x3,cy-y3,cc)
def ring(x,y,r,c):
lcd.pixel(x-r,y,c)
lcd.pixel(x+r,y,c)
lcd.pixel(x,y-r,c)
lcd.pixel(x,y+r,c)
for i in range(1,r):
a = int(math.sqrt(r*r-i*i)) # Uses Pythagoras
lcd.pixel(x-a,y-i,c)
lcd.pixel(x+a,y-i,c)
lcd.pixel(x-a,y+i,c)
lcd.pixel(x+a,y+i,c)
lcd.pixel(x-i,y-a,c)
lcd.pixel(x+i,y-a,c)
lcd.pixel(x-i,y+a,c)
lcd.pixel(x+i,y+a,c)
def triangle(x1,y1,x2,y2,x3,y3,c): # Draw outline triangle
lcd.line(x1,y1,x2,y2,c)
lcd.line(x2,y2,x3,y3,c)
lcd.line(x3,y3,x1,y1,c)
def tri_filled(x1,y1,x2,y2,x3,y3,c): # Draw filled triangle
t=Triangle(Point(x1,y1),Point(x2,y2),Point(x3,y3)) # Define corners
t.fillTri() # Call main code block
def circle(x,y,r,c):
lcd.hline(x-r,y,r*2,c)
for i in range(1,r):
a = int(math.sqrt(r*r-i*i)) # Pythagoras!
lcd.hline(x-a,y+i,a*2,c) # Lower half
lcd.hline(x-a,y-i,a*2,c) # Upper half
# =================== Main ======================
lcd = LCD_1inch3() # Start screen
lcd.fill(colour(0,0,0)) # BLACK
lcd.show()
'''
# Set up buttons & joystick
keyA = Pin(15,Pin.IN,Pin.PULL_UP)
keyB = Pin(17,Pin.IN,Pin.PULL_UP)
keyX = Pin(19 ,Pin.IN,Pin.PULL_UP)
keyY= Pin(21 ,Pin.IN,Pin.PULL_UP)
up = Pin(2,Pin.IN,Pin.PULL_UP)
down = Pin(18,Pin.IN,Pin.PULL_UP)
left = Pin(16,Pin.IN,Pin.PULL_UP)
right = Pin(20,Pin.IN,Pin.PULL_UP)
ctrl = Pin(3,Pin.IN,Pin.PULL_UP)
if keyA.value() == 0: print("A Pressed")
'''
# ========== Start of crib code ==============
printstring("Basic",35,20,2,colour(255,0,0)) # red
printstring("Graphical",35,40,2,colour(0,255,0)) # green
printstring("Components",35,60,2,colour(0,0,255)) # blue
c = colour(200,200,200) # Calculate 2-byte colour code
lcd.pixel(10,120,c) # single pixel (x,y,c)
lcd.hline(20,120,200,c) # horizontal line (x,y,width,c)
lcd.vline(10,130,100,c) # vertical line (x,y,height,c)
lcd.rect(20,140,80,20,c) # hollow rectangle (x,y,w,h,c)
lcd.fill_rect(20,170,80,20,c) # solid rectangle (x,y,w,h,c)
lcd.line(120,130,230,210,c) # slanted lines (x0,y0,x1,y1,c)
lcd.line(140,235,235,145,c)
lcd.text("Framebuffer text",20,200,c) # simple text (s,x,y,c)
lcd.show() # Draw on the screen
utime.sleep(5) # Wait 5 seconds
lcd.fill(colour(0,0,0)) # Clear screen to Black
lcd.show()
printstring("Triangles",35,20,2,colour(255,0,0)) # Extra font size 2
printstring("Circles",35,40,2,colour(0,255,0)) # (s,x,y,size,c)
printstring("and Rings",35,60,2,colour(0,0,255))
c = colour(255,0,0)
lcd.show()
# Triangles
x0=30
y0=115
x1=10
y1=200
x2=120
y2=225
c = colour(255,0,0)
triangle(40,115,10,180,95,235,c) # outline triangle (x0,y0,x1,y1,x2,y2,c)
lcd.show()
utime.sleep(1.2)
c = colour(0,255,0)
tri_filled(x0,y0,x1,y1,x2,y2,c) # solid triangle (x0,y0,x1,y1,x2,y2,c)
lcd.show()
utime.sleep(1.2)
c = colour(0,0,255)
tri_filled(50,120,5,190,70,230,c)
triangle(5,125,100,140,100,230,colour(255,0,255))
lcd.show()
# Circles
c = colour(0,0,255)
ring(150,180,25,c) # circular ring (cx,xy,r,c)
lcd.show()
utime.sleep(1.2)
circle(150,180,25,c) # Filled circle (cx,cy,r,c)
lcd.show()
utime.sleep(1.2)
c = colour(255,255,0)
ring(150,180,25,c)
ring(150,180,30,c)
lcd.show()
utime.sleep(1.2)
c = colour(255,0,0)
circle(150,180,10,c)
ring_old(200,120,40,colour(255,0,255)) # Alternative ring (cx,cy,r,c)
lcd.show()
utime.sleep(3)
lcd.fill(0)
# Comparing text fonts and sizes - Instructions
lcd.text("Framebuffer text",10,10,colour(255,255,255)) # (s,x,y,c)
printstring("Text size 1",10,25,1,colour(255,255,0)) # (s,x,y,size,c)
printstring("Text size 2",10,40,2,colour(255,0,0))
printstring("Text Size 3",10,65,3,colour(255,255,0))
centrestring("Centred",100,3,colour(0,0,255))
centrestring("Press Button A",170,2,colour(0,255,0))
centrestring("Three times",190,2,colour(0,255,0))
lcd.show() # Show the instructions
keyA = Pin(12,Pin.IN,Pin.PULL_UP) # Set up button A - INPUT
led = Pin(25,Pin.OUT) # Set up Pico LED - OUTPUT
# Looping with counter
count = 0
while count < 3:
lcd.rect(99,134,42,22,colour(255,0,0)) # Simulated LED edge
lcd.show()
if keyA.value() == 0: # Check if button pressed
led.value(1) # LED on
count = count + 1
centrestring("A pressed "+ str(count),215,2,colour(255,0,0)) # Message
lcd.fill_rect(100,135,40,20,colour(0,255,0)) # Simulated LED On
lcd.show() # Display message & sim LED
while keyA.value() == 0: # Wait until button A is released
utime.sleep(0.01)
lcd.fill_rect(0,215,240,18,0) # Clear text with background filled rectangle
lcd.fill_rect(100,135,40,20,0) # Simulated LED Off
lcd.show() # Display no message
else:
led.value(0) # LED off in the loop
led.value(0) # LED off after the loop
# ========== End of crib code =============
# Finish
lcd.fill(colour(0,0,40)) # Dark Blue
for r in range(10):
ring(118,120,65+r,colour(255,255,0))
c = colour(255,0,0)
centrestring("Finished",110,2,c)
lcd.text("Tony Goodhew",75,220,colour(150,150,150))
lcd.show()
# Tidy up
utime.sleep(3)
lcd.fill(0)
lcd.show()
I used to teach Computing/IT so I hope this helps.
I do not like the way CircuitPython does graphics. It is very difficult for a novice to get their head round all the layers, grids, colour palettes etc. This is all based on being able to draw a single pixel where you want and in the colour you want.
A row of pixels makes a line, vertical and horizontal lines outline a rectangle, a block of lines fills a rectangle and trigonometry and Pythagoras help draw circles. Writing text to the screen is just a matter of dropping pixels in the right place from coded table. This is all great fun if you work it out for yourself and slowly store your knowledge in a set of procedures which you can call in the future.