4 - Embedded Programming

Summary
This week we were tasked with understanding different embedded development workflows and toolchains, and programming a microcontroller to interact and communicate.
Approach
Our local lesson went through different toolchains and development workflows. We we're also handed a QPAD-Xiao designed by former student now instructor Quentin Bolsee which we had to solder together ourselves. For my program I decided to make a bunch of visualization tools and take an existing retro gaming repository written for the Raspberry Pi Pico board and tweak it to work on our QPAD.
Assignment
- Group assignment:
- Individual assignment:
Comparison
Arduino
Our local lesson on wednesday started with a presentation from Leo Kuipers, a former Fab Academy student. He walked us through the history of microcontrollers, the different programming languages and what programmers and bootloader are. He brought us Arduino Uno kits and walked us through some examples. We then hooked up our own; running the examples on my setup was very easy, after downloading the Arduino IDE through the Tools menu I set Board to Arduino UNO and Port to my USB port.

Through the File menu I opened up the blink example, click the upload button and watch my arduino blink away.

We then played around with the delay to change the blinking speed, and how we can use that to dim the light.
// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level)
delay(20);
digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
delay(2);
}

Thonny
Then we moved on to our QPAD-Xiao, which we had to solder first. Henk shows us how it's done, we first solder on the two resistors, the Xiao RP2040 microcontroller and the OLED screen.

Leo shows us how to first nuke our device by following the instructions in the Raspberry Pi documentation, this clears the device from any previous code. Now we can open up the Thonny Python IDE and start sending instructions to our QPAD. After opening up Thonny I go to the settings and set the interpreter to MicroPython (Raspberry Pi Pico) and click Install or update MicroPython to install MicroPython on the device.

Using the Xiao documentation we look up which pin number corresponds to what. We then use the shell in Thonny to directly send instructions to the device.
>>> import machine
>>> ledB = machine.Pin(25, machine.Pin.OUT)
>>> ledB.high()
>>> ledB.low()
Afterwards we load examples from Quentin to test the Neopixel, OLED screen and touch buttons.

VSCodium
Next up I do the same thing on VSCodium by following these steps:
- Install Raspberry Pi Pico extension in VSCodium
- Nuke my Xiao by following the instructions here: https://www.raspberrypi.com/documentation/microcontrollers/pico-series.html#resetting-flash-memory (auto ejects and remounts)
- Upload Micropython by following the instructions here (pico version): https://www.raspberrypi.com/documentation/microcontrollers/micropython.html#drag-and-drop-micropython
- Make sure my device isn't in BOOTSEL mode and in VSCodium press cmd+shift+p and run MicroPico: connect. This adds a new toolbar to the bottom of the VSCodium window with the connection status.
Now I can run any code on the device by clicking the run button when the device is connected. To upload a file or library to the device right click it in the explorer sidebar and select Upload file to Pico. Any file called main.py will load on boot.
Takeaway
Each toolchain comes with a different trade-off.
- Arduino IDE is the most straightforward to get started with. It hides a lot of complexity, which makes it great for quick experiments, but less flexible once things get more custom.
- Thonny (MicroPython) offers the fastest feedback loop. Being able to run and tweak code directly on the board makes it ideal for testing ideas and iterating quickly.
- VSCodium is the most powerful setup, especially for larger or more structured projects, but also requires more configuration and understanding of the underlying system.
Browse a datasheet
Since we're working with the RP2040 microcontroller this week I've studied this datasheet. Rather than reading it front to back, I focused on the parts that actually relate to what I was building. The datasheet also clarifies how communication works on the board. Protocols like I²C, SPI, and UART are all supported, which explains how components like the OLED screen are connected and controlled in practice. The document itself is quite dense, but even skimming it helps to get a better sense of both the limitations and capabilities of the microcontroller.
Writing a program
Visualizer
To see if I can control the touch buttons I decide to build some simple visualizations that can be controlled through the buttons. I start by studying the examples by Quentin that lets you control the LED color. For the touch buttons it uses a step response library and some "if else logic" to determine whether a button is pressed. By playing around with the OLED example I figure out the top 16px are yellow, while the rest of the screen is blue.
Then I explain to ChatGPT:
- that I'm working with MicroPython
- how my OLED is set up
- which pins my buttons are
- how the "if else logic" works
- that I want the yellow pixels to display a status bar
Now I ask ChatGPT to make me a plasma like visualization example that is button controlled and uses the status bar to give feedback on the button pressed. I copy the code ChatGPT gives me and run it on the QPAD. It works, though I don't really like the visualization all that much. The top and left button control the speed, the right and bottom button control the size, the lower button on the right side of the screen freezes the animation and the upper one randomizes the visualization.

It's hard to photograph the full screen, cause the OLED refreshes the screen line-by-line while my iPhone camera captures images using a rolling shutter, so their mismatched scanning timing causes only part of the display to appear in the photo.
Since the visualization isn't to my liking I ask ChatGPT to come up with something more smooth and blobby. This just gives me some floating balls whose speed and size I can adjust. Cute, but still boring. I ask ChatGPT for anything else and it gives me a Star Wars like starfield viz, kinda fun.
I do this a bunch more times until I have a whole list of python files all with a statusbar, button control and a visual. My favorite of all is something called a Moire pattern, which I asked ChatGPT to generate. Depending on the zoom level it goes through some pretty cool phases from more geometric to more organic.
While I'm noticing more success when I ask ChatGPT for specific visualizations, when I ask for a reproduction of the famous Joy Division album it looks nothing like it. I look only for some code online that does a good job at this visualization and find this blog by Max Halford. I ask ChatGPT to base it's code on this blog and it gets pretty close.

Joy Division never looked more adorable. That’s enough for the visualization for now. I could add a selection menu and clean things up with classes for the visualization and status bar, but that would mostly be standard Python optimization – not really new territory for me – so I’m moving on.
Retro gamer
Next up I want to see if I can run some retro games on the QPAD. I find a repository that does some really cool stuff on the Raspberry Pi Pico, but this has more Flash memory than my QPAD and a screen with more than 1 color, so it's not gonna work. My search continues and I stumble into a repo that turns the Pico into a retro gaming device. This looks doable so I clone the repo locally.
First I ask ChatGPT to 'just make it work' for me, we go through a bunch of iterations end up having to nuke the device multiple times, cause it crashes on start and basically gets nowhere. After some rest the next day I decide to do things the old school way and tweak the code by hand. I start by nuking the device again and uploading only the files I need. In this case only the SSD1306 OLED driver. The repo uses the one by MicroPython, while Quentin's examples use the one by Seeed, the company that makes the Xiao; I upload the latter to the device. For fast debugging I run menu.py directly from VSCodium instead of uploading it to the device.
I start actually looking at the code and quickly replace the OLED settings. The display communicates over I²C, a simple two-wire protocol using SDA (data) and SCL (clock), so the main thing is matching the pins and frequency to my board. This loads the menu on the screen, but of course, none of the buttons work.
- # OLED Screen connected to GP14 (SDA) and GP15 (SCL)
- i2c = machine.I2C(1, sda = Pin(14), scl = Pin(15), freq = 400000)
- oled = SSD1306_I2C(SCREEN_WIDTH, SCREEN_HEIGHT, i2c)
+ # OLED Screen connected to GP6 (SDA) and GP7 (SCL)
+ i2c = I2C(1, scl=Pin(7), sda=Pin(6), freq=200000)
+ oled = SSD1306_I2C(128, 64, i2c)
In the repo the buttons are mapped to variables and then called by a method called .value(). In the MicroPython documentation you can see this is a method of the Pin class. I scan through the games and see this method is used all over the code. If I want to keep the overhaul of the code simple, I need to mimic this behavior and make sure I can assign a button to a variable and read it like up.value(). I decide to make a Button class.
from machine import Pin
from steptime import STEPTIME
LOOP = 200
SETTLE = 20000
THRESH = 10000
class Button:
def __init__(self, btnType, pin, sm_id):
self.type = btnType
self.pin = Pin(pin, Pin.IN, Pin.PULL_UP)
self.min_val = 1e6
self.btnPressed = ''
self.sm = STEPTIME(sm_id, pin)
def value(self):
sm = self.sm
val = self.type
sm.put(LOOP)
sm.put(SETTLE)
result = 4294967296 - sm.get()
if result < self.min_val: # No button is being pressed
self.min_val = result
if result - self.min_val > THRESH: # Button is pressed
if self.btnPressed != val: # Not a long press
self.btnPressed = val
return False
else:
self.btnPressed = ''
return True
This class uses the logic from the LED touch example we saw earlier to determine whether a button is touched. It also enables the internal pull-up resistor on the pin. Now I can instantiate a new button like up = Button('up', 26, 0) where the second argument is the Pin number and the third argument is the ID used by the steptime library, just like in the example.
- up = Pin(2, Pin.IN, Pin.PULL_UP)
- down = Pin(3, Pin.IN, Pin.PULL_UP)
- left = Pin(4, Pin.IN, Pin.PULL_UP)
- right = Pin(5, Pin.IN, Pin.PULL_UP)
- button1 = Pin(6, Pin.IN, Pin.PULL_UP)
- button2 = Pin(7, Pin.IN, Pin.PULL_UP)
+ up = Button('up', 26, 0) # Button args: type, pin_num, state_machine_id
+ right = Button('right', 1, 1)
+ down = Button('down', 2, 2)
+ left = Button('left', 27, 3)
+ button1 = Button('button1', 4, 4)
+ button2 = Button('button2', 3, 5)
It works! I can load the menu screen and scroll through the games with the button. Of course when I actually select a game nothing happens, I haven't uploaded any of the game files to my device yet.

To determine which game to start with I look at the filesize and start with the smallest one: PicoLunarModule.py. This one only has two buttons, easy! I update the OLED config, load in the Button library I made and change the button variables to match my class.
from ssd1306 import SSD1306_I2C
import time
import random
+from button import Button
def pico_lunar_module_main():
- # OLED Screen connected to GP14 (SDA) and GP15 (SCL)
- i2c = I2C(1, sda = Pin(14), scl = Pin(15), freq = 400000)
+ # OLED Screen connected to GP6 (SDA) and GP7 (SCL)
+ i2c = I2C(1, scl=Pin(7), sda=Pin(6), freq=200000)
oled = SSD1306_I2C(128, 64, i2c)
oled.fill(0)
gravity= 1
fuel = 25
fire = 0
- button1 = Pin(6, Pin.IN, Pin.PULL_UP)
- button2 = Pin(7, Pin.IN, Pin.PULL_UP)
+ button1 = Button('button1', 4, 4)
+ button2 = Button('button2', 3, 5)
shift = 0
Tadaaaaa, I can actually play a little game on my device, this is so cool! Next up I upload the game file to the device and test if it loads from the menu as well, all good. I make the same changes in PicoFullSpeed, PicoPong and PicoSnake. The other games are built more elegantly and use a library called PicoGame to share some attributes and methods. Updating this to use my OLED and Button class is also very straight forward.
from framebuf import FrameBuffer, MONO_HLSB
import time
import random
+from button import Button^M
class PicoGame(SSD1306_I2C):
def __init__(self):
self.SCREEN_WIDTH = 128
self.SCREEN_HEIGHT = 64
- self.__up = Pin(2, Pin.IN, Pin.PULL_UP)
- self.__down = Pin(3, Pin.IN, Pin.PULL_UP)
- self.__left = Pin(4, Pin.IN, Pin.PULL_UP)
- self.__right = Pin(5, Pin.IN, Pin.PULL_UP)
- self.__button_A = Pin(6, Pin.IN, Pin.PULL_UP)
- self.__button_B = Pin(7, Pin.IN, Pin.PULL_UP)
+ self.__up = Button('up', 26, 0)
+ self.__right = Button('right', 1, 1)
+ self.__down = Button('down', 2, 2)
+ self.__left = Button('left', 27, 3)
+ self.__button_A = Button('button1', 4, 4)
+ self.__button_B = Button('button2', 3, 5)
- self.__i2c = I2C(1, sda=Pin(14), scl=Pin(15), freq=400000)
+ self.__i2c = I2C(1, scl=Pin(7), sda=Pin(6), freq=200000)
super().__init__(self.SCREEN_WIDTH, self.SCREEN_HEIGHT, self.__i2c)
self.__fb=[] # Array of FrameBuffer objects for sprites
And there you have it, a little retro gaming device run on the QPAD-Xiao. It would be more clean for all the games to use the PicoGame class and we could add arguments to make the button setup configurable, but since this is more classic coding than embedded programming I decided to leave it for now.
Files & resources
Leftovers previous week
- week 2 documentation
- week 3 documentation
- copy editing on previous writing
Further exploration
- Platform.io
- C in MicroPython
- Windows maze screensaver