uctypes — åtkomst till binära data på ett strukturerat sätt¶
Denna modul implementerar ett ”gränssnitt för främmande data” (foreign data interface) för MicroPython. Tanken bakom den liknar CPythons ctypes-modul, men det faktiska API:et är annorlunda, förenklat och optimerat för liten storlek. Modulens grundidé är att definiera en datastrukturs layout med ungefär samma uttryckskraft som C-språket tillåter, och sedan komma åt den med välbekant punktsyntax för att referera till delfält.
Varning
uctypes-modulen ger åtkomst till godtyckliga minnesadresser i maskinen (inklusive I/O- och styrregister). Oförsiktig användning av den kan leda till krascher, dataförlust och till och med felfunktion i hårdvaran.
Se även
- Modul
struct Standardmodulen i Python för att packa och packa upp binära data.
structarbetar på hela buffertar åt gången med en kompakt formatsträng (t.ex.'<HBB4sI'), vilket fungerar bra för ett fåtal fasta fält men skalar dåligt till stora eller djupt nästlade strukturer: varje läsning eller skrivning tolkar formatsträngen på nytt, unioner och bitfält stöds inte, och det finns inget sätt att få en typad vy in i en befintlig buffert.uctypeskompletterarstructgenom att låta dig beskriva layouten en gång, koppla den till en minnesregion (RAM, kringutrustningsregister, enbytearray) och sedan komma åt enskilda fält som namngivna attribut – vilket undviker upprepad tolkning och kopiering, och lägger till stöd för nästlade strukturer, arrayer, unioner och bitfält.
Användningsexempel:
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)
Definiera strukturlayout¶
Strukturlayouten definieras av en ”deskriptor” - en Python-ordbok som kodar fältnamn som nycklar och andra egenskaper som krävs för att komma åt dem som tillhörande värden:
{
"field1": <properties>,
"field2": <properties>,
...
}
För närvarande kräver uctypes att offset anges explicit för varje fält. Offset anges i byte från strukturens början.
Följande är kodningsexempel för olika fälttyper:
Skalära typer:
"field_name": offset | uctypes.UINT32med andra ord är värdet en skalär typidentifierare OR:ad med en fältoffset (i byte) från strukturens början.
Rekursiva strukturer:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
dvs. värdet är en 2-tupel vars första element är en offset och vars andra är en ordbok som beskriver strukturen (observera: offset i rekursiva deskriptorer är relativa till den struktur de definierar). Rekursiva strukturer kan naturligtvis anges inte bara med en bokstavlig ordbok, utan genom att referera till en strukturdeskriptor-ordbok (definierad tidigare) med namn.
Arrayer av primitiva typer:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),dvs. värdet är en 2-tupel vars första element är ARRAY-flaggan OR:ad med en offset, och vars andra är den skalära elementtypen OR:ad med antalet element i arrayen.
Arrayer av sammansatta typer:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),dvs. värdet är en 3-tupel vars första element är ARRAY-flaggan OR:ad med en offset, andra är antalet element i arrayen, och tredje är en deskriptor av elementtypen.
Pekare till en primitiv typ:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),dvs. värdet är en 2-tupel vars första element är PTR-flaggan OR:ad med en offset, och vars andra är en skalär elementtyp.
Pekare till en sammansatt typ:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),dvs. värdet är en 2-tupel vars första element är PTR-flaggan OR:ad med en offset, och vars andra är en deskriptor av typen som pekas på.
Bitfält:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,dvs. värdet är en typ av skalärt värde som innehåller det givna bitfältet (typnamnen liknar de skalära typerna, men med prefixet
BF), OR:ad med offseten för det skalära värde som innehåller bitfältet, och vidare OR:ad med värden för bitfältets bitposition och bitlängd inom det skalära värdet, förskjutna med BF_POS respektive BF_LEN bitar. En bitfältsposition räknas från den minst signifikanta biten i skaläret (med position 0) och är numret på fältets längst åt höger placerade bit (med andra ord är det antalet bitar som ett skalär behöver skiftas åt höger för att extrahera bitfältet).I exemplet ovan extraheras först ett UINT16-värde vid offset 0 (denna detalj kan vara viktig vid åtkomst till hårdvaruregister, där en viss åtkomststorlek och justering krävs), och därefter extraheras det bitfält vars längst åt höger placerade bit är biten lsbit i detta UINT16, och vars längd är bitsize bitar. Om till exempel lsbit är 0 och bitsize är 8, så kommer det i praktiken åt den minst signifikanta byten i UINT16.
Observera att bitfältsoperationer är oberoende av målets byteordning; i synnerhet kommer exemplet ovan åt den minst signifikanta byten i UINT16 i såväl little-endian- som big-endian-strukturer. Men det förutsätter att den minst signifikanta biten är numrerad 0. Vissa mål kan använda en annan numrering i sitt ursprungliga ABI, men
uctypesanvänder alltid den normaliserade numrering som beskrivs ovan.
Modulinnehåll¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Instansierar ett objekt av typen ”främmande datastruktur” baserat på strukturadressen i minnet, en deskriptor (kodad som en ordbok) och en layouttyp (se nedan).
- uctypes.LITTLE_ENDIAN: int¶
Layouttyp för en packad little-endian-struktur. (Packad innebär att varje fält upptar exakt så många byte som definieras i deskriptorn, dvs. justeringen är 1).
- uctypes.NATIVE: int¶
Layouttyp för en ursprunglig (native) struktur - med dataendianness och justering som överensstämmer med ABI:t för det system som MicroPython körs på.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Returnerar datastrukturens storlek i byte. Argumentet struct kan vara antingen en strukturklass eller ett specifikt instansierat strukturobjekt (eller dess sammansatta fält).
- uctypes.addressof(obj: Any) int¶
Returnerar adressen till ett objekt. Argumentet ska vara bytes, bytearray eller ett annat objekt som stöder buffertprotokollet (och adressen till denna buffert är vad som faktiskt returneras).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Fångar minnet vid den givna adressen och storleken som ett bytes-objekt. Eftersom ett bytes-objekt är oföränderligt dupliceras och kopieras minnet i praktiken in i bytes-objektet, så om minnesinnehållet ändras senare behåller det skapade objektet sitt ursprungliga värde.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Fångar minnet vid den givna adressen och storleken som ett bytearray-objekt. Till skillnad från funktionen bytes_at() ovan fångas minnet via referens, så det kan både skrivas till, och du kommer åt det aktuella värdet vid den givna minnesadressen.
Skalära heltalstyper. Var och en upptar det uppenbara antalet byte (1, 2, 4 eller 8) och läses/skrivs med endianness enligt strukturens layouttyp (en av NATIVE, LITTLE_ENDIAN eller BIG_ENDIAN).
- uctypes.INT64: int¶
Heltal med tecken, 64 bitar. Intervall
-0x8000000000000000–0x7FFFFFFFFFFFFFFF.
- uctypes.FLOAT32: int¶
IEEE 754 flyttal med enkel precision (4 byte). Läsningar och skrivningar konverteras till/från en Python-
float.
- uctypes.FLOAT64: int¶
IEEE 754 flyttal med dubbel precision (8 byte). Läsningar och skrivningar konverteras till/från en Python-
float.
- uctypes.VOID: int¶
Alias för
UINT8. Tillhandahålls så att fält av C-typenvoid *kan beskrivas idiomatiskt som(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
Markerar ett deskriptorfält som en pekare till en annan typ. Ett pekarfält skrivs som en tvåtupel
(offset | PTR, target_type_or_descriptor). Att dereferera pekaren ger en typad vy in i den adress den innehåller.
- uctypes.ARRAY: int¶
Markerar ett deskriptorfält som en array med fast längd av en annan typ. Ett arrayfält är antingen
(offset | ARRAY, count | element_type)för arrayer av skalärer eller(offset | ARRAY, count, element_descriptor)för arrayer av strukturer. Antalet element fastställs vid deskriptortillfället.
Det finns ingen explicit konstant för strukturer: en sammansatt deskriptor som varken använder PTR eller ARRAY behandlas som en struktur.
Strukturdeskriptorer och instansiering av strukturobjekt¶
Givet en strukturdeskriptor-ordbok och dess layouttyp kan du instansiera en specifik strukturinstans på en given minnesadress med konstruktorn uctypes.struct(). Minnesadressen kommer vanligtvis från följande källor:
En fördefinierad adress, vid åtkomst till hårdvaruregister på ett baremetal-system. Slå upp dessa adresser i databladet för en viss MCU/SoC.
Som returvärde från ett anrop till någon FFI-funktion (Foreign Function Interface).
Från
uctypes.addressof(), när du vill skicka argument till en FFI-funktion, eller alternativt för att komma åt data för I/O (till exempel data som lästs från en fil eller en nätverkssocket).
Strukturobjekt¶
Strukturobjekt gör det möjligt att komma åt enskilda fält med standardpunktnotation: my_struct.substruct1.field1. Om ett fält är av skalär typ ger hämtning av det ett primitivt värde (ett Python-heltal eller -flyttal) som motsvarar värdet i fältet. Ett skalärt fält kan också tilldelas ett värde.
Om ett fält är en array kan dess enskilda element nås med standardindexeringsoperatorn [] - både läsas och tilldelas.
Om ett fält är en pekare kan det dereferas med syntaxen [0] (vilket motsvarar C-operatorn *, även om [0] också fungerar i C). Att indexera en pekare med andra heltalsvärden än 0 stöds också, med samma semantik som i C.
Sammanfattningsvis följer åtkomst till strukturfält i allmänhet C-syntaxen, förutom vid dereferering av pekare, då du behöver använda operatorn [0] i stället för *.
Begränsningar¶
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:
Undvik att komma åt nästlade strukturer. I stället för
mcu_registers.peripheral_a.register1definierar du till exempel separata layoutdeskriptorer för varje kringutrustning, som nås somperipheral_a.register1. Eller cachelagra bara en viss kringutrustning:peripheral_a = mcu_registers.peripheral_a. Om ett register består av flera bitfält behöver du cachelagra referenser till ett specifikt register:reg_a = mcu_registers.peripheral_a.reg_a.Undvik andra icke-skalära data, som arrayer. I stället för
peripheral_a.register[0]använder du till exempelperipheral_a.register0. Återigen är ett alternativ att cachelagra mellanliggande värden, t.ex.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).