uctypes — pristup binarnim podacima na strukturiran način

Ovaj modul implementira „sučelje za strane podatke” (foreign data interface) za MicroPython. Ideja koja stoji iza njega slična je CPythonovom modulu ctypes, no stvarni API je drugačiji, pojednostavljen i optimiziran za malu veličinu. Osnovna ideja modula jest definirati raspored podatkovne strukture s otprilike jednakom izražajnošću koju omogućuje jezik C, a zatim mu pristupati koristeći poznatu sintaksu s točkom za referenciranje podpolja.

Upozorenje

Modul uctypes omogućuje pristup proizvoljnim memorijskim adresama stroja (uključujući I/O i upravljačke registre). Neoprezno korištenje može dovesti do rušenja, gubitka podataka, pa čak i kvara hardvera.

Više informacija

Modul struct

Standardni Python modul za pakiranje i raspakiravanje binarnih podataka. struct radi nad cijelim međuspremnicima odjednom koristeći kompaktni format-niz (npr. '<HBB4sI'), što dobro funkcionira za nekoliko fiksnih polja, ali se loše skalira na velike ili duboko ugniježđene strukture: svako čitanje ili pisanje iznova parsira format-niz, unije i bitovna polja nisu podržani, a ne postoji način za dobivanje tipiziranog pogleda u postojeći međuspremnik. uctypes nadopunjuje struct tako da raspored opišete jednom, povežete ga s memorijskim područjem (RAM, registri periferije, bytearray) i zatim pristupate pojedinačnim poljima kao imenovanim atributima – izbjegavajući ponovljeno parsiranje i kopiranje te dodajući podršku za ugniježđene strukture, polja, unije i bitovna polja.

Primjeri korištenja:

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)

Definiranje rasporeda strukture

Raspored strukture definira se „deskriptorom” - Python rječnikom koji kodira imena polja kao ključeve i ostala svojstva potrebna za pristup njima kao pridružene vrijednosti:

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

Trenutno uctypes zahtijeva eksplicitno navođenje pomaka za svako polje. Pomaci se zadaju u bajtovima od početka strukture.

Slijede primjeri kodiranja za razne tipove polja:

  • Skalarni tipovi:

    "field_name": offset | uctypes.UINT32
    

    drugim riječima, vrijednost je identifikator skalarnog tipa OR-an s pomakom polja (u bajtovima) od početka strukture.

  • Rekurzivne strukture:

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

    tj. vrijednost je 2-torka, čiji je prvi element pomak, a drugi je rječnik deskriptora strukture (napomena: pomaci u rekurzivnim deskriptorima relativni su u odnosu na strukturu koju definiraju). Naravno, rekurzivne strukture mogu se specificirati ne samo doslovnim rječnikom, već i referenciranjem rječnika deskriptora strukture (definiranog ranije) po imenu.

  • Polja primitivnih tipova:

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

    tj. vrijednost je 2-torka, čiji je prvi element zastavica ARRAY OR-ana s pomakom, a drugi je skalarni tip elementa OR-an s brojem elemenata u polju.

  • Polja agregatnih tipova:

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

    tj. vrijednost je 3-torka, čiji je prvi element zastavica ARRAY OR-ana s pomakom, drugi je broj elemenata u polju, a treći je deskriptor tipa elementa.

  • Pokazivač na primitivni tip:

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

    tj. vrijednost je 2-torka, čiji je prvi element zastavica PTR OR-ana s pomakom, a drugi je skalarni tip elementa.

  • Pokazivač na agregatni tip:

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

    tj. vrijednost je 2-torka, čiji je prvi element zastavica PTR OR-ana s pomakom, a drugi je deskriptor tipa na koji se pokazuje.

  • Bitovna polja:

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

    tj. vrijednost je tip skalarne vrijednosti koja sadrži zadano bitovno polje (imena tipova slična su skalarnim tipovima, ali s prefiksom BF), OR-an s pomakom za skalarnu vrijednost koja sadrži bitovno polje, te dodatno OR-an s vrijednostima za bitovni položaj i bitovnu duljinu bitovnog polja unutar skalarne vrijednosti, pomaknutima za BF_POS odnosno BF_LEN bitova. Položaj bitovnog polja broji se od najmanje značajnog bita skalara (koji ima položaj 0), i predstavlja broj krajnje desnog bita polja (drugim riječima, to je broj bitova za koji skalar treba pomaknuti udesno kako bi se izvuklo bitovno polje).

    U gornjem primjeru, prvo će se izvući vrijednost UINT16 na pomaku 0 (ovaj detalj može biti važan pri pristupu hardverskim registrima, gdje su potrebne određena veličina pristupa i poravnanje), a zatim će se izvući bitovno polje čiji je krajnje desni bit lsbit bit ovog UINT16, a duljina je bitsize bitova. Na primjer, ako je lsbit jednak 0, a bitsize jednak 8, tada će efektivno pristupiti najmanje značajnom bajtu UINT16.

    Imajte na umu da su operacije nad bitovnim poljima neovisne o redoslijedu bajtova (endianness) cilja, posebice, gornji primjer pristupit će najmanje značajnom bajtu UINT16 i u little-endian i u big-endian strukturama. No to ovisi o tome da je najmanje značajni bit označen brojem 0. Neki ciljevi mogu koristiti drugačije numeriranje u svom izvornom ABI-ju, ali uctypes uvijek koristi normalizirano numeriranje opisano gore.

Sadržaj modula

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

Instancira objekt „strane podatkovne strukture” na temelju adrese strukture u memoriji, deskriptora (kodiranog kao rječnik) i tipa rasporeda (vidi dolje).

uctypes.LITTLE_ENDIAN: int

Tip rasporeda za little-endian zbijenu strukturu. (Zbijeno znači da svako polje zauzima točno onoliko bajtova koliko je definirano u deskriptoru, tj. poravnanje je 1).

uctypes.BIG_ENDIAN: int

Tip rasporeda za big-endian zbijenu strukturu.

uctypes.NATIVE: int

Tip rasporeda za izvornu (native) strukturu - s redoslijedom bajtova podataka i poravnanjem sukladnima ABI-ju sustava na kojem MicroPython radi.

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

Vraća veličinu podatkovne strukture u bajtovima. Argument struct može biti ili klasa strukture ili konkretno instancirani objekt strukture (ili njegovo agregatno polje).

uctypes.addressof(obj: Any) int

Vraća adresu objekta. Argument trebaju biti bytes, bytearray ili drugi objekt koji podržava protokol međuspremnika (a vraća se zapravo adresa tog međuspremnika).

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

Hvata memoriju na zadanoj adresi i veličini kao bytes objekt. Budući da je bytes objekt nepromjenjiv, memorija se zapravo duplicira i kopira u bytes objekt, pa ako se sadržaj memorije kasnije promijeni, stvoreni objekt zadržava izvornu vrijednost.

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

Hvata memoriju na zadanoj adresi i veličini kao bytearray objekt. Za razliku od gornje funkcije bytes_at(), memorija se hvata po referenci, pa se može i pisati u nju, a pristupat ćete trenutnoj vrijednosti na zadanoj memorijskoj adresi.

Skalarni cjelobrojni tipovi. Svaki zauzima očigledan broj bajtova (1, 2, 4 ili 8) i čita se/piše koristeći redoslijed bajtova (endianness) tipa rasporeda strukture (jedan od NATIVE, LITTLE_ENDIAN ili BIG_ENDIAN).

uctypes.UINT8: int

Neoznačeni 8-bitni cijeli broj. Raspon 0255.

uctypes.INT8: int

Označeni 8-bitni cijeli broj. Raspon -128127.

uctypes.UINT16: int

Neoznačeni 16-bitni cijeli broj. Raspon 065535.

uctypes.INT16: int

Označeni 16-bitni cijeli broj. Raspon -3276832767.

uctypes.UINT32: int

Neoznačeni 32-bitni cijeli broj. Raspon 00xFFFFFFFF.

uctypes.INT32: int

Označeni 32-bitni cijeli broj. Raspon -0x800000000x7FFFFFFF.

uctypes.UINT64: int

Neoznačeni 64-bitni cijeli broj. Raspon 00xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Označeni 64-bitni cijeli broj. Raspon -0x80000000000000000x7FFFFFFFFFFFFFFF.

uctypes.FLOAT32: int

IEEE 754 broj s pomičnim zarezom jednostruke preciznosti (4 bajta). Čitanja i pisanja pretvaraju se u/iz Python float.

uctypes.FLOAT64: int

IEEE 754 broj s pomičnim zarezom dvostruke preciznosti (8 bajtova). Čitanja i pisanja pretvaraju se u/iz Python float.

uctypes.VOID: int

Alias za UINT8. Naveden kako bi se C-stilska polja void * mogla idiomatski opisati kao (uctypes.PTR, uctypes.VOID).

uctypes.PTR: int

Označava polje deskriptora kao pokazivač na drugi tip. Polje pokazivača piše se kao dvije-torka (offset | PTR, target_type_or_descriptor). Dereferenciranje pokazivača daje tipizirani pogled u adresu koju sadrži.

uctypes.ARRAY: int

Označava polje deskriptora kao polje fiksne duljine drugog tipa. Polje (array) piše se ili kao (offset | ARRAY, count | element_type) za polja skalara ili kao (offset | ARRAY, count, element_descriptor) za polja struktura. Broj elemenata fiksiran je u trenutku definiranja deskriptora.

Ne postoji eksplicitna konstanta za strukture: agregatni deskriptor koji ne koristi ni PTR ni ARRAY tretira se kao struktura.

Deskriptori struktura i instanciranje objekata strukture

Uz zadani rječnik deskriptora strukture i njegov tip rasporeda, možete instancirati konkretnu instancu strukture na zadanoj memorijskoj adresi koristeći konstruktor uctypes.struct(). Memorijska adresa obično dolazi iz sljedećih izvora:

  • Predefinirana adresa, pri pristupu hardverskim registrima na sustavu bez operacijskog sustava (baremetal). Te adrese potražite u dokumentaciji (datasheet) za pojedini MCU/SoC.

  • Kao povratna vrijednost poziva neke FFI (Foreign Function Interface) funkcije.

  • Iz uctypes.addressof(), kada želite proslijediti argumente FFI funkciji, ili alternativno, pristupiti nekim podacima za I/O (na primjer, podacima pročitanim iz datoteke ili mrežnog socketa).

Objekti strukture

Objekti strukture omogućuju pristup pojedinačnim poljima koristeći standardnu notaciju s točkom: my_struct.substruct1.field1. Ako je polje skalarnog tipa, njegovo dohvaćanje proizvest će primitivnu vrijednost (Python cijeli broj ili float) koja odgovara vrijednosti sadržanoj u polju. Skalarnom polju može se i pridruživati vrijednost.

Ako je polje polje (array), njegovim pojedinačnim elementima može se pristupati standardnim operatorom indeksiranja [] - i za čitanje i za pridruživanje.

Ako je polje pokazivač, može se dereferencirati koristeći sintaksu [0] (što odgovara C operatoru *, iako [0] radi i u C-u). Indeksiranje pokazivača cjelobrojnim vrijednostima različitima od 0 također je podržano, s istom semantikom kao u C-u.

Ukratko, pristup poljima strukture općenito slijedi C sintaksu, osim za dereferenciranje pokazivača, kada umjesto operatora * trebate koristiti operator [0].

Ograničenja

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:

  • Izbjegavajte pristup ugniježđenim strukturama. Na primjer, umjesto mcu_registers.peripheral_a.register1, definirajte zasebne deskriptore rasporeda za svaku periferiju, kojima ćete pristupati kao peripheral_a.register1. Ili jednostavno predmemorirajte određenu periferiju: peripheral_a = mcu_registers.peripheral_a. Ako se registar sastoji od više bitovnih polja, trebali biste predmemorirati reference na pojedini registar: reg_a = mcu_registers.peripheral_a.reg_a.

  • Izbjegavajte ostale neskalarne podatke, poput polja. Na primjer, umjesto peripheral_a.register[0] koristite peripheral_a.register0. Ponovno, alternativa je predmemoriranje međuvrijednosti, npr. 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).