Cet article concerne mon premier projet open source "repl" (lien vers le référentiel ci-dessous). L'idée de ce projet est de permettre au programmeur du microcontrôleur de déboguer le programme dans le microcontrôleur via l'une de ses interfaces, tandis que le débogage ne diffère pas beaucoup du débogage via l'interface jtag. Il était possible d'arrêter le programme, de définir des points d'arrêt, de visualiser les registres, la mémoire, par un débogage instructif du programme.
La première chose qui me vient à l'esprit est de créer une application 2x, l'un des threads est responsable de l'interface de débogage, l'autre du programme utilisateur, ce que j'ai fait. La commutation entre les threads est effectuée sur une minuterie, chaque thread a sa propre pile. J'ai décidé de ne pas utiliser un tas pour écrire l'interface de débogage. ils doivent être utilisés 2 différents, ou lorsque vous travaillez avec un tas, vous devez constamment basculer vers un thread.
La première idée d'implémentation pour le débogage d'instructions était de réduire le temps entre les interruptions de la minuterie juste assez pour qu'une seule instruction puisse être exécutée. Cette option a montré son travail idéal sur le microcontrôleur Atmega328p, le fait est que le temps minimum entre les interruptions pour Atmega est de 1 horloge processeur, toute instruction, quel que soit le nombre de cycles d'horloge nécessaire à son exécution, se terminera toujours si son exécution a commencé.
Malheureusement, lorsque je suis passé à stm32, cette option ne fonctionnait pas, car le noyau Cortex-M peut interrompre l'exécution d'une instruction au milieu, puis, au retour de l'interruption, recommencer à l'exécuter. Alors j'ai bien décidé, pas de problème, je vais juste augmenter le nombre de cycles d'horloge entre les interruptions jusqu'à ce que l'instruction soit exécutée, puis je suis tombé sur un autre problème, certaines instructions sont exécutées uniquement avec l'instruction qui les suit ou pas du tout exécutées, malheureusement ce problème ne peut pas être résolu, alors j'ai décidé de changer radicalement d’approche.
J'ai décidé d'imiter ces instructions. À première vue, l'idée d'écrire un émulateur de noyau Cortex M, et même un qui s'intégrerait dans la mémoire du microcontrôleur, semble fantastique. Mais je me suis rendu compte que je n'avais pas besoin d'émuler toutes les instructions, mais seulement celles associées au compteur de programme, il n'y en avait pas beaucoup. Tout le reste, je peux simplement me déplacer vers un autre emplacement mémoire et y exécuter, et pour qu'une seule instruction soit exécutée, ajoutez l'instruction de saut «b» après elle.
Et puis le tour des points d'arrêt est venu, pour leur implémentation j'ai décidé d'utiliser une instruction qui implémente la logique while (1);. Nous remplaçons l'instruction située à l'adresse où nous voulons mettre le point d'arrêt et attendons l'interruption du timer. Je comprends que quelque chose comme une instruction qui lèverait une exception serait mieux, mais je voulais faire une version universelle. Une fois exécutée, nous remplaçons cette instruction. Une bonne option est d'exécuter le programme dans la RAM du microcontrôleur, sinon la mémoire flash du microcontrôleur ne durera pas longtemps. Mais à ce stade, j'avais déjà fini d'écrire un émulateur d'instructions pour stm32 et j'ai décidé pourquoi ne pas écrire le même pour Atmega328 et je l'ai écrit. Vous n'avez plus besoin de remplacer les instructions, elles peuvent être émulées.
Afin de me faire des amis avec le runtime, j'ai d'abord voulu écrire mon propre client gdb. Malheureusement, il prend en charge deux interfaces pour travailler avec ide. C'est à elle de décider quelle idée utiliser. Mettre en œuvre les deux (le premier me paraissait assez simple, le second pas très), en plus je devrais combiner les sources avec le firmware, ce qui ne me semblait pas une très bonne idée. J'ai donc décidé d'écrire mon propre gdbserver, heureusement, il n'y avait qu'un seul protocole et c'était assez simple.
Ma première interface, que j'ai décidé de mettre en œuvre, était le GSM. En tant qu'émetteur-récepteur, j'ai décidé d'utiliser SIM800, en tant que serveur, un serveur avec support php. L'idée principale était qu'après l'arrivée d'une post-demande du microcontrôleur, maintenez la connexion au microcontrôleur pendant 30 secondes et contactez la base de données toutes les 100 ms, si les données semblaient les envoyer en réponse à la requête et attendez la prochaine requête du microcontrôleur.
La première connexion du client gdb au serveur a montré qu'il y avait trop de requêtes du client gdb au serveur avec la commande pause ou step. Par conséquent, il a été décidé de combiner toutes ces demandes en une seule grande pour le microcontrôleur, pour cela j'ai compris la logique de ces demandes et appris à les prédire. Maintenant, ces commandes n'ont pas été exécutées si rapidement, je voudrais plus vite, mais supportable.
L'interface suivante était usb, pour le microcontrôleur Atmega328, j'ai décidé d'utiliser la bibliothèque V-usb. Pour travailler avec USB, j'ai réécrit la logique de commande d'exécution. Maintenant, le microcontrôleur après cette commande n'a pas démarré le programme en attente de la commande de pause, il l'a démarré pendant 1 seconde, puis une nouvelle commande d'exécution lui a été envoyée, etc. Cette logique est nécessaire car Je désactive l'interface pendant que le programme est en cours d'exécution. Pour ne pas avoir à écrire un pilote, j'ai décidé d'utiliser le pilote standard caché. Tandis que les fonctionnalités de communication cachaient le rapport sur les fonctionnalités, elles cachaient le rapport sur les fonctionnalités définies.
En ce qui concerne le clignotement des microcontrôleurs, j'ai décidé que c'était superflu pour l'interface usb, donc pour le premier firmware, vous avez toujours besoin d'un programmeur. Mais pour l'interface GSM, c'est le plus. Habituellement, un programme séparé est écrit à cet effet, mais j'ai décidé de le faire différemment, pour le lieu d'écriture d'un programme séparé, j'ai décidé de charger complètement le programme dans la mémoire flash du microcontrôleur, puis une fois le téléchargement terminé, copiez ce programme au début de la mémoire. Ensuite, j'ai pensé pourquoi devrais-je envoyer le programme entier, je ne peux envoyer que la différence entre le fichier binaire actuel et le fichier binaire précédent du firmware.
Pour minimiser cette différence, j'ai décidé de renommer les sections du tableau .text, .data, .bss, .contructors pour une partie du programme utilisateur (le nom généralisé est différent pour différents microcontrôleurs) et de les placer en mémoire juste après le programme principal.
J'ai également dû écrire mes propres fonctions pour initialiser ces sections. Désormais, dans la plupart des cas, un petit changement de programme est égal à un petit changement binaire égal à une petite quantité de données transférées. En conséquence, le microcontrôleur est souvent flashé plus rapidement que les commandes RUN, STEP, PAUSE fonctionnent.
Et enfin, une vidéo de travail: le
débogage Stm32 via l'interface usb.
Débogage Stm32 via l'interface gsm.
Débogage Atmega328 via l'interface USB.
Débogage Atmega328 via l'interface gsm.
Dépôt Git