Pico explorer display with Microptyhon

Hello,

I’m a teacher and I use the Pico Explorer board with CircuitPython. I’d like to switch to MicroPython, but for teaching reasons, I don’t want to use the uf2 firmware file provided by Pimoroni.
However, I can’t get the ST7789 240x240 display to work with the available Micropython libraries on the web.
Could you provide a Micropython version of your Picographics library?

Best regards,
Mike

Our libraries are written in C and pretty enmeshed in the rest of the rest of the graphics stuff in pimoroni-pico, so we don’t really have a standalone MicroPython driver we can send you I’m afraid.

There’s a bunch of ST7789 drivers listed here you could try though:

The big thing, IMHO, when trying a driver from another source. Is to get the pins matched up in the code to what is actually used by the Pico Explorer.
And be careful regaurding pin number versus GP number.

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.

Thanks a lot for your help.
I will try your solution as soon as I can.
I’ll keep you informed.

Mike

Thank you so much Tony!!!
Your example works perfectly well on the Pico Explorer map screen.
I’ll keep exploring this in the hope that at some point I’ll be able to display an image on the screen.
Thanks again for your help.
Best regards,
Mike

What sort of image? Do you mean a photograph?
You can do it on Explorer