C语言如何扩展MicroPython?

99ANYc3cd6
预计阅读时长 24 分钟
位置: 首页 C语言 正文

核心概念:MicroPython 的 C 扩展机制

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

c语言扩展micropython
(图片来源网络,侵删)

这个过程的核心是 MicroPython API,这个 API 提供了一系列函数,让你能够:

  1. 创建新的 Python 对象:比如整数、浮点数、字符串、列表、字典,甚至是自定义的类。
  2. 定义 C 函数:并让它们在 Python 端可以被调用。
  3. 定义新的 Python 模块:将一组相关的 C 函数打包成一个模块,方便 import
  4. 定义新的 Python 类型:创建类似 listdict 的自定义对象,可以有自己的方法和属性。
  5. 与 MicroPython 的运行时交互:比如访问虚拟机 (mp_obj_t)、垃圾回收器等。

关键的数据结构是 mp_obj_t,它是 MicroPython 中所有对象的通用表示,你不需要关心它具体是什么(它可能是一个指针,也可能是一个内联的值),你只需要通过 MicroPython API 来操作它。


开发环境准备

在开始之前,你需要一个可以编译 MicroPython 的环境。

  1. 获取 MicroPython 源码:

    c语言扩展micropython
    (图片来源网络,侵删)
    git clone https://github.com/micropython/micropython.git
    cd micropython
  2. 安装依赖:

    • 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
  3. 安装工具链: 你需要一个针对目标设备的交叉编译器,为 ESP32 编译:

    # ESP32
    pip install rshell esptool
    # 或者直接从 ESP 官网下载工具链并添加到 PATH
  4. 配置和编译: 以 esp32 板卡为例:

    # 进入 ports/esp32 目录
    cd ports/esp32
    # 运行配置脚本,会生成一个 .config 文件
    make BOARD=ESP32_GENERIC deploy
    # 之后就可以直接编译了
    make

    这会下载依赖项,配置项目,并首次编译固件。

    c语言扩展micropython
    (图片来源网络,侵删)

开发流程:创建一个 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);

代码解释:

  1. my_c_module_hello: 这是我们真正的 C 函数,它接受一个 mp_obj_t 类型的参数(可以是任何 Python 对象),返回一个 mp_obj_t,我们使用 MicroPython API 来操作这些对象。
  2. my_c_module_hello_obj: 这是一个静态的 mp_rom_map_elem_t 结构体,它充当了 Python 世界和 C 世界之间的桥梁,它将 Python 的函数名 (MP_QSTR_hello) 映射到我们的 C 函数 (my_c_module_hello)。
  3. my_c_module_globals: 这个字典定义了模块的顶层内容,我们通常在这里放 __name__ 属性和我们定义的函数。
  4. my_c_module_user_cmodule: 这是模块的最终表示,一个 mp_obj_module_t 结构体。
  5. 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 中测试

固件烧录成功后,打开串口终端(例如使用 rshellminicom),你就可以测试你的新模块了!

>>> 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.Pinarray.array 那样有状态的对象,你需要定义一个新的 Python 类型,这比定义模块函数要复杂一些。

核心概念:

  1. mp_obj_base_t: 所有自定义类型的基类,你的类型结构体需要包含它作为第一个成员。
  2. type 表 (mp_obj_type_t): 定义类型的所有行为,包括构造函数、方法、打印函数、二元操作等。
  3. make_new: 类的构造函数 __new__ 的 C 实现。
  4. locals_dict: 一个字典,用于定义实例方法。

简化流程:

  1. 定义 C 结构体:
    typedef struct _my_object_t {
        mp_obj_base_t base;
        int value; // 自定义数据
    } my_object_t;
  2. 实现方法函数:
    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;
    }
  3. 定义 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);
  4. 定义 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,
    };
  5. 在模块中注册类型:
    static const mp_rom_map_elem_t my_module_globals_table[] = {
        // ...
        { MP_ROM_QSTR(MP_QSTR_MyObject), MP_ROM_PTR(&my_type) },
    };

最佳实践与技巧

  1. 内存管理: MicroPython 有自己的垃圾回收器,当你创建新的 mp_obj_t 时,要确保它们的生命周期被正确管理,MicroPython API 会自动处理,但如果你分配了原始的 C 内存(如 malloc),你需要小心处理,或者使用 MicroPython 的内存分配器。
  2. 错误处理: 使用 mp_raise_ValueError("...") 等宏来从 C 函数中抛出 Python 异常。
  3. 性能分析: 使用 MicroPython 的 utime.ticks_ms() 来测量你的 C 函数的执行时间,并与纯 Python 版本进行对比,确保优化是有效的。
  4. 阅读源码: 最好的学习资源就是 MicroPython 和 CPython 本身,看看 builtinsarraycollections 等模块是如何用 C 实现的。
  5. 模块命名: 避免与内置模块重名,最好使用前缀,如 my_c_module

为 MicroPython 编写 C 扩展是一个功能强大的技能,它允许你:

  • 榨干硬件性能: 将计算密集型任务(如 FFT、加密、物理模拟)用 C 实现。
  • 访问底层硬件: 直接操作寄存器或 DMA,而无需经过 Python 层的抽象。
  • 集成现有库: 无需重写,即可将成熟的 C/C++ 库(如 SQLite, TensorFlow Lite)引入 MicroPython。

虽然它比纯 Python 开发更复杂,需要你理解 C 语言和 MicroPython 的内部机制,但一旦掌握,你将能够构建出性能卓越、功能强大的 MicroPython 应用程序。

-- 展开阅读全文 --
头像
dede include是什么意思?
« 上一篇 昨天
dede默认index.html如何设置与优化?
下一篇 » 昨天

相关文章

取消
微信二维码
支付宝二维码

目录[+]