uctypes — accès structuré aux données binaires¶
Ce module implémente une « interface de données externes » pour MicroPython. L’idée sous-jacente est similaire à celle du module ctypes de CPython, mais l’API réelle est différente, simplifiée et optimisée pour une faible empreinte mémoire. Le principe de base du module consiste à définir la disposition d’une structure de données avec à peu près la même puissance que celle qu’autorise le langage C, puis à y accéder à l’aide de la syntaxe par points familière pour référencer les sous-champs.
Avertissement
Le module uctypes permet d’accéder à des adresses mémoire arbitraires de la machine (y compris les registres d’entrée/sortie et de contrôle). Une utilisation imprudente peut entraîner des plantages, des pertes de données, voire un dysfonctionnement matériel.
Voir aussi
- Module
struct Le module Python standard pour l’empaquetage et le dépaquetage de données binaires.
structopère sur des tampons entiers à la fois à l’aide d’une chaîne de format compacte (par exemple'<HBB4sI'), ce qui convient bien à quelques champs fixes mais s’adapte mal aux structures volumineuses ou profondément imbriquées : chaque lecture ou écriture réanalyse la chaîne de format, les unions et les champs de bits ne sont pas pris en charge, et il n’existe aucun moyen d’obtenir une vue typée sur un tampon existant.uctypescomplètestructen vous permettant de décrire la disposition une seule fois, de l’attacher à une région mémoire (RAM, registres de périphériques, unbytearray), puis d’accéder aux champs individuels en tant qu’attributs nommés – ce qui évite les analyses et les copies répétées, tout en ajoutant la prise en charge des structures imbriquées, des tableaux, des unions et des champs de bits.
Exemples d’utilisation
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)
Définition de la disposition d’une structure¶
La disposition d’une structure est définie par un « descripteur » - un dictionnaire Python qui encode les noms de champs en tant que clés et les autres propriétés nécessaires pour y accéder en tant que valeurs associées
{
"field1": <properties>,
"field2": <properties>,
...
}
Actuellement, uctypes exige la spécification explicite des décalages pour chaque champ. Les décalages sont indiqués en octets à partir du début de la structure.
Voici des exemples d’encodage pour différents types de champs :
Types scalaires
"field_name": offset | uctypes.UINT32autrement dit, la valeur est un identifiant de type scalaire combiné par OR avec un décalage de champ (en octets) à partir du début de la structure.
Structures récursives
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
c’est-à-dire que la valeur est un tuple de deux éléments, dont le premier est un décalage et le second un dictionnaire descripteur de structure (note : les décalages dans les descripteurs récursifs sont relatifs à la structure qu’ils définissent). Bien entendu, les structures récursives peuvent être spécifiées non seulement par un dictionnaire littéral, mais aussi en se référant par son nom à un dictionnaire descripteur de structure (défini précédemment).
Tableaux de types primitifs
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),c’est-à-dire que la valeur est un tuple de deux éléments, dont le premier est le drapeau ARRAY combiné par OR avec un décalage, et le second le type d’élément scalaire combiné par OR avec le nombre d’éléments du tableau.
Tableaux de types agrégés
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),c’est-à-dire que la valeur est un tuple de trois éléments, dont le premier est le drapeau ARRAY combiné par OR avec un décalage, le second le nombre d’éléments du tableau, et le troisième un descripteur du type d’élément.
Pointeur vers un type primitif
"ptr": (offset | uctypes.PTR, uctypes.UINT8),c’est-à-dire que la valeur est un tuple de deux éléments, dont le premier est le drapeau PTR combiné par OR avec un décalage, et le second un type d’élément scalaire.
Pointeur vers un type agrégé
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),c’est-à-dire que la valeur est un tuple de deux éléments, dont le premier est le drapeau PTR combiné par OR avec un décalage, et le second un descripteur du type pointé.
Champs de bits
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,c’est-à-dire que la valeur est un type de valeur scalaire contenant le champ de bits donné (les noms de types sont similaires aux types scalaires, mais préfixés par
BF), combiné par OR avec le décalage de la valeur scalaire contenant le champ de bits, puis combiné par OR avec les valeurs de la position et de la longueur en bits du champ de bits au sein de la valeur scalaire, décalées respectivement de BF_POS et BF_LEN bits. La position d’un champ de bits est comptée à partir du bit de poids le plus faible du scalaire (qui a la position 0) et correspond au numéro du bit le plus à droite d’un champ (autrement dit, c’est le nombre de bits dont un scalaire doit être décalé vers la droite pour extraire le champ de bits).Dans l’exemple ci-dessus, une valeur UINT16 sera d’abord extraite au décalage 0 (ce détail peut être important lors de l’accès aux registres matériels, où une taille d’accès et un alignement particuliers sont requis), puis le champ de bits dont le bit le plus à droite est le bit lsbit de ce UINT16, et dont la longueur est de bitsize bits, sera extrait. Par exemple, si lsbit vaut 0 et bitsize vaut 8, cela accédera effectivement à l’octet de poids le plus faible du UINT16.
Notez que les opérations sur les champs de bits sont indépendantes du boutisme de la cible ; en particulier, l’exemple ci-dessus accédera à l’octet de poids le plus faible du UINT16 dans les structures little-endian comme big-endian. En revanche, cela dépend du fait que le bit de poids le plus faible porte le numéro 0. Certaines cibles peuvent utiliser une numérotation différente dans leur ABI native, mais
uctypesutilise toujours la numérotation normalisée décrite ci-dessus.
Contenu du module¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Instancie un objet « structure de données externes » à partir d’une adresse de structure en mémoire, d’un descripteur (encodé sous forme de dictionnaire) et d’un type de disposition (voir ci-dessous).
- uctypes.LITTLE_ENDIAN: int¶
Type de disposition pour une structure compactée little-endian. (Compactée signifie que chaque champ occupe exactement le nombre d’octets défini dans le descripteur, c’est-à-dire que l’alignement est de 1).
- uctypes.NATIVE: int¶
Type de disposition pour une structure native - dont le boutisme et l’alignement des données sont conformes à l’ABI du système sur lequel MicroPython s’exécute.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Renvoie la taille de la structure de données en octets. L’argument struct peut être soit une classe de structure, soit un objet structure instancié spécifique (ou l’un de ses champs agrégés).
- uctypes.addressof(obj: Any) int¶
Renvoie l’adresse d’un objet. L’argument doit être un objet bytes, bytearray ou tout autre objet prenant en charge le protocole de tampon (et c’est l’adresse de ce tampon qui est réellement renvoyée).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Capture la mémoire à l’adresse et la taille données sous forme d’objet bytes. Comme un objet bytes est immuable, la mémoire est en réalité dupliquée et copiée dans l’objet bytes ; ainsi, si le contenu de la mémoire change ultérieurement, l’objet créé conserve sa valeur d’origine.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Capture la mémoire à l’adresse et la taille données sous forme d’objet bytearray. Contrairement à la fonction bytes_at() ci-dessus, la mémoire est capturée par référence, de sorte qu’elle peut aussi bien être écrite, et vous accéderez à la valeur actuelle à l’adresse mémoire donnée.
Types entiers scalaires. Chacun occupe le nombre d’octets évident (1, 2, 4 ou 8) et est lu/écrit en utilisant le boutisme du type de disposition de la structure (l’un de NATIVE, LITTLE_ENDIAN ou BIG_ENDIAN).
- uctypes.FLOAT32: int¶
Nombre à virgule flottante IEEE 754 en simple précision (4 octets). Les lectures et les écritures sont converties depuis/vers un
floatPython.
- uctypes.FLOAT64: int¶
Nombre à virgule flottante IEEE 754 en double précision (8 octets). Les lectures et les écritures sont converties depuis/vers un
floatPython.
- uctypes.VOID: int¶
Alias de
UINT8. Fourni pour que les champs de type Cvoid *puissent être décrits de manière idiomatique sous la forme(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
Marque un champ de descripteur comme un pointeur vers un autre type. Un champ pointeur s’écrit sous la forme d’un tuple de deux éléments
(offset | PTR, target_type_or_descriptor). Le déréférencement du pointeur produit une vue typée sur l’adresse qu’il contient.
- uctypes.ARRAY: int¶
Marque un champ de descripteur comme un tableau de longueur fixe d’un autre type. Un champ tableau s’écrit soit
(offset | ARRAY, count | element_type)pour les tableaux de scalaires, soit(offset | ARRAY, count, element_descriptor)pour les tableaux de structures. Le nombre d’éléments est fixé au moment de la définition du descripteur.
Il n’existe pas de constante explicite pour les structures : un descripteur agrégé qui n’utilise ni PTR ni ARRAY est traité comme une structure.
Descripteurs de structure et instanciation d’objets structure¶
À partir d’un dictionnaire descripteur de structure et de son type de disposition, vous pouvez instancier une structure spécifique à une adresse mémoire donnée à l’aide du constructeur uctypes.struct(). L’adresse mémoire provient généralement des sources suivantes :
Une adresse prédéfinie, lors de l’accès aux registres matériels sur un système baremetal. Recherchez ces adresses dans la fiche technique du MCU/SoC concerné.
Une valeur de retour d’un appel à une fonction FFI (Foreign Function Interface).
Depuis
uctypes.addressof(), lorsque vous souhaitez transmettre des arguments à une fonction FFI, ou bien accéder à certaines données pour des entrées/sorties (par exemple, des données lues depuis un fichier ou un socket réseau).
Objets structure¶
Les objets structure permettent d’accéder aux champs individuels à l’aide de la notation par points standard : my_struct.substruct1.field1. Si un champ est de type scalaire, sa lecture produit une valeur primitive (un entier ou un flottant Python) correspondant à la valeur contenue dans le champ. Un champ scalaire peut également faire l’objet d’une affectation.
Si un champ est un tableau, ses éléments individuels peuvent être accédés avec l’opérateur d’indexation standard [] - aussi bien en lecture qu’en affectation.
Si un champ est un pointeur, il peut être déréférencé à l’aide de la syntaxe [0] (correspondant à l’opérateur * du C, bien que [0] fonctionne aussi en C). L’indexation d’un pointeur avec des valeurs entières autres que 0 est également prise en charge, avec la même sémantique qu’en C.
En résumé, l’accès aux champs d’une structure suit généralement la syntaxe C, à l’exception du déréférencement de pointeur, où vous devez utiliser l’opérateur [0] au lieu de *.
Limitations¶
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:
Évitez d’accéder aux structures imbriquées. Par exemple, au lieu de
mcu_registers.peripheral_a.register1, définissez des descripteurs de disposition distincts pour chaque périphérique, à accéder sous la formeperipheral_a.register1. Ou bien mettez simplement en cache un périphérique particulier :peripheral_a = mcu_registers.peripheral_a. Si un registre est composé de plusieurs champs de bits, vous devrez mettre en cache des références à un registre particulier :reg_a = mcu_registers.peripheral_a.reg_a.Évitez les autres données non scalaires, comme les tableaux. Par exemple, au lieu de
peripheral_a.register[0], utilisezperipheral_a.register0. Là encore, une alternative consiste à mettre en cache les valeurs intermédiaires, par exempleregister0 = 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).