uctypes – 以结构化的方式访问二进制数据

警告

虽然这个基于 MicroPython 的库可能可用于某些 CircuitPython 版本,但它不受支持,并且其功能在未来可能会发生重大变化。随着 CircuitPython 的不断发展,它可能会更改以更紧密地符合相应的标准 Python 库。如果您依赖它当前提供的任何非标准功能,您以后可能需要更改您的代码。

该模块为 MicroPython 实现了“外部数据接口”。它背后的想法类似于 CPython 的ctypes 模块,但实际的 API 不同,针对小尺寸进行了精简和优化。该模块的基本思想是定义数据结构布局,其功能与 C 语言允许的功能大致相同,然后使用熟悉的点语法访问它以引用子字段。

警告

uctypes 模块允许访问机器的任意内存地址(包括 I/O 和控制寄存器)。不小心使用它可能会导致崩溃、数据丢失,甚至硬件故障。

也可以看看

模块 struct

访问二进制数据结构的标准 Python 方式(不能很好地扩展到大型和复杂的结构)。

用法示例:

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,
    })
    

    即值是一个二元组,第一个元素是一个偏移量,第二个元素是一个结构描述符字典(注意:递归描述符中的偏移量是相对于它定义的结构)。当然,递归结构不仅可以通过文字字典来指定,还可以通过名称引用结构描述符字典(之前定义的)来指定。

  • 原始类型数组:

    "arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),
    

    即 value 是一个 2 元组,其中第一个元素是 ARRAY 标志与偏移量的 ORed,第二个元素是标量元素类型 ORed 数组中元素的数量。

  • 聚合类型的数组:

    "arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),
    

    即 value 是一个 3 元组,其中第一个元素是 ARRAY 标志与偏移量的 ORed,第二个是数组中的元素数量,第三个是元素类型的描述符。

  • 指向原始类型的指针:

    "ptr": (offset | uctypes.PTR, uctypes.UINT8),
    

    即 value 是一个 2 元组,其中第一个元素是 PTR 标志与偏移量的 ORed,第二个元素是标量元素类型。

  • 指向聚合类型的指针:

    "ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),
    

    即 value 是一个 2 元组,其中第一个元素是 PTR 标志与偏移量进行或运算,第二个元素是指向的类型的描述符。

  • 位域:

    "bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,
    

    即 value 是一种包含给定位域的标量值类型(类型名称类似于标量类型,但前缀BF为标量值,分别移位 BF_POS 和 BF_LEN 位。位域位置是从标量的最低有效位(位置为 0)开始计算的,并且是字段最右边的位数(换句话说,它是标量需要右移的位数)提取位域)。

    在上面的示例中,首先将在偏移量 0 处提取 UINT16 值(此细节在访问硬件寄存器时可能很重要,其中需要特定的访问大小和对齐方式),然后是该 UINT16的最右边位是lsbit位的位域,以及长度是bitsize位,将被提取。例如,如果lsbit为 0 且 bitsize为 8,那么它将有效地访问 UINT16 的最低有效字节。

    请注意,位域操作与目标字节顺序无关,特别是,上面的示例将访问小端和大端结构中 UINT16 的最低有效字节。但这取决于编号为 0 的最低有效位。某些目标可能在其本机 ABI 中使用不同的编号,但uctypes始终使用上述规范化编号。

模块内容

class uctypes.struct(addr, descriptor, layout_type=NATIVE, /)

根据内存中的结构地址、描述符(编码为字典)和布局类型(见下文)实例化“外部数据结构”对象。

uctypes.LITTLE_ENDIAN

小端打包结构的布局类型。(打包意味着每个字段占用的字节数与描述符中定义的字节数完全相同,即对齐为 1)。

uctypes.BIG_ENDIAN

大端打包结构的布局类型。

uctypes.NATIVE

本机结构的布局类型 - 数据字节序和对齐符合 MicroPython 运行的系统的 ABI。

uctypes.sizeof(struct, layout_type=NATIVE, /)

以字节为单位返回数据结构的大小。的结构参数可以是一个类结构或特定实例化结构对象(或其聚集体字段)。

uctypes.addressof(obj)

对象的返回地址。参数应该是字节、字节数组或其他支持缓冲区协议的对象(这个缓冲区的地址是实际返回的地址)。

uctypes.bytes_at(addr, size)

将给定地址和大小的内存捕获为字节对象。由于 bytes 对象是不可变的,内存实际上是被复制并复制到 bytes 对象中的,所以如果以后内存内容发生变化,创建的对象会保留原始值。

uctypes.bytearray_at(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

VOID 是 的别名UINT8用于方便地定义 C 的空指针:。(uctypes.PTR, uctypes.VOID).

uctypes.PTR
uctypes.ARRAY

为指针和数组键入常量。请注意,结构没有显式常量,它是隐式的:没有PTRARRAY标志的聚合类型是结构。

结构描述符和实例化结构对象

给定一个结构描述符字典及其布局类型,您可以使用uctypes.struct() 构造函数在给定的内存地址处实例化一个特定的结构实例。内存地址通常来自以下来源:

  • 访问裸机系统上的硬件寄存器时的预定义地址。在特定 MCU/SoC 的数据表中查找这些地址。

  • 作为调用某些 FFI(外函数接口)函数的返回值。

  • From 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 模块支持的偏移量范围有限。支持的确切范围被认为是一个实现细节,一般建议是拆分结构定义以涵盖从几千字节到几十千字节的最大值。在大多数情况下,无论如何,这是一种自然情况,例如,在一个结构中定义 MCU 的所有寄存器(分布在 32 位地址空间)是没有意义的,而是逐个外设块定义外设块。在某些极端情况下,您可能需要人为地将一个结构分成几个部分(例如,如果访问中间带有多兆字节数组的本机数据结构,尽管这将是一个非常综合的情况)。