uctypes — доступ до бінарних даних у структурованому вигляді

Цей модуль реалізує «інтерфейс зовнішніх даних» для MicroPython. Ідея схожа на модуль ctypes у CPython, але фактичний API відрізняється: він спрощений і оптимізований для малого розміру. Основна ідея модуля — описати розташування структури даних із тією самою виразністю, що й мова C, а потім звертатися до підполів за допомогою знайомого синтаксису з крапкою.

Попередження

Модуль uctypes дозволяє звертатися до довільних адрес пам’яті машини (включно з регістрами вводу/виводу та керування). Необережне використання може призвести до збоїв, втрати даних і навіть несправності апаратного забезпечення.

Дивись також

Модуль struct

Стандартний модуль Python для пакування та розпакування бінарних даних. struct працює з цілими буферами за допомогою компактного рядка формату (наприклад, '<HBB4sI'), що добре підходить для кількох фіксованих полів, але погано масштабується для великих або глибоко вкладених структур: кожне зчитування або запис повторно аналізує рядок формату, об’єднання та бітові поля не підтримуються, і немає можливості отримати типізований вигляд у наявний буфер. uctypes доповнює struct, дозволяючи описати розташування один раз, прив’язати його до області пам’яті (RAM, регістри периферійних пристроїв, bytearray) і потім звертатися до окремих полів як до іменованих атрибутів — уникаючи повторного розбору та копіювання, і додаючи підтримку вкладених структур, масивів, об’єднань та бітових полів.

Приклади використання:

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)

Визначення розташування структури

Розташування структури описується «дескриптором» — словником Python, де ключами є назви полів, а відповідними значеннями — властивості, необхідні для доступу до них:

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

Наразі uctypes вимагає явного зазначення зміщень для кожного поля. Зміщення задаються у байтах від початку структури.

Нижче наведено приклади кодування для різних типів полів:

  • Скалярні типи:

    "field_name": offset | uctypes.UINT32
    

    іншими словами, значення — це ідентифікатор скалярного типу, об’єднаний за допомогою OR зі зміщенням поля (у байтах) від початку структури.

  • Рекурсивні структури:

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

    тобто значення — це 2-кортеж, перший елемент якого — зміщення, а другий — словник-дескриптор структури (зверніть увагу: зміщення у рекурсивних дескрипторах відносні щодо тієї структури, яку вони описують). Звичайно, рекурсивні структури можна вказувати не лише у вигляді літерального словника, але й посилаючись на словник-дескриптор структури (визначений раніше) за назвою.

  • Масиви примітивних типів:

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

    тобто значення — це 2-кортеж, перший елемент якого — прапорець ARRAY, об’єднаний за допомогою OR зі зміщенням, а другий — скалярний тип елемента, об’єднаний за допомогою OR з кількістю елементів у масиві.

  • Масиви агрегатних типів:

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

    тобто значення — це 3-кортеж, перший елемент якого — прапорець ARRAY, об’єднаний за допомогою OR зі зміщенням, другий — кількість елементів у масиві, а третій — дескриптор типу елемента.

  • Вказівник на примітивний тип:

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

    тобто значення — це 2-кортеж, перший елемент якого — прапорець PTR, об’єднаний за допомогою OR зі зміщенням, а другий — скалярний тип елемента.

  • Вказівник на агрегатний тип:

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

    тобто значення — це 2-кортеж, перший елемент якого — прапорець PTR, об’єднаний за допомогою OR зі зміщенням, а другий — дескриптор типу, на який вказує вказівник.

  • Бітові поля:

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

    тобто значення — це тип скалярного значення, що містить дане бітове поле (назви типів аналогічні скалярним, але з префіксом BF), об’єднаний за допомогою OR зі зміщенням для скалярного значення, що містить бітове поле, і далі об’єднаний за допомогою OR зі значеннями для позиції біта та довжини бітового поля у скалярному значенні, зсунутими відповідно на BF_POS та BF_LEN бітів. Позиція бітового поля відраховується від молодшого значущого біта скаляра (з позицією 0) і є номером крайнього правого біта поля (іншими словами, це кількість бітів, на яку потрібно зсунути скаляр вправо, щоб виділити бітове поле).

    У наведеному вище прикладі спочатку зчитується значення UINT16 за зміщенням 0 (ця деталь може бути важливою при зверненні до апаратних регістрів, де потрібні певний розмір доступу та вирівнювання), а потім витягується бітове поле, крайній правий біт якого є бітом lsbit цього UINT16, а довжина — bitsize бітів. Наприклад, якщо lsbit дорівнює 0, а bitsize — 8, то фактично буде здійснений доступ до молодшого байта UINT16.

    Зверніть увагу, що операції з бітовими полями не залежать від порядку байтів цільової системи; зокрема, наведений вище приклад звертатиметься до молодшого байта UINT16 як у структурах з порядком від молодшого (little-endian), так і від старшого (big-endian). Але це залежить від нумерації молодшого значущого біта, починаючи з 0. Деякі цільові системи можуть використовувати іншу нумерацію у своєму рідному ABI, однак uctypes завжди використовує нормалізовану нумерацію, описану вище.

Вміст модуля

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

Створити об’єкт «зовнішньої структури даних» на основі адреси структури в пам’яті, дескриптора (закодованого як словник) та типу розташування (див. нижче).

uctypes.LITTLE_ENDIAN: int

Тип розташування для упакованої структури з порядком байтів від молодшого (little-endian). (Упакованість означає, що кожне поле займає рівно стільки байтів, скільки визначено у дескрипторі, тобто вирівнювання дорівнює 1).

uctypes.BIG_ENDIAN: int

Тип розташування для упакованої структури з порядком байтів від старшого (big-endian).

uctypes.NATIVE: int

Тип розташування для рідної структури — з порядком байтів і вирівнюванням відповідно до ABI системи, на якій виконується MicroPython.

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

Повернути розмір структури даних у байтах. Аргумент struct може бути або класом структури, або конкретним інстанційованим об’єктом структури (або його агрегатним полем).

uctypes.addressof(obj: Any) int

Повернути адресу об’єкта. Аргументом має бути bytes, bytearray або інший об’єкт, що підтримує протокол буферу (і саме адреса цього буфера повертається).

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

Захопити пам’ять за вказаною адресою та розміром як об’єкт bytes. Оскільки об’єкт bytes є незмінним, пам’ять фактично дублюється і копіюється в об’єкт bytes, тому якщо вміст пам’яті пізніше зміниться, створений об’єкт збереже початкове значення.

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

Захопити пам’ять за вказаною адресою та розміром як об’єкт bytearray. На відміну від функції bytes_at() вище, пам’ять захоплюється за посиланням, тому в неї можна як записувати, так і зчитувати поточне значення за вказаною адресою пам’яті.

Скалярні цілочисельні типи. Кожен займає очевидну кількість байтів (1, 2, 4 або 8) і зчитується/записується з урахуванням порядку байтів типу розташування структури (одного з NATIVE, LITTLE_ENDIAN або BIG_ENDIAN).

uctypes.UINT8: int

Беззнаковий 8-бітний цілочисельний тип. Діапазон 0255.

uctypes.INT8: int

Знаковий 8-бітний цілочисельний тип. Діапазон -128127.

uctypes.UINT16: int

Беззнаковий 16-бітний цілочисельний тип. Діапазон 065535.

uctypes.INT16: int

Знаковий 16-бітний цілочисельний тип. Діапазон -3276832767.

uctypes.UINT32: int

Беззнаковий 32-бітний цілочисельний тип. Діапазон 00xFFFFFFFF.

uctypes.INT32: int

Знаковий 32-бітний цілочисельний тип. Діапазон -0x800000000x7FFFFFFF.

uctypes.UINT64: int

Беззнаковий 64-бітний цілочисельний тип. Діапазон 00xFFFFFFFFFFFFFFFF.

uctypes.INT64: int

Знаковий 64-бітний цілочисельний тип. Діапазон -0x80000000000000000x7FFFFFFFFFFFFFFF.

uctypes.FLOAT32: int

Число з плаваючою комою одинарної точності IEEE 754 (4 байти). Читання та записи перетворюються з/до Python float.

uctypes.FLOAT64: int

Число з плаваючою комою подвійної точності IEEE 754 (8 байтів). Читання та записи перетворюються з/до Python float.

uctypes.VOID: int

Псевдонім для UINT8. Надається для того, щоб поля типу C void * можна було ідіоматично описати як (uctypes.PTR, uctypes.VOID).

uctypes.PTR: int

Позначає поле дескриптора як вказівник на інший тип. Поле-вказівник записується у вигляді двокортежу (offset | PTR, target_type_or_descriptor). Розіменування вказівника повертає типізований вигляд пам’яті за адресою, яку він містить.

uctypes.ARRAY: int

Позначає поле дескриптора як масив фіксованої довжини іншого типу. Поле-масив записується або як (offset | ARRAY, count | element_type) для масивів скалярів, або як (offset | ARRAY, count, element_descriptor) для масивів структур. Кількість елементів фіксується на момент визначення дескриптора.

Для структур явної константи не існує: агрегатний дескриптор, що не використовує ні PTR, ні ARRAY, вважається структурою.

Дескриптори структур та інстанціювання об’єктів структур

Маючи словник-дескриптор структури та тип її розташування, можна створити конкретний екземпляр структури за заданою адресою пам’яті за допомогою конструктора uctypes.struct(). Адреса пам’яті зазвичай надходить з таких джерел:

  • Попередньо визначена адреса при зверненні до апаратних регістрів на системах без операційної системи. Ці адреси слід шукати в технічній документації (datasheet) для конкретного MCU/SoC.

  • Як значення, що повертається при виклику функції FFI (Foreign Function Interface).

  • З uctypes.addressof(), коли потрібно передати аргументи функції FFI або, як альтернатива, звернутися до даних для вводу/виводу (наприклад, до даних, зчитаних з файлу або мережевого сокета).

Об’єкти структур

Об’єкти структур дозволяють звертатися до окремих полів за допомогою стандартного синтаксису з крапкою: my_struct.substruct1.field1. Якщо поле є скалярним типом, його отримання поверне примітивне значення (ціле число або число з плаваючою комою Python), що відповідає значенню, яке міститься у полі. До скалярного поля також можна присвоїти значення.

Якщо поле є масивом, до його окремих елементів можна звертатися за допомогою стандартного оператора індексації [] — як для читання, так і для присвоєння.

Якщо поле є вказівником, його можна розіменувати за допомогою синтаксису [0] (що відповідає оператору * у C, хоча [0] також працює у C). Також підтримується індексація вказівника іншими цілочисельними значеннями, відмінними від 0, з тією самою семантикою, що і в C.

Підсумовуючи, доступ до полів структури загалом відповідає синтаксису C, за винятком розіменування вказівника, коли замість * слід використовувати оператор [0].

Обмеження

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:

  • Уникайте доступу до вкладених структур. Наприклад, замість mcu_registers.peripheral_a.register1 визначте окремі дескриптори розташування для кожного периферійного пристрою та звертайтеся до них як peripheral_a.register1. Або просто кешуйте конкретний периферійний пристрій: peripheral_a = mcu_registers.peripheral_a. Якщо регістр складається з кількох бітових полів, необхідно кешувати посилання на конкретний регістр: reg_a = mcu_registers.peripheral_a.reg_a.

  • Уникайте інших нескалярних даних, наприклад масивів. Наприклад, замість peripheral_a.register[0] використовуйте peripheral_a.register0. Знову ж таки, альтернативою є кешування проміжних значень, наприклад 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).