ماذا لو مسكت HardFault؟

ماذا لو مسكت HardFault؟ كيف نفهم ما سبب ذلك؟ كيف تحدد سطر الكود الذي أدى إلى ذلك؟ دعونا نفهم ذلك.



مرحبا! من الصعب العثور على مبرمج متحكم لم يسبق له أن واجه إخفاقًا شديدًا. في كثير من الأحيان ، لا تتم معالجتها بأي شكل من الأشكال ، ولكنها تظل معلقة في حلقة لا نهائية من المعالج المتوفر في ملف بدء التشغيل الخاص بالشركة المصنعة. في نفس الوقت ، يحاول المبرمج إيجاد سبب الفشل بشكل بديهي. في رأيي ، هذه ليست أفضل طريقة لحل المشكلة.



في هذه المقالة ، أريد أن أصف تقنية لتحليل الإخفاقات الشديدة لوحدات التحكم الدقيقة الشائعة باستخدام نواة Cortex M3 / M4. على الرغم من أن "التقنية" ربما تكون كلمة عالية جدًا. بدلاً من ذلك ، سأأخذ مثالاً فقط على كيفية تحليل حدوث الإخفاقات الشديدة وإظهار ما يمكن فعله في موقف مماثل. سأستخدم برنامج IAR ولوحة تصحيح الأخطاء STM32F4DISCOVERY ، حيث يمتلك العديد من المبرمجين الطموحين هذه الأدوات. ومع ذلك ، هذا غير ذي صلة تمامًا ، يمكن تكييف هذا المثال مع أي معالج من العائلة وأي بيئة تطوير.







الوقوع في HardFault



قبل أن تحاول تحليل HatdFault ، عليك الدخول فيه. هناك طرق عديدة للقيام بذلك. حدث لي على الفور محاولة تبديل المعالج من حالة الإبهام إلى حالة ARM عن طريق تعيين عنوان تعليمات القفز غير المشروط إلى رقم زوجي.



استطرادية صغيرة. كما تعلم ، تستخدم المتحكمات الدقيقة لعائلة Cortex M3 / M4 مجموعة تعليمات التجميع Thumb-2 وتعمل دائمًا في وضع الإبهام. وضع ARM غير مدعوم. إذا حاولت تعيين قيمة عنوان الانتقال غير المشروط (BX reg) مع إزالة البت الأقل أهمية ، فسيحدث استثناء UsageFault ، حيث سيحاول المعالج تبديل حالته إلى ARM. يمكنك قراءة المزيد عن هذا في [1] (البنود 2.8 مجموعة التعليمات ؛ 4.3.4 لغة المجمع: الاتصال والفرع غير المشروط).



بادئ ذي بدء ، أقترح محاكاة قفزة غير مشروطة إلى عنوان زوجي في C / C ++. للقيام بذلك ، سأقوم بإنشاء وظيفة func_hard_fault ، ثم سأحاول تسميتها بالمؤشر ، بعد تقليل عنوان المؤشر بمقدار واحد. ويمكن القيام بذلك على النحو التالي:



void func_hard_fault(void);

void main(void)
{
  void (*ptr_hard_fault_func) (void); //    
  ptr_hard_fault_func = reinterpret_cast<void(*)()>(reinterpret_cast<uint8_t *>(func_hard_fault) - 1); //      
  ptr_hard_fault_func(); //  
  
  while(1) continue;
}

void func_hard_fault(void) //,     
{
  while(1) continue;
}


دعنا نرى مع مصحح الأخطاء ما فعلته.







باللون الأحمر ، قمت بتمييز تعليمات الانتقال الحالية على العنوان الموجود في RON R1 ، والذي يحتوي على عنوان انتقال منتظم. نتيجة لذلك:







يمكن إجراء هذه العملية بشكل أكثر بساطة باستخدام إدخالات المجمّع:



void main(void)
{
  //    
  asm("LDR R1, =0x0800029A"); //-    f
  asm("BX r1"); //   R1
  
  while(1) continue;
}


الصيحة ، وصلنا إلى HardFault ، أنجزت المهمة!



تحليل HardFault



من أين وصلنا إلى HardFault؟



في رأيي ، أهم شيء هو معرفة من أين وصلنا إلى HardFault. هذا ليس من الصعب القيام به. أولاً ، دعنا نكتب المعالج الخاص بنا لموقف HardFault.



extern "C"
{
void HardFault_Handler(void)
{
  
}
}


الآن دعنا نتحدث عن كيفية اكتشاف كيف وصلنا إلى هنا. يحتوي نواة معالج Cortex M3 / M4 على شيء رائع مثل الحفاظ على السياق [1] (الفقرة 9.1.1 التراص). بعبارات بسيطة ، عند حدوث أي استثناء ، يتم تخزين محتويات سجلات R0-R3 و R12 و LR و PC و PSR على المكدس.







هنا سيكون أهم سجل بالنسبة لنا هو سجل الكمبيوتر ، والذي يحتوي على معلومات حول التعليمات الجاري تنفيذها حاليًا. نظرًا لأنه تم دفع قيمة السجل إلى المكدس في وقت الاستثناء ، فستحتوي على عنوان آخر تعليمات تم تنفيذها. تعتبر بقية السجلات أقل أهمية للتحليل ، ولكن يمكن انتزاع شيء مفيد منها. LR هو عنوان المرسل للانتقال الأخير ، R0-R3 ، R12 هي قيم يمكن أن تخبر في أي اتجاه يتحرك ، PSR هو مجرد سجل عام لحالة البرنامج.



أقترح معرفة قيم السجلات في المعالج. للقيام بذلك ، كتبت الكود التالي (رأيت رمزًا مشابهًا في أحد ملفات الشركة المصنعة):



extern "C"
{
void HardFault_Handler(void)
{
  struct 
  {
    uint32_t r0;
    uint32_t r1;
    uint32_t r2;
    uint32_t r3;
    uint32_t r12;
    uint32_t lr;
    uint32_t pc;
    uint32_t psr;
  }*stack_ptr; //    (SP)

 
  asm(
      "TST lr, #4 \n" // 3   ( )
      "ITE EQ \n"   //     3?
      "MRSEQ %[ptr], MSP  \n"  //,    
      "MRSNE %[ptr], PSP  \n"  //,    
      : [ptr] "=r" (stack_ptr)
      );

  while(1) continue;
}
}


نتيجة لذلك ، لدينا قيم جميع السجلات المحفوظة:







ماذا حدث هنا؟ أولاً ، حصلنا على مؤشر المكدس stack_ptr ، كل شيء واضح هنا. تنشأ الصعوبات عند إدخال المُجمِّع (إذا كانت هناك حاجة لفهم تعليمات التجميع الخاصة بـ Cortex ، فأنا أوصي [2]).



لماذا لم نحفظ المكدس عبر MRS stack_ptr ، MSP؟ الحقيقة هي أن نوى Cortex M3 / M4 لها مؤشرا مكدس [1] (البند 3.1.3 Stack Pointer R13) - مؤشر مكدس MSP الرئيسي ومؤشر مكدس عملية PSP. يتم استخدامها لأوضاع المعالج المختلفة. لن أتعمق في ما يتم من أجله وكيف يعمل ، لكني سأقدم شرحًا بسيطًا.



لمعرفة وضع تشغيل المعالج (المستخدم في MSP أو PSP) ، تحتاج إلى التحقق من البتة الثالثة من سجل الاتصالات. يحدد هذا البت مؤشر المكدس المستخدم للعودة من استثناء. إذا تم تعيين هذا البت ، فسيكون MSP ، وإذا لم يكن كذلك ، فسيكون PSP. بشكل عام ، تستخدم معظم التطبيقات المكتوبة بلغة C / C ++ فقط MSPs ، ويمكن حذف هذا الاختيار.



إذن ما هو بيت القصيد؟ بوجود قائمة بالسجلات المحفوظة ، يمكننا بسهولة تحديد مكان سقوط البرنامج في HardFault من سجل الكمبيوتر الشخصي. يشير الكمبيوتر الشخصي إلى العنوان 0x0800029A ، وهو عنوان تعليمات "القطع". لا تنسى أهمية قيم السجلات المتبقية.



سبب HardFault



في الواقع ، يمكننا أيضًا معرفة سبب خطأ HardFault. سيساعدنا سجلان في هذا. سجل حالة الخطأ الثابت (HFSR) وسجل حالة الخطأ القابل للتكوين (CFSR ؛ UFSR + BFSR + MMFSR). يتكون سجل CFSR من ثلاثة سجلات: سجل حالة خطأ الاستخدام (UFSR) ، وسجل حالة خطأ الناقل (BFSR) ، وسجل عنوان خطأ إدارة الذاكرة (MMFSR). يمكنك أن تقرأ عنها ، على سبيل المثال ، في [1] و [3].



أقترح أن أرى ما تنتجه هذه السجلات في حالتي:







أولاً ، تم تعيين بت HFSR FORCED. هذا يعني أنه قد حدث فشل لا يمكن معالجته. لمزيد من التشخيصات ، افحص باقي سجلات حالة الخطأ.



ثانيًا ، يتم تعيين بت CFSR INVSTATE. وهذا يعني حدوث خطأ في الاستخدام لأن المعالج حاول تنفيذ تعليمات تستخدم EPSR بشكل غير قانوني.



ما هو EPSR؟ EPSR - سجل حالة برنامج التنفيذ. هذا سجل PSR داخلي - سجل حالة برنامج خاص (والذي ، كما نتذكر ، يتم تخزينه على المكدس). يشير الجزء الرابع والعشرون من هذا السجل إلى الحالة الحالية للمعالج (الإبهام أو ARM). هذا يمكن أن يحدد سبب فشلنا. دعنا نحاول حسابها:



volatile uint32_t EPSR = 0xFFFFFFFF;
  
  asm(
    "MRS %[epsr], PSP  \n"
    : [epsr] "=r" (EPSR)
      );


نتيجة للتنفيذ ، حصلنا على قيمة EPSR = 0.







اتضح أن السجل يظهر حالة ARM ووجدنا سبب الفشل؟ ليس صحيحا. في الواقع ، وفقًا لـ [3] (ص 23) ، فإن قراءة هذا السجل باستخدام أمر MSR خاص يؤدي دائمًا إلى إرجاع الصفر. ليس من الواضح جدًا بالنسبة لي سبب عمله بهذه الطريقة ، لأن هذا السجل للقراءة فقط بالفعل ، ولكن هنا لا يمكن قراءته بالكامل (يمكن استخدام بعض وحدات البت فقط عبر xPSR). ربما تكون هذه بعض القيود المعمارية.



نتيجة لذلك ، للأسف ، كل هذه المعلومات لا تعطي عمليا أي شيء لمبرمج MK عادي. هذا هو السبب في أنني أعتبر كل هذه السجلات مجرد إضافة إلى تحليل السياق المخزن.



ومع ذلك ، على سبيل المثال ، إذا كان الفشل ناتجًا عن القسمة على الصفر (يُسمح بهذا الفشل عن طريق تعيين بت DIV_0_TRP من سجل CCR) ، فسيتم تعيين بت DIVBYZERO في سجل CFSR ، والذي سيوضح لنا سبب هذا الفشل بالذات.



ماذا بعد؟



ما الذي يمكن عمله بعد تحليل سبب الفشل؟ يبدو أن الإجراء التالي خيار جيد:



  1. اطبع قيم جميع السجلات التي تم تحليلها إلى وحدة التحكم في التصحيح (printf). لا يمكن القيام بذلك إلا إذا كان لديك مصحح أخطاء JTAG.
  2. حفظ معلومات الفشل في الفلاش الداخلي أو الخارجي (إن وجد). من الممكن أيضًا عرض قيمة تسجيل الكمبيوتر على شاشة الجهاز (إن وجدت).
  3. أعد تحميل المعالج NVIC_SystemReset ().






المصادر



  1. جوزيف يو. الدليل النهائي لـ ARM Cortex-M3.
  2. دليل المستخدم العام لأجهزة Cortex-M3.
  3. دليل البرمجة STM32 Cortex-M4 MCUs و MPUs.



All Articles