Skip to content

Embedded Programming

Summary

This week we tasked to get a good understanding of the different toolchains and development workflows for available embedded architectures as well as write a program for a microcontroller to interact and communicate.

Approach / idea / acknowledgements

On our local lesson on Thursday we went through different toolchains and development workflows. We also we're 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:
    • Demonstrate and compare the toolchains and development workflows for available embedded architectures
  • Individual assignment:
    • Browse through the datasheet for a microcontroller
    • Write and test a program for an embedded system using a microcontroller to interact (with local input &/or output devices) and communicate (with remote wired or wireless connections)

Comparison

Arduino

Our local lesson on wednesday started with a presentation from Leo Kuipers, former Fab Academy student. He walked us through the history of microcontrollers, the different programming languages and programmers and bootloaders. 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.

Then 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

We then move on to our QPAD-Xiao, but we have to solder those together 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 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:

  1. Install Raspberry Pi Pico extension in VSCodium
  2. Nuke my Xiao by following the instructions here: https://www.raspberrypi.com/documentation/microcontrollers/pico-series.html#resetting-flash-memory (auto ejects and remounts)
  3. Upload Micropython by following the instructions here (pico version): https://www.raspberrypi.com/documentation/microcontrollers/micropython.html#drag-and-drop-micropython
  4. 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 connect. To upload a file to the device right click it in the explorer sidebar and select Upload file to Pico. This is also how to add libraries. Any file called main.py will load on boot.

Datasheet

Since we're working with the RP2040 microcontroller this week I've studied this datasheet.

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 study the examples by Quentin that let's you control the LED color I've loaded earlier. 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 Starwars 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.

I'm noticing more success when I ask ChatGPT for specific visualization's, but when I ask for a reproduction of the famous Joy Division album it looks nothing like it. I look only for some code I can copy 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 get's pretty close.

Joy Division has never looked more adorable. Ok that's the visualization done for now. I could add a selection menu and abstract some of the logic and make a visualization and status bar class, but this would be mostly regular Python code optimization, which isn't a new topic for me, so I move on.

Retro gamer

Next up I want to see if I can run some retro games on the QPAD. I first find a repository that does some really cool stuff on the Raspberry Pi Pico, but this has more Flash memory than my QPAD and 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 get nowhere. After some rest the next day I decide to do things the old school way and code by hand. I start by nuking the device again and uploading only the files I need to the device. 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. 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 map to variables and then called by 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 all over the code to check whether buttons are pushed. 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 arugment 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 an 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 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