MicroPython:Библиотеки/uctypes: различия между версиями

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску
Нет описания правки
Нет описания правки
 
(не показано 19 промежуточных версий 1 участника)
Строка 5: Строка 5:
=Модуль uctypes – доступ к двоичным данным при помощи структур данных<ref>[http://docs.micropython.org/en/latest/library/uctypes.html docs.micropython.org - uctypes – access binary data in a structured way]</ref>=
=Модуль uctypes – доступ к двоичным данным при помощи структур данных<ref>[http://docs.micropython.org/en/latest/library/uctypes.html docs.micropython.org - uctypes – access binary data in a structured way]</ref>=


В этом модуле реализован «интерфейс для внешних данных» для MicroPython. Его идея близка CPython’овскому модулю ctypes, но сам API отличается, упрощен и оптимизирован под маленькие ресурсы. Базовая идея модуля uctypes в том, чтобы возможность задать макет структуры данных была не хуже, чем в языке C, а также в том, чтобы потом получить доступ к этой структуре и ее подполям при помощи знакомого точечного синтаксиса.
В этом модуле реализован ''«интерфейс для внешних данных»'' для [[MicroPython]]. Его идея близка [[CPython]]’овскому модулю ctypes, но сам [[API]] отличается, упрощен и оптимизирован под маленькие ресурсы. Базовая идея модуля uctypes в том, чтобы возможность задать макет структуры данных была не хуже, чем в языке [[C]], а также в том, чтобы потом получить доступ к этой структуре и ее подполям при помощи знакомого точечного синтаксиса.


Внимание! Модуль ctypes позволяет получить доступ к любым адресам памяти на устройстве (включая регистры чтения/записи и управляющие регистры). Неосторожное использование может привести к вылетам, потере данных и даже неисправности устройства.
{{Блок/Инфо4
|1=Внимание
|2=Модуль ctypes позволяет получить доступ к любым адресам памяти на устройстве (включая регистры чтения/записи и управляющие регистры). Неосторожное использование может привести к вылетам, потере данных и даже неисправности устройства.}}


Смотрите также: Модуль ustruct. В нем реализован стандартный Python’овский способ доступа к двоичным структурам данных (но он плохо масштабируется для больших и сложных структур).
{{Блок/Инфо1
|1=Смотрите также
|2=''Модуль ustruct.'' В нем реализован стандартный [[Python]]’овский способ доступа к двоичным структурам данных (но он плохо масштабируется для больших и сложных структур).}}


Примеры использования:
Примеры использования:


<syntaxhighlight lang="python" enclose="div">
<syntaxhighlight lang="python">
import uctypes
import uctypes


Строка 69: Строка 73:
WWDG.WWDG_CR.WDGA = 1
WWDG.WWDG_CR.WDGA = 1
print("Текущее значение счетчика:", WWDG.WWDG_CR.T)
print("Текущее значение счетчика:", WWDG.WWDG_CR.T)
Макет структуры данных
</syntaxhighlight>
Макет структуры задается в «дескрипторе» – Python-словаре, в котором есть поля, которые служат ключами, а также другие свойства, через которые можно получить доступ к привязанным к ним значениям.
 
== Макет структуры данных ==
 
Макет структуры задается в ''«дескрипторе»'' – Python-словаре, в котором есть поля, которые служат ключами, а также другие свойства, через которые можно получить доступ к привязанным к ним значениям.
 
<syntaxhighlight lang="python">
{
{
     "field1": <properties>,
     "field1": <properties>,
Строка 82: Строка 91:
Ниже – несколько примеров разных типов полей.
Ниже – несколько примеров разных типов полей.


* Скалярные типы:
* '''Скалярные типы:'''
 
<syntaxhighlight lang="python">
"field_name": offset | uctypes.UINT32
"field_name": offset | uctypes.UINT32
Другими словами, значение – это идентификатор скалярного типа данных, рядом с которым по принципу логического ИЛИ размещено смещение поля (в байтах) от начала структуры.
</syntaxhighlight>
* Рекурсивные структуры:
 
Другими словами, значение – это идентификатор скалярного типа данных, рядом с которым по принципу логического ''ИЛИ'' размещено смещение поля (в байтах) от начала структуры.
 
* '''Рекурсивные структуры:'''
 
<syntaxhighlight lang="python">
"sub": (offset, {
"sub": (offset, {
     "b0": 0 | uctypes.UINT8,
     "b0": 0 | uctypes.UINT8,
     "b1": 1 | uctypes.UINT8,
     "b1": 1 | uctypes.UINT8,
})
})
</syntaxhighlight>
То есть значение – это 2-элементный кортеж. Его первый элемент – это смещение, а второй – это словарь с дескриптором структуры (примечание: смещения в рекурсивных дескрипторах связаны со структурой, которую они задают). Разумеется, рекурсивную структуру можно задать не только самим словарем, но и указанием на словарь с дескриптором структуры (который был задан ранее) по его названию.
То есть значение – это 2-элементный кортеж. Его первый элемент – это смещение, а второй – это словарь с дескриптором структуры (примечание: смещения в рекурсивных дескрипторах связаны со структурой, которую они задают). Разумеется, рекурсивную структуру можно задать не только самим словарем, но и указанием на словарь с дескриптором структуры (который был задан ранее) по его названию.
* Массивы примитивных типов данных:
 
* '''Массивы примитивных типов данных:'''
 
<syntaxhighlight lang="python">
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),
То есть значение – это 2-элементный кортеж. Его первый элемент – это флаг ARRAY, рядом с которым по принципу логического ИЛИ размещено смещение, а второй – это идентификатор скалярного типа данных, рядом с которым по принципу логического ИЛИ размещено количество элементов в массиве.
</syntaxhighlight>
* Массивы агрегатных типов данных:
 
То есть значение – это 2-элементный кортеж. Его первый элемент – это флаг ''ARRAY'', рядом с которым по принципу логического ИЛИ размещено смещение, а второй – это идентификатор скалярного типа данных, рядом с которым по принципу логического ''ИЛИ'' размещено количество элементов в массиве.
 
* '''Массивы агрегатных типов данных:'''
 
<syntaxhighlight lang="python">
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),
То есть значение – это 3-элементный кортеж. Его первый элемент – это флаг ARRAY, рядом с которым по принципу логического ИЛИ размещено смещение, второй – это количество элементов в массиве, а третий – это дескриптор заданного типа.
</syntaxhighlight>
* Указатель на примитивный тип данных:
 
То есть значение – это 3-элементный кортеж. Его первый элемент – это флаг ''ARRAY'', рядом с которым по принципу логического ''ИЛИ'' размещено смещение, второй – это количество элементов в массиве, а третий – это дескриптор заданного типа.
 
* '''Указатель на примитивный тип данных:'''
 
<syntaxhighlight lang="python">
"ptr": (offset | uctypes.PTR, uctypes.UINT8),
"ptr": (offset | uctypes.PTR, uctypes.UINT8),
То есть значение – это 2-элементный кортеж. Его первый элемент – это флаг PTR, рядом с которым по принципу логического ИЛИ размещено смещение, а второй – это идентификатор скалярного типа данных.
</syntaxhighlight>
* Указатель на агрегатный тип данных:
 
То есть значение – это 2-элементный кортеж. Его первый элемент – это флаг ''PTR'', рядом с которым по принципу логического ''ИЛИ'' размещено смещение, а второй – это идентификатор скалярного типа данных.
 
* '''Указатель на агрегатный тип данных:'''
 
<syntaxhighlight lang="python">
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),
То есть значение – это 2-элементный кортеж. Его первый элемент – это флаг PTR, рядом с которым по принципу логического ИЛИ размещено смещение, а второй – это тип дескриптора, на который ссылается указатель.
</syntaxhighlight>
* Битовые поля:
 
То есть значение – это 2-элементный кортеж. Его первый элемент – это флаг ''PTR'', рядом с которым по принципу логического ''ИЛИ'' размещено смещение, а второй – это тип дескриптора, на который ссылается указатель.
 
* '''Битовые поля:'''
 
<syntaxhighlight lang="python">
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,
То есть значение – это идентификатор скалярного типа данных, содержащего заданное битовое поле (названия типов похожи на названия скалярных типов, но с префиксом BF), рядом с которым по принципу логического ИЛИ размещено смещение для скалярного значения, содержащего битовое поле, а также по принципу логического ИЛИ объединенное со значением для позиции бита и длины битового поля (в битах), смещенные битами BF_POS и BF_LEN соответственно. Стартовая позиция битового поля начинается с самого младшего бита скалярного значения (это позиция «0»), и это номер самого правого бита в битовом поле (другими словами, это количество битов, на которое нужно сместить скалярное значение вправо, чтобы извлечь битовое поле).
</syntaxhighlight>
В примере выше сначала будет извлечено UINT16-значение на смещении «0» (эта деталь может быть важна при доступе к аппаратным регистрам, для доступа к которым требуются длина и выравнивание), а затем будут извлечены само битовое поле, чей самый правый бит – это самый младший бит (lsbit) этого UINT16-значения, и его длина (bitsize). Например, если lsbit – это «0», а bitsize – это «8», то фактически это даст доступ к самому младшему байту UINT16-значения.
 
Помните, что операции с битовыми полями не зависят от порядка байтов. В частности, в примере выше доступ к самому младшему байту UINT16-значения будет получен и если порядок в структуре будет «от старшего к младшему», и если он будет «от младшего к старшему». Но важно, чтобы у самого младшего бита был номер «0». У разных значений может быть разная нумерация в их нативных ABI, но в uctypes всегда используется описанная выше нормализированная нумерация.
То есть значение – это идентификатор скалярного типа данных, содержащего заданное битовое поле (названия типов похожи на названия скалярных типов, но с префиксом ''BF''), рядом с которым по принципу логического ''ИЛИ'' размещено смещение для скалярного значения, содержащего битовое поле, а также по принципу логического ''ИЛИ'' объединенное со значением для позиции бита и длины битового поля (в битах), смещенные битами ''BF_POS'' и ''BF_LEN'' соответственно. Стартовая позиция битового поля начинается с самого младшего бита скалярного значения (это позиция ''«0»''), и это номер самого правого бита в битовом поле (другими словами, это количество битов, на которое нужно сместить скалярное значение вправо, чтобы извлечь битовое поле).
Содержимое модуля
 
* Класс uctypes.struct(addr, descriptor, layout_type=NATIVE, /) – инстанцинирует объект «внешней структуры данных» на основе адреса структуры в памяти (addr), дескриптора, зашифрованного в виде словаря (descriptor), и типа макета (layout_type; подробнее читайте ниже).
В примере выше сначала будет извлечено UINT16-значение на смещении ''«0»'' (эта деталь может быть важна при доступе к аппаратным регистрам, для доступа к которым требуются длина и выравнивание), а затем будут извлечены само битовое поле, чей самый правый бит – это самый младший бит (lsbit) этого UINT16-значения, и его длина (bitsize). Например, если lsbit – это «0», а bitsize – это «8», то фактически это даст доступ к самому младшему байту UINT16-значения.
* uctypes.LITTLE_ENDIAN – тип макета для структуры, упакованной с порядком байтов «от младшего к старшему». «Упакованной» означает, что каждое поле занимает именно столько байтов, сколько было задано в дескрипторе, т.е. выравнивание равно «1»).
 
* uctypes.BIG_ENDIAN – тип макета для структуры, упакованной с порядком байтов «от старшего к младшему».
Помните, что операции с битовыми полями не зависят от порядка байтов. В частности, в примере выше доступ к самому младшему байту UINT16-значения будет получен и если порядок в структуре будет ''«от старшего к младшему»'', и если он будет ''«от младшего к старшему»''. Но важно, чтобы у самого младшего бита был номер ''«0»''. У разных значений может быть разная нумерация в их нативных ''ABI'', но в uctypes всегда используется описанная выше нормализированная нумерация.
* 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 сохранит свое изначальное значение.
* [[MicroPython:Библиотеки/uctypes/uctypes.struct()|uctypes.struct()]]
* uctypes.bytearray_at(addr, size) – извлекает данные, находящиеся по адресу addr в памяти и имеющие размер size, и возвращает их в виде объекта bytearray. В отличие от функции bytes_at() выше, извлечение выполняется при помощи указателя, поэтому оба объекта будут изменяемыми. То есть в результате вы всегда получаете доступ к текущему значению, находящемуся в заданной ячейке памяти.
* [[MicroPython:Библиотеки/uctypes/uctypes.LITTLE_ENDIAN|uctypes.LITTLE_ENDIAN]]
* uctypes.UINT8, uctypes.INT8, uctypes.UINT16, uctypes.INT16, uctypes.UINT32, uctypes.INT32, uctypes.UINT64, uctypes.INT64 – типы целых чисел для дескриптора структуры данных. Константы для 8-, 16-, 32- и 64-битных типов данных представлены и в знаковом, и беззнаковом вариантах.
* [[MicroPython:Библиотеки/uctypes/uctypes.BIG_ENDIAN|uctypes.BIG_ENDIAN]]
* uctypes.FLOAT32, uctypes.FLOAT64 – типы чисел с плавающей точкой для дескриптора структуры данных.
* [[MicroPython:Библиотеки/uctypes/uctypes.NATIVE|uctypes.NATIVE]]
* uctypes.VOID – это псевдоним для типа данных UINT8, и он нужен для совместимости с пустыми указателями языка C: uctypes.PTR, uctypes.VOID.
* [[MicroPython:Библиотеки/uctypes/uctypes.sizeof()|uctypes.sizeof()]]
* uctypes.PTR, uctypes.ARRAY – константы для указателей и массивов. Явных констант для структур в модуле uctypes нет, они неявные: агрегатный тип без флагов PTR и ARRAY будет считаться структурой.
* [[MicroPython:Библиотеки/uctypes/uctypes.addressof()|uctypes.addressof()]]
Дескрипторы структуры данных и инстанцинирование объекта структуры
* [[MicroPython:Библиотеки/uctypes/uctypes.bytes_at()|uctypes.bytes_at()]]
* [[MicroPython:Библиотеки/uctypes/uctypes.bytearray_at()|uctypes.bytearray_at()]]
* [[MicroPython:Библиотеки/uctypes/uctypes.UINT8|uctypes.UINT8]]
* [[MicroPython:Библиотеки/uctypes/uctypes.INT8|uctypes.INT8]]
* [[MicroPython:Библиотеки/uctypes/uctypes.UINT16|uctypes.UINT16]]
* [[MicroPython:Библиотеки/uctypes/uctypes.INT16|uctypes.INT16]]
* [[MicroPython:Библиотеки/uctypes/uctypes.UINT32|uctypes.UINT32]]
* [[MicroPython:Библиотеки/uctypes/uctypes.INT32|uctypes.INT32]]
* [[MicroPython:Библиотеки/uctypes/uctypes.UINT64|uctypes.UINT64]]
* [[MicroPython:Библиотеки/uctypes/uctypes.INT64|uctypes.INT64]]
* [[MicroPython:Библиотеки/uctypes/uctypes.FLOAT32|uctypes.FLOAT32]]
* [[MicroPython:Библиотеки/uctypes/uctypes.FLOAT64|uctypes.FLOAT64]]
* [[MicroPython:Библиотеки/uctypes/uctypes.VOID|uctypes.VOID]]
* [[MicroPython:Библиотеки/uctypes/uctypes.PTR|uctypes.PTR]]
* [[MicroPython:Библиотеки/uctypes/uctypes.ARRAY|uctypes.ARRAY]]
 
== Дескрипторы структуры данных и инстанцинирование объекта структуры ==
 
Задав в конструкторе uctypes.struct() словарь с дескриптором структуры и тип макета, вы можете инстанцинировать экземпляр нужной вам структуры в нужном адресе памяти. Адрес в памяти обычно можно узнать следующим образом:
Задав в конструкторе uctypes.struct() словарь с дескриптором структуры и тип макета, вы можете инстанцинировать экземпляр нужной вам структуры в нужном адресе памяти. Адрес в памяти обычно можно узнать следующим образом:
* Это может быть адрес, заданный заранее – если речь об аппаратных регистрах на «голой» системе. Ищите эти адреса в документации к своему микроконтроллеру/SoC.
* Это может быть адрес, заданный заранее – если речь об аппаратных регистрах на ''«голой»'' системе. Ищите эти адреса в документации к своему микроконтроллеру/[[SoC]].
* При помощи некоторых FFI-функций (от англ. «foreign function interface», т.е. речь о функциях, служащих интерфейсом для внешних функций).
* При помощи некоторых [[FFI-функций]] (от англ. ''«foreign function interface»'', т.е. речь о функциях, служащих интерфейсом для внешних функций).
* При помощи uctypes.addressof() – когда вам нужно передать аргументы FFI-функции или получить доступ к некоторым данным для I/O-операций (к примеру, для чтения данных из файла или сетевого сокета).
* При помощи uctypes.addressof() – когда вам нужно передать аргументы [[FFI-функции]] или получить доступ к некоторым данным для I/O-операций (к примеру, для чтения данных из файла или [[сетевого сокета]]).
Объекты структуры данных
 
Объекты структуры данных позволяют получить доступ к отдельным полям при помощи стандартного точечного синтаксиса: my_struct.substruct1.field1. Если это поле скалярного типа, то извлечение данных из него сгенерирует примитивное значение (Python’овское целое число или число с плавающей точкой), соответствующее значению, содержащемуся в поле. Скалярное поле также можно присвоить.  
== Объекты структуры данных ==
Если поле – это массив, доступ к его отдельным элементам можно получить при помощи стандартного оператора индексирования [] – и для чтения, и для присвоения.
 
Если поле – это указатель, то его можно разыменовать при помощи синтаксиса [0] (это соответствует C-оператору *, но [0] работает и в C тоже). Индексирование указателей при помощи целого числа (кроме «0») тоже поддерживается – при помощи той же семантики, что и в C.
Объекты структуры данных позволяют получить доступ к отдельным полям при помощи стандартного точечного синтаксиса: ''my_struct.substruct1.field1''. Если это поле скалярного типа, то извлечение данных из него сгенерирует примитивное значение ([[Python]]’овское целое число или число с плавающей точкой), соответствующее значению, содержащемуся в поле. Скалярное поле также можно присвоить.  
Подытоживая, доступ к полям структуры данных в целом повторяет синтаксис языка 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.  
Если поле – это указатель, то его можно разыменовать при помощи синтаксиса '''[0]''' (это соответствует [[C-оператор]]у '''*''', но '''[0]''' работает и в [[C]] тоже). Индексирование указателей при помощи целого числа (кроме [[«0»]]) тоже поддерживается – при помощи той же семантики, что и в [[C]].
* Избегайте других нескалярных данных вроде массивов. Например, вместо peripheral_a.register[0] используйте peripheral_a.register0. Опять же, в качестве альтернативы можно закэшировать промежуточные значения, например, register0 = peripheral_a.register[0].
 
2. Диапазон смещений, поддерживаемых модулем uctypes, ограничен. В принципе, этот диапазон зависит от используемой реализации, но в целом можно посоветовать разбивать структуры таким образом, чтобы уложиться в диапазон между несколькими килобайтами и несколькими десятками килобайт. В принципе, в большинстве случаев это нормальная ситуация. Например, вам нет смысла задавать в одной структуре все регистры микроконтроллера (простирающиеся по всему 32-битному адресному пространству) – обычно вам нужны лишь регистры используемых периферийных компонентов. В крайнем случае вам может понадобиться специально разбить структуру на несколько частей (например, чтобы получить доступ к нативной структуре данных с много-мегабайтным массивом посередине, но это очень гипотетическая ситуация).
Подытоживая, доступ к полям структуры данных в целом повторяет синтаксис языка [[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">
<syntaxhighlight lang="python">


=См.также=
=См.также=

Текущая версия от 18:17, 14 мая 2023

Перевод: Максим Кузьмин
Проверка/Оформление/Редактирование: Мякишев Е.А.


Модуль 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">

См.также

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