مرحبا يا هبر!
فيما يتعلق
بتخفيف النظام في اليوم الآخر ، فإن السخط في تعليقات أحد المنشورات المجاورة أن المقالات حول ميكروكنترولر هي كل يومض فقط من قبل LED ، وكذلك الموت غير المحدود لبلدي بلوق القياسية ، وأنا كسول جدا لاستعادتها ، وسأنقل هنا مادة مفيدة حول ضوء القليل المؤسف خدعة صحفية في العمل مع مراكز Cortex-M - التحقق من العناوين العشوائية للتأكد من صحتها.
تتمثل إحدى الميزات المفيدة للغاية وفي الوقت نفسه لسبب ما في الميزات الجاهزة غير الموصوفة في أي مكان على ميكروكنترولر Cortex-M (الكل) في القدرة على التحقق من صحة العنوان في الذاكرة. مع ذلك ، يمكنك تحديد حجم الفلاش وذاكرة الوصول العشوائي و EEPROM ، وتحديد التواجد على معالج معين للأجهزة الطرفية والسجلات المحددة ، والتغلب على العمليات المتساقطة مع الحفاظ على الصحة العامة لنظام التشغيل ، إلخ.
في الوضع العادي ، عندما تصل إلى عنوان غير موجود في Cortex-M3 / M4 / M7 ، يتم طرح استثناء BusFault ، وفي حالة عدم وجود معالج ، يتصاعد إلى HardFault. في Cortex-M0 ، لا توجد استثناءات "مفصلة" (MemFault ، BusFault ، UseFault) ، ويتم تصعيد أي حالات فشل على الفور إلى HardFault.
بشكل عام ، لا يمكنك تجاهل HardFault - فقد يكون ذلك نتيجة لتعطل في الأجهزة ، على سبيل المثال ، وسيصبح من غير المتوقع توقع سلوك إضافي للجهاز. ولكن في حالة معينة ، يمكن ويجب القيام بذلك.
Cortex-M3 و Cortex-M4: BusFault غير المحققة
في Cortex-M3 والإصدارات الأحدث ، يعد التحقق من صلاحية العنوان أمرًا بسيطًا للغاية: يجب عليك حظر جميع الاستثناءات (باستثناء ، من الواضح ، غير قابلة للقناع) من خلال سجل FAULTMASK ، وتعطيل معالجة BusFault على وجه التحديد ، ثم الدخول إلى العنوان الذي يتم التحقق منه ومعرفة ما إذا كانت علامة BFARVALID في سجل BFAR ، بمعنى تسجيل عنوان خطأ الحافلة. إذا كنت قد اتخذت ذلك ، فقد حصلت للتو على BusFault ، أي العنوان غير صحيح
يشبه الرمز هذا ، جميع التعريفات والوظائف هي من نظام CMSIS القياسي (بخلاف البائع) ، لذلك يجب أن تعمل على أي M3 أو M4 أو M7:
bool cpu_check_address(volatile const char *address) { static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos); bool is_valid = true; SCB->CFSR |= BFARVALID_MASK; uint32_t mask = __get_FAULTMASK(); __disable_fault_irq(); SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk; *address; if ((SCB->CFSR & BFARVALID_MASK) != 0) { is_valid = false; } SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk; __set_FAULTMASK(mask); return is_valid; }
Cortex-M0 و Cortex-M0 +
مع Cortex-M0 و Cortex-M0 + ، كل شيء أكثر تعقيدًا ، كما قلت أعلاه ، ليس لديهم BusFault وجميع السجلات المقابلة ، ويتم تصاعد الاستثناءات على الفور إلى HardFault. لذلك ، لا يوجد سوى مخرج واحد - لجعل معالج HardFault قادرًا على فهم أن الاستثناء كان متعمدًا ، والعودة إلى الوظيفة التي أطلق عليها ، ويمر هناك علامة تشير إلى وجود HardFault.
ويتم ذلك بحتة في المجمع. في المثال أدناه ، يتم تعيين السجل R5 على 1 ، ويتم كتابة "رقمين سحريين" في السجلات R1 و R2. إذا حدث HardFault بعد محاولة تحميل القيمة إلى العنوان الذي يتم التحقق منه ، فيجب عليه التحقق من قيم R1 و R2 ، وإذا عثر على الأرقام اللازمة ، قم بتعيين R5 على صفر. يتم نقل قيمة R5 إلى رمز syshech من خلال متغير خاص مرتبط بشكل صارم بهذا السجل ، والعنوان المراد تجميعه في مجمّع ضمني ، ونحن نعلم فقط أنه في arm-none-eabi ، يتم وضع المعلمة الأولى للوظيفة في R0.
bool cpu_check_address(volatile const char *address) { (void)address; register uint32_t result __asm("r5"); __asm__ volatile ( "ldr r5, =1 \n" "ldr r1, =0xDEADF00D \n" "ldr r2, =0xCAFEBABE \n" "ldrb r3, [r0] \n" ); return result; }
يبدو رمز معالج HardFault بأبسط أشكاله كما يلي:
__attribute__((naked)) void hard_fault_default(void) { __asm__ volatile ( "movs r0, #4 \n" "mov r2, lr \n" "tst r2, r0 \n" "bne use_psp \n" "mrs r0, msp \n" "b out \n" " use_psp: \n" "mrs r0, psp \n" " out: \n" "ldr r1, [r0, #0x04] \n" "ldr r2, =0xDEADF00D \n" "cmp r1, r2 \n" "bne regular_handler \n" "ldr r1, [r0, #0x08] \n" "ldr r2, =0xCAFEBABE \n" "cmp r1, r2 \n" "bne regular_handler \n" "ldr r1, [r0, #0x18] \n" "add r1, r1, #2 \n" "str r1, [r0, #0x18] \n" "ldr r5, =0 \n" "bx lr \n" " regular_handler: \n" )
في لحظة مغادرة معالج الاستثناءات ، تقوم Cortex بإلقاء السجلات ، التي يُضمن إفسادها بواسطة المعالج (R0-R3 ، R12 ، LR ، PC ...) ، على الحزمة. الجزء الأول - وهو موجود بالفعل في معظم معالجات HardFault الجاهزة ، باستثناء تلك المكتوبة تحت المعدن الخالص - يحدد المكدس: عند العمل في نظام التشغيل ، يمكن أن يكون إما MSP أو PSP ، ولهما عناوين مختلفة. في المشروعات المعدنية المجردة ، عادةً ما يتم تثبيت مكدس MSP (Main Stack Pointer) بشكل مسبق ، بدون التحقق - لأن PSP (Process Stack Pointer) لا يمكن أن يكون هناك بسبب نقص العمليات.
بعد تحديد المكدس المرغوب فيه ووضع عنوانه في R0 ، نقرأ القيمتين R1 (الإزاحة 0x04) و R2 (الإزاحة 0x08) منه ، ومقارنته بالكلمات السحرية ، إذا كان كلاهما متطابقين - نقرأ قيمة الكمبيوتر (الإزاحة 0x18) من المكدس ، أضف 2 (2 بايت - حجم التعليمات على Cortex-M *) وحفظه مرة أخرى على المكدس. إذا لم يتم ذلك ، فعندما نعود من المعالج ، سنجد أنفسنا على نفس التعليمات التي تسببت بالفعل في حدوث الاستثناء ، وسنعمل دائمًا في دوائر. الملحق 2 ينتقل بنا إلى التعليمات التالية في وقت العودة.
* التحديث. في التعليقات ، نشأ سؤال حول حجم الإرشادات الموجودة على Cortex-M ، سأقوم بالإجابة الصحيحة هنا: في هذه الحالة ، يتسبب التعطل في تعليمة LDRB ، المتوفرة في بنية ARMv7-M في نسختين - 16 بت و 32 بت. سيتم تحديد الخيار الثاني في حالة تحقق أحد الشروط على الأقل:
- أشار المؤلف بشكل صريح إلى التعليمات LDRB.W بدلاً من LDRB (نحن لا)
- تستخدم السجلات أعلاه R7 (بالنسبة لنا - R0 و R3)
- تم تحديد إزاحة أكبر من 31 بايت (ليس لدينا إزاحة)
في جميع الحالات الأخرى (على سبيل المثال ، عندما تتوافق المعاملات مع تنسيق الإصدار 16 بت من التعليمات) ، يجب على المجمع اختيار الإصدار 16 بت.
لذلك ، في حالتنا ، سيكون هناك دائمًا تعليمة ثنائية البايت يجب تجاوزها ، ولكن إذا قمت بتحرير الكود بشدة ، فإن الخيارات ممكنة.بعد ذلك ، اكتب 0 في R5 ، والذي يعد بمثابة مؤشر للدخول إلى HardFault. لا يتم حفظ السجلات بعد R3 في المجموعة قبل السجلات الخاصة ، وعند الخروج من المعالج ، لا تتم استعادتها بأي شكل من الأشكال ، لذلك من ضميرنا أن يفسدها أو لا يفسدها. في هذه الحالة ، نقوم بتغيير R5 من 1 إلى 0 عن قصد.
تتم العودة من معالج المقاطعة في طريقة واحدة بالضبط. عند الدخول إلى المعالج ، يتم كتابة قيمة خاصة في سجل LR يسمى EXC_RETURN ، وهو ضروري للكتابة إلى الكمبيوتر للخروج من المعالج - وليس فقط كتابته ، ولكن القيام بذلك باستخدام أمر POP أو BX (أي ، "mov pc ، lr ، على سبيل المثال ، لا يعمل ، على الرغم من أن أول مرة قد يبدو لك أنه يعمل). يبدو BX LR بمثابة محاولة للانتقال إلى عنوان بلا معنى (في LR سيكون هناك شيء مثل 0xFFFFFFF1 لا علاقة له بالعنوان الحقيقي للإجراء الذي نحتاج إلى العودة إليه) ، ولكن في الواقع يرى المعالج هذه القيمة على الكمبيوتر (حيث سيذهب تلقائيًا) ، ستقوم باستعادة السجلات من المكدس ومواصلة تنفيذ الإجراء الخاص بنا - مع الإجراء التالي بعد استدعاء HardFault نظرًا لأننا قمنا بزيادة عدد أجهزة الكمبيوتر يدويًا في هذه المجموعة بمقدار 2.
يمكنك أن تقرأ عن جميع الإزاحات والأوامر
بوضوح أين ، بالطبع.
حسنًا ، أو إذا كانت الأرقام السحرية غير مرئية ، فسيذهب كل شيء إلى معالج عادي ، وبعد ذلك يتبع إجراء معالجة HardFault المعتاد - كقاعدة عامة ، هذه وظيفة تقوم بطباعة قيم التسجيل على وحدة التحكم ، وتقرر ما يجب القيام به بعد ذلك مع المعالج ، إلخ.
تحديد حجم ذاكرة الوصول العشوائي
استخدام كل هذا بسيط ومباشر. نريد أن نكتب البرامج الثابتة التي تعمل على عدة ميكروكنترولر مع كميات مختلفة من ذاكرة الوصول العشوائي ، بينما في كل مرة تستخدم ذاكرة الوصول العشوائي في برنامج كامل؟
نعم سهل:
static uint32_t cpu_find_memory_size(char *base, uint32_t block, uint32_t maxsize) { char *address = base; do { address += block; if (!cpu_check_address(address)) { break; } } while ((uint32_t)(address - base) < maxsize); return (uint32_t)(address - base); } uint32_t get_cpu_ram_size(void) { return cpu_find_memory_size((char *)SRAM_BASE, 4096, 80*1024); }
هناك حاجة إلى تكبير الحجم هنا ، حيث قد لا توجد فجوة في cpu_check_address عند أقصى قدر ممكن من ذاكرة الوصول العشوائي بينه وبين المجموعة التالية من العناوين. في هذا المثال ، يكون 80 كيلو بايت. كما أنه ليس من المنطقي البحث في جميع العناوين - ما عليك سوى إلقاء نظرة على ورقة البيانات لمعرفة الحد الأدنى للخطوة الممكنة بين طرازي وحدة التحكم وتعيينها ككتلة.
الانتقال البرنامجي إلى أداة تحميل التشغيل الموجودة في منتصف اللا مكان
في بعض الأحيان ، يمكنك القيام ببعض الحيل الصعبة - على سبيل المثال ، تخيل أنك تريد الانتقال برمجيًا إلى أداة تحميل مصنع STM32 في المصنع للتبديل إلى وضع تحديث البرنامج الثابت عبر UART أو USB ، دون عناء كتابة برنامج تحميل الإقلاع.
توجد أداة تحميل التشغيل STM32 في المنطقة المسماة System Memory ، والتي تحتاج إلى الانتقال إليها ، ولكن هناك مشكلة واحدة - تحتوي هذه المنطقة على عناوين مختلفة ليس فقط على سلسلة مختلفة من المعالجات ، ولكن على طرازات مختلفة من نفس السلسلة (يمكن العثور على لوحة ملحمية في
AN2606 على الصفحات 22 إلى 26). عندما تضيف الوظيفة المقابلة إلى النظام الأساسي بشكل عام ، وليس فقط لمنتج معين ، فأنت تريد براعة.
في ملفات CMSIS ، يفتقد أيضًا عنوان البدء لذاكرة النظام. لا يمكن تحديده بواسطة معرف Bootloader ، لأن هذه مشكلة في الدجاج والبيض - يكمن معرّف Bootloader في آخر بايت من ذاكرة النظام ، مما يعيدنا إلى مسألة العنوان.
ومع ذلك ، إذا نظرنا إلى بطاقة الذاكرة STM32 ، فسنرى شيئًا من هذا القبيل:

في هذه الحالة ، نحن مهتمون ببيئة ذاكرة النظام - على سبيل المثال ، في الأعلى منطقة قابلة للبرمجة مرة واحدة (وليس في جميع STM32) وخيارات البايت (في الكل). تتم ملاحظة هذا الهيكل ليس فقط في الطرز المختلفة ، ولكن في سطور STM32 المختلفة ، مع وجود الفرق الوحيد في وجود OTP ووجود فجوة في العناوين بين ذاكرة النظام والخيارات.
ولكن بالنسبة لنا في هذه الحالة ، فإن أهم شيء هو أن عنوان البدء لخيارات البايت موجود في رؤوس CMSIS العادية - يطلق عليه OB_BASE هناك.
مزيد من البساطة. نكتب الوظيفة للبحث عن أول عنوان صالح أو غير صحيح لأسفل أو أعلى من العنوان المحدد:
char *cpu_find_next_valid_address(char *start, char *stop, bool valid) { char *address = start; while (true) { if (address == stop) { return NULL; } if (cpu_check_address(address) == valid) { return address; } if (stop > start) { address++; } else { address--; } }; return NULL; }
وانظر لأسفل من خيارات البايت ، أول نهاية لذاكرة النظام ، أو OTP المجاور لها ، ثم بداية ذاكرة النظام في مسارين:
char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; c = cpu_find_next_valid_address(a, b, true); c = cpu_find_next_valid_address(c, b, false) + 1;
وبدون صعوبة كبيرة ، نقوم بترتيب هذا في وظيفة تعثر على بداية ذاكرة النظام وتقفز عليها ، أي أنها تقوم بتشغيل أداة تحميل التشغيل:
static void jump_to_bootloader(void) __attribute__ ((noreturn)); static void jump_to_bootloader(void) { char *a, *b, *c; a = (char *)(OB_BASE - 1); b = 0; c = cpu_find_next_valid_address(a, b, true); c = cpu_find_next_valid_address(c, b, false) + 1; if (!c) { NVIC_SystemReset(); } uint32_t boot_addr = (uint32_t)c; uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr); uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4); void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr); __set_MSP(boot_stack_ptr); dfu_bootloader(); while (1); }
ذلك يعتمد على طراز المعالج المحدد ... لا شيء يعتمد. لن يعمل المنطق على الطرز التي بها فجوة بين OTP وذاكرة النظام - لكنني لم أتحقق مما إذا كان هناك أي منها على الإطلاق. سوف تعمل بنشاط مع مكتب المدعي العام - تحقق.
تنطبق الحيل الأخرى فقط على الإجراء المعتاد للاتصال بووتلوأدر من التعليمات البرمجية الخاصة بك - لا تنس إعادة تعيين مؤشر المكدس واستدعاء الإجراء لترك محمل الإقلاع قبل تهيئة الأجهزة الطرفية في المعالج وسرعات الساعة ، وما إلى ذلك: بسبب بساطتها ، يمكن لل بووتلوأد تهيئة المحيط ونتوقع أن يكون في الحالة الافتراضية. من الخيارات الجيدة للاتصال بووتلوأدر من مكان تعسفي في البرنامج الخاص بك هو الكتابة إلى RTC Backup Register أو ببساطة إلى عنوان معروف في ذاكرة الرقم السحري ، وإعادة تشغيل البرنامج والتحقق في المراحل الأولى من تهيئة هذا الرقم.
ملاحظة: نظرًا لأن جميع العناوين الموجودة في بطاقة ذاكرة المعالج تتم محاذاتها في أسوأ الحالات بحلول 4 ، فسيتم تسريع الإجراء المذكور أعلاه إلى حد كبير بفكرة التنقل خلاله بزيادة قدرها 4 بايت بدلاً من واحدة.
إشعار مهم
ملحوظة: يرجى ملاحظة أن صلاحية عنوان معين على وحدة تحكم معينة لا تشير بالضرورة إلى التواجد الفعلي للوظائف التي قد تكون موجودة في هذا العنوان. على سبيل المثال ، قد يكون عنوان السجل الذي يتحكم في كتلة طرفية اختيارية صالحًا ، على الرغم من أن الكتلة نفسها غير موجودة في هذا النموذج. من جانب الشركة المصنعة ، فإن الحيل الأكثر إثارة للاهتمام ممكنة ، وعادة ما تكون متجذرة في استخدام نفس البلورات لنماذج المعالجات المختلفة. ومع ذلك ، في معظم الحالات ، تعمل هذه الإجراءات وتثبت أنها مفيدة للغاية.