uctypes — acces la date binare într-un mod structurat¶
Acest modul implementează o „interfață pentru date externe” pentru MicroPython. Ideea din spatele lui este similară cu cea a modulului ctypes din CPython, dar API-ul propriu-zis este diferit, simplificat și optimizat pentru dimensiuni reduse. Ideea de bază a modulului este de a defini structura de date cu aproximativ aceeași putere de exprimare pe care o permite limbajul C, și apoi de a o accesa folosind sintaxa familiară cu punct pentru a face referire la subcâmpuri.
Atenționare
Modulul uctypes permite accesul la adrese de memorie arbitrare ale mașinii (inclusiv registre de I/O și de control). Utilizarea sa neglijentă poate duce la blocaje, pierderi de date și chiar la funcționarea defectuoasă a hardware-ului.
Vezi și
- Modulul
struct Modulul standard Python pentru împachetarea și despachetarea datelor binare.
structoperează asupra unor tampoane (buffer) întregi deodată, folosind un șir de format compact (de exemplu'<HBB4sI'), care funcționează bine pentru câteva câmpuri fixe, dar se scalează prost la structuri mari sau profund imbricate: fiecare citire sau scriere reanalizează șirul de format, uniunile și câmpurile de biți nu sunt acceptate, și nu există nicio modalitate de a obține o vizualizare tipizată asupra unui tampon (buffer) existent.uctypescompleteazăstructpermițându-vă să descrieți structura o singură dată, să o atașați unei regiuni de memorie (RAM, registre de periferice, unbytearray) și apoi să accesați câmpuri individuale ca atribute denumite – evitând analizarea și copierea repetată și adăugând suport pentru structuri imbricate, tablouri, uniuni și câmpuri de biți.
Exemple de utilizare:
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)
Definirea structurii de date¶
Structura de date este definită de un „descriptor” - un dicționar Python care codifică numele câmpurilor drept chei și alte proprietăți necesare pentru accesarea lor drept valori asociate:
{
"field1": <properties>,
"field2": <properties>,
...
}
În prezent, uctypes necesită specificarea explicită a decalajelor (offset) pentru fiecare câmp. Decalajele sunt date în octeți de la începutul structurii.
Urmează exemple de codificare pentru diverse tipuri de câmpuri:
Tipuri scalare:
"field_name": offset | uctypes.UINT32cu alte cuvinte, valoarea este un identificator de tip scalar combinat prin OR cu un decalaj de câmp (offset, în octeți) de la începutul structurii.
Structuri recursive:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
adică valoarea este un tuplu cu 2 elemente, primul element fiind un decalaj (offset), iar al doilea un dicționar descriptor de structură (notă: decalajele din descriptorii recursivi sunt relative la structura pe care o definesc). Bineînțeles, structurile recursive pot fi specificate nu doar printr-un dicționar literal, ci și prin referirea după nume a unui dicționar descriptor de structură (definit anterior).
Tablouri de tipuri primitive:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),adică valoarea este un tuplu cu 2 elemente, primul element fiind indicatorul ARRAY combinat prin OR cu decalajul (offset), iar al doilea fiind tipul scalar al elementului combinat prin OR cu numărul de elemente din tablou.
Tablouri de tipuri agregate:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),adică valoarea este un tuplu cu 3 elemente, primul element fiind indicatorul ARRAY combinat prin OR cu decalajul (offset), al doilea fiind numărul de elemente din tablou, iar al treilea un descriptor al tipului elementelor.
Pointer către un tip primitiv:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),adică valoarea este un tuplu cu 2 elemente, primul element fiind indicatorul PTR combinat prin OR cu decalajul (offset), iar al doilea fiind un tip scalar al elementului.
Pointer către un tip agregat:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),adică valoarea este un tuplu cu 2 elemente, primul element fiind indicatorul PTR combinat prin OR cu decalajul (offset), iar al doilea un descriptor al tipului spre care se face referire.
Câmpuri de biți:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,adică valoarea este un tip de valoare scalară care conține câmpul de biți dat (numele de tip sunt similare cu cele ale tipurilor scalare, dar prefixate cu
BF), combinat prin OR cu decalajul (offset) pentru valoarea scalară care conține câmpul de biți, și combinat suplimentar prin OR cu valorile pentru poziția bitului și lungimea în biți a câmpului de biți din cadrul valorii scalare, deplasate cu BF_POS, respectiv BF_LEN biți. Poziția unui câmp de biți se numără de la cel mai puțin semnificativ bit al scalarului (având poziția 0) și reprezintă numărul bitului cel mai din dreapta al unui câmp (cu alte cuvinte, este numărul de biți cu care trebuie deplasat la dreapta un scalar pentru a extrage câmpul de biți).În exemplul de mai sus, mai întâi se va extrage o valoare UINT16 la decalajul (offset) 0 (acest detaliu poate fi important la accesarea registrelor hardware, unde sunt necesare o anumită dimensiune de acces și aliniere), și apoi se va extrage câmpul de biți al cărui bit cel mai din dreapta este bitul lsbit al acestui UINT16 și a cărui lungime este de bitsize biți. De exemplu, dacă lsbit este 0 și bitsize este 8, atunci practic se va accesa octetul cel mai puțin semnificativ al UINT16.
Rețineți că operațiile pe câmpuri de biți sunt independente de ordinea octeților (endianness) a țintei; în particular, exemplul de mai sus va accesa octetul cel mai puțin semnificativ al UINT16 atât în structuri little-endian, cât și big-endian. Însă depinde de faptul că bitul cel mai puțin semnificativ este numerotat cu 0. Unele ținte pot folosi o numerotare diferită în ABI-ul lor nativ, dar
uctypesfolosește întotdeauna numerotarea normalizată descrisă mai sus.
Conținutul modulului¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Instanțiază un obiect „structură de date externă” pe baza adresei structurii din memorie, a descriptorului (codificat ca dicționar) și a tipului de aranjament (a se vedea mai jos).
- uctypes.LITTLE_ENDIAN: int¶
Tip de aranjament pentru o structură împachetată little-endian. (Împachetată înseamnă că fiecare câmp ocupă exact atâția octeți câți sunt definiți în descriptor, adică alinierea este 1).
- uctypes.NATIVE: int¶
Tip de aranjament pentru o structură nativă - cu ordinea octeților și alinierea datelor conforme cu ABI-ul sistemului pe care rulează MicroPython.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Returnează dimensiunea structurii de date în octeți. Argumentul struct poate fi fie o clasă de structură, fie un anumit obiect structură instanțiat (sau un câmp agregat al acestuia).
- uctypes.addressof(obj: Any) int¶
Returnează adresa unui obiect. Argumentul ar trebui să fie bytes, bytearray sau alt obiect care acceptă protocolul de tampon (buffer) (și adresa acestui tampon este ceea ce se returnează de fapt).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Capturează memoria de la adresa și dimensiunea date ca obiect bytes. Deoarece obiectul bytes este imutabil, memoria este de fapt duplicată și copiată în obiectul bytes, astfel încât, dacă conținutul memoriei se modifică ulterior, obiectul creat își păstrează valoarea originală.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Capturează memoria de la adresa și dimensiunea date ca obiect bytearray. Spre deosebire de funcția bytes_at() de mai sus, memoria este capturată prin referință, astfel încât poate fi și scrisă și veți accesa valoarea curentă de la adresa de memorie dată.
Tipuri întregi scalare. Fiecare ocupă numărul evident de octeți (1, 2, 4 sau 8) și este citit/scris folosind ordinea octeților (endianness) a tipului de aranjament al structurii (unul dintre NATIVE, LITTLE_ENDIAN sau BIG_ENDIAN).
- uctypes.INT64: int¶
Întreg cu semn pe 64 de biți. Interval
-0x8000000000000000–0x7FFFFFFFFFFFFFFF.
- uctypes.FLOAT32: int¶
Virgulă mobilă în simplă precizie IEEE 754 (4 octeți). Citirile și scrierile sunt convertite spre/dinspre un
floatPython.
- uctypes.FLOAT64: int¶
Virgulă mobilă în dublă precizie IEEE 754 (8 octeți). Citirile și scrierile sunt convertite spre/dinspre un
floatPython.
- uctypes.VOID: int¶
Alias pentru
UINT8. Furnizat astfel încât câmpurilevoid *în stil C să poată fi descrise idiomatic ca(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
Marchează un câmp descriptor ca pointer către alt tip. Un câmp pointer este scris ca un tuplu cu două elemente
(offset | PTR, target_type_or_descriptor). Dereferențierea pointerului produce o vizualizare tipizată asupra adresei pe care o conține.
- uctypes.ARRAY: int¶
Marchează un câmp descriptor ca tablou cu lungime fixă de alt tip. Un câmp tablou este fie
(offset | ARRAY, count | element_type)pentru tablouri de scalari, fie(offset | ARRAY, count, element_descriptor)pentru tablouri de structuri. Numărul de elemente este fixat în momentul definirii descriptorului.
Nu există nicio constantă explicită pentru structuri: un descriptor agregat care nu folosește nici PTR, nici ARRAY este tratat ca o structură.
Descriptori de structură și instanțierea obiectelor structură¶
Având un dicționar descriptor de structură și tipul său de aranjament, puteți instanția o anumită instanță de structură la o adresă de memorie dată folosind constructorul uctypes.struct(). Adresa de memorie provine de obicei din următoarele surse:
O adresă predefinită, la accesarea registrelor hardware pe un sistem baremetal. Căutați aceste adrese în fișa tehnică (datasheet) a unui anumit MCU/SoC.
Ca valoare de retur de la un apel către o funcție FFI (Foreign Function Interface).
De la
uctypes.addressof(), atunci când doriți să transmiteți argumente unei funcții FFI sau, alternativ, să accesați anumite date pentru I/O (de exemplu, date citite dintr-un fișier sau dintr-un socket de rețea).
Obiecte structură¶
Obiectele structură permit accesarea câmpurilor individuale folosind notația standard cu punct: my_struct.substruct1.field1. Dacă un câmp este de tip scalar, obținerea lui va produce o valoare primitivă (un întreg sau un float Python) corespunzătoare valorii conținute în câmp. Unui câmp scalar i se poate, de asemenea, atribui o valoare.
Dacă un câmp este un tablou, elementele sale individuale pot fi accesate cu operatorul standard de indexare [] - atât pentru citire, cât și pentru atribuire.
Dacă un câmp este un pointer, el poate fi dereferențiat folosind sintaxa [0] (corespunzătoare operatorului C *, deși [0] funcționează și în C). Indexarea unui pointer cu alte valori întregi decât 0 este de asemenea acceptată, cu aceeași semantică ca în C.
Pe scurt, accesarea câmpurilor unei structuri urmează în general sintaxa C, cu excepția dereferențierii pointerilor, când trebuie să folosiți operatorul [0] în loc de *.
Limitări¶
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ți accesarea structurilor imbricate. De exemplu, în loc de
mcu_registers.peripheral_a.register1, definiți descriptori de aranjament separați pentru fiecare periferic, care să fie accesați caperipheral_a.register1. Sau, pur și simplu, memorați în cache un anumit periferic:peripheral_a = mcu_registers.peripheral_a. Dacă un registru este alcătuit din mai multe câmpuri de biți, ar trebui să memorați în cache referințe către un anumit registru:reg_a = mcu_registers.peripheral_a.reg_a.Evitați alte date non-scalare, precum tablourile. De exemplu, în loc de
peripheral_a.register[0]folosițiperipheral_a.register0. Din nou, o alternativă este memorarea în cache a valorilor intermediare, de exempluregister0 = 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).