Depurando Microcontroladores ARM Cortex-M pela UART Parte 2

No último artigo, falei sobre a interrupção do DebugMon e os registros associados a ela.

Neste artigo, escreveremos a implementação do depurador para UART.

Peça de baixo nível


Aqui e aqui, há uma descrição da estrutura de solicitações e respostas do servidor GDB. Embora pareça simples, não o implementaremos no microcontrolador pelos seguintes motivos:

  • Redundância de big data. Endereços, valores de registradores, variáveis ​​são codificadas como uma sequência hexadecimal, o que aumenta o volume da mensagem em 2 vezes
  • A análise e a coleta de mensagens exigirão recursos adicionais
  • O rastreamento do final do pacote é necessário por tempo limite (o cronômetro estará ocupado) ou por uma máquina automática complexa, que aumentará o tempo gasto na interrupção do UART

Para obter o módulo de depuração mais fácil e rápido, usaremos um protocolo binário com sequências de controle:

  • 0xAA 0xFF - Início do quadro
  • 0xAA 0x00 - Fim do quadro
  • 0xAA 0xA5 - Interrupção
  • 0xAA 0xAA - Substituído por 0xAA

Para processar essas seqüências durante a recepção, é necessário um autômato com 4 estados:

  • Aguardando o caractere ESC
  • Aguardando o segundo caractere do início da sequência de quadros
  • Recepção de dados
  • A última vez que um caractere Esc foi aceito

Mas para enviar estados, você já precisa do 7:

  • Enviando o primeiro byte Início do quadro
  • Enviando um segundo byte Início do quadro
  • Enviando dados
  • Enviando Fim do quadro
  • Enviando substituição de caracteres Esc
  • Enviando o primeiro byte de interrupção
  • Enviando segundo byte de interrupção

Vamos escrever a definição da estrutura dentro da qual todas as variáveis ​​do módulo estarão localizadas:

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) //   ,         

Os estados das máquinas de recebimento e transmissão são combinados em uma variável, pois o trabalho será realizado no modo half duplex. Agora você pode escrever autômatos com um manipulador de interrupções.

Manipulador 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;
  }
}


Tudo é bem simples aqui. Dependendo do evento que ocorreu, o manipulador de interrupção chama uma máquina receptora ou uma máquina de transmissão. Para verificar se tudo funciona, escrevemos um manipulador de pacotes que responde com um byte:

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);
}

Compilar, costurar, correr. O resultado é visível na tela, funcionou.

Troca de teste


Em seguida, você precisa implementar os análogos de comando do protocolo do servidor GDB:

  • leitura de memória
  • registro de memória
  • parada do programa
  • execução continuada
  • leitura do registro do kernel
  • entrada de registro do kernel
  • definindo um ponto de interrupção
  • excluir ponto de interrupção

O comando será codificado com o primeiro byte de dados. Os códigos das equipes têm números na ordem de sua implementação:

  • 2 - ler memória
  • 3 - registro de memória
  • 4 - parar
  • 5 - continuação
  • 6 - ler caso
  • 7 - instalar ponto de interrupção
  • 8 - ponto de interrupção de limpeza
  • 9 - etapa (falha ao implementar)
  • 10 - entrada de registro (não implementada)

Os parâmetros serão transmitidos nos seguintes bytes de dados.

A resposta não conterá o número do comando, como já sabemos qual equipe enviou.

Para impedir que o módulo cause exceções do BusFault durante operações de leitura / gravação, você deve mascará-lo quando usado no M3 ou superior, ou gravar um manipulador do HardFault para o M0.

Memcpy seguro
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;
}


A configuração do ponto de interrupção é implementada procurando o primeiro registro inativo FP_COMP.

Código de instalação de pontos de interrupção
	
  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;


A limpeza é feita procurando o ponto de interrupção definido. Parar a execução define o ponto de interrupção no PC atual. Ao sair da interrupção do UART, o kernel entra imediatamente no DebugMon_Handler.

O próprio manipulador DebugMon é muito simples:

  • 1. O sinalizador para parar a execução está definido.
  • 2. Todos os pontos de interrupção definidos são limpos.
  • 3. Aguardando a conclusão do envio de uma resposta ao comando em uart (se não tiver tempo para ir)
  • 4. O envio da sequência de interrupção começa
  • 5. No loop, os manipuladores das máquinas de transmissão e recepção são chamados até que o sinalizador de parada seja abaixado.

Código do manipulador 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(); 
  }
}


Leia os registros do kernel do C-Syshny quando a tarefa for problemática, então reescrevi parte do código no ASM. O resultado é que nem DebugMon_Handler, nem o manipulador de interrupções UART, nem as máquinas usam a pilha. Isso simplificou a definição de valores de registro do kernel.

Servidor Gdb


A parte do microcontrolador do depurador funciona, agora vamos escrever o link entre o IDE e nosso módulo.

Do zero, escrever um servidor de depuração não faz sentido; portanto, tomemos um já pronto como base. Como tenho mais experiência no desenvolvimento de programas em .net, tomei esse projeto como base e o reescrevi para outros requisitos. Seria mais correto adicionar suporte para a nova interface no OpenOCD, mas levaria mais tempo.

Na inicialização, o programa pergunta com qual porta COM trabalhar, depois começa a escutar na porta TCP 3333 e aguarda a conexão do cliente GDB.

Todos os comandos do protocolo GDB são convertidos em um protocolo binário.

Como resultado, uma implementação de depuração UART viável foi lançada.

Resultado final


Conclusão


Descobriu-se que a depuração do próprio controlador não é algo super complicado.
Teoricamente, colocando este módulo em uma seção de memória separada, ele também pode ser usado para piscar o controlador.

Os arquivos de origem foram postados no GitHub para o estudo geral da

parte do microcontrolador do
servidor GDB

All Articles