Debuggen von ARM Cortex-M-Mikrocontrollern durch UART Teil 2

Im letzten Artikel habe ich über den DebugMon-Interrupt und die damit verbundenen Register gesprochen.

In diesem Artikel werden wir die Implementierung des Debuggers für UART schreiben.

Low Level Teil


Hier und hier gibt es eine Beschreibung der Struktur von Anfragen und Antworten des GDB-Servers. Obwohl es einfach erscheint, werden wir es aus folgenden Gründen nicht im Mikrocontroller implementieren:

  • Big-Data-Redundanz. Adressen, Registerwerte und Variablen werden als Hex-String codiert, wodurch sich das Nachrichtenvolumen um das Zweifache erhöht
  • Das Parsen und Sammeln von Nachrichten erfordert zusätzliche Ressourcen
  • Das Verfolgen des Paketende ist entweder durch eine Zeitüberschreitung (der Timer ist ausgelastet) oder durch eine komplexe automatische Maschine erforderlich, wodurch sich die im UART-Interrupt verbrachte Zeit erhöht

Um das einfachste und schnellste Debugging-Modul zu erhalten, verwenden wir ein Binärprotokoll mit Steuersequenzen:

  • 0xAA 0xFF - Start des Frames
  • 0xAA 0x00 - Rahmenende
  • 0xAA 0xA5 - Interrupt
  • 0xAA 0xAA - Ersetzt durch 0xAA

Um diese Sequenzen während des Empfangs zu verarbeiten, ist ein Automat mit 4 Zuständen erforderlich:

  • Warten auf ESC-Zeichen
  • Warten auf das zweite Zeichen der Sequenz "Start of Frame"
  • Datenempfang
  • Das letzte Mal, dass ein Esc-Charakter akzeptiert wurde

Aber um Zustände zu senden, benötigen Sie bereits 7:

  • Senden des ersten Bytes Frame-Anfang
  • Senden eines zweiten Bytes Rahmenanfang
  • Daten senden
  • Senden Ende des Rahmens
  • Ersatz für Esc-Zeichen senden
  • Senden des ersten Interrupt-Bytes
  • Senden des zweiten Interrupt-Bytes

Schreiben wir die Definition der Struktur, in der sich alle Modulvariablen befinden:

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

Die Zustände der Empfangs- und Sendemaschinen werden zu einer Variablen zusammengefasst, da die Arbeit im Halbduplexmodus ausgeführt wird. Jetzt können Sie mit einem Interrupt-Handler selbst Automaten schreiben.

UART-Handler
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;
  }
}


Hier ist alles ziemlich einfach. Abhängig vom aufgetretenen Ereignis ruft der Interrupt-Handler entweder eine empfangende Maschine oder eine Sendemaschine auf. Um zu überprüfen, ob alles funktioniert, schreiben wir einen Paket-Handler, der mit einem Byte antwortet:

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

Kompilieren, nähen, ausführen. Das Ergebnis ist auf dem Bildschirm sichtbar, es hat funktioniert.

Testaustausch


Als Nächstes müssen Sie Befehlsanaloga aus dem GDB-Serverprotokoll implementieren:

  • Speicher lesen
  • Speicheraufzeichnung
  • Programmstopp
  • Fortsetzung der Ausführung
  • Kernel Register gelesen
  • Kernel-Registereintrag
  • Festlegen eines Haltepunkts
  • Haltepunkt löschen

Der Befehl wird mit dem ersten Datenbyte codiert. Die Codes der Teams haben Nummern in der Reihenfolge ihrer Implementierung:

  • 2 - Speicher lesen
  • 3 - Speicheraufzeichnung
  • 4 - aufhören
  • 5 - weiter
  • 6 - Fall lesen
  • 7 - Haltepunkt installieren
  • 8 - Löschen des Haltepunkts
  • 9 - Schritt (nicht implementiert)
  • 10 - Registereintrag (nicht implementiert)

Parameter werden in den folgenden Datenbytes übertragen.

Die Antwort enthält nicht die Befehlsnummer, as Wir wissen bereits, welches Team gesendet hat.

Um zu verhindern, dass das Modul beim Lesen / Schreiben BusFault-Ausnahmen verursacht, müssen Sie es bei Verwendung auf M3 oder höher maskieren oder einen HardFault-Handler für M0 schreiben.

Sicheres Gedächtnis
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;
}


Die Haltepunkteinstellung wird implementiert, indem nach dem ersten inaktiven Register FP_COMP gesucht wird.

Code Installieren von Haltepunkten
	
  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;


Die Reinigung erfolgt durch Suchen nach dem eingestellten Haltepunkt. Durch das Stoppen der Ausführung wird der Haltepunkt auf dem aktuellen PC festgelegt. Beim Beenden des UART-Interrupts tritt der Kernel sofort in DebugMon_Handler ein.

Der DebugMon-Handler selbst ist sehr einfach:

  • 1. Das Flag zum Stoppen der Ausführung wird gesetzt.
  • 2. Alle eingestellten Haltepunkte werden gelöscht.
  • 3. Warten auf den Abschluss des Sendens einer Antwort auf den Befehl in uart (wenn es keine Zeit hatte zu gehen)
  • 4. Das Senden der Interrupt-Sequenz beginnt
  • 5. In der Schleife werden die Handler der Sende- und Empfangsmaschinen aufgerufen, bis das Stoppflag gesenkt wird.

DebugMon-Handler-Code
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(); 
  }
}


Lesen Sie die Kernel-Register von C-Syshny, wenn die Aufgabe problematisch ist. Deshalb habe ich einen Teil des Codes in ASM neu geschrieben. Das Ergebnis ist, dass weder DebugMon_Handler noch der UART-Interrupt-Handler oder die Maschinen den Stack verwenden. Dies vereinfachte die Definition von Kernelregisterwerten.

GDB-Server


Der Mikrocontroller-Teil des Debuggers funktioniert. Schreiben wir nun die Verknüpfung zwischen der IDE und unserem Modul.

Von Grund auf ist das Schreiben eines Debug-Servers nicht sinnvoll. Nehmen wir also einen vorgefertigten als Grundlage. Da ich die meiste Erfahrung in der Entwicklung von Programmen auf .net habe, habe ich dieses Projekt als Grundlage genommen und es an andere Anforderungen angepasst. Es wäre korrekter, Unterstützung für die neue Schnittstelle in OpenOCD hinzuzufügen, aber es würde mehr Zeit in Anspruch nehmen.

Beim Start fragt das Programm, mit welchem ​​COM-Port gearbeitet werden soll, beginnt dann, den TCP-Port 3333 abzuhören, und wartet, bis der GDB-Client eine Verbindung hergestellt hat.

Alle GDB-Protokollbefehle werden in ein Binärprotokoll übersetzt.

Als Ergebnis wurde eine funktionsfähige UART-Debugging-Implementierung veröffentlicht.

Endergebnis


Fazit


Es stellte sich heraus, dass das Debuggen des Controllers selbst nicht besonders kompliziert ist.
Theoretisch kann dieses Modul durch Platzieren in einem separaten Speicherbereich auch zum Flashen des Controllers verwendet werden.

Der Quellcode wurde auf GitHub für die allgemeine Untersuchung des

Mikrocontroller-Teils des
GDB-Servers veröffentlicht

All Articles