MicroPython:Библиотеки/uctypes
Модуль uctypes – доступ к двоичным данным при помощи структур данных[1]
В этом модуле реализован «интерфейс для внешних данных» для MicroPython. Его идея близка CPython’овскому модулю ctypes, но сам API отличается, упрощен и оптимизирован под маленькие ресурсы. Базовая идея модуля uctypes в том, чтобы возможность задать макет структуры данных была не хуже, чем в языке C, а также в том, чтобы потом получить доступ к этой структуре и ее подполям при помощи знакомого точечного синтаксиса.
Примеры использования:
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()
- uctypes.LITTLE_ENDIAN
- uctypes.BIG_ENDIAN
- uctypes.NATIVE
- uctypes.sizeof()
- uctypes.addressof()
- uctypes.bytes_at()
- uctypes.bytearray_at()
- uctypes.UINT8
- uctypes.INT8
- uctypes.UINT16
- uctypes.INT16
- uctypes.UINT32
- uctypes.INT32
- uctypes.UINT64
- uctypes.INT64
- uctypes.FLOAT32
- uctypes.FLOAT64
- uctypes.VOID
- uctypes.PTR
- uctypes.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] вместо *.
Ограничения
- Доступ к нескалярным полям ведет к немедленному выделению памяти под объекты для них. Это значит, что нужно быть особенно осторожным при работе с макетом структуры, доступ к которой нужно получить в момент, когда выделение памяти отключено (например, из прерывания). Рекомендации:
- Избегайте доступа к вложенным структурам. Например, вместо 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].
- Диапазон смещений, поддерживаемых модулем uctypes, ограничен. В принципе, этот диапазон зависит от используемой реализации, но в целом можно посоветовать разбивать структуры таким образом, чтобы уложиться в диапазон между несколькими килобайтами и несколькими десятками килобайт. В принципе, в большинстве случаев это нормальная ситуация. Например, вам нет смысла задавать в одной структуре все регистры микроконтроллера (простирающиеся по всему 32-битному адресному пространству) – обычно вам нужны лишь регистры используемых периферийных компонентов. В крайнем случае вам может понадобиться специально разбить структуру на несколько частей (например, чтобы получить доступ к нативной структуре данных с много-мегабайтным массивом посередине, но это очень гипотетическая ситуация).
<syntaxhighlight lang="python" enclose="div">