uctypes
– 以结构化的方式访问二进制数据¶
该模块实现MicroPython的“外部数据接口”。其背后的设想与CPython的 ctypes
模块相似,
但是实际的API与之不同,简化并针对小规模进行了优化。该模块的基本设想是定义数据结构布局,
其功能与C语言所允许大致相同,并使用熟悉的点语法来引用子字段。
警告
uctypes
模块允许访问机器的任意内存地址(包括I/O和控制寄存器)。不小心使用它可能会导致崩溃、数据丢失,甚至硬件故障。
参见
- Module
ustruct
- 访问二进制数据结构的标准Python方法(不太适合大而复杂的结构)。
Usage examples:
import uctypes
# Example 1: Subset of ELF file header 示例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" is an ELF file opened in binary mode “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, "Oops, wrong endianness. Could retry with uctypes.BIG_ENDIAN."
print("machine:", hex(header.e_machine))
# Example 2: In-memory data structure, with pointers 例2:内存中的数据结构,带有指针
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" 假设在addr中有一个结构类型为STRUCT1的地址
# uctypes.NATIVE is optional (used by default) uctypes。NATIVE是可选的(默认使用)
struct1 = uctypes.struct(addr, STRUCT1, uctypes.NATIVE)
print("x:", struct1.ptr[0].x)
# Example 3: Access to CPU registers. Subset of STM32F4xx WWDG block 例3:访问CPU寄存器。STM32F4xx WWDG块的子集
WWDG_LAYOUT = {
"WWDG_CR": (0, {
# BFUINT32 here means size of the WWDG_CR register 这里的FUINT32表示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("Current counter:", WWDG.WWDG_CR.T)
定义结构布局¶
结构布局是由”descriptor”定义的——Python字典,该字典将字段名称编码为键,并将访问它们所需的其他属性编码为关联值:
{
"field1": <properties>,
"field2": <properties>,
...
}
目前,uctypes
要求明确指定每个字段的偏移量。偏移量从结构开始以字节为单位给出。
以下为不同字段类型的编码示例:
标量类型:
"field_name": offset | uctypes.UINT32
换言之,值为从结构起始处的字段偏移量(以字节为单位)与标量类型标识符ORed进行或运算。
递归布局:
- “sub”: (offset, {
“b0”: 0 | uctypes.UINT8, “b1”: 1 | uctypes.UINT8,})
即:值为一个2元组,元组中第一项为偏移量,第二项为结构描述符词典(注意:递归描述符中的偏移量与其定义的结构相对)。
当然,不仅可以通过文字字典指定递归结构,还可以通过名称引用结构描述符字典(在前面定义)来指定。
原始类型数组:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),
即:值为一个2元组,元组中第一项为ARRAY标记与偏移量进行或运算,第二项为数组元素的标量元素类型ORed数。
集合类型的数组:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),
即:值为一个3元组,元组中第一项为ARRAY标记与偏移量进行或运算,第二项为数组中元素的数量,第三项为元素类型的描述符。
指向原始类型的指针:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),
即:值为一个2元组,元组中第一项为PTR标记与偏移量进行或运算,第二项为标量元素类型。
指向集合类型的指针:
"ptr2": (uctypes.PTR | 0, {"b": uctypes.UINT8 | 0}),
即:值为一个2元组,元组中第一项为PTR标记与偏移量进行或运算,第二项为指向的描述符类型。
位字段:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,
即:值为包含给定位字段的标量值的类型(类型名称与标量类型相似,但是带有“BF”前缀),与包含位字段的标量值进行或运算, 并与位偏移量的值和标量值内的位字段的位长度(分别由BF_POS和BF_LEN位置转换而来)进行或运算。 位字段位置是从最低有效位开始计数,且为字段的最右位的数字(换言之,标量需右移至额外位字段,位字段位置即为右移的位数)。
在以上示例中,第一个UINT16值将在偏移量0处提取(访问硬件寄存器时,即需特定的访问大小和对齐,此细节便不容忽视), 位域的最右位是该UINT16的最低位,其长度是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)¶ 返回以字节为单位的数据结构的大小。参数可为结构类或特定实例化结构对象(或其聚合字段)。
-
uctypes.
addressof
(obj)¶ 返回对象的地址。参数应为字节、字节数组或其他支持缓冲区协议(返回的即为此缓冲区的地址)。
-
uctypes.
bytes_at
(addr, size)¶ 在给定地址和以给定大小捕捉内存为字节对象。因为字节对象为可变的,内存实际上被复制到字节对象中,所以内存内容稍后有所更改,被创建的对象保留初始值。
-
uctypes.
bytearray_at
(addr, size)¶ 在给定地址和以给定大小捕捉内存为字节数组对象。与上述bytes_at()函数不同,内存是通过引用捕获的,因此其也可被写入。您将在给定内存地址访问当前值。
-
uctypes.
UINT8
¶ -
uctypes.
INT8
¶ -
uctypes.
UINT16
¶ -
uctypes.
INT16
¶ -
uctypes.
UINT32
¶ -
uctypes.
INT32
¶ -
uctypes.
UINT64
¶ -
uctypes.
INT64
¶ 结构描述符的整数类型。提供了8、16、32和64位类型的常量,包括有符号的和无符号的。
-
uctypes.
VOID
¶ VOID
是UINT8
的别名,用于方便地定义C的VOID指针:(uctypes.PTR, uctypes.VOID)
。
结构描述符和实例化结构对象¶
给定一个结构描述符和其层次类型,您可在给定内存地址使用 uctypes.struct()
实例化一个特定结构实例。内存地址通常来自以下来源:
- 预定义的地址,当在baremental系统中访问硬件寄存器时。在关于特定MCU/SoC的数据表中查找这些地址。
- 作为从FFI函数(外来函数接口)调用返回的值。
- 从uctypes.addressof()中,当您要将参数传递给FFI函数时,或者为I/O访问某些数据(例如,从文件中或网络socket中读取的数据)。
结构对象¶
结构对象允许使用标准点记法访问单个字段: my_struct.substruct1.field1
。
若一字段为标量类型的,获取其将生成一个与字段中包含的值相对应的初始值(Python整数或浮动值)。一个标量字段也可被赋值。
若字段为一个数组,则可使用标准下标运算符 []
访问其单个元素—可被读取或赋值。
若字段为一个指针,则可使用 [0]
语法(与C *
运算符相对应,尽管 [0]
也在C中运行)来消除引用。
使用其他整数值对指针进行下标(也支持0),与C中具有相同语义。
总而言之,当您需使用 [0]
运算符而非 *
时,除指针解除引用外,访问结构字段一般遵循C语法。
局限性¶
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.register0
而非peripheral_a.register[0]
。 另一种方法是缓存中间值,例如register0 = peripheral_a.register[0]
。
2.``uctypes``模块支持的偏移范围有限。支持的确切范围被认为是实现细节,一般的建议是将结构定义从几kb扩展到几十kb。 在大多数情况下,这是一种自然的情况,例如,在一个结构中定义一个MCU的所有寄存器(分布在32位地址空间中)是没有意义的,而是一个外围块接一个外围块地定义。 在某些极端情况下,可能需要将结构人为地分成几个部分(例如,如果访问中间有几兆字节数组的本机数据结构,尽管这是非常综合的情况)。