Depuración de microcontroladores ARM Cortex-M por UART Parte 2

En el último artículo, hablé sobre la interrupción de DebugMon y los registros asociados con ella.

En este artículo escribiremos la implementación del depurador para UART.

Parte de bajo nivel


Aquí y aquí hay una descripción de la estructura de solicitudes y respuestas del servidor GDB. Aunque parezca simple, no lo implementaremos en el microcontrolador por las siguientes razones:

  • Gran redundancia de datos. Las direcciones, los valores de los registros y las variables se codifican como una cadena hexadecimal, lo que aumenta el volumen del mensaje 2 veces
  • Analizar y recopilar mensajes requerirá recursos adicionales
  • Es necesario realizar un seguimiento del final del paquete ya sea por tiempo de espera (el temporizador estará ocupado) o por una máquina automática compleja, lo que aumentará el tiempo pasado en la interrupción UART

Para obtener el módulo de depuración más fácil y rápido, utilizaremos un protocolo binario con secuencias de control:

  • 0xAA 0xFF - Inicio de trama
  • 0xAA 0x00 - Fin del marco
  • 0xAA 0xA5 - Interrupción
  • 0xAA 0xAA - Reemplazado por 0xAA

Para procesar estas secuencias durante la recepción, se requiere un autómata con 4 estados:

  • Esperando al personaje ESC
  • Esperando el segundo carácter de la secuencia de inicio de trama
  • Recepción de datos
  • La última vez que se aceptó un personaje Esc

Pero para enviar estados, ya necesita 7:

  • Envío del primer byte Inicio de trama
  • Enviar un segundo byte Inicio de trama
  • Enviando datos
  • Enviar fin de marco
  • Envío de reemplazo de personaje Esc
  • Enviar el primer byte de interrupción
  • Envío del segundo byte de interrupción

Escribamos la definición de la estructura dentro de la cual se ubicarán todas las variables del módulo:

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

Los estados de las máquinas receptoras y transmisoras se combinan en una variable, ya que el trabajo se realizará en modo semidúplex. Ahora puede escribir autómatas con un controlador de interrupciones.

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


Aquí todo es bastante simple. Dependiendo del evento que ocurrió, el controlador de interrupción llama a una máquina receptora o una máquina de transmisión. Para verificar que todo funciona, escribimos un controlador de paquetes que responde con un 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, coser, correr. El resultado es visible en la pantalla, funcionó.

Prueba de intercambio


A continuación, debe implementar comandos análogos del protocolo del servidor GDB:

  • lectura de memoria
  • registro de memoria
  • parada del programa
  • ejecución continua
  • registro del núcleo leído
  • entrada de registro de kernel
  • establecer un punto de interrupción
  • eliminar punto de interrupción

El comando se codificará con el primer byte de datos. Los códigos de los equipos tienen números en el orden de su implementación:

  • 2 - leer memoria
  • 3 - registro de memoria
  • 4 - parada
  • 5 - continuación
  • 6 - leer caso
  • 7 - instalar punto de interrupción
  • 8 - despejar el punto de interrupción
  • 9 - paso (no se pudo implementar)
  • 10 - registro de entrada (no implementado)

Los parámetros se transmitirán en los siguientes bytes de datos.

La respuesta no contendrá el número de comando, como Ya sabemos qué equipo envió.

Para evitar que el módulo genere excepciones de BusFault durante las operaciones de lectura / escritura, debe enmascararlo cuando se usa en M3 o superior, o escribir un controlador HardFault para 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;
}


La configuración del punto de interrupción se implementa buscando el primer registro inactivo FP_COMP.

Código de instalación de puntos de interrupción
	
  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;


La limpieza se realiza buscando el punto de interrupción establecido. La detención de la ejecución establece un punto de interrupción en la PC actual. Al salir de la interrupción UART, el núcleo ingresa inmediatamente a DebugMon_Handler.

El controlador DebugMon en sí es muy simple:

  • 1. Se establece la bandera para detener la ejecución.
  • 2. Todos los puntos de interrupción establecidos se borran.
  • 3. Esperando la finalización del envío de una respuesta al comando en uart (si no tuvo tiempo de irse)
  • 4. Comienza el envío de la secuencia de interrupción
  • 5. En el bucle, se llama a los manejadores de las máquinas de transmisión y recepción hasta que se baja la bandera de detención.

Código de controlador 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(); 
  }
}


Leer los registros del kernel de C-Syshny cuando la tarea es problemática, por lo que reescribí parte del código en ASM. El resultado es que ni DebugMon_Handler, ni el controlador de interrupciones UART, ni las máquinas usan la pila. Esto simplificó la definición de los valores de registro del núcleo.

Servidor gdb


La parte del microcontrolador del depurador funciona, ahora escribamos el enlace entre el IDE y nuestro módulo.

Desde cero, escribir un servidor de depuración no tiene sentido, así que tomemos uno listo como base. Como tengo la mayor experiencia en el desarrollo de programas en .net, tomé este proyecto como base y lo reescribí para otros requisitos. Sería más correcto agregar soporte para la nueva interfaz en OpenOCD, pero tomaría más tiempo.

Al inicio, el programa pregunta con qué puerto COM trabajar, luego comienza a escuchar en el puerto TCP 3333 y espera a que se conecte el cliente GDB.

Todos los comandos del protocolo GDB se traducen en un protocolo binario.

Como resultado, se lanzó una implementación de depuración UART viable.

Resultado final


Conclusión


Resultó que depurar el controlador en sí no es algo súper complicado.
Teóricamente, al colocar este módulo en una sección de memoria separada, también se puede usar para actualizar el controlador.

Los archivos fuente se publicaron en GitHub para el estudio general de la

parte del microcontrolador del
servidor GDB

All Articles