uctypes --- mengakses data biner secara terstruktur

Modul ini mengimplementasikan "antarmuka data asing" untuk MicroPython. Ide di baliknya mirip dengan modul ctypes CPython, namun API aktualnya berbeda, lebih ramping dan dioptimalkan untuk ukuran kecil. Ide dasar modul ini adalah mendefinisikan tata letak struktur data dengan kemampuan yang kurang lebih setara dengan bahasa C, kemudian mengaksesnya menggunakan sintaks titik yang familiar untuk merujuk sub-field.

Peringatan

Modul uctypes memungkinkan akses ke alamat memori arbitrer pada mesin (termasuk register I/O dan kontrol). Penggunaan yang tidak hati-hati dapat menyebabkan crash, kehilangan data, bahkan kerusakan perangkat keras.

Lihat juga

Modul struct

Modul Python standar untuk mengemas dan membongkar data biner. struct beroperasi pada seluruh buffer sekaligus menggunakan string format yang ringkas (misalnya '<HBB4sI'), yang bekerja baik untuk beberapa field tetap namun skalanya buruk untuk struktur besar atau bersarang dalam: setiap pembacaan atau penulisan mem-parsing ulang string format, union dan bitfield tidak didukung, dan tidak ada cara untuk mendapatkan tampilan bertipe ke buffer yang ada. uctypes melengkapi struct dengan memungkinkan Anda mendeskripsikan tata letak sekali, menempelkannya ke wilayah memori (RAM, register periferal, bytearray) lalu mengakses field individual sebagai atribut bernama -- menghindari parsing dan penyalinan berulang, serta menambah dukungan untuk struct bersarang, array, union, dan bitfield.

Contoh penggunaan:

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)

Mendefinisikan tata letak struktur

Tata letak struktur didefinisikan oleh sebuah "deskriptor" - kamus Python yang mengkodekan nama field sebagai kunci dan properti lain yang diperlukan untuk mengaksesnya sebagai nilai terkait:

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

Saat ini, uctypes memerlukan spesifikasi offset eksplisit untuk setiap field. Offset diberikan dalam byte dari awal struktur.

Berikut adalah contoh pengkodean untuk berbagai tipe field:

  • Tipe skalar:

    "field_name": offset | uctypes.UINT32
    

    dengan kata lain, nilai adalah identifier tipe skalar yang di-OR dengan offset field (dalam byte) dari awal struktur.

  • Struktur rekursif:

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

    yaitu nilai adalah 2-tuple, elemen pertamanya adalah offset, dan elemen kedua adalah kamus deskriptor struktur (catatan: offset dalam deskriptor rekursif relatif terhadap struktur yang didefinisikannya). Tentu saja, struktur rekursif dapat ditentukan tidak hanya dengan kamus literal, tetapi juga dengan merujuk kamus deskriptor struktur (yang didefinisikan sebelumnya) berdasarkan nama.

  • Array tipe primitif:

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

    yaitu nilai adalah 2-tuple, elemen pertamanya adalah flag ARRAY yang di-OR dengan offset, dan elemen kedua adalah tipe elemen skalar yang di-OR dengan jumlah elemen dalam array.

  • Array tipe agregat:

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

    yaitu nilai adalah 3-tuple, elemen pertamanya adalah flag ARRAY yang di-OR dengan offset, elemen kedua adalah jumlah elemen dalam array, dan elemen ketiga adalah deskriptor tipe elemen.

  • Pointer ke tipe primitif:

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

    yaitu nilai adalah 2-tuple, elemen pertamanya adalah flag PTR yang di-OR dengan offset, dan elemen kedua adalah tipe elemen skalar.

  • Pointer ke tipe agregat:

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

    yaitu nilai adalah 2-tuple, elemen pertamanya adalah flag PTR yang di-OR dengan offset, elemen kedua adalah deskriptor dari tipe yang ditunjuk.

  • Bitfield:

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

    yaitu nilai adalah tipe nilai skalar yang mengandung bitfield yang diberikan (nama tipe mirip dengan tipe skalar, tetapi dengan prefiks BF), di-OR dengan offset untuk nilai skalar yang mengandung bitfield, dan selanjutnya di-OR dengan nilai untuk posisi bit dan panjang bit dari bitfield dalam nilai skalar, digeser masing-masing oleh bit BF_POS dan BF_LEN. Posisi bitfield dihitung dari bit paling tidak signifikan dari skalar (yang memiliki posisi 0), dan merupakan nomor bit paling kanan dari sebuah field (dengan kata lain, ini adalah jumlah bit yang perlu digeser ke kanan oleh skalar untuk mengekstrak bitfield).

    Dalam contoh di atas, pertama nilai UINT16 akan diekstrak pada offset 0 (detail ini mungkin penting saat mengakses register perangkat keras, di mana ukuran akses dan penyelarasan tertentu diperlukan), kemudian bitfield yang bit paling kanannya adalah bit lsbit dari UINT16 ini, dan panjangnya adalah bit bitsize, akan diekstrak. Misalnya, jika lsbit adalah 0 dan bitsize adalah 8, maka secara efektif akan mengakses byte paling tidak signifikan dari UINT16.

    Perhatikan bahwa operasi bitfield tidak bergantung pada endianness byte target; khususnya, contoh di atas akan mengakses byte paling tidak signifikan dari UINT16 pada struktur little-endian maupun big-endian. Namun ini bergantung pada bit paling tidak signifikan yang diberi nomor 0. Beberapa target mungkin menggunakan penomoran berbeda dalam ABI aslinya, tetapi uctypes selalu menggunakan penomoran yang dinormalisasi seperti yang dijelaskan di atas.

Isi modul

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

Instantiasi objek "struktur data asing" berdasarkan alamat struktur di memori, deskriptor (dikodekan sebagai kamus), dan tipe tata letak (lihat di bawah).

uctypes.LITTLE_ENDIAN: int

Tipe tata letak untuk struktur packed little-endian. (Packed berarti setiap field menempati tepat sebanyak byte yang didefinisikan dalam deskriptor, yaitu penyelarasannya adalah 1).

uctypes.BIG_ENDIAN: int

Tipe tata letak untuk struktur packed big-endian.

uctypes.NATIVE: int

Tipe tata letak untuk struktur native - dengan endianness data dan penyelarasan yang sesuai dengan ABI dari sistem tempat MicroPython berjalan.

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

Mengembalikan ukuran struktur data dalam byte. Argumen struct dapat berupa kelas struktur atau objek struktur yang sudah diinstansiasi (atau field agregatnya).

uctypes.addressof(obj: Any) int

Mengembalikan alamat dari sebuah objek. Argumen harus berupa bytes, bytearray, atau objek lain yang mendukung protokol buffer (dan alamat buffer inilah yang sebenarnya dikembalikan).

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

Menangkap memori pada alamat dan ukuran yang diberikan sebagai objek bytes. Karena objek bytes tidak dapat diubah, memori sebenarnya diduplikasi dan disalin ke objek bytes, sehingga jika isi memori berubah kemudian, objek yang dibuat tetap mempertahankan nilai aslinya.

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

Menangkap memori pada alamat dan ukuran yang diberikan sebagai objek bytearray. Tidak seperti fungsi bytes_at() di atas, memori ditangkap dengan referensi, sehingga dapat ditulis maupun dibaca, dan Anda akan mengakses nilai saat ini di alamat memori yang diberikan.

Tipe integer skalar. Masing-masing menempati jumlah byte yang jelas (1, 2, 4, atau 8) dan dibaca/ditulis menggunakan endianness dari tipe tata letak struktur (salah satu dari NATIVE, LITTLE_ENDIAN, atau BIG_ENDIAN).

uctypes.UINT8: int

Integer 8-bit tidak bertanda. Rentang 0 -- 255.

uctypes.INT8: int

Integer 8-bit bertanda. Rentang -128 -- 127.

uctypes.UINT16: int

Integer 16-bit tidak bertanda. Rentang 0 -- 65535.

uctypes.INT16: int

Integer 16-bit bertanda. Rentang -32768 -- 32767.

uctypes.UINT32: int

Integer 32-bit tidak bertanda. Rentang 0 -- 0xFFFFFFFF.

uctypes.INT32: int

Integer 32-bit bertanda. Rentang -0x80000000 -- 0x7FFFFFFF.

uctypes.UINT64: int

Integer 64-bit tidak bertanda. Rentang 0 -- 0xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Integer 64-bit bertanda. Rentang -0x8000000000000000 -- 0x7FFFFFFFFFFFFFFF.

uctypes.FLOAT32: int

Floating-point presisi tunggal IEEE 754 (4 byte). Pembacaan dan penulisan dikonversi ke/dari float Python.

uctypes.FLOAT64: int

Floating-point presisi ganda IEEE 754 (8 byte). Pembacaan dan penulisan dikonversi ke/dari float Python.

uctypes.VOID: int

Alias untuk UINT8. Disediakan agar field void * bergaya C dapat dideskripsikan secara idiomatis sebagai (uctypes.PTR, uctypes.VOID).

uctypes.PTR: int

Menandai field deskriptor sebagai pointer ke tipe lain. Field pointer ditulis sebagai 2-tuple (offset | PTR, target_type_or_descriptor). Melakukan dereference pointer menghasilkan tampilan bertipe ke alamat yang dipegangnya.

uctypes.ARRAY: int

Menandai field deskriptor sebagai array panjang tetap dari tipe lain. Field array berupa (offset | ARRAY, count | element_type) untuk array skalar, atau (offset | ARRAY, count, element_descriptor) untuk array struktur. Jumlah elemen ditetapkan pada waktu deskriptor dibuat.

Tidak ada konstanta eksplisit untuk struktur: deskriptor agregat yang tidak menggunakan PTR maupun ARRAY diperlakukan sebagai struktur.

Deskriptor struktur dan menginstansiasi objek struktur

Dengan kamus deskriptor struktur dan tipe tata letaknya, Anda dapat menginstansiasi instance struktur tertentu pada alamat memori yang diberikan menggunakan konstruktor uctypes.struct(). Alamat memori biasanya berasal dari sumber berikut:

  • Alamat yang telah ditentukan sebelumnya, saat mengakses register perangkat keras pada sistem baremetal. Cari alamat-alamat ini di datasheet untuk MCU/SoC tertentu.

  • Sebagai nilai kembalian dari panggilan ke fungsi FFI (Foreign Function Interface) tertentu.

  • Dari uctypes.addressof(), saat Anda ingin meneruskan argumen ke fungsi FFI, atau sebagai alternatif, untuk mengakses beberapa data untuk I/O (misalnya, data yang dibaca dari file atau soket jaringan).

Objek struktur

Objek struktur memungkinkan mengakses field individual menggunakan notasi titik standar: my_struct.substruct1.field1. Jika field bertipe skalar, mengaksesnya akan menghasilkan nilai primitif (integer atau float Python) yang sesuai dengan nilai yang terkandung dalam field. Field skalar juga dapat diassign.

Jika field adalah array, elemen-elemen individualnya dapat diakses dengan operator subscript standar [] - baik dibaca maupun diassign.

Jika field adalah pointer, ia dapat didereferensi menggunakan sintaks [0] (sesuai dengan operator * di C, meskipun [0] juga berfungsi di C). Subscripting pointer dengan nilai integer selain 0 juga didukung, dengan semantik yang sama seperti di C.

Singkatnya, mengakses field struktur umumnya mengikuti sintaks C, kecuali untuk dereference pointer, di mana Anda perlu menggunakan operator [0] alih-alih *.

Keterbatasan

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:

  • Hindari mengakses struktur bersarang. Misalnya, alih-alih mcu_registers.peripheral_a.register1, definisikan deskriptor tata letak terpisah untuk setiap periferal, untuk diakses sebagai peripheral_a.register1. Atau cukup simpan periferal tertentu dalam cache: peripheral_a = mcu_registers.peripheral_a. Jika register terdiri dari beberapa bitfield, Anda perlu menyimpan referensi ke register tertentu dalam cache: reg_a = mcu_registers.peripheral_a.reg_a.

  • Hindari data non-skalar lainnya, seperti array. Misalnya, alih-alih peripheral_a.register[0] gunakan peripheral_a.register0. Sekali lagi, alternatifnya adalah menyimpan nilai perantara dalam cache, misalnya 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).