Maintenance note (September 1, 2026): This page now begins with a current Raspberry Pi GPIO and CPython-extension workflow. The complete 2019 body remains at the end as a dated archive. Its wiringPi-numbering assumptions, direct
setup.pycommands, and debugging notes describe the old environment and are not the maintained instructions.
The original project controlled 3D-printer motors from Python, observed poor motion, and moved GPIO calls into C. That is a useful performance investigation, but “Python is slow” is not a sufficient diagnosis. GPIO backends, Linux scheduling, per-call overhead, electrical drivers, and the motion controller can each be the limiting factor. This guide separates those layers instead of replacing the whole stack at once.
Table of Contents
1. Choose the narrowest current layer that solves the problem
For a new Raspberry Pi OS project, use this order:
| Need | Maintained starting point |
|---|---|
| Identify header pins | Raspberry Pi’s pinout command and official board diagram |
| Buttons, LEDs, relays through a driver, or ordinary events | GPIO Zero with its default lgpio pin factory |
Existing RPi.GPIO API that cannot yet be redesigned |
Evaluate the rpi-lgpio compatibility implementation and test it on the exact Pi model |
| Native Linux GPIO access | libgpiod, after checking the installed major version |
| CPU-heavy work inside a Python application | A small, measured CPython extension for the batch computation only |
| Deterministic motor pulses or hard real-time behavior | A dedicated motor controller, microcontroller, or suitable hardware peripheral—not a user-space timing loop |
Raspberry Pi’s GPIO best-practices paper identifies the Linux GPIO character-device stack as the forward-looking abstraction. GPIO Zero currently selects LGPIOFactory when the dependency is present, and its documentation says lgpio is the supported factory across all Pi models, including Raspberry Pi 5.
The actively maintained WiringPi repository is not the same as the abandoned release commonly installed by old tutorials. Its version-3 wrappers are not guaranteed to track the C library. For a new Python project, do not introduce wiringPi numbering or an unverified Python wrapper merely to reproduce the 2019 setup.
2. Confirm wiring, numbering, and permissions before writing code
Turn the Pi off before changing connections. Raspberry Pi GPIO uses 3.3 V logic. The official hardware documentation warns against feeding 5 V into 3.3 V components, requires a current-limiting resistor for an LED, and says motors must use an H-bridge or motor-controller board rather than connect directly to a GPIO pin.
Inspect the actual header and character devices:
pinout
ls -l /dev/gpiochip*
id -nG
This guide uses Broadcom GPIO numbers: LED(17) means GPIO17, which is physical header pin 11—not physical pin 17 and not wiringPi pin 17. Record both the BCM number and physical pin in the project’s wiring diagram.
GPIO access normally comes from membership in the gpio group. If a service account is missing that group, have an administrator add the minimum required membership, then start a fresh login session. Do not solve a permissions error by running the whole application as root or by making every /dev/gpiochip* device world-writable.
3. Establish a GPIO Zero baseline
On Raspberry Pi OS, prefer the distribution package. A virtual environment with system packages enabled can reuse its hardware bindings:
sudo apt update
sudo apt install --yes python3-gpiozero python3-venv python3-dev build-essential
python3 -m venv .venv --system-site-packages
. .venv/bin/activate
python -c 'from importlib.metadata import version; print(version("gpiozero"))'
Device.pin_factory is created lazily, so inspect it only after the first device exists. Test application logic without energizing hardware by selecting GPIO Zero’s mock factory:
from gpiozero import Device, LED
from gpiozero.pins.mock import MockFactory
Device.pin_factory = MockFactory()
with LED(17) as led:
led.on()
assert led.value == 1
led.off()
assert led.value == 0
Only after that test passes, connect a current-limited LED to GPIO17 and ground and run the hardware baseline:
from gpiozero import Device, LED
from signal import pause
with LED(17) as led:
print(type(Device.pin_factory).__name__)
led.blink(on_time=0.5, off_time=0.5)
pause()
If this fails on a current Pi, capture the model, Raspberry Pi OS release, kernel, GPIO Zero version, selected pin factory, and /dev/gpiochip* permissions before changing libraries.
4. Measure the bottleneck before adding C
Removing sleep() only removes an intentional delay; it does not reveal whether latency comes from Python dispatch, the GPIO backend, Linux scheduling, or the hardware. Establish three separate measurements:
- application computation with GPIO calls replaced by a fake;
- API-call duration while driving only the safe LED test circuit; and
- physical edge timing measured with a logic analyser or oscilloscope.
This small diagnostic measures Python-to-backend call duration, not the exact time of the electrical edge:
from statistics import median
from time import perf_counter_ns
from gpiozero import LED
durations = []
with LED(17) as led:
for _ in range(1000):
started = perf_counter_ns()
led.toggle()
durations.append(perf_counter_ns() - started)
print({
"minimum_ns": min(durations),
"median_ns": int(median(durations)),
"maximum_ns": max(durations),
})
Run it on an otherwise representative system and repeat it rather than trusting one result. Large maximum latency matters for pulse generation even when the median looks good. Moving the loop to C can reduce Python call overhead, but it does not make general-purpose Linux a hard real-time controller.
5. Add a C extension only around measured batch work
The safe pattern is to keep device ownership and high-level control in Python, then move a self-contained CPU-heavy operation across the extension boundary in one call. The example below counts transitions in a captured byte buffer. It does not claim GPIO lines, toggle motors, or create timing guarantees.
Create gpiofast.c:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject *
count_edges(PyObject *self, PyObject *args)
{
Py_buffer samples;
const unsigned char *data;
Py_ssize_t edges = 0;
(void)self;
if (!PyArg_ParseTuple(args, "y*:count_edges", &samples)) {
return NULL;
}
data = (const unsigned char *)samples.buf;
for (Py_ssize_t index = 1; index < samples.len; index++) {
if ((data[index - 1] == 0) != (data[index] == 0)) {
edges++;
}
}
PyBuffer_Release(&samples);
return PyLong_FromSsize_t(edges);
}
static PyMethodDef gpiofast_methods[] = {
{"count_edges", count_edges, METH_VARARGS,
"Count low/high transitions in a bytes-like sample buffer."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef gpiofast_module = {
PyModuleDef_HEAD_INIT,
.m_name = "gpiofast",
.m_doc = "Small batch helpers for GPIO sample analysis.",
.m_size = -1,
.m_methods = gpiofast_methods,
};
PyMODINIT_FUNC
PyInit_gpiofast(void)
{
return PyModule_Create(&gpiofast_module);
}
Create pyproject.toml:
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
Create setup.py as build configuration—not as a command to run directly:
from setuptools import Extension, setup
setup(
name="gpiofast",
version="0.1.0",
ext_modules=[Extension("gpiofast", ["gpiofast.c"])],
)
Build and install inside the active virtual environment through pip’s build interface:
python -m pip install .
python - <<'PY'
import gpiofast
samples = bytes([0, 0, 1, 1, 0, 1])
assert gpiofast.count_edges(samples) == 3
print("extension test passed")
PY
Benchmark the real batch size against a clear Python implementation. Keep the extension only if the improvement matters at the application level; every compiled module adds CPython ABI, compiler, architecture, and packaging obligations.
6. Understand the old build errors without copying the old build
The 2019 notes contain useful symptoms, but their remedies need current context:
| Symptom | Current interpretation |
|---|---|
Python.h: No such file or directory |
Install headers matching the interpreter used to build, then verify python3-config --includes; do not mix system Python and a different custom interpreter |
undefined symbol: digitalWrite |
The extension was not correctly linked to the library that supplies the symbol, or loaded an incompatible library; use that library’s current package metadata and inspect the final link command |
dynamic module does not define module export function |
The import name, extension name, and PyInit_<name> symbol must agree exactly |
Bad call flags or obsolete calling convention |
Match METH_VARARGS to a two-argument C function, or METH_VARARGS | METH_KEYWORDS to the documented three-argument signature and parser |
Works only with sudo |
Treat this as a permissions/ownership defect, not an installation instruction |
Do not use sudo python setup.py install, and do not put an empty string into library_dirs as a generic fix. Modern pip builds in an isolated environment and installs into the selected virtual environment; external C libraries still need explicit, version-correct headers and linker metadata.
7. If native code must own GPIO, start with libgpiod
Raspberry Pi’s best-practices paper recommends the kernel GPIO character-device interface for portable current work. Install its tools and development metadata from the OS, then inspect rather than assuming an API version:
sudo apt install --yes gpiod libgpiod-dev pkg-config
gpiodetect
gpioinfo
pkg-config --modversion libgpiod
libgpiod version 1 and version 2 have different command and C APIs. Follow the upstream examples for the major version printed on the target Pi, and declare that version in the project’s build and deployment documentation. A generic C-extension snippet that silently assumes one major version is less reliable than the GPIO Zero baseline above.
WiringPi version 3 is actively maintained upstream and can still be appropriate for an existing audited C codebase. That does not revive old distribution packages or guarantee its language wrappers. If retaining it, use the current upstream release, choose BCM numbering explicitly, and test every required feature on the target Pi model. Do not mix its wPi numbers with GPIO Zero numbers.
8. Verification checklist
Before attaching an actuator:
- preserve the model-specific
pinoutoutput with the wiring notes; - run pure logic tests through
MockFactory; - verify the selected pin factory and gpiochip permissions as the service user;
- test only a current-limited LED or instrumented low-energy fixture first;
- measure physical timing with an instrument when timing is part of correctness;
- confirm a shutdown or exception leaves the driver and actuator in a safe state;
- record Pi model, OS, kernel, Python, GPIO Zero, gpio backend, and native-library versions; and
- move deterministic motor pulse generation to appropriate hardware rather than relying on a Python or C busy loop.
What changed from the 2019 procedure
| 2019 archive detail | 2026 maintained treatment |
|---|---|
| Slow motor motion attributed directly to Python | Separate computation, binding, scheduler, electrical, and controller measurements |
| Motor control discussed without an electrical boundary | Require an H-bridge or motor controller and begin with a safe LED fixture |
gpio readall and differing wiringPi numbering |
Use official pinout, BCM identifiers, and a recorded physical-pin mapping |
| wiringPi selected as the default C route | Start new projects with GPIO Zero/lgpio or version-matched libgpiod; treat current WiringPi as an explicit legacy-code choice |
python setup.py build/install |
Use a virtual environment, pyproject.toml, setuptools as a backend, and python -m pip install . |
| Extension errors listed as isolated fixes | Tie each error to interpreter headers, linker metadata, module naming, or calling convention |
Authoritative references
- Raspberry Pi hardware documentation: GPIO header, voltage, motor, and permission guidance
- Raspberry Pi application note: GPIO history and current best practices
- GPIO Zero documentation: installation and virtual environments
- GPIO Zero documentation: pin factories, mock pins, and lgpio
- Python documentation: extending CPython with C or C++
- Python documentation: building C and C++ extensions
- Python documentation: virtual environments
- Setuptools documentation: building extension modules
- libgpiod upstream development documentation
- WiringPi maintained upstream repository and version-3 wrapper warning
Check the manuals and versions installed on the target Pi before choosing an ABI or GPIO backend. The guide deliberately avoids promising hard real-time behavior from general-purpose Linux.
Complete 2019 body (verbatim archive; do not execute)
I used gpiozero and RPi.GPIO to control my 3D printer. However, the speed is quite slow. After some nervewracking thinking, I found it the python that cause the slow motion of those motors. Even I delete the time.sleep() line, it still run slowly. This give me no choice that I have to rewrite my control module into c code. After the rewriting, the problem finally solved.
In the c code, I choose wiringPi library to control the GPIO.
The numbering system of the gpiozero and RPi.GPIO with wiringPi is quite different. You should use
gpio readall
to get the number map of your Pi.
Next you should wrap your c code in python.
Create a file test.c
and a file setup.py
Once the coding is finished, you should build and install you code. I suggest you install this in a virtual environment.
sudo apt install python-virtualenv
python setup.py build
python setup.py install
You might meet some problems in compiling as well as importing.
PyArg_ParseTuple()
Compiling
python.h: No such file or directory
Reinstall python3-dev solved this.
Importing
As we used library wiringPi , it should be specified in setup.py. Otherwise, we’ll get error
undefined symbol digitalWrite()
Extension(…,
library_dirs=[”],
libraries=[‘wiringPi’])
‘ImportError: dynamic module does not define init function’
I named the init function in a wrong name. PyInit_keywdarg should has the same suffix as defined in your keywdargmodule.
static struct PyModuleDef keywdargmodule = {
PyModuleDef_HEAD_INIT,
“keywdarg”,
NULL,
-1,
keywdarg_methods
};
PyMODINIT_FUNC
PyInit_keywdarg(void)
{
return PyModule_Create(&keywdargmodule);
}
SystemError: Bad call flags in PyCFunction_Call. METH_OLDARGS is no longer supported! In the myMethods array, the field number of parameters should be defined as
METH_VARARGS | METH_KEYWORDS
static PyMethodDef keywdarg_methods[] = {
/* The cast of the function is necessary since PyCFunction values
*/
{“parrot”, (PyCFunction)keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
“Print a lovely skit to standard output.”},
{NULL, NULL, 0, NULL} / sentinel /
};
- only take two PyObject* parameters, and keywdarg_parrot() takes
- three.
More detail in
[https://docs.python.org/3/extending/extending.html#the-module-s-method-table-and-initialization-function](https://docs.python.org/3/extending/extending.html#the-module-s-method-table-and-initialization-function)
