uctypes — binaire data op een gestructureerde manier benaderen

Deze module implementeert de “foreign data interface” voor MicroPython. Het achterliggende idee is vergelijkbaar met de ctypes-modules van CPython, maar de daadwerkelijke API is anders, gestroomlijnd en geoptimaliseerd voor een kleine omvang. Het basisidee van de module is om de indeling van een datastructuur te definiëren met ongeveer dezelfde expressiviteit als de C-taal toestaat, en deze vervolgens te benaderen met de vertrouwde punt-syntax om subvelden te refereren.

Waarschuwing

De uctypes-module geeft toegang tot willekeurige geheugenadressen van de machine (inclusief I/O- en besturingsregisters). Onzorgvuldig gebruik ervan kan leiden tot crashes, dataverlies en zelfs hardwarestoringen.

Zie ook

Module struct

De standaard Python-module voor het inpakken en uitpakken van binaire data. struct werkt telkens op volledige buffers met behulp van een compacte formaatstring (bijv. '<HBB4sI'), wat goed werkt voor enkele vaste velden maar slecht schaalt naar grote of diep geneste structuren: elke lees- of schrijfbewerking parseert de formaatstring opnieuw, unions en bitfields worden niet ondersteund, en er is geen manier om een getypeerde weergave van een bestaande buffer te krijgen. uctypes vult struct aan doordat je de indeling eenmalig beschrijft, deze koppelt aan een geheugengebied (RAM, randapparaatregisters, een bytearray) en vervolgens individuele velden als benoemde attributen benadert – waardoor herhaald parseren en kopiëren worden vermeden en ondersteuning wordt toegevoegd voor geneste structs, arrays, unions en bitfields.

Gebruiksvoorbeelden:

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)

De indeling van een structuur definiëren

De indeling van een structuur wordt gedefinieerd door een “descriptor” - een Python-dictionary die veldnamen als sleutels codeert en de andere eigenschappen die nodig zijn om ze te benaderen als bijbehorende waarden:

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

Momenteel vereist uctypes een expliciete opgave van offsets voor elk veld. Offsets worden gegeven in bytes vanaf het begin van de structuur.

Hieronder volgen coderingsvoorbeelden voor verschillende veldtypen:

  • Scalaire typen:

    "field_name": offset | uctypes.UINT32
    

    met andere woorden, de waarde is een scalair type-identifier ge-OR’d met een veld-offset (in bytes) vanaf het begin van de structuur.

  • Recursieve structuren:

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

    d.w.z. de waarde is een 2-tuple, waarvan het eerste element een offset is en het tweede een dictionary met een structuurdescriptor (let op: offsets in recursieve descriptors zijn relatief ten opzichte van de structuur die ze definiëren). Recursieve structuren kunnen uiteraard niet alleen worden opgegeven via een letterlijke dictionary, maar ook door te verwijzen naar een (eerder gedefinieerde) dictionary met een structuurdescriptor op naam.

  • Arrays van primitieve typen:

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

    d.w.z. de waarde is een 2-tuple, waarvan het eerste element de ARRAY-vlag is ge-OR’d met de offset, en het tweede het scalaire elementtype ge-OR’d met het aantal elementen in de array.

  • Arrays van samengestelde typen:

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

    d.w.z. de waarde is een 3-tuple, waarvan het eerste element de ARRAY-vlag is ge-OR’d met de offset, het tweede het aantal elementen in de array, en het derde een descriptor van het elementtype.

  • Pointer naar een primitief type:

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

    d.w.z. de waarde is een 2-tuple, waarvan het eerste element de PTR-vlag is ge-OR’d met de offset, en het tweede een scalair elementtype.

  • Pointer naar een samengesteld type:

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

    d.w.z. de waarde is een 2-tuple, waarvan het eerste element de PTR-vlag is ge-OR’d met de offset, en het tweede een descriptor van het type waarnaar wordt verwezen.

  • Bitfields:

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

    d.w.z. de waarde is een type scalaire waarde dat het opgegeven bitfield bevat (typenamen zijn vergelijkbaar met scalaire typen, maar voorafgegaan door BF), ge-OR’d met de offset voor de scalaire waarde die het bitfield bevat, en verder ge-OR’d met waarden voor de bitpositie en bitlengte van het bitfield binnen de scalaire waarde, respectievelijk verschoven over BF_POS en BF_LEN bits. Een bitfieldpositie wordt geteld vanaf de minst significante bit van de scalair (met positie 0), en is het nummer van de meest rechtse bit van een veld (met andere woorden, het is het aantal bits waarover een scalair naar rechts moet worden verschoven om het bitfield te extraheren).

    In het bovenstaande voorbeeld wordt eerst een UINT16-waarde geëxtraheerd op offset 0 (dit detail kan belangrijk zijn bij het benaderen van hardwareregisters, waar een bepaalde toegangsgrootte en uitlijning vereist zijn), en vervolgens wordt het bitfield geëxtraheerd waarvan de meest rechtse bit de lsbit-bit van deze UINT16 is, en de lengte bitsize bits is. Als lsbit bijvoorbeeld 0 is en bitsize 8, dan benadert het in feite de minst significante byte van de UINT16.

    Merk op dat bitfieldbewerkingen onafhankelijk zijn van de byte-endianness van het doel; het bovenstaande voorbeeld benadert in het bijzonder de minst significante byte van de UINT16 in zowel little-endian als big-endian structuren. Het hangt echter wel af van het feit dat de minst significante bit nummer 0 is. Sommige doelen gebruiken mogelijk een andere nummering in hun native ABI, maar uctypes gebruikt altijd de hierboven beschreven genormaliseerde nummering.

Module-inhoud

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

Instantieert een “foreign data structure”-object op basis van een structuuradres in het geheugen, een descriptor (gecodeerd als een dictionary) en een layout-type (zie hieronder).

uctypes.LITTLE_ENDIAN: int

Layout-type voor een little-endian packed structuur. (Packed betekent dat elk veld precies zoveel bytes inneemt als gedefinieerd in de descriptor, d.w.z. de uitlijning is 1).

uctypes.BIG_ENDIAN: int

Layout-type voor een big-endian packed structuur.

uctypes.NATIVE: int

Layout-type voor een native structuur - met data-endianness en uitlijning conform de ABI van het systeem waarop MicroPython draait.

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

Geeft de grootte van de datastructuur in bytes terug. Het argument struct kan zowel een structuurklasse zijn als een specifiek geïnstantieerd structuurobject (of een samengesteld veld daarvan).

uctypes.addressof(obj: Any) int

Geeft het adres van een object terug. Het argument moet bytes, bytearray of een ander object zijn dat het bufferprotocol ondersteunt (en het adres van deze buffer is wat daadwerkelijk wordt teruggegeven).

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

Legt het geheugen op het opgegeven adres en met de opgegeven grootte vast als een bytes-object. Aangezien een bytes-object onveranderlijk is, wordt het geheugen feitelijk gedupliceerd en in het bytes-object gekopieerd, dus als de geheugeninhoud later verandert, behoudt het aangemaakte object de oorspronkelijke waarde.

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

Legt het geheugen op het opgegeven adres en met de opgegeven grootte vast als een bytearray-object. In tegenstelling tot de bovenstaande functie bytes_at() wordt het geheugen vastgelegd via een referentie, zodat er zowel naar geschreven kan worden als dat je de huidige waarde op het opgegeven geheugenadres benadert.

Scalaire integertypen. Elk neemt het voor de hand liggende aantal bytes in (1, 2, 4 of 8) en wordt gelezen/geschreven met de endianness van het layout-type van de structuur (een van NATIVE, LITTLE_ENDIAN of BIG_ENDIAN).

uctypes.UINT8: int

Unsigned 8-bits integer. Bereik 0255.

uctypes.INT8: int

Signed 8-bits integer. Bereik -128127.

uctypes.UINT16: int

Unsigned 16-bits integer. Bereik 065535.

uctypes.INT16: int

Signed 16-bits integer. Bereik -3276832767.

uctypes.UINT32: int

Unsigned 32-bits integer. Bereik 00xFFFFFFFF.

uctypes.INT32: int

Signed 32-bits integer. Bereik -0x800000000x7FFFFFFF.

uctypes.UINT64: int

Unsigned 64-bits integer. Bereik 00xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Signed 64-bits integer. Bereik -0x80000000000000000x7FFFFFFFFFFFFFFF.

uctypes.FLOAT32: int

IEEE 754 single-precision floating-point (4 bytes). Lees- en schrijfbewerkingen worden geconverteerd van/naar een Python float.

uctypes.FLOAT64: int

IEEE 754 double-precision floating-point (8 bytes). Lees- en schrijfbewerkingen worden geconverteerd van/naar een Python float.

uctypes.VOID: int

Alias voor UINT8. Aangeboden zodat void *-velden in C-stijl idiomatisch kunnen worden beschreven als (uctypes.PTR, uctypes.VOID).

uctypes.PTR: int

Markeert een descriptorveld als een pointer naar een ander type. Een pointerveld wordt geschreven als een 2-tuple (offset | PTR, target_type_or_descriptor). Het dereferentiëren van de pointer levert een getypeerde weergave op van het adres dat hij bevat.

uctypes.ARRAY: int

Markeert een descriptorveld als een array met vaste lengte van een ander type. Een arrayveld is ofwel (offset | ARRAY, count | element_type) voor arrays van scalairen, ofwel (offset | ARRAY, count, element_descriptor) voor arrays van structuren. Het aantal elementen ligt vast op het moment van descriptordefinitie.

Er is geen expliciete constante voor structuren: een samengestelde descriptor die noch PTR noch ARRAY gebruikt, wordt als een structuur behandeld.

Structuurdescriptors en het instantiëren van structuurobjecten

Gegeven een dictionary met een structuurdescriptor en het bijbehorende layout-type, kun je een specifieke structuurinstantie op een bepaald geheugenadres instantiëren met de constructor uctypes.struct(). Het geheugenadres komt meestal uit de volgende bronnen:

  • Een vooraf gedefinieerd adres, bij het benaderen van hardwareregisters op een baremetal-systeem. Zoek deze adressen op in het datasheet van een bepaalde MCU/SoC.

  • Als retourwaarde van een aanroep naar een FFI-functie (Foreign Function Interface).

  • Vanuit uctypes.addressof(), wanneer je argumenten wilt doorgeven aan een FFI-functie, of als alternatief om bepaalde data voor I/O te benaderen (bijvoorbeeld data die uit een bestand of netwerksocket is gelezen).

Structuurobjecten

Structuurobjecten maken het mogelijk om individuele velden te benaderen met de standaard puntnotatie: my_struct.substruct1.field1. Als een veld van een scalair type is, levert het opvragen ervan een primitieve waarde op (een Python-integer of -float) die overeenkomt met de waarde in het veld. Aan een scalair veld kan ook een waarde worden toegekend.

Als een veld een array is, kunnen de individuele elementen ervan worden benaderd met de standaard subscript-operator [] - zowel om te lezen als om aan toe te kennen.

Als een veld een pointer is, kan deze worden gedereferentieerd met de [0]-syntax (overeenkomend met de C-operator *, hoewel [0] ook in C werkt). Het subscripten van een pointer met andere integerwaarden dan 0 wordt ook ondersteund, met dezelfde semantiek als in C.

Samengevat volgt het benaderen van structuurvelden over het algemeen de C-syntax, behalve voor het dereferentiëren van pointers, waarbij je de [0]-operator moet gebruiken in plaats van *.

Beperkingen

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:

  • Vermijd het benaderen van geneste structuren. Definieer bijvoorbeeld in plaats van mcu_registers.peripheral_a.register1 aparte layout-descriptors voor elk randapparaat, te benaderen als peripheral_a.register1. Of cache gewoon een bepaald randapparaat: peripheral_a = mcu_registers.peripheral_a. Als een register uit meerdere bitfields bestaat, zou je referenties naar een bepaald register moeten cachen: reg_a = mcu_registers.peripheral_a.reg_a.

  • Vermijd andere niet-scalaire data, zoals arrays. Gebruik bijvoorbeeld in plaats van peripheral_a.register[0] liever peripheral_a.register0. Ook hier is een alternatief om tussenliggende waarden te cachen, bijv. 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).