uctypes — Zugriff auf Binärdaten in strukturierter Form¶
Dieses Modul implementiert eine „Schnittstelle für fremde Daten“ (foreign data interface) für MicroPython. Die zugrunde liegende Idee ähnelt den ctypes-Modulen von CPython, aber die eigentliche API ist anders, optimiert und auf geringe Größe ausgelegt. Die Grundidee des Moduls besteht darin, das Layout einer Datenstruktur mit etwa derselben Ausdruckskraft zu definieren, die auch die Sprache C erlaubt, und dann mit der vertrauten Punkt-Syntax auf Unterfelder zuzugreifen.
Warnung
Das Modul uctypes erlaubt den Zugriff auf beliebige Speicheradressen der Maschine (einschließlich I/O- und Steuerregistern). Unvorsichtige Verwendung kann zu Abstürzen, Datenverlust und sogar zu Hardware-Fehlfunktionen führen.
Siehe auch
- Modul
struct Das Standard-Python-Modul zum Packen und Entpacken von Binärdaten.
structarbeitet jeweils mit ganzen Puffern unter Verwendung eines kompakten Formatstrings (z. B.'<HBB4sI'), was bei wenigen festen Feldern gut funktioniert, aber bei großen oder tief verschachtelten Strukturen schlecht skaliert: Jeder Lese- oder Schreibvorgang parst den Formatstring erneut, Unions und Bitfelder werden nicht unterstützt, und es gibt keine Möglichkeit, eine typisierte Sicht auf einen vorhandenen Puffer zu erhalten.uctypesergänztstruct, indem es Ihnen erlaubt, das Layout einmal zu beschreiben, es an einen Speicherbereich (RAM, Peripherieregister, einbytearray) anzuhängen und dann auf einzelne Felder als benannte Attribute zuzugreifen – wodurch wiederholtes Parsen und Kopieren vermieden und Unterstützung für verschachtelte Strukturen, Arrays, Unions und Bitfelder hinzugefügt wird.
Verwendungsbeispiele:
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)
Definition des Strukturlayouts¶
Das Strukturlayout wird durch einen „Deskriptor“ definiert - ein Python-Dictionary, das Feldnamen als Schlüssel und weitere zum Zugriff erforderliche Eigenschaften als zugehörige Werte kodiert:
{
"field1": <properties>,
"field2": <properties>,
...
}
Derzeit erfordert uctypes die explizite Angabe von Offsets für jedes Feld. Offsets werden in Bytes ab dem Strukturanfang angegeben.
Im Folgenden finden Sie Kodierungsbeispiele für verschiedene Feldtypen:
Skalare Typen:
"field_name": offset | uctypes.UINT32Mit anderen Worten: Der Wert ist eine skalare Typkennung, die mit einem Feld-Offset (in Bytes) ab dem Strukturanfang per ODER verknüpft ist.
Rekursive Strukturen:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
Das heißt, der Wert ist ein 2-Tupel, dessen erstes Element ein Offset und dessen zweites ein Strukturdeskriptor-Dictionary ist (Hinweis: Offsets in rekursiven Deskriptoren sind relativ zu der Struktur, die sie definieren). Natürlich können rekursive Strukturen nicht nur durch ein wörtliches Dictionary angegeben werden, sondern auch durch den Verweis auf ein (zuvor definiertes) Strukturdeskriptor-Dictionary anhand seines Namens.
Arrays primitiver Typen:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),Das heißt, der Wert ist ein 2-Tupel, dessen erstes Element das ARRAY-Flag per ODER mit dem Offset verknüpft ist und dessen zweites der skalare Elementtyp per ODER mit der Anzahl der Elemente im Array ist.
Arrays zusammengesetzter Typen:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),Das heißt, der Wert ist ein 3-Tupel, dessen erstes Element das ARRAY-Flag per ODER mit dem Offset verknüpft ist, dessen zweites die Anzahl der Elemente im Array und dessen drittes ein Deskriptor des Elementtyps ist.
Zeiger auf einen primitiven Typ:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),Das heißt, der Wert ist ein 2-Tupel, dessen erstes Element das PTR-Flag per ODER mit dem Offset verknüpft ist und dessen zweites ein skalarer Elementtyp ist.
Zeiger auf einen zusammengesetzten Typ:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),Das heißt, der Wert ist ein 2-Tupel, dessen erstes Element das PTR-Flag per ODER mit dem Offset verknüpft ist und dessen zweites ein Deskriptor des Typs ist, auf den gezeigt wird.
Bitfelder:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,Das heißt, der Wert ist ein skalarer Werttyp, der das angegebene Bitfeld enthält (die Typnamen ähneln den skalaren Typen, sind jedoch mit
BFpräfixiert), per ODER verknüpft mit dem Offset für den das Bitfeld enthaltenden skalaren Wert und weiter per ODER verknüpft mit den Werten für Bitposition und Bitlänge des Bitfelds innerhalb des skalaren Werts, jeweils um BF_POS bzw. BF_LEN Bits verschoben. Eine Bitfeldposition wird vom niederwertigsten Bit des Skalars an gezählt (mit der Position 0) und entspricht der Nummer des am weitesten rechts liegenden Bits eines Feldes (mit anderen Worten: Es ist die Anzahl der Bits, um die ein Skalar nach rechts verschoben werden muss, um das Bitfeld zu extrahieren).Im obigen Beispiel wird zunächst ein UINT16-Wert am Offset 0 extrahiert (dieses Detail kann beim Zugriff auf Hardwareregister wichtig sein, bei denen eine bestimmte Zugriffsgröße und Ausrichtung erforderlich sind), und dann wird das Bitfeld extrahiert, dessen am weitesten rechts liegendes Bit das lsbit-Bit dieses UINT16 ist und dessen Länge bitsize Bits beträgt. Ist beispielsweise lsbit gleich 0 und bitsize gleich 8, so wird effektiv auf das niederwertigste Byte des UINT16 zugegriffen.
Beachten Sie, dass Bitfeldoperationen unabhängig von der Byte-Reihenfolge des Zielsystems sind; insbesondere greift das obige Beispiel sowohl in Little-Endian- als auch in Big-Endian-Strukturen auf das niederwertigste Byte des UINT16 zu. Es hängt jedoch davon ab, dass das niederwertigste Bit mit 0 nummeriert wird. Manche Zielsysteme verwenden in ihrer nativen ABI eine andere Nummerierung, aber
uctypesverwendet stets die oben beschriebene normalisierte Nummerierung.
Modulinhalt¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Instanziiert ein Objekt für eine „fremde Datenstruktur“ auf Basis der Strukturadresse im Speicher, des Deskriptors (kodiert als Dictionary) und des Layout-Typs (siehe unten).
- uctypes.LITTLE_ENDIAN: int¶
Layout-Typ für eine gepackte Little-Endian-Struktur. (Gepackt bedeutet, dass jedes Feld genau so viele Bytes belegt, wie im Deskriptor definiert sind, d. h. die Ausrichtung ist 1).
- uctypes.NATIVE: int¶
Layout-Typ für eine native Struktur - bei der Datenendianness und Ausrichtung der ABI des Systems entsprechen, auf dem MicroPython ausgeführt wird.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Gibt die Größe der Datenstruktur in Bytes zurück. Das Argument struct kann entweder eine Strukturklasse oder ein konkret instanziiertes Strukturobjekt (oder dessen zusammengesetztes Feld) sein.
- uctypes.addressof(obj: Any) int¶
Gibt die Adresse eines Objekts zurück. Das Argument sollte bytes, bytearray oder ein anderes Objekt sein, das das Pufferprotokoll unterstützt (und die Adresse dieses Puffers ist es, die tatsächlich zurückgegeben wird).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Erfasst den Speicher an der angegebenen Adresse und Größe als bytes-Objekt. Da ein bytes-Objekt unveränderlich ist, wird der Speicher tatsächlich dupliziert und in das bytes-Objekt kopiert; ändert sich der Speicherinhalt also später, behält das erzeugte Objekt seinen ursprünglichen Wert.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Erfasst den Speicher an der angegebenen Adresse und Größe als bytearray-Objekt. Im Gegensatz zur obigen Funktion bytes_at() wird der Speicher per Referenz erfasst, sodass er sowohl beschrieben werden kann als auch stets auf den aktuellen Wert an der angegebenen Speicheradresse zugegriffen wird.
Skalare Ganzzahltypen. Jeder belegt die naheliegende Anzahl von Bytes (1, 2, 4 oder 8) und wird mit der Byte-Reihenfolge des Layout-Typs der Struktur gelesen/geschrieben (einer von NATIVE, LITTLE_ENDIAN oder BIG_ENDIAN).
- uctypes.INT64: int¶
Vorzeichenbehaftete 64-Bit-Ganzzahl. Bereich
-0x8000000000000000–0x7FFFFFFFFFFFFFFF.
- uctypes.FLOAT32: int¶
IEEE-754-Gleitkommazahl mit einfacher Genauigkeit (4 Bytes). Lese- und Schreibvorgänge werden in einen bzw. aus einem Python-
floatumgewandelt.
- uctypes.FLOAT64: int¶
IEEE-754-Gleitkommazahl mit doppelter Genauigkeit (8 Bytes). Lese- und Schreibvorgänge werden in einen bzw. aus einem Python-
floatumgewandelt.
- uctypes.VOID: int¶
Alias für
UINT8. Wird bereitgestellt, damit Felder im C-Stil vom Typvoid *idiomatisch als(uctypes.PTR, uctypes.VOID)beschrieben werden können.
- uctypes.PTR: int¶
Kennzeichnet ein Deskriptorfeld als Zeiger auf einen anderen Typ. Ein Zeigerfeld wird als 2-Tupel
(offset | PTR, target_type_or_descriptor)geschrieben. Das Dereferenzieren des Zeigers liefert eine typisierte Sicht auf die Adresse, die er enthält.
- uctypes.ARRAY: int¶
Kennzeichnet ein Deskriptorfeld als Array fester Länge eines anderen Typs. Ein Array-Feld ist entweder
(offset | ARRAY, count | element_type)für Arrays von Skalaren oder(offset | ARRAY, count, element_descriptor)für Arrays von Strukturen. Die Anzahl der Elemente wird zum Zeitpunkt der Deskriptordefinition festgelegt.
Es gibt keine explizite Konstante für Strukturen: Ein zusammengesetzter Deskriptor, der weder PTR noch ARRAY verwendet, wird als Struktur behandelt.
Strukturdeskriptoren und das Instanziieren von Strukturobjekten¶
Mit einem Strukturdeskriptor-Dictionary und seinem Layout-Typ können Sie über den Konstruktor uctypes.struct() eine konkrete Strukturinstanz an einer bestimmten Speicheradresse instanziieren. Die Speicheradresse stammt üblicherweise aus den folgenden Quellen:
Eine vordefinierte Adresse beim Zugriff auf Hardwareregister eines Baremetal-Systems. Schlagen Sie diese Adressen im Datenblatt des jeweiligen MCU/SoC nach.
Als Rückgabewert eines Aufrufs einer FFI-Funktion (Foreign Function Interface).
Aus
uctypes.addressof(), wenn Sie Argumente an eine FFI-Funktion übergeben möchten oder alternativ auf Daten für die Ein-/Ausgabe zugreifen wollen (zum Beispiel auf Daten, die aus einer Datei oder einem Netzwerk-Socket gelesen wurden).
Strukturobjekte¶
Strukturobjekte erlauben den Zugriff auf einzelne Felder über die übliche Punktnotation: my_struct.substruct1.field1. Ist ein Feld vom skalaren Typ, liefert das Abrufen einen primitiven Wert (Python-Integer oder -Float), der dem im Feld enthaltenen Wert entspricht. Ein skalares Feld kann auch zugewiesen werden.
Ist ein Feld ein Array, kann auf seine einzelnen Elemente mit dem üblichen Subskript-Operator [] zugegriffen werden - sowohl lesend als auch zuweisend.
Ist ein Feld ein Zeiger, kann er mit der [0]-Syntax dereferenziert werden (entsprechend dem C-Operator *, wobei [0] auch in C funktioniert). Das Indizieren eines Zeigers mit anderen Ganzzahlwerten als 0 wird ebenfalls unterstützt, mit derselben Semantik wie in C.
Zusammengefasst folgt der Zugriff auf Strukturfelder im Allgemeinen der C-Syntax, mit Ausnahme der Zeigerdereferenzierung, bei der Sie statt * den Operator [0] verwenden müssen.
Einschränkungen¶
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:
Vermeiden Sie den Zugriff auf verschachtelte Strukturen. Definieren Sie zum Beispiel statt
mcu_registers.peripheral_a.register1separate Layout-Deskriptoren für jedes Peripheriegerät, auf die alsperipheral_a.register1zugegriffen wird. Oder zwischenspeichern Sie einfach ein bestimmtes Peripheriegerät:peripheral_a = mcu_registers.peripheral_a. Besteht ein Register aus mehreren Bitfeldern, müssten Sie Referenzen auf ein bestimmtes Register zwischenspeichern:reg_a = mcu_registers.peripheral_a.reg_a.Vermeiden Sie andere nicht-skalare Daten, wie etwa Arrays. Verwenden Sie zum Beispiel statt
peripheral_a.register[0]lieberperipheral_a.register0. Auch hier besteht eine Alternative darin, Zwischenwerte zwischenzuspeichern, z. B.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).