"مع الخبرة ، يأتي منهج علمي قياسي لحساب حجم المكدس الصحيح: خذ رقمًا عشوائيًا والأمل في الأفضل".
- جاك جانسل ، "فن تصميم الأنظمة المدمجة"مرحبا يا هبر!
غريب كما يبدو ، في الغالبية العظمى من "الاشعاعات STM32" التي رأيتها على وجه الخصوص والمتحكمات الدقيقة بشكل عام ، لا يوجد شيء عمومًا حول شيء مثل تخصيص الذاكرة ، ووضع المكدس ، والأهم من ذلك ، منع تجاوز الذاكرة - نتيجة لذلك منطقة واحدة تكتسح الآخر وكل شيء ينهار ، عادةً مع تأثيرات ساحرة.
ويعزى ذلك جزئيًا إلى بساطة مشاريع التدريب التي يتم تنفيذها على لوحات تصحيح الأخطاء باستخدام وحدات تحكم دقيقة دهنية نسبيًا ، والتي يمكن أن تواجه نقصًا في الذاكرة من خلال وميض مؤشر LED - ومع ذلك ، فمن الصعب جدًا ، حتى مؤخرًا ، حتى بالنسبة إلى الهواة المبتدئين ، فإن الإشارات إلى STM32F030F4P6 من النوع الأكثر شيوعًا. ، سهلة التركيب ، يستحق فلسا واحدا ، ولكن أيضا الذاكرة وجود وحدات من الكيلوبايت.
تتيح لك وحدات التحكم هذه القيام بأمور جدية للغاية لنفسك (حسنًا ، هنا ، على سبيل المثال ، تم إجراء مثل هذا
القياس المناسب تمامًا على STM32F042K6T6 مع 6 كيلوبايت من ذاكرة الوصول العشوائي (RAM) ، والتي لا يزال أكثر من 100 بايت منها مجانيًا) ، ولكن عند التعامل مع الذاكرة ، فإنك تحتاج إلى قدر معين من الذاكرة نظافة.
اريد التحدث عن هذه الدقة ستكون المقالة قصيرة ، ولن يتعلم المحترفون شيئًا جديدًا - ولكن ينصح بشدة بهذه المعرفة للمبتدئين.
في مشروع نموذجي على متحكم يعتمد على كورتيكس- M ، يحتوي RAM على تقسيم شرطي إلى أربعة أقسام:
- البيانات - البيانات التي تمت تهيئتها بقيمة محددة
- bss - البيانات التي تمت تهيئتها إلى الصفر
- الكومة - الكومة (المنطقة الديناميكية التي يتم تخصيص الذاكرة منها بشكل صريح باستخدام malloc)
- مكدس - مكدس (المنطقة الحيوية التي يتم تخصيص الذاكرة من قبل المحول البرمجي ضمنيًا)
يمكن أن تحدث منطقة noinit أيضًا من حين لآخر (متغيرات غير مهيأة - فهي ملائمة من حيث أنها تحتفظ بالقيمة بين عمليات إعادة التمهيد) ، وحتى في كثير من الأحيان ، بعض المناطق الأخرى المخصصة لمهام محددة.
وهي موجودة في الذاكرة الفعلية بطريقة محددة إلى حد ما - والحقيقة هي أن المكدس في ميكروكنترولر على النوى ARM ينمو من أعلى إلى أسفل. لذلك ، يقع بشكل منفصل عن كتل الذاكرة المتبقية ، في نهاية ذاكرة الوصول العشوائي:

بشكل افتراضي ، يكون عنوانه مساوياً لأحدث عنوان RAM ، ومن هناك ينخفض أثناء نموه - وهناك سمة غير سارة للغاية من المكدس تنمو منه: يمكن أن تصل إلى bss وتعيد كتابتها ، ولن تعرفها بأي طريقة واضحة.
مناطق ذاكرة ثابتة وديناميكية
يتم تقسيم كل الذاكرة إلى فئتين - تم تخصيصهما بشكل ثابت ، أي الذاكرة ، المبلغ الإجمالي الذي يتضح من نص البرنامج ولا يعتمد على ترتيب تنفيذه ، والمخصص بشكل حيوي ، يعتمد حجمه المطلوب على تقدم البرنامج.
يتضمن الأخير كومة (نأخذ منها أجزاء باستخدام malloc ونعود باستخدامها مجانًا) ومكدس ينمو وينكمش بمفرده.
بشكل عام ، فإن استخدام malloc على ميكروكنترولر هو أمر محبط
للغاية إلا إذا كنت تعرف بالضبط ما تفعله. المشكلة الرئيسية التي يجلبونها هي تجزئة الذاكرة - إذا قمت بتخصيص 10 قطع من 10 بايت ، ثم حرر كل ثانية ، فلن تحصل على 50 بايت مجانًا. ستحصل على 5 قطع مجانية لكل منها 10 بايت.
بالإضافة إلى ذلك ، في مرحلة تجميع البرنامج ، لن يتمكن المحول البرمجي من تحديد مقدار الذاكرة التي تتطلبها malloc تلقائيًا (لا سيما مع مراعاة التجزئة ، والتي لا تعتمد فقط على حجم القطع المطلوبة ، ولكن على تسلسل تخصيصها وإطلاقها) ، وبالتالي لن تكون قادرًا على تحذيرك إذا في النهاية لا توجد ذاكرة كافية.
هناك طرق لحل هذه المشكلة - تطبيقات malloc الخاصة التي تعمل ضمن منطقة مخصصة بشكل ثابت ، وليس ذاكرة الوصول العشوائي (RAM) بأكملها ، والاستخدام الدقيق لل malloc مع مراعاة التجزئة المحتملة على مستوى منطق البرنامج ، إلخ. - ولكن بشكل عام
malloc من الأفضل عدم لمس .
يتم تسجيل جميع مناطق الذاكرة ذات الحدود والعناوين في ملف بملحق .LD ، والذي يتم توجيه الرابط إليه عند إنشاء المشروع.
الذاكرة المخصصة بشكل ثابت
لذلك ، من الذاكرة المخصصة بشكل ثابت ، لدينا منطقتان - bss والبيانات ، والتي تختلف رسميًا فقط. عندما تتم تهيئة النظام ، يتم نسخ كتلة البيانات من الفلاش ، حيث يتم تخزين قيم التهيئة اللازمة لذلك ، يتم ملء كتلة bss بالأصفار (على الأقل يعد تعبئة الأصفار نموذجًا جيدًا).
تتم كلتا الأمرين - النسخ من فلاش وملء بالأصفار - في رمز البرنامج
في شكل واضح ، ولكن ليس في الرئيسي الخاص بك () ، ولكن في ملف منفصل الذي يتم تنفيذه أولاً ، يتم كتابته مرة واحدة وبسهولة من المشروع إلى المشروع.
ومع ذلك ، ليس هذا هو ما يهمنا الآن - ولكن كيف نفهم ما إذا كانت بياناتنا تتوافق مع ذاكرة الوصول العشوائي الخاصة بوحدة التحكم الخاصة بنا.
يتم التعرف عليه بكل بساطة - من خلال الأداة المساعدة بحجم arm-eabi ذات المعلمة الفردية - ملف ELF المترجم من برنامجنا (غالبًا ما يتم إدراج مكالمته في نهاية Makefile ، لأنه مناسب):

النص هنا هو مقدار بيانات البرنامج الموجودة في الفلاش ، و bss والبيانات هي مناطقنا المخصصة بشكل ثابت في ذاكرة الوصول العشوائي. لا يزعجنا العمودين الأخيرين - هذا هو مجموع الثلاثة الأولى ، وليس له معنى عملي.
إجمالي ، ثابت في ذاكرة الوصول العشوائي نحتاج bss + بايت البيانات ، في هذه الحالة - 5324 بايت. وحدة التحكم لديها 6144 بايت من ذاكرة الوصول العشوائي ، ونحن لا نستخدم malloc ، تبقى 820 بايت.
والتي ينبغي أن تكون كافية بالنسبة لنا على المكدس.
لكن بما فيه الكفاية؟ لأنه إذا لم يكن الأمر كذلك ، فإن مجموعتنا ستنمو لتصبح بياناتنا الخاصة ، وبعد ذلك سوف تقوم بالكتابة فوق البيانات ، ثم البيانات ستحل محله ، ومن ثم سيتعطل كل شيء. علاوة على ذلك ، يمكن أن يستمر البرنامج في العمل بين النقطتين الأولى والثانية دون إدراك وجود بيانات غير مهمة في البيانات التي يعالجها. في أسوأ الحالات ، ستكون البيانات التي قمت بتدوينها عندما كان كل شيء على ما يرام مع المكدس ، والآن تقرأ للتو - على سبيل المثال ، معلمات المعايرة لبعض أجهزة الاستشعار - ومن ثم ليس لديك أي طريقة واضحة لفهم أن كل شيء سيء معها ، سيستمر تشغيل هذا البرنامج ، كما لو أنه لم يحدث أي شيء ، مما يتيح لك القمامة في الإخراج.
ذاكرة مخصصة بشكل حيوي
وهنا يبدأ الجزء الأكثر إثارة للاهتمام - إذا قمت بتقليل الحكاية إلى عبارة واحدة ، فمن
المستحيل تقريبًا تحديد حجم الرصة مسبقًا .
من الناحية النظرية ، يمكنك أن تطلب من المحول البرمجي أن يمنحك حجم الرصة المستخدم من قبل كل وظيفة فردية ، ثم اطلب منه إرجاع شجرة تنفيذ البرنامج الخاص بك ، ولكل فرع فيه حساب مجموع مكدسات جميع الوظائف الموجودة في هذه الشجرة. هذا وحده لأي برنامج أكثر أو أقل تعقيدًا سوف يأخذك إلى وقت كبير.
ثم تتذكر أنه في أي لحظة قد يحدث انقطاع ، يحتاج المعالج أيضًا إلى ذاكرة.
ثم - يمكن أن يحدث اثنين أو ثلاثة مقاطعات متداخلة ، والتي يمكن من معالجات ...
بشكل عام ، أنت تفهم. تعد محاولة حساب مجموعة البرامج الخاصة ببرنامج معين نشاطًا مثيرًا ومفيدًا بشكل عام ، ولكن في أغلب الأحيان لن تقوم بذلك.
لذلك ، في الممارسة العملية ، يتم استخدام تقنية واحدة تسمح لك على الأقل بطريقة ما بفهم ما إذا كان كل شيء في حياتنا يتطور بشكل جيد - ما يسمى "لوحة الذاكرة" (لوحة الذاكرة).
الأمر المريح في هذه الطريقة هو أنه لا يعتمد على أدوات تصحيح الأخطاء التي تستخدمها ، وإذا كان لدى النظام على الأقل بعض وسائل إخراج المعلومات ، فيمكنك الاستغناء عن أدوات تصحيح الأخطاء على الإطلاق.
جوهرها هو أننا نملأ المصفوفة بالكامل من نهاية bss إلى بداية المكدس في مكان ما في المرحلة المبكرة جدًا من تنفيذ البرنامج ، عندما لا تزال المجموعة مكدسة تمامًا ، بنفس القيمة.
علاوة على ذلك ، التحقق من عنوان هذه القيمة قد اختفى بالفعل ، ونحن نفهم أين تعطل المكدس. نظرًا لأنه لن يتم استعادة اللون الممحو نفسه ، يمكن إجراء الفحص بشكل متقطع - سيُظهر الحد الأقصى لحجم المكدس الذي تم الوصول إليه.
حدِّد لون الطلاء - لا يهم القيمة المحددة ، أسفل أنا فقط أصبعي بإصبع يدي. الشيء الرئيسي هو عدم اختيار 0 و FF:
#define STACK_CANARY_WORD (0xCACACACAUL)
- , -, :
volatile unsigned *top, *start;
__asm__ volatile ("mov %[top], sp" : [top] "=r" (top) : : );
start = &_ebss;
while (start < top) {
*(start++) = STACK_CANARY_WORD;
}
? top , — ; start — bss (, ,
*.ld — libopencm3). bss .
:
unsigned check_stack_size(void) {
/* top of data section */
unsigned *addr = &_ebss;
/* look for the canary word till the end of RAM */
while ((addr < &_stack) && (*addr == STACK_CANARY_WORD)) {
addr++;
}
return ((unsigned)&_stack - (unsigned)addr);
}
_ebss , _stack —
, , , , .
.
— - check_stack_size() , , , .
.
712 — 6 108 .
Word of caution
— , , 100-% . ,
, , , , . , , -, 10-20 %, 108 .
, , , .
P.S. RTOS — MSP, , PSP. , — .