Graeme Winter

Hacking CircuitPython

Adafruit boards are primarily supported by CircuitPython rather than MicroPython - the former is a dialect of the latter, but with an emphasis on user friendly getting started rather than a low level language. In particular, some things I was used to from MicroPython on rp2040 - mem32, asm_thumb and native extension modules were missing.

Spoilers, turns out most of those things are there, if you know where to look, as CircuitPython does pull from upstream from time to time. You just need to maintain your own custom port.

Custom Port

This is fairly well documented on the web page so doesn’t need describing here. Key point is usually you have a finite working space for your formware (e.g. 512kB in ItsyBitsy M4) and the build needs to fit in there. This will involve some trade-off since CircuitPython tends to include the kitchen sink. My personal trade-off was to ditch the use of animated GIFs and BLE in favour of the dynamic runtime and uctypes modules. In mpconfigboard.h:

#define MICROPY_PY_UCTYPES (1)

and in mpconfigboard.mk:

CIRCUITPY_ENABLE_MPY_NATIVE = 1
CIRCUITPY_GIFIO = 0
CIRCUITPY_BLEIO_HCI = 0
MICROPY_ENABLE_DYNRUNTIME = 1

i.e. enable native code emitter, dynamic runtime and disable GIF, BLE. This build still fits in the flash:

Memory region         Used Size  Region Size  %age Used
FLASH_BOOTLOADER:          0 GB        16 KB      0.00%
  FLASH_FIRMWARE:      487168 B       488 KB     97.49%
FLASH_FILESYSTEM:          0 GB         0 GB
    FLASH_CONFIG:          0 GB         0 GB
       FLASH_NVM:          0 GB         8 KB      0.00%
             RAM:       34292 B       192 KB     17.44%

488508 bytes used, 11204 bytes free in flash firmware space out of 499712 bytes (488.0kB).
34288 bytes used, 162320 bytes free in ram for stack and heap out of 196608 bytes (192.0kB).

Modules

Once the changes above are made everything works like it did before, with MicroPython (only defined MPY_DIR to point at the circuitpython sources) -> win. Build the mandelbrot.mpy module from here as an example…

Usage

Since this is now CircuitPython can make use of some different features like the built in usb_cdc.data connection as a second UART over the USB connection, which is rather tidy. To enable this, create boot.py with:

import usb_cdc

usb_cdc.enable(console=True, data=True)

this has to go in boot.py as it needs to be run before the console connection etc. are made. WIth that in place:

import time
import mandelbrot
from uctypes import addressof

import digitalio
import board
import usb_cdc

def main():
    
    led = digitalio.DigitalInOut(board.LED)
    led.direction = digitalio.Direction.OUTPUT

    buffer = bytearray(1280*4)
    address = addressof(buffer)

    t0 = time.time()
    for i in range(1280):
        Ci = (-5 << 22) + 0x4000 + i * 0x8000
        mandelbrot.mandelbrot(address, Ci)
        usb_cdc.data.write(buffer)
        usb_cdc.data.flush()
        led.value = not led.value
    t1 = time.time()

    print("Mandelbrot set calculation: %ds" % (t1 - t0))


main()

Is enough to perform the calculation. CircuitPython now includes nearly everything I was used to from MicroPython.