Coding with a Raspberry Pi Pico microcontroller
July 14, 2021
Share this with your colleagues
To celebrate Novak Djokovic’s Wimbledon victory, I’ve written a simple retro tennis game as an introduction to microcontrollers and coding in MicroPython. It’s straightforward enough that anyone can try, and I’d strongly recommend microcontroller coding as a beneficial exercise for developers at any level.
When I was 10, I persuaded my headmaster to let me run a computer club at my school. We would huddle around a ZX Spectrum after hours, getting it to make weird noises or graphics. Sometimes we would play games, and looking back I still marvel at how much fun we used to have with just 16 kilobytes of RAM.
I admire the coders of yesteryear’s games. Faced with tiny computing resources – they managed to squeeze impossible sound and graphics out of nowhere. It’s one of the reasons that gaming industry coders are always in high demand – they literally make the impossible possible.
Today I’m going to recreate a simple tennis game from my youth by coding up not a Spectrum but the Raspberry Pi Pico microcontroller.
From cars to children’s toys, from musical keyboards to the QWERTY kind, from burglar alarms to kitchen appliances – microcontrollers are everywhere. You are likely, quite literally, to be sitting in clothes right now that have been washed by a microcontroller. Microcontrollers are the silicon chips that power real-world devices, combining a small amount of on-board processing power to carry out simple tasks and an interface that can connect to the real world – buttons, sensors, lights, motors, displays, etc. Over the last 40 years, you will have found them in pretty much anything that has buttons or a display.
In the last decade there has been an explosion of new devices aimed at hobbyists and STEM students – including the Arduino, one of the most famous. The Raspberry Pi ‘Pico’ is the latest offering from the Raspberry Pi Foundation and is based on their own silicon; the RP2040 chip. Supporting C/C++, MicroPython and CircuitPython from the get-go, it offers an astonishing array of options in a tiny $4 package.
Our parts list
- Raspberry Pi Pico
(this is available with pre-soldered pins if you don’t fancy soldering) - Pimoroni Pico Explorer
- Two 100k ohm potentiometers
- Wire
Connecting up the hardware
Wiring up using the integrated breadboard is straightforward enough. Make sure that you don’t short out the Pico’s delicate 3.3v power supply so only wire up with the Pico disconnected from the USB.
We’re using two 100k potentiometers that will act as our ‘paddle’ controllers.
Flashing the Pico
In order to use the features of the Pico Explorer board there is a dedicated firmware you can download from Pimoroni:
https://github.com/pimoroni/pimoroni-pico/releases/tag/v0.2.2
- Connect the Pico to a USB port whilst pressing the ‘bootsel’ button
- Once connected, release the button and an RPI-RP2 drive should appear
- Drag and drop the uf2 firmware file
Coding the game
The first step is to import all the modules we’re going to need and to set up some constraints:
from machine import ADC, Timer
from utime import sleep
from micropython import const
import picoexplorer as pico
import random
# pico tennis - by Stewart Twynham
# first - some gameplay elements...
MAX_SCORE = const(15)
PADDLE_SIZE = const(30)
BALL_SPEED = const(7)
# get our screen dimensions
screen_width = pico.get_width()
screen_height = pico.get_height()
# these will be the constraints of our ball and our players on the screen
ball_xmin = 22
ball_xmax = screen_width - 22
ball_ymin = 72
ball_ymax = screen_height - 22
paddle_range = screen_height - PADDLE_SIZE - 74
Next we need to initialise our display and the audio features. Note that we’re using 2 bytes per pixel for the display, so almost half the Pico’s RAM will be used up for our display buffer:
# initialise display
# this is 2 bytes per pixel (RGB565)
display_buffer = bytearray(screen_width * screen_height * 2)
pico.init(display_buffer)
# initialise the timer - we use this for the sounds
tim = Timer()
# prepare the sound on GPIO0
pico.set_audio_pin(0)
# these are the scores on the doors
player1 = 0
player2 = 0
Coding the moving parts
We’re going to use classes for our ball and our two paddle controllers. This helps to keep our game readable and easy to follow. Features such as ball position, velocity and initialisation of our ADCs in the case of the two controllers can be built into the classes.
# our ball and its movements are defined in a class
class Ball:
def __init__(self, x=0, y=0, dx=0, dy=0):
self.x = x
self.y = y
self.dx = dx
self.dy = dy
# this is where we draw the ball from the x/y position
def draw(self):
pico.set_pen(224, 231, 34)
pico.circle(self.x, self.y, 10)
# this resets the ball to emerge from the net during the game
def reset(self):
self.x = int(screen_width / 2)
self.y = int(screen_height / 2) + 30
# we give some randomisation to the starting direction
if (random.randint(0,1) == 0):
self.dx = - BALL_SPEED
else:
self.dx = BALL_SPEED
if (random.randint(0,1) == 0):
self.dy = - BALL_SPEED
else:
self.dy = BALL_SPEED
# this moves the ball around the screen as the game progresses
def move(self):
self.x += self.dx
self.y += self.dy
# we hit the top or bottom of the screen so we reverse the Y component
def bounce(self):
self.dy *= -1
self.y += self.dy
# we hit a paddle - so we reverse the X component
def hit(self):
self.dx *= -1
self.move()
# now we create an instance of the ball and reset it to the start position
ball = Ball()
ball.reset()
The ADC for our paddle controllers are 12 bit, but expanded into an unsigned 16 bit value (0-65535). Turning the potentiometer a full 270 degrees to cover the court is hard work, so I will only be using the ‘middle’ 50% of the range.
# the paddle is our controller for our player
class Paddle:
def __init__(self, channel, xpos, top = 0, bottom = 0):
# we have defaults for two values but not the ADC channel and the X-position on the screen
self.channel = channel
self.xpos = xpos
self.top = top
self.bottom = bottom
def draw(self):
# calculate the Y-position - scaled over the range we expect to see
self.top = int(paddle_range / 32767 * self.readchannel(self.channel)) + 62
self.bottom = self.top + PADDLE_SIZE
pico.set_pen(255, 255, 255)
pico.rectangle(self.xpos, self.top, 4, PADDLE_SIZE)
# this reads the ADC value for the given channel, set when the paddle is first initialised
def readchannel(self, channel):
# read the paddle position as an unsigned 16 bit integer
raw = machine.ADC(channel).read_u16()
# we will only use the middle 50% of the potentiometer's 0-65535 range to make controls a little easier
if raw > 49151:
raw = 49151
if raw < 16384:
raw = 16384
# return a range 0-32767
return(raw - 16384)
# now we will initialise two paddles - left and right - defining their ADC channel and X positions
paddle = [Paddle(1, 46), Paddle(2, screen_width - 50)]
We’ll also use this opportunity to make some noise – just like the Atari-type games I remember, and define some code to display the scores and draw our ‘court’.
# it isn't a game if we don't have sound...
def bleep(frequency):
pico.set_tone(frequency)
tim.init (period = 150, mode = Timer.ONE_SHOT, callback = lambda t: pico.set_tone(-1))
# this shows the player scores
def show_scores():
# show the player scores
pico.set_pen(255, 255, 255)
pico.text("{:.0f}".format(player1), 60, 20, 40, 4)
pico.text("{:.0f}".format(player2), screen_width - 80, 20, 40, 4)
# this will draw the court for us
def draw_court():
# set a grass background
pico.set_pen(0, 192, 0)
pico.clear()
# switch to light grey for the court
pico.set_pen(224, 224, 224)
pico.rectangle(10, 60, screen_width - 20, 2)
pico.rectangle(10, screen_height - 12, screen_width - 20, 2)
pico.rectangle(10, 60, 2, screen_height - 70)
pico.rectangle(screen_width - 12, 60, 2, screen_height - 70)
# the net is a dashed line in the middle of the screen
ypos = 80
while (ypos < screen_height - 20):
pico.rectangle(int(screen_width/2), ypos, 2, 10)
ypos += 20
# this shows a text message inside a rectangle for when the game is paused or a player wins...
def show_text(text):
pico.set_pen(64, 128, 64)
pico.rectangle(30, 80, screen_width - 60, 60)
pico.set_pen(224, 224, 224)
pico.text (text, 60, 90, screen_width - 90,3)
pico.update()
The core of the game
The game is essentially a continuous loop which draws the court, the paddles and moves the ball. We then have to respond to events such as hitting an edge or a paddle. If the ball reaches the other side of the court then the other player gains a point.
# we will run this forever
while True:
draw_court()
paddle[0].draw()
paddle[1].draw()
ball.move()
# if the centre of the ball is colliding with either bat then reverse the ball's direction and move the ball back
if (ball.x >= ball_xmax - 40) and (ball.x <= ball_xmax - 30) and (ball.y > paddle[1].top - 10) and (ball.y < paddle[1].bottom + 10) and (ball.dx > 0):
ball.hit()
bleep(880)
elif (ball.x <= ball_xmin + 40) and (ball.x >= ball_xmin + 30) and (ball.y > paddle[0].top - 10) and (ball.y < paddle[0].bottom + 10) and (ball.dx < 0):
ball.hit()
bleep(880)
# if we hit the top or bottom of the screen - we bounce the ball and bleep
if ball.y < ball_ymin or ball.y > ball_ymax:
ball.bounce()
bleep(440)
# oh dear, the ball ran off the edge - alter the score and set the ball up for a 'serve'
elif ball.x > ball_xmax:
ball.reset()
player1 += 1
bleep(220)
elif ball.x < ball_xmin:
ball.reset()
player2 += 1
bleep(220)
show_scores()
# check if a player wins
if (player1 >= MAX_SCORE or player2 >= MAX_SCORE):
if (player1 >= MAX_SCORE):
show_text("Player one wins")
else:
show_text("Player two wins")
# now wait until the X button is pressed to reset the game
while not pico.is_pressed(2):
sleep(0.01)
ball.reset()
player1 = 0
player2 = 0
sleep(0.25)
Now we are ready to draw in our ball and update the display. We’ll also respond to the X and Y buttons in case we need to either reset the game or pause.
ball.draw()
pico.update()
# reset the game on X
if pico.is_pressed(2):
ball.reset()
player1 = 0
player2 = 0
# pause the game on Y
if pico.is_pressed(3):
sleep(0.25)
show_text("Game paused")
while not pico.is_pressed(3):
sleep(0.01)
sleep(0.25)
# slight delay in the loop, although not much is really needed - this isn't super fast
sleep(0.01)
The full source code for this game can be downloaded at:
https://github.com/twynham/pi-pico/blob/main/examples/pico_tennis.py
Solving memory constraints
Pimoroni’s Explorer board really is a great piece of kit with a 240×240 pixel LCD screen, but as mentioned earlier, this created some practical memory challenges when coding the game.
Switching between code running on main.py, the REPL and from Thonny itself over the serial interface caused many random lock-ups and crashes. These all seemed to stem from slightly lazy garbage collection in this version of micropython (1.15). Although, to be fair, we are dealing with a very limited heap size to begin with. Don’t forget, nearly half of our 264k RAM is dedicated to the display buffer.
The fix was simple: always clear out main.py – or replace it with a print (“hello world”) so you aren’t competing for memory reserved by code that had already pre-loaded on boot.
Summary
I absolutely love this format of controller. It’s cheap, surprisingly capable and has growing community support. Whilst it’s not at Arduino levels, it has support for C/C++ Micropython and Circuit Python – with a promise of a full blown RTOS coming soon.
I firmly believe all developers should be encouraged to code on a microcontroller from time to time. Without infinite memory and CPU cycles you’re forced to think hard about the problem you’re trying to solve and take time to understand what is going on under the bonnet. To solve problems you’ll have to think like the coders of yesteryear, with more elegant code and making more out of less.

Stewart Twynham
VP Operations & Security @ MishiPay
Follow him on – Medium
Find more posts on – MishiPay Engineering