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() словарь с дескриптором структуры и тип макета, вы можете инстанцинировать экземпляр нужной вам структуры в нужном адресе памяти. Адрес в памяти обычно можно узнать следующим образом:

  • Это может быть адрес, заданный заранее – если речь об аппаратных регистрах на «голой» системе. Ищите эти адреса в документации к своему микроконтроллеру/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">

См.также

Внешние ссылки