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
{
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)
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 UARTvoid 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;
}
}
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é.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ûrint 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;
}
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;
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;
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 DebugMonvoid 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();
}
}
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.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 lapartie microcontrôleur duserveur GDB