uctypes --- เข้าถึงข้อมูลไบนารีในรูปแบบโครงสร้าง¶
โมดูลนี้ใช้งาน "foreign data interface" สำหรับ MicroPython แนวคิดเบื้องหลังคล้ายคลึงกับโมดูล ctypes ของ CPython แต่ API จริงนั้นแตกต่าง กระชับ และปรับให้เหมาะกับขนาดที่เล็ก แนวคิดพื้นฐานของโมดูลคือการกำหนดโครงสร้างข้อมูลด้วยความสามารถใกล้เคียงกับภาษา C แล้วเข้าถึงด้วยไวยากรณ์จุดที่คุ้นเคยเพื่ออ้างอิงฟิลด์ย่อย
Warning
โมดูล uctypes อนุญาตให้เข้าถึงที่อยู่หน่วยความจำใด ๆ ของเครื่อง (รวมถึง I/O และรีจิสเตอร์ควบคุม) การใช้งานอย่างไม่ระมัดระวังอาจนำไปสู่การขัดข้อง การสูญหายของข้อมูล และแม้แต่ฮาร์ดแวร์ทำงานผิดพลาด
See also
- โมดูล
struct โมดูล Python มาตรฐานสำหรับการแพ็กและแกะข้อมูลไบนารี
structทำงานกับบัฟเฟอร์ทั้งหมดในคราวเดียวโดยใช้สตริงรูปแบบที่กระชับ (เช่น'<HBB4sI') ซึ่งทำงานได้ดีสำหรับฟิลด์คงที่จำนวนน้อย แต่ขยายได้ไม่ดีกับโครงสร้างขนาดใหญ่หรือซ้อนกันลึก: ทุกการอ่านหรือเขียนจะวิเคราะห์สตริงรูปแบบใหม่, union และ bitfield ไม่ได้รับการสนับสนุน, และไม่มีทางได้มุมมองแบบมีชนิดข้อมูลเข้าไปในบัฟเฟอร์ที่มีอยู่uctypesเป็นส่วนเสริมของstructโดยให้คุณอธิบาย layout ครั้งเดียว แนบกับบริเวณหน่วยความจำ (RAM, รีจิสเตอร์อุปกรณ์ต่อพ่วง,bytearray) แล้วเข้าถึงฟิลด์แต่ละตัวเป็น attribute ที่มีชื่อ -- หลีกเลี่ยงการวิเคราะห์และคัดลอกซ้ำ และเพิ่มการสนับสนุน struct ซ้อน, array, union และ bitfield
ตัวอย่างการใช้งาน:
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)
การกำหนด layout โครงสร้าง¶
Layout โครงสร้างถูกกำหนดโดย "descriptor" - dictionary ของ Python ที่เข้ารหัสชื่อฟิลด์เป็น key และคุณสมบัติอื่น ๆ ที่จำเป็นในการเข้าถึงเป็น value ที่เกี่ยวข้อง:
{
"field1": <properties>,
"field2": <properties>,
...
}
ปัจจุบัน uctypes ต้องการการระบุ offset สำหรับแต่ละฟิลด์อย่างชัดเจน offset จะระบุเป็นไบต์จากจุดเริ่มต้นของโครงสร้าง
ต่อไปนี้เป็นตัวอย่างการเข้ารหัสสำหรับประเภทฟิลด์ต่าง ๆ:
ประเภท scalar:
"field_name": offset | uctypes.UINT32กล่าวอีกนัยหนึ่ง value คือตัวระบุประเภท scalar ที่ OR กับ offset ฟิลด์ (เป็นไบต์) จากจุดเริ่มต้นของโครงสร้าง
โครงสร้างแบบ recursive:
"sub": (offset, { "b0": 0 | uctypes.UINT8, "b1": 1 | uctypes.UINT8, })
กล่าวคือ value คือ 2-tuple โดย element แรกคือ offset และ element ที่สองคือ dictionary ของ descriptor โครงสร้าง (หมายเหตุ: offset ใน descriptor แบบ recursive เป็นค่าสัมพัทธ์กับโครงสร้างที่กำหนด) แน่นอนว่าโครงสร้างแบบ recursive สามารถระบุได้ไม่เพียงแค่จาก dictionary literal เท่านั้น แต่ยังอ้างอิง dictionary ของ descriptor โครงสร้างที่กำหนดไว้ก่อนหน้าด้วยชื่อได้
Array ของประเภท primitive:
"arr": (offset | uctypes.ARRAY, size | uctypes.UINT8),กล่าวคือ value คือ 2-tuple โดย element แรกคือ ARRAY flag ที่ OR กับ offset และ element ที่สองคือประเภท element scalar ที่ OR กับจำนวน element ใน array
Array ของประเภท aggregate:
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),กล่าวคือ value คือ 3-tuple โดย element แรกคือ ARRAY flag ที่ OR กับ offset, element ที่สองคือจำนวน element ใน array และ element ที่สามคือ descriptor ของประเภท element
Pointer ไปยังประเภท primitive:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),กล่าวคือ value คือ 2-tuple โดย element แรกคือ PTR flag ที่ OR กับ offset และ element ที่สองคือประเภท element scalar
Pointer ไปยังประเภท aggregate:
"ptr2": (offset | uctypes.PTR, {"b": 0 | uctypes.UINT8}),กล่าวคือ value คือ 2-tuple โดย element แรกคือ PTR flag ที่ OR กับ offset และ element ที่สองคือ descriptor ของประเภทที่ถูกชี้ถึง
Bitfield:
"bitf0": offset | uctypes.BFUINT16 | lsbit << uctypes.BF_POS | bitsize << uctypes.BF_LEN,กล่าวคือ value คือประเภทของ scalar value ที่มี bitfield ที่กำหนด (ชื่อประเภทคล้ายกับประเภท scalar แต่มีคำนำหน้าด้วย
BF) ที่ OR กับ offset สำหรับ scalar value ที่มี bitfield และ OR กับค่าสำหรับตำแหน่ง bit และความยาว bit ของ bitfield ภายใน scalar value โดยเลื่อนด้วย BF_POS และ BF_LEN bits ตามลำดับ ตำแหน่ง bitfield นับจาก bit ที่มีนัยสำคัญน้อยที่สุดของ scalar (มีตำแหน่ง 0) และเป็นหมายเลขของ bit ขวาสุดของฟิลด์ (กล่าวอีกนัยหนึ่งคือจำนวน bit ที่ scalar ต้องเลื่อนขวาเพื่อแยก bitfield)ในตัวอย่างข้างต้น ก่อนอื่น UINT16 value จะถูกดึงมาที่ offset 0 (รายละเอียดนี้อาจสำคัญเมื่อเข้าถึงรีจิสเตอร์ฮาร์ดแวร์ซึ่งต้องการขนาดและการ alignment ในการเข้าถึงที่เฉพาะเจาะจง) แล้วจะดึง bitfield ที่ bit ขวาสุดคือ bit lsbit ของ UINT16 นี้ และมีความยาว bitsize bits ตัวอย่างเช่น หาก lsbit คือ 0 และ bitsize คือ 8 แล้ว จริง ๆ แล้วจะเข้าถึง byte ที่มีนัยสำคัญน้อยที่สุดของ UINT16
โปรดทราบว่าการดำเนินการ bitfield ไม่ขึ้นอยู่กับ byte endianness ของเป้าหมาย โดยเฉพาะอย่างยิ่ง ตัวอย่างข้างต้นจะเข้าถึง byte ที่มีนัยสำคัญน้อยที่สุดของ UINT16 ทั้งใน little-endian และ big-endian structures แต่ขึ้นอยู่กับ bit ที่มีนัยสำคัญน้อยที่สุดที่ถูกกำหนดหมายเลขเป็น 0 เป้าหมายบางตัวอาจใช้การกำหนดหมายเลขที่แตกต่างกันใน native ABI ของตน แต่
uctypesใช้การกำหนดหมายเลขแบบ normalized ที่อธิบายข้างต้นเสมอ
เนื้อหาของโมดูล¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
สร้าง object "foreign data structure" โดยอาศัยที่อยู่ของโครงสร้างในหน่วยความจำ, descriptor (เข้ารหัสเป็น dictionary) และประเภท layout (ดูด้านล่าง)
- uctypes.LITTLE_ENDIAN: int¶
ประเภท layout สำหรับโครงสร้างแบบ packed little-endian (Packed หมายความว่าทุกฟิลด์ใช้ไบต์พอดีตามที่กำหนดใน descriptor กล่าวคือ alignment คือ 1)
- uctypes.NATIVE: int¶
ประเภท layout สำหรับโครงสร้าง native - ที่มี data endianness และ alignment ตาม ABI ของระบบที่ MicroPython ทำงานอยู่
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
คืนค่าขนาดของโครงสร้างข้อมูลเป็นไบต์ อาร์กิวเมนต์ struct สามารถเป็น class โครงสร้างหรือ object โครงสร้างที่สร้างขึ้นเฉพาะเจาะจง (หรือฟิลด์ aggregate ของมัน)
- uctypes.addressof(obj: Any) int¶
คืนค่าที่อยู่ของ object อาร์กิวเมนต์ควรเป็น bytes, bytearray หรือ object อื่นที่รองรับ buffer protocol (และที่อยู่ของบัฟเฟอร์นี้คือสิ่งที่คืนค่าจริง ๆ)
- uctypes.bytes_at(addr: int, size: int) bytes¶
จับหน่วยความจำที่ที่อยู่และขนาดที่กำหนดเป็น object bytes เนื่องจาก object bytes ไม่สามารถเปลี่ยนแปลงได้ หน่วยความจำจะถูกทำซ้ำและคัดลอกไปยัง object bytes จริง ๆ ดังนั้น หากเนื้อหาหน่วยความจำเปลี่ยนแปลงในภายหลัง object ที่สร้างขึ้นจะยังคงมีค่าเดิม
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
จับหน่วยความจำที่ที่อยู่และขนาดที่กำหนดเป็น object bytearray ต่างจากฟังก์ชัน bytes_at() ข้างต้น หน่วยความจำจะถูกจับโดยการอ้างอิง ดังนั้นจึงสามารถเขียนได้และคุณจะเข้าถึงค่าปัจจุบันที่ที่อยู่หน่วยความจำที่กำหนด
ประเภทจำนวนเต็ม scalar แต่ละตัวใช้จำนวนไบต์ที่ชัดเจน (1, 2, 4 หรือ 8) และอ่าน/เขียนโดยใช้ endianness ของประเภท layout ของโครงสร้าง (หนึ่งใน NATIVE, LITTLE_ENDIAN หรือ BIG_ENDIAN)
- uctypes.FLOAT32: int¶
จุดทศนิยม IEEE 754 ความแม่นยำเดี่ยว (4 ไบต์) การอ่านและเขียนจะถูกแปลงไป/มาจาก Python
float
- uctypes.FLOAT64: int¶
จุดทศนิยม IEEE 754 ความแม่นยำคู่ (8 ไบต์) การอ่านและเขียนจะถูกแปลงไป/มาจาก Python
float
- uctypes.VOID: int¶
Alias สำหรับ
UINT8ให้มาเพื่อให้ฟิลด์ C-stylevoid *สามารถอธิบายได้อย่างเป็นธรรมชาติว่าเป็น(uctypes.PTR, uctypes.VOID)
- uctypes.PTR: int¶
ทำเครื่องหมายฟิลด์ descriptor ว่าเป็น pointer ไปยังประเภทอื่น ฟิลด์ pointer เขียนเป็น 2-tuple
(offset | PTR, target_type_or_descriptor)การ dereference pointer จะให้มุมมองแบบมีชนิดข้อมูลเข้าไปยังที่อยู่ที่มันถือไว้
- uctypes.ARRAY: int¶
ทำเครื่องหมายฟิลด์ descriptor ว่าเป็น array ขนาดคงที่ของประเภทอื่น ฟิลด์ array เป็นทั้ง
(offset | ARRAY, count | element_type)สำหรับ array ของ scalar หรือ(offset | ARRAY, count, element_descriptor)สำหรับ array ของโครงสร้าง จำนวน element ถูกกำหนดคงที่เมื่อถึงเวลา descriptor
ไม่มีค่าคงที่ชัดเจนสำหรับโครงสร้าง: descriptor แบบ aggregate ที่ไม่ใช้ทั้ง PTR และ ARRAY จะถูกถือว่าเป็นโครงสร้าง
Descriptor โครงสร้างและการสร้าง object โครงสร้าง¶
เมื่อกำหนด dictionary ของ descriptor โครงสร้างและประเภท layout แล้ว คุณสามารถสร้าง instance โครงสร้างเฉพาะที่ที่อยู่หน่วยความจำที่กำหนดได้โดยใช้ constructor uctypes.struct() ที่อยู่หน่วยความจำมักมาจากแหล่งต่อไปนี้:
ที่อยู่ที่กำหนดไว้ล่วงหน้า เมื่อเข้าถึงรีจิสเตอร์ฮาร์ดแวร์บนระบบ baremetal ค้นหาที่อยู่เหล่านี้ใน datasheet สำหรับ MCU/SoC ที่เฉพาะเจาะจง
ค่าที่คืนมาจากการเรียก FFI (Foreign Function Interface) function
จาก
uctypes.addressof()เมื่อคุณต้องการส่งอาร์กิวเมนต์ไปยัง FFI function หรืออีกทางหนึ่งคือเข้าถึงข้อมูลบางอย่างสำหรับ I/O (ตัวอย่างเช่น ข้อมูลที่อ่านจากไฟล์หรือ network socket)
Object โครงสร้าง¶
Object โครงสร้างอนุญาตให้เข้าถึงฟิลด์แต่ละตัวโดยใช้ notation จุดมาตรฐาน: my_struct.substruct1.field1 หากฟิลด์เป็นประเภท scalar การดึงค่าจะได้ค่า primitive (Python integer หรือ float) ที่สอดคล้องกับค่าที่มีอยู่ในฟิลด์ ฟิลด์ scalar สามารถกำหนดค่าได้เช่นกัน
หากฟิลด์เป็น array สามารถเข้าถึง element แต่ละตัวด้วย subscript operator มาตรฐาน [] - ทั้งอ่านและกำหนดค่าได้
หากฟิลด์เป็น pointer สามารถ dereference ด้วยไวยากรณ์ [0] (สอดคล้องกับตัวดำเนินการ * ใน C แม้ว่า [0] ก็ใช้ได้ใน C เช่นกัน) การ subscript pointer ด้วยค่าจำนวนเต็มอื่นนอกจาก 0 ก็รองรับเช่นกัน ด้วย semantics เดียวกับใน C
สรุปได้ว่า การเข้าถึงฟิลด์โครงสร้างโดยทั่วไปตาม C syntax ยกเว้นการ dereference pointer เมื่อคุณต้องใช้ตัวดำเนินการ [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ให้กำหนด descriptor layout แยกต่างหากสำหรับแต่ละอุปกรณ์ต่อพ่วง เพื่อเข้าถึงเป็นperipheral_a.register1หรือเพียงแคช peripheral ที่เฉพาะเจาะจง:peripheral_a = mcu_registers.peripheral_aหากรีจิสเตอร์ประกอบด้วย bitfield หลายตัว คุณจะต้องแคชการอ้างอิงไปยังรีจิสเตอร์เฉพาะ:reg_a = mcu_registers.peripheral_a.reg_aหลีกเลี่ยงข้อมูลที่ไม่ใช่ scalar อื่น ๆ เช่น array ตัวอย่างเช่น แทนที่จะใช้
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).