uctypes — bináris adatok strukturált elérése

Ez a modul a MicroPython „idegenadat-interfészét” valósítja meg. A mögöttes elgondolás hasonló a CPython ctypes moduljához, de a tényleges API más, áramvonalas és kis méretre optimalizált. A modul alapötlete, hogy az adatszerkezet elrendezését nagyjából olyan kifejezőerővel írjuk le, mint amit a C nyelv lehetővé tesz, majd a megszokott pont-szintaxissal érjük el az almezőkre hivatkozva.

Figyelem

Az uctypes modul hozzáférést biztosít a gép tetszőleges memóriacímeihez (beleértve az I/O- és vezérlőregisztereket is). Óvatlan használata összeomlásokhoz, adatvesztéshez, sőt akár hardvermeghibásodáshoz is vezethet.

Lásd még

struct modul

A bináris adatok csomagolására és kicsomagolására szolgáló standard Python modul. A struct egész puffereken dolgozik egyszerre, kompakt formátum-karakterlánc (pl. '<HBB4sI') segítségével, ami néhány rögzített mező esetén jól működik, de nagy vagy mélyen beágyazott szerkezetekre rosszul skálázódik: minden olvasás vagy írás újraelemzi a formátum-karakterláncot, az uniókat és bitmezőket nem támogatja, és nincs mód arra, hogy egy meglévő pufferről típusos nézetet kapjunk. A uctypes kiegészíti a struct modult azzal, hogy az elrendezést egyszer leírhatjuk, egy memóriarégióhoz (RAM, perifériaregiszterek, egy bytearray) csatolhatjuk, majd az egyes mezőket nevesített attribútumokként érhetjük el – elkerülve az ismételt elemzést és másolást, és támogatva a beágyazott szerkezeteket, tömböket, uniókat és bitmezőket.

Használati példák:

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)

Szerkezet elrendezésének definiálása

A szerkezet elrendezését egy „leíró” határozza meg - egy Python szótár, amely a mezőneveket kulcsként, az eléréshez szükséges egyéb tulajdonságokat pedig a hozzájuk tartozó értékként kódolja:

{
    "field1": <properties>,
    "field2": <properties>,
    ...
}

Jelenleg az uctypes megköveteli minden mező eltolásának explicit megadását. Az eltolásokat a szerkezet kezdetétől számított bájtokban kell megadni.

Az alábbiakban kódolási példák szerepelnek a különböző mezőtípusokhoz:

  • Skalár típusok:

    "field_name": offset | uctypes.UINT32
    

    más szóval az érték egy skalártípus-azonosító, amely a szerkezet kezdetétől számított mezőeltolással (bájtban) van ORozva.

  • Rekurzív szerkezetek:

    "sub": (offset, {
        "b0": 0 | uctypes.UINT8,
        "b1": 1 | uctypes.UINT8,
    })
    

    vagyis az érték egy 2-elemű tuple, amelynek első eleme egy eltolás, a második pedig egy szerkezetleíró szótár (megjegyzés: a rekurzív leírókban az eltolások az általuk definiált szerkezethez viszonyítva értendők). Természetesen a rekurzív szerkezetek nem csak literál szótárral adhatók meg, hanem egy (korábban definiált) szerkezetleíró szótárra névvel hivatkozva is.

  • Primitív típusok tömbjei:

    "arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),
    

    vagyis az érték egy 2-elemű tuple, amelynek első eleme az ARRAY flag eltolással ORozva, a második pedig a skalár elemtípus a tömb elemszámával ORozva.

  • Összetett típusok tömbjei:

    "arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),
    

    vagyis az érték egy 3-elemű tuple, amelynek első eleme az ARRAY flag eltolással ORozva, a második a tömb elemeinek száma, a harmadik pedig az elemtípus leírója.

  • Mutató primitív típusra:

    "ptr": (offset | uctypes.PTR, uctypes.UINT8),
    

    vagyis az érték egy 2-elemű tuple, amelynek első eleme a PTR flag eltolással ORozva, a második pedig egy skalár elemtípus.

  • Mutató összetett típusra:

    "ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),
    

    vagyis az érték egy 2-elemű tuple, amelynek első eleme a PTR flag eltolással ORozva, a második pedig a mutatott típus leírója.

  • Bitmezők:

    "bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,
    

    vagyis az érték az adott bitmezőt tartalmazó skalárérték típusa (a típusnevek hasonlóak a skalár típusokhoz, de BF előtaggal), amely a bitmezőt tartalmazó skalárérték eltolásával van ORozva, majd a bitmező bitpozíciójának és bithosszának értékeivel is, amelyek a skalárértéken belül rendre BF_POS és BF_LEN bittel vannak eltolva. A bitmező pozícióját a skalár legkisebb helyiértékű bitjétől számítjuk (amelynek pozíciója 0), és ez a mező legjobboldalibb bitjének száma (más szóval ennyi bittel kell jobbra eltolni a skalárt a bitmező kinyeréséhez).

    A fenti példában először egy UINT16 érték kerül kinyerésre a 0-s eltolásnál (ez a részlet fontos lehet hardverregiszterek elérésekor, ahol meghatározott hozzáférési méret és igazítás szükséges), majd az a bitmező kerül kinyerésre, amelynek legjobboldalibb bitje ennek a UINT16-nak a lsbit bitje, hossza pedig bitsize bit. Például, ha lsbit értéke 0 és bitsize értéke 8, akkor gyakorlatilag a UINT16 legkisebb helyiértékű bájtját éri el.

    Vegyük figyelembe, hogy a bitmező-műveletek függetlenek a célplatform bájtsorrendjétől, így a fenti példa a UINT16 legkisebb helyiértékű bájtját éri el little- és big-endian szerkezetekben egyaránt. Azonban attól függ, hogy a legkisebb helyiértékű bit számozása 0. Egyes célplatformok eltérő számozást használhatnak a natív ABI-jukban, de az uctypes mindig a fent leírt normalizált számozást használja.

A modul tartalma

class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)

Létrehoz egy „idegenadat-szerkezet” objektumot a memóriabeli szerkezetcím, a (szótárként kódolt) leíró és az elrendezés típusa (lásd alább) alapján.

uctypes.LITTLE_ENDIAN: int

Elrendezés típusa little-endian csomagolt szerkezethez. (A csomagolt azt jelenti, hogy minden mező pontosan annyi bájtot foglal el, amennyi a leíróban definiálva van, azaz az igazítás 1.)

uctypes.BIG_ENDIAN: int

Elrendezés típusa big-endian csomagolt szerkezethez.

uctypes.NATIVE: int

Elrendezés típusa natív szerkezethez - az adatok bájtsorrendje és igazítása annak a rendszernek az ABI-jához igazodik, amelyen a MicroPython fut.

uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int

Visszaadja az adatszerkezet méretét bájtokban. A struct argumentum lehet egy szerkezetosztály vagy egy konkrét példányosított szerkezetobjektum (vagy annak összetett mezője).

uctypes.addressof(obj: Any) int

Visszaadja egy objektum címét. Az argumentumnak bytes-nak, bytearray-nek vagy más, a pufferprotokollt támogató objektumnak kell lennie (és valójában ennek a puffernek a címét adja vissza).

uctypes.bytes_at(addr: int, size: int) bytes

Rögzíti a megadott címen és méreten lévő memóriát bytes objektumként. Mivel a bytes objektum megváltoztathatatlan, a memória valójában megkettőződik és a bytes objektumba másolódik, így ha a memória tartalma később megváltozik, a létrehozott objektum megtartja az eredeti értéket.

uctypes.bytearray_at(addr: int, size: int) bytearray

Rögzíti a megadott címen és méreten lévő memóriát bytearray objektumként. A fenti bytes_at() függvénnyel ellentétben a memória hivatkozással kerül rögzítésre, így írható is, és mindig a megadott memóriacímen lévő aktuális értéket éri el.

Skalár egész típusok. Mindegyik a kézenfekvő számú bájtot foglalja el (1, 2, 4 vagy 8), és a szerkezet elrendezésének bájtsorrendjével (a NATIVE, LITTLE_ENDIAN vagy BIG_ENDIAN egyikével) olvasandó/írandó.

uctypes.UINT8: int

Előjel nélküli 8 bites egész. Tartomány: 0255.

uctypes.INT8: int

Előjeles 8 bites egész. Tartomány: -128127.

uctypes.UINT16: int

Előjel nélküli 16 bites egész. Tartomány: 065535.

uctypes.INT16: int

Előjeles 16 bites egész. Tartomány: -3276832767.

uctypes.UINT32: int

Előjel nélküli 32 bites egész. Tartomány: 00xFFFFFFFF.

uctypes.INT32: int

Előjeles 32 bites egész. Tartomány: -0x800000000x7FFFFFFF.

uctypes.UINT64: int

Előjel nélküli 64 bites egész. Tartomány: 00xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Előjeles 64 bites egész. Tartomány: -0x80000000000000000x7FFFFFFFFFFFFFFF.

uctypes.FLOAT32: int

IEEE 754 egyszeres pontosságú lebegőpontos szám (4 bájt). Az olvasások és írások Python float típusra/típusból konvertálódnak.

uctypes.FLOAT64: int

IEEE 754 dupla pontosságú lebegőpontos szám (8 bájt). Az olvasások és írások Python float típusra/típusból konvertálódnak.

uctypes.VOID: int

A UINT8 aliasa. Azért biztosított, hogy a C-stílusú void * mezők idiomatikusan leírhatók legyenek (uctypes.PTR, uctypes.VOID) formában.

uctypes.PTR: int

Egy leírómezőt másik típusra mutató mutatóként jelöl meg. A mutatómezőt két elemű tuple-ként írjuk: (offset | PTR, target_type_or_descriptor). A mutató dereferálása az általa tárolt címre adott típusos nézetet eredményez.

uctypes.ARRAY: int

Egy leírómezőt másik típus rögzített hosszúságú tömbjeként jelöl meg. A tömbmező vagy (offset | ARRAY, count | element_type) skalárok tömbjéhez, vagy (offset | ARRAY, count, element_descriptor) szerkezetek tömbjéhez. Az elemek száma a leíró megadásakor rögzül.

Szerkezetekhez nincs explicit konstans: egy olyan összetett leíró, amely sem a PTR, sem az ARRAY jelölést nem használja, szerkezetként kezelt.

Szerkezetleírók és szerkezetobjektumok példányosítása

Egy szerkezetleíró szótár és annak elrendezéstípusa alapján egy konkrét szerkezetpéldányt egy adott memóriacímen a uctypes.struct() konstruktorral példányosíthatsz. A memóriacím általában a következő forrásokból származik:

  • Előre meghatározott cím, amikor hardverregisztereket érünk el egy baremetal rendszeren. Ezeket a címeket az adott MCU/SoC adatlapjában kell megkeresni.

  • Egy FFI (Foreign Function Interface) függvényhívás visszatérési értékeként.

  • A uctypes.addressof() függvényből, amikor argumentumokat szeretnél átadni egy FFI függvénynek, vagy alternatívaként valamilyen adatot szeretnél elérni I/O céljából (például fájlból vagy hálózati socketből olvasott adatot).

Szerkezetobjektumok

A szerkezetobjektumok lehetővé teszik az egyes mezők elérését a standard pontjelöléssel: my_struct.substruct1.field1. Ha egy mező skalár típusú, lekérése a mezőben tárolt értéknek megfelelő primitív értéket (Python egész vagy float) ad. Egy skalár mezőhöz értéket is rendelhetünk.

Ha egy mező tömb, az egyes elemei a standard indexelő operátorral [] érhetők el - olvasásra és értékadásra egyaránt.

Ha egy mező mutató, akkor a [0] szintaxissal dereferálható (ami a C * operátorának felel meg, bár a [0] C-ben is működik). A mutató 0-tól eltérő egész értékekkel való indexelése is támogatott, ugyanazzal a szemantikával, mint C-ben.

Összefoglalva: a szerkezetmezők elérése általában a C szintaxist követi, kivéve a mutató dereferálását, ahol a * helyett a [0] operátort kell használni.

Korlátozások

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:

  • Kerüld a beágyazott szerkezetek elérését. Például az mcu_registers.peripheral_a.register1 helyett definiálj külön elrendezésleírókat minden perifériához, amelyeket peripheral_a.register1 formában érhetsz el. Vagy egyszerűen gyorsítótárazz egy adott perifériát: peripheral_a = mcu_registers.peripheral_a. Ha egy regiszter több bitmezőből áll, akkor egy adott regiszterre mutató hivatkozásokat kell gyorsítótárazni: reg_a = mcu_registers.peripheral_a.reg_a.

  • Kerüld a többi nem skalár adatot, például a tömböket. Például a peripheral_a.register[0] helyett használd a peripheral_a.register0 formát. Itt is alternatíva a köztes értékek gyorsítótárazása, pl. 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).