Отладка микроконтроллеров ARM Cortex-M по UART Часть 2

от автора

В прошлой статье я рассказывал про прерывание DebugMon и регистры с ним связанные.

В этой статье будем писать реализацию отладчика по UART.

Низкоуровневая часть

Тут и тут есть описание структуры запросов и ответов GDB сервера. Хоть оно и кажется простым, но реализовывать в микроконтроллере его мы не будем по следующим причинам:

  • Большая избыточность данных. Адреса, значения регистров, переменных кодируются в виде hex-строки, что увеличивает объем сообщений в 2 раза
  • Парсить и собирать сообщения займет дополнительные ресурсы
  • Отслеживать конец пакета требуется либо по таймауту (будет занят таймер), либо сложным автоматом, что увеличит время нахождения в прерывании UART

Для получения наиболее легкого и быстрого модуля отладки будем использовать бинарный протокол с управляющими последовательностями:

  • 0хАА 0xFF — Start of frame
  • 0xAA 0x00 — End of frame
  • 0xAA 0xA5 — Interrupt
  • 0xAA 0xAA — Заменяется на 0xAA

Для обработки этих последовательностей при приеме потребуется автомат с 4мя состояниями:

  • Ожидание ESC символа
  • Ожидание второго символа последовательности Start of frame
  • Прием данных
  • Прошлый раз был принят Esc символ

А вот для отправки состояний потребуется уже 7:

  • Отправка первого байта Start of frame
  • Отправка второго байта Start of frame
  • Отправка данных
  • Отправка End of frame
  • Отправка Esc символа замены
  • Отправка первого байта Interrupt
  • Отправка второго байта Interrupt

Напишем определение структуры, внутри которой будут находиться все переменные модуля:

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) // адрес задан жестко, в настройках линкера эта часть озу убирается из доступной

Состояния приемного и передающего автоматов объеденены в одну переменную так как работа будет вестись полудуплексном режиме. Теперь можно писать сами автоматы с обработчиком прерываний.

Обработчик 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 сервера:

  • чтение памяти
  • запись памяти
  • останов программы
  • продолжение выполнения
  • чтение регистра ядра
  • запись регистра ядра
  • установка точки останова
  • удаление точки останова

Команда будет кодироваться первым байтом данных. Коды команд имеют номера в порядке их реализации:

  • 2 — чтение памяти
  • 3 — запись памяти
  • 4 — останов
  • 5 — продолжение
  • 6 — чтение регистра
  • 7 — установка breakpointа
  • 8 — очистка breakpointа
  • 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; }

Установка breakpointа реализуется через поиск первого неактивного регистра FP_COMP.

Код, устанавливающий breakpointы

	   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;

Очистка реализуется через поиск установленной точки останова. Остановка выполнения устанавливает breakpoint на текущий PC. При выходе из прерывания UART, ядро сразу попадает в DebugMon_Handler.

Сам же обработчик DebugMon выполнен очень просто:

  • 1. Устанавливается флаг остановки выполнения.
  • 2. Очищаются все установленные точки останова.
  • 3. Ожидается завершение отправки ответа на команду в uart (если он не успел отправиться)
  • 4. Начинается отправка последовательности Interrupt
  • 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();    } }

Читать регистры ядра из СИшного когда задача проблематичная, поэтому я переписал часть кода на ASM. В результате получилось что ни DebugMon_Handler, ни обработчик прерывания UART, ни автоматы не используют стек. Благодаря этому упростилось определение значений регистров ядра.

GDB server

Микроконтроллерная часть отладчика работает, теперь займемся написанием связующего звена между IDE и нашим модулем.

С нуля писать сервер отладки не имеет смысла, поэтому за основу возьмем готовый. Так как больше всего опыта у меня в разработке программ на .net, взял за основу этот проект и переписал под другие требования. Правильнее было бы дописать поддержку нового интерфейса в OpenOCD, но это бы заняло больше времени.

При запуске программа спрашивает с каким COM портом работать, далее запускает на прослушивание TCP порт 3333 и ждет подключения GDB клиента.

Все команды GDB протокола транслируются в бинарный протокол.

В результате вышла работоспособная реализация отладки по UART.

Итоговый результат

Заключение

Оказалось что отладка контроллером самого себя не является чем-то сверх сложным.
Теоретически, разместив данный модуль в отдельной секции памяти, его можно использовать и для прошивки контроллера.

Исходники выложил на GitHub для всеобщего изучения

Микроконтроллерная часть
GDB сервер

ссылка на оригинал статьи https://habr.com/ru/post/489540/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *