turbo: Gecompileerde versnelling voor CircuitPython

CircuitPython bevat al @micropython.native and @micropython.viper. Er hoeft niets aan de taal te veranderen. Wat ontbrak, was een board dat machinecode kan laden en een tool die deze code genereert. turbo biedt beide:

  1. Firmware gebouwd met CIRCUITPYLOADNATIVE=1, wat een loader van 2 tot 3 KB toevoegt zonder een on-board compiler.
  2. Een host CLI die je module compileert met de officiële mpy-cross en deze installeert op een plek waar de importer deze kan vinden.

Op de twaalf boards die tot nu toe zijn gemeten, draait een viper inner loop 19x tot 72x sneller dan dezelfde code als bytecode.

Bestandsstructuur

De CLI verplaatst je bestanden nooit zelfstandig.

  • turbo init maakt een src/ map aan en plaatst daar de shim. Je verplaatst vervolgens de module die je wilt versnellen naar src/.
  • turbo build leest alleen uit src/ en schrijft naar lib/turbo/.
  • code.py wordt door geen enkele opdracht aangeraast.

Vergelijking van de mappenstructuur

Vóór turbo init + turbo build

CIRCUITPY/
├── code.py
└── pixels.py

Na turbo init + turbo build

CIRCUITPY/
├── code.py                    (ongewijzigd, blijft broncode)
├── lib/
│   ├── turbo.py               (DE SHIM: nieuw, 49 regels)
│   └── turbo/
│       ├── turbo.json         (manifest: src sha256, formaten)
│       └── armv7emsp/
│           ├── pixels.mpy     (geïnstalleerde winnaar: viper)
│           ├── pixels.viper.mpy (bewaarde kandidaten)
│           └── pixels.native.mpy
└── src/
    └── pixels.py              (door JOU hierheen verplaatst)

Niets anders op de schijf verandert. Omdat circup de map lib/turbo/<arch>/ letterlijk kopieert, kan dit als een normale bibliotheek worden verzonden.

Werking op de host

Het proces verloopt als volgt voor een bestand zoals src/pixels.py:

  1. Markering: De @turbo.viper marker wordt herkend (dit doet niets op het board, alleen op de host).
  2. Herschrijven: Er wordt een tijdelijke kopie gemaakt waarin @turbo.viper wordt omgezet naar @micropython.viper (en apart voor @micropython.native). Je originele bestand wordt niet bewerkt.
  3. Compileren: De tool voert mpy-cross -march=armv7emsp twee keer uit:
  • Resultaat 1: lib/turbo/armv7emsp/pixels.viper.mpy (639 B)
  • Resultaat 2: lib/turbo/armv7emsp/pixels.native.mpy (1,204 B)
  1. Installatie: De snelste versie wordt geïnstalleerd onder de eenvoudige naam lib/turbo/armv7emsp/pixels.mpy.

Werking op het board tijdens boot

Wanneer het board opstart, gebeurt het volgende:

  1. code.py wordt uitgevoerd (altijd als broncode, nooit gecompileerd).
  2. import turbo wordt aangeroepen:
  • De shim (lib/turbo.py) leest sys.implementation._mpy >> 10 (bijv. resultaat 7).
  • Waarde 7 wordt gekoppeld aan de architectuur "armv7emsp".
  • De shim controleert of /lib/turbo/armv7emsp bestaat.
  • Zo ja, dan wordt deze map, samen met /src, vooraan in sys.path geplaatst.

sys.path BEFORE: ["", "/", ".frozen", "/lib"] sys.path AFTER: ["/lib/turbo/armv7emsp", "/src", "", "/", ".frozen", "/lib"]

  1. import pixels wordt aangeroepen:
  • De eerste hit is /lib/turbo/armv7emsp/pixels.mpymachinecode.

Belangrijk: De map src/ staat standaard niet in sys.path. Dit is bewust gedaan omdat CircuitPython in dezelfde directory altijd voorkeur geeft aan name.py boven name.mpy. Door de broncode te verbergen in een map die de importer niet direct ziet, werkt de versnelling zonder dat de importer zelf aangepast hoeft te worden.

Scenario's van importen

Situatieturbo.archImporteert vanuit
turbo firmware, arch directory gebouwdarmv7emsp/lib/turbo/armv7emsp/pixels.mpy
turbo firmware, geen dir voor die archxtensawin/src/pixels.py
stock firmware, geen native supportNone/src/pixels.py

Consequenties:

  • Als je pixels.mpy verwijdert, landt de import op /src/pixels.py: het resultaat is identiek, maar trager.
  • code.py kan niet worden versneld, omdat dit via pyexec_file wordt uitgevoerd en niet via import.

Voorbeeld van een sessie

Verbatim van de farm Metro RP2040, 2026-09-08.

$ turbo doctor
board       Adafruit Metro RP2040         adafruit_metro_rp2040
port        /dev/ttyACM18
drive       /media/sklarm/CIRCUITPY5
firmware    CircuitPython 10.3.0-42-g3cdb20693f
_mpy        0x1306   arch armv6m · mpy 6.3 · native loader present
dev build of 10.3.0; using the 10.3.0 mpy-cross, abi checked below
toolchain   ~/.cache/turbo/mpy-cross/10.3.0/linux-amd64/mpy-cross   1411 KB   mpy v6.3
ready       turbo build compiles -march=armv6m

$ turbo init --example
wrote  lib/turbo.py              shim, 49 lines, identity decorators on stock firmware
made   src/                      your source, kept off sys.path so it never shadows .mpy
made   lib/turbo/armv6m/         where compiled modules land
wrote  src/pixels.py             mandelbrot, 12-bit fixed point, @turbo.viper
wrote  code.py                   imports turbo, then pixels; prints the checksum

$ turbo build
pixels     viper   armv6m       603 B      native     635 B
1 built, 1081 ms, copied 1 module, shim to /media/sklarm/CIRCUITPY5

$ turbo bench pixels
board: mpy v6.3 arch armv6m on /dev/ttyACM18
bytecode  median    8335.3 ms  value 407644
native    median    4778.0 ms  value 407644
viper     median     422.9 ms  value 407644
installed viper for armv6m (19.71x over bytecode)

De doctor functie haalt bij eerste gebruik de officiële mpy-cross op en cached deze, zodat er geen CircuitPython checkout of toolchain build nodig is. De waarde 407644 is een checksum; als een variant een andere checksum heeft, is het een ander programma en wordt deze niet geïnstalleerd.

De CLI bevindt zich nu in een eigen repository: mikeysklar/turbo-cli en zal beschikbaar komen op PyPI als adafruit-turbo.

Prestatiebenchmarks

De volgende resultaten zijn behaald met een Mandelbrot-test (160x120, 64 iteraties, mediane waarde van 8 metingen, in milliseconden).

BoardCorefloat bytecode@native@viperviper / float
Metro M0 ExpressCortex-M0+ 48 MHz72,74123,4081,01471.7x
Metro RP2040Cortex-M0+ 125 MHz13,9424,73938436.3x
Metro M4 AirLiftCortex-M4F 120 MHz11,2213,53643126.0x
Feather nRF52840Cortex-M4F 64 MHz20,7887,44578126.6x
Metro ESP32-S2Xtensa LX7 240 MHz7,4382,11020636.1x
Metro RP2350Cortex-M33 150 MHz6,3712,38026124.4x
Metro ESP32-S3Xtensa LX7 240 MHz4,8731,70818626.2x
Feather STM32F405Cortex-M4F 168 MHz8,1212,77941619.5x
ESP32-C5 DevKitCRISC-V rv32imc 240 MHz7,5911,80917244.0x
nRF54L15 DKCortex-M33 128 MHz10,2332,83634929.3x
nRF54LM20 DKCortex-M33 128 MHz10,2882,84035129.3x
EK-RA8D1Cortex-M85 480 MHz1,8024884738.3x

De snelheid van float bytecode is nagenoeg ongewijzigd (onder 0,1%) door de toevoeging van de loader. De volledige tabel is te vinden in docs/turbo-on-the-farm.md.

Handmatige Workflow

Voor de CLI bestond het proces uit de volgende handmatige stappen:

# Eenmalig per CircuitPython tag: mpy-cross en loader firmware bouwen
$ make -C mpy-cross -j4
$ make -C ports/raspberrypi BOARD=adafruit_metro_rp2350 CIRCUITPY_LOAD_NATIVE=1 -j4

# Per wijziging: compileren voor de core van het board, broncode buiten sys.path houden
$ ~/cp-1030/mpy-cross/build/mpy-cross -march=armv7emsp src/pixels.py -o lib/turbo/armv7emsp/pixels.mpy
$ cp lib/turbo.py lib/turbo/armv7emsp/pixels.mpy src/pixels.py -> CIRCUITPY

# Het image: firmware plus project als één UF2
$ folder2uf2 --board adafruit_metro_rp2350 --combine firmware.uf2 -o turbo-demo.uf2 turbo-demo/

# Bewijzen: benchmarks draaien
$ python3 tools/pyboard.py /dev/cu.usbmodem14201 bench.py

De CLI automatiseert bijna al deze complexe stappen.

Firmware verkrijgen

Download de .uf2 voor jouw board uit de nieuwste cp-<versie> release, of via de command line: gh release download cp-10.3.0 -R mikeysklar/turbo -p 'metro_esp32s3.uf2'

Elke release bevat ook een bijbehorende mpy-cross voor Linux en een BUILD.txt per board met de exacte commit, toolchain en flags. Instructies voor het bouwen van een nieuwe tag staan in docs/build.md.

Huidige Status

  • ESP32-S2 en ESP32-S3 (Xtensa): Werkend, geverifieerd op hardware.
  • ESP32-C5 (RISC-V): Werkend, geverifieerd op hardware (172 ms op de Mandelbrot loop, 44x sneller dan float bytecode).
  • ESP32-C3 / C6 / P4: Gecompileerd, maar nog niet getest op hardware.

Loader-only model

Sinds 2026-09-06 wordt het "loader-only" model gehanteerd. Omdat turbo op de host compileert met mpy-cross, heeft het board alleen de native .mpy loader nodig en niet de on-board emitter. CIRCUITPYLOADNATIVE=1 bouwt dit:

  • Gebruik van 2 tot 3 KB meer dan stock (in plaats van 20 tot 50 KB).
  • Dezelfde viper-snelheid.
  • @micropython.viper vanuit broncode veroorzaakt een SyntaxError.

Ondersteunde Boards

BoardArchNative / viperBronIn release
Metro RP2040Thumb (armv6m)loader-only, no emitterfork branchemitter build
Metro RP2350Thumb (armv7em)loader-only, no emitterfork branchemitter build
Feather nRF52840Thumb (armv7em)loader-only, no emitterfork branchemitter build
Feather STM32F405Thumb (armv7em)loader-only, no emitterfork branchemitter build
Metro M0 ExpressThumb (armv6m)loader-only, no emitterfork branchniet nog niet
Metro M4 AirLiftThumb (armv7em)loader-only, no emitterfork branchniet nog niet
EK-RA8D1Thumb (armv7emdp)werkt, D-cache aanfork branchniet nog niet
Metro ESP32-S2Xtensa LX7loader-only, no emitterfork branchemitter build
Metro ESP32-S3Xtensa LX7loader-only, no emitterfork branchemitter build
ESP32-C5 DevKitCRISC-V (rv32imc)werktfork branchniet nog niet
ESP32-C3/C6/P4RISC-Vniet getestfork branchniet nog niet

ESP32 Firmware Wijzigingen

Er zijn vijf commits gedaan op de esp32-native branch van de CircuitPython fork: 1 Selectie van de native emitter op basis van architectuur, niet alleen Thumb. 2 Commit van native machinecode in executeerbare RAM (de GC heap is niet executeerbaar). 3 Geheugenbescherming uitschakelen wanneer native code is ingeschakeld. 4 De Xtensa assembler laten compileren onder CircuitPython's warning flags. 5 De ARM Thumb interworking bit niet instellen op non-ARM pointers.

De laatste wijziging loste een bug op waarbij MICROPYMAKEPOINTER_CALLABLE de ARM Thumb bit in elke codepointer plaatste, wat op Xtensa leidde tot een sprong naar een oneven adres en een hard-fault bij de eerste native call.

Architectuur ID's

De waarde van sys.implementation._mpy >> 10 correspondeert met de volgende architecturen:

IDArchIDArch
4armv6m8armv7emdp
5armv7m9xtensa
6armv7em10xtensawin
7armv7emsp11rv32imc

Inhoud van deze repository

  • shim/turbo.py: De on-board @turbo decorator.
  • cli/turbo_cli.py: De originele host tool (actieve ontwikkeling is verplaatst naar mikeysklar/turbo-cli).
  • examples/mandelbrot/: De benchmark gebruikt voor de bovenstaande cijfers.
  • docs/: Documentatie over conversieprocedures, farm notes, applicaties, werklogboek, analyses en build-instructies.
  • .github/workflows/firmware.yml: Bouwt alle boards en publiceert de release.

Toekomstige ontwikkelingen: Webinterface

Er wordt gewerkt aan een webpagina-versie van de CLI. Deze zal gebruikmaken van WebSerial om:

  • Het board te detecteren.
  • Een .py bestand te lezen.
  • Te adviseren welke functies gecompileerd moeten worden.
  • De installatie uit te voeren en de snelheid voor en na de optimalisatie te meten op het eigen board van de gebruiker.

Een skelet hiervan is beschikbaar in mikeysklar/turbo-web.