uctypes — dostęp do danych binarnych w sposób strukturalny¶
Ten moduł implementuje „interfejs danych obcych” (foreign data interface) dla MicroPython. Idea, która za nim stoi, jest podobna do modułu ctypes z CPython, ale rzeczywiste API jest inne, uproszczone i zoptymalizowane pod kątem małego rozmiaru. Podstawowa idea modułu polega na zdefiniowaniu układu struktury danych z mniej więcej taką samą siłą wyrazu, jaką oferuje język C, a następnie na dostępie do niej za pomocą znanej składni z kropką do odwoływania się do podpól.
Ostrzeżenie
Moduł uctypes umożliwia dostęp do dowolnych adresów pamięci maszyny (w tym do rejestrów wejścia/wyjścia i sterowania). Nieostrożne użycie może prowadzić do awarii, utraty danych, a nawet uszkodzenia sprzętu.
Zobacz także
- Moduł
struct Standardowy moduł Pythona do pakowania i rozpakowywania danych binarnych.
structoperuje na całych buforach naraz przy użyciu zwartego łańcucha formatu (np.'<HBB4sI'), co dobrze sprawdza się przy kilku stałych polach, ale słabo skaluje się dla dużych lub głęboko zagnieżdżonych struktur: każdy odczyt lub zapis na nowo parsuje łańcuch formatu, unie i pola bitowe nie są obsługiwane, a nie ma sposobu na uzyskanie typowanego widoku istniejącego bufora.uctypesuzupełniastruct, pozwalając opisać układ raz, dołączyć go do obszaru pamięci (RAM, rejestry urządzeń peryferyjnych,bytearray), a następnie uzyskiwać dostęp do poszczególnych pól jako nazwanych atrybutów – unikając wielokrotnego parsowania i kopiowania oraz dodając obsługę zagnieżdżonych struktur, tablic, unii i pól bitowych.
Przykłady użycia:
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)
Definiowanie układu struktury¶
Układ struktury jest definiowany przez „deskryptor” - słownik Pythona, który koduje nazwy pól jako klucze, a inne właściwości wymagane do dostępu do nich jako powiązane wartości:
{
"field1": <properties>,
"field2": <properties>,
...
}
Obecnie uctypes wymaga jawnego określenia przesunięć (offsetów) dla każdego pola. Przesunięcia podawane są w bajtach od początku struktury.
Poniżej znajdują się przykłady kodowania dla różnych typów pól:
Typy skalarne:
"field_name": offset | uctypes.UINT32innymi słowy, wartość to identyfikator typu skalarnego połączony operatorem OR z przesunięciem pola (w bajtach) od początku struktury.
Struktury rekurencyjne:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
tzn. wartość jest 2-elementową krotką, której pierwszym elementem jest przesunięcie, a drugim słownik deskryptora struktury (uwaga: przesunięcia w deskryptorach rekurencyjnych są względne wobec struktury, którą definiują). Oczywiście struktury rekurencyjne można określić nie tylko za pomocą literału słownika, ale także odwołując się po nazwie do słownika deskryptora struktury (zdefiniowanego wcześniej).
Tablice typów prostych:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),tzn. wartość jest 2-elementową krotką, której pierwszym elementem jest flaga ARRAY połączona operatorem OR z przesunięciem, a drugim typ elementu skalarnego połączony operatorem OR z liczbą elementów tablicy.
Tablice typów złożonych:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),tzn. wartość jest 3-elementową krotką, której pierwszym elementem jest flaga ARRAY połączona operatorem OR z przesunięciem, drugim liczba elementów tablicy, a trzecim deskryptor typu elementu.
Wskaźnik do typu prostego:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),tzn. wartość jest 2-elementową krotką, której pierwszym elementem jest flaga PTR połączona operatorem OR z przesunięciem, a drugim typ elementu skalarnego.
Wskaźnik do typu złożonego:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),tzn. wartość jest 2-elementową krotką, której pierwszym elementem jest flaga PTR połączona operatorem OR z przesunięciem, a drugim deskryptor typu, na który wskazuje wskaźnik.
Pola bitowe:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,tzn. wartość jest typem wartości skalarnej zawierającej dane pole bitowe (nazwy typów są podobne do typów skalarnych, ale poprzedzone przedrostkiem
BF), połączonym operatorem OR z przesunięciem wartości skalarnej zawierającej pole bitowe, a następnie ponownie połączonym operatorem OR z wartościami pozycji bitu i długości bitowej pola bitowego w obrębie wartości skalarnej, przesuniętymi odpowiednio o bity BF_POS i BF_LEN. Pozycja pola bitowego jest liczona od najmniej znaczącego bitu skalara (mającego pozycję 0) i jest numerem skrajnie prawego bitu pola (innymi słowy, jest to liczba bitów, o jaką skalar musi zostać przesunięty w prawo, aby wyodrębnić pole bitowe).W powyższym przykładzie najpierw zostanie wyodrębniona wartość UINT16 pod przesunięciem 0 (ten szczegół może być istotny przy dostępie do rejestrów sprzętowych, gdzie wymagany jest określony rozmiar dostępu i wyrównanie), a następnie zostanie wyodrębnione pole bitowe, którego skrajnie prawym bitem jest bit lsbit tego UINT16, a długość wynosi bitsize bitów. Na przykład, jeśli lsbit wynosi 0, a bitsize wynosi 8, to faktycznie nastąpi dostęp do najmniej znaczącego bajtu UINT16.
Należy zauważyć, że operacje na polach bitowych są niezależne od kolejności bajtów (endianness) celu; w szczególności powyższy przykład uzyska dostęp do najmniej znaczącego bajtu UINT16 zarówno w strukturach little-endian, jak i big-endian. Zależy to jednak od tego, że najmniej znaczący bit jest numerowany jako 0. Niektóre cele mogą używać innego numerowania w swoim natywnym ABI, ale
uctypeszawsze stosuje znormalizowane numerowanie opisane powyżej.
Zawartość modułu¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Tworzy obiekt „obcej struktury danych” na podstawie adresu struktury w pamięci, deskryptora (zakodowanego jako słownik) oraz typu układu (patrz niżej).
- uctypes.LITTLE_ENDIAN: int¶
Typ układu dla upakowanej struktury little-endian. (Upakowana oznacza, że każde pole zajmuje dokładnie tyle bajtów, ile zdefiniowano w deskryptorze, tzn. wyrównanie wynosi 1).
- uctypes.NATIVE: int¶
Typ układu dla struktury natywnej - z kolejnością bajtów i wyrównaniem danych zgodnymi z ABI systemu, na którym działa MicroPython.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Zwraca rozmiar struktury danych w bajtach. Argument struct może być klasą struktury albo konkretnym utworzonym obiektem struktury (lub jego polem złożonym).
- uctypes.addressof(obj: Any) int¶
Zwraca adres obiektu. Argument powinien być obiektem bytes, bytearray lub innym obiektem obsługującym protokół bufora (i to właśnie adres tego bufora jest faktycznie zwracany).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Przechwytuje pamięć pod podanym adresem o podanym rozmiarze jako obiekt bytes. Ponieważ obiekt bytes jest niemodyfikowalny, pamięć jest w rzeczywistości duplikowana i kopiowana do obiektu bytes, więc jeśli zawartość pamięci później się zmieni, utworzony obiekt zachowa pierwotną wartość.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Przechwytuje pamięć pod podanym adresem o podanym rozmiarze jako obiekt bytearray. W przeciwieństwie do funkcji bytes_at() powyżej, pamięć jest przechwytywana przez referencję, więc można do niej zarówno zapisywać, jak i uzyskiwać dostęp do bieżącej wartości pod podanym adresem pamięci.
Skalarne typy całkowite. Każdy zajmuje oczywistą liczbę bajtów (1, 2, 4 lub 8) i jest odczytywany/zapisywany z użyciem kolejności bajtów (endianness) typu układu struktury (jednego z NATIVE, LITTLE_ENDIAN lub BIG_ENDIAN).
- uctypes.INT64: int¶
64-bitowa liczba całkowita ze znakiem. Zakres
-0x8000000000000000–0x7FFFFFFFFFFFFFFF.
- uctypes.FLOAT32: int¶
Liczba zmiennoprzecinkowa pojedynczej precyzji IEEE 754 (4 bajty). Odczyty i zapisy są konwertowane do/z typu
floatPythona.
- uctypes.FLOAT64: int¶
Liczba zmiennoprzecinkowa podwójnej precyzji IEEE 754 (8 bajtów). Odczyty i zapisy są konwertowane do/z typu
floatPythona.
- uctypes.VOID: int¶
Alias dla
UINT8. Udostępniony po to, aby polavoid *w stylu C mogły być opisywane idiomatycznie jako(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
Oznacza pole deskryptora jako wskaźnik do innego typu. Pole wskaźnika zapisuje się jako dwuelementową krotkę
(offset | PTR, target_type_or_descriptor). Dereferencja wskaźnika daje typowany widok adresu, który przechowuje.
- uctypes.ARRAY: int¶
Oznacza pole deskryptora jako tablicę o stałej długości innego typu. Pole tablicy ma postać
(offset | ARRAY, count | element_type)dla tablic skalarów lub(offset | ARRAY, count, element_descriptor)dla tablic struktur. Liczba elementów jest ustalana w momencie tworzenia deskryptora.
Nie ma jawnej stałej dla struktur: deskryptor złożony, który nie używa ani PTR, ani ARRAY, jest traktowany jako struktura.
Deskryptory struktur i tworzenie obiektów struktur¶
Mając słownik deskryptora struktury i jego typ układu, możesz utworzyć konkretną instancję struktury pod podanym adresem pamięci za pomocą konstruktora uctypes.struct(). Adres pamięci zwykle pochodzi z następujących źródeł:
Predefiniowany adres, przy dostępie do rejestrów sprzętowych na systemie baremetal. Adresy te należy wyszukać w karcie katalogowej (datasheet) konkretnego MCU/SoC.
Jako wartość zwracana z wywołania pewnej funkcji FFI (Foreign Function Interface).
Z
uctypes.addressof(), gdy chcesz przekazać argumenty do funkcji FFI lub alternatywnie uzyskać dostęp do pewnych danych dla operacji wejścia/wyjścia (na przykład danych odczytanych z pliku lub gniazda sieciowego).
Obiekty struktur¶
Obiekty struktur umożliwiają dostęp do poszczególnych pól przy użyciu standardowej notacji z kropką: my_struct.substruct1.field1. Jeśli pole jest typu skalarnego, jego pobranie da wartość prostą (liczbę całkowitą lub zmiennoprzecinkową Pythona) odpowiadającą wartości zawartej w polu. Do pola skalarnego można również przypisywać.
Jeśli pole jest tablicą, dostęp do jego poszczególnych elementów można uzyskać za pomocą standardowego operatora indeksowania [] - zarówno do odczytu, jak i przypisania.
Jeśli pole jest wskaźnikiem, można je zdereferencjonować za pomocą składni [0] (odpowiadającej operatorowi * w C, choć [0] działa również w C). Indeksowanie wskaźnika innymi wartościami całkowitymi niż 0 jest również obsługiwane, z tą samą semantyką co w C.
Podsumowując, dostęp do pól struktury generalnie odbywa się zgodnie ze składnią C, z wyjątkiem dereferencji wskaźnika, gdzie zamiast * należy użyć operatora [0].
Ograniczenia¶
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:
Unikaj dostępu do zagnieżdżonych struktur. Na przykład zamiast
mcu_registers.peripheral_a.register1zdefiniuj osobne deskryptory układu dla każdego urządzenia peryferyjnego, aby uzyskiwać do nich dostęp jakoperipheral_a.register1. Albo po prostu zapamiętaj (zbuforuj) konkretne urządzenie peryferyjne:peripheral_a = mcu_registers.peripheral_a. Jeśli rejestr składa się z wielu pól bitowych, trzeba będzie zbuforować referencje do konkretnego rejestru:reg_a = mcu_registers.peripheral_a.reg_a.Unikaj innych danych nieskalarnych, takich jak tablice. Na przykład zamiast
peripheral_a.register[0]użyjperipheral_a.register0. Ponownie, alternatywą jest buforowanie wartości pośrednich, np.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).