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을 컴파일하되(getting started 참조), 두 가지를 수정해 적용하세요:
빌드 타임 플래그
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
...
rp2 같은 CMake 기반 포트의 경우 이는 약간 다르게 보입니다(CMake가 실제로는 make에 의해 호출된다는 점에 유의하세요):
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
이 경우에도 CMake가 사용자 모듈을 인식하도록 먼저 make clean을 실행해야 할 수 있습니다. 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 힙”을 갖추고 있는 것은 아닙니다.
Tier 1 및 2 포트는 “C 힙”을 통한 C 동적 메모리 할당에 대해 지원 정도가 다릅니다:
unix, windows, esp32, webassembly 포트는 C 동적 메모리 할당을 지원합니다.
rp2 포트는 C 힙을 위해
n바이트의 메모리를 예약하도록MICROPY_C_HEAP_SIZE=n으로 펌웨어를 빌드하지 않는 한 런타임에 메모리 할당에 실패합니다. 이 메모리는 Python 코드가 사용할 수 없습니다.alif, mimxrt, nrf, renesas-ra, samd, stm32 포트 빌드에 동적 C 할당이 포함되면
undefined reference to `malloc'같은 오류와 함께 링크 시점에 실패합니다. MicroPython은 이러한 포트에서 동적 C 할당을 기본적으로 지원하지 않습니다. 어떤 해결책이든 사용자 정의 빌드에 C 힙 구현을 수동으로 추가해야 합니다.zephyr 포트는 현재 사용자 모듈과 함께 빌드하는 것을 지원하지 않습니다.
C 힙으로서의 Python 힙¶
C 코드가 대신 m_malloc(), m_malloc0(), m_free() 같은 “Python 힙” 동적 할당 함수를 호출하는 것이 실용적일 수 있습니다.
이 접근법에 대한 자세한 내용은 C 코드에서의 MicroPython 메모리를 참조하세요.
C++ 모듈¶
대부분의 Tier 1 및 2 MicroPython 포트(그리고 일부 Tier 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 파일 래퍼로 구현됩니다.