Внешние модули на 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 изменения:

  1. Установите флаг времени сборки USER_C_MODULES так, чтобы он указывал на модули, которые вы хотите включить. Для портов, использующих Make, эта переменная должна быть каталогом, в котором модули ищутся автоматически. Для портов, использующих CMake, эта переменная должна быть файлом, который включает собираемые модули. Подробности см. ниже.

  2. Включите модули, установив соответствующий макрос препроцессора 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++.