uctypes — pääsy binääridataan rakenteisesti

Tämä moduuli toteuttaa MicroPythonille ”vieraan datan rajapinnan” (foreign data interface). Sen taustalla oleva ajatus on samankaltainen kuin CPythonin ctypes-moduuleissa, mutta varsinainen API on erilainen, virtaviivaistettu ja optimoitu pieneen kokoon. Moduulin perusajatuksena on määritellä datarakenteen asettelu suunnilleen samalla ilmaisuvoimalla kuin C-kieli sallii, ja sitten käyttää sitä tutulla pisteyntaksilla alikenttiin viittaamiseen.

Varoitus

uctypes-moduuli mahdollistaa pääsyn koneen mielivaltaisiin muistiosoitteisiin (mukaan lukien I/O- ja ohjausrekisterit). Sen huolimaton käyttö voi johtaa kaatumisiin, datan menetykseen ja jopa laitteiston toimintahäiriöihin.

Katso myös

Moduuli struct

Vakio-Python-moduuli binääridatan pakkaamiseen ja purkamiseen. struct käsittelee kokonaisia puskureita kerralla käyttäen tiivistä muotoilumerkkijonoa (esim. '<HBB4sI'), mikä toimii hyvin muutamalle kiinteälle kentälle mutta skaalautuu huonosti suuriin tai syvästi sisäkkäisiin rakenteisiin: jokainen luku tai kirjoitus jäsentää muotoilumerkkijonon uudelleen, unioneita ja bittikenttiä ei tueta, eikä olemassa olevaan puskuriin ole tapaa saada tyypitettyä näkymää. uctypes täydentää struct-moduulia antamalla sinun kuvata asettelu kerran, liittää se muistialueeseen (RAM, oheislaiterekisterit, bytearray) ja sitten käyttää yksittäisiä kenttiä nimettyinä attribuutteina – välttäen toistuvan jäsentämisen ja kopioinnin sekä lisäten tuen sisäkkäisille rakenteille, taulukoille, unioneille ja bittikentille.

Käyttöesimerkkejä:

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)

Rakenteen asettelun määrittely

Rakenteen asettelu määritellään ”kuvaajalla” (descriptor) - Python-sanakirjalla, joka koodaa kenttänimet avaimiksi ja muut niiden käyttöön tarvittavat ominaisuudet liittyviksi arvoiksi:

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

Tällä hetkellä uctypes edellyttää kunkin kentän siirtymän (offset) eksplisiittistä määrittelyä. Siirtymät annetaan tavuina rakenteen alusta.

Seuraavassa on koodausesimerkkejä erilaisille kenttätyypeille:

  • Skalaarityypit:

    "field_name": offset | uctypes.UINT32
    

    toisin sanoen arvo on skalaarityypin tunniste, joka on ORattu kentän siirtymän (tavuina) kanssa rakenteen alusta.

  • Rekursiiviset rakenteet:

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

    eli arvo on 2-monikko, jonka ensimmäinen alkio on siirtymä ja toinen on rakennekuvaajan sanakirja (huomaa: rekursiivisissa kuvaajissa siirtymät ovat suhteellisia sen rakenteen suhteen, jonka ne määrittelevät). Tietysti rekursiiviset rakenteet voidaan määritellä paitsi literaalina sanakirjana myös viittaamalla nimellä (aiemmin määriteltyyn) rakennekuvaajan sanakirjaan.

  • Primitiivisten tyyppien taulukot:

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

    eli arvo on 2-monikko, jonka ensimmäinen alkio on ARRAY-lippu ORattuna siirtymän kanssa, ja toinen on skalaarialkiotyyppi ORattuna taulukon alkioiden lukumäärän kanssa.

  • Koostetyyppien taulukot:

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

    eli arvo on 3-monikko, jonka ensimmäinen alkio on ARRAY-lippu ORattuna siirtymän kanssa, toinen on taulukon alkioiden lukumäärä ja kolmas on alkiotyypin kuvaaja.

  • Osoitin primitiiviseen tyyppiin:

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

    eli arvo on 2-monikko, jonka ensimmäinen alkio on PTR-lippu ORattuna siirtymän kanssa, ja toinen on skalaarialkiotyyppi.

  • Osoitin koostetyyppiin:

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

    eli arvo on 2-monikko, jonka ensimmäinen alkio on PTR-lippu ORattuna siirtymän kanssa, ja toinen on osoitetun tyypin kuvaaja.

  • Bittikentät:

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

    eli arvo on annetun bittikentän sisältävän skalaariarvon tyyppi (tyyppinimet ovat samankaltaisia kuin skalaarityypeissä, mutta etuliitteenä on BF), ORattuna bittikentän sisältävän skalaariarvon siirtymän kanssa, ja edelleen ORattuna bittikentän bittiposition ja bittipituuden arvojen kanssa skalaariarvon sisällä, siirrettynä vastaavasti BF_POS- ja BF_LEN-biteillä. Bittikentän positio lasketaan skalaarin vähiten merkitsevästä bitistä (jonka positio on 0) ja se on kentän oikeanpuoleisimman bitin numero (toisin sanoen se on bittien lukumäärä, jonka verran skalaaria täytyy siirtää oikealle bittikentän poimimiseksi).

    Yllä olevassa esimerkissä ensin poimitaan UINT16-arvo siirtymässä 0 (tämä yksityiskohta voi olla tärkeä laitteistorekistereitä käytettäessä, joissa vaaditaan tietty käyttökoko ja tasaus), ja sitten poimitaan bittikenttä, jonka oikeanpuoleisin bitti on tämän UINT16:n lsbit-bitti ja jonka pituus on bitsize bittiä. Esimerkiksi jos lsbit on 0 ja bitsize on 8, se käyttää tehokkaasti UINT16:n vähiten merkitsevää tavua.

    Huomaa, että bittikenttäoperaatiot ovat riippumattomia kohteen tavujärjestyksestä; erityisesti yllä oleva esimerkki käyttää UINT16:n vähiten merkitsevää tavua sekä little-endian- että big-endian-rakenteissa. Se kuitenkin olettaa, että vähiten merkitsevä bitti on numeroitu 0:ksi. Jotkin kohteet saattavat käyttää eri numerointia natiivissa ABI:ssaan, mutta uctypes käyttää aina edellä kuvattua normalisoitua numerointia.

Moduulin sisältö

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

Luo ”vieraan datarakenteen” objektin muistissa olevan rakenteen osoitteen, kuvaajan (sanakirjana koodattuna) ja asettelutyypin (katso alla) perusteella.

uctypes.LITTLE_ENDIAN: int

Asettelutyyppi little-endian-pakatulle rakenteelle. (Pakattu tarkoittaa, että jokainen kenttä vie täsmälleen niin monta tavua kuin kuvaajassa on määritelty, eli tasaus on 1).

uctypes.BIG_ENDIAN: int

Asettelutyyppi big-endian-pakatulle rakenteelle.

uctypes.NATIVE: int

Asettelutyyppi natiivirakenteelle - jonka tavujärjestys ja tasaus noudattavat sen järjestelmän ABI:a, jolla MicroPython on käynnissä.

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

Palauttaa datarakenteen koon tavuina. struct-argumentti voi olla joko rakenneluokka tai tietty instantioitu rakenneobjekti (tai sen koostekenttä).

uctypes.addressof(obj: Any) int

Palauttaa objektin osoitteen. Argumentin tulee olla bytes, bytearray tai muu puskuriprotokollaa tukeva objekti (ja juuri tämän puskurin osoite palautetaan).

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

Kaappaa annetussa osoitteessa ja koossa olevan muistin bytes-objektina. Koska bytes-objekti on muuttumaton, muisti itse asiassa monistetaan ja kopioidaan bytes-objektiin, joten jos muistin sisältö muuttuu myöhemmin, luotu objekti säilyttää alkuperäisen arvon.

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

Kaappaa annetussa osoitteessa ja koossa olevan muistin bytearray-objektina. Toisin kuin yllä oleva bytes_at()-funktio, muisti kaapataan viittauksena, joten siihen voidaan myös kirjoittaa, ja saat käyttöösi annetussa muistiosoitteessa olevan nykyisen arvon.

Skalaariset kokonaislukutyypit. Kukin vie ilmeisen tavumäärän (1, 2, 4 tai 8), ja se luetaan/kirjoitetaan rakenteen asettelutyypin tavujärjestyksen mukaisesti (yksi seuraavista: NATIVE, LITTLE_ENDIAN tai BIG_ENDIAN).

uctypes.UINT8: int

Etumerkitön 8-bittinen kokonaisluku. Alue 0255.

uctypes.INT8: int

Etumerkillinen 8-bittinen kokonaisluku. Alue -128127.

uctypes.UINT16: int

Etumerkitön 16-bittinen kokonaisluku. Alue 065535.

uctypes.INT16: int

Etumerkillinen 16-bittinen kokonaisluku. Alue -3276832767.

uctypes.UINT32: int

Etumerkitön 32-bittinen kokonaisluku. Alue 00xFFFFFFFF.

uctypes.INT32: int

Etumerkillinen 32-bittinen kokonaisluku. Alue -0x800000000x7FFFFFFF.

uctypes.UINT64: int

Etumerkitön 64-bittinen kokonaisluku. Alue 00xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Etumerkillinen 64-bittinen kokonaisluku. Alue -0x80000000000000000x7FFFFFFFFFFFFFFF.

uctypes.FLOAT32: int

IEEE 754 -yksinkertaisen tarkkuuden liukuluku (4 tavua). Luvut ja kirjoitukset muunnetaan Python-tyyppiin float ja siitä takaisin.

uctypes.FLOAT64: int

IEEE 754 -kaksinkertaisen tarkkuuden liukuluku (8 tavua). Luvut ja kirjoitukset muunnetaan Python-tyyppiin float ja siitä takaisin.

uctypes.VOID: int

Alias tyypille UINT8. Tarjolla, jotta C-tyyliset void * -kentät voidaan kuvata idiomaattisesti muodossa (uctypes.PTR, uctypes.VOID).

uctypes.PTR: int

Merkitsee kuvaajan kentän osoittimeksi toiseen tyyppiin. Osoitinkenttä kirjoitetaan kaksimonikkona (offset | PTR, target_type_or_descriptor). Osoittimen viittauksen purkaminen tuottaa tyypitetyn näkymän sen sisältämään osoitteeseen.

uctypes.ARRAY: int

Merkitsee kuvaajan kentän kiinteäpituiseksi taulukoksi toista tyyppiä. Taulukkokenttä on joko (offset | ARRAY, count | element_type) skalaaritaulukoille tai (offset | ARRAY, count, element_descriptor) rakennetaulukoille. Alkioiden lukumäärä on kiinteä kuvaajan määrittelyhetkellä.

Rakenteille ei ole eksplisiittistä vakiota: koostekuvaaja, joka ei käytä PTR- eikä ARRAY-vakiota, käsitellään rakenteena.

Rakennekuvaajat ja rakenneobjektien instantiointi

Kun sinulla on rakennekuvaajan sanakirja ja sen asettelutyyppi, voit instantioida tietyn rakenneinstanssin annettuun muistiosoitteeseen uctypes.struct() -konstruktorilla. Muistiosoite tulee yleensä seuraavista lähteistä:

  • Ennalta määritelty osoite, kun käytetään laitteistorekistereitä baremetal-järjestelmässä. Etsi nämä osoitteet tietyn MCU:n/SoC:n datalehdestä.

  • Paluuarvona kutsusta johonkin FFI- (Foreign Function Interface) funktioon.

  • Funktiosta uctypes.addressof(), kun haluat välittää argumentteja FFI-funktiolle, tai vaihtoehtoisesti käyttää jotain dataa I/O:ta varten (esimerkiksi tiedostosta tai verkkosocketista luettua dataa).

Rakenneobjektit

Rakenneobjektit mahdollistavat yksittäisten kenttien käytön tavanomaisella pistenotaatiolla: my_struct.substruct1.field1. Jos kenttä on skalaarityyppiä, sen lukeminen tuottaa kentän sisältämää arvoa vastaavan primitiivisen arvon (Python-kokonaisluvun tai -liukuluvun). Skalaarikenttään voidaan myös sijoittaa arvo.

Jos kenttä on taulukko, sen yksittäisiin alkioihin voidaan päästä tavanomaisella indeksointioperaattorilla [] - sekä lukemiseen että sijoittamiseen.

Jos kenttä on osoitin, sen viittaus voidaan purkaa [0]-syntaksilla (joka vastaa C-kielen *-operaattoria, vaikka [0] toimii myös C:ssä). Osoittimen indeksointi muilla kokonaislukuarvoilla kuin 0:lla on myös tuettu, samalla semantiikalla kuin C:ssä.

Yhteenvetona: rakennekenttien käyttö noudattaa yleisesti C-syntaksia, lukuun ottamatta osoittimen viittauksen purkamista, jolloin sinun täytyy käyttää [0]-operaattoria *:n sijaan.

Rajoitukset

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:

  • Vältä sisäkkäisten rakenteiden käyttöä. Esimerkiksi sen sijaan, että käyttäisit mcu_registers.peripheral_a.register1, määrittele erilliset asettelukuvaajat kullekin oheislaitteelle käytettäväksi muodossa peripheral_a.register1. Tai tallenna vain tietty oheislaite välimuistiin: peripheral_a = mcu_registers.peripheral_a. Jos rekisteri koostuu useista bittikentistä, sinun täytyisi tallentaa viittaukset tiettyyn rekisteriin välimuistiin: reg_a = mcu_registers.peripheral_a.reg_a.

  • Vältä muuta ei-skalaarista dataa, kuten taulukoita. Esimerkiksi sen sijaan, että käyttäisit peripheral_a.register[0], käytä peripheral_a.register0. Jälleen vaihtoehtona on tallentaa välimuotoiset arvot välimuistiin, esim. 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).