Pimoroni's Pico Display Pack 2.8" provides a 320 x 240 pixel, 2.8" IPS LCD screen, one RGB LED and four buttons, which can be controlled by a Raspberry Pi Pico or Pico W inserted into the socket on the rear.
Instructions for how to use the various elements of this device and to use PicoGraphics to display things on the screen are scattered across a number of readme files and examples and mixed with instructions for other types of display. The following is an attempt to tease out the important instructions relevant for this specific display, expanded slightly to fill in a few gaps and with a few potentially useful elaborations.
Setting up
Flash the Pico with the Pimoroni custom build of MicroPython, which has built-in support for controlling all of the features of the device as well as the PicoGraphics library. The version used when putting together this article was MicroPython v1.25.0, picow v1.25.0
.
Import the PicoGraphics library and the display and instantiate it:
from picographics import PicoGraphics, DISPLAY_PICO_DISPLAY_2
display = PicoGraphics(display=DISPLAY_PICO_DISPLAY_2)
An optional rotate
keyword argument to the PicoGraphics
initialiser can rotate the display in multiples of 90º so that it can be used in different orientations, including portrait, e.g.
display = PicoGraphics(display=DISPLAY_PICO_DISPLAY_2, rotate=90)
After adding items to the display, it must be refreshed before anything actually appears on it using:
display.update()
The brightness of the backlight can be changed by setting it to any decimal value between 0 and 1.0. Set the backlight to full with:
display.set_backlight(1.0)
The width and height of the display in pixels can be obtained with:
display.get_bounds()
This returns a tuple of (width
, height
), so the following will extract these as integers into variables:
WIDTH, HEIGHT = display.get_bounds()
Background
If the screen isn't cleared between refreshes, any new elements added will be superimposed onto the existing display.
To clear the screen and set it to one colour, set the pen colour and clear the display:
BLACK_PEN = display.create_pen(0, 0, 0)
display.set_pen(BLACK_PEN)
display.clear()
As with any other changes, the display must be updated for this to take effect.
Set the boundaries within which drawing will occur with:
display.set_clip(x, y, w, h)
This will limit the clear above to just clear or colour one area of the screen. It will also limit the placement of any other items such as text until the clipping is removed with:
display.remove_clip()
Text
To print text to the screen, first set the font. Available bitmap typefaces are bitmap6
, bitmap8
and bitmap14_outline
; vector typefaces are sans
, gothic
, cursive
, serif_italic
and serif
. If using vector (Hershey) fonts, set the thickness to something other than the default 1px to make it more readable. Also set the pen colour before adding the text to the screen.
WHITE_PEN = display.create_pen(255, 255, 255)
display.set_font("sans")
display.set_pen(WHITE_PEN)
display.set_thickness(5)
display.text("text to display", 20, 10, 200, 4)
The text parameters are:
-
string
- the text string to draw. -
x
- distance across in pixels of the top left corner of the first character from the top left corner of the screen. -
y
- distance down in pixels of the top left corner of the first character from the top left corner of the screen. -
wordwrap
- number of pixels width before the text breaks onto a new line. -
scale
- size of text—an integer from 1 to 6 for bitmaps. Vector fonts can use decimal values, including values of less than 1 to scale down.
The last two parameters are optional and can also be used as named as well as positional parameters.
Other optional keyword parameters are:
-
angle
- rotational angle of text (only multiples of 90º work—0, 90, 180, 270, 360—anything else doesn't throw an error but the text will not be displayed).x
andy
parameters may need adjusting to keep the text on the screen. -
spacing
- letter spacing in pixels. -
fixed_width
- set toTrue
to make characters fixed width.
The width of a text string in pixels can be measured using:
width = display.measure_text(text, scale, spacing, fixed_width)
All parameters apart from text are optional (defaulting to 1, 1, False respectively).
A single character can be printed with its ASCII code using:
display.character(char, x, y, scale)
scale
is optional and can also be a keyword parameter.
Shapes
Draw a line between two points, x1, y1 and x2, y2, with:
display.line(x1, y1, x2, y2, thickness)
The thickness
parameter is optional; default is 1px.
To draw a circle with its centre at x, y and a radius of r:
display.circle(x, y, r)
To draw a rectangle with its top left corner at x, y with width w and height h:
display.rectangle(x, y, w, h)
For a triangle, the x, y coordinates of all three vertices need to be provided:
display.triangle(x1, y1, x2, y2, x3, y3)
For other polygons, provide a list of tuples, each with the x and y coordinates of a vertex:
display.polygon([(x1, y1), (x2, y2), (x3, y3) ...])
A single pixel can be set with:
display.pixel(x, y)
A horizontal line of 1px thickness with its left side at x, y can be drawn with:
display.pixel_span(x, y, length)
Images
JPEGs
To display a JPEG image—which must be small enough to fit in memory for decoding and must not be progressive—first upload it to the Pico. Import BitBank's JPEGDEC library (included in Pimoroni's build of MicroPython), initialise it and open and decode the image, setting the coordinates for where it should appear on the screen:
import jpegdec
j = jpegdec.JPEG(display)
j.open_file("example.jpg")
j.decode(x, y, scale, dither=True)
Parameters are:
-
x
- distance across from top left corner. -
y
- distance down from top left corner. -
scale
(optional) - must be one ofjpegdec.JPEG_SCALE_FULL
,jpegdec.JPEG_SCALE_HALF
,jpegdec.JPEG_SCALE_QUARTER
orjpegdec.JPEG_SCALE_EIGHTH
. -
dither
(optional, keyword parameter) - whether dithering is applied (seems to default to True).
PNGs
To display an image in PNG format, first upload it to the Pico. Import Bitbank's PNGdec library (included in Pimoroni's build of MicroPython), initialise it and open and decode the image, setting the coordinates for where it should appear on the screen:
import pngdec
png = pngdec.PNG(display)
png.open_file("example.png")
png.decode(x, y)
Parameters are:
-
x
- X coordinate across from top left corner. -
y
- Y coordinate down from top left corner.
The following are optional keyword arguments:
-
source
- the portion of the image to display given as a 4-integer tuple (x, y, w, h), where x is the distance from the left of the image, y is the distance from the top, w is the width of the portion to be used and h is the height. -
scale
- use an integer to scale up by a fixed amount or a 2-integer tuple to scale across the x and y axes by different amounts, e.g.scale=(2,4)
. -
rotate
- rotates the image clockwise by multiples of 90º. -
mode
- dithering mode, one ofpngdec.PNG_COPY
,pngdec.PNG_DITHER
andpngdec.PNG_POSTERISE
—the last of these is the default.
Sprites
The PicoGraphics support for spritesheets works, but as it only supports 8x8 pixel sprites, they are too small to be useful on the Pico Display and more suited to LED matrix displays.
It should be possible to achieve something similar with a PNG spritesheet, using the source
parameter to select a specific part to display as described above. However, the whole spritesheet is loaded into memory, so it may hit memory limits and crash.
The following class would give a similar experience to the built-in spritesheets:
from pngdec import PNG
class PNGSprite:
def __init__(self, display, png_file, width, height):
self.png_file = png_file
self.width = width
self.height = height
self.display = display
def get_sprite(self, sx, sy, dx, dy, scale=1):
png = PNG(self.display)
png.open_file(self.png_file)
png.decode(dx, dy, scale, source=(sx * self.width, sy * self.height, self.width, self.height))
This should be initiated by passing in the display parameter, the name of the PNG file and the width and height of each sprite on the sheet in pixels:
sprite = PNGSprite(display, "spritesheet.png", 100, 100)
One image can then be chosen with the get_sprite
method, passing in the number of the sprite counting from the left of the spritesheet then down from the top (counting from 0) then the x and y coordinates of where it should be placed on the screen, e.g.
sprite.get_sprite(0, 0, 0, 0)
An optional 5th parameter for scale can take either an integer to scale the image up proportionately or a 2-integer tuple to scale up the width and height respectively.
LED
To set the onboard LED, the RGBLED class must be imported and initialised with the correct GPIO pins for this device:
from pimoroni import RGBLED
led = RGBLED(26, 27, 28)
Set the colour using:
led.set_rgb(r, g, b)
Buttons
The four buttons use GPIO pins 12, 13, 14 and 15 for the buttons labelled A, B, X and Y respectively. They can be initiated with:
from machine import Pin
button_a = Pin(12, Pin.IN, Pin.PULL_UP)
button_b = Pin(13, Pin.IN, Pin.PULL_UP)
button_x = Pin(14, Pin.IN, Pin.PULL_UP)
button_y = Pin(15, Pin.IN, Pin.PULL_UP)
If a button is pressed, its value()
method will return 0; if unpressed it will return 1. Test whether a button has been pressed with, e.g.
if button_y.value() == 0:
print("Button Y pressed")
Buttons can also be observed using interrupts, leaving the rest of the script to continue rather than looping purely to test for a keypress. For example:
from machine import Pin
# Handler function for when any button pressed
def button_pressed(pin):
# Parse out pin number from returned value
pin_no = int(str(pin).split(", ")[0][8:])
print(pin_no)
# Compare returned value with each of buttons to see which pressed
if pin == button_a:
print("Button A pressed")
elif pin == button_b:
print("Button B pressed")
elif pin == button_x:
print("Button X pressed")
elif pin == button_y:
print("Button Y pressed")
# Attach the interrupt to the pin
button_a.irq(trigger=Pin.IRQ_FALLING, handler=button_pressed)
button_b.irq(trigger=Pin.IRQ_FALLING, handler=button_pressed)
button_x.irq(trigger=Pin.IRQ_FALLING, handler=button_pressed)
button_y.irq(trigger=Pin.IRQ_FALLING, handler=button_pressed)
The trigger
tells the interrupt to operate when the signal from the pin changes from 1 to 0 (IRQ_FALLING), while the hander
provides the name of the function to run when this condition is met.
The interrupt will return a Pin
object to the handler function. In the example above, this is firstly turned into a string so that the GPIO pin number can be parsed out of it, then it is compared directly to the Pin
objects already defined earlier in the program to test each in turn to see whether it was pressed.
This is a great little screen with lots of possibilities for useful applications, or it's just fun to tinker with. Hopefully this will help someone to get started with it a bit more easily and perhaps get more out of it.
Top comments (0)