树莓派 GPIO、Python 与 C 扩展:安全的现代开发流程(2026)

维护说明(2026 年 9 月 1 日):本页现在先提供一套现行的树莓派 GPIO 与 CPython 扩展流程,文末以日期明确的档案保留完整 2019 年正文。旧文中的 wiringPi 编号假设、直接运行 setup.py 的命令和调试记录只描述当时环境,不属于当前操作步骤。

原项目用 Python 控制 3D 打印机电机,发现运动不理想后把 GPIO 调用移到 C。这是一次有价值的性能调查,但“Python 很慢”并不足以构成诊断。GPIO 后端、Linux 调度、逐次调用开销、电气驱动和运动控制器都可能成为瓶颈。本指南会逐层区分,而不是一次替换整个技术栈。

1. 选择能够解决问题的最小现行层

新建 Raspberry Pi OS 项目时,建议按以下顺序选择:

需求现行起点
识别排针Raspberry Pi 的 pinout 命令和官方开发板图
按键、LED、经驱动器连接的继电器或普通事件GPIO Zero 及其默认 lgpio pin factory
暂时无法重构的既有 RPi.GPIO API评估 rpi-lgpio 兼容实现,并在确切 Pi 型号上测试
原生 Linux GPIO 访问先确认已安装主版本,再使用 libgpiod
Python 应用中的 CPU 密集工作只为批量计算编写一个小型、经过测量的 CPython 扩展
确定性电机脉冲或硬实时行为使用专用电机控制器、微控制器或合适的硬件外设,而非用户态定时循环

Raspberry Pi 的 GPIO 最佳实践文档把 Linux GPIO 字符设备栈视为面向未来的抽象。安装依赖后,GPIO Zero 当前会选择 LGPIOFactory;其文档说明 lgpio 支持包括 Raspberry Pi 5 在内的所有 Pi 型号。

目前活跃维护的 WiringPi 仓库并不等同于旧教程经常安装的废弃版本。其 version 3 语言包装器也不保证与 C 库同步。新 Python 项目不应只是为了复现 2019 年环境,就引入 wiringPi 编号或未经验证的 Python 包装器。

2. 编程前确认接线、编号和权限

更改接线前先关闭 Pi 电源。树莓派 GPIO 使用 3.3 V 逻辑。官方硬件文档警告不要向 3.3 V 元件输入 5 V,LED 必须串联限流电阻,电机必须通过 H-bridge 或电机控制板连接,不能直接接 GPIO。

检查实际排针和字符设备:

pinout
ls -l /dev/gpiochip*
id -nG

本指南使用 Broadcom GPIO 编号:LED(17) 表示 GPIO17,也就是物理排针 11;它不是物理排针 17,也不是 wiringPi pin 17。项目接线图应同时记录 BCM 编号和物理排针编号。

GPIO 访问通常来自 gpio 组成员资格。若服务账号不在该组,应由管理员授予最小必要组权限,然后重新登录。不要为了绕过权限错误而以 root 运行整个应用,也不要把所有 /dev/gpiochip* 设备改成全员可写。

3. 建立 GPIO Zero 基线

在 Raspberry Pi OS 上优先使用发行版软件包。启用系统包的虚拟环境可以复用硬件绑定:

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 会延迟创建,因此应在第一个 device 实例出现后再检查它。先选择 GPIO Zero 的 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

测试通过后,才把带限流电阻的 LED 接到 GPIO17 和地线,并运行硬件基线:

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()

若它在当前 Pi 上失败,先记录型号、Raspberry Pi OS 版本、内核、GPIO Zero 版本、所选 pin factory 和 /dev/gpiochip* 权限,再考虑更换库。

4. 加入 C 之前先测量瓶颈

删除 sleep() 只会去掉人为延迟,无法说明延迟来自 Python 调度、GPIO 后端、Linux 调度还是硬件。应分别建立三项测量:

  1. 用 fake 替换 GPIO 调用后的应用计算耗时;
  2. 只驱动安全 LED 测试电路时的 API 调用时长;
  3. 用逻辑分析仪或示波器测得的物理边沿时序。

下面的小诊断测量 Python 到后端的调用时长,而不是精确的电气边沿时刻:

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),
})

应在具有代表性的系统状态下重复运行,不要相信单次结果。即使中位数很好,最大延迟仍会影响脉冲生成。把循环移到 C 可以减少 Python 调用开销,却不会让通用 Linux 变成硬实时控制器。

5. 只为测得的批量工作加入 C 扩展

更安全的模式是让 Python 保持设备所有权和高级控制,再用一次扩展调用把独立的 CPU 密集操作整体交给 C。下面示例统计采集字节缓冲区中的状态转换;它不会占用 GPIO line、切换电机或承诺定时精度。

创建 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);
}

创建 pyproject.toml

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

创建 setup.py 作为构建配置,而不是直接运行的命令:

from setuptools import Extension, setup

setup(
    name="gpiofast",
    version="0.1.0",
    ext_modules=[Extension("gpiofast", ["gpiofast.c"])],
)

通过 pip 构建接口,在已激活的虚拟环境中构建并安装:

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

用真实批量大小与清晰的 Python 实现进行基准比较。只有性能提升对整个应用有实际意义时才保留扩展;每个编译模块都会增加 CPython ABI、编译器、架构和打包义务。

6. 理解旧构建错误,但不要复制旧构建流程

2019 年记录中的症状仍有参考价值,但处理办法需要当前语境:

症状当前解释
Python.h: No such file or directory安装与构建所用解释器匹配的头文件,并检查 python3-config --includes;不要混用系统 Python 与另一个自定义解释器
undefined symbol: digitalWrite扩展未正确链接提供该符号的库,或运行时载入了不兼容库;使用该库当前的包元数据并检查最终链接命令
dynamic module does not define module export function导入名、扩展名和 PyInit_<name> 符号必须完全一致
Bad call flags 或过时调用约定两参数 C 函数应匹配 METH_VARARGSMETH_VARARGS | METH_KEYWORDS 则应匹配文档规定的三参数签名和解析器
只能在 sudo 下运行把它当作权限或所有权缺陷,而不是安装说明

不要运行 sudo python setup.py install,也不要把空字符串写入 library_dirs 当作通用修复。现代 pip 会在隔离环境中构建,并安装到所选虚拟环境;外部 C 库仍需要明确且版本正确的头文件与链接元数据。

7. 若原生代码必须占用 GPIO,从 libgpiod 开始

Raspberry Pi 最佳实践文档建议当前可移植项目使用内核 GPIO 字符设备接口。先从操作系统安装工具和开发元数据,然后检查环境,不要猜测 API 版本:

sudo apt install --yes gpiod libgpiod-dev pkg-config
gpiodetect
gpioinfo
pkg-config --modversion libgpiod

libgpiod 版本 1 和版本 2 的命令及 C API 不同。应按目标 Pi 显示的主版本使用上游示例,并在构建与部署文档中声明该版本。一个默默假定某个主版本的通用 C 扩展示例,反而不如前面的 GPIO Zero 基线可靠。

WiringPi version 3 目前在上游活跃维护,对经过审计的既有 C 代码库仍可能合适。但这不会使旧发行版软件包恢复有效,也不保证语言包装器同步。若保留它,应使用当前上游版本、明确选择 BCM 编号,并在目标 Pi 型号上测试所有必需功能。不要把它的 wPi 编号与 GPIO Zero 编号混用。

8. 验证清单

连接 actuator 之前:

  • 把特定型号的 pinout 输出与接线记录一起保存;
  • 通过 MockFactory 运行纯逻辑测试;
  • 以服务用户身份验证所选 pin factory 和 gpiochip 权限;
  • 先只测试带限流电阻的 LED 或有仪器监测的低能量夹具;
  • 若时序属于正确性要求,用仪器测量物理时序;
  • 确认关机或异常会让驱动器和 actuator 进入安全状态;
  • 记录 Pi 型号、OS、内核、Python、GPIO Zero、GPIO 后端和原生库版本;
  • 把确定性电机脉冲生成移到合适硬件,不要依赖 Python 或 C 忙等循环。

相比 2019 年流程的变化

2019 年档案细节2026 年维护方案
直接把电机缓慢归因于 Python分别测量计算、绑定、调度、电气和控制器层
讨论电机控制时没有电气边界强制使用 H-bridge 或电机控制器,并从安全 LED 夹具开始
gpio readall 与不同 wiringPi 编号使用官方 pinout、BCM 标识和已记录的物理排针对照
默认选择 wiringPi 作为 C 路线新项目从 GPIO Zero/lgpio 或版本匹配的 libgpiod 开始;当前 WiringPi 只作为明确的旧代码选择
python setup.py build/install使用虚拟环境、pyproject.toml、setuptools 后端和 python -m pip install .
把扩展错误列为零散修复把每项错误对应到解释器头文件、链接元数据、模块命名或调用约定

权威参考资料

选择 ABI 或 GPIO 后端前,应检查目标 Pi 上实际安装的手册与版本。本指南特意不承诺通用 Linux 能提供硬实时行为。

完整 2019 年正文(原样档案,请勿执行)


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)

Leave a Reply