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.
structwerkt 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.uctypesvultstructaan doordat je de indeling eenmalig beschrijft, deze koppelt aan een geheugengebied (RAM, randapparaatregisters, eenbytearray) 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.UINT32met 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
uctypesgebruikt 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.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.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 zodatvoid *-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.register1aparte layout-descriptors voor elk randapparaat, te benaderen alsperipheral_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]lieverperipheral_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).