uctypes --- الوصول إلى البيانات الثنائية بطريقة منظمة

ينفّذ هذا الوحدة "واجهة البيانات الأجنبية" لـ MicroPython. الفكرة وراءها مشابهة لوحدات ctypes في CPython، لكن واجهة برمجة التطبيقات الفعلية مختلفة، ومبسّطة ومُحسَّنة للحجم الصغير. الفكرة الأساسية للوحدة هي تعريف تخطيط بنية البيانات بنفس القدرة تقريباً التي تتيحها لغة C، ثم الوصول إليها باستخدام صيغة النقطة المألوفة للإشارة إلى الحقول الفرعية.

تحذير

تتيح وحدة uctypes الوصول إلى عناوين ذاكرة عشوائية في الجهاز (بما في ذلك سجلات الإدخال/الإخراج والتحكم). قد يؤدي الاستخدام غير الحذِر لها إلى أعطال، وفقدان البيانات، وحتى خلل في العتاد.

شاهد أيضا

الوحدة struct

وحدة Python القياسية لتعبئة البيانات الثنائية وفكّها. تعمل struct على مخازن مؤقتة كاملة دفعة واحدة باستخدام سلسلة تنسيق مدمجة (مثل '<HBB4sI')، وهو ما يعمل جيداً مع بضعة حقول ثابتة لكنه يتوسّع بشكل ضعيف للبنى الكبيرة أو المتداخلة بعمق: فكل عملية قراءة أو كتابة تعيد تحليل سلسلة التنسيق، والاتحادات والحقول البتية غير مدعومة، ولا توجد طريقة للحصول على عرض مُصنَّف لمخزن مؤقت موجود. تكمّل 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)

تعريف تخطيط البنية

يُعرَّف تخطيط البنية بواسطة "واصف" - وهو قاموس Python يرمّز أسماء الحقول كمفاتيح والخصائص الأخرى المطلوبة للوصول إليها كقيم مرتبطة:

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

حالياً، تتطلب uctypes تحديداً صريحاً للإزاحات لكل حقل. تُعطى الإزاحات بالبايتات من بداية البنية.

فيما يلي أمثلة ترميز لأنواع الحقول المختلفة:

  • الأنواع القياسية (Scalar):

    "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 مع عدد العناصر في المصفوفة.

  • مصفوفات الأنواع المركّبة:

    "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 مع الإزاحة، والثاني واصف النوع المُشار إليه.

  • الحقول البتية:

    "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.

    لاحظ أن عمليات الحقول البتية مستقلة عن ترتيب البايتات في الهدف، وعلى وجه الخصوص، فإن المثال أعلاه سيصل إلى البايت الأقل أهمية من UINT16 في كل من البنى ذات الترتيب الصغير والكبير. لكنه يعتمد على ترقيم البت الأقل أهمية بالرقم 0. قد تستخدم بعض الأهداف ترقيماً مختلفاً في واجهة التطبيق الثنائية الأصلية الخاصة بها، لكن uctypes تستخدم دائماً الترقيم المُطبَّع الموصوف أعلاه.

محتويات الوحدة

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

إنشاء كائن "بنية بيانات أجنبية" استناداً إلى عنوان البنية في الذاكرة، والواصف (المرمّز كقاموس)، ونوع التخطيط (انظر أدناه).

uctypes.LITTLE_ENDIAN: int

نوع تخطيط لبنية مرصوصة بترتيب البايتات الصغير. (مرصوصة تعني أن كل حقل يشغل بالضبط عدد البايتات المُعرَّف في الواصف، أي أن المحاذاة هي 1).

uctypes.BIG_ENDIAN: int

نوع تخطيط لبنية مرصوصة بترتيب البايتات الكبير.

uctypes.NATIVE: int

نوع تخطيط لبنية أصلية - مع ترتيب البايتات والمحاذاة المتوافقين مع واجهة التطبيق الثنائية للنظام الذي يعمل عليه MicroPython.

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() أعلاه، تُلتقط الذاكرة بالمرجعية، لذا يمكن الكتابة إليها أيضاً، وستصل إلى القيمة الحالية عند عنوان الذاكرة المعطى.

أنواع الأعداد الصحيحة القياسية. يشغل كل منها العدد البديهي من البايتات (1 أو 2 أو 4 أو 8) ويُقرأ/يُكتب باستخدام ترتيب البايتات لنوع تخطيط البنية (أحد NATIVE أو LITTLE_ENDIAN أو BIG_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 بايتات). تُحوَّل عمليات القراءة والكتابة من/إلى float في Python.

uctypes.FLOAT64: int

عدد عشري مزدوج الدقة وفق معيار IEEE 754 (8 بايتات). تُحوَّل عمليات القراءة والكتابة من/إلى float في Python.

uctypes.VOID: int

اسم بديل لـ UINT8. مُتاح حتى يمكن وصف حقول void * بأسلوب لغة C بشكل اصطلاحي على هيئة (uctypes.PTR, uctypes.VOID).

uctypes.PTR: int

يُعلِّم حقل واصف كمؤشر إلى نوع آخر. يُكتب حقل المؤشر كصف ثنائي (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 (واجهة الدوال الأجنبية).

  • من uctypes.addressof()، عندما تريد تمرير وسائط إلى دالة FFI، أو بدلاً من ذلك، للوصول إلى بعض البيانات للإدخال/الإخراج (على سبيل المثال، البيانات المقروءة من ملف أو من مقبس شبكة).

كائنات البنية

تتيح كائنات البنية الوصول إلى الحقول الفردية باستخدام تدوين النقطة القياسي: 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).