Parallel MicroPython Benchmarking

On the left, a Raspberry Pi Pico 2W. On the right, a Raspberry Pi Pico. Each is connected to its own small OLED screen. When a button is pressed, both boards calculate and display the Mandelbrot set, along with its completion time. Needless to say, the Pico 2 W is quite a bit quicker.
two small OLED screens side by side on a breadboard. They're the type that are surplus from pulse oximeter machines, so the top 16 pixels are yellow, and the rest of the rows are blue.

The left screen displays: "micropython 1.25.0.preview RP2350 150 MHz 128*64; 120", while the screen on the right shows "micropython 1.24.1 RP2040 125 MHz 128*64; 120"
the before screens …
The same two OLED screens, this time showing a complete Mandelbrot set and an elapsed time for each microcontroller. Pico 2 comes in at 10.3 seconds, original Pico at 19.8 seconds
Pico 2 comes in at 10.3 seconds, original Pico at 19.8 seconds

Stuff I found out setting this up:

  • some old OLEDs, like these surplus pulse oximeter ones, don’t have pull-up resistors on their data lines. These I’ve carefully hidden behind the displays, but they’re there.
  • Some MicroPython ports don’t include the complex type, so I had to lose the elegant z→z²+C mapping to some ugly code.
  • Some MicroPython ports don’t have os.uname(), but sys.implementation seems to cover most of the data I need.
  • On some boards, machine.freq() is an integer value representing the CPU frequency. On others, it’s a list. Aargh.

These displays came from the collection of the late Tom Luff, a Toronto maker who passed away late 2024 after a long illness. Tom had a huge component collection, and my way of remembering him is to show off his stuff being used.

Source:

# benchmark Mandelbrot set (aka Brooks-Matelski set) on OLED
# scruss, 2025-01
# MicroPython
# -*- coding: utf-8 -*-

from machine import Pin, I2C, idle, reset, freq

# from os import uname
from sys import implementation
from ssd1306 import SSD1306_I2C
from time import ticks_ms, ticks_diff

# %%% These are the only things you should edit %%%
startpin = 16  # pin for trigger configured with external pulldown
# I2C connection for display
i2c = machine.I2C(1, freq=400000, scl=19, sda=18, timeout=50000)
# %%% Stop editing here - I mean it!!!1! %%%


# maps value between istart..istop to range ostart..ostop
def valmap(value, istart, istop, ostart, ostop):
    return ostart + (ostop - ostart) * (
        (value - istart) / (istop - istart)
    )


WIDTH = 128
HEIGHT = 64
TEXTSIZE = 8  # 16x8 text chars
maxit = 120  # DO NOT CHANGE!
# value of 120 gives roughly 10 second run time for Pico 2W

# get some information about the board
# thanks to projectgus for the sys.implementation tip
if type(freq()) is int:
    f_mhz = freq() // 1_000_000
else:
    # STM32 has freq return a tuple
    f_mhz = freq()[0] // 1_000_000
sys_id = (
    implementation.name,
    ".".join([str(x) for x in implementation.version]).rstrip(
        "."
    ),  # version
    implementation._machine.split()[-1],  # processor
    "%d MHz" % (f_mhz),  # frequency
    "%d*%d; %d" % (WIDTH, HEIGHT, maxit),  # run parameters
)

p = Pin(startpin, Pin.IN)

# displays I have are yellow/blue, have no pull-up resistors
#  and have a confusing I2C address on the silkscreen
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)
oled.contrast(31)
oled.fill(0)
# display system info
ypos = (HEIGHT - TEXTSIZE * len(sys_id)) // 2
for s in sys_id:
    ts = s[: WIDTH // TEXTSIZE]
    xpos = (WIDTH - TEXTSIZE * len(ts)) // 2
    oled.text(ts, xpos, ypos)
    ypos = ypos + TEXTSIZE

oled.show()

while p.value() == 0:
    # wait for button press
    idle()

oled.fill(0)
oled.show()
start = ticks_ms()
# NB: oled.pixel() is *slow*, so only refresh once per row
for y in range(HEIGHT):
    # complex range reversed because display axes wrong way up
    cc = valmap(float(y + 1), 1.0, float(HEIGHT), 1.2, -1.2)
    for x in range(WIDTH):
        cr = valmap(float(x + 1), 1.0, float(WIDTH), -2.8, 2.0)
        # can't use complex type as small boards don't have it dammit)
        zr = 0.0
        zc = 0.0
        for k in range(maxit):
            t = zr
            zr = zr * zr - zc * zc + cr
            zc = 2 * t * zc + cc
            if zr * zr + zc * zc > 4.0:
                oled.pixel(x, y, k % 2)  # set pixel if escaped
                break
    oled.show()
elapsed = ticks_diff(ticks_ms(), start) / 1000
elapsed_str = "%.1f s" % elapsed
# oled.text(" " * len(elapsed_str), 0, HEIGHT - TEXTSIZE)
oled.rect(
    0, HEIGHT - TEXTSIZE, TEXTSIZE * len(elapsed_str), TEXTSIZE, 0, True
)

oled.text(elapsed_str, 0, HEIGHT - TEXTSIZE)
oled.show()

# we're done, so clear screen and reset after the button is pressed
while p.value() == 0:
    idle()
oled.fill(0)
oled.show()
reset()

(also here: benchmark Mandelbrot set (aka Brooks-Matelski set) on OLED – MicroPython)

I will add more tests as I get to wiring up the boards. I have so many (too many?) MicroPython boards!

Results are here: MicroPython Benchmarks

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *