تصحيح أخطاء وحدات التحكم ARM Cortex-M بواسطة UART Part 2

في المقالة الأخيرة ، تحدثت عن مقاطعة DebugMon والسجلات المرتبطة بها.

في هذه المقالة سنكتب تنفيذ مصحح الأخطاء لـ UART.

جزء منخفض المستوى


هنا و هنا وهناك وصفا للهيكل الطلبات والردود خادم GDB. على الرغم من أن الأمر يبدو بسيطًا ، إلا أننا لن ننفذه في وحدة التحكم الدقيقة للأسباب التالية:

  • تكرار البيانات الضخمة. يتم ترميز العناوين وقيم السجلات والمتغيرات كسلسلة سداسية ، مما يزيد من حجم الرسالة مرتين
  • يتطلب تحليل الرسائل وجمعها موارد إضافية
  • مطلوب تتبع نهاية الحزمة إما عن طريق المهلة (سيكون الموقت مشغولاً) ، أو بواسطة آلة أوتوماتيكية معقدة ، مما سيزيد من الوقت المستغرق في مقاطعة UART

للحصول على أسهل وأسرع وحدة تصحيح ، سنستخدم بروتوكولًا ثنائيًا مع تسلسلات التحكم:

  • 0xAA 0xFF - بداية الإطار
  • 0xAA 0x00 - نهاية الإطار
  • 0xAA 0xA5 - المقاطعة
  • 0xAA 0xAA - تم استبداله بـ 0xAA

لمعالجة هذه التسلسلات أثناء الاستقبال ، يلزم وجود آلة أوتوماتيكية بأربع حالات:

  • في انتظار حرف ESC
  • في انتظار الحرف الثاني من بداية تسلسل الإطار
  • استقبال البيانات
  • آخر مرة تم قبول حرف Esc

ولكن لإرسال الدول ، تحتاج بالفعل إلى 7:

  • إرسال البايت الأول بداية الإطار
  • إرسال بايت الثانية بداية الإطار
  • إرسال البيانات
  • تقديم نهاية الإطار
  • جاري إرسال حرف Esc
  • إرسال بايت المقاطعة الأول
  • إرسال بايت المقاطعة الثانية

دعونا نكتب تعريف الهيكل الذي ستوضع فيه جميع متغيرات الوحدة:

typedef struct 
{    
  // disable receive data
  unsigned tx:1;
  // program stopped
  unsigned StopProgramm:1;
  union {
    enum rx_state_e 
    {
      rxWaitS = 0, // wait Esc symbol
      rxWaitC = 1, // wait Start of frame
      rxReceive = 2, // receiving
      rxEsc = 3, // Esc received
    } rx_state;
    enum tx_state_e 
    {
      txSendS = 0, // send first byte of Start of frame
      txSendC = 1, // send second byte
      txSendN = 2, // send byte of data
      txEsc = 3,   // send escaped byte of data
      txEnd = 4,   // send End of frame
      txSendS2 = 5,// send first byte of Interrupt
      txBrk = 6,   // send second byte
    } tx_state;
  };
  uint8_t pos; // receive/send position
  uint8_t buf[128]; // offset = 3
  uint8_t txCnt; // size of send data
} dbg_t;
#define dbgG ((dbg_t*)DBG_ADDR) //   ,         

يتم دمج حالات آلات الاستقبال والإرسال في متغير واحد حيث سيتم تنفيذ العمل في وضع نصف مزدوج. يمكنك الآن كتابة automata بأنفسهم باستخدام معالج المقاطعة.

معالج UART
void USART6_IRQHandler(void)
{
  if (((USART6->ISR & USART_ISR_RXNE) != 0U)
      && ((USART6->CR1 & USART_CR1_RXNEIE) != 0U))
  {
    rxCb(USART6->RDR);
    return;
  }

  if (((USART6->ISR & USART_ISR_TXE) != 0U)
      && ((USART6->CR1 & USART_CR1_TXEIE) != 0U))
  {
    txCb();
    return;
  }
}

void rxCb(uint8_t byte)
{
  dbg_t* dbg = dbgG; // debug vars pointer
  
  if (dbg->tx) // use half duplex mode
    return;
  
  switch(dbg->rx_state)
  {
  default:
  case rxWaitS:
    if (byte==0xAA)
      dbg->rx_state = rxWaitC;
    break;
  case rxWaitC:
    if (byte == 0xFF)
      dbg->rx_state = rxReceive;
    else
      dbg->rx_state = rxWaitS;
    dbg->pos = 0;
    break;
  case rxReceive:
    if (byte == 0xAA)
      dbg->rx_state = rxEsc;
    else
      dbg->buf[dbg->pos++] = byte;
    break;
  case rxEsc:
    if (byte == 0xAA)
    {
      dbg->buf[dbg->pos++] = byte;
      dbg->rx_state  = rxReceive;
    }
    else if (byte == 0x00)
    {
      parseAnswer();
    }
    else
      dbg->rx_state = rxWaitS;
  }
}

void txCb()
{
  dbg_t* dbg = dbgG;
  switch (dbg->tx_state)
  {
  case txSendS:
    USART6->TDR = 0xAA;
    dbg->tx_state = txSendC;
    break;
  case txSendC:
    USART6->TDR = 0xFF;
    dbg->tx_state = txSendN;
    break;
  case txSendN:
    if (dbg->txCnt>=dbg->pos)
    {
      USART6->TDR = 0xAA;
      dbg->tx_state = txEnd;
      break;
    }
    if (dbg->buf[dbg->txCnt]==0xAA)
    {
      USART6->TDR = 0xAA;
      dbg->tx_state = txEsc;
      break;
    }
    USART6->TDR = dbg->buf[dbg->txCnt++];
    break;
  case txEsc:
    USART6->TDR = 0xAA;
    dbg->txCnt++;
    dbg->tx_state = txSendN;
    break;
  case txEnd:
    USART6->TDR = 0x00;
    dbg->rx_state = rxWaitS;
    dbg->tx = 0;
    CLEAR_BIT(USART6->CR1, USART_CR1_TXEIE);
    break;
  case txSendS2:
    USART6->TDR = 0xAA;
    dbg->tx_state = txBrk;
    break;
  case txBrk:
    USART6->TDR = 0xA5;
    dbg->rx_state = rxWaitS;
    dbg->tx = 0;
    CLEAR_BIT(USART6->CR1, USART_CR1_TXEIE);
    break;
  }
}


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

void parseAnswer()
{
  dbg_t* dbg = dbgG;
  dbg->pos = 1;
  dbg->buf[0] = 0x33;
  dbg->txCnt = 0;
  dbg->tx = 1;
  dbg->tx_state = txSendS;
  SET_BIT(USART6->CR1, USART_CR1_TXEIE);
}

تجميع وخياطة وتشغيل. النتيجة مرئية على الشاشة ، لقد نجحت.

تبادل الاختبار


بعد ذلك ، تحتاج إلى تنفيذ نظائر الأوامر من بروتوكول خادم GDB:

  • قراءة الذاكرة
  • سجل الذاكرة
  • توقف البرنامج
  • استمرار التنفيذ
  • سجل نواة قراءة
  • إدخال سجل kernel
  • تحديد نقطة توقف
  • حذف نقطة التوقف

سيتم ترميز الأمر بالبايت الأول للبيانات. تحتوي رموز الفرق على أرقام حسب ترتيب تنفيذها:

  • 2 - قراءة الذاكرة
  • 3- سجل الذاكرة
  • 4 - توقف
  • 5 - تابع
  • 6 - اقرأ الحالة
  • 7- تثبيت نقطة توقف
  • 8 - المقاصة
  • 9 - الخطوة (فشل في التنفيذ)
  • 10 - تسجيل الدخول (لم ينفذ)

سيتم إرسال المعلمات بالبايت التالي من البيانات.

لن تحتوي الإجابة على رقم الأمر ، مثل نحن نعلم بالفعل الفريق الذي أرسل.

لمنع الوحدة من رفع استثناءات BusFault أثناء عمليات القراءة / الكتابة ، يجب إخفاءها عند استخدامها على M3 أو أعلى ، أو كتابة معالج HardFault لـ M0.

memcpy آمنة
int memcpySafe(uint8_t* to,uint8_t* from, int len)
{
    /* Cortex-M3, Cortex-M4, Cortex-M4F, Cortex-M7 are supported */
    static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos);
    int cnt = 0;

    /* Clear BFARVALID flag by writing 1 to it */
    SCB->CFSR |= BFARVALID_MASK;

    /* Ignore BusFault by enabling BFHFNMIGN and disabling interrupts */
    uint32_t mask = __get_FAULTMASK();
    __disable_fault_irq();
    SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk;

    while ((cnt<len))
    {
      *(to++) = *(from++);
      cnt++;
    }

    /* Reenable BusFault by clearing  BFHFNMIGN */
    SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk;
    __set_FAULTMASK(mask);

    return cnt;
}


يتم تنفيذ إعداد نقطة التوقف بالبحث عن أول سجل غير نشط FP_COMP.

تثبيت نقاط التوقف
	
  dbg->pos = 0; //  -    0
    addr = ((*(uint32_t*)(&dbg->buf[1])))|1; //    FP_COMP
    for (tmp = 0;tmp<8;tmp++) //      breakpoint 
      if (FP->FP_COMP[tmp] == addr)
        break;
    
    if (tmp!=8) //  , 
      break;
    
    for (tmp=0;tmp<NUMOFBKPTS;tmp++) //   
      if (FP->FP_COMP[tmp]==0) // ?
      {
        FP->FP_COMP[tmp] = addr; // 
        break; //  
      }
    break;


يتم التنظيف من خلال البحث عن نقطة التوقف المحددة. يؤدي إيقاف التنفيذ إلى تعيين نقطة توقف على جهاز الكمبيوتر الحالي. عند الخروج من مقاطعة UART ، تدخل النواة على الفور إلى DebugMon_Handler.

معالج DebugMon نفسه بسيط للغاية:

  • 1. يتم تعيين العلم لوقف التنفيذ.
  • 2. يتم مسح جميع نقاط التوقف المحددة.
  • 3. في انتظار اكتمال إرسال رد على الأمر في uart (إذا لم يكن لديه الوقت للذهاب)
  • 4. يبدأ إرسال تسلسل المقاطعة
  • 5. في الحلقة ، يتم استدعاء معالجات أجهزة الإرسال والاستقبال حتى يتم تخفيض إشارة التوقف.

رمز معالج DebugMon
void DebugMon_Handler(void)
{
  dbgG->StopProgramm = 1; //   
  
  for (int i=0;i<NUMOFBKPTS;i++) //  breakpoint
    FP->FP_COMP[i] = 0;
  
  while (USART6->CR1 & USART_CR1_TXEIE) //    
    if ((USART6->ISR & USART_ISR_TXE) != 0U)
      txCb();

  
  dbgG->tx_state = txSendS2; //   Interrupt 
  dbgG->tx = 1;
  SET_BIT(USART6->CR1, USART_CR1_TXEIE);

  while (dbgG->StopProgramm) //       
  {
  	//   UART  
    if (((USART6->ISR & USART_ISR_RXNE) != 0U)
        && ((USART6->CR1 & USART_CR1_RXNEIE) != 0U))
      rxCb(USART6->RDR);

    if (((USART6->ISR & USART_ISR_TXE) != 0U)
        && ((USART6->CR1 & USART_CR1_TXEIE) != 0U))
      txCb(); 
  }
}


قراءة تسجيلات kernel من C-Syshny عندما تكون المهمة صعبة ، لذلك أعدت كتابة بعض التعليمات البرمجية على ASM. والنتيجة أن لا DebugMon_Handler ولا معالج المقاطعة UART ولا الأجهزة تستخدم المكدس. هذا يبسط تعريف قيم تسجيل النواة.

خادم Gdb


يعمل جزء وحدة التحكم الدقيقة في المصحح ، والآن دعنا نكتب الرابط بين IDE ووحدتنا.

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

عند بدء التشغيل ، يسألك البرنامج عن أي منفذ COM للعمل معه ، ثم يبدأ في الاستماع على منفذ TCP 3333 وينتظر اتصال عميل GDB.

تتم ترجمة جميع أوامر بروتوكول GDB إلى بروتوكول ثنائي.

ونتيجة لذلك ، تم إصدار تطبيق تصحيح أخطاء UART قابل للتطبيق.

النتيجة النهائية


استنتاج


اتضح أن تصحيح وحدة التحكم نفسها ليس أمرًا معقدًا للغاية.
نظريًا ، من خلال وضع هذه الوحدة في قسم ذاكرة منفصل ، يمكن أيضًا استخدامها وميض وحدة التحكم.

تم نشر ملفات المصدر على GitHub للدراسة العامة

لجزء متحكم
خادم GDB

All Articles