uctypes --- 以結構化的方式存取二進位資料

本模組為 MicroPython 實作了「外部資料介面」(foreign data interface)。其背後的構想與 CPython 的 ctypes 模組類似,但實際的 API 並不相同,而是經過精簡並針對小體積進行最佳化。本模組的基本概念是以與 C 語言大致相同的表達能力來定義資料結構的版面配置,然後使用熟悉的點記法(dot-syntax)來參考子欄位以存取資料。

警告

uctypes 模組可存取機器的任意記憶體位址(包括 I/O 與控制暫存器)。使用不慎可能導致當機、資料遺失,甚至硬體故障。

請參閱

struct 模組

用於封裝與解封裝二進位資料的標準 Python 模組。struct 一次操作整個緩衝區,使用精簡的格式字串(例如 '<HBB4sI'),這對於少數固定欄位運作良好,但難以擴展到大型或深層巢狀的結構:每次讀寫都會重新解析格式字串,不支援聯集(union)與位元欄位(bitfield),也無法對既有緩衝區取得具型別的檢視。uctypes 補足了 struct 的不足,讓你只需描述一次版面配置,將其附加到某個記憶體區域(RAM、周邊暫存器、bytearray),即可將個別欄位當作具名屬性來存取——避免重複解析與複製,並新增對巢狀結構、陣列、聯集與位元欄位的支援。

使用範例::

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)

定義結構版面配置

結構版面配置由一個「描述子」(descriptor)來定義——這是一個 Python 字典,以欄位名稱作為鍵,並以存取這些欄位所需的其他屬性作為對應的值::

{
    "field1": <properties>,
    "field2": <properties>,
    ...
}

目前 uctypes 要求明確指定每個欄位的偏移量(offset)。偏移量以從結構起始處算起的位元組數給出。

以下是各種欄位型別的編碼範例:

  • 純量型別(Scalar types)::

    "field_name": offset | uctypes.UINT32
    

    換言之,其值是一個純量型別識別碼與欄位偏移量(以位元組計,從結構起始處算起)進行 OR 運算的結果。

  • 遞迴結構::

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

    亦即其值是一個 2-tuple,第一個元素為偏移量,第二個元素為一個結構描述子字典(注意:遞迴描述子中的偏移量是相對於它所定義的結構而言)。當然,遞迴結構不僅可以用字面的字典指定,也可以透過名稱參考(先前定義的)結構描述子字典。

  • 原始型別的陣列::

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

    亦即其值是一個 2-tuple,第一個元素為 ARRAY 旗標與偏移量進行 OR 運算的結果,第二個元素為純量元素型別與陣列中元素數量進行 OR 運算的結果。

  • 彙總型別(aggregate type)的陣列::

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

    亦即其值是一個 3-tuple,第一個元素為 ARRAY 旗標與偏移量進行 OR 運算的結果,第二個元素為陣列中的元素數量,第三個元素為元素型別的描述子。

  • 指向原始型別的指標::

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

    亦即其值是一個 2-tuple,第一個元素為 PTR 旗標與偏移量進行 OR 運算的結果,第二個元素為純量元素型別。

  • 指向彙總型別的指標::

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

    亦即其值是一個 2-tuple,第一個元素為 PTR 旗標與偏移量進行 OR 運算的結果,第二個元素為所指向型別的描述子。

  • 位元欄位(Bitfields)::

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

    亦即其值是包含指定位元欄位的純量值型別(型別名稱與純量型別類似,但帶有 BF 前綴),與包含該位元欄位之純量值的偏移量進行 OR 運算,再進一步與該位元欄位在純量值內的位元位置(bit position)與位元長度(bit length)值進行 OR 運算,這兩個值分別左移 BF_POS 與 BF_LEN 個位元。位元欄位的位置是從純量的最低有效位元(最低有效位元的位置為 0)開始計數,並指該欄位最右側位元的編號(換言之,它是為了取出該位元欄位而需將純量向右移位的位元數)。

    在上述範例中,首先會在偏移量 0 處取出一個 UINT16 值(在存取硬體暫存器時,這個細節可能很重要,因為這類暫存器要求特定的存取大小與對齊),然後再取出一個位元欄位,其最右側位元為此 UINT16 的第 lsbit 個位元,長度為 bitsize 個位元。例如,若 lsbit 為 0 且 bitsize 為 8,則實際上會存取 UINT16 的最低有效位元組。

    請注意,位元欄位的運算與目標的位元組位元組順序(byte endianness)無關,特別是上述範例在小端序與大端序結構中都會存取 UINT16 的最低有效位元組。但它取決於最低有效位元被編號為 0。某些目標在其原生 ABI 中可能採用不同的編號方式,但 uctypes 一律使用上述的標準化編號。

模組內容

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

根據結構在記憶體中的位址、描述子(以字典編碼)以及版面配置型別(見下文),實例化一個「外部資料結構」物件。

uctypes.LITTLE_ENDIAN: int

小端序緊湊結構(little-endian packed structure)的版面配置型別。(緊湊意指每個欄位所佔用的位元組數恰好等於描述子中所定義的數目,亦即對齊為 1)。

uctypes.BIG_ENDIAN: int

大端序緊湊結構的版面配置型別。

uctypes.NATIVE: int

原生結構(native structure)的版面配置型別——其資料的位元組順序與對齊方式遵循 MicroPython 執行所在系統的 ABI。

uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int

傳回資料結構的大小(以位元組計)。struct 引數可以是結構類別,也可以是已實例化的特定結構物件(或其彙總欄位)。

uctypes.addressof(obj: Any) int

傳回某個物件的位址。引數應為 bytes、bytearray 或其他支援緩衝區協定的物件(實際傳回的是該緩衝區的位址)。

uctypes.bytes_at(addr: int, size: int) bytes

將指定位址與大小處的記憶體擷取為 bytes 物件。由於 bytes 物件是不可變的,記憶體實際上會被複製並拷貝進 bytes 物件中,因此若記憶體內容稍後變更,所建立的物件仍保留原始的值。

uctypes.bytearray_at(addr: int, size: int) bytearray

將指定位址與大小處的記憶體擷取為 bytearray 物件。與上述的 bytes_at() 函式不同,記憶體是以參考方式擷取的,因此既可寫入,也可存取到指定記憶體位址處的當前值。

純量整數型別。每種型別佔用相應的位元組數(1248),並使用結構版面配置型別的位元組順序(NATIVELITTLE_ENDIANBIG_ENDIAN 之一)進行讀寫。

uctypes.UINT8: int

無號 8 位元整數。範圍 0 -- 255

uctypes.INT8: int

有號 8 位元整數。範圍 -128 -- 127

uctypes.UINT16: int

無號 16 位元整數。範圍 0 -- 65535

uctypes.INT16: int

有號 16 位元整數。範圍 -32768 -- 32767

uctypes.UINT32: int

無號 32 位元整數。範圍 0 -- 0xFFFFFFFF

uctypes.INT32: int

有號 32 位元整數。範圍 -0x80000000 -- 0x7FFFFFFF

uctypes.UINT64: int

無號 64 位元整數。範圍 0 -- 0xFFFFFFFFFFFFFFFF

uctypes.INT64: int

有號 64 位元整數。範圍 -0x8000000000000000 -- 0x7FFFFFFFFFFFFFFF

uctypes.FLOAT32: int

IEEE 754 單精度浮點數(4 位元組)。讀寫時會與 Python 的 float 互相轉換。

uctypes.FLOAT64: int

IEEE 754 雙精度浮點數(8 位元組)。讀寫時會與 Python 的 float 互相轉換。

uctypes.VOID: int

UINT8 的別名。提供此別名是為了讓 C 風格的 void * 欄位能以慣用的方式描述為 (uctypes.PTR, uctypes.VOID)

uctypes.PTR: int

將描述子欄位標記為指向另一型別的指標。指標欄位寫成一個 two-tuple (offset | PTR, target_type_or_descriptor)。對指標解參考會產生對其所持位址的具型別檢視。

uctypes.ARRAY: int

將描述子欄位標記為另一型別的固定長度陣列。陣列欄位對純量陣列寫成 (offset | ARRAY, count | element_type),對結構陣列寫成 (offset | ARRAY, count, element_descriptor)。元素數量在定義描述子時即固定。

結構沒有明確的常數:一個既不使用 PTR 也不使用 ARRAY 的彙總描述子會被視為結構。

結構描述子與實例化結構物件

給定一個結構描述子字典及其版面配置型別,你可以使用 uctypes.struct() 建構子在指定的記憶體位址實例化一個特定的結構實例。記憶體位址通常來自下列來源:

  • 預先定義的位址,用於在裸機(baremetal)系統上存取硬體暫存器。可在特定 MCU/SoC 的資料手冊中查詢這些位址。

  • 作為某個 FFI(Foreign Function Interface,外部函式介面)函式呼叫的傳回值。

  • 來自 uctypes.addressof(),當你想將引數傳給 FFI 函式時,或者用於存取某些 I/O 資料(例如從檔案或網路 socket 讀取的資料)。

結構物件

結構物件可使用標準的點記法存取個別欄位:my_struct.substruct1.field1。若某欄位為純量型別,取得它會產生對應於該欄位所含值的原始值(Python 整數或浮點數)。純量欄位也可以被指派值。

若某欄位為陣列,可使用標準的下標運算子 [] 存取其個別元素——可讀取也可指派值。

若某欄位為指標,可使用 [0] 語法對其解參考(對應於 C 的 * 運算子,不過 [0] 在 C 中同樣有效)。也支援以 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).