1. 什么是 Native Module
1.1 官方定义
根据 MicroPython 官方文档,Native Module(原生模块)是一种特殊的 .mpy
文件,它包含来自非Python语言的原生机器代码。这允许你用C等语言编写代码,将其编译并链接到 .mpy
文件中,然后像普通Python模块一样导入这个文件。
1.2 核心优势与限制
主要优势:
- 动态加载:原生机器码可以被脚本动态导入,无需重新构建主MicroPython固件
- 性能优化:关键代码路径使用原生机器码执行,性能显著提升
- 内存效率:按需加载,减少固件大小
- 开发便利:支持热更新和快速迭代
主要限制:
- C代码编写复杂度:相比直接在MicroPython自定义固件中编写C代码,natmod的C代码编写会更麻烦
- 外部库依赖:引用外部库需要通过跳转表,没有跳转桥的话只能重复集成C代码
- 内存限制:运行在SRAM中,受代码大小限制
- 架构绑定:每个
.mpy
文件绑定特定架构,不能跨平台使用
1.3 与外部C模块的区别
Native Module 与 MicroPython外部C模块 的主要区别:
特性
Native Module
外部C模块
编译时机
独立编译,动态加载
编译到固件中
更新方式
无需重新刷固件
需要重新编译固件
内存使用
运行时分配
静态分配
开发复杂度
需要处理重定位
直接集成
2. 支持的功能和限制
2.1 支持的架构
当前支持的架构(ARCH
变量的有效选项):
x86
(32 bit)x64
(64 bit x86)armv6m
(ARM Thumb, 如 Cortex-M0)armv7m
(ARM Thumb 2, 如 Cortex-M3)armv7emsp
(ARM Thumb 2, 单精度浮点, 如 Cortex-M4F, Cortex-M7)armv7emdp
(ARM Thumb 2, 双精度浮点, 如 Cortex-M7)xtensa
(非窗口化, 如 ESP8266)xtensawin
(窗口化, 窗口大小8, 如 ESP32, ESP32S3)rv32imc
(RISC-V 32位, 压缩指令, 如 ESP32C3, ESP32C6)
2.2 支持的功能
链接器和动态加载器支持的功能:
- 可执行代码 (text)
- 只读数据 (rodata),包括字符串和常量数据(数组、结构体等)
- 零初始化数据 (BSS)
- text段中指向text、rodata和BSS的指针
- rodata段中指向text、rodata和BSS的指针
2.3 已知限制
- 不支持data段:解决方法:使用BSS数据并显式初始化数据值
- 不支持静态BSS变量:解决方法:使用全局BSS变量
- 运行时库链接:原生模块不会自动链接标准静态库如
libm.a
和libgcc.a
- 符号表限制:原生模块不会链接到完整MicroPython固件的符号表
3. 快速开始:最小示例
3.1 项目结构
factorial/
├── factorial.c
└── Makefile
3.2 C源代码
// factorial.c
#include "py/dynruntime.h"
// 计算阶乘的辅助函数
static mp_int_t factorial_helper(mp_int_t x) {
if (x == 0) {
return 1;
}
return x * factorial_helper(x - 1);
}
// 这是将从Python调用的函数,作为 factorial(x)
static mp_obj_t factorial(mp_obj_t x_obj) {
// 从MicroPython输入对象中提取整数
mp_int_t x = mp_obj_get_int(x_obj);
// 计算阶乘
mp_int_t result = factorial_helper(x);
// 将结果转换为MicroPython整数对象并返回
return mp_obj_new_int(result);
}
// 定义对上述函数的Python引用
static MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial);
// 这是入口点,在模块导入时调用
mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
// 这必须是第一个,它设置globals dict和其他内容
MP_DYNRUNTIME_INIT_ENTRY
// 使函数在模块的命名空间中可用
mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj));
// 这必须是最后一个,它恢复globals dict
MP_DYNRUNTIME_INIT_EXIT
}
3.3 Makefile
# 顶级MicroPython目录的位置
MPY_DIR = ../../..
# 模块名称
MOD = factorial
# 源文件 (.c 或 .py)
SRC = factorial.c
# 构建架构 (x86, x64, armv6m, armv7m, xtensa, xtensawin, rv32imc)
ARCH = x64
# 包含以获得编译和链接模块的规则
include $(MPY_DIR)/py/dynruntime.mk
3.4 编译模块
先决条件:
- MicroPython仓库(至少
py/
和tools/
目录) - CPython 3 和 pyelftools库(如
pip install 'pyelftools>=0.25'
) - GNU make
- 目标架构的C编译器(如果使用C源码)
- 可选的
mpy-cross
(如果使用.py源码)
编译命令:
$ make
# 或者指定架构
$ make ARCH=armv7m
3.5 在MicroPython中使用
import factorial
print(factorial.factorial(10))
# 应该显示 3628800
4. 技术实现深度解析
4.1 .mpy文件格式
根据源码分析,.mpy
文件的头部结构如下:
// py/persistentcode.h
#define MPY_VERSION 6
#define MPY_SUB_VERSION 3
// 文件头格式(4字节)
typedef struct {
uint8_t magic; // 0x4D ('M')
uint8_t version; // MPY_VERSION
uint8_t features; // 架构信息 + 子版本
uint8_t small_int_bits; // 小整数位数
} mpy_header_t;
特征字节编码:
// py/persistentcode.h
#define MPY_FEATURE_ENCODE_SUB_VERSION(version) (version)
#define MPY_FEATURE_DECODE_SUB_VERSION(feat) ((feat) & 3)
#define MPY_FEATURE_ENCODE_ARCH(arch) ((arch) << 2)
#define MPY_FEATURE_DECODE_ARCH(feat) ((feat) >> 2)
4.2 支持的架构
// py/persistentcode.h
enum {
MP_NATIVE_ARCH_NONE = 0,
MP_NATIVE_ARCH_X86,
MP_NATIVE_ARCH_X64,
MP_NATIVE_ARCH_ARMV6,
MP_NATIVE_ARCH_ARMV6M,
MP_NATIVE_ARCH_ARMV7M,
MP_NATIVE_ARCH_ARMV7EM,
MP_NATIVE_ARCH_ARMV7EMSP,
MP_NATIVE_ARCH_ARMV7EMDP,
MP_NATIVE_ARCH_XTENSA, // ESP8266
MP_NATIVE_ARCH_XTENSAWIN, // ESP32, ESP32-S3
MP_NATIVE_ARCH_RV32IMC, // ESP32-C3, ESP32-C6
};
4.3 文件结构详解
.mpy文件结构:
┌─────────────────┐
│ 头部 │ 4字节:魔数+版本+架构+小整数位数
├─────────────────┤
│ qstr表 │ 字符串常量表
├─────────────────┤
│ 对象表 │ 常量对象表
├─────────────────┤
│ 代码段 │ 机器码或字节码
├─────────────────┤
│ 重定位信息 │ 符号重定位指令
└─────────────────┘
5. 编译时处理:mpy_ld.py链接器
5.1 链接器架构
mpy_ld.py
是Native Module编译流程的核心工具,它实现了从ELF目标文件到.mpy
文件的转换。
# tools/mpy_ld.py
class LinkEnv:
def __init__(self, arch):
self.arch = ARCH_DATA[arch]
self.sections = [] # 输出段列表
self.known_syms = {} # 已知符号
self.unresolved_syms = [] # 未解析符号
self.mpy_relocs = [] # 重定位信息
5.2 架构特定配置
# tools/mpy_ld.py
ARCH_DATA = {
"xtensawin": ArchData(
"EM_XTENSA",
MP_NATIVE_ARCH_XTENSAWIN << 2,
4,
(R_XTENSA_32, R_XTENSA_PLT),
asm_jump_xtensa,
separate_rodata=True,
),
"rv32imc": ArchData(
"EM_RISCV",
MP_NATIVE_ARCH_RV32IMC << 2,
4,
(R_RISCV_32, R_RISCV_GOT_HI20, R_RISCV_GOT32_PCREL),
asm_jump_rv32,
),
}
5.3 GOT(Global Offset Table)构建
# tools/mpy_ld.py
def populate_got(env):
# 计算GOT目标地址
for got_entry in env.got_entries.values():
sym = got_entry.sym
if hasattr(sym, "resolved"):
sym = sym.resolved
sec = sym.section
addr = sym["st_value"]
got_entry.sec_name = sec.name
got_entry.link_addr += sec.addr + addr
# 布局并填充GOT
offset = 0
for got_entry in got_list:
got_entry.offset = offset
offset += env.arch.word_size
o = env.got_section.addr + got_entry.offset
env.full_text[o : o + env.arch.word_size] = got_entry.link_addr.to_bytes(
env.arch.word_size, "little"
)
5.4 .mpy文件生成
# tools/mpy_ld.py
def build_mpy(env, entry_offset, fmpy, native_qstr_vals):
# 写入跳转指令到文本段开始
jump = env.arch.asm_jump(entry_offset)
env.full_text[: len(jump)] = jump
# MPY: 头部
out.write_bytes(bytearray([
ord("M"), MPY_VERSION,
env.arch.mpy_feature | MPY_SUB_VERSION,
MP_SMALL_INT_BITS
]))
# MPY: qstr表
out.write_uint(1 + len(native_qstr_vals))
out.write_qstr(fmpy) # 文件名
for q in native_qstr_vals:
out.write_qstr(q)
# MPY: 机器码
out.write_uint(len(env.full_text) << 3 | (MP_CODE_NATIVE_VIPER - MP_CODE_BYTECODE))
out.write_bytes(env.full_text)
# MPY: 重定位信息
for base, addr, kind in env.mpy_relocs:
# 编码重定位类型和地址
if isinstance(kind, str) and kind.startswith(".text"):
kind = 0
elif kind == "mp_fun_table":
kind = 8
else:
kind = 9 + kind
out.write_reloc(base, addr // env.arch.word_size, kind, 1)
6. 运行时动态加载机制
6.1 内存分配策略
不同平台采用不同的可执行内存分配策略:
ESP32平台:
// ports/esp32/main.c
void *esp_native_code_commit(void *buf, size_t len, void *reloc) {
len = (len + 3) & ~3; // 4字节对齐
size_t len_node = sizeof(native_code_node_t) + len;
// 分配可执行内存(IRAM)
native_code_node_t *node = heap_caps_malloc(len_node, MALLOC_CAP_EXEC);
if (node == NULL) {
m_malloc_fail(len_node);
}
// 链接到native代码链表
node->next = native_code_head;
native_code_head = node;
void *p = node->data;
// 执行重定位
if (reloc) {
mp_native_relocate(reloc, buf, (uintptr_t)p);
}
// 复制代码到可执行内存
memcpy(p, buf, len);
return p;
}
Unix平台:
// ports/unix/alloc.c
void mp_unix_alloc_exec(size_t min_size, void **ptr, size_t *size) {
*size = (min_size + 0xfff) & (~0xfff); // 页面对齐
*ptr = mmap(NULL, *size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (*ptr == MAP_FAILED) {
*ptr = NULL;
}
}
6.2 重定位机制详解
重定位是Native Module技术的核心,它解决了机器码中符号地址的动态解析问题。
// py/persistentcode.c
void mp_native_relocate(void *ri_in, uint8_t *text, uintptr_t reloc_text) {
reloc_info_t *ri = ri_in;
uint8_t op;
uintptr_t *addr_to_adjust = NULL;
while ((op = read_byte(ri->reader)) != 0xff) {
if (op & 1) {
// 确定需要调整的地址位置
size_t addr = read_uint(ri->reader);
if ((addr & 1) == 0) {
// 指向text段
addr_to_adjust = &((uintptr_t *)text)[addr >> 1];
} else {
// 指向rodata段
addr_to_adjust = &((uintptr_t *)ri->rodata)[addr >> 1];
}
}
op >>= 1;
// 确定目标地址
uintptr_t dest;
size_t n = 1;
if (op <= 5) {
if (op & 1) {
n = read_uint(ri->reader); // 读取调整次数
}
op >>= 1;
if (op == 0) {
dest = reloc_text; // text段
} else if (op == 1) {
dest = (uintptr_t)ri->rodata; // rodata段
} else {
dest = (uintptr_t)ri->bss; // bss段
}
} else if (op == 6) {
dest = (uintptr_t)ri->context->constants.qstr_table; // qstr表
} else if (op == 7) {
dest = (uintptr_t)ri->context->constants.obj_table; // 对象表
} else if (op == 8) {
dest = (uintptr_t)&mp_fun_table; // 函数表本身
} else {
dest = ((uintptr_t *)&mp_fun_table)[op - 9]; // 函数表中的具体函数
}
// 执行重定位
while (n--) {
*addr_to_adjust++ += dest;
}
}
}
6.3 重定位类型编码
重定位信息使用紧凑的编码格式:
重定位指令格式:
┌─────────┬─────────┬─────────┐
│ 操作码 │ 地址 │ 目标 │
└─────────┴─────────┴─────────┘
操作码编码:
- 0-5: 内部段重定位(text/rodata/bss)
- 6: qstr_table重定位
- 7: obj_table重定位
- 8: mp_fun_table重定位
- 9+: mp_fun_table中的具体函数
7. Python-C桥接机制
7.1 mp_fun_table函数表
mp_fun_table
是Python和C代码之间的核心桥接机制:
// py/nativeglue.h
typedef struct _mp_fun_table_t {
mp_const_obj_t const_none;
mp_const_obj_t const_false;
mp_const_obj_t const_true;
mp_uint_t (*native_from_obj)(mp_obj_t obj, mp_uint_t type);
mp_obj_t (*native_to_obj)(mp_uint_t val, mp_uint_t type);
mp_obj_dict_t *(*swap_globals)(mp_obj_dict_t * new_globals);
mp_obj_t (*load_name)(qstr qst);
mp_obj_t (*load_global)(qstr qst);
mp_obj_t (*load_attr)(mp_obj_t base, qstr attr);
mp_obj_t (*call_function_n_kw)(mp_obj_t fun_in, size_t n_args_kw, const mp_obj_t *args);
// ... 更多函数指针
} mp_fun_table_t;
// 全局函数表实例
extern const mp_fun_table_t mp_fun_table;
7.2 动态运行时API
Native Module使用动态运行时API来访问MicroPython的功能:
// py/dynruntime.h
#define m_malloc(n) (m_malloc_dyn((n)))
#define mp_printf(p, ...) (mp_fun_table.printf_((p), __VA_ARGS__))
#define mp_obj_new_int(x) (mp_fun_table.native_to_obj((x), MP_NATIVE_TYPE_INT))
#define mp_obj_get_int(o) (mp_fun_table.native_from_obj((o), MP_NATIVE_TYPE_INT))
// 内存分配实现
static inline void *m_malloc_dyn(size_t n) {
return mp_fun_table.realloc_(NULL, n, false);
}
static inline void m_free_dyn(void *ptr) {
mp_fun_table.realloc_(ptr, 0, false);
}
7.3 模块初始化机制
// py/dynruntime.h
#define MP_DYNRUNTIME_INIT_ENTRY \
mp_obj_t old_globals = mp_fun_table.swap_globals(self->context->module.globals); \
mp_raw_code_truncated_t rc; \
rc.proto_fun_indicator[0] = MP_PROTO_FUN_INDICATOR_RAW_CODE_0; \
rc.proto_fun_indicator[1] = MP_PROTO_FUN_INDICATOR_RAW_CODE_1; \
rc.kind = MP_CODE_NATIVE_VIPER; \
rc.is_generator = 0; \
(void)rc;
#define MP_DYNRUNTIME_INIT_EXIT \
mp_fun_table.swap_globals(old_globals); \
return mp_const_none;
8. 架构特定的实现细节
8.1 ESP32 (Xtensa) 平台
内存布局:
// ESP32使用IRAM作为可执行内存
#define IRAM1_END (0x40108000)
#define FLASH_START (0x40200000)
// 可执行内存分配
native_code_node_t *node = heap_caps_malloc(len_node, MALLOC_CAP_EXEC);
Xtensa特定重定位:
// tools/mpy_ld.py
elif env.arch.name == "EM_XTENSA" and r_info_type == R_XTENSA_SLOT0_OP:
# Xtensa特定的字面量槽操作
sec = s.section
if sec.name.startswith(".text"):
return # 已正确重定位
assert sec.name.startswith(".literal"), sec.name
lit_idx = "{}+0x{:x}".format(sec.filename, r_addend)
lit_ptr = env.xt_literals[lit_idx]
if isinstance(lit_ptr, str):
addr = env.got_section.addr + env.got_entries[lit_ptr].offset
else:
addr = env.lit_section.addr + env.lit_entries[lit_ptr].offset
reloc = addr - r_offset
8.2 ESP32-C3/C6 (RISC-V) 平台
RISC-V特定重定位:
# tools/mpy_ld.py
elif env.arch.name == "EM_RISCV" and r_info_type in (
R_RISCV_32, R_RISCV_GOT_HI20, R_RISCV_GOT32_PCREL
):
# RISC-V GOT相对寻址
got_entry = env.got_entries[s.name]
addr = env.got_section.addr + got_entry.offset
reloc = addr - r_offset + r_addend
9. 性能优化与限制
9.1 性能优势
- 直接机器码执行:避免字节码解释开销
- 内存局部性:代码和数据紧密布局
- 编译器优化:利用现代编译器的优化能力
- 减少函数调用开销:内联和直接调用
9.2 当前限制
// 已知限制(来自文档)
// 1. 不支持data段,必须使用BSS段
// 2. 不支持静态BSS变量,必须使用全局BSS变量
// 3. 只能调用mp_fun_table中定义的函数
// 4. 架构特定的限制
// 5. 外部库依赖需要通过跳转表
// 6. 运行在SRAM中,受代码大小限制
9.3 内存管理考虑
// 内存跟踪机制
#if MICROPY_PERSISTENT_CODE_TRACK_FUN_DATA
// 跟踪函数数据内存,防止被GC回收
track_root_pointer(fun_data);
#endif
#if MICROPY_PERSISTENT_CODE_TRACK_BSS_RODATA
// 跟踪BSS/rodata内存
track_root_pointer(data);
#endif
10. 调试与开发工具
10.1 mpy-tool.py分析工具
# 分析.mpy文件内容
./tools/mpy-tool.py -xd myfile.mpy
# 输出示例
simple_name: factorial
raw data: 128 0x12345678...
prelude: (0, 0, 0, 1, 0, 0, 0)
args: ['x']
10.2 版本兼容性检查
# 检查系统支持的.mpy版本
import sys
sys_mpy = sys.implementation._mpy
arch = [None, 'x86', 'x64', 'armv6', 'armv6m', 'armv7m',
'armv7em', 'armv7emsp', 'armv7emdp', 'xtensa',
'xtensawin', 'rv32imc'][sys_mpy >> 10]
print('mpy version:', sys_mpy & 0xff)
print('mpy sub-version:', sys_mpy >> 8 & 3)
print('mpy flags: -march=' + arch if arch else '')
11. 最佳实践和注意事项
11.1 开发建议
- 选择合适的架构:确保编译时指定的架构与目标设备匹配
- 内存管理:注意BSS数据的使用限制,避免使用data段
- 错误处理:使用适当的异常处理机制
- 性能测试:对比原生模块与纯Python实现的性能差异
11.2 常见问题解决
- 未定义符号错误:检查是否所有外部函数都在mp_fun_table中
- 内存分配失败:考虑SRAM大小限制
- 架构不匹配:确保编译和运行环境使用相同架构
- 重定位失败:检查符号引用是否正确
11.3 性能优化技巧
- 减少函数调用:内联简单函数
- 使用BSS数据:避免重复初始化
- 合理使用常量:将不变的数据放在rodata段
- 内存对齐:注意数据结构的对齐要求
12. 未来发展与扩展
12.1 可能的改进方向
- 更多架构支持:ARM64、MIPS等
- 更丰富的API:扩展mp_fun_table
- 更好的调试支持:符号表和调试信息
- 性能优化:JIT编译和代码缓存
12.2 生态系统发展
- 标准库模块:更多内置模块的native版本
- 第三方库:社区贡献的native模块
- 工具链完善:更好的开发和调试工具
结论
MicroPython的Native Module技术通过精心的架构设计,实现了Python和C代码的无缝集成。从.mpy
文件格式的设计,到mpy_ld.py
链接器的实现,再到运行时的动态加载和重定位机制,每一个环节都体现了对嵌入式系统特性的深度考虑。
这一技术的主要优势在于无需重新刷固件即可动态加载高性能的C代码,为嵌入式Python开发带来了显著的便利性。然而,它也带来了一些限制,如C代码编写的复杂性增加、外部库依赖需要通过跳转表处理、以及SRAM中的代码大小限制等。
随着MicroPython生态系统的不断发展,Native Module技术将继续演进,为更多应用场景提供支持,成为嵌入式Python开发的重要工具。开发者需要在便利性和性能之间找到平衡,合理使用这一技术来优化关键代码路径。