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
{
unsigned tx:1;
unsigned StopProgramm:1;
union {
enum rx_state_e
{
rxWaitS = 0,
rxWaitC = 1,
rxReceive = 2,
rxEsc = 3,
} rx_state;
enum tx_state_e
{
txSendS = 0,
txSendC = 1,
txSendN = 2,
txEsc = 3,
txEnd = 4,
txSendS2 = 5,
txBrk = 6,
} tx_state;
};
uint8_t pos;
uint8_t buf[128];
uint8_t txCnt;
} 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-Handlervoid 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;
if (dbg->tx)
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.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ächtnisint memcpySafe(uint8_t* to,uint8_t* from, int len)
{
static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos);
int cnt = 0;
SCB->CFSR |= BFARVALID_MASK;
uint32_t mask = __get_FAULTMASK();
__disable_fault_irq();
SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk;
while ((cnt<len))
{
*(to++) = *(from++);
cnt++;
}
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;
addr = ((*(uint32_t*)(&dbg->buf[1])))|1;
for (tmp = 0;tmp<8;tmp++)
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-Codevoid DebugMon_Handler(void)
{
dbgG->StopProgramm = 1;
for (int i=0;i<NUMOFBKPTS;i++)
FP->FP_COMP[i] = 0;
while (USART6->CR1 & USART_CR1_TXEIE)
if ((USART6->ISR & USART_ISR_TXE) != 0U)
txCb();
dbgG->tx_state = txSendS2;
dbgG->tx = 1;
SET_BIT(USART6->CR1, USART_CR1_TXEIE);
while (dbgG->StopProgramm)
{
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.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 desMikrocontroller-Teils desGDB-Servers veröffentlicht