uctypes — strukturovaný přístup k binárním datům¶
Tento modul implementuje „rozhraní pro cizí data“ (foreign data interface) pro MicroPython. Myšlenka za ním je podobná modulu ctypes z CPythonu, ale samotné API je odlišné, zjednodušené a optimalizované pro malou velikost. Základní myšlenkou modulu je definovat rozvržení datové struktury přibližně se stejnými možnostmi, jaké umožňuje jazyk C, a poté k ní přistupovat pomocí známé tečkové syntaxe pro odkazování na dílčí pole.
Varování
Modul uctypes umožňuje přístup k libovolným adresám paměti stroje (včetně I/O a řídicích registrů). Neopatrné použití může vést k pádům, ztrátě dat, a dokonce i k poruše hardwaru.
Viz také
- Modul
struct Standardní modul Pythonu pro balení a rozbalování binárních dat.
structpracuje vždy s celými buffery najednou pomocí kompaktního formátovacího řetězce (např.'<HBB4sI'), což dobře funguje pro několik pevně daných polí, ale špatně se škáluje na velké nebo hluboce vnořené struktury: každé čtení nebo zápis znovu parsuje formátovací řetězec, sjednocení (unions) a bitová pole nejsou podporována a neexistuje způsob, jak získat typovaný pohled do existujícího bufferu.uctypesdoplňujestructtím, že vám umožní popsat rozvržení jednou, připojit ho k oblasti paměti (RAM, registry periferií,bytearray) a poté přistupovat k jednotlivým polím jako k pojmenovaným atributům – čímž se vyhnete opakovanému parsování a kopírování a přidává se podpora vnořených struktur, polí, sjednocení a bitových polí.
Příklady použití:
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)
Definování rozvržení struktury¶
Rozvržení struktury je definováno „deskriptorem“ - slovníkem Pythonu, který kóduje názvy polí jako klíče a další vlastnosti potřebné pro přístup k nim jako přidružené hodnoty:
{
"field1": <properties>,
"field2": <properties>,
...
}
V současnosti uctypes vyžaduje explicitní specifikaci offsetů pro každé pole. Offsety se udávají v bajtech od začátku struktury.
Následují příklady kódování pro různé typy polí:
Skalární typy:
"field_name": offset | uctypes.UINT32jinými slovy, hodnota je identifikátor skalárního typu spojený operací OR s offsetem pole (v bajtech) od začátku struktury.
Rekurzivní struktury:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
tj. hodnota je 2-tice, jejímž prvním prvkem je offset a druhým je slovník deskriptoru struktury (poznámka: offsety v rekurzivních deskriptorech jsou relativní vůči struktuře, kterou definují). Rekurzivní struktury lze samozřejmě specifikovat nejen literálním slovníkem, ale i odkazem na slovník deskriptoru struktury (definovaný dříve) podle jména.
Pole primitivních typů:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),tj. hodnota je 2-tice, jejímž prvním prvkem je příznak ARRAY spojený operací OR s offsetem a druhým je skalární typ prvku spojený operací OR s počtem prvků v poli.
Pole agregovaných typů:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),tj. hodnota je 3-tice, jejímž prvním prvkem je příznak ARRAY spojený operací OR s offsetem, druhým je počet prvků v poli a třetím je deskriptor typu prvku.
Ukazatel na primitivní typ:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),tj. hodnota je 2-tice, jejímž prvním prvkem je příznak PTR spojený operací OR s offsetem a druhým je skalární typ prvku.
Ukazatel na agregovaný typ:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),tj. hodnota je 2-tice, jejímž prvním prvkem je příznak PTR spojený operací OR s offsetem a druhým je deskriptor typu, na který ukazuje.
Bitová pole:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,tj. hodnota je typ skalární hodnoty obsahující dané bitové pole (názvy typů jsou podobné skalárním typům, ale s předponou
BF), spojený operací OR s offsetem pro skalární hodnotu obsahující bitové pole a dále spojený operací OR s hodnotami pro bitovou pozici a bitovou délku bitového pole v rámci skalární hodnoty, posunutými o BF_POS, respektive BF_LEN bitů. Pozice bitového pole se počítá od nejméně významného bitu skaláru (s pozicí 0) a je číslem nejpravějšího bitu pole (jinými slovy, je to počet bitů, o které je třeba skalár posunout doprava pro extrakci bitového pole).Ve výše uvedeném příkladu se nejprve extrahuje hodnota UINT16 na offsetu 0 (tento detail může být důležitý při přístupu k hardwarovým registrům, kde je vyžadována konkrétní velikost přístupu a zarovnání) a poté se extrahuje bitové pole, jehož nejpravější bit je bit lsbit tohoto UINT16 a délka je bitsize bitů. Například pokud je lsbit roven 0 a bitsize roven 8, pak se efektivně přistupuje k nejméně významnému bajtu UINT16.
Všimněte si, že operace s bitovými poli jsou nezávislé na endianitě cílových bajtů, zejména výše uvedený příklad přistupuje k nejméně významnému bajtu UINT16 jak v little-endian, tak v big-endian strukturách. Závisí to však na tom, že nejméně významný bit je číslován jako 0. Některé cíle mohou ve svém nativním ABI používat odlišné číslování, ale
uctypesvždy používá výše popsané normalizované číslování.
Obsah modulu¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Vytvoří instanci objektu „cizí datové struktury“ na základě adresy struktury v paměti, deskriptoru (zakódovaného jako slovník) a typu rozvržení (viz níže).
- uctypes.LITTLE_ENDIAN: int¶
Typ rozvržení pro little-endian zabalenou (packed) strukturu. (Zabalené znamená, že každé pole zabírá přesně tolik bajtů, kolik je definováno v deskriptoru, tj. zarovnání je 1).
- uctypes.NATIVE: int¶
Typ rozvržení pro nativní strukturu - s endianitou dat a zarovnáním odpovídajícím ABI systému, na kterém běží MicroPython.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Vrací velikost datové struktury v bajtech. Argument struct může být buď třída struktury, nebo konkrétní instance objektu struktury (nebo jeho agregované pole).
- uctypes.addressof(obj: Any) int¶
Vrací adresu objektu. Argument by měl být bytes, bytearray nebo jiný objekt podporující buffer protokol (a adresa tohoto bufferu je to, co se ve skutečnosti vrací).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Zachytí paměť na dané adrese a o dané velikosti jako objekt bytes. Protože objekt bytes je neměnný, paměť se ve skutečnosti duplikuje a zkopíruje do objektu bytes, takže pokud se obsah paměti později změní, vytvořený objekt si zachová původní hodnotu.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Zachytí paměť na dané adrese a o dané velikosti jako objekt bytearray. Na rozdíl od výše uvedené funkce bytes_at() se paměť zachytí odkazem, takže do ní lze i zapisovat a budete přistupovat k aktuální hodnotě na dané adrese paměti.
Skalární celočíselné typy. Každý zabírá zřejmý počet bajtů (1, 2, 4 nebo 8) a čte/zapisuje se s použitím endianity typu rozvržení struktury (jeden z NATIVE, LITTLE_ENDIAN nebo BIG_ENDIAN).
- uctypes.INT64: int¶
Znaménkové 64bitové celé číslo. Rozsah
-0x8000000000000000–0x7FFFFFFFFFFFFFFF.
- uctypes.FLOAT32: int¶
Číslo s pohyblivou řádovou čárkou v jednoduché přesnosti dle IEEE 754 (4 bajty). Čtení a zápisy se převádějí na/z Python
float.
- uctypes.FLOAT64: int¶
Číslo s pohyblivou řádovou čárkou v dvojnásobné přesnosti dle IEEE 754 (8 bajtů). Čtení a zápisy se převádějí na/z Python
float.
- uctypes.VOID: int¶
Alias pro
UINT8. Poskytuje se proto, aby bylo možné pole typu Cvoid *popsat idiomaticky jako(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
Označuje pole deskriptoru jako ukazatel na jiný typ. Pole ukazatele se zapisuje jako dvojice
(offset | PTR, target_type_or_descriptor). Dereference ukazatele poskytuje typovaný pohled na adresu, kterou obsahuje.
- uctypes.ARRAY: int¶
Označuje pole deskriptoru jako pole pevné délky jiného typu. Pole typu pole se zapisuje buď jako
(offset | ARRAY, count | element_type)pro pole skalárů, nebo jako(offset | ARRAY, count, element_descriptor)pro pole struktur. Počet prvků je pevně dán v čase definice deskriptoru.
Pro struktury neexistuje žádná explicitní konstanta: agregovaný deskriptor, který nepoužívá ani PTR, ani ARRAY, je považován za strukturu.
Deskriptory struktur a vytváření instancí objektů struktur¶
Při daném slovníku deskriptoru struktury a jeho typu rozvržení můžete vytvořit konkrétní instanci struktury na dané adrese paměti pomocí konstruktoru uctypes.struct(). Adresa paměti obvykle pochází z následujících zdrojů:
Předdefinovaná adresa při přístupu k hardwarovým registrům na baremetal systému. Tyto adresy vyhledejte v datovém listu konkrétního MCU/SoC.
Jako návratová hodnota volání nějaké FFI (Foreign Function Interface) funkce.
Z
uctypes.addressof(), když chcete předat argumenty FFI funkci, nebo alternativně přistupovat k nějakým datům pro I/O (například k datům přečteným ze souboru nebo síťového socketu).
Objekty struktur¶
Objekty struktur umožňují přístup k jednotlivým polím pomocí standardní tečkové notace: my_struct.substruct1.field1. Pokud je pole skalárního typu, jeho získání vytvoří primitivní hodnotu (celé číslo nebo float v Pythonu) odpovídající hodnotě obsažené v poli. Skalárnímu poli lze také přiřazovat.
Pokud je pole polem (array), k jeho jednotlivým prvkům lze přistupovat pomocí standardního operátoru indexování [] - lze je jak číst, tak jim přiřazovat.
Pokud je pole ukazatelem, lze ho dereferencovat pomocí syntaxe [0] (odpovídající operátoru * v jazyce C, ačkoli [0] funguje v C také). Indexování ukazatele jinými celočíselnými hodnotami než 0 je rovněž podporováno se stejnou sémantikou jako v C.
Shrnuto, přístup k polím struktury obecně odpovídá syntaxi jazyka C, s výjimkou dereference ukazatele, kdy je třeba místo * použít operátor [0].
Omezení¶
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:
Vyhněte se přístupu k vnořeným strukturám. Například místo
mcu_registers.peripheral_a.register1definujte samostatné deskriptory rozvržení pro každou periferii, k nimž lze přistupovat jakoperipheral_a.register1. Nebo si konkrétní periferii prostě uložte do mezipaměti:peripheral_a = mcu_registers.peripheral_a. Pokud se registr skládá z více bitových polí, museli byste si do mezipaměti uložit odkazy na konkrétní registr:reg_a = mcu_registers.peripheral_a.reg_a.Vyhněte se dalším neskalárním datům, jako jsou pole. Například místo
peripheral_a.register[0]použijteperipheral_a.register0. Opět platí, že alternativou je uložit mezilehlé hodnoty do mezipaměti, např.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).