Débogage des microcontrôleurs ARM Cortex-M par UART partie 2

Dans le dernier article, j'ai parlé de l'interruption DebugMon et des registres qui lui sont associés.

Dans cet article, nous allons écrire l'implémentation du débogueur pour UART.

Pièce de bas niveau


Ici et ici, il y a une description de la structure des requêtes et des réponses du serveur GDB. Bien que cela semble simple, nous ne l'implémenterons pas dans le microcontrôleur pour les raisons suivantes:

  • Redondance du Big Data. Les adresses, les valeurs des registres, les variables sont codées sous forme de chaîne hexadécimale, ce qui augmente le volume des messages de 2 fois
  • L'analyse et la collecte de messages nécessiteront des ressources supplémentaires
  • Le suivi de la fin du paquet est requis soit par timeout (le temporisateur sera occupé), soit par une machine automatique complexe, ce qui augmentera le temps passé dans l'interruption UART

Pour obtenir le module de débogage le plus simple et le plus rapide, nous utiliserons un protocole binaire avec des séquences de contrôle:

  • 0xAA 0xFF - Début de trame
  • 0xAA 0x00 - Fin de trame
  • 0xAA 0xA5 - Interruption
  • 0xAA 0xAA - Remplacé par 0xAA

Pour traiter ces séquences lors de la réception, un automate à 4 états est nécessaire:

  • En attente du caractère ESC
  • En attente du deuxième caractère de la séquence de début de trame
  • Réception des données
  • La dernière fois qu'un caractère Esc a été accepté

Mais pour envoyer des états, il vous faut déjà 7:

  • Envoi du premier octet Début de la trame
  • Envoi d'un deuxième octet Début de trame
  • Envoi de données
  • Envoi de fin de trame
  • Envoi du remplacement du caractère Esc
  • Envoi du premier octet d'interruption
  • Envoi du deuxième octet d'interruption

Écrivons la définition de la structure à l'intérieur de laquelle toutes les variables du module seront situées:

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

Les états des machines réceptrices et émettrices sont combinés en une seule variable puisque le travail sera effectué en mode semi-duplex. Vous pouvez maintenant écrire les automates eux-mêmes avec un gestionnaire d'interruption.

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


Tout est assez simple ici. Selon l'événement qui s'est produit, le gestionnaire d'interruption appelle soit une machine réceptrice soit une machine émettrice. Pour vérifier que tout fonctionne, nous écrivons un gestionnaire de paquets qui répond avec un octet:

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

Compilez, cousez, exécutez. Le résultat est visible sur l'écran, ça a marché.

Échange de test


Ensuite, vous devez implémenter des analogues de commande à partir du protocole du serveur GDB:

  • lecture de mémoire
  • enregistrement de mémoire
  • arrêt du programme
  • exécution continue
  • lecture du registre du noyau
  • entrée de registre du noyau
  • définir un point d'arrêt
  • supprimer le point d'arrêt

La commande sera codée avec le premier octet de données. Les codes d'équipes ont des numéros dans l'ordre de leur mise en œuvre:

  • 2 - lire la mémoire
  • 3 - enregistrement en mémoire
  • 4 - arrêter
  • 5 - suite
  • 6 - lire le cas
  • 7 - installer le point d'arrêt
  • 8 - effacement du point d'arrêt
  • 9 - étape (échec de mise en œuvre)
  • 10 - entrée de registre (non implémentée)

Les paramètres seront transmis dans les octets de données suivants.

La réponse ne contiendra pas le numéro de commande, comme nous savons déjà quelle équipe a envoyé.

Pour empêcher le module de provoquer des exceptions BusFault pendant les opérations de lecture / écriture, vous devez le masquer lorsqu'il est utilisé sur M3 ou supérieur, ou écrire un gestionnaire HardFault pour M0.

Memcpy sûr
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;
}


Le réglage du point d'arrêt est implémenté en recherchant le premier registre inactif FP_COMP.

Code d'installation des points d'arrêt
	
  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;


Le nettoyage se fait en recherchant le point d'arrêt défini. L'arrêt de l'exécution définit un point d'arrêt sur le PC actuel. En quittant l'interruption UART, le noyau entre immédiatement dans DebugMon_Handler.

Le gestionnaire DebugMon lui-même est très simple:

  • 1. L'indicateur d'arrêt de l'exécution est défini.
  • 2. Tous les points d'arrêt définis sont effacés.
  • 3. En attente de la fin de l'envoi d'une réponse à la commande dans uart (si elle n'a pas eu le temps d'aller)
  • 4. L'envoi de la séquence d'interruption commence
  • 5. Dans la boucle, les gestionnaires des machines d'émission et de réception sont appelés jusqu'à ce que l'indicateur d'arrêt soit abaissé.

Code du gestionnaire 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(); 
  }
}


Lire les registres du noyau de C-Syshny lorsque la tâche est problématique, j'ai donc réécrit une partie du code sur ASM. Le résultat est que ni DebugMon_Handler, ni le gestionnaire d'interruption UART, ni les machines n'utilisent la pile. Cela a simplifié la définition des valeurs de registre du noyau.

Serveur Gdb


La partie microcontrôleur du débogueur fonctionne, écrivons maintenant le lien entre l'IDE et notre module.

À partir de zéro, l'écriture d'un serveur de débogage n'a pas de sens, alors prenons un serveur prêt à l'emploi comme base. Étant donné que j'ai le plus d'expérience dans le développement de programmes sur .net, j'ai pris ce projet comme base et l' ai réécrit pour d'autres exigences. Il serait plus correct d'ajouter la prise en charge de la nouvelle interface dans OpenOCD, mais cela prendrait plus de temps.

Au démarrage, le programme demande avec quel port COM travailler, puis commence à écouter sur le port TCP 3333 et attend que le client GDB se connecte.

Toutes les commandes du protocole GDB sont traduites en un protocole binaire.

En conséquence, une implémentation de débogage UART réalisable a été publiée.

Résultat final


Conclusion


Il s'est avéré que le débogage du contrôleur lui-même n'est pas quelque chose de super compliqué.
Théoriquement, en plaçant ce module dans une section de mémoire distincte, il peut également être utilisé pour flasher le contrôleur.

Les fichiers sources ont été postés sur GitHub pour l'étude générale de la

partie microcontrôleur du
serveur GDB

All Articles