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_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
同樣地,對 C++ 原始碼檔案請使用
SRC_USERMOD_CXX與SRC_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 項修改:
設定建置期旗標
USER_C_MODULES以指向你想納入的模組。對於使用 Make 的連接埠,此變數應為一個目錄,系統會自動於其中搜尋模組。對於使用 CMake 的連接埠,此變數應為一個包含要建置之模組的檔案。詳情請見下文。藉由將對應的 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.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 函式庫函式 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++ 預設的記憶體配置器(即 new 與 delete 運算子)通常實作為 C 動態記憶體配置 之上的一層。
對於不包含 C 動態記憶體配置支援的 MicroPython 連接埠,可以下列兩種方式之一來支援 C++ 動態記憶體配置:
在你的自訂建置中實作 C 動態記憶體配置。
在你的自訂建置中實作自訂的 C++ 配置器。
連結相關考量¶
由於 MicroPython 是以 C 為基礎的專案,任何連結到 MicroPython 或自 MicroPython 連結出去的符號,在 C++ 程式碼中都需要以 extern "C" 加以限定。
強烈建議遵循 examples/usercmodule/cppexample 所示範的模式,其中 Python 模組是在一個極簡的 C 檔案中實作,作為 C++ 程式碼外層的包裝。