This is an evolving collection of tips and tricks about working on the CircuitPython core and related libraries. Topics include code analysis and debugging, working with git and GitHub, maintaining documentation, and install/config for dev tools. Mostly these are notes to future-me, but perhaps they will also help other people who want to improve CircuitPython.
Control Flow Graphs
When you're getting started working on a new-to-you module of complex code, it can be pretty hard to build a picture in your head of how the code works just from reading the text. Fortunately, there are tools which can analyze source code and build actual pictures for you with directed graphs of which functions call which other functions.
Below are some examples of how I've been generating graphs of control flow between the various functions in CircuitPython's usb module and TinyUSB library dependency. To make the graphs, I use Debian with the cflow code analysis tool to generate a graphviz (.dot) graph file, then turn that into a PNG image to view with the mirage image viewer. For mirage to work, you need to run the last command in a terminal in X (vnc or physical display will work, but not ssh).
shared-bindings/usb/*
This example builds a directed graph of functions calls within the CircuitPython shared-bindings/usb/* code. The grep -v ... stuff is removing some macros that would clutter up the graph without adding much benefit. The sed '2i ...' stuff is inserting a line with additional graphviz attributes to improve the layout of the graph spec file created by cflow. The sfdp command is part of graphviz, and it's designed to lay out complex graphs.
cd ~/adafruit/circuitpython/shared-bindings/usb
cflow -i _ --all --omit-arguments -f dot {.,core,util}/{*.c,*.h} \
| sed '2i overlap=false; K=0.4; repulsiveforce=0.8; smoothing="power_dist"' \
| grep -v 'MP_' > ~/cflow.dot
sfdp -Tpng ~/cflow.dot > ~/cflow-shared-bindings-usb.png && mirage ~/cflow-shared-bindings-usb.png
cd circuitpython/shared-module/usb
cflow -i _ --all --omit-arguments -f dot {.,core}/{*.c,*.h} util/*.c \
| sed '2i overlap=false; K=0.9; repulsiveforce=0.8; splines=true;' \
| grep -v 'MP_' > ~/cflow.dot
sfdp -Tpng ~/cflow.dot > ~/cflow-shared-module-usb.png && mirage ~/cflow-shared-module-usb.png
supervisor/shared/usb.c
For this one, I'm using --preprocess='arm-none-eabi-gcc -E' and a bunch of -D ... defines and -I ... include paths to run the preprocessor approximately like a make BOARD=adafruit_fruit_jam command.
cd circuitpython/supervisor cflow \ -D CIRCUITPY_USB_CDC \ -D CIRCUITPY_USB_DEVICE \ -D CIRCUITPY_USB_HOST \ -D DISABLE_FILESYSTEM \ -I . \ -I lib/tinyusb/src \ -I ports/raspberrypi/ \ -I ports/raspberrypi/boards/adafruit_fruit_jam \ -I ports/raspberrypi/build-adafruit_fruit_jam \ -I ports/raspberrypi/sdk_config \ -I ports/raspberrypi/sdk/src/common/pico_base_headers/include \ -I ports/raspberrypi/sdk/src/common/pico_sync/include \ -I ports/raspberrypi/sdk/src/common/pico_time/include \ -I ports/raspberrypi/sdk/src/host/pico_stdio/include \ -I ports/raspberrypi/sdk/src/rp2_common/cmsis/ \ -I ports/raspberrypi/sdk/src/rp2_common/hardware_base/include \ -I ports/raspberrypi/sdk/src/rp2_common/hardware_sync_spin_lock/include \ -I ports/raspberrypi/sdk/src/rp2_common/hardware_sync/include \ -I ports/raspberrypi/sdk/src/rp2_common/hardware_timer/include \ -I ports/raspberrypi/sdk/src/rp2_common/pico_platform_compiler/include \ -I ports/raspberrypi/sdk/src/rp2_common/pico_platform_panic/include \ -I ports/raspberrypi/sdk/src/rp2_common/pico_platform_sections/include \ -I ports/raspberrypi/sdk/src/rp2_common/pico_stdio_usb/include \ -I ports/raspberrypi/sdk/src/rp2350/hardware_regs/include \ -I ports/raspberrypi/sdk/src/rp2350/hardware_structs/include \ -I ports/raspberrypi/sdk/src/rp2350/pico_platform/include \ -i _ \ --preprocess='arm-none-eabi-gcc -E' \ --omit-arguments \ -f dot supervisor/shared/usb/usb.c \ | sed '2i overlap=false; K=0.4; repulsiveforce=0.8; smoothing="power_dist"' \ | grep -v 'MP_' > ~/cflow.dot neato -Tpng ~/cflow.dot > ~/cflow-supervisor-usb.png && mirage ~/cflow-supervisor-usb.png
Printf Debugging
If you want to do basic printf debugging within the CircuitPython core, you can put mp_printf() lines pretty much wherever you want (but be careful about interrupt handlers):
mp_printf(&mp_plat_print, "this is a format string. the number is: %x\n", 0x4F);
CAUTION: There are a variety of reasons why your mp_printf() lines might not get flushed to the serial port immediately. So, if you want feedback with very low latency, this might be an unsuitable choice.
LED Debugging
This code snippet (git diff below) modifies the CircuitPython core USB implementation to turn on the Fruit Jam's red LED when a USB device is plugged in and off when it's unplugged. The LED is active low and shares pin 29 with the IR decoder, so LED drive is open drain.
You could adapt this for any situation where you want a 1-bit debugging indicator while working on the CircuitPython core. To get multiple bits, or higher frequency, you'd probably want to use a logic analyzer on the GPIO header pins and modify the debug macros to use push-pull output instead of open drain.
diff --git a/shared-module/usb/core/Device.c b/shared-module/usb/core/Device.c
index c9df0cf738..f650ed318a 100644
--- a/shared-module/usb/core/Device.c
+++ b/shared-module/usb/core/Device.c
@@ -18,16 +18,35 @@
#include "supervisor/shared/tick.h"
#include "supervisor/usb.h"
+// === DON'T MERGE THIS ===
+#include "hardware/gpio.h"
+#define FRUIT_JAM_LED_PIN (29)
+#define DEBUG_GPIO_OUT_LOW(pin) { \
+ gpio_put(pin, 0); \
+ gpio_set_dir(pin, GPIO_OUT); \
+ gpio_set_function(pin, GPIO_FUNC_SIO); \
+}
+#define DEBUG_GPIO_HIGHZ(pin) { \
+ gpio_set_function(pin, GPIO_FUNC_NULL); \
+}
+// =========================
+
// Track what device numbers are mounted. We can't use tuh_ready() because it is
// true before enumeration completes and TinyUSB drivers are started.
static size_t _mounted_devices = 0;
void tuh_mount_cb(uint8_t dev_addr) {
_mounted_devices |= 1 << dev_addr;
+ // === DON'T MERGE THIS ===
+ DEBUG_GPIO_OUT_LOW(FRUIT_JAM_LED_PIN);
+ //mp_printf(&mp_plat_print, "m%x\n", dev_addr);
}
void tuh_umount_cb(uint8_t dev_addr) {
_mounted_devices &= ~(1 << dev_addr);
+ // === DON'T MERGE THIS ===
+ DEBUG_GPIO_HIGHZ(FRUIT_JAM_LED_PIN);
+ //mp_printf(&mp_plat_print, "u%x\n", dev_addr);
}
static xfer_result_t _xfer_result;
TinyUSB Debug Log
TinyUSB has built in debug logging that can be routed to a UART with a serial adapter or RTT with a debug probe. To enable UART logging for Fruit Jam:
- In
ports/raspberrypi/boards/adafruit_fruit_jam/mpconfigboard.h, uncomment the lines for the UART pins and TinyUSB debug:// #define CIRCUITPY_CONSOLE_UART_TX (&pin_GPIO44)
// #define CIRCUITPY_CONSOLE_UART_RX (&pin_GPIO45)
// #define CIRCUITPY_DEBUG_TINYUSB 0 - Increase the TinyUSB log level from 0 to 2 (2 means show USB host log messages):
#define CIRCUITPY_CONSOLE_UART_TX (&pin_GPIO44)
#define CIRCUITPY_CONSOLE_UART_RX (&pin_GPIO45)
#define CIRCUITPY_DEBUG_TINYUSB 2 - Build and flash a firmware image
(.venv) $ cd ports/raspberrypi/
(.venv) $ make -j BOARD=adafruit_fruit_jam - Connect a Raspberry Pi Debug Probe to the Fruit Jam Debug port and GPIO header:
Black wire (probe ground) to Fruit Jam GND
Yellow wire (probe RX) to Fruit Jam console TX on A4 (GPIO44)
Orange wire (probe TX) to Fruit Jam console RX on A5 (GPIO45) - Start a serial monitor with a large scrollback buffer at 115200 baud to listen to the Raspberry Pi Debug Probe UART (example for Debian):
$ screen -h 9999 -fn /dev/serial/by-id/usb-Raspberry_Pi_Debug_Probe* 115200
Contributing Changes
Contributing fixes or new features to CircuitPython core and CircuitPython libraries requires some effort to follow project conventions and to avoid breaking stuff that was previously working. These guides are a useful starting point:
- https://learn.adafruit.com/contribute-to-circuitpython-with-git-and-github (general info)
- https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library (pre-commit, code linters, etc)
Some things to keep in mind:
- CircuitPython uses a lot of submodule dependencies for libraries and frozen modules. The circuitpython repo has Makefile rules to manage submodules, so you often need to use
maketo manage submodules instead of the standardgit submodule ...command line invocations. For example, supposing you want to build for an RP2350 board, when you initially clone your fork of circuitpython, you might docd ports/raspberrypifollowed bymake fetch-port-submodules. - When you sync your local circuitpython fork to get upstream changes from adafruit/circuitpython, you may need to update submodules for libraries or frozen modules.
- When the upstream repo for a frozen module gets updated with a new tag, there may be a time window when the circuitpython submodule commit reference for that module is set to the previous tag. In that case, using
make update-frozen-librariesmight give you the latest upstream tag instead of the correct one for the current submodule commit. - If you increase the size of code for built-in modules or frozen modules that get included in Atmel SAMD21 builds, you may cause CI build check failures due to the code not fitting in flash for certain translations of some boards. The typical fix for this is to edit board config files and turn off built-in features.
- When you make a pull request (PR) to adafruit/circuitpython, your code will go through many CI checks that can last for up to a half hour or longer. To avoid delays due to simple coding errors, it helps to enable the "pre-commit" tools to run some of the checks on your local computer where they will finish much faster.
Keep Your Fork in Sync
On GitHub, you can just press a button to sync your fork of CircuitPython to the upstream Adafruit repo to follow new commits and releases. In your local repo, you can get the changes with git pull. But... that wont fix your submodules!
Be sure to change directories into the port you're working with, then use make to fetch the port submodules. For example:
Edit Docs for CircuitPython Core
- Documentation comments are in
shared-bindings/. The//|prefix is a MicroPython/CircuitPython preprocessor thing. After the prefix, documentation comments use Sphinx reStructuredText syntax. - To build the docs, do
make htmlfrom the top level of the circuitpython repo - To serve local docs, do
cd _build/html/; python3 -m http.server - View the docs on port 8000 of your dev computer's IP address
Edit Docs for Libraries
CircuitPython libraries use inline documentation with reStructuredText formatted comments. To build local html previews of your documentation edits:
- Make sure you have a python virtual environment (venv) set up and active.
- Make sure your venv has
Sphinxandsphinx-rtd-themeinstalled (that will normally happen if you follow the guides for setting up your dev environment to work on CircuitPython core or library repos) - In a library repo, say perhaps Adafruit_CircuitPython_TLV320, run these shell commands to build the html documentation files and start a local webserver:
cd Adafruit_CircuitPython_TLV320/docs sphinx-build -E -W -b html . _build/html cd _build/html # type Ctrl-C to stop this web server when you're done python3 -m http.server --bind 127.0.0.1
- Once the web server is running, you can view the html documentation by going to http://localhost:8080 in your web browser.
- To stop the web server, type Ctrl-C in the same terminal window where you started it.
cd circuitpython/ports/raspberrypi make fetch-port-submodules
Cynthion USB Analyzer
The Cynthion Getting Started documentation is mostly pretty straightforward and easy to follow. But, there are some things that require extra steps...
Fix udev rule for Debian
On Debian, if you follow the docs to install a 54-cynthion.rules file in /etc/udev/rules.d, those udev rules use TAG+="uaccess" to grant access to the USB device for your Cynthion. The problem with using a uaccess tag is that it only works for users logged into the console (physically attached monitor/keyboard/mouse). So, if you try to remote control your Cynthion over SSH or VNC, you'll get mysterious permission errors.
The fix is easy. Just edit the rules file, changing TAG+="uaccess" to GROUP="plugdev". If for some reason your user account on the Debian box does not already belong to the plugdev group, then add it to plugdev.
The above udev stuff is current at the time I write this (August 31, 2025). Things may change as there are open GitHub issues about udev related permissions issues in the greatscottgadgets/cynthion repo. Related links:
This page (CircuitPython Core Dev & Debug Tricks) was last updated on December 08, 2025.