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. struct pracuje 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. uctypes doplňuje struct tí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.UINT32
    

    jiný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 uctypes vž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.BIG_ENDIAN: int

Typ rozvržení pro big-endian zabalenou (packed) strukturu.

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.UINT8: int

Bezznaménkové 8bitové celé číslo. Rozsah 0255.

uctypes.INT8: int

Znaménkové 8bitové celé číslo. Rozsah -128127.

uctypes.UINT16: int

Bezznaménkové 16bitové celé číslo. Rozsah 065535.

uctypes.INT16: int

Znaménkové 16bitové celé číslo. Rozsah -3276832767.

uctypes.UINT32: int

Bezznaménkové 32bitové celé číslo. Rozsah 00xFFFFFFFF.

uctypes.INT32: int

Znaménkové 32bitové celé číslo. Rozsah -0x800000000x7FFFFFFF.

uctypes.UINT64: int

Bezznaménkové 64bitové celé číslo. Rozsah 00xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Znaménkové 64bitové celé číslo. Rozsah -0x80000000000000000x7FFFFFFFFFFFFFFF.

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 C void * 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.register1 definujte samostatné deskriptory rozvržení pro každou periferii, k nimž lze přistupovat jako peripheral_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žijte peripheral_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).