uctypes — acesso a dados binários de forma estruturada¶
Este módulo implementa uma «interface de dados externos» para MicroPython. A ideia por detrás é semelhante ao módulo ctypes do CPython, mas a API real é diferente, simplificada e otimizada para tamanho reduzido. A ideia base do módulo é definir a disposição de estruturas de dados com uma capacidade semelhante à que a linguagem C permite, e depois aceder a ela usando a familiar notação de ponto para referenciar subcampos.
Aviso
O módulo uctypes permite o acesso a endereços de memória arbitrários da máquina (incluindo registos de I/O e de controlo). A sua utilização descuidada pode causar falhas, perda de dados e até mau funcionamento do hardware.
Veja também
- Módulo
struct O módulo Python padrão para empacotar e desempacotar dados binários. O
structopera sobre buffers inteiros de cada vez, usando uma cadeia de formato compacta (por exemplo,'<HBB4sI'), o que funciona bem para alguns campos fixos, mas escala mal para estruturas grandes ou profundamente aninhadas: cada leitura ou escrita volta a analisar a cadeia de formato, unions e bitfields não são suportados, e não existe forma de obter uma vista tipificada sobre um buffer existente. Ouctypescomplementa ostructao permitir descrever a disposição uma única vez, associá-la a uma região de memória (RAM, registos de periféricos, umbytearray) e depois aceder a campos individuais como atributos com nome – evitando análise e cópia repetidas, e acrescentando suporte para structs aninhados, arrays, unions e bitfields.
Exemplos de utilização:
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)
Definir a disposição da estrutura¶
A disposição da estrutura é definida por um «descritor» – um dicionário Python que codifica os nomes dos campos como chaves e outras propriedades necessárias para aceder a eles como valores associados:
{
"field1": <properties>,
"field2": <properties>,
...
}
Atualmente, o uctypes requer a especificação explícita de desvios para cada campo. Os desvios são indicados em bytes a partir do início da estrutura.
Seguem-se exemplos de codificação para vários tipos de campo:
Tipos escalares:
"field_name": offset | uctypes.UINT32ou seja, o valor é um identificador de tipo escalar combinado com OR com um desvio de campo (em bytes) a partir do início da estrutura.
Estruturas recursivas:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
ou seja, o valor é um par (2-tuple), cujo primeiro elemento é um desvio e o segundo é um dicionário descritor de estrutura (nota: os desvios em descritores recursivos são relativos à estrutura que definem). Claro que as estruturas recursivas podem ser especificadas não apenas por um dicionário literal, mas referenciando pelo nome um dicionário descritor de estrutura (definido anteriormente).
Arrays de tipos primitivos:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),ou seja, o valor é um par (2-tuple), cujo primeiro elemento é o sinalizador ARRAY combinado com OR com o desvio, e o segundo é o tipo de elemento escalar combinado com OR com o número de elementos no array.
Arrays de tipos agregados:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),ou seja, o valor é um triplo (3-tuple), cujo primeiro elemento é o sinalizador ARRAY combinado com OR com o desvio, o segundo é o número de elementos no array, e o terceiro é um descritor do tipo de elemento.
Ponteiro para um tipo primitivo:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),ou seja, o valor é um par (2-tuple), cujo primeiro elemento é o sinalizador PTR combinado com OR com o desvio, e o segundo é um tipo de elemento escalar.
Ponteiro para um tipo agregado:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),ou seja, o valor é um par (2-tuple), cujo primeiro elemento é o sinalizador PTR combinado com OR com o desvio, e o segundo é um descritor do tipo apontado.
Bitfields:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,ou seja, o valor é um tipo de valor escalar contendo o bitfield dado (os nomes de tipo são semelhantes aos tipos escalares, mas com o prefixo
BF), combinado com OR com o desvio do valor escalar que contém o bitfield, e ainda combinado com OR com os valores para a posição de bit e o comprimento de bit do bitfield dentro do valor escalar, deslocados por BF_POS e BF_LEN bits, respetivamente. A posição de um bitfield é contada a partir do bit menos significativo do escalar (com posição 0), e corresponde ao bit mais à direita de um campo (por outras palavras, é o número de bits que um escalar precisa de ser deslocado para a direita para extrair o bitfield).No exemplo acima, primeiro é extraído um valor UINT16 no desvio 0 (este detalhe pode ser importante ao aceder a registos de hardware, onde são necessários um tamanho de acesso e alinhamento específicos), e depois é extraído o bitfield cujo bit mais à direita é o bit lsbit deste UINT16, e cujo comprimento é bitsize bits. Por exemplo, se lsbit for 0 e bitsize for 8, então efetivamente será acedido o byte menos significativo do UINT16.
Note-se que as operações de bitfield são independentes da ordenação de bytes (endianness) do destino; em particular, o exemplo acima acede ao byte menos significativo do UINT16 tanto em estruturas little-endian como big-endian. Mas depende de o bit menos significativo ser numerado como 0. Alguns destinos podem usar uma numeração diferente no seu ABI nativo, mas o
uctypesusa sempre a numeração normalizada descrita acima.
Conteúdo do módulo¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Instancia um objeto «estrutura de dados externos» com base no endereço da estrutura em memória, no descritor (codificado como dicionário) e no tipo de disposição (ver abaixo).
- uctypes.LITTLE_ENDIAN: int¶
Tipo de disposição para uma estrutura compacta little-endian. (Compacta significa que cada campo ocupa exatamente o número de bytes definido no descritor, ou seja, o alinhamento é 1).
- uctypes.NATIVE: int¶
Tipo de disposição para uma estrutura nativa – com ordenação de bytes e alinhamento de dados em conformidade com o ABI do sistema no qual o MicroPython corre.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Retorna o tamanho da estrutura de dados em bytes. O argumento struct pode ser uma classe de estrutura ou um objeto de estrutura instanciado específico (ou o seu campo agregado).
- uctypes.addressof(obj: Any) int¶
Retorna o endereço de um objeto. O argumento deve ser bytes, bytearray ou outro objeto que suporte o protocolo de buffer (e o endereço deste buffer é o que é realmente retornado).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Captura a memória no endereço e tamanho indicados como objeto bytes. Como um objeto bytes é imutável, a memória é efetivamente duplicada e copiada para o objeto bytes; por isso, se o conteúdo da memória mudar depois, o objeto criado retém o valor original.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Captura a memória no endereço e tamanho indicados como objeto bytearray. Ao contrário da função bytes_at() acima, a memória é capturada por referência, pelo que pode ser tanto escrita como lida, e acederá ao valor atual no endereço de memória indicado.
Tipos inteiros escalares. Cada um ocupa o número óbvio de bytes (1, 2, 4 ou 8) e é lido/escrito usando a ordenação de bytes do tipo de disposição da estrutura (um de NATIVE, LITTLE_ENDIAN ou BIG_ENDIAN).
- uctypes.INT64: int¶
Inteiro com sinal de 64 bits. Intervalo
-0x8000000000000000–0x7FFFFFFFFFFFFFFF.
- uctypes.FLOAT32: int¶
Vírgula flutuante de precisão simples IEEE 754 (4 bytes). As leituras e escritas são convertidas de/para um
floatPython.
- uctypes.FLOAT64: int¶
Vírgula flutuante de precisão dupla IEEE 754 (8 bytes). As leituras e escritas são convertidas de/para um
floatPython.
- uctypes.VOID: int¶
Alias para
UINT8. Fornecido para que campos do tipo Cvoid *possam ser descritos idiomaticamente como(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
Marca um campo de descritor como ponteiro para outro tipo. Um campo de ponteiro é escrito como um par
(offset | PTR, target_type_or_descriptor). A desreferenciação do ponteiro produz uma vista tipificada no endereço que este contém.
- uctypes.ARRAY: int¶
Marca um campo de descritor como um array de comprimento fixo de outro tipo. Um campo de array é
(offset | ARRAY, count | element_type)para arrays de escalares ou(offset | ARRAY, count, element_descriptor)para arrays de estruturas. O número de elementos é fixado no momento da definição do descritor.
Não existe uma constante explícita para estruturas: um descritor agregado que não use nem PTR nem ARRAY é tratado como uma estrutura.
Descritores de estrutura e instanciação de objetos de estrutura¶
Dado um dicionário descritor de estrutura e o seu tipo de disposição, pode instanciar uma instância específica de estrutura num determinado endereço de memória usando o construtor uctypes.struct(). O endereço de memória normalmente provém das seguintes fontes:
Endereço predefinido, ao aceder a registos de hardware num sistema baremetal. Consulte esses endereços na ficha técnica do MCU/SoC específico.
Como valor de retorno de uma chamada a uma função FFI (Foreign Function Interface).
De
uctypes.addressof(), quando pretende passar argumentos para uma função FFI ou, alternativamente, aceder a alguns dados para I/O (por exemplo, dados lidos de um ficheiro ou socket de rede).
Objetos de estrutura¶
Os objetos de estrutura permitem aceder a campos individuais usando a notação de ponto padrão: my_struct.substruct1.field1. Se um campo for de tipo escalar, a sua leitura produz um valor primitivo (inteiro ou float Python) correspondente ao valor contido no campo. Um campo escalar também pode ser atribuído.
Se um campo for um array, os seus elementos individuais podem ser acedidos com o operador de subscrição padrão [] – tanto para leitura como para atribuição.
Se um campo for um ponteiro, pode ser desreferenciado usando a sintaxe [0] (correspondendo ao operador C *, embora [0] também funcione em C). Subscrever um ponteiro com outros valores inteiros além de 0 também é suportado, com a mesma semântica que em C.
Em resumo, o acesso a campos de estrutura segue geralmente a sintaxe C, exceto na desreferenciação de ponteiros, onde é necessário usar o operador [0] em vez de *.
Limitações¶
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:
Evite aceder a estruturas aninhadas. Por exemplo, em vez de
mcu_registers.peripheral_a.register1, defina descritores de disposição separados para cada periférico, a ser acedidos comoperipheral_a.register1. Ou simplesmente faça cache de um periférico específico:peripheral_a = mcu_registers.peripheral_a. Se um registo for composto por múltiplos bitfields, precisará de fazer cache de referências a um registo específico:reg_a = mcu_registers.peripheral_a.reg_a.Evite outros dados não escalares, como arrays. Por exemplo, em vez de
peripheral_a.register[0], useperipheral_a.register0. Mais uma vez, uma alternativa é fazer cache de valores intermédios, por exemploregister0 = 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).