核心概念:MicroPython 的 C 扩展机制
MicroPython 的核心是用 C 编写的,它本身就是一个解释器,当你用 MicroPython 编写代码时,你实际上是在和这个解释器交互,当你用 C 语言编写扩展时,你是在直接向这个解释器中添加新的、原生的函数、模块或类型。

这个过程的核心是 MicroPython API,这个 API 提供了一系列函数,让你能够:
- 创建新的 Python 对象:比如整数、浮点数、字符串、列表、字典,甚至是自定义的类。
- 定义 C 函数:并让它们在 Python 端可以被调用。
- 定义新的 Python 模块:将一组相关的 C 函数打包成一个模块,方便
import。 - 定义新的 Python 类型:创建类似
list或dict的自定义对象,可以有自己的方法和属性。 - 与 MicroPython 的运行时交互:比如访问虚拟机 (
mp_obj_t)、垃圾回收器等。
关键的数据结构是 mp_obj_t,它是 MicroPython 中所有对象的通用表示,你不需要关心它具体是什么(它可能是一个指针,也可能是一个内联的值),你只需要通过 MicroPython API 来操作它。
开发环境准备
在开始之前,你需要一个可以编译 MicroPython 的环境。
-
获取 MicroPython 源码:
(图片来源网络,侵删)git clone https://github.com/micropython/micropython.git cd micropython
-
安装依赖:
- Linux (Ubuntu/Debian):
sudo apt-get update sudo apt-get install build-essential libffi-dev git pkg-config libgmp-dev
- macOS (使用 Homebrew):
brew install pkg-config libffi gmp
- Linux (Ubuntu/Debian):
-
安装工具链: 你需要一个针对目标设备的交叉编译器,为 ESP32 编译:
# ESP32 pip install rshell esptool # 或者直接从 ESP 官网下载工具链并添加到 PATH
-
配置和编译: 以
esp32板卡为例:# 进入 ports/esp32 目录 cd ports/esp32 # 运行配置脚本,会生成一个 .config 文件 make BOARD=ESP32_GENERIC deploy # 之后就可以直接编译了 make
这会下载依赖项,配置项目,并首次编译固件。
(图片来源网络,侵删)
开发流程:创建一个 C 扩展模块
我们将创建一个名为 my_c_module 的模块,它包含一个 C 函数 hello,该函数接受一个字符串并打印出来,然后返回一个整数。
步骤 1:编写 C 代码
在 MicroPython 源码的根目录下创建一个新文件夹,extmod/my_c_module,并在其中创建 my_c_module.c 文件。
extmod/my_c_module/my_c_module.c
#include "py/runtime.h"
#include "py/obj.h"
// 1. 定义 C 函数的实现
// 这个函数将作为 Python 端的 my_c_module.hello()
static mp_obj_t my_c_module_hello(mp_obj_t name_in) {
// mp_obj_get_str 将 Python 对象 (mp_obj_t) 转换为 MicroPython 的字符串对象
mp_obj_str_t *name = MP_OBJ_TO_PTR(mp_obj_get_str(name_in));
const char *name_str = mp_obj_str_get_str(name);
// 使用标准 C 的 printf 打印到控制台
printf("Hello from C, %s!\n", name_str);
// 返回一个整数 42
// MP_OBJ_NEW_SMALL_INT 是一个宏,用于创建一个小的整数对象
return MP_OBJ_NEW_SMALL_INT(42);
}
// 2. 定义 C 函数的 "表单" (Method Table)
// MicroPython 通过一个静态的 mp_rom_map_elem_t 数组来查找函数
// MP_ROM_QSTR 是一个宏,用于将 C 字符串转换为 MicroPython 的静态字符串对象
// MP_ROM_PTR 是一个宏,用于将 C 函数指针转换为 MicroPython 的对象
static MP_DEFINE_CONST_FUN_OBJ_1(my_c_module_hello_obj, my_c_module_hello);
// 3. 定义模块本身
// 这个表定义了模块的属性,包括我们刚刚定义的函数
static const mp_rom_map_elem_t my_c_module_globals_table[] = {
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_my_c_module) },
{ MP_ROM_QSTR(MP_QSTR_hello), MP_ROM_PTR(&my_c_module_hello_obj) },
};
static MP_DEFINE_CONST_DICT(my_c_module_globals, my_c_module_globals_table);
// 4. 定义模块对象
// 这个结构体是 MicroPython 识别一个模块的入口点
const mp_obj_module_t my_c_module_user_cmodule = {
.base = { &mp_type_module },
.globals = (mp_obj_dict_t *)&my_c_module_globals,
};
// 5. 注册模块
// 这是最关键的一步!它告诉 MicroPython 解释器在启动时加载这个模块
// MP_REGISTER_MODULE(MP_QSTR_module_name, module_object, is_builtin)
// MP_QSTR_my_c_module 是模块在 Python 中的名字
// my_c_module_user_cmodule 是我们上面定义的模块对象
// 0 表示它不是一个内置模块,而是需要被显式 import 的模块
MP_REGISTER_MODULE(MP_QSTR_my_c_module, my_c_module_user_cmodule, 0);
代码解释:
my_c_module_hello: 这是我们真正的 C 函数,它接受一个mp_obj_t类型的参数(可以是任何 Python 对象),返回一个mp_obj_t,我们使用 MicroPython API 来操作这些对象。my_c_module_hello_obj: 这是一个静态的mp_rom_map_elem_t结构体,它充当了 Python 世界和 C 世界之间的桥梁,它将 Python 的函数名 (MP_QSTR_hello) 映射到我们的 C 函数 (my_c_module_hello)。my_c_module_globals: 这个字典定义了模块的顶层内容,我们通常在这里放__name__属性和我们定义的函数。my_c_module_user_cmodule: 这是模块的最终表示,一个mp_obj_module_t结构体。MP_REGISTER_MODULE: 这是魔法所在,这个宏必须在 C 文件的顶层被调用,它在编译时被展开,向 MicroPython 的内部模块注册表中添加一条记录,当 MicroPython 启动并处理import语句时,如果发现my_c_module在注册表中,它就会加载我们定义的这个模块。
步骤 2:修改 Makefile
要让 MicroPython 编译器找到并编译我们的新模块,我们需要修改相应端口的 Makefile。
打开 ports/esp32/Makefile,找到 SRC_C 变量,并添加我们的新 C 文件的路径。
ports/esp32/Makefile (修改部分)
# ... 其他内容 ...
SRC_C = \
main.c \
memory.c \
# ... 其他文件 ...
$(addprefix $(TOP)/, $(DRIVERS_SRC_C)) \
# 添加我们的新模块
$(TOP)/extmod/my_c_module/my_c_module.c \ # <--- 添加这一行
$(TOP)/lib/libc/memchr.c \
# ... 其他内容 ...
步骤 3:重新编译并部署
重新编译固件并烧录到你的设备上。
# 在 ports/esp32 目录下 make # 烧录到设备 make deploy
步骤 4:在 MicroPython 中测试
固件烧录成功后,打开串口终端(例如使用 rshell 或 minicom),你就可以测试你的新模块了!
>>> import my_c_module
Hello from C, MicroPython!
>>> my_c_module.hello("World")
Hello from C, World!
42
>>> my_c_module.hello(123)
Hello from C, 123!
42
>>> my_c_module.__name__
'my_c_module'
>>> dir(my_c_module)
['__name__', 'hello']
太棒了!你已经成功地将一个 C 函数集成到了 MicroPython 中。
高级主题:创建自定义类型
如果你想要创建一个类似 machine.Pin 或 array.array 那样有状态的对象,你需要定义一个新的 Python 类型,这比定义模块函数要复杂一些。
核心概念:
mp_obj_base_t: 所有自定义类型的基类,你的类型结构体需要包含它作为第一个成员。type表 (mp_obj_type_t): 定义类型的所有行为,包括构造函数、方法、打印函数、二元操作等。make_new: 类的构造函数__new__的 C 实现。locals_dict: 一个字典,用于定义实例方法。
简化流程:
- 定义 C 结构体:
typedef struct _my_object_t { mp_obj_base_t base; int value; // 自定义数据 } my_object_t; - 实现方法函数:
static mp_obj_t my_method(mp_obj_t self_in) { my_object_t *self = MP_OBJ_TO_PTR(self_in); // ... 使用 self->value ... return mp_const_none; } - 定义
locals_dict:static const mp_rom_map_elem_t my_type_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_method), MP_ROM_PTR(&my_method_obj) }, }; static MP_DEFINE_CONST_DICT(my_type_locals_dict, my_type_locals_dict_table); - 定义
mp_obj_type_t:const mp_obj_type_t my_type = { { &mp_type_type }, .name = MP_QSTR_MyObject, .make_new = my_make_new, // 构造函数 .print = my_print, // 打印函数 .locals_dict = (mp_obj_dict_t *)&my_type_locals_dict, }; - 在模块中注册类型:
static const mp_rom_map_elem_t my_module_globals_table[] = { // ... { MP_ROM_QSTR(MP_QSTR_MyObject), MP_ROM_PTR(&my_type) }, };
最佳实践与技巧
- 内存管理: MicroPython 有自己的垃圾回收器,当你创建新的
mp_obj_t时,要确保它们的生命周期被正确管理,MicroPython API 会自动处理,但如果你分配了原始的 C 内存(如malloc),你需要小心处理,或者使用 MicroPython 的内存分配器。 - 错误处理: 使用
mp_raise_ValueError("...")等宏来从 C 函数中抛出 Python 异常。 - 性能分析: 使用 MicroPython 的
utime.ticks_ms()来测量你的 C 函数的执行时间,并与纯 Python 版本进行对比,确保优化是有效的。 - 阅读源码: 最好的学习资源就是 MicroPython 和 CPython 本身,看看
builtins、array、collections等模块是如何用 C 实现的。 - 模块命名: 避免与内置模块重名,最好使用前缀,如
my_c_module。
为 MicroPython 编写 C 扩展是一个功能强大的技能,它允许你:
- 榨干硬件性能: 将计算密集型任务(如 FFT、加密、物理模拟)用 C 实现。
- 访问底层硬件: 直接操作寄存器或 DMA,而无需经过 Python 层的抽象。
- 集成现有库: 无需重写,即可将成熟的 C/C++ 库(如 SQLite, TensorFlow Lite)引入 MicroPython。
虽然它比纯 Python 开发更复杂,需要你理解 C 语言和 MicroPython 的内部机制,但一旦掌握,你将能够构建出性能卓越、功能强大的 MicroPython 应用程序。
