uctypes — структурированный доступ к двоичным данным¶
Этот модуль реализует «интерфейс внешних данных» для MicroPython. Лежащая в его основе идея похожа на модуль ctypes из CPython, однако сам API отличается: он упрощён и оптимизирован под малый объём. Основная идея модуля состоит в том, чтобы описать структуру данных примерно с теми же возможностями, что предоставляет язык C, а затем обращаться к ней с помощью привычного точечного синтаксиса для ссылки на вложенные поля.
Предупреждение
Модуль uctypes позволяет обращаться к произвольным адресам памяти машины (включая регистры ввода-вывода и управления). Неосторожное его использование может привести к сбоям, потере данных и даже к неисправности оборудования.
См. также
- Модуль
struct Стандартный модуль Python для упаковки и распаковки двоичных данных.
structобрабатывает буфер целиком за один раз с помощью компактной строки формата (например,'<HBB4sI'), что хорошо подходит для нескольких фиксированных полей, но плохо масштабируется на большие или глубоко вложенные структуры: при каждом чтении или записи строка формата разбирается заново, объединения и битовые поля не поддерживаются, и нет способа получить типизированное представление существующего буфера.uctypesдополняетstruct, позволяя один раз описать структуру, привязать её к области памяти (ОЗУ, регистрам периферийных устройств, объекту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иными словами, значение представляет собой идентификатор скалярного типа, объединённый операцией ИЛИ со смещением поля (в байтах) от начала структуры.
Рекурсивные структуры:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
то есть значение представляет собой 2-кортеж, первый элемент которого — смещение, а второй — словарь-дескриптор структуры (обратите внимание: смещения в рекурсивных дескрипторах отсчитываются относительно той структуры, которую они описывают). Разумеется, рекурсивные структуры можно задавать не только литеральным словарём, но и ссылкой по имени на словарь-дескриптор структуры (определённый ранее).
Массивы примитивных типов:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),то есть значение представляет собой 2-кортеж, первый элемент которого — флаг ARRAY, объединённый операцией ИЛИ со смещением, а второй — скалярный тип элемента, объединённый операцией ИЛИ с числом элементов в массиве.
Массивы составных типов:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),то есть значение представляет собой 3-кортеж, первый элемент которого — флаг ARRAY, объединённый операцией ИЛИ со смещением, второй — число элементов в массиве, а третий — дескриптор типа элемента.
Указатель на примитивный тип:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),то есть значение представляет собой 2-кортеж, первый элемент которого — флаг PTR, объединённый операцией ИЛИ со смещением, а второй — скалярный тип элемента.
Указатель на составной тип:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),то есть значение представляет собой 2-кортеж, первый элемент которого — флаг PTR, объединённый операцией ИЛИ со смещением, а второй — дескриптор типа, на который указывает указатель.
Битовые поля:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,то есть значение представляет собой тип скалярного значения, содержащего заданное битовое поле (имена типов аналогичны скалярным типам, но имеют префикс
BF), объединённый операцией ИЛИ со смещением скалярного значения, содержащего битовое поле, и далее объединённый операцией ИЛИ со значениями позиции бита и длины битового поля внутри скалярного значения, сдвинутыми соответственно на 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.FLOAT32: int¶
Число с плавающей запятой одинарной точности IEEE 754 (4 байта). При чтении и записи преобразуется в/из Python-объекта
float.
- uctypes.FLOAT64: int¶
Число с плавающей запятой двойной точности IEEE 754 (8 байтов). При чтении и записи преобразуется в/из Python-объекта
float.
- uctypes.VOID: int¶
Псевдоним для
UINT8. Предоставляется для того, чтобы поля в стиле Cvoid *можно было описывать идиоматично как(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(). Адрес памяти обычно поступает из следующих источников:
Заранее заданный адрес — при доступе к аппаратным регистрам в системе без операционной системы (baremetal). Эти адреса следует искать в технической документации на конкретный 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).