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_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.cmakeINTERFACE 라이브러리를 정의하고 거기에 소스 파일, 컴파일 정의, 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 참조), 두 가지를 수정해 적용하세요:

  1. 빌드 타임 플래그 USER_C_MODULES가 포함하려는 모듈을 가리키도록 설정합니다. Make를 사용하는 포트의 경우 이 변수는 모듈을 자동으로 검색할 디렉터리여야 합니다. CMake를 사용하는 포트의 경우 이 변수는 빌드할 모듈들을 포함하는 파일이어야 합니다. 자세한 내용은 아래를 참조하세요.

  2. 해당하는 C 전처리기 매크로를 1로 설정하여 모듈을 활성화합니다. 이는 빌드하려는 모듈이 자동으로 활성화되지 않는 경우에만 필요합니다.

MicroPython과 함께 제공되는 예제 모듈을 빌드하려면, Make의 경우 USER_C_MODULESexamples/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_MODULESmy_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++ 기본 메모리 할당자(즉, newdelete 연산자)는 일반적으로 C 동적 메모리 할당 위의 계층으로 구현됩니다.

C 동적 메모리 할당 지원을 포함하지 않는 MicroPython 포트의 경우, C++ 동적 메모리 할당은 다음 두 가지 방법 중 하나로 지원될 수 있습니다:

  • 사용자 정의 빌드에 C 동적 메모리 할당을 구현합니다.

  • 사용자 정의 빌드에 사용자 정의 C++ 할당자를 구현합니다.

링크 고려 사항

MicroPython은 C 기반 프로젝트이므로, MicroPython으로 또는 MicroPython으로부터 링크되는 모든 심볼은 C++ 코드에서 extern "C"로 한정되어야 합니다.

examples/usercmodule/cppexample에 나와 있는 패턴을 따르는 것이 강력히 권장됩니다. 여기서 Python 모듈은 C++ 코드를 감싸는 최소한의 C 파일 래퍼로 구현됩니다.