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

该模块为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
    

    换句话说,值是标量类型标识符,与从结构开始的字段偏移量(以字节为单位)进行OR运算。

  • 递归结构:

    "sub": (offset, {
        "b0": 0 | uctypes.UINT8,
        "b1": 1 | uctypes.UINT8,
    })
    

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

  • 原始类型数组:

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

    例如,值是一个二元组,其第一个元素是ARRAY标志与偏移量进行OR运算,第二个是标量元素类型和数组中的元素数进行OR运算。

  • 聚合类型数组:

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

    即,值是一个三元组,其第一个元素是ARRAY标志与偏移量进行OR运算,第二个是数组中的元素数,第三个是元素类型的描述符。

  • 指向原始类型的指针:

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

    即,值是一个二元组,其第一个元素是PTR标志与偏移量进行OR运算,第二个是标量元素类型。

  • 指向聚合类型的指针:

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

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

  • 位域:

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

    即,值是包含给定位域的标量值的类型(类型名称类似于标量类型,但前缀为 BF),与包含位域的标量值的偏移量进行OR运算,并且进一步与位域在标量值中的位位置和位长度的值进行OR运算,分别按 BF_POS 和 BF_LEN 位移动。位域位置从标量的最低有效位(位置为0)开始计数,并且是标量值中字段的最右位(换句话说,它是标量需要右移的位数以提取位域的位数)。

    在上面的示例中,首先将在偏移量为0的位置提取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, /)

返回数据结构的字节数。 struct 参数可以是结构类或特定实例化的结构对象(或其聚合字段)。

uctypes.addressof(obj)

返回对象的地址。参数应为bytes、bytearray或支持缓冲区协议的其他对象(实际返回的是该缓冲区的地址)。

uctypes.bytes_at(addr, size)

将给定地址和大小的内存捕获为字节对象。由于字节对象是不可变的,因此实际上会复制并复制内存到字节对象,因此如果稍后更改了内存内容,则创建的对象保留原始值。

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

VOIDUINT8 的别名,提供了方便地定义C的void指针的方法: (uctypes.PTR, uctypes.VOID)

uctypes.PTR
uctypes.ARRAY

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

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

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

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

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

  • uctypes.addressof() ,当您希望向FFI函数传递参数时,或者用于访问一些I/O数据(例如,从文件或网络套接字读取的数据)。

结构对象

结构对象允许使用标准点表示法访问各个字段:my_struct.substruct1.field1。如果字段是标量类型,则获取它将产生与字段中包含的值对应的原始值(Python整数或浮点数)。也可以对标量字段进行赋值。

如果字段是数组,则可以使用标准下标操作符 [] 来访问其各个元素 - 读取和分配都可以。

如果字段是指针,则可以使用 [0] 语法对其进行解引用(对应于C中的 * 运算符,尽管在C中也可以使用 [0])。还支持使用除0以外的其他整数值对指针进行下标操作,其语义与C中相同。

总之,访问结构字段通常遵循C语法,但在解引用指针时,需要使用 [0] 运算符而不是 *

限制

1. Accessing non-scalar fields leads to allocation of intermediate objects to represent them. This means that special care should be taken to layout a structure which needs to be accessed when memory allocation is disabled (e.g. from an interrupt). The recommendations are:

  • 避免访问嵌套结构。例如,不要使用 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. Range of offsets supported by the uctypes module is limited. The exact range supported is considered an implementation detail, and the general suggestion is to split structure definitions to cover from a few kilobytes to a few dozen of kilobytes maximum. In most cases, this is a natural situation anyway, e.g. it doesn’t make sense to define all registers of an MCU (spread over 32-bit address space) in one structure, but rather a peripheral block by peripheral block. In some extreme cases, you may need to split a structure in several parts artificially (e.g. if accessing native data structure with multi-megabyte array in the middle, though that would be a very synthetic case).