How Turbo Works
Compile on the computer. Run on the board.
Start with a function in a module that your main program imports. Turbo uses mpy-cross on your computer to compile the selected function for the board's CPU architecture. It puts the compiled code in a .mpy file, and compatible CircuitPython firmware loads it when you import the module. Compile the function once, then copy the prepared module to the board. The board can call that function over and over, running every required loop without asking the computer to compile it again. Recompile when you change the function or target architecture.
The animation below follows the current Turbo toolchain.
Source code is the Python you write. CircuitPython normally compiles it into bytecode, a compact instruction stream that its virtual machine executes. Machine code contains instructions the target CPU executes directly. The interpreter itself is also software running on that CPU; native code removes interpreter work from the compiled function, but may still call runtime helpers.
AOT means ahead of time: Turbo compiles on your computer before the board imports the module. And .mpy is the lunchbox, not the lunch. It is a file container that can hold bytecode or native code. A filename ending in .mpy alone does not promise a speedup. Native contents must match the board's architecture and firmware.
Sources: MicroPython's compiler, the .mpy format, Turbo's toolchain.
Keep the fast function small
Native mode compiles control flow while retaining Python objects. Viper can also use machine-word integers and pointers, which can remove much more overhead from a suitable inner loop. The Mandelbrot example uses integer fixed-point math for this reason.
Keep code.py as the coordinator. Put the work to accelerate in an imported module, such as src/pixels.py. The Turbo shim places lib/turbo/<arch>/ ahead of src/ in the import search path, so an installed native module is found first. Import turbo before importing that module.
Keeping the source in a separate directory matters: a .py beside a same-named .mpy can take priority. The provided layout preserves the readable source without accidentally selecting it first.
Choose work that can benefit
Start with something you can time: a pixel transform, a pass through a buffer, or an integer calculation. Measure the function before changing it, and check whether a built-in CircuitPython operation already does the job. If most of the time is spent waiting for a sensor, a file, or the display, work on that part of the project too.
Source fallback is selected at import time when the native path or module is absent. It is useful for portability and comparison; it does not recover from a faulty native module. Native code needs the right architecture and compatible firmware.
Counting money in cents lets you represent $1.50 as the integer 150. This Mandelbrot example uses the same idea with 4,096 steps per unit: 12 fractional bits, because 2**12 = 4096. So 1.5 is stored as 6144. The scale is agreed in advance; the point stays in its assigned seat.
For x*x and y*y, the code shifts right by 12 after multiplication to restore the scale. Viper's machine-word integers make this useful for the loop, but range and rounding still matter. On RP2040, these integers are 32 bits; they do not automatically grow to hold larger values as Python integers do. Check intermediate values as well as final answers.
Sources: the actual fixed-point loop, Viper's integer rules.
CUDA is NVIDIA's parallel computing platform and programming model. In its usual host/device model, CPU code launches work on an NVIDIA GPU. A kernel is a function launched for execution on the GPU, usually across many threads organized into blocks and grids.
Imagine sending a tray of similar jobs to a kitchen with many cooks. You still have to divide the work, make the ingredients available, and collect the results. Data movement and coordination count toward the bill.
Turbo's compiled functions here run on the microcontroller's CPU. The RP2040 has no NVIDIA GPU, and these examples do not use CUDA. The shared idea is choosing a suitable part of a program to accelerate. The hardware and execution model differ.
Sources: NVIDIA's CUDA programming model, RP2040 specifications.
Try a different kind of optimization
Jeff Epler's Game of Life example shows another useful approach: change how the Python code accesses and copies data. His comments explain why flat bitmap indexes can help, how two buffers reduce copying, and what that costs in readability. It is a useful example to study before choosing a function to compile.
Page last edited September 11, 2026
Text editor powered by tinymce.