Ce que nous allons faire - Prérequis
Le programme le plus simple qu'on puisse faire sur un système embarqué consiste à faire clignoter une LED. Le code est minimal, donc il est peu probable qu'il contienne une erreur, donc si problème il y a, il viendra de l'environnement de développement.
Cet exercice permet donc de vérifier que nous avons correctement installé notre environnement de développement (MounRiver Studio, MRS), que nous disposons de toute la documentation nécessaire, et que nous savons utiliser tous les outils. En clair, une fois que notre LED clignote, nous avons l'assurance d'être prêt pour aborder n'importe quel autre projet.
La LED qui nous intéresse porte le repère D1 sur le schéma de la carte de développement WeAct Studio CH32V003F4U6, reproduit ci-dessous par commodité.
On voit que sa cathode est reliée directement à la masse et que son anode est connectée via une résistance de 10 kΩ à la ligne de GPIO PC4, qui correspond à la broche 11 du CH32V003F4U6. Cela signifie que pour allumer la LED, il faut porter PC4 au niveau logique haut (ou "1"). Un niveau logique bas (ou "0") l'éteindra.
Nous avons toutes les informations dont nous avons besoin pour faire clignoter notre LED, reste à voir comment procéder étape par étape, en commençant par expliquer ce qu'est un GPIO.
Les prérequis de ce cours sont :
Connaissance des bases du langage C.
Capacité à lire un schéma électronique.
Disposer de la data sheet et du manuel de référence du CH32V003.
Disposer d'un adaptateur WCH-LinkE et d'une carte de développement WeAct Studio CH32V003.
Avoir téléchargé et installé MounRiver Studio (mais voir aussi Avant de commencer sur cette même page si vous utilisez Linux).
Si vous avez des questions sur les prérequis, vous trouverez des réponses sur cette page.
Présentation du GPIO, utilisation en sortie
GPIO est l'abréviation de General-Purpose Input-Output, c'est-à-dire entrées-sorties à usage général.
La fonction d'un micro-contrôleur est de commander des sous-systèmes de sortie (ex. moteur, affichage LCD) en fonction de l'état de sous-systèmes d'entrée (ex. capteurs, clavier, lecteur de badge). Il est donc nécessaire de pouvoir interfacer (relier) le micro-contrôleur avec ces différents sous-systèmes et il dispose pour cela d'un certain nombre de broches ("pattes").
Seulement, le nombre de broches étant limité, chacune d'elles pourra être utilisée pour différentes fonctions. Par exemple, une broche donnée peut être utilisée soit comme entrée logique (0 ou 1), soit comme sortie logique, soit comme entrée analogique (ex. pour lire la tension produite par un capteur).
La broche étant physiquement reliée à un sous-système, sa fonction est évidemment déterminée une fois pour toutes. Le fait que la fonction d'une broche soit configurable signifie que le même modèle de micro-contrôleur peut être utilisé pour créer des appareils très différents, mais une fois fabriqué, un appareil donné ne peut plus être physiquement modifié, seules ses règles de fonctionnement internes peuvent être ajustées en modifiant le programme qui les contient (le firmware).
Le GPIO prend en charge à la fois la configuration des broches et les entrées-sorties logiques. Pour allumer notre LED, nous aurons besoin d'une sortie logique sur la broche 11, qui correspond à PC4, c'est-à-dire la ligne 4 (la première porte le n°0) du port C du GPIO (on appelle "port" un ensemble de lignes de GPIO contrôlées par un même jeu de registres).
Si nous relions un bouton poussoir à la ligne PD2 (broche 16), nous devrons la configurer en entrée logique et c'est encore le GPIO qui la gérera. Par contre, si nous voulons y connecter une thermistance pour calculer la température ambiante, il s'agira d'une entrée analogique (A3), donc le GPIO ne fera que diriger la broche vers le convertisseur analogique-numérique (ADC, nous en parlerons dans un autre cours), qui est un périphérique indépendant.
Même chose si nous voulons utiliser un timer pour générer un signal PWM (objet d'un autre cours), le GPIO fera l'aiguillage de la broche vers le timer, qui est un périphérique indépendant. Les fonctions d'une broche qui ne relèvent pas des entrées-sorties logiques sont appelées alternate functions dans la documentation.
Le schéma ci-dessous représente le fonctionnement d'une sortie logique.
On y voit 2 choses intéressantes :
La sortie peut être configurée dans 3 modes : push-pull, open drain, ou disabled.
En mode push-pull, si on écrit un 1 en sortie, le transistor P-MOS est passant et N-MOS est bloqué. La broche de sortie est donc reliée directement à VDD, c'est-à-dire au pôle positif de l'alimentation du circuit. Si on écrit un 0, c'est le contraire et la broche de sortie est donc reliée directement à VSS, c'est-à-dire à la masse.
En mode open drain, le transistor P-MOS est toujours bloqué. Si on écrit un 1 en sortie, le transistor N-MOS est passant. Si on écrit un 0, il est bloqué. Ce mode est donc utile pour contrôler une charge dont l'autre pôle est relié à VDD.
En mode disabled, les 2 transistors sont toujours bloqués. Ce mode permet de garantir qu'aucun courant n'entre ni ne sort par la broche de sortie, ce qui évite par exemple qu'un moteur ne démarre alors que le système n'est pas encore initialisé.
- La broche de sortie est reliée non seulement à l'étage de sortie, mais aussi au registre d'entrée. Si la valeur que vous lisez pour cette broche est différente de celle que vous lui avez imposée, c'est peut-être qu'un court-circuit ou autre défaillance s'est produit et que vous devriez en informer l'utilisateur de l'appareil. Dans d'autres cas, ça peut aussi être une condition particulière d'un fonctionnement normal, par exemple dans le protocole one-wire, et il est important de pouvoir la détecter.
Vous trouverez une description complète du GPIO au chapitre 7 du manuel de référence du CH32V003 et la description des broches et de leurs fonctions au chapitre 2 de sa data sheet.
La data sheet nous donne un information très importante pour l'utilisation du GPIO en sortie dans la section 3.3.9 (I/O port characteristics) : le courant entrant (sortie au niveau logique bas) ou sortant (sortie au niveau logique haut) d'une ligne de GPIO est au maximum de 8mA, ou 20mA si on accepte une forte dégradation de sa tension de sortie (dernière ligne du tableau 3-17, Output voltage characteristics).
La data sheet précise en outre que la somme des courants de toutes les lignes de GPIO configurées en sortie ne peut excéder les valeurs indiquées dans le tableau 3-1 (Absolute maximum ratings) sur les lignes IVDD et IVSS.
Vérifions que nous sommes dans les clous :
La LED est reliée à PC4, sur laquelle la tension à l'état haut est au minimum égale à VDD - 0.4 (tableau 3-17).
La LED est bleue, donc la tension à ses bornes lorsqu'elle conduit VF est d'environ 2.5V (vérifiée au multimètre).
La valeur de la résistance série de la LED (R1) est de 10kΩ.
Que notre carte de développement (rappel : nous avons choisi un modèle 5V) soit alimentée par son connecteur USB ou par le WCH-LinkE (voir plus bas), la tension sur la broche VDD du CH32V003 est de 5V.
Le courant traversant la LED lorsque PC4 est à l'état haut est donc : (VDD - 0.4 - VF) / R1 = 210µA.
Conclusion : on ne risque d'abîmer ni la LED, ni le micro-contrôleur !
Utilisation de MounRiver Studio (MRS)
La première fois que vous lancez MRS, il vous demandera de choisir un "workspace directory". Le workspace est l'endroit où sont créés tous vos projets. Créez où vous voulez un nouveau répertoire à cette fin et pensez à cocher la case "Use this as the default and do not ask again" avant de cliquer sur le bouton "Launch". De cette façon, vous n'aurez plus à vous en soucier.
Création d'un projet
Pour créer un projet, cliquez sur la flèche de l'icône de création puis sélectionnez "MounRiver project" comme indiqué
ci-dessous :
La boîte de dialogue des options du projet est alors affichée. Vous devrez y indiquer, dans l'ordre :
la série de micro-contrôleur utilisée, ici CH32V003
le modèle précis de micro-contrôleur, ici CH32V003F4U6. Dans notre cas, ça n'a pas d'importance, mais dans certaines série, le code de démarrage ou le fichier de configuration du linker peuvent varier selon les modèles ;
le nom que vous voulez donner au projet, ici "blinky". Si, très logiquement, vous commencez par cette information, elle sera écrasée par le nom du micro-contrôleur. C'est un peu agaçant, mais on s'y fait.
Pour finir, cliquez sur le bouton "Finish".
Anatomie du projet
Lors de la création du projet, MRS a copié un squelette complet intégrant à la fois un exemple d'application et tous les sources du SDK. Je vous invite à un petit tour du propriétaire en dépliant les différentes rubriques du projet, à commencer par "Includes".
On trouve ici les différents répertoires qui seront utilisés pour la recherche des fichiers d'entête. Les 3 premiers sont ceux de GCC et tous ceux qui commencent par "blinky/" sont ceux du SDK et de notre application ("blinky/User").
Ensuite, les rubriques Core, Debug, Ld, Peripheral et Startup correspondent aux sources du SDK. Core contient les définitions relatives à la plateforme RISC‑V, Debug contient des utilitaires de mise au point (configuration de l'UART, printf), Ld contient le script de configuration du linker, Peripheral les API permettant d'utiliser les différents périphériques du micro-contrôleur, et enfin, Startup contient le code de démarrage, c'est-à-dire essentiellement la définition des vecteurs de reset et d'interruption et un saut vers main().
Enfin, la rubrique User contient le squelette d'application créé pour nous par MRS.
On y trouve ch32v00x_conf.h, un fichier d'entête qui inclue tout ce dont vous pourriez avoir besoin, ch32v00x_it.c qui définit des routines d'interruption (ISR), que vous voudrez peut-être organiser autrement si vous en avez besoin, system_ch32v00x.c qui contient le code d'initialisation de l'horloge du micro-contrôleur, et main.c qui contient le code de démarrage de votre application, ou sa totalité si elle est aussi simple que dans notre cas. Le fichier blinky.launch sera utilisé pour le premier lancement du debugger, comme nous le verrons plus tard.
Modification des sources et compilation
La première chose à faire est de modifier system_ch32v00x.c pour configurer l'horloge. Double-cliquez sur son nom pour l'ouvrir, vous verrez ceci :
La configuration par défaut correspond à la carte d'évaluation officielle de WCH, qui utilise un quartz. Dans notre cas, nous n'avons pas de quartz (ce qui libère 2 lignes de GPIO de plus), donc nous devrons commenter la ligne commençant par "#define SYSCLK_FREQ_48MHz_HSE" et dé-commenter la ligne commençant par "#define SYSCLK_FREQ_48MHZ_HSI".
HSE signifie "High-Speed External oscillator", donc oscillateur externe (à quartz). HSI signifie "High-Speed Internal oscillator", donc oscillateur interne (à réseau RC).
48MHz est la fréquence maximale à laquelle le CH32V003 peut fonctionner. Nous l'avons choisie par souci de simplicité, car WCH ne nous a pas facilité les choses pour la modifier. De plus, nous n'avons aucune raison particulière pour en choisir une autre.
Le SDK de WCH n'est vraiment pas une réussite en termes de conception logicielle, WCH s'étant contenté de reproduire celui des STM32, y compris avec tous ses défauts. On peut en prendre son parti, choisir de l'améliorer, ou encore concevoir son propre SDK. Tout est question de rapport bénéfices / efforts. Dans notre contexte, nous ferons avec.
Bref, si vous avez suivi ces explications, la portion modifiée de system_ch32v00x.c doit maintenant correspondre à ceci (les 2 lignes modifiées sont en rouge) :
//#define SYSCLK_FREQ_8MHz_HSI 8000000 //#define SYSCLK_FREQ_24MHZ_HSI HSI_VALUE #define SYSCLK_FREQ_48MHZ_HSI 48000000 //#define SYSCLK_FREQ_8MHz_HSE 8000000 //#define SYSCLK_FREQ_24MHz_HSE HSE_VALUE //#define SYSCLK_FREQ_48MHz_HSE 48000000
Vous allez maintenant ouvrir main.c, supprimer tout son contenu et le remplacer par ce qui suit :
// Contient toutes les définitions du SDK du CH32V003. #include <ch32v00x.h> // Contient la définition de la fonction Delay_Ms(). #include <debug.h> /* * La LED de la carte de développement WeAct Studio CH32V003F4U6 est connectée * à la broche 4 du port C et s'allume lorsque cette broche est à un niveau * logique haut. * * Nous utilisons des #define pour ces éléments de configuration car ceux-ci * permettent de centraliser ces définitions, ce qui facilite l'adaptation * du code à une carte de développement différente, mais aussi permet de * s'assurer que tous les endroits du code où ces informations apparaissent * utilisent bien la même valeur. En effet, LED_GPIO_PIN et LED_GPIO_PORT * sont utilisés à 2 endroits. LED_GPIO_RCC n'est utilisé qu'une fois, mais * il est essentiel qu'il soit changé en même temps que LED_GPIO_PORT, donc * utiliser un #define permet d'assurer la cohérence des différents éléments. */ #define LED_GPIO_PIN GPIO_Pin_4 #define LED_GPIO_PORT GPIOC #define LED_GPIO_RCC RCC_APB2Periph_GPIOC int main() { // Prépare l'utilisation de la fonction Delay_Ms(). SystemCoreClockUpdate(); Delay_Init(); // Déclare et initialise une structure contenant les paramètres // de configuration de la broche de GPIO. GPIO_InitTypeDef gpioInit = { 0 }; gpioInit.GPIO_Pin = LED_GPIO_PIN; gpioInit.GPIO_Mode = GPIO_Mode_Out_PP; gpioInit.GPIO_Speed = GPIO_Speed_50MHz; // Active le port du GPIO. RCC_APB2PeriphClockCmd(LED_GPIO_RCC, ENABLE); // Configure la broche que nous utilisons. GPIO_Init(LED_GPIO_PORT, &gpioInit); // Cette variable définit l'état que nous voulons assigner // à la broche de GPIO de la LED. BitAction status = Bit_SET; while (1) { // Fixe l'état de la broche de GPIO à la valeur souhaitée. GPIO_WriteBit(LED_GPIO_PORT, LED_GPIO_PIN, status); // Inverse l'état souhaité en prévision du tour de boucle suivant. status = (status == Bit_RESET) ? Bit_SET : Bit_RESET; // Attend 125ms avant de changer de nouveau l'état de la broche. Delay_Ms(125); } }
On notera la boucle infinie while (1), considérée comme une erreur dans un programme classique, mais obligatoire dans une application embarquée, car elle doit précisément remplir toujours la même fonction tant que l'appareil est en service.
Cette boucle est appelée "super loop". Dans notre cas, elle n'a pas grand chose de "super" car elle ne contient que 3 lignes, mais on peut développer des applications bien plus sophistiquées sur cette base.
Il existe deux autres façons de structurer une application embarquée, utiliser un système d'exploitation temps réel (Real-Time Operating system, RTOS), ou utiliser Linux. L'approche choisie pour un projet donné dépend entre autres choses de sa complexité.
La super loop (on dit aussi "bare metal") est appropriée lorsque les spécifications fonctionnelles sont suffisamment simples pour être implémentées de façon fiable sous cette forme. L'avantage de la super loop dans ce cas est que n'importe qui peut maintenir l'application, même un débutant qui vient de rejoindre l'équipe.
Un RTOS s'impose quand les spécifications fonctionnelles sont plus complexes, en particulier avec des tâches qui communiquent entre elles, mais sans pour autant exiger de composants techniques aussi sophistiqués qu'une base de données ou un serveur web.
Linux est requis lorsque les spécifications fonctionnelles sont si riches et élaborées, en particulier sur le plan de la connectivité réseau ou l'interface utilisateur, que l'utilisation d'un RTOS imposerait des coûts et des délais prohibitifs pour mettre en place l'infrastructure technique du projet.
Linux offre aussi un avantage énorme en termes de recrutement. Une grande partie des développeurs embarqués sont des électroniciens qui se sont formés au développement, l'ingénierie logicielle n'est donc pas leur fort, alors que c'est crucial pour développer sous Linux. D'autre part, les développeurs embarqués sont rares sur le marché du travail, alors que les développeurs issus de cursus purement informatiques sont beaucoup plus nombreux. En termes de gestion des risques, il vaut donc mieux recruter un développeur Linux et lui inculquer les quelques rudiments d'électronique dont il aura besoin, notamment pour communiquer avec les électroniciens.
Évidemment, un RTOS exige plus de connaissances qu'une super loop, et Linux encore bien plus. Cependant, lorsque ces connaissances sont acquises et un ou deux projets ont été réalisés, la tentation est grande d'utiliser ce qu'on connaît dans tous les cas. Ce sont alors les contraintes de coût qui permettent de choisir une approche plutôt qu'une autre.
Voilà, tout est maintenant en ordre, nous pouvons compiler notre application
en cliquant sur l'icône .
Vous devriez maintenant être gratifiés d'un message indiquant 0 erreurs et 0 warnings.
Félicitations, c'est un bon départ !
Transfert du firmware sur le micro-contrôleur
Maintenant que votre firmware (le code qui doit s'exécuter sur le micro-contrôleur) est compilé, il va falloir le "flasher", c'est-à-dire le transférer dans la mémoire flash du micro-contrôleur. Pour cela, vous devez connecter votre carte de développement à votre adaptateur WCH-LinkE, puis insérer celui-ci dans un des ports USB de votre ordinateur.
Il y a 3 connexions à établir entre la carte de développement et le WCH-LinkE :
La broche du WCH-LinkE doit être reliée | à la broche de la carte de développement |
---|---|
SWDIO | DIO |
GND | GND |
5V | VCC |
Concrètement, ça donne ça :
Une votre WCH-LinkE connecté à votre ordinateur, il ne reste plus qu'à lancer le
transfert en cliquant sur l'icône .
Chaque mise à jour de MRS vient avec une nouvelle version du firmware de chaque modèle de WCH-Link (dans le dossier MRS_Community/update/Firmware_Link) et MRS vous oblige à faire la mise à jour. Vous verrez alors la boîte de dialogue suivante :
Bien qu'il y ait un bouton "No", vous ne pourrez pas transférer votre firmware tant que vous n'aurez pas cliqué sur "Yes"...
Après toutes ces vaillantes manipulations, vous avez maintenant l'ineffable satisfaction de voir la LED de votre carte de développement clignoter tous les quarts de seconde. Chaleureuses félicitations, vous l'avez bien mérité !
Encore un petit détail : la vue "Project" comporte maintenant 2 rubriques de plus, Binaries et obj.
Binaries n'est qu'un raccourci vers obj/blinky.elf, donc nous nous concentrerons sur le contenu de obj. Tous les fichiers de ce répertoire sont générés à différents stades de la compilation à partir des sources et de la configuration du projet. Vous pouvez donc sans problème supprimer complètement le répertoire obj, tout sera régénéré en fonction des besoins. Comme la plupart de ces fichiers (.o, .d, .mk) sont destinés à différents outils de la chaîne de compilation et non au développeur, nous n'en retiendrons que deux :
blinky.elf contient le firmware compilé avec toutes ses informations de debugging (numéros de lignes, symboles). Vous en avez besoin pour utiliser le debugger.
blinky.hex contient une représentation ASCII du firmware et est généré à partir de blinky.elf. Il est destiné aux logiciels utilisés pour le flashage (ex. WCH-LinkUtility). Vous en avez besoin pour flasher les produits finis lors de leur production.
Utilisation du debugger
Option de compilation
Pour pouvoir utiliser les fonctionnalités du debugger que nous allons voir ci-après, il est nécessaire de modifier la manière dont le compilateur optimise le code généré. Cette opération est à faire une seule fois pour chacun de vos projets. Voici la procédure :
Faites un clic droit sur le nom du projet dans la vue "Project Explorer" et sélectionnez l'option "Properties" du menu contextuel.
Dans le panneau de gauche de la boîte de dialogue, dépliez la ligne "C/C++ Build" et sélectionnez "Settings". La partie droite se met à jour. Cliquez sur "Optimization" en dessous de "GNU RISC-V Cross C Compiler". La boîte de dialogue se présente alors ainsi :
En regard de "Other optimization flags", saisissez "-Og" comme indiqué sur la capture d'écran précédente, puis cliquez sur le bouton vert "Apply and Close".
Recompilez votre projet (icône "marteau").
Si vous oubliez de le faire, l'affichage des variables et le positionnement dans les sources lors de l'utilisation du debugger seront anarchiques.
Points d'arrêt
Un debugger permet de placer des points d'arrêt à des endroits précis de votre firmware pour que vous puissiez examiner son état (valeurs des variables, expressions) et au besoin exécuter le code pas-à-pas pour bien comprendre ce qui se passe, ce qui est essentiel pour diagnostiquer un bug.
Placer un point d'arrêt est très facile, il suffit de double-cliquer dans la marge de la fenêtre du code source, à gauche du numéro de ligne. Elle se présente ensuite comme ceci :
Double-cliquer sur un point d'arrêt permet de le supprimer. Comme sur la copie d'écran précédente, vous allez placer un point d'arrêt sur la ligne contenant l'appel à la fonction GPIO_Write().
Une fois que vous avez placé le ou les points d'arrêt dont vous avez besoin, il vous faut lancer le debugger. La toute première fois, vous devrez faire un clic droit sur blinky.launch, sélectionner "Debug as..." puis "blinky", comme ceci :
Cette action, en plus de lancer le debugger, créera une configuration de debugging
qui vous permettra par la suite de relancer le debugger en cliquant simplement sur
l'icône . Cette
configuration de debugging est sauvegardée dans votre workspace. Si vous avez plusieurs
projets dans votre workspace, ce qui arrivera vite, vous pouvez choisir avec quelle
configuration lancer le debugger en cliquant sur la petite flèche à droite de l'insecte.
Les perspectives
La première fois que vous lancez le debugger, MRS vous demande de confirmer le changement de perspective. Comme c'est une question idiote, pensez à cocher la case "Remember my decision" pour éviter de la subir de nouveau et cliquez sur "Switch".
Une perspective est un arrangement prédéfini de fenêtres correspondant à une
activité donnée, édition du code ou debugging, par exemple. Une barre d'icônes
permet de changer manuellement de perspective : . La première icône est la
perspective "édition de code" standard d'Eclipse CDT (sur lequel est basé MRS),
la seconde icône est celle de MRS, et la troisième la perspective "debugging"
standard d'Eclipse CDT, conservée par MRS.
A peine le debugger lancé, il s'arrêtera sur un point d'arrêt que vous n'avez pas posé, situé dans le code de démarrage. C'est inutile et ennuyeux, donc vous voudrez vous en débarrasser pour les lancements ultérieurs en effectuant la manipulation suivante, à faire une seule fois pour chaque projet :
Faites un clic droit sur le nom du projet dans la vue "Project Explorer", sans le menu contextuel, déroulez l'option "Debug As" et cliquez sur "Debug configurations...".
-
Dans la boîte de dialogue qui s'affiche :
Vérifiez que le bon projet est sélectionné dans la liste de gauche.
Cliquez sur l'onglet "Startup" et utilisez l'ascenseur pour aller tout en bas de la boîte de dialogue.
Décochez la case "Set breakpoint at".
Cliquez sur "Apply".
Cliquez sur "Close".
La barre d'icônes du debugger
C'est le bon moment pour parler de la barre d'icônes du debugger :
.
L'icône en forme de flèche verte est active quand l'exécution est interrompue (point d'arrêt ou exécution pas-à-pas). Nous allons immédiatement nous en servir pour relancer l'exécution après cet arrêt non désiré.
L'icône suivante (symbole pause) permet d'interrompre l'exécution là où elle en est. Ce peut être utile quand le programme ne répond plus et que vous voulez savoir où ça bloque. Elle n'est pas active car nous somme déjà arrêtés sur un point d'arrêt.
Le carré rouge stoppe complètement le debugger.
L'icône en zigzag permet de se connecter à un remote debugger, ce qui ne nous concerne pas ici.
La flèche qui descend entre 2 points est la fonction "step into". Elle permet de "descendre" à l'intérieur de la fonction sur laquelle vous êtes arrêté, pour comprendre en détail ce qui se passe.
La flèche qui passe par dessus un point est la fonction "step over". Elle permet d'exécuter la ligne sur laquelle vous vous trouvez sans entrer dans le détail.
La flèche qui sort entre 2 points est la fonction "step out". Elle permet de quitter une fonction dans laquelle vous avez fait un "step into".
Après avoir relancé l'exécution, il est probable que vous soyez de nouveau arrêté au tout début de la fonction main(). Là encore, relancez l'exécution avec la flèche verte jusqu'à ce que vous arriviez au point d'arrêt que vous avez posé. Maintenant que nous y sommes, nous allons parler des vues situées dans la partie droite de la perspective :
Les vues du debugger
A chaque pause sur un point d'arrêt, cette zone est repositionnée sur la vue Disassembly, qui ne nous intéresse absolument pas. Par contre, 4 autres vues nous seront d'une grande utilité :
La vue Variables nous permet d'inspecter les valeurs des différentes variables visibles depuis la ligne où nous sommes arrêtés. Les valeurs qui viennent d'être modifiées apparaissent sur fond jaune, comme sur la copie d'écran précédente.
La vue Breakpoints affiche la liste des points d'arrêts et permet de les activer/désactiver ou de les supprimer.
La vue Expressions permet de saisir des expressions (ex. accès à un membre d'une structure, à un objet référencé par un pointeur, ou exécution d'un calcul simple). Elle complète très utilement la vue Variables.
La vue Peripherals liste tous les périphériques du micro-contrôleur et vous permet d'en sélectionner un ou plusieurs. Les registres de tous les périphériques sélectionnés apparaissent dans la vue Memory (dans la zone du bas) de la même façon que si c'étaient des variables. Vous pouvez donc visualiser l'effet de votre code sur les périphériques que vous utilisez.
Pour vous débarrasser de la vue Disassembly, il suffit de "drag-n-dropper" son onglet à côté de la vue Debug. :)
Voilà, vous avez toutes les cartes en main, je vous invite à manipuler le debugger jusqu'à ce qu'il n'ait plus de secret pour vous.
Utilisation de printf()
Votre WCH-LinkE sert à flasher votre MCU et à le debugger, mais il intègre aussi un adaptateur USB-série qui peut être connecté à l'UART du CH32V003 pour envoyer des messages sur-mesure qui seront affichés dans une vue Terminal de MRS.
C'est très utile pour par exemple avoir une trace de l'exécution de votre code avec les valeurs de certaines variables clés. Le debugger ne vous donne qu'une vue à un instant t et pas ce contexte temporel.
C'est aussi très utile pour connaître l'état de votre programme lorsque le temps passé sur un point d'arrêt est incompatible avec les contraintes fonctionnelles de temps d'exécution de votre code. Dans ces cas printf() peut même parfois être trop lent et vous en êtes alors réduit à changer l'état d'une ou plusieurs lignes de GPIO et à les observer à l'oscilloscope ou à l'analyseur logique. Cependant, printf() reste utile dans un très large éventail de situations.
Pour pouvoir utiliser printf(), nous devons ajouter 2 nouvelles connexions entre la carte de développement et le WCH-LinkE :
La broche du WCH-LinkE doit être reliée | à la broche de la carte de développement |
---|---|
RX | D5 |
TX | D6 |
Pourquoi ? Parce que la data sheet nous indique que la ligne RX de l'UART est une "alternate function" de la ligne de GPIO PD5, idem pour TX et PD6.
Voilà ce que ça donne avec ces 2 nouveaux fils :
Vous allez maintenant modifier main.c de la manière suivante (les lignes ajoutées ou modifiées sont en rouge) :
// Contient toutes les définitions du SDK du CH32V003. #include <ch32v00x.h> // Contient la définition des fonctions Delay_Ms() et USART_Printf_Init(). #include <debug.h> /* * La LED de la carte de développement WeAct Studio CH32V003F4U6 est connectée * à la broche 4 du port C et s'allume lorsque cette broche est à un niveau * logique haut. * * Nous utilisons des #define pour ces éléments de configuration car ceux-ci * permettent de centraliser ces définitions, ce qui facilite l'adaptation * du code à une carte de développement différente, mais aussi permet de * s'assurer que tous les endroits du code où ces informations apparaissent * utilisent bien la même valeur. En effet, LED_GPIO_PIN et LED_GPIO_PORT * sont utilisés à 2 endroits. LED_GPIO_RCC n'est utilisé qu'une fois, mais * il est essentiel qu'il soit changé en même temps que LED_GPIO_PORT, donc * utiliser un #define permet d'assurer la cohérence des différents éléments. */ #define LED_GPIO_PIN GPIO_Pin_4 #define LED_GPIO_PORT GPIOC #define LED_GPIO_RCC RCC_APB2Periph_GPIOC int main() { // Prépare l'utilisation de la fonction Delay_Ms(). Delay_Init(); // Configure le fonctionnement de la fonction printf(), ici avec sortie // sur USART à 115200 Baud, 8 bits, 1 stop, sans bit de parité. USART_Printf_Init(115200); // Déclare et initialise une structure contenant les paramètres // de configuration de la broche de GPIO. GPIO_InitTypeDef gpioInit = { 0 }; gpioInit.GPIO_Pin = LED_GPIO_PIN; gpioInit.GPIO_Mode = GPIO_Mode_Out_PP; gpioInit.GPIO_Speed = GPIO_Speed_50MHz; // Active le port du GPIO. RCC_APB2PeriphClockCmd(LED_GPIO_RCC, ENABLE); // Configure la broche que nous utilisons. GPIO_Init(LED_GPIO_PORT, &gpioInit); // Cette variable définit l'état que nous voulons assigner // à la broche de GPIO de la LED. BitAction status = Bit_SET; // Variable utilisée pour changer le message envoyé au terminal. int count = 0; while (1) { // Fixe l'état de la broche de GPIO à la valeur souhaitée. GPIO_WriteBit(LED_GPIO_PORT, LED_GPIO_PIN, status); // Inverse l'état souhaité en prévision du tour de boucle suivant. status = (status == Bit_RESET) ? Bit_SET : Bit_RESET; // Attend 125ms avant de changer de nouveau l'état de la broche. Delay_Ms(125); // Envoie un message sur le terminal. printf("count = %d\r\n", count); // IMPORTANT : le message ne sera envoyé au terminal que s'il // est terminé par \r\n, comme dans l'exemple ci-dessus. count++; } }
La vue Terminal
Votre source est modifié, votre carte de développement et votre WCH-LinkE sont connectés à votre ordinateur, il ne reste plus qu'à créer une vue Terminal pour être prêt à afficher les messages de votre application lorsqu'elle sera lancée. Pour cela, basculez sur la perspective Debug puis, comme illustré sur la capture d'écran suivante :
Cliquez sur la zone de la perspective où vous voulez placer la vue Terminal.
Déroulez le menu "Window", puis "Show view", puis cliquez sur "Terminal".
Il reste maintenant à paramétrer la connexion du terminal :
Cliquez sur l'icône "Open a terminal".
Dans la fenêtre popup, choisissez "Serial Terminal", puis le port série correspondant au WCH-LinkE (normalement /dev/ttyACM0), puis cliquez sur OK.
A présent, il n'y a plus qu'à lancer le debugger et vous verrez défiler les messages dans la vue Terminal. Vous disposez maintenant des 2 outils, le debugger et les messages, et vous êtes en mesure de diagnostiquer une foultitude de bugs.
Pour finir, notez que l'icône "Open a new terminal" de la vue Terminal permet d'en ouvrir d'autres, qui peuvent être connectés à d'autres UART, mais aussi correspondre à un shell (Local Terminal) ou une connexion à une machine virtuelle ou un serveur distant (SSH Terminal).
Si vous souhaitez conserver la configuration de ces différentes connexions, vous pouvez utiliser la vue Connections en association avec la vue Console. Ça demande plus de manipulation, mais ça sert à la même chose.
Notes sur les caractères accentués
WCH a codé en dur dans MRS que vos sources utilisent l'encodage GBK, dédié aux caractères chinois, donc vous pouvez avoir des surprises en ouvrant vos sources avec un autre éditeur.
Cependant, une astuce permet de forcer MRS à utiliser l'encodage de votre système (ex. UTF-8) : une fois le projet créé, ouvrez le fichier main.c, remplacez tout son contenu par un ç (c cédille), puis appuyez sur Ctrl-S pour le sauvegarder. Une popup d'erreur sera alors affichée :
Cliquez sur le bouton "Save as UTF-8" et le tour est joué, vous pouvez maintenant passer à la programmation. Bien sûr, si vous créez d'autres fichiers source, vous devrez recourir à la même astuce à chaque fois.
Notez également que si vous copiez des fichiers dans votre gestionnaire de fichiers habituel (ex. explorateur Windows, Caja, Nautilus, etc.) et que vous les collez ensuite dans un répertoire d'un projet sous MRS, ces fichiers seront importés avec l'encodage par défaut de votre système d'exploitation. Les caractères accentués seront donc préservés.
Si par contre vous copiez des fichiers hors de MRS et que vous rafraîchissez ensuite le Project Expolorer de MRS pour les voir, ceux-ci seront présumés être encodés en GBK et les caractères accentués apparaîtront comme des caractères chinois.
Utiliser un autre IDE
Une alternative est d'utiliser Eclipse CDT (ou autre IDE de votre choix) et de le configurer pour utiliser la toolchain de MRS, qui peut être téléchargée séparément. Le problème auquel vous serez alors confronté est le flashage du firmware, qu'il faudra faire soit avec OpenOCD, soit manuellement avec WCH-LinkUtility ou WCHISPTool, comme décrit ici.
Pour flasher votre MCU avec OpenOCD (la version spéciale de WCH), vous devez au préalable enlever la protection en écriture du micro-contrôleur avec WCH-LinkUtility ou WCHISPTool. Ensuite, vous pourrez utiliser la ligne de commande suivante à volonté :
/chemin_openocd_wch/bin/openocd -f /chemin_openocd_wch/wch-riscv.cfg -c "program /votre/fichier.elf verify reset exit"
La question du flashage se posera de toute façon si vous voulez mettre en place de l'intégration et du déploiement en continu (CI/CD).
En attendant, malgré ses quelques défauts, MRS aura grandement facilité vos premiers pas avec le CH32V003 et c'est le principal. Vous avez ainsi une base de départ fonctionnelle à partir de laquelle vous pourrez si besoin mettre en place l'outillage qui vous conviendra le mieux.
Ce que nous avons appris
Vous savez maintenant :
Installer et utiliser MounRiver Studio.
Comment connecter votre carte de développement, votre WCH-LinkE et votre ordinateur.
Ce qu'est un GPIO et comment l'utiliser (en sortie pour l'instant).
Ce qu'est un debugger et comment s'en servir.
Utiliser printf() dans les cas où le debugger n'est pas adapté.