MicroPython:Библиотеки/uctypes
Модуль uctypes – доступ к двоичным данным при помощи структур данных[1]
В этом модуле реализован «интерфейс для внешних данных» для MicroPython. Его идея близка CPython’овскому модулю ctypes, но сам API отличается, упрощен и оптимизирован под маленькие ресурсы. Базовая идея модуля uctypes в том, чтобы возможность задать макет структуры данных была не хуже, чем в языке C, а также в том, чтобы потом получить доступ к этой структуре и ее подполям при помощи знакомого точечного синтаксиса.
Внимание! Модуль ctypes позволяет получить доступ к любым адресам памяти на устройстве (включая регистры чтения/записи и управляющие регистры). Неосторожное использование может привести к вылетам, потере данных и даже неисправности устройства.
Смотрите также: Модуль ustruct. В нем реализован стандартный Python’овский способ доступа к двоичным структурам данных (но он плохо масштабируется для больших и сложных структур).
Примеры использования:
import uctypes
# Пример 1: часть заголовка ELF-файла.
# 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" – это ELF-файл, открытый в двоичном режиме.
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, "Упс, неправильный порядок следования байтов. Можно попробовать снова с uctypes.BIG_ENDIAN."
print("machine:", hex(header.e_machine))
# Пример 2: Резидентная структура данных, с указателями.
COORD = {
"x": 0 | uctypes.FLOAT32,
"y": 4 | uctypes.FLOAT32,
}
STRUCT1 = {
"data1": 0 | uctypes.UINT8,
"data2": 4 | uctypes.UINT32,
"ptr": (8 | uctypes.PTR, COORD),
}
# Предположим, у вас есть адрес структуры типа STRUCT1 в «addr».
# Аргумент uctypes.NATIVE опционален (по умолчанию используется).
struct1 = uctypes.struct(addr, STRUCT1, uctypes.NATIVE)
print("x:", struct1.ptr[0].x)
# Пример 3: доступ к регистрам CPU. Часть WWDG-блока STM32F4xx.
WWDG_LAYOUT = {
"WWDG_CR": (0, {
# «BFUINT32» означает размер регистра WWDG_CR.
"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("Текущее значение счетчика:", 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-значения будет получен и если порядок в структуре будет «от старшего к младшему», и если он будет «от младшего к старшему». Но важно, чтобы у самого младшего бита был номер «0». У разных значений может быть разная нумерация в их нативных ABI, но в uctypes всегда используется описанная выше нормализированная нумерация. Содержимое модуля
- Класс uctypes.struct(addr, descriptor, layout_type=NATIVE, /) – инстанцинирует объект «внешней структуры данных» на основе адреса структуры в памяти (addr), дескриптора, зашифрованного в виде словаря (descriptor), и типа макета (layout_type; подробнее читайте ниже).
- uctypes.LITTLE_ENDIAN – тип макета для структуры, упакованной с порядком байтов «от младшего к старшему». «Упакованной» означает, что каждое поле занимает именно столько байтов, сколько было задано в дескрипторе, т.е. выравнивание равно «1»).
- uctypes.BIG_ENDIAN – тип макета для структуры, упакованной с порядком байтов «от старшего к младшему».
- uctypes.NATIVE – тип макета для нативной структуры. То есть порядок байтов и выравнивание соответствуют ABI системы, на которой запущен MicroPython.
- uctypes.sizeof(struct, layout_type=NATIVE, /) – возвращает размер структуры данных (в байтах). В аргументе struct можно задать или класс структуры, или конкретный инстанцинированный объект структуры (или его агрегатное поле).
- uctypes.addressof(obj) – возвращает адрес объекта. В аргументе obj должен быть объект bytes, массив байтов или другой объект, поддерживающий буферный протокол (фактически функция как раз возвращает адрес этого буфера).
- uctypes.bytes_at(addr, size) – извлекает данные, находящиеся по адресу addr в памяти и имеющие размер size, и возвращает их в виде объекта bytes. Поскольку объект bytes неизменяем, данные фактически дублируются и копируются в этот объект bytes, поэтому если содержимое памяти в будущем изменится, вновь созданный объект bytes сохранит свое изначальное значение.
- uctypes.bytearray_at(addr, size) – извлекает данные, находящиеся по адресу addr в памяти и имеющие размер size, и возвращает их в виде объекта bytearray. В отличие от функции bytes_at() выше, извлечение выполняется при помощи указателя, поэтому оба объекта будут изменяемыми. То есть в результате вы всегда получаете доступ к текущему значению, находящемуся в заданной ячейке памяти.
- uctypes.UINT8, uctypes.INT8, uctypes.UINT16, uctypes.INT16, uctypes.UINT32, uctypes.INT32, uctypes.UINT64, uctypes.INT64 – типы целых чисел для дескриптора структуры данных. Константы для 8-, 16-, 32- и 64-битных типов данных представлены и в знаковом, и беззнаковом вариантах.
- uctypes.FLOAT32, uctypes.FLOAT64 – типы чисел с плавающей точкой для дескриптора структуры данных.
- uctypes.VOID – это псевдоним для типа данных UINT8, и он нужен для совместимости с пустыми указателями языка C: uctypes.PTR, uctypes.VOID.
- uctypes.PTR, uctypes.ARRAY – константы для указателей и массивов. Явных констант для структур в модуле uctypes нет, они неявные: агрегатный тип без флагов PTR и ARRAY будет считаться структурой.
Дескрипторы структуры данных и инстанцинирование объекта структуры Задав в конструкторе uctypes.struct() словарь с дескриптором структуры и тип макета, вы можете инстанцинировать экземпляр нужной вам структуры в нужном адресе памяти. Адрес в памяти обычно можно узнать следующим образом:
- Это может быть адрес, заданный заранее – если речь об аппаратных регистрах на «голой» системе. Ищите эти адреса в документации к своему микроконтроллеру/SoC.
- При помощи некоторых FFI-функций (от англ. «foreign function interface», т.е. речь о функциях, служащих интерфейсом для внешних функций).
- При помощи uctypes.addressof() – когда вам нужно передать аргументы FFI-функции или получить доступ к некоторым данным для I/O-операций (к примеру, для чтения данных из файла или сетевого сокета).
Объекты структуры данных Объекты структуры данных позволяют получить доступ к отдельным полям при помощи стандартного точечного синтаксиса: my_struct.substruct1.field1. Если это поле скалярного типа, то извлечение данных из него сгенерирует примитивное значение (Python’овское целое число или число с плавающей точкой), соответствующее значению, содержащемуся в поле. Скалярное поле также можно присвоить. Если поле – это массив, доступ к его отдельным элементам можно получить при помощи стандартного оператора индексирования [] – и для чтения, и для присвоения. Если поле – это указатель, то его можно разыменовать при помощи синтаксиса [0] (это соответствует C-оператору *, но [0] работает и в C тоже). Индексирование указателей при помощи целого числа (кроме «0») тоже поддерживается – при помощи той же семантики, что и в C. Подытоживая, доступ к полям структуры данных в целом повторяет синтаксис языка C, за исключением разыменовывания указателей, где нужно использовать оператор [0] вместо *. Ограничения 1. Доступ к нескалярным полям ведет к немедленному выделению памяти под объекты для них. Это значит, что нужно быть особенно осторожным при работе с макетом структуры, доступ к которой нужно получить в момент, когда выделение памяти отключено (например, из прерывания). Рекомендации:
- Избегайте доступа к вложенным структурам. Например, вместо 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. Диапазон смещений, поддерживаемых модулем uctypes, ограничен. В принципе, этот диапазон зависит от используемой реализации, но в целом можно посоветовать разбивать структуры таким образом, чтобы уложиться в диапазон между несколькими килобайтами и несколькими десятками килобайт. В принципе, в большинстве случаев это нормальная ситуация. Например, вам нет смысла задавать в одной структуре все регистры микроконтроллера (простирающиеся по всему 32-битному адресному пространству) – обычно вам нужны лишь регистры используемых периферийных компонентов. В крайнем случае вам может понадобиться специально разбить структуру на несколько частей (например, чтобы получить доступ к нативной структуре данных с много-мегабайтным массивом посередине, но это очень гипотетическая ситуация).
<syntaxhighlight lang="python" enclose="div">