uctypes — acceso a datos binarios de forma estructurada¶
Este módulo implementa la «interfaz de datos externos» para MicroPython. La idea que hay detrás es similar a la de los módulos ctypes de CPython, pero la API real es distinta, simplificada y optimizada para ocupar poco espacio. La idea básica del módulo es definir la disposición de una estructura de datos con aproximadamente la misma potencia que permite el lenguaje C, y luego acceder a ella mediante la familiar sintaxis de punto para referenciar subcampos.
Advertencia
El módulo uctypes permite acceder a direcciones de memoria arbitrarias de la máquina (incluidos los registros de E/S y de control). Un uso descuidado puede provocar fallos, pérdida de datos e incluso un mal funcionamiento del hardware.
Ver también
- Módulo
struct El módulo estándar de Python para empaquetar y desempaquetar datos binarios.
structopera sobre búferes completos a la vez utilizando una cadena de formato compacta (por ejemplo,'<HBB4sI'), lo que funciona bien para unos pocos campos fijos pero escala mal con estructuras grandes o profundamente anidadas: cada lectura o escritura vuelve a analizar la cadena de formato, no se admiten uniones ni campos de bits, y no hay manera de obtener una vista tipada sobre un búfer existente.uctypescomplementa astructpermitiéndote describir la disposición una sola vez, asociarla a una región de memoria (RAM, registros de periféricos, unbytearray) y luego acceder a campos individuales como atributos con nombre, evitando análisis y copias repetidos, y añadiendo compatibilidad con estructuras anidadas, arrays, uniones y campos de bits.
Ejemplos de uso:
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)
Definición de la disposición de una estructura¶
La disposición de una estructura se define mediante un «descriptor»: un diccionario de Python que codifica los nombres de los campos como claves y otras propiedades necesarias para acceder a ellos como los valores asociados:
{
"field1": <properties>,
"field2": <properties>,
...
}
Actualmente, uctypes requiere la especificación explícita de los desplazamientos (offsets) de cada campo. Los desplazamientos se indican en bytes desde el inicio de la estructura.
A continuación se muestran ejemplos de codificación para varios tipos de campos:
Tipos escalares:
"field_name": offset | uctypes.UINT32en otras palabras, el valor es un identificador de tipo escalar combinado mediante OR con un desplazamiento de campo (en bytes) desde el inicio de la estructura.
Estructuras recursivas:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
es decir, el valor es una tupla de 2 elementos, cuyo primer elemento es un desplazamiento y el segundo es un diccionario descriptor de estructura (nota: los desplazamientos en los descriptores recursivos son relativos a la estructura que definen). Por supuesto, las estructuras recursivas se pueden especificar no solo mediante un diccionario literal, sino también refiriéndose por nombre a un diccionario descriptor de estructura (definido previamente).
Arrays de tipos primitivos:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),es decir, el valor es una tupla de 2 elementos, cuyo primer elemento es el indicador ARRAY combinado mediante OR con el desplazamiento, y el segundo es el tipo de elemento escalar combinado mediante OR con el número de elementos del array.
Arrays de tipos agregados:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),es decir, el valor es una tupla de 3 elementos, cuyo primer elemento es el indicador ARRAY combinado mediante OR con el desplazamiento, el segundo es el número de elementos del array y el tercero es un descriptor del tipo de elemento.
Puntero a un tipo primitivo:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),es decir, el valor es una tupla de 2 elementos, cuyo primer elemento es el indicador PTR combinado mediante OR con el desplazamiento, y el segundo es un tipo de elemento escalar.
Puntero a un tipo agregado:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),es decir, el valor es una tupla de 2 elementos, cuyo primer elemento es el indicador PTR combinado mediante OR con el desplazamiento, y el segundo es un descriptor del tipo al que apunta.
Campos de bits:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,es decir, el valor es un tipo de valor escalar que contiene el campo de bits dado (los nombres de tipo son similares a los de los tipos escalares, pero con el prefijo
BF), combinado mediante OR con el desplazamiento del valor escalar que contiene el campo de bits, y combinado además mediante OR con los valores de la posición y la longitud en bits del campo de bits dentro del valor escalar, desplazados BF_POS y BF_LEN bits, respectivamente. La posición de un campo de bits se cuenta desde el bit menos significativo del escalar (que tiene la posición 0), y es el número del bit más a la derecha de un campo (en otras palabras, es el número de bits que se debe desplazar un escalar hacia la derecha para extraer el campo de bits).En el ejemplo anterior, primero se extraerá un valor UINT16 en el desplazamiento 0 (este detalle puede ser importante al acceder a registros de hardware, donde se requieren un tamaño de acceso y una alineación concretos), y luego se extraerá el campo de bits cuyo bit más a la derecha es el bit lsbit de este UINT16 y cuya longitud es de bitsize bits. Por ejemplo, si lsbit es 0 y bitsize es 8, entonces accederá efectivamente al byte menos significativo del UINT16.
Ten en cuenta que las operaciones con campos de bits son independientes de la endianness de bytes del destino; en particular, el ejemplo anterior accederá al byte menos significativo del UINT16 tanto en estructuras little-endian como big-endian. Pero depende de que el bit menos significativo esté numerado como 0. Algunos destinos pueden usar una numeración diferente en su ABI nativa, pero
uctypessiempre usa la numeración normalizada descrita anteriormente.
Contenido del módulo¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Instancia un objeto de «estructura de datos externa» basado en la dirección de la estructura en memoria, el descriptor (codificado como un diccionario) y el tipo de disposición (véase más abajo).
- uctypes.LITTLE_ENDIAN: int¶
Tipo de disposición para una estructura empaquetada little-endian. (Empaquetada significa que cada campo ocupa exactamente tantos bytes como se define en el descriptor, es decir, la alineación es 1).
- uctypes.NATIVE: int¶
Tipo de disposición para una estructura nativa, con la endianness y la alineación de los datos conformes a la ABI del sistema en el que se ejecuta MicroPython.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Devuelve el tamaño de la estructura de datos en bytes. El argumento struct puede ser tanto una clase de estructura como un objeto de estructura instanciado concreto (o uno de sus campos agregados).
- uctypes.addressof(obj: Any) int¶
Devuelve la dirección de un objeto. El argumento debe ser bytes, bytearray u otro objeto que admita el protocolo de búfer (y lo que en realidad se devuelve es la dirección de ese búfer).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Captura la memoria en la dirección y el tamaño dados como un objeto bytes. Como el objeto bytes es inmutable, la memoria se duplica y se copia en el objeto bytes, de modo que si el contenido de la memoria cambia más tarde, el objeto creado conserva el valor original.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Captura la memoria en la dirección y el tamaño dados como un objeto bytearray. A diferencia de la función bytes_at() anterior, la memoria se captura por referencia, por lo que puede escribirse en ella y accederás al valor actual de la dirección de memoria dada.
Tipos enteros escalares. Cada uno ocupa el número obvio de bytes (1, 2, 4 u 8) y se lee/escribe usando la endianness del tipo de disposición de la estructura (uno de NATIVE, LITTLE_ENDIAN o BIG_ENDIAN).
- uctypes.FLOAT32: int¶
Coma flotante IEEE 754 de precisión simple (4 bytes). Las lecturas y escrituras se convierten desde/hacia un
floatde Python.
- uctypes.FLOAT64: int¶
Coma flotante IEEE 754 de precisión doble (8 bytes). Las lecturas y escrituras se convierten desde/hacia un
floatde Python.
- uctypes.VOID: int¶
Alias de
UINT8. Se proporciona para que los camposvoid *de estilo C puedan describirse de forma idiomática como(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
Marca un campo del descriptor como un puntero a otro tipo. Un campo de puntero se escribe como una tupla de dos elementos
(offset | PTR, target_type_or_descriptor). Desreferenciar el puntero produce una vista tipada de la dirección que contiene.
- uctypes.ARRAY: int¶
Marca un campo del descriptor como un array de longitud fija de otro tipo. Un campo de array es
(offset | ARRAY, count | element_type)para arrays de escalares o(offset | ARRAY, count, element_descriptor)para arrays de estructuras. El número de elementos se fija en el momento de definir el descriptor.
No existe una constante explícita para las estructuras: un descriptor agregado que no usa ni PTR ni ARRAY se trata como una estructura.
Descriptores de estructura e instanciación de objetos de estructura¶
Dado un diccionario descriptor de estructura y su tipo de disposición, puedes instanciar una instancia de estructura concreta en una dirección de memoria dada usando el constructor uctypes.struct(). La dirección de memoria suele provenir de las siguientes fuentes:
Una dirección predefinida, al acceder a registros de hardware en un sistema baremetal. Busca estas direcciones en la hoja de datos del MCU/SoC concreto.
Como valor de retorno de una llamada a alguna función FFI (Foreign Function Interface).
De
uctypes.addressof(), cuando quieras pasar argumentos a una función FFI o, alternativamente, acceder a algunos datos para E/S (por ejemplo, datos leídos de un archivo o de un socket de red).
Objetos de estructura¶
Los objetos de estructura permiten acceder a campos individuales mediante la notación de punto estándar: my_struct.substruct1.field1. Si un campo es de tipo escalar, obtenerlo producirá un valor primitivo (un entero o un float de Python) correspondiente al valor contenido en el campo. A un campo escalar también se le puede asignar un valor.
Si un campo es un array, se puede acceder a sus elementos individuales con el operador de subíndice estándar [], tanto para leer como para asignar.
Si un campo es un puntero, se puede desreferenciar usando la sintaxis [0] (que corresponde al operador * de C, aunque [0] también funciona en C). También se admite subindexar un puntero con valores enteros distintos de 0, con la misma semántica que en C.
En resumen, el acceso a los campos de una estructura sigue en general la sintaxis de C, salvo en la desreferencia de punteros, donde necesitas usar el operador [0] en lugar de *.
Limitaciones¶
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:
Evita acceder a estructuras anidadas. Por ejemplo, en lugar de
mcu_registers.peripheral_a.register1, define descriptores de disposición separados para cada periférico, de modo que se acceda comoperipheral_a.register1. O simplemente cachea un periférico concreto:peripheral_a = mcu_registers.peripheral_a. Si un registro consta de varios campos de bits, necesitarías cachear referencias a un registro concreto:reg_a = mcu_registers.peripheral_a.reg_a.Evita otros datos no escalares, como los arrays. Por ejemplo, en lugar de
peripheral_a.register[0]usaperipheral_a.register0. De nuevo, una alternativa es cachear valores intermedios, por ejemploregister0 = 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).