uctypes --- truy cập dữ liệu nhị phân theo cấu trúc¶
Mô-đun này triển khai "giao diện dữ liệu nước ngoài" cho MicroPython. Ý tưởng đằng sau nó tương tự như mô-đun ctypes của CPython, nhưng API thực tế khác nhau, được tinh giản và tối ưu hóa cho kích thước nhỏ. Ý tưởng cơ bản của mô-đun là định nghĩa bố cục cấu trúc dữ liệu với khả năng tương đương ngôn ngữ C, sau đó truy cập nó bằng cú pháp dấu chấm quen thuộc để tham chiếu các trường con.
Cảnh báo
Mô-đun uctypes cho phép truy cập các địa chỉ bộ nhớ tùy ý của máy (bao gồm cả các thanh ghi I/O và điều khiển). Sử dụng không cẩn thận có thể dẫn đến lỗi sập, mất dữ liệu, thậm chí hỏng phần cứng.
Xem thêm
- Mô-đun
struct Mô-đun Python chuẩn để đóng gói và giải nén dữ liệu nhị phân.
structhoạt động trên toàn bộ bộ đệm cùng một lúc bằng chuỗi định dạng nhỏ gọn (ví dụ'<HBB4sI'), hoạt động tốt cho một vài trường cố định nhưng mở rộng kém với các cấu trúc lớn hoặc lồng nhau sâu: mỗi lần đọc hoặc ghi đều phải phân tích lại chuỗi định dạng, union và bitfield không được hỗ trợ, và không có cách nào để lấy chế độ xem kiểu vào bộ đệm hiện có.uctypesbổ sung chostructbằng cách cho phép bạn mô tả bố cục một lần, gắn nó vào một vùng bộ nhớ (RAM, các thanh ghi ngoại vi, mộtbytearray) rồi truy cập các trường riêng lẻ dưới dạng các thuộc tính được đặt tên -- tránh việc phân tích và sao chép lặp đi lặp lại, đồng thời thêm hỗ trợ cho struct lồng nhau, mảng, union và bitfield.
Ví dụ sử dụng:
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)
Định nghĩa bố cục cấu trúc¶
Bố cục cấu trúc được định nghĩa bởi một "bộ mô tả" - một từ điển Python mã hóa tên trường làm khóa và các thuộc tính khác cần thiết để truy cập chúng làm giá trị liên kết:
{
"field1": <properties>,
"field2": <properties>,
...
}
Hiện tại, uctypes yêu cầu chỉ định rõ ràng offset cho mỗi trường. Offset được tính bằng byte từ đầu cấu trúc.
Sau đây là các ví dụ mã hóa cho các kiểu trường khác nhau:
Kiểu vô hướng:
"field_name": offset | uctypes.UINT32nói cách khác, giá trị là một định danh kiểu vô hướng được OR với offset của trường (tính bằng byte) từ đầu cấu trúc.
Cấu trúc đệ quy:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
tức là giá trị là một 2-tuple, phần tử đầu tiên là offset, phần tử thứ hai là từ điển bộ mô tả cấu trúc (lưu ý: offset trong các bộ mô tả đệ quy là tương đối so với cấu trúc mà nó định nghĩa). Tất nhiên, các cấu trúc đệ quy có thể được chỉ định không chỉ bằng từ điển literal mà còn bằng cách tham chiếu đến từ điển bộ mô tả cấu trúc (đã định nghĩa trước đó) theo tên.
Mảng của kiểu nguyên thủy:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),tức là giá trị là một 2-tuple, phần tử đầu tiên là cờ ARRAY được OR với offset, và phần tử thứ hai là kiểu phần tử vô hướng được OR với số phần tử trong mảng.
Mảng của kiểu tổng hợp:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),tức là giá trị là một 3-tuple, phần tử đầu tiên là cờ ARRAY được OR với offset, phần tử thứ hai là số phần tử trong mảng, và phần tử thứ ba là bộ mô tả của kiểu phần tử.
Con trỏ tới kiểu nguyên thủy:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),tức là giá trị là một 2-tuple, phần tử đầu tiên là cờ PTR được OR với offset, và phần tử thứ hai là kiểu phần tử vô hướng.
Con trỏ tới kiểu tổng hợp:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),tức là giá trị là một 2-tuple, phần tử đầu tiên là cờ PTR được OR với offset, phần tử thứ hai là bộ mô tả của kiểu được trỏ đến.
Bitfield:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,tức là giá trị là kiểu của giá trị vô hướng chứa bitfield đã cho (tên kiểu tương tự như kiểu vô hướng, nhưng có tiền tố là
BF), được OR với offset cho giá trị vô hướng chứa bitfield, và tiếp tục OR với các giá trị cho vị trí bit và độ dài bit của bitfield trong giá trị vô hướng, được dịch chuyển lần lượt bởi BF_POS và BF_LEN bit. Vị trí bitfield được tính từ bit có trọng số thấp nhất của vô hướng (có vị trí 0), và là số của bit ngoài cùng bên phải của trường (nói cách khác, đó là số bit mà vô hướng cần được dịch phải để trích xuất bitfield).Trong ví dụ trên, đầu tiên một giá trị UINT16 sẽ được trích xuất tại offset 0 (chi tiết này có thể quan trọng khi truy cập các thanh ghi phần cứng, nơi kích thước truy cập và căn chỉnh cụ thể được yêu cầu), sau đó bitfield có bit ngoài cùng bên phải là bit lsbit của UINT16 này, và độ dài là bitsize bit, sẽ được trích xuất. Ví dụ, nếu lsbit là 0 và bitsize là 8, thì thực tế nó sẽ truy cập byte ít quan trọng nhất của UINT16.
Lưu ý rằng các thao tác bitfield độc lập với thứ tự byte đích, đặc biệt, ví dụ trên sẽ truy cập byte ít quan trọng nhất của UINT16 trong cả cấu trúc little-endian và big-endian. Nhưng nó phụ thuộc vào bit ít quan trọng nhất được đánh số là 0. Một số đích có thể sử dụng cách đánh số khác trong ABI gốc của chúng, nhưng
uctypesluôn sử dụng cách đánh số chuẩn hóa được mô tả ở trên.
Nội dung mô-đun¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
Khởi tạo đối tượng "cấu trúc dữ liệu nước ngoài" dựa trên địa chỉ cấu trúc trong bộ nhớ, bộ mô tả (được mã hóa dưới dạng từ điển), và kiểu bố cục (xem bên dưới).
- uctypes.LITTLE_ENDIAN: int¶
Kiểu bố cục cho cấu trúc đóng gói little-endian. (Đóng gói có nghĩa là mỗi trường chiếm đúng số byte được định nghĩa trong bộ mô tả, tức là căn chỉnh là 1).
- uctypes.NATIVE: int¶
Kiểu bố cục cho cấu trúc gốc - với thứ tự byte dữ liệu và căn chỉnh tuân theo ABI của hệ thống mà MicroPython chạy trên.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
Trả về kích thước của cấu trúc dữ liệu tính bằng byte. Tham số struct có thể là một lớp cấu trúc hoặc một đối tượng cấu trúc được khởi tạo cụ thể (hoặc trường tổng hợp của nó).
- uctypes.addressof(obj: Any) int¶
Trả về địa chỉ của một đối tượng. Tham số phải là bytes, bytearray hoặc đối tượng khác hỗ trợ giao thức bộ đệm (và địa chỉ của bộ đệm này là thứ thực sự được trả về).
- uctypes.bytes_at(addr: int, size: int) bytes¶
Lấy bộ nhớ tại địa chỉ và kích thước đã cho dưới dạng đối tượng bytes. Vì đối tượng bytes là bất biến, bộ nhớ thực sự được sao chép và chép vào đối tượng bytes, do đó nếu nội dung bộ nhớ thay đổi sau này, đối tượng đã tạo vẫn giữ giá trị ban đầu.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
Lấy bộ nhớ tại địa chỉ và kích thước đã cho dưới dạng đối tượng bytearray. Khác với hàm bytes_at() ở trên, bộ nhớ được lấy bằng tham chiếu, do đó nó có thể được ghi vào, và bạn sẽ truy cập giá trị hiện tại tại địa chỉ bộ nhớ đã cho.
Các kiểu số nguyên vô hướng. Mỗi kiểu chiếm số byte tương ứng rõ ràng (1, 2, 4 hoặc 8) và được đọc/ghi bằng thứ tự byte của kiểu bố cục cấu trúc (một trong NATIVE, LITTLE_ENDIAN, hoặc BIG_ENDIAN).
- uctypes.FLOAT32: int¶
Số thực dấu phẩy động độ chính xác đơn IEEE 754 (4 byte). Các lần đọc và ghi được chuyển đổi sang/từ
floatcủa Python.
- uctypes.FLOAT64: int¶
Số thực dấu phẩy động độ chính xác kép IEEE 754 (8 byte). Các lần đọc và ghi được chuyển đổi sang/từ
floatcủa Python.
- uctypes.VOID: int¶
Bí danh cho
UINT8. Được cung cấp để các trườngvoid *theo kiểu C có thể được mô tả một cách thành ngữ là(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
Đánh dấu một trường bộ mô tả là con trỏ tới kiểu khác. Một trường con trỏ được viết dưới dạng 2-tuple
(offset | PTR, target_type_or_descriptor). Hủy tham chiếu con trỏ cho ra một chế độ xem kiểu vào địa chỉ mà nó giữ.
- uctypes.ARRAY: int¶
Đánh dấu một trường bộ mô tả là mảng có độ dài cố định của kiểu khác. Một trường mảng có thể là
(offset | ARRAY, count | element_type)cho mảng các vô hướng hoặc(offset | ARRAY, count, element_descriptor)cho mảng các cấu trúc. Số phần tử được cố định tại thời điểm tạo bộ mô tả.
Không có hằng số rõ ràng cho các cấu trúc: một bộ mô tả tổng hợp không sử dụng cả PTR lẫn ARRAY đều được coi là một cấu trúc.
Bộ mô tả cấu trúc và khởi tạo đối tượng cấu trúc¶
Cho một từ điển bộ mô tả cấu trúc và kiểu bố cục của nó, bạn có thể khởi tạo một thể hiện cấu trúc cụ thể tại một địa chỉ bộ nhớ đã cho bằng hàm tạo uctypes.struct(). Địa chỉ bộ nhớ thường đến từ các nguồn sau:
Địa chỉ được xác định trước, khi truy cập các thanh ghi phần cứng trên hệ thống bare-metal. Tra cứu các địa chỉ này trong datasheet của MCU/SoC cụ thể.
Là giá trị trả về từ lời gọi đến một số hàm FFI (Foreign Function Interface).
Từ
uctypes.addressof(), khi bạn muốn truyền tham số cho một hàm FFI, hoặc để truy cập một số dữ liệu cho I/O (ví dụ: dữ liệu đọc từ file hoặc socket mạng).
Các đối tượng cấu trúc¶
Các đối tượng cấu trúc cho phép truy cập các trường riêng lẻ bằng ký hiệu dấu chấm chuẩn: my_struct.substruct1.field1. Nếu một trường là kiểu vô hướng, lấy nó sẽ tạo ra một giá trị nguyên thủy (số nguyên hoặc float của Python) tương ứng với giá trị chứa trong trường. Một trường vô hướng cũng có thể được gán giá trị.
Nếu một trường là mảng, các phần tử riêng lẻ của nó có thể được truy cập bằng toán tử chỉ số chuẩn [] - cả đọc và gán giá trị.
Nếu một trường là con trỏ, nó có thể được hủy tham chiếu bằng cú pháp [0] (tương ứng với toán tử * trong C, mặc dù [0] cũng hoạt động trong C). Chỉ số con trỏ với các giá trị nguyên khác 0 cũng được hỗ trợ, với ngữ nghĩa giống như trong C.
Tóm lại, việc truy cập các trường cấu trúc nhìn chung tuân theo cú pháp C, ngoại trừ hủy tham chiếu con trỏ, khi bạn cần sử dụng toán tử [0] thay vì *.
Hạn chế¶
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:
Tránh truy cập các cấu trúc lồng nhau. Ví dụ, thay vì
mcu_registers.peripheral_a.register1, hãy định nghĩa các bộ mô tả bố cục riêng biệt cho mỗi ngoại vi, để truy cập dưới dạngperipheral_a.register1. Hoặc chỉ cần lưu cache một ngoại vi cụ thể:peripheral_a = mcu_registers.peripheral_a. Nếu một thanh ghi bao gồm nhiều bitfield, bạn sẽ cần lưu cache các tham chiếu đến một thanh ghi cụ thể:reg_a = mcu_registers.peripheral_a.reg_a.Tránh các dữ liệu không vô hướng khác, như mảng. Ví dụ, thay vì
peripheral_a.register[0]hãy dùngperipheral_a.register0. Tương tự, một lựa chọn thay thế là lưu cache các giá trị trung gian, ví dụ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).