Внешние модули на C для MicroPython¶
При разработке модулей для использования с MicroPython вы можете столкнуться с ограничениями среды Python, часто связанными с невозможностью доступа к определённым аппаратным ресурсам или с ограничениями скорости Python.
Если ваши ограничения не удаётся устранить с помощью рекомендаций из Максимальное повышение скорости MicroPython, написание части или всего модуля на C (и/или C++, если это реализовано для вашего порта) является приемлемым вариантом.
Если ваш модуль предназначен для доступа к широко распространённому оборудованию или библиотекам или для работы с ними, рассмотрите возможность его реализации внутри дерева исходного кода MicroPython рядом с аналогичными модулями и отправки его в виде pull request. Однако если вы ориентируетесь на малоизвестные или проприетарные системы, может иметь больше смысла держать его вне основного репозитория MicroPython.
В этой главе описывается, как скомпилировать такие внешние модули в исполняемый файл или образ прошивки MicroPython. Поддерживаются оба инструмента сборки — Make и CMake, и при написании внешнего модуля рекомендуется добавлять файлы сборки для обоих этих инструментов, чтобы модуль можно было использовать на всех портах. Но при компиляции конкретного порта вам потребуется использовать только один способ сборки — либо Make, либо CMake.
Альтернативный подход — использование Нативный машинный код в файлах .mpy, что позволяет писать пользовательский код на C, который помещается в файл .mpy и может динамически импортироваться в работающую систему MicroPython без необходимости перекомпиляции основной прошивки.
Структура внешнего модуля на C¶
Пользовательский модуль на C для MicroPython — это каталог со следующими файлами:
Файлы исходного кода
*.c/*.cpp/*.hдля вашего модуля.Обычно они включают реализуемую низкоуровневую функциональность и функции привязки MicroPython, которые предоставляют доступ к функциям и модулю(ям).
В настоящее время лучшим справочным материалом для написания таких функций/модулей является поиск похожих модулей в дереве MicroPython и использование их в качестве примеров.
micropython.mkсодержит фрагмент Makefile для этого модуля.$(USERMOD_DIR)доступна вmicropython.mkкак путь к каталогу вашего модуля. Поскольку она переопределяется для каждого модуля на C, её следует развернуть в вашемmicropython.mkв локальную переменную make, напримерEXAMPLE_MOD_DIR := $(USERMOD_DIR)Ваш
micropython.mkдолжен добавить файлы исходного кода вашего модуля в переменныеSRC_USERMOD_CилиSRC_USERMOD_LIB_C. Первая будет обработана на наличие определенийMP_QSTR_иMP_REGISTER_MODULE, вторая — нет (например, вспомогательный и библиотечный код, не специфичный для MicroPython). Эти пути должны включать вашу развёрнутую копию$(USERMOD_DIR), например:SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
Аналогично используйте
SRC_USERMOD_CXXиSRC_USERMOD_LIB_CXXдля файлов исходного кода на C++. Если вы хотите включить файлы ассемблера, используйтеSRC_USERMOD_LIB_ASM.Если у вас есть пользовательские параметры компилятора (например,
-Iдля добавления каталогов поиска заголовочных файлов), их следует добавить вCFLAGS_USERMODдля кода на C и вCXXFLAGS_USERMODдля кода на C++.micropython.cmakeсодержит конфигурацию CMake для этого модуля.В
micropython.cmakeвы можете использовать${CMAKE_CURRENT_LIST_DIR}как путь к текущему модулю.Ваш
micropython.cmakeдолжен определить библиотекуINTERFACEи связать с ней ваши файлы исходного кода, определения компиляции и каталоги включения. Затем эту библиотеку следует слинковать с цельюusermod.add_library(usermod_cexample INTERFACE) target_sources(usermod_cexample INTERFACE ${CMAKE_CURRENT_LIST_DIR}/examplemodule.c ) target_include_directories(usermod_cexample INTERFACE ${CMAKE_CURRENT_LIST_DIR} ) target_link_libraries(usermod INTERFACE usermod_cexample)
Полный пример использования см. ниже.
Базовый пример¶
Модуль cexample предоставляет примеры для функции и класса. Функция cexample.add_ints(a, b) складывает два целочисленных аргумента и возвращает результат. Тип cexample.Timer() создаёт таймеры, которые можно использовать для измерения времени, прошедшего с момента создания объекта.
Модуль можно найти в дереве исходного кода MicroPython в каталоге examples; он содержит файл исходного кода и фрагмент Makefile с содержимым, описанным выше:
micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
├── micropython.mk
└── micropython.cmake
За дополнительными пояснениями обращайтесь к комментариям в этих файлах. Рядом с модулем cexample также есть cppexample, который работает таким же образом, но демонстрирует один из способов смешивания кода на C и C++ в MicroPython.
Компиляция cmodule в MicroPython¶
Чтобы собрать такой модуль, скомпилируйте MicroPython (см. getting started), применив 2 изменения:
Установите флаг времени сборки
USER_C_MODULESтак, чтобы он указывал на модули, которые вы хотите включить. Для портов, использующих Make, эта переменная должна быть каталогом, в котором модули ищутся автоматически. Для портов, использующих CMake, эта переменная должна быть файлом, который включает собираемые модули. Подробности см. ниже.Включите модули, установив соответствующий макрос препроцессора C в значение 1. Это необходимо только в том случае, если собираемые модули не включаются автоматически.
Для сборки примеров модулей, поставляемых с MicroPython, установите USER_C_MODULES в каталог examples/usercmodule для Make или в examples/usercmodule/micropython.cmake для CMake.
Например, вот как собрать порт unix с примерами модулей:
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule
Возможно, потребуется один раз выполнить make clean в начале при включении новых пользовательских модулей в сборку. Вывод сборки покажет найденные модули:
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
Для порта на основе CMake, такого как rp2, это будет выглядеть немного иначе (обратите внимание, что CMake фактически вызывается через make):
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
Опять же, возможно, потребуется сначала выполнить make clean, чтобы CMake обнаружил пользовательские модули. Вывод сборки CMake перечисляет модули по именам:
...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...
Содержимое верхнеуровневого micropython.cmake можно использовать для управления тем, какие модули включены.
Для ваших собственных проектов удобнее держать пользовательский код вне основного дерева исходного кода MicroPython, поэтому типичная структура каталога проекта будет выглядеть так:
my_project/
├── modules/
│ ├── example1/
│ │ ├── example1.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ ├── example2/
│ │ ├── example2.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ └── micropython.cmake
└── micropython/
├──ports/
... ├──stm32/
...
При сборке с помощью Make установите USER_C_MODULES в каталог my_project/modules. Например, сборка порта stm32:
cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules
При сборке с помощью CMake верхнеуровневый micropython.cmake — находящийся непосредственно в каталоге my_project/modules — должен include все модули, которые вы хотите иметь доступными:
include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake) include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)
Затем выполните сборку с помощью:
cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake
Вы также можете указать абсолютные пути для USER_C_MODULES.
Все модули, указанные переменной USER_C_MODULES (либо найденные в этом каталоге при использовании Make, либо добавленные через include при использовании CMake), будут скомпилированы, но только те из них, которые включены, будут доступны для импорта. Пользовательские модули обычно включены по умолчанию (это решает разработчик модуля), и в этом случае не требуется ничего, кроме установки USER_C_MODULES, как описано выше.
Если модуль не включён по умолчанию, то необходимо включить соответствующий макрос препроцессора C. Имя этого макроса можно найти, выполнив поиск строки MP_REGISTER_MODULE в исходном коде модуля (обычно она находится в конце основного файла исходного кода). Этот макрос должен быть окружён парой #if X / #endif, и параметр конфигурации X должен быть установлен в 1 с помощью CFLAGS_EXTRA, чтобы сделать модуль доступным. Если пары #if X / #endif нет, то модуль включён по умолчанию.
Например, модуль examples/usercmodule/cexample включён по умолчанию, поэтому в его исходном коде есть следующая строка:
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
Или же, чтобы сделать этот модуль отключённым по умолчанию, но выбираемым через параметр конфигурации препроцессора, это будет:
#if MODULE_CEXAMPLE_ENABLED MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule); #endif
В этом случае модуль включается добавлением CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 в команду make или редактированием mpconfigport.h или mpconfigboard.h с добавлением
#define MODULE_CEXAMPLE_ENABLED (1)
Обратите внимание, что точный метод зависит от порта, так как они имеют разную структуру. Если это сделано неправильно, сборка пройдёт, но при импорте модуль не будет найден.
Использование модуля в MicroPython¶
После встраивания в вашу копию MicroPython к модулю теперь можно обращаться в Python так же, как к любому другому встроенному модулю, например
import cexample
print(cexample.add_ints(1, 3))
# should display 4
from cexample import Timer
from time import sleep_ms
watch = Timer()
sleep_ms(1000)
print(watch.time())
# should display approximately 1000
Динамическое выделение памяти в C¶
MicroPython использует свою собственную «кучу Python» для Управление памятью, которая не совпадает с «кучей C», используемой функциями библиотеки C malloc(), free() и т. д. Не каждый порт MicroPython вообще поставляется с «кучей C».
Порты Tier 1 и 2 имеют разную поддержку динамического выделения памяти в C через «кучу C»:
Порты unix, windows, esp32 и webassembly поддерживают динамическое выделение памяти в C.
Порт rp2 не сможет выделить какую-либо память во время выполнения, если только прошивка не собрана с
MICROPY_C_HEAP_SIZE=nдля резервированияnбайт памяти под кучу C. Эта память не будет доступна для использования кодом Python.Сборки портов alif, mimxrt, nrf, renesas-ra, samd и stm32, включающие динамическое выделение памяти в C, завершатся ошибкой на этапе компоновки с ошибками вроде
undefined reference to `malloc'. MicroPython не имеет встроенной поддержки динамического выделения памяти в C на этих портах. Любое решение требует ручного добавления реализации кучи C в пользовательскую сборку.Порт zephyr в настоящее время не поддерживает сборку с пользовательскими модулями.
Куча Python в качестве кучи C¶
Для кода на C может быть практичнее вызывать функции динамического выделения памяти «кучи Python», такие как m_malloc(), m_malloc0() и m_free().
Дополнительную информацию об этом подходе см. в Память MicroPython из кода на C.
Модули на C++¶
Большинство портов MicroPython уровня Tier 1 и 2 (и некоторые Tier 3) поддерживают сборку пользовательских модулей на C++ с использованием специфичных для C++ переменных среды, описанных выше.
Успешная интеграция C++ и MicroPython связана с некоторыми дополнительными соображениями:
Динамическое выделение памяти в C++¶
Программы на C++ (а также возможности стандартной библиотеки C++) обычно используют динамическое выделение памяти. Стандартный аллокатор памяти C++ (т. е. операторы new и delete) обычно реализован как слой поверх Динамическое выделение памяти в C.
Для портов MicroPython, которые не включают поддержку динамического выделения памяти в C, динамическое выделение памяти в C++ может быть обеспечено одним из двух способов:
Реализуйте динамическое выделение памяти в C в вашей пользовательской сборке.
Реализуйте пользовательский аллокатор C++ в вашей пользовательской сборке.
Соображения по компоновке¶
Поскольку MicroPython — это проект на основе C, любые символы, которые ссылаются на MicroPython или из него, должны быть квалифицированы как extern "C" в коде на C++.
Настоятельно рекомендуется следовать шаблону, продемонстрированному в examples/usercmodule/cppexample, где модуль Python реализован в минимальной обёртке на C вокруг кода на C++.