MicroPython 外部 C 模組

在開發供 MicroPython 使用的模組時,你可能會發現自己受限於 Python 環境,這通常是因為無法存取某些硬體資源,或受到 Python 速度上的限制。

如果你的限制無法透過 將 MicroPython 速度最大化 中的建議解決,那麼以 C(以及/或 若你的連接埠有實作 C++)撰寫部分或全部模組會是一個可行的選項。

如果你的模組是設計來存取或搭配常見硬體或函式庫使用,請考慮將其實作在 MicroPython 原始碼樹中,與類似的模組放在一起,並以 pull request 的方式提交。但如果你的目標是冷門或專有的系統,將其保留在 MicroPython 主儲存庫之外可能更為合理。

本章說明如何將這類外部模組編譯進 MicroPython 可執行檔或韌體映像中。Make 與 CMake 兩種建置工具都受到支援;撰寫外部模組時,最好為這兩種工具都加上建置檔,如此模組便能在所有連接埠上使用。但在編譯特定連接埠時,你只需要使用其中一種建置方法,即 Make 或 CMake。

另一種做法是使用 .mpy 檔案中的原生機器碼,它允許撰寫自訂的 C 程式碼並放入 .mpy 檔案中,這個檔案可以動態匯入正在執行的 MicroPython 系統,而無需重新編譯主韌體。

外部 C 模組的結構

MicroPython 使用者 C 模組是一個包含以下檔案的目錄:

  • *.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_CSRC_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
    

    同樣地,對 C++ 原始碼檔案請使用 SRC_USERMOD_CXXSRC_USERMOD_LIB_CXX。如果你想納入組合語言檔案,請使用 SRC_USERMOD_LIB_ASM

    如果你有自訂的編譯器選項(例如以 -I 新增搜尋標頭檔的目錄),對 C 程式碼應將其加入 CFLAGS_USERMOD,對 C++ 程式碼則加入 CXXFLAGS_USERMOD

  • micropython.cmake 包含此模組的 CMake 設定。

    micropython.cmake 中,你可以使用 ${CMAKE_CURRENT_LIST_DIR} 作為目前模組的路徑。

    你的 micropython.cmake 應定義一個 INTERFACE 函式庫,並將你的原始碼檔案、編譯定義與 include 目錄與其關聯。接著該函式庫應連結到 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,它的運作方式相同,但展示了在 MicroPython 中混合 C 與 C++ 程式碼的一種方法。

將 cmodule 編譯進 MicroPython

若要建置這類模組,請編譯 MicroPython(參見 入門指南),並套用 2 項修改:

  1. 設定建置期旗標 USER_C_MODULES 以指向你想納入的模組。對於使用 Make 的連接埠,此變數應為一個目錄,系統會自動於其中搜尋模組。對於使用 CMake 的連接埠,此變數應為一個包含要建置之模組的檔案。詳情請見下文。

  2. 藉由將對應的 C 前置處理器巨集設為 1 來啟用模組。只有當你要建置的模組未被自動啟用時才需要這麼做。

若要建置 MicroPython 隨附的範例模組,對 Make 請將 USER_C_MODULES 設為 examples/usercmodule 目錄,對 CMake 則設為 examples/usercmodule/micropython.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 時在此目錄中找到的,或使用 CMake 時透過 include 加入的)都會被編譯,但只有被啟用的模組才能供匯入使用。使用者模組通常預設為啟用(這由模組的開發者決定),在此情況下,除了如上所述設定 USER_C_MODULES 之外便無需再做其他事。

如果某個模組預設未啟用,則必須啟用對應的 C 前置處理器巨集。可藉由在模組原始碼中搜尋 MP_REGISTER_MODULE 那一行來找到此巨集名稱(它通常出現在主原始碼檔案的結尾)。此巨集應被一組 #if X / #endif 包圍,且必須使用 CFLAGS_EXTRA 將設定選項 X 設為 1,才能使模組可用。如果沒有 #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

在此情況下,啟用此模組的方式是在 make 指令中加上 CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1,或編輯 mpconfigport.hmpconfigboard.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 函式庫函式 malloc()free() 等所使用的「C 堆積」不同。並非每個 MicroPython 連接埠都附帶「C 堆積」。

第 1 級與第 2 級連接埠對透過「C 堆積」進行 C 動態記憶體配置的支援程度各不相同:

  • unix、windows、esp32 與 webassembly 連接埠支援 C 動態記憶體配置。

  • rp2 連接埠在執行期將無法配置任何記憶體,除非韌體建置時加上 MICROPY_C_HEAP_SIZE=n 以保留 n 個位元組的記憶體作為 C 堆積。此記憶體將無法供 Python 程式碼使用。

  • 包含動態 C 配置的 alif、mimxrt、nrf、renesas-ra、samd 與 stm32 連接埠建置,會在連結期失敗並出現諸如 undefined reference to `malloc' 的錯誤。MicroPython 在這些連接埠上沒有內建對動態 C 配置的支援。任何解決方案都需要手動為自訂建置加入 C 堆積實作。

  • zephyr 連接埠目前不支援搭配使用者模組進行建置。

以 Python 堆積作為 C 堆積

讓 C 程式碼改為呼叫「Python 堆積」的動態配置函式(例如 m_malloc()m_malloc0()m_free())可能會比較實用。

關於這種做法的更多資訊,請見 從 C 程式碼存取 MicroPython 記憶體

C++ 模組

大多數第 1 級與第 2 級的 MicroPython 連接埠(以及部分第 3 級)支援建置 C++ 使用者模組,方式是使用上述 C++ 專屬的環境變數。

要成功整合 C++ 與 MicroPython,需要考量一些額外事項:

C++ 動態記憶體配置

C++ 程式(以及 C++ 標準函式庫功能)通常會使用動態記憶體配置。C++ 預設的記憶體配置器(即 newdelete 運算子)通常實作為 C 動態記憶體配置 之上的一層。

對於不包含 C 動態記憶體配置支援的 MicroPython 連接埠,可以下列兩種方式之一來支援 C++ 動態記憶體配置:

  • 在你的自訂建置中實作 C 動態記憶體配置。

  • 在你的自訂建置中實作自訂的 C++ 配置器。

連結相關考量

由於 MicroPython 是以 C 為基礎的專案,任何連結到 MicroPython 或自 MicroPython 連結出去的符號,在 C++ 程式碼中都需要以 extern "C" 加以限定。

強烈建議遵循 examples/usercmodule/cppexample 所示範的模式,其中 Python 模組是在一個極簡的 C 檔案中實作,作為 C++ 程式碼外層的包裝。