Bringing TensorFlow & PyTorch Models to CircuitPython

Tensorflow model performing handwritten digit inference on Raspberry Pi Pico

CircuitPython is loved for its simplicity, excellent documentation, and rich ecosystem of libraries. But when it comes to machine learning, especially deep learning, things quickly get complicated.

Training models? Impossible on-device.
Running inference? Painfully slow if done in pure Python.

In this article, I’ll show you a practical and scalable approach to bridge that gap:

Train your neural network in TensorFlow or PyTorch, and run it inside CircuitPython using a single line of Python code.

outcome = digit_classifier.invoke(image_data)

Yes — real CNN inference in CircuitPython, backed by native C code, without rewriting your model logic by hand.

Why This Approach?

CircuitPython excels at developer experience:

But it is not designed for heavy numerical workloads like neural network inference.

I’ve previously experimented with:

While technically possible, these approaches suffer from:

  • Tedious development
  • Error-prone math
  • Very slow inference (often seconds per run)

Instead of fighting CircuitPython’s design philosophy, this approach embraces it.

The Core Idea

  1. Train the model on your computer

  2. Export the model to ONNX

  3. Convert ONNX → C, using onnx2c

  4. Compile the model into CircuitPython

  5. Expose it as a Python module

The result is the best of both worlds:

  • Python-level simplicity
  • C-level performance

Prerequisites

Before starting, ensure you have:

  1. CircuitPython (source build, tested with commit fc6bf9648b)
  1. onnx2c, tested with commit e0d9b35

  2. A TinyML-suitable neural network

  1. Basic C/C++ understanding. You won’t write much, but knowing headers and build systems helps

📌 If you are new to TinyML, here are some cool TinyML projects to get you started:
https://ashishware.com/tags/tinyml/

Step 1: Build CircuitPython from Source

CircuitPython provides excellent documentation for building from source:

📘 Official guide
https://learn.adafruit.com/building-circuitpython

Key Things to Verify

  • You are using GCC 14 for ARM cross-compilation (as required by recent builds)
  • You can successfully generate a UF2 file:
make BOARD=raspberry_pi_pico

If this works, your build environment is ready.

Step 2: Train and Export the Neural Network

The model used in this article is a small CNN for handwritten digit classification. It is a tiny CNN model that takes a 30x30 handwritten digit image as input and tries to classify the digit (0-9). Here is link to the notebook that trains the model and exports weights to onnx.
https://www.kaggle.com/code/finalepoch/digit-classifier-to-onnx

⚠️ Important:
Your model must be TinyML-friendly:

  • Small input size
  • Few layers
  • No unsupported operators

Step 3: Build and run onnx2c

onnx2c converts an ONNX model into plain C code, which can then be compiled into any embedded project. You will need to clone the repository and build the tool from source first.

📦 Repository:
https://github.com/kraiskil/onnx2c

Follow the instructions to on the website to generate a pure C version of your model from onnx dump of your tensorflow or pytorch model:

onnx2c cnn_model.onnx > digit_classifier.c

No CircuitPython-specific steps are required here. You can also download a pre-built file for handwritten digit recognition here :
https://github.com/code2k13/neural_network_cp/blob/main/src/digit_classifier.c

Step 4: Integrate the Model into CircuitPython

This is where everything comes together.

4.1 Create a New Shared Binding

Inside the CircuitPython source tree:

circuitpython/
└── shared-bindings/
└── digit_classifier/

Add the following files:

Shared bindings are how native C modules are exposed to CircuitPython.

4.2 Define the Model Interface

The digit_classifier.h contains signature for the function to invoke model:

#ifndef MICROPY_INCLUDED_SHARED_BINDINGS_DIGIT_CLASSIFIER_H
#define MICROPY_INCLUDED_SHARED_BINDINGS_DIGIT_CLASSIFIER_H

void entry(const float tensor_input[1][30][30][1], float tensor_output[1][10]);

#endif // MICROPY_INCLUDED_SHARED_BINDINGS_DIGIT_CLASSIFIER_H

This function signature is not generated by onnx2c and has to be created manually. It acts as the bridge between Python and C.

4.3 Enable the Module in the Build System

Add following line to ports/raspberrypi/mpconfigport.mk

CIRCUITPY_ADD_MODULES += digit_classifier

Add below line to ports/raspberrypi/Makefile

SRC_SHARED_BINDINGS += shared-bindings/digit_classifier/__init__.c

SRC_C += shared-bindings/digit_classifier/__init__.c
SRC_C += shared-bindings/digit_classifier/digit_classifier.c

Add below line to py/circuitpy_mpconfig.h

#include "shared-bindings/digit_classifier/__init__.h"
#define CIRCUITPY_DIGIT_CLASSIFIER (1)

4.4 Build and Flash

make BOARD=raspberry_pi_pico

Flash the generated UF2 file to your board.

Step 5: Run Inference from CircuitPython 🎉

import digit_classifier
import array

# Create dummy input
data = array.array('f', [0.1] * 900)

# Run model inference
res = digit_classifier.invoke_model(data)

print(res)

Your neural network is now running inside CircuitPython, using compiled C code.

Performance Results

Approach Inference Time
Pure CircuitPython CNN ~2 seconds
C-backed ONNX model ~0.16 seconds

That’s over 10× faster, with a cleaner and more maintainable workflow.

Complete Working Example

A fully working reference implementation is available here:
https://github.com/code2k13/neural_network_cp/tree/main/example

This includes full content of modified files along with end to end example, that captures pictures using OV7670 camera and displays classification results on tiny TFT LCD screen.

Conclusion

This experiment significantly changed how I approach ML in CircuitPython.

Key Takeaways

  • ✅ Easier than expected
  • 🚀 Massive performance improvement
  • 🧠 Train once, deploy anywhere
  • 🧩 Reusable integration pattern

Most importantly, this article demonstrates a repeatable blueprint for embedding deep learning models into CircuitPython—without sacrificing performance or developer experience.

If you love CircuitPython and want real ML inference on microcontrollers, this approach is the most practical path forward today.