uctypes — acesso a dados binários de forma estruturada

Este módulo implementa uma “interface de dados externos” (foreign data interface) para o MicroPython. A ideia por trás dele é semelhante à do módulo ctypes do CPython, mas a API em si é diferente, simplificada e otimizada para um tamanho reduzido. A ideia básica do módulo é definir o layout de uma estrutura de dados com aproximadamente o mesmo poder permitido pela linguagem C e, em seguida, acessá-la usando a familiar sintaxe 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 registradores de I/O e de controle). O uso descuidado dele pode levar a travamentos, perda de dados e até mesmo a falhas de hardware.

Ver também

Módulo struct

O módulo Python padrão para empacotar e desempacotar dados binários. O struct opera sobre buffers inteiros de uma só vez usando uma string de formato compacta (por exemplo, '<HBB4sI'), o que funciona bem para alguns poucos campos fixos, mas escala mal para estruturas grandes ou profundamente aninhadas: toda leitura ou escrita reanalisa a string de formato, uniões e bitfields não são suportados e não há como obter uma visão tipada de um buffer existente. O uctypes complementa o struct ao permitir que você descreva o layout uma única vez, o associe a uma região de memória (RAM, registradores de periféricos, um bytearray) e então acesse campos individuais como atributos nomeados – evitando análise e cópia repetidas, e adicionando suporte a structs aninhadas, arrays, uniões e bitfields.

Exemplos 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)

Definindo o layout da estrutura

O layout da estrutura é definido por um “descritor” - um dicionário Python que codifica os nomes dos campos como chaves e outras propriedades necessárias para acessá-los como os valores associados:

{
    "field1": <properties>,
    "field2": <properties>,
    ...
}

Atualmente, o uctypes exige a especificação explícita dos offsets de cada campo. Os offsets são dados em bytes a partir do início da estrutura.

A seguir estão exemplos de codificação para vários tipos de campo:

  • Tipos escalares:

    "field_name": offset | uctypes.UINT32
    

    em outras palavras, o valor é um identificador de tipo escalar combinado por OR com um offset de campo (em bytes) a partir do início da estrutura.

  • Estruturas recursivas:

    "sub": (offset, {
        "b0": 0 | uctypes.UINT8,
        "b1": 1 | uctypes.UINT8,
    })
    

    isto é, o valor é uma 2-tupla, cujo primeiro elemento é um offset e o segundo é um dicionário descritor de estrutura (nota: os offsets em descritores recursivos são relativos à estrutura que eles definem). É claro que 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),
    

    isto é, o valor é uma 2-tupla, cujo primeiro elemento é a flag ARRAY combinada por OR com o offset, e o segundo é o tipo escalar do elemento combinado por OR com o número de elementos no array.

  • Arrays de tipos agregados:

    "arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),
    

    isto é, o valor é uma 3-tupla, cujo primeiro elemento é a flag ARRAY combinada por OR com o offset, 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),
    

    isto é, o valor é uma 2-tupla, cujo primeiro elemento é a flag PTR combinada por OR com o offset, e o segundo é um tipo escalar de elemento.

  • Ponteiro para um tipo agregado:

    "ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),
    

    isto é, o valor é uma 2-tupla, cujo primeiro elemento é a flag PTR combinada por OR com o offset, e o segundo é um descritor do tipo apontado.

  • Bitfields:

    "bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,
    

    isto é, o valor é um tipo de valor escalar que contém o bitfield em questão (os nomes dos tipos são semelhantes aos dos tipos escalares, mas com o prefixo BF), combinado por OR com o offset do valor escalar que contém o bitfield, e ainda combinado por OR com os valores da posição do bit e do comprimento em bits do bitfield dentro do valor escalar, deslocados respectivamente por BF_POS e BF_LEN bits. A posição de um bitfield é contada a partir do bit menos significativo do escalar (que tem posição 0) e corresponde ao número do bit mais à direita de um campo (em outras palavras, é o número de bits que um escalar precisa ser deslocado para a direita para extrair o bitfield).

    No exemplo acima, primeiro um valor UINT16 será extraído no offset 0 (esse detalhe pode ser importante ao acessar registradores de hardware, onde um tamanho e alinhamento de acesso específicos são exigidos) e, em seguida, será extraído o bitfield cujo bit mais à direita é o bit lsbit desse UINT16 e cujo comprimento é de bitsize bits. Por exemplo, se lsbit for 0 e bitsize for 8, então efetivamente será acessado o byte menos significativo do UINT16.

    Observe que as operações de bitfield independem da endianness de byte do alvo; em particular, o exemplo acima acessará o byte menos significativo do UINT16 tanto em estruturas little-endian quanto big-endian. Mas isso depende de o bit menos significativo ser numerado como 0. Alguns alvos podem usar uma numeração diferente em sua ABI nativa, mas o uctypes sempre usa 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 de “estrutura de dados externos” (foreign data structure) com base no endereço da estrutura na memória, em um descritor (codificado como um dicionário) e em um tipo de layout (veja abaixo).

uctypes.LITTLE_ENDIAN: int

Tipo de layout para uma estrutura empacotada little-endian. (Empacotada significa que cada campo ocupa exatamente o número de bytes definido no descritor, ou seja, o alinhamento é 1).

uctypes.BIG_ENDIAN: int

Tipo de layout para uma estrutura empacotada big-endian.

uctypes.NATIVE: int

Tipo de layout para uma estrutura nativa - com a endianness e o alinhamento dos dados conformes à ABI do sistema no qual o MicroPython é executado.

uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int

Retorna o tamanho da estrutura de dados em bytes. O argumento struct pode ser tanto uma classe de estrutura quanto um objeto de estrutura específico já instanciado (ou um de seus campos agregados).

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 desse buffer é o que de fato é retornado).

uctypes.bytes_at(addr: int, size: int) bytes

Captura a memória no endereço e tamanho fornecidos como um objeto bytes. Como o objeto bytes é imutável, a memória é na verdade duplicada e copiada para o objeto bytes, de modo que, se o conteúdo da memória mudar depois, o objeto criado mantém o valor original.

uctypes.bytearray_at(addr: int, size: int) bytearray

Captura a memória no endereço e tamanho fornecidos como um objeto bytearray. Diferentemente da função bytes_at() acima, a memória é capturada por referência, de modo que ela pode tanto ser escrita quanto permite acessar o valor atual no endereço de memória fornecido.

Tipos inteiros escalares. Cada um ocupa o número óbvio de bytes (1, 2, 4 ou 8) e é lido/escrito usando a endianness do tipo de layout da estrutura (um de NATIVE, LITTLE_ENDIAN ou BIG_ENDIAN).

uctypes.UINT8: int

Inteiro de 8 bits sem sinal. Faixa 0255.

uctypes.INT8: int

Inteiro de 8 bits com sinal. Faixa -128127.

uctypes.UINT16: int

Inteiro de 16 bits sem sinal. Faixa 065535.

uctypes.INT16: int

Inteiro de 16 bits com sinal. Faixa -3276832767.

uctypes.UINT32: int

Inteiro de 32 bits sem sinal. Faixa 00xFFFFFFFF.

uctypes.INT32: int

Inteiro de 32 bits com sinal. Faixa -0x800000000x7FFFFFFF.

uctypes.UINT64: int

Inteiro de 64 bits sem sinal. Faixa 00xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Inteiro de 64 bits com sinal. Faixa -0x80000000000000000x7FFFFFFFFFFFFFFF.

uctypes.FLOAT32: int

Ponto flutuante de precisão simples IEEE 754 (4 bytes). Leituras e escritas são convertidas de/para um float do Python.

uctypes.FLOAT64: int

Ponto flutuante de precisão dupla IEEE 754 (8 bytes). Leituras e escritas são convertidas de/para um float do Python.

uctypes.VOID: int

Alias para UINT8. Fornecido para que campos void * no estilo C possam ser descritos de forma idiomática como (uctypes.PTR, uctypes.VOID).

uctypes.PTR: int

Marca um campo do descritor como um ponteiro para outro tipo. Um campo ponteiro é escrito como uma tupla de dois elementos (offset | PTR, target_type_or_descriptor). Desreferenciar o ponteiro produz uma visão tipada do endereço que ele contém.

uctypes.ARRAY: int

Marca um campo do descritor como um array de comprimento fixo de outro tipo. Um campo 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 há uma constante explícita para estruturas: um descritor agregado que não usa 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 seu tipo de layout, você pode instanciar uma instância de estrutura específica em um determinado endereço de memória usando o construtor uctypes.struct(). O endereço de memória normalmente vem das seguintes fontes:

  • Endereço predefinido, ao acessar registradores de hardware em um sistema baremetal. Procure esses endereços no datasheet de um MCU/SoC específico.

  • Como valor de retorno de uma chamada a alguma função FFI (Foreign Function Interface).

  • De uctypes.addressof(), quando você deseja passar argumentos para uma função FFI ou, alternativamente, acessar alguns dados para I/O (por exemplo, dados lidos de um arquivo ou de um socket de rede).

Objetos de estrutura

Os objetos de estrutura permitem acessar campos individuais usando a notação de ponto padrão: my_struct.substruct1.field1. Se um campo for de tipo escalar, obtê-lo produzirá um valor primitivo (inteiro ou float do Python) correspondente ao valor contido no campo. Um campo escalar também pode receber uma atribuição.

Se um campo for um array, seus elementos individuais podem ser acessados com o operador de subscrito padrão [] - tanto lidos quanto atribuídos.

Se um campo for um ponteiro, ele pode ser desreferenciado usando a sintaxe [0] (correspondente ao operador * do C, embora [0] também funcione em C). Subscrever um ponteiro com valores inteiros diferentes de 0 também é suportado, com a mesma semântica do C.

Resumindo, o acesso aos campos de uma estrutura geralmente segue a sintaxe do C, exceto pela desreferência de ponteiro, em que é preciso 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 acessar estruturas aninhadas. Por exemplo, em vez de mcu_registers.peripheral_a.register1, defina descritores de layout separados para cada periférico, para serem acessados como peripheral_a.register1. Ou simplesmente armazene em cache um periférico específico: peripheral_a = mcu_registers.peripheral_a. Se um registrador consistir em múltiplos bitfields, você precisaria armazenar em cache referências a um registrador 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] use peripheral_a.register0. Novamente, uma alternativa é armazenar em cache os valores intermediários, por exemplo 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).