uctypes — גישה לנתונים בינאריים בצורה מובנית¶
מודול זה מממש ”ממשק נתונים זרים“ (foreign data interface) עבור MicroPython. הרעיון שמאחוריו דומה למודול ctypes של CPython, אך ה-API בפועל שונה, יעיל וממוטב לגודל קטן. הרעיון הבסיסי של המודול הוא להגדיר מבנה (layout) של מבנה נתונים בעוצמה דומה לזו שמאפשרת שפת C, ולאחר מכן לגשת אליו באמצעות תחביר הנקודה המוכר כדי להתייחס לתת-שדות.
אזהרה
מודול uctypes מאפשר גישה לכתובות זיכרון שרירותיות של המכונה (כולל אוגרי I/O ובקרה). שימוש לא זהיר בו עלול להוביל לקריסות, לאובדן נתונים ואף לתקלות חומרה.
ראה גם
- מודול
struct מודול ה-Python הסטנדרטי לאריזה ופריקה של נתונים בינאריים.
structפועל על חוצצים שלמים בבת אחת באמצעות מחרוזת פורמט קומפקטית (למשל'<HBB4sI'), מה שעובד היטב עבור מספר שדות קבועים אך מתרחב בצורה גרועה למבנים גדולים או מקוננים עמוקות: כל קריאה או כתיבה מנתחת מחדש את מחרוזת הפורמט, איגודים (unions) ושדות סיביות (bitfields) אינם נתמכים, ואין דרך לקבל תצוגה מטופסת אל תוך חוצץ קיים.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)
הגדרת מבנה (layout) של מבנה הנתונים¶
המבנה (layout) של מבנה הנתונים מוגדר על ידי ”מתאר“ (descriptor) - מילון Python שמקודד שמות שדות כמפתחות ותכונות נוספות הנדרשות כדי לגשת אליהם כערכים המשויכים:
{
"field1": <properties>,
"field2": <properties>,
...
}
כרגע, uctypes דורש ציון מפורש של היסטים (offsets) עבור כל שדה. ההיסטים ניתנים בבתים מתחילת המבנה.
להלן דוגמאות קידוד עבור סוגי שדות שונים:
סוגים סקלריים:
"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):
"arr2": (offset | uctypes.ARRAY, size, {"b": 0 | uctypes.UINT8}),כלומר, הערך הוא שלשה (3-tuple), שהאיבר הראשון בו הוא דגל ARRAY המשולב באמצעות OR עם היסט, השני הוא מספר האיברים במערך, והשלישי הוא מתאר של סוג האיבר.
מצביע לסוג פרימיטיבי:
"ptr": (offset | uctypes.PTR, uctypes.UINT8),כלומר, הערך הוא צמד (2-tuple), שהאיבר הראשון בו הוא דגל PTR המשולב באמצעות OR עם היסט, והשני הוא סוג איבר סקלרי.
מצביע לסוג מצרפי (aggregate):
"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 עם היסט עבור הערך הסקלרי המכיל את שדה הסיביות, ומשולב נוסף באמצעות OR עם ערכים עבור מיקום הסיבית ואורך הסיביות של שדה הסיביות בתוך הערך הסקלרי, מוזזים ב-BF_POS וב-BF_LEN סיביות, בהתאמה. מיקום שדה סיביות נספר מהסיבית הפחות משמעותית של הסקלר (בעל מיקום 0), והוא מספר הסיבית הימנית ביותר של שדה (במילים אחרות, זהו מספר הסיביות שצריך להזיז את הסקלר ימינה כדי לחלץ את שדה הסיביות).בדוגמה שלמעלה, תחילה יחולץ ערך UINT16 בהיסט 0 (פרט זה עשוי להיות חשוב בעת גישה לאוגרי חומרה, שבהם נדרשים גודל גישה ויישור מסוימים), ולאחר מכן יחולץ שדה סיביות שהסיבית הימנית ביותר שלו היא הסיבית ה-lsbit של UINT16 זה, ואורכו הוא bitsize סיביות. לדוגמה, אם lsbit הוא 0 ו-bitsize הוא 8, אז למעשה תתבצע גישה לבית הפחות משמעותי של UINT16.
שים לב ששדה סיביות פעולות בלתי תלויות בסדר הבתים (endianness) של היעד, ובפרט, הדוגמה שלמעלה תיגש לבית הפחות משמעותי של UINT16 הן במבנים little-endian והן במבנים big-endian. אך הדבר תלוי בכך שהסיבית הפחות משמעותית ממוספרת כ-0. יעדים מסוימים עשויים להשתמש במספור שונה ב-ABI המקורי שלהם, אך
uctypesמשתמש תמיד במספור המנורמל המתואר לעיל.
תוכן המודול¶
- class uctypes.struct(addr: int, descriptor: dict, layout_type: int = NATIVE, /)¶
יוצר אובייקט של ”מבנה נתונים זר“ (foreign data structure) על בסיס כתובת המבנה בזיכרון, מתאר (descriptor, מקודד כמילון), וסוג המבנה (layout type, ראה להלן).
- uctypes.LITTLE_ENDIAN: int¶
סוג מבנה (layout type) עבור מבנה ארוז (packed) בסדר little-endian. (ארוז פירושו שכל שדה תופס בדיוק כמספר הבתים שהוגדר במתאר, כלומר היישור הוא 1).
- uctypes.NATIVE: int¶
סוג מבנה (layout type) עבור מבנה מקורי (native) - עם סדר בתים (endianness) ויישור התואמים ל-ABI של המערכת שעליה רץ MicroPython.
- uctypes.sizeof(struct: dict | Any, layout_type: int = NATIVE, /) int¶
מחזיר את גודל מבנה הנתונים בבתים. הארגומנט struct יכול להיות מחלקת מבנה או אובייקט מבנה ספציפי שיוצר (או שדה מצרפי שלו).
- uctypes.addressof(obj: Any) int¶
מחזיר את כתובת האובייקט. הארגומנט צריך להיות bytes, bytearray או אובייקט אחר התומך בפרוטוקול החוצץ (buffer protocol) (וכתובת חוצץ זה היא מה שמוחזר בפועל).
- uctypes.bytes_at(addr: int, size: int) bytes¶
לוכד את הזיכרון בכתובת ובגודל הנתונים כאובייקט bytes. מכיוון שאובייקט bytes הוא בלתי משתנה (immutable), הזיכרון למעשה משוכפל ומועתק לתוך אובייקט bytes, כך שאם תוכן הזיכרון משתנה מאוחר יותר, האובייקט שנוצר משמר את הערך המקורי.
- uctypes.bytearray_at(addr: int, size: int) bytearray¶
לוכד את הזיכרון בכתובת ובגודל הנתונים כאובייקט bytearray. בניגוד לפונקציה bytes_at() שלמעלה, הזיכרון נלכד באמצעות הפניה (reference), כך שניתן גם לכתוב אליו, ותיגש לערך הנוכחי בכתובת הזיכרון הנתונה.
סוגי מספרים שלמים סקלריים. כל אחד תופס את מספר הבתים המתבקש (1, 2, 4 או 8) ונקרא/נכתב באמצעות סדר הבתים (endianness) של סוג המבנה (layout type) של המבנה (אחד מבין NATIVE, LITTLE_ENDIAN, או BIG_ENDIAN).
- uctypes.INT64: int¶
מספר שלם מסומן (signed) בגודל 64 סיביות. טווח
-0x8000000000000000–0x7FFFFFFFFFFFFFFF.
- uctypes.FLOAT32: int¶
מספר נקודה צפה בדיוק יחיד לפי IEEE 754 (4 בתים). קריאות וכתיבות מומרות אל/מתוך
floatשל Python.
- uctypes.FLOAT64: int¶
מספר נקודה צפה בדיוק כפול לפי IEEE 754 (8 בתים). קריאות וכתיבות מומרות אל/מתוך
floatשל Python.
- uctypes.VOID: int¶
כינוי (alias) ל-
UINT8. מסופק כך שניתן לתאר שדותvoid *בסגנון C בצורה אידיומטית כ-(uctypes.PTR, uctypes.VOID).
- uctypes.PTR: int¶
מסמן שדה במתאר כמצביע לסוג אחר. שדה מצביע נכתב כצמד (two-tuple)
(offset | PTR, target_type_or_descriptor). גישה לערך שעליו מצביע המצביע (dereferencing) מפיקה תצוגה מטופסת אל הכתובת שהוא מחזיק.
- uctypes.ARRAY: int¶
מסמן שדה במתאר כמערך באורך קבוע של סוג אחר. שדה מערך הוא
(offset | ARRAY, count | element_type)עבור מערכים של סקלרים או(offset | ARRAY, count, element_descriptor)עבור מערכים של מבנים. מספר האיברים נקבע באופן קבוע בזמן הגדרת המתאר.
אין קבוע מפורש עבור מבנים: מתאר מצרפי (aggregate) שאינו משתמש לא ב-PTR ולא ב-ARRAY נחשב כמבנה.
מתארי מבנה ויצירת אובייקטי מבנה¶
בהינתן מילון מתאר מבנה וסוג המבנה (layout type) שלו, ניתן ליצור מופע מבנה ספציפי בכתובת זיכרון נתונה באמצעות הבנאי uctypes.struct(). כתובת הזיכרון מגיעה בדרך כלל מהמקורות הבאים:
כתובת מוגדרת מראש, בעת גישה לאוגרי חומרה במערכת baremetal. חפש כתובות אלו בגיליון הנתונים (datasheet) של ה-MCU/SoC המסוים.
כערך מוחזר מקריאה לפונקציית FFI כלשהי (Foreign Function Interface).
מתוך
uctypes.addressof(), כאשר ברצונך להעביר ארגומנטים לפונקציית FFI, או לחלופין, לגשת לנתונים כלשהם עבור I/O (לדוגמה, נתונים שנקראו מקובץ או מ-socket רשת).
אובייקטי מבנה¶
אובייקטי מבנה מאפשרים גישה לשדות בודדים באמצעות תחביר נקודה סטנדרטי: my_struct.substruct1.field1. אם שדה הוא מסוג סקלרי, קבלתו תפיק ערך פרימיטיבי (מספר שלם או נקודה צפה של Python) המתאים לערך הכלול בשדה. ניתן גם להשמות ערך לשדה סקלרי.
אם שדה הוא מערך, ניתן לגשת לאיבריו הבודדים באמצעות אופרטור התחתית הסטנדרטי [] - הן לקריאה והן להשמה.
אם שדה הוא מצביע, ניתן לגשת לערך שעליו הוא מצביע באמצעות תחביר [0] (המתאים לאופרטור * של C, אם כי [0] עובד גם ב-C). תמיכה קיימת גם בהפעלת תחתית על מצביע עם ערכי מספר שלם אחרים מלבד 0, עם אותה סמנטיקה כמו ב-C.
לסיכום, גישה לשדות מבנה עוקבת בדרך כלל אחר תחביר C, למעט גישה לערך שעליו מצביע מצביע (dereference), שבה עליך להשתמש באופרטור [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, הגדר מתארי מבנה (layout) נפרדים עבור כל התקן היקפי, שאליהם תיגש כ-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).