uctypes — 바이너리 데이터에 구조화된 방식으로 접근¶
이 모듈은 MicroPython을 위한 “외부 데이터 인터페이스(foreign data interface)”를 구현합니다. 그 기본 발상은 CPython의 ctypes 모듈과 유사하지만, 실제 API는 다르며 작은 크기에 맞게 간소화되고 최적화되어 있습니다. 이 모듈의 기본 아이디어는 C 언어가 허용하는 것과 거의 동일한 표현력으로 데이터 구조의 레이아웃을 정의한 다음, 익숙한 점(dot) 구문을 사용해 하위 필드를 참조하며 접근하는 것입니다.
경고
uctypes 모듈은 머신의 임의 메모리 주소(I/O 및 제어 레지스터 포함)에 접근할 수 있게 해줍니다. 부주의하게 사용하면 충돌, 데이터 손실, 심지어 하드웨어 오작동으로 이어질 수 있습니다.
더 보기
struct모듈바이너리 데이터를 패킹하고 언패킹하기 위한 표준 Python 모듈입니다.
struct는 컴팩트한 포맷 문자열(예:'<HBB4sI')을 사용해 한 번에 버퍼 전체를 다루는데, 이는 소수의 고정 필드에는 잘 동작하지만 크거나 깊게 중첩된 구조에는 잘 확장되지 않습니다. 즉 읽기나 쓰기를 할 때마다 포맷 문자열을 다시 파싱하고, 유니온과 비트필드를 지원하지 않으며, 기존 버퍼에 대한 타입화된 뷰를 얻을 방법도 없습니다.uctypes는 레이아웃을 한 번 기술한 뒤 이를 메모리 영역(RAM, 주변장치 레지스터,bytearray)에 붙이고, 개별 필드를 이름 있는 어트리뷰트로 접근할 수 있게 함으로써struct를 보완합니다. 이렇게 하면 반복적인 파싱과 복사를 피할 수 있고, 중첩 구조체, 배열, 유니온, 비트필드에 대한 지원도 더해집니다.
사용 예시:
import uctypes
# Example 1: Subset of ELF file header
# https://wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
ELF_HEADER = {
"EI_MAG": (0x0 | uctypes.ARRAY, 4 | uctypes.UINT8),
"EI_DATA": 0x5 | uctypes.UINT8,
"e_machine": 0x12 | uctypes.UINT16,
}
# "f" is an ELF file opened in binary mode
buf = f.read(uctypes.sizeof(ELF_HEADER, uctypes.LITTLE_ENDIAN))
header = uctypes.struct(uctypes.addressof(buf), ELF_HEADER, uctypes.LITTLE_ENDIAN)
assert header.EI_MAG == b"\x7fELF"
assert header.EI_DATA == 1, "Oops, wrong endianness. Could retry with uctypes.BIG_ENDIAN."
print("machine:", hex(header.e_machine))
# Example 2: In-memory data structure, with pointers
COORD = {
"x": 0 | uctypes.FLOAT32,
"y": 4 | uctypes.FLOAT32,
}
STRUCT1 = {
"data1": 0 | uctypes.UINT8,
"data2": 4 | uctypes.UINT32,
"ptr": (8 | uctypes.PTR, COORD),
}
# Suppose you have address of a structure of type STRUCT1 in "addr"
# uctypes.NATIVE is optional (used by default)
struct1 = uctypes.struct(addr, STRUCT1, uctypes.NATIVE)
print("x:", struct1.ptr[0].x)
# Example 3: Access to CPU registers. Subset of STM32F4xx WWDG block
WWDG_LAYOUT = {
"WWDG_CR": (0, {
# BFUINT32 here means size of the WWDG_CR register
"WDGA": 7 << uctypes.BF_POS | 1 << uctypes.BF_LEN | uctypes.BFUINT32,
"T": 0 << uctypes.BF_POS | 7 << uctypes.BF_LEN | uctypes.BFUINT32,
}),
"WWDG_CFR": (4, {
"EWI": 9 << uctypes.BF_POS | 1 << uctypes.BF_LEN | uctypes.BFUINT32,
"WDGTB": 7 << uctypes.BF_POS | 2 << uctypes.BF_LEN | uctypes.BFUINT32,
"W": 0 << uctypes.BF_POS | 7 << uctypes.BF_LEN | uctypes.BFUINT32,
}),
}
WWDG = uctypes.struct(0x40002c00, WWDG_LAYOUT)
WWDG.WWDG_CFR.WDGTB = 0b10
WWDG.WWDG_CR.WDGA = 1
print("Current counter:", WWDG.WWDG_CR.T)
구조체 레이아웃 정의하기¶
구조체 레이아웃은 “디스크립터” 즉, 필드 이름을 키로 인코딩하고 해당 필드에 접근하는 데 필요한 기타 속성을 연관 값으로 인코딩하는 Python 딕셔너리로 정의됩니다:
{
"field1": <properties>,
"field2": <properties>,
...
}
현재 uctypes 는 각 필드의 오프셋을 명시적으로 지정해야 합니다. 오프셋은 구조체 시작 지점으로부터의 바이트 수로 주어집니다.
다음은 다양한 필드 타입에 대한 인코딩 예시입니다:
스칼라 타입:
"field_name": offset | uctypes.UINT32다시 말해, 값은 스칼라 타입 식별자를 구조체 시작 지점으로부터의 필드 오프셋(바이트 단위)과 OR 연산한 것입니다.
재귀적 구조체:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
즉, 값은 2-튜플로, 첫 번째 요소는 오프셋이고 두 번째는 구조체 디스크립터 딕셔너리입니다(주의: 재귀적 디스크립터에서의 오프셋은 해당 디스크립터가 정의하는 구조체를 기준으로 합니다). 물론 재귀적 구조체는 리터럴 딕셔너리뿐만 아니라, (앞서 정의한) 구조체 디스크립터 딕셔너리를 이름으로 참조하여 지정할 수도 있습니다.
기본 타입의 배열:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),즉, 값은 2-튜플로, 첫 번째 요소는 ARRAY 플래그를 오프셋과 OR 연산한 것이고, 두 번째는 스칼라 요소 타입을 배열의 요소 개수와 OR 연산한 것입니다.
집합 타입의 배열:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),즉, 값은 3-튜플로, 첫 번째 요소는 ARRAY 플래그를 오프셋과 OR 연산한 것이고, 두 번째는 배열의 요소 개수이며, 세 번째는 요소 타입의 디스크립터입니다.
기본 타입에 대한 포인터:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),즉, 값은 2-튜플로, 첫 번째 요소는 PTR 플래그를 오프셋과 OR 연산한 것이고, 두 번째는 스칼라 요소 타입입니다.
집합 타입에 대한 포인터:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),즉, 값은 2-튜플로, 첫 번째 요소는 PTR 플래그를 오프셋과 OR 연산한 것이고, 두 번째는 가리키는 대상 타입의 디스크립터입니다.
비트필드:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,즉, 값은 주어진 비트필드를 담는 스칼라 값의 타입(타입 이름은 스칼라 타입과 유사하지만 앞에
BF가 붙습니다)을, 비트필드를 담는 스칼라 값의 오프셋과 OR 연산하고, 다시 스칼라 값 내에서 비트필드의 비트 위치와 비트 길이 값을 각각 BF_POS와 BF_LEN 비트만큼 시프트하여 OR 연산한 것입니다. 비트필드 위치는 스칼라의 최하위 비트(위치 0)부터 세며, 필드의 가장 오른쪽 비트의 번호입니다(다시 말해, 비트필드를 추출하기 위해 스칼라를 오른쪽으로 시프트해야 하는 비트 수입니다).위 예시에서는 먼저 오프셋 0에서 UINT16 값을 추출하고(이 세부 사항은 특정 접근 크기와 정렬이 요구되는 하드웨어 레지스터에 접근할 때 중요할 수 있습니다), 그런 다음 이 UINT16의 lsbit 비트를 가장 오른쪽 비트로 하고 길이가 bitsize 비트인 비트필드를 추출합니다. 예를 들어 lsbit 이 0이고 bitsize 가 8이면, 실질적으로 UINT16의 최하위 바이트에 접근하게 됩니다.
비트필드 연산은 대상의 바이트 엔디언과 무관하다는 점에 유의하십시오. 특히 위 예시는 리틀엔디언 구조체와 빅엔디언 구조체 모두에서 UINT16의 최하위 바이트에 접근합니다. 다만 최하위 비트가 0번으로 번호 매겨진다는 점에는 의존합니다. 일부 대상은 자신의 네이티브 ABI에서 다른 번호 매김을 사용할 수 있지만,
uctypes는 항상 위에서 설명한 정규화된 번호 매김을 사용합니다.
모듈 내용¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
메모리상의 구조체 주소, (딕셔너리로 인코딩된) 디스크립터, 그리고 레이아웃 타입(아래 참조)을 기반으로 “외부 데이터 구조체” 객체를 인스턴스화합니다.
- uctypes.LITTLE_ENDIAN: int¶
리틀엔디언 팩드(packed) 구조체를 위한 레이아웃 타입입니다. (팩드란 모든 필드가 디스크립터에 정의된 만큼의 바이트만 정확히 차지함을, 즉 정렬이 1임을 의미합니다.)
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
데이터 구조체의 크기를 바이트 단위로 반환합니다. struct 인수는 구조체 클래스이거나 인스턴스화된 특정 구조체 객체(또는 그 집합 필드)일 수 있습니다.
- uctypes.addressof(obj: Any) int¶
객체의 주소를 반환합니다. 인수는 bytes, bytearray 또는 버퍼 프로토콜을 지원하는 다른 객체여야 합니다(실제로 반환되는 것은 이 버퍼의 주소입니다).
- uctypes.bytes_at(addr: int, size: int) bytes¶
주어진 주소와 크기의 메모리를 bytes 객체로 캡처합니다. bytes 객체는 불변이므로 메모리는 실제로 복제되어 bytes 객체로 복사됩니다. 따라서 이후 메모리 내용이 바뀌더라도 생성된 객체는 원래 값을 유지합니다.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
주어진 주소와 크기의 메모리를 bytearray 객체로 캡처합니다. 위의 bytes_at() 함수와 달리 메모리는 참조로 캡처되므로, 쓰기도 가능하고 주어진 메모리 주소의 현재 값에 접근하게 됩니다.
스칼라 정수 타입입니다. 각각은 명백한 수의 바이트(1, 2, 4 또는 8)를 차지하며, 구조체의 레이아웃 타입(NATIVE, LITTLE_ENDIAN, BIG_ENDIAN 중 하나)의 엔디언을 사용해 읽고 씁니다.
- uctypes.VOID: int¶
UINT8의 별칭입니다. C 스타일의void *필드를(uctypes.PTR, uctypes.VOID)처럼 관용적으로 기술할 수 있도록 제공됩니다.
- uctypes.PTR: int¶
디스크립터 필드를 다른 타입에 대한 포인터로 표시합니다. 포인터 필드는 2-튜플
(offset | PTR, target_type_or_descriptor)로 작성합니다. 포인터를 역참조하면 그것이 담고 있는 주소에 대한 타입화된 뷰가 생성됩니다.
- uctypes.ARRAY: int¶
디스크립터 필드를 다른 타입의 고정 길이 배열로 표시합니다. 배열 필드는 스칼라 배열의 경우
(offset | ARRAY, count | element_type), 구조체 배열의 경우(offset | ARRAY, count, element_descriptor)입니다. 요소 개수는 디스크립터 정의 시점에 고정됩니다.
구조체를 위한 명시적 상수는 없습니다. PTR 도 ARRAY 도 사용하지 않는 집합 디스크립터는 구조체로 취급됩니다.
구조체 디스크립터와 구조체 객체 인스턴스화하기¶
구조체 디스크립터 딕셔너리와 그 레이아웃 타입이 주어지면, uctypes.struct() 생성자를 사용해 주어진 메모리 주소에 특정 구조체 인스턴스를 인스턴스화할 수 있습니다. 메모리 주소는 보통 다음 출처에서 나옵니다:
베어메탈 시스템에서 하드웨어 레지스터에 접근할 때의 사전 정의된 주소. 이러한 주소는 특정 MCU/SoC의 데이터시트에서 찾아보십시오.
어떤 FFI(Foreign Function Interface) 함수 호출의 반환 값으로서.
FFI 함수에 인수를 전달하고자 하거나, 또는 I/O를 위한 데이터(예: 파일이나 네트워크 소켓에서 읽은 데이터)에 접근하고자 할 때
uctypes.addressof()로부터.
구조체 객체¶
구조체 객체는 표준 점(dot) 표기법을 사용해 개별 필드에 접근할 수 있게 합니다: my_struct.substruct1.field1. 필드가 스칼라 타입이면, 그것을 가져오면 필드에 담긴 값에 해당하는 기본 값(Python 정수 또는 부동소수점)이 생성됩니다. 스칼라 필드에는 값을 대입할 수도 있습니다.
필드가 배열이면, 표준 첨자 연산자 [] 로 개별 요소에 접근할 수 있으며 읽기와 대입이 모두 가능합니다.
필드가 포인터이면, [0] 구문(C의 * 연산자에 해당하며, [0] 은 C에서도 동작합니다)으로 역참조할 수 있습니다. 0이 아닌 다른 정수 값으로 포인터에 첨자를 다는 것도 지원되며, 그 의미는 C와 동일합니다.
정리하자면, 구조체 필드 접근은 일반적으로 C 구문을 따르되, 포인터 역참조의 경우 * 대신 [0] 연산자를 사용해야 한다는 점이 다릅니다.
제한 사항¶
1. Accessing non-scalar fields leads to allocation of intermediate objects to represent them. This means that special care should be taken to layout a structure which needs to be accessed when memory allocation is disabled (e.g. from an interrupt). The recommendations are:
중첩 구조체 접근을 피하십시오. 예를 들어
mcu_registers.peripheral_a.register1대신, 각 주변장치마다 별도의 레이아웃 디스크립터를 정의하여peripheral_a.register1로 접근하십시오. 또는 특정 주변장치를 그냥 캐시하십시오:peripheral_a = mcu_registers.peripheral_a. 레지스터가 여러 비트필드로 구성되어 있다면, 특정 레지스터에 대한 참조를 캐시해야 합니다:reg_a = mcu_registers.peripheral_a.reg_a.배열과 같은 다른 비스칼라 데이터도 피하십시오. 예를 들어
peripheral_a.register[0]대신peripheral_a.register0를 사용하십시오. 마찬가지로, 대안은 중간 값을 캐시하는 것입니다(예:register0 = peripheral_a.register[0]).
2. Range of offsets supported by the uctypes module is limited.
The exact range supported is considered an implementation detail,
and the general suggestion is to split structure definitions to
cover from a few kilobytes to a few dozen of kilobytes maximum.
In most cases, this is a natural situation anyway, e.g. it doesn’t make
sense to define all registers of an MCU (spread over 32-bit address
space) in one structure, but rather a peripheral block by peripheral
block. In some extreme cases, you may need to split a structure in
several parts artificially (e.g. if accessing native data structure
with multi-megabyte array in the middle, though that would be a very
synthetic case).