uctypes — accesso ai dati binari in modo strutturato

Questo modulo implementa la «foreign data interface» per MicroPython. L’idea alla base è simile ai moduli ctypes di CPython, ma l’API effettiva è diversa, semplificata e ottimizzata per ridurre le dimensioni. L’idea fondamentale del modulo è definire il layout di una struttura dati con all’incirca la stessa potenza espressiva del linguaggio C, per poi accedervi tramite la familiare sintassi con il punto per referenziare i sotto-campi.

Avvertimento

Il modulo uctypes consente l’accesso a indirizzi di memoria arbitrari della macchina (inclusi i registri di I/O e di controllo). Un uso incauto può portare a crash, perdita di dati e persino malfunzionamenti dell’hardware.

Vedi anche

Modulo struct

Il modulo standard di Python per impacchettare e spacchettare dati binari. struct opera su interi buffer alla volta usando una stringa di formato compatta (ad esempio '<HBB4sI'), che funziona bene per pochi campi fissi ma si adatta male a strutture grandi o profondamente annidate: ogni lettura o scrittura ri-analizza la stringa di formato, le union e i bitfield non sono supportati e non c’è modo di ottenere una vista tipizzata su un buffer esistente. uctypes completa struct permettendoti di descrivere il layout una sola volta, associarlo a una regione di memoria (RAM, registri di periferica, un bytearray) e poi accedere ai singoli campi come attributi con nome, evitando analisi e copie ripetute e aggiungendo il supporto per struct annidate, array, union e bitfield.

Esempi di utilizzo:

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)

Definizione del layout di una struttura

Il layout di una struttura è definito da un «descrittore», un dizionario Python che codifica i nomi dei campi come chiavi e le altre proprietà necessarie per accedervi come valori associati:

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

Attualmente uctypes richiede la specifica esplicita degli offset per ogni campo. Gli offset sono espressi in byte a partire dall’inizio della struttura.

Di seguito alcuni esempi di codifica per vari tipi di campo:

  • Tipi scalari:

    "field_name": offset | uctypes.UINT32
    

    in altre parole, il valore è un identificatore di tipo scalare combinato tramite OR con un offset di campo (in byte) a partire dall’inizio della struttura.

  • Strutture ricorsive:

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

    ovvero il valore è una 2-tupla, il cui primo elemento è un offset e il secondo è un dizionario descrittore di struttura (nota: gli offset nei descrittori ricorsivi sono relativi alla struttura che definiscono). Naturalmente, le strutture ricorsive possono essere specificate non solo tramite un dizionario letterale, ma anche facendo riferimento per nome a un dizionario descrittore di struttura (definito in precedenza).

  • Array di tipi primitivi:

    "arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),
    

    ovvero il valore è una 2-tupla, il cui primo elemento è il flag ARRAY combinato tramite OR con un offset, e il secondo è il tipo scalare degli elementi combinato tramite OR con il numero di elementi dell’array.

  • Array di tipi aggregati:

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

    ovvero il valore è una 3-tupla, il cui primo elemento è il flag ARRAY combinato tramite OR con un offset, il secondo è il numero di elementi dell’array e il terzo è un descrittore del tipo degli elementi.

  • Puntatore a un tipo primitivo:

    "ptr": (offset | uctypes.PTR, uctypes.UINT8),
    

    ovvero il valore è una 2-tupla, il cui primo elemento è il flag PTR combinato tramite OR con un offset, e il secondo è un tipo scalare degli elementi.

  • Puntatore a un tipo aggregato:

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

    ovvero il valore è una 2-tupla, il cui primo elemento è il flag PTR combinato tramite OR con un offset, e il secondo è un descrittore del tipo puntato.

  • Bitfield:

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

    ovvero il valore è un tipo di valore scalare che contiene il bitfield dato (i nomi dei tipi sono simili ai tipi scalari, ma con prefisso BF), combinato tramite OR con l’offset del valore scalare che contiene il bitfield, e ulteriormente combinato tramite OR con i valori della posizione e della lunghezza in bit del bitfield all’interno del valore scalare, spostati rispettivamente di BF_POS e BF_LEN bit. La posizione di un bitfield è contata a partire dal bit meno significativo dello scalare (che ha posizione 0) ed è il numero del bit più a destra di un campo (in altre parole, è il numero di bit di cui uno scalare deve essere spostato a destra per estrarre il bitfield).

    Nell’esempio sopra, prima viene estratto un valore UINT16 all’offset 0 (questo dettaglio può essere importante quando si accede ai registri hardware, dove sono richiesti dimensioni e allineamento di accesso specifici), e poi viene estratto il bitfield il cui bit più a destra è il bit lsbit di questo UINT16 e la cui lunghezza è di bitsize bit. Ad esempio, se lsbit è 0 e bitsize è 8, in pratica si accederà al byte meno significativo dell’UINT16.

    Si noti che le operazioni sui bitfield sono indipendenti dall’endianness del target; in particolare, l’esempio sopra accederà al byte meno significativo dell’UINT16 sia in strutture little-endian che big-endian. Dipende però dal fatto che il bit meno significativo sia numerato come 0. Alcuni target possono usare una numerazione diversa nel proprio ABI nativo, ma uctypes usa sempre la numerazione normalizzata descritta sopra.

Contenuti del modulo

class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)

Istanzia un oggetto «foreign data structure» basato sull’indirizzo della struttura in memoria, sul descrittore (codificato come dizionario) e sul tipo di layout (vedi sotto).

uctypes.LITTLE_ENDIAN: int

Tipo di layout per una struttura impacchettata little-endian. (Impacchettata significa che ogni campo occupa esattamente il numero di byte definito nel descrittore, ovvero l’allineamento è 1).

uctypes.BIG_ENDIAN: int

Tipo di layout per una struttura impacchettata big-endian.

uctypes.NATIVE: int

Tipo di layout per una struttura nativa, con endianness e allineamento dei dati conformi all’ABI del sistema su cui MicroPython è in esecuzione.

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

Restituisce la dimensione della struttura dati in byte. L’argomento struct può essere sia una classe di struttura sia uno specifico oggetto struttura istanziato (o un suo campo aggregato).

uctypes.addressof(obj: Any) int

Restituisce l’indirizzo di un oggetto. L’argomento deve essere bytes, bytearray o un altro oggetto che supporta il protocollo buffer (e ciò che viene effettivamente restituito è l’indirizzo di questo buffer).

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

Cattura la memoria all’indirizzo e con la dimensione dati come oggetto bytes. Poiché l’oggetto bytes è immutabile, la memoria viene effettivamente duplicata e copiata nell’oggetto bytes, quindi se il contenuto della memoria cambia in seguito, l’oggetto creato mantiene il valore originale.

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

Cattura la memoria all’indirizzo e con la dimensione dati come oggetto bytearray. A differenza della funzione bytes_at() sopra, la memoria viene catturata per riferimento, quindi può sia essere scritta sia consentirti di accedere al valore corrente all’indirizzo di memoria dato.

Tipi interi scalari. Ciascuno occupa l’ovvio numero di byte (1, 2, 4 o 8) e viene letto/scritto usando l’endianness del tipo di layout della struttura (uno tra NATIVE, LITTLE_ENDIAN o BIG_ENDIAN).

uctypes.UINT8: int

Intero a 8 bit senza segno. Intervallo 0255.

uctypes.INT8: int

Intero a 8 bit con segno. Intervallo -128127.

uctypes.UINT16: int

Intero a 16 bit senza segno. Intervallo 065535.

uctypes.INT16: int

Intero a 16 bit con segno. Intervallo -3276832767.

uctypes.UINT32: int

Intero a 32 bit senza segno. Intervallo 00xFFFFFFFF.

uctypes.INT32: int

Intero a 32 bit con segno. Intervallo -0x800000000x7FFFFFFF.

uctypes.UINT64: int

Intero a 64 bit senza segno. Intervallo 00xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Intero a 64 bit con segno. Intervallo -0x80000000000000000x7FFFFFFFFFFFFFFF.

uctypes.FLOAT32: int

Virgola mobile a precisione singola IEEE 754 (4 byte). Le letture e le scritture vengono convertite da/verso un float Python.

uctypes.FLOAT64: int

Virgola mobile a precisione doppia IEEE 754 (8 byte). Le letture e le scritture vengono convertite da/verso un float Python.

uctypes.VOID: int

Alias di UINT8. Fornito affinché i campi C-style void * possano essere descritti in modo idiomatico come (uctypes.PTR, uctypes.VOID).

uctypes.PTR: int

Contrassegna un campo del descrittore come puntatore a un altro tipo. Un campo puntatore è scritto come una tupla a due elementi (offset | PTR, target_type_or_descriptor). Dereferenziare il puntatore produce una vista tipizzata sull’indirizzo che esso contiene.

uctypes.ARRAY: int

Contrassegna un campo del descrittore come array a lunghezza fissa di un altro tipo. Un campo array è (offset | ARRAY, count | element_type) per gli array di scalari oppure (offset | ARRAY, count, element_descriptor) per gli array di strutture. Il numero di elementi è fissato al momento della definizione del descrittore.

Non esiste una costante esplicita per le strutture: un descrittore aggregato che non usa né PTRARRAY viene trattato come una struttura.

Descrittori di struttura e istanziazione di oggetti struttura

Dato un dizionario descrittore di struttura e il suo tipo di layout, è possibile istanziare una specifica istanza di struttura a un dato indirizzo di memoria usando il costruttore uctypes.struct(). L’indirizzo di memoria proviene di solito dalle seguenti fonti:

  • Un indirizzo predefinito, quando si accede ai registri hardware su un sistema baremetal. Cerca questi indirizzi nel datasheet di un particolare MCU/SoC.

  • Come valore di ritorno di una chiamata a una funzione FFI (Foreign Function Interface).

  • Da uctypes.addressof(), quando vuoi passare argomenti a una funzione FFI, oppure in alternativa accedere a dati per I/O (ad esempio dati letti da un file o da un socket di rete).

Oggetti struttura

Gli oggetti struttura consentono di accedere ai singoli campi usando la notazione standard con il punto: my_struct.substruct1.field1. Se un campo è di tipo scalare, la sua lettura produrrà un valore primitivo (un intero o un float Python) corrispondente al valore contenuto nel campo. Un campo scalare può anche essere assegnato.

Se un campo è un array, ai suoi singoli elementi si può accedere con l’operatore di indicizzazione standard [], sia in lettura che in assegnazione.

Se un campo è un puntatore, può essere dereferenziato usando la sintassi [0] (corrispondente all’operatore C *, anche se [0] funziona pure in C). È supportata anche l’indicizzazione di un puntatore con valori interi diversi da 0, con la stessa semantica del C.

Riassumendo, l’accesso ai campi di una struttura segue in generale la sintassi del C, fatta eccezione per la dereferenziazione dei puntatori, dove è necessario usare l’operatore [0] invece di *.

Limitazioni

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 di accedere a strutture annidate. Ad esempio, invece di mcu_registers.peripheral_a.register1, definisci descrittori di layout separati per ciascuna periferica, da accedere come peripheral_a.register1. Oppure metti semplicemente in cache una particolare periferica: peripheral_a = mcu_registers.peripheral_a. Se un registro è composto da più bitfield, dovrai mettere in cache i riferimenti a un particolare registro: reg_a = mcu_registers.peripheral_a.reg_a.

  • Evita altri dati non scalari, come gli array. Ad esempio, invece di peripheral_a.register[0] usa peripheral_a.register0. Anche in questo caso, un’alternativa è mettere in cache i valori intermedi, ad esempio 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).