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

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

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

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


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

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


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

  • 0хАА 0xFF — Start of frame
  • 0xAA 0×00 — 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);
}


Компилим, зашиваем, запускаем. Результат виден на скрине, оно заработало.

Тестовый обмен
ytnhaxq7durfoqkvawblnjldrns.png


Далее нужно реализовать аналоги команд из протокола 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 ((cntCCR &= ~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;tmpFP_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;iFP_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.

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


Заключение


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

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

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

© Habrahabr.ru