Manuel de laboratoire pour contrôleurs embarqués

Utilisation du langage C et de la plateforme Arduino

James M. Fiore

Traducteurs

| f-leb | Auteur | Aegnor1975 | Vincent PETIT | Delias | kicadder |

Ce manuel est destiné à être utilisé en cours d'introduction aux microprocesseurs et microcontrôleurs embarqués et est approprié aux formations technologiques en génie électrique dispensées en deux ou quatre ans. Il utilise la programmation en langage C et une plateforme matérielle open source et bon marché, à savoir Arduino, et plus spécialement l'Arduino Uno avec son microcontrôleur Atmel ATmega 328P.

Pour réagir au contenu de ce manuel, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : Manuel de laboratoire pour contrôleurs embarqués

- Auteur : James M. Fiore

- Traducteurs : f-leb, Auteur, Aegnor1975, Vincent PETIT, Delias, kicadder

- Parution : 8 mai 2017

- Mise à jour : 23 juillet 2018

- Public visé :

- Licence : Le contenu de cet article est rédigé par James M. Fiore et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. Note de l'auteur

- III. Introduction à la programmation en langage C

- IV. Les entrées-sorties standard

- V. Les structures conditionnelles

- VI. Les boucles

- VII. Introduction aux pointeurs

- VIII. Hello Arduino

- VIII-A. Installation

- VIII-B. EDI Arduino

- VIII-C. Codage

- IX. Les sorties numériques

- X. Les entrées numériques

- XI. Les entrées analogiques

- XI-A. Capteur de force

- XI-B. Affichage à LED

- XII. Mesure de temps de réaction

- XIII. Arduino Reaction Timer Redux

- XIV. Arduino Analog Output via PWM

- XV. Arduino Event Counter

- XVI. Arduino Arbitrary Waveform Generator

- XVII. Notes de la Rédaction Developpez.com

I - Introduction

Ce manuel est destiné à être utilisé en cours d'introduction aux microprocesseurs et microcontrôleurs embarqués et est approprié aux formations technologiques en génie électrique dispensées en deux ou quatre ans. Il utilise la programmation en langage C et une plateforme matérielle open source et bon marché, à savoir Arduino, et plus spécialement l'Arduino Uno avec son microcontrôleur Atmel ATmega 328P.

Le manuel contient des exercices en nombre suffisant pour un cours de 15 semaines incluant deux ou trois heures de travaux pratiques hebdomadaires. Certains exercices peuvent requérir davantage qu'une seule séance de travaux pratiques (en particulier dans le cas du générateur de forme d'onde arbitraire). La première partie se limite à une introduction au langage C en utilisant un environnement de bureau standard. N'importe quel compilateur fera l'affaire, et beaucoup d'entre eux sont disponibles gratuitement. La seconde partie (environ les deux tiers de l'ouvrage) est consacrée à la plateforme Arduino.

Le système Arduino a été choisi parce que l'environnement logiciel est libre, open source et multiplateforme (Windows, Mac et Linux). Il y a plusieurs choix de cartes matérielles possibles, dont la grande majorité est bon marché et open source. Bien que ce manuel se concentre sur la carte Arduino Uno, d'autres cartes peuvent être utilisées moyennant quelques modifications dans le texte. Les composants choisis sont assez courants : LED et afficheurs 7 segments, interrupteurs, condensateurs, diodes, résistances basse puissance et transistors en commutation comme les 2N3904 ou 2N2222 (NDLR Ou un BC337 plus récent). Un composant plus évolué tel un FSR (Force Sensitive Resistor ou capteur de force résistif) sera utilisé bien que le circuit puisse être implémenté avec un simple bouton-poussoir à appui momentané. Un autre composant d'intérêt est un petit moteur à courant continu ou un ventilateur de boîtier pour PC (et optionnellement, un transistor de puissance MOSFET ZVN4206A).

Chaque exercice débute avec une vue d'ensemble des points à discuter. Cela inclut généralement des bouts de code à titre d'illustration. Une application d'intérêt est alors développée en pseudocode. Le pseudocode est ensuite décomposé en bouts de code appropriés en langage C. S'il y a lieu, les solutions pour interfacer le matériel sont discutées. Pour terminer, on rassemble le tout.

Il y a un ensemble de ressources éducatives open source (OER : Open Educational Resource) pour accompagner ce manuel de laboratoire. Les autres manuels de cette série (NDLR Sur le site dissidents.com) incluent les circuits électriques à courant continu (DC) ou alternatif (AC), la programmation en Python, les amplificateurs opérationnels, les circuits intégrés linéaires et les semi-conducteurs. D'autres ressources OER sur les amplificateurs opérationnels, les circuits intégrés linéaires et les semi-conducteurs sont également attendues au début de 2017. Veuillez consulter mon site web pour obtenir les dernières versions.

II - Note de l'auteur

Ce manuel est utilisé au Mohawk Valley Community College d'Utica (État de New York) pour son programme de formation au grade d'associé AAS (Associate of Applied Science) en Génie électrique accrédité par l'ABET (Accreditation Board for Engineering and Technology, Inc.). Il était capital que ce programme minimise les coûts pour les étudiants afin qu'ils puissent eux-mêmes acheter leur propre système de développement. L'accès à tout moment au développement matériel et logiciel peut être une source de motivation pour les étudiants. Je suis redevable envers mes étudiants, collègues ainsi qu'à toute la communauté du MVVC pour leur soutien et leurs encouragements dans ce projet. Alors qu'il aurait été possible de trouver un éditeur classique pour publier ce travail, en tant que partisan et contributeur de longue date au développement informatique de gratuiciels et partagiciels, j'ai préféré publier ce manuel sous licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions. J'encourage chacun à utiliser ce manuel dans le cadre de son propre travail et à poursuivre ses travaux en s'en servant comme base.

Si vous le faites, je serais heureux d'en être informé.

Resist corporate power

- jmf

Pour plus d'informations ou pour contacter l'auteur :

James Fiore, Professor

Electrical Engineering Technology

Mohawk Valley Community College

1101 Sherman Drive

Utica, NY 13501

jfiore@mvcc.edu

www.mvcc.edu/jfiore

III - Introduction à la programmation en langage C

L'objectif principal de ce premier exercice de programmation est de se familiariser avec les outils de programmation du langage. Les programmes dans cet exercice seront assez triviaux, mais serviront de tremplin pour des travaux futurs. Des programmes seront alors développés en se basant sur des programmes similaires à ceux étudiés dans ce cours.

Le paquetage C utilisé ici est sans grande importance. Des entreprises diverses ont créé différents outils de programmation et bien que les caractéristiques peuvent varier sur certains points, les bases restent les mêmes. Tous les outils de programmation en C nécessitent de compiler du code C et de l'assembler. Ce code, une fois assemblé, va en outre devoir se lier à des modules et des bibliothèques qu'il aura fallu assembler aussi, afin d'aboutir à un programme exécutable. Dans le cas le plus simple, ces outils seront disponibles sous forme d'utilitaires en ligne de commande, c'est-à-dire qu'ils seront lancés depuis l'invite de commande d'un shell. Habituellement, les outils font partie d'un environnement de développement intégré graphique, ou EDI (Environnement de Développement Intégré). Un EDI dispose normalement d'un éditeur de texte. Les compilateurs C s'attendent à travailler sur un fichier texte brut. N'essayez pas de compiler un fichier issu d'un traitement de texte, comme un fichier .doc. Si vous utilisez un simple outil en ligne de commande au lieu d'un EDI, vous créerez vos fichiers sources avec un éditeur de texte basique comme Notepad.

Les fichiers sources en C utilisent une extension « .c ». Le compilateur produit en sortie un fichier objet, avec une extension « .o » ou « .obj ». Dans l'environnement Windows, les exécutables produits ont une extension « .exe ». La plupart des EDI requièrent la création d'un projet avant de saisir du code. Le projet inclut certaines informations utiles comme les noms de fichier utilisés pour le code source (il peut y en avoir plusieurs pour les grands projets), les bibliothèques à lier, le nom du fichier exécutable final, et d'autres choses encore. Pour des tâches simples de programmation impliquant de petits codes sources, tout ceci peut paraître lourd, mais ces outils sont merveilleux pour de plus ambitieux projets. Tous les codes sources de tous nos exercices vus pendant ce cours peuvent facilement tenir sur une modeste clé USB (64 Mo !). Cela inclut les fichiers du projet qui peuvent occuper une taille plus importante (plusieurs mégaoctets) que le fichier source en C lui-même. Dans les laboratoires en réseau du campus, les fichiers projets et les fichiers sources peuvent être sauvegardés dans l'espace de travail de la session des étudiants. Pour ceux qui travailleraient en dehors du réseau du campus, il sera probablement plus simple au besoin de créer de nouveaux projets sur leur propre disque dur.

Ce laboratoire utilisera l'environnement Pelles C. Il n'y a rien d'exceptionnel et de magique dans Pelles C et d'autres environnements sont tout à fait acceptables. On utilisera cependant Pelles C pour introduire des exercices.

Une fois lancé, l'accent pourra être mis sur le développement des cartes Arduino.

Nos premiers exercices porteront sur la création de projets, l'édition de code source, la compilation et l'édition de liens, puis le test de l'application. On éditera alors à nouveau le code puis le processus sera répété. Les rapports d'erreurs seront également analysés. Si vous passez par des utilitaires en ligne de commande, voyez la note à la fin de cet exercice avant de poursuivre.

Pour commencer, ouvrez l'EDI. Dans Pelles C, sélectionnez « Start a new project » depuis le panneau de démarrage. Vous avez de nombreux choix pour créer un nouveau projet. On ne créera pas de programme avec une interface graphique Windows, mais plutôt des interfaces en mode texte en console DOS. Pour cela, sélectionnez une application Win32 ou Win64 (selon votre système d'exploitation) en mode console et donnez un nom à votre projet. Pour créer un code source en C, vous devrez créer un nouveau fichier texte. Sélectionnez New >> Source code du menu File. Une fenêtre d'édition vierge s'ouvrira.

Tapez le code suivant dans l'éditeur :

#include <stdio.h>
/* Tout commence ici */

int main( void )
{
printf("Hello world!\n");
}

Sauvegardez le code sous le nom hello.c en passant par le menu File >> Save as. Une boîte de dialogue apparaît pour vous proposer de sauvegarder ce fichier dans le projet courant. Sélectionnez « Yes ». Notez que dans certains EDI, vous devrez insérer manuellement le fichier dans le projet (dans un tel cas, il faudra trouver le menu approprié dans l'interface).

Bien qu'il soit possible de compiler et lier les modules séparément, la plupart des développeurs utilisent le raccourci Build. Ce raccourci permet de lancer les opérations de compilation et d'édition de liens avec tous les fichiers nécessaires. Dans Pelles C, vous pouvez sélectionner le menu Build soit depuis le menu Project soit depuis la barre d'outils. Pendant la génération de l'exécutable du projet, vous noterez les messages qui s'affichent dans la barre d'état en bas. Si tout se passe correctement, vous lirez le message « Building <nom du projet> Done ». Vous pouvez maintenant tester le programme. Sélectionnez soit le bouton Execute de la barre d'outils soit Project >> Execute from depuis le menu. Une petite fenêtre DOS devrait apparaître avec le message « Hello World! ». Pressez une touche pour effacer cette fenêtre et retourner dans l'EDI. Vous avez brillamment réalisé votre premier programme !

En fonction des paramètres de configuration de votre EDI, vous pouvez observer différentes colorations du texte dans l'EDI. Dans la plupart des EDI, vous pouvez spécifier la coloration du texte selon qu'il s'agit de mots clés, de constantes, d'opérateurs mathématiques, de commentaires et autres. Cela rend le code plus lisible.

Éditez le code source afin d'y apporter quelque chose d'autre. Modifiez le code comme suit :

#include <stdio.h>
/* Programme 2 */

int main( void )
{
int x, y;

x = 10;
y = x + 20;
printf("Le resultat est %d\n",y);
}

Générez à nouveau l'exécutable (Build), et vérifiez le résultat. Vous devriez obtenir un message qui dit « Le resultat est 30 ».

La plupart des éditeurs intégrés aux EDI ont normalement les fonctionnalités de couper-copier-coller (cut/copy/paste) et de recherche-remplacement (search/replace) de texte. Certains ont même des fonctionnalités automatiques d'indentation, d'ajouts de parenthèses ou crochets, etc.

D'accord, mais que se passe-t-il si on fait une erreur dans son code ? Vous allez insérer quelques erreurs exprès dans le code et observer ce qui se passe. Modifiez le programme vu précédemment pour qu'il ressemble à ce qui suit :

#include <stdio.h>
/* Programme 3, avec des erreurs */

int main( void )
{
int x, y;

x = 10
y = x + 20;
printf(Le resultat est %d\n",y);
}

Notez que le point-virgule est volontairement omis à la fin de la ligne x = 10, ainsi que le guillemet qui ouvre la chaîne de caractères dans la fonction printf(). Générez à nouveau le projet. Cette fois vous recevrez une flopée d'erreurs et d'avertissements. Ils peuvent différer dans la formulation d'un environnement de développement à l'autre, mais vous devriez lire un message concernant un point-virgule manquant avant le y. Vous devriez probablement aussi découvrir une erreur concernant un identifiant « Le » non déclaré, et d'autres avertissements encore. Habituellement, double-cliquer sur le message d'erreur met en surbrillance la ligne fautive dans le code. Parfois, une simple omission et la compilation génère des douzaines de messages d'erreur, car le compilateur perd, en quelque sorte, le fil, et commence à repérer des erreurs là où le code est pourtant correct. Pour cette raison, si vous avez des erreurs (et immanquablement, vous en aurez), commencez toujours par repérer et résoudre la première erreur dans la liste des messages. Commencer la chasse aux erreurs en commençant par la dernière reportée pourrait vous mener sur une fausse piste.

Finalement, vous voudrez peut-être sauvegarder votre code. Sélectionnez simplement File >> Save as et saisissez un nom approprié. Encore une fois, les fichiers sources doivent avoir une extension « .c ». Remarquez que vous pouvez créer, lire et éditer un fichier source en C sans EDI. Tout ce dont vous avez besoin est un simple éditeur de texte. Vous ne pourrez pas compiler ou générer un exécutable avec, mais vous pourrez au moins travailler un peu en mission si vous n'avez pas de compilateur sous la main.

Pour ceux qui utiliseraient un compilateur en ligne de commande (sans EDI), le processus est similaire à celui décrit précédemment, bien que moins automatisé. Vous devrez créer un fichier source avec un éditeur de texte. Vous invoquerez le compilateur depuis une fenêtre DOS dans un shell Windows, habituellement avec la commande qui suit :

cc hello.c

Cela créera le fichier objet. Puis vous évoquerez l'édition de liens, habituellement avec une commande comme :

ln hello.exe hello.obj stdio.lib

Vous aurez à consulter la documentation pour les commandes appropriées et la syntaxe. Une fois l'exécutable créé, vous pourrez le lancer depuis le shell en tapant son nom :

hello.exe

Le message prévu s'affichera dans la fenêtre du shell. Pour modifier le programme, rouvrez le fichier source C avec l'éditeur de texte, faites les modifications, sauvegardez le fichier, et répétez les commandes de compilation et d'édition de liens. Si des erreurs apparaissent, les messages d'erreurs seront affichés dans la fenêtre du shell.

IV - Les entrées-sorties standard

Dans cet exercice, on étudiera plus en détail les fonctions d'entrées-sorties standard, printf() et scanf().

On approfondira sur l'usage des fonctions personnalisées et leur prototype ainsi que sur l'importance d'une spécification du programme adéquate et le choix judicieux des types de variables.

Cet exercice consiste à créer un programme qui contribuera à l'analyse d'un circuit électrique série simple à courant continu en prenant en compte les tolérances des composants. Supposez qu'on ait un circuit avec une source de tension constante et une résistance.

On cherche à déterminer le courant traversant le circuit et la puissance dissipée par la résistance. C'est un exercice assez simple faisant seulement appel à la loi d'Ohm et à l'expression de la puissance électrique. Pour rendre la chose plus intéressante, la tolérance sur la valeur de la résistance sera prise en compte. Le programme créera un tableau des courants et puissances pour les valeurs nominale, maximale et minimale de la résistance. Ce programme devrait être très utile pour quelqu'un qui commence ses études en électricité. Gardons cela à l'esprit pendant la conception du programme.

Quand on conçoit un programme, on essaie de garder les interactions utilisateur les plus simples et les plus claires possible. Ainsi, essayez de structurer le code afin de faciliter sa maintenance. Les études ont montré que la majeure partie du temps passé en programmation d'applications non triviales relève du domaine de la maintenance (ajouter des fonctionnalités, corriger les bogues, etc.). Efforcez-vous de produire du code aussi clair que l'eau de roche et d'y inclure les commentaires appropriés. Toutefois, n'ajoutez pas de commentaires inutiles et semant la confusion sur du code que n'importe quel débutant comprendrait. Voici un exemple de commentaire superflu :

a = b + c; /* additionner b et c */

Bien entendu, ce commentaire n'apporte aucune valeur ajoutée. De même, utilisez des noms de variables mnémoniques quand c'est possible. C'est une façon de commenter le code, par exemple :

x = y + z * 60;
total_seconds = seconds + minutes * 60;

Ces deux lignes de code réalisent chacune les mêmes opérations mathématiques, mais la seconde ligne vous donne un indice sur l'intention du développeur. La première ligne devrait probablement nécessiter l'insertion d'un commentaire pour éclairer tandis que la seconde ligne se suffit à elle-même.

Le programme

Voici une première tentative de spécification de notre programme.

Au prompt (invite de commande), l'utilisateur doit saisir la valeur de la source de tension continue, la valeur nominale de la résistance et sa tolérance. Le programme affichera alors les valeurs de courant et de puissance dissipée sur la base des valeurs nominale, minimale et maximale de la résistance.

Pas mal, mais il faut encore préciser les choses. Premièrement, les programmes en ligne de commande doivent afficher des indications au démarrage. Souvenez-vous qu'il n'y a pas d'interface graphique avec des menus d'aide pour guider l'utilisateur.

Deuxièmement, toujours indiquer les unités des valeurs à saisir au prompt. Si le programme s'attend à des valeurs en ohms et que l'utilisateur saisit des kiloohms, il va y avoir des ennuis. À moins qu'il n'y ait un motif impérieux, il faut toujours préférer les unités de base (ohms plutôt que kiloohms par exemple).

Voici nos spécifications revues et affinées.

Le programme commencera par donner des directives et explications à l'utilisateur sur son fonctionnement. Le programme demande ensuite à l'utilisateur de saisir la valeur de la source de tension continue en volts, la valeur nominale de la résistance en ohms et sa tolérance exprimée en pourcentage. Il affichera alors les valeurs de courant en ampères et la puissance dissipée en watts sur la base des valeurs nominale, minimale et maximale de la résistance.

Remarquez qu'on a spécifié une tolérance exprimée en pourcentage plutôt qu'en facteur. Cela parce que l'utilisateur a plutôt tendance à saisir « 10 » pour 10 % que 0,1. Vous pouvez maintenant exploiter cette spécification pour préparer un pseudocode ou un diagramme. Voici un pseudocode possible :

1. Affichage des directives à l'utilisateur ;

2. Saisie de la valeur de la tension (en volts) ;

3. Saisie de la valeur de la résistance (en ohms) ;

4. Saisie de la valeur de la tolérance (en pourcentage) ;

5. Détermination des valeurs minimale et maximale de la résistance ;

6. Calcul des courants sur la base des trois valeurs de résistance ;

7. Calcul des puissances dissipées sur la base des trois valeurs de résistance ;

8. Affichage d'un entête de tableau pour les valeurs ;

9. Affichage des valeurs sous forme tabulaire.

Vous pouvez évidemment choisir un algorithme alternatif ou une autre méthode. Par exemple, vous préférerez peut-être afficher l'entête du tableau avant de faire les calculs, puis afficher les valeurs au fur et à mesure qu'elles sont calculées. Vous préférerez peut-être aussi changer le format d'affichage avec trois colonnes pour chacune des résistances, les courants et puissances étant disposés en ligne.

Vous pourriez aussi changer totalement d'approche en utilisant des boucles ou des tableaux. Il y aura des avantages et des inconvénients pour chaque approche. Parfois, la question n'est pas « Puis-je résoudre ce problème ? », mais « Quelle est la meilleure façon de résoudre ce problème ? » Étendez votre réflexion avant de coder.

Sur la base du pseudocode ci-dessus, le programme suivant devrait répondre aux objectifs. Il sera amélioré plus tard. Notez l'usage du type de données double étant donné qu'on a affaire à des valeurs fractionnaires.

Afin d'éviter tout problème d'encodage des caractères accentués dans l'éditeur ou la console, ceux-ci ont volontairement été retirés du code source.

#include <stdio.h>
int main( void )
{
double v, tol;
double rnom, rlow, rhigh;
double inom, ilow, ihigh;
double pnom, plow, phigh;
printf("Ce programme determine le courant et la puissance.\n");
printf("Entrez la valeur de la source de tension en volts.\n");
scanf("%lf", &v);
printf("Entrez la valeur de la resistance nominale en ohms.\n");
scanf("%lf", &rnom);
printf("Entrez la valeur de la tolerance de la resistance en pourcentage.\n");
scanf("%lf", &tol);
tol = tol/100.0; /* tolerance convertie en facteur */
rlow = rnom - rnom*tol;
rhigh = rnom + rnom*tol;
inom = v/rnom;
ihigh = v/rlow;
ilow = v/rhigh;
pnom = v * inom;
plow = v * ilow;
phigh = v * ihigh;
printf("Resistance (ohm)  Courant (amp)  Puissance (watt)\n");
printf("%lf         %lf         %lf\n", rnom, inom, pnom );
printf("%lf         %lf         %lf\n", rhigh, ilow, plow );
printf("%lf         %lf         %lf\n", rlow, ihigh, phigh );
}

Une mise au point s'impose : la variable ihigh est la valeur la plus élevée du courant, et non la valeur du courant associée à la plus grande valeur de la résistance. Ce point pourrait prêter à confusion, et pourrait amener à insérer un commentaire ! Ainsi, les bases sont posées. Dans tous les cas, saisissez le code précédent et générez l'exécutable, puis vérifiez qu'il fonctionne.

Vous avez peut-être noté qu'il y avait des répétitions dans le code au niveau des formules et des affichages.

Il serait plus commode de créer des fonctions pour gérer ces répétitions. Par exemple, on pourrait créer une fonction pour calculer le courant :

double find_current( double voltage, double resistance )
{
double current;
current = voltage/resistance;
return( current );
}

Vous pourriez aussi faire cela en une ligne :

double find_current( double voltage, double resistance )
{
return( voltage/resistance );
}

Après mise à jour du programme, on obtient le code suivant :

#include <stdio.h>

double find_current( double voltage, double resistance )
{
return( voltage/resistance );
}

int main( void )
{
double v, tol;
double rnom, rlow, rhigh;
double inom, ilow, ihigh;
double pnom, plow, phigh;
printf("Ce programme determine le courant et la puissance.\n");
printf("Entrez la valeur de la source de tension en volts.\n");
scanf("%lf", &v);
printf("Entrez la valeur de la resistance nominale en ohms.\n");
scanf("%lf", &rnom);
printf("Entrez la valeur de la tolerance de la resistance en pourcentage.\n");
scanf("%lf", &tol);
tol = tol/100.0; /* tolerance convertie en facteur */
rlow = rnom - rnom*tol;
rhigh = rnom + rnom*tol;
inom = find_current( v, rnom );
ihigh = find_current( v, rlow );
ilow = find_current( v, rhigh );
pnom = v * inom;
plow = v * ilow;
phigh = v * ihigh;
printf("Resistance (ohm)  Courant (amp)  Puissance (watt)\n");
printf("%lf         %lf         %lf\n", rnom, inom, pnom );
printf("%lf         %lf         %lf\n", rhigh, ilow, plow );
printf("%lf         %lf         %lf\n", rlow, ihigh, phigh );
}

L'amélioration n'est pas flagrante. En fait, elle semble même rallonger le code ! C'est vrai, mais attardez-vous un moment sur l'idée sous-jacente. Que deviendrait le code si le calcul du courant impliquait une douzaine de lignes au lieu d'une seule ? Cette nouvelle écriture permettrait donc d'économiser considérablement le nombre de lignes de code. Il ne s'agit pas seulement d'avoir moins de code à taper, mais plutôt d'économiser sur la taille de l'exécutable, et cela est particulièrement important sur les systèmes embarqués contraints avec peu de mémoire disponible.

Remarquez que la nouvelle fonction a été ajoutée avant le main(). Ce n'est pas obligatoire. On pourrait l'ajouter après le main(), mais dans ce cas il faut ajouter le prototype de la fonction pour que le compilateur soit informé quand il découvrira l'appel de la fonction dans le main(). Le code avec le prototype devrait ressembler à ce qui suit :

#include <stdio.h>

/* Prototype de la fonction afin de prevenir le compilateur */
double find_current( double voltage, double resistance );

int main( void )
{
....
}

double find_current( double voltage, double resistance )
{
return( voltage/resistance );
}

Complétez le programme pour mettre en œuvre cette nouvelle fonction de calcul du courant, et testez-le. Complétez le programme à nouveau pour implanter une fonction qui calcule la puissance, et une fois encore pour implanter une fonction d'affichage des trois valeurs.

Prenez la fonction de calcul du courant comme modèle. Finalement, reconsidérez le fonctionnement du programme et envisagez les dysfonctionnements possibles.

Qu'arriverait-il si l'utilisateur saisissait la valeur 0 comme valeur de résistance ? Comment pourrait-on résoudre ce problème ?

V - Les structures conditionnelles

Dans cet exercice, on examinera l'utilisation des structures conditionnelles. Celles-ci comprennent les instructions if/else et switch/case. Les structures conditionnelles sont utilisées pour faire un choix, c'est-à-dire aiguiller le déroulement du programme dans différentes directions, en fonction des circonstances rencontrées. La construction if/else fonctionne bien s'il n'y a que deux actions possibles, alors que le switch/case est plutôt conçu pour aiguiller le programme selon une liste de possibilités établies. La structure conditionnelle la plus simple est le if(). Si le ou les éléments à tester sont évalués comme vrais, alors telle action est prise. En regroupant des clauses avec les opérateurs logiques || et &&, des tests complexes peuvent être créés. Les déclarations if() peuvent être imbriquées, et inclure le traitement pour l'échec du test en utilisant la déclaration else. Si vous devez choisir un seul élément dans une liste, par exemple lors du traitement d'un élément sélectionné dans un menu, cela peut être réalisé de la manière suivante :

if( choice == 1 )
{
/* faire des choses pour le choix 1 */
}
else
{
if( choice == 2 )
{
/* faire des choses pour le choix 2 */
}
else
{
if( choice == 3 )
{
/* faire des choses pour le choix 3 */
}
/* et ainsi de suite */
}
}

Cet arrangement est un peu encombrant en cas de choix dans une grande liste. En outre, il est difficile de traiter des choix multiples (par exemple, choix des éléments 1 et 3). Pour ces situations, le langage C offre la solution switch/case. Il n'y a rien qu'un switch/case peut faire que vous ne puissiez pas recréer avec des if/else imbriqués, mais la première solution offre une plus grande commodité ainsi qu'un code plus clair et plus compact. Le switch/case est utilisé fréquemment, mais en fin de compte, if/else est plus souple, car il ne se limite pas à choisir parmi une liste de valeurs numériques. Habituellement, les constantes numériques ne sont pas utilisées dans le code, comme dans l'exemple ci-dessus. Au lieu de cela, on utilise des directives #define pour les constantes afin de rendre le code plus lisible et intelligible. Une version switch/case de l'exemple précédent pourrait ressembler à :

#define WALK_DOG 1
#define LET_OUT_CAT 2
#define COMB_WOMBAT 3

switch( choice )
{
case WALK_DOG:
/* viens mon chien... */
break;

case LET_OUT_CAT:
/* la porte est la... */
break;

case COMB_WOMBAT:
/* D'abord le shampooing... */
break;

/* et ainsi de suite */
}

Dans cet exercice, nous allons utiliser les deux constructions. Le programme consiste à calculer des paramètres de polarisation DC pour des circuits à transistors simples. On donnera à l'utilisateur un choix de trois polarisations différentes (polarisation par diviseur de tension, polarisation d'émetteur, et polarisation par réaction de collecteur). Le programme demandera alors les valeurs des composants appropriés, déterminera le courant de collecteur au repos et la tension collecteur-émetteur. Il déterminera également si le circuit est en saturation. Ces valeurs seront présentées à l'utilisateur.

Une approche de ce problème est de traiter cela comme trois problèmes élémentaires liés entre eux. C'est-à-dire considérer ce qu'on doit faire pour une polarisation, puis répliquer le traitement avec les changements appropriés pour les deux autres. L'ensemble est ensuite lié avec un simple traitement de menu. Voici un pseudocode :

1. Donner à l'utilisateur une direction appropriée et une liste de polarisations au choix ;

2. Demander à l'utilisateur son choix de la polarisation ;

3. Aller à la routine appropriée pour la polarisation choisie. Pour chaque polarisation :

1. demander les valeurs nécessaires des composants (résistances, alimentation, bêta),

2. calculer Ic et Vce et déterminer si le circuit est en saturation,

3. présenter les valeurs à l'utilisateur.

Les équations appropriées pour chaque polarisation sont les suivantes. Toutes les polarisations utilisent ce qui suit : Vcc est la source d'alimentation positive. Re est la résistance de l'émetteur tandis que Rc est la résistance du collecteur. Bêta est le gain en courant (hfe). La tension base-émetteur (Vbe) peut être supposée égale à 0,7 volt. Notez que si Ic-saturation est supérieure à Ic, alors l'Ic actuel est égal à Ic-saturation et Vce sera égale à 0.

Polarisation par diviseur de tension : nécessite également R1, R2 (résistances supérieure et inférieure du diviseur).

Vth = Vcc*R2/(R1+R2)

Rth = R1*R2/(R1+R2)

Ic = (Vth-Vbe)/(Re+Rth/beta)

Vce = Vcc-Ic*(Re+Rc)

Ic-saturation = Vcc/(Rc+Re)

Polarisation par réaction de collecteur : nécessite également Rb (résistance de base).

Ic = (Vcc-Vbe)/(Re+Rc+Rb/beta)

Vce = Vcc-Ic*(Re+Rc)

Ic-saturation = Vcc/(Rc+Re)

Polarisation d'émetteur : nécessite également Vee (alimentation négative de l'émetteur) et Rb (résistance de base).

Ic = (Vee-Vbe)/(Re+Rb/beta)

Vce = Vee+Vcc-Ic*(Re+Rc)

Ic-saturation = (Vee+Vcc)/(Rc+Re)

Où Vee est une valeur absolue dans tous les cas.

Le programme est découpé en plusieurs morceaux, présentés ci-dessous dans l'ordre d'écriture. D'abord, le squelette principal :

#include <stdio.h>
#include <math.h>
#define VOLTAGE_DIVIDER 1
#define EMITTER 2
#define COLLECTOR_FEEDBACK 3
#define VBE 0.7

int main( void )
{
int choice;
give_directions();
choice = get_choice();

switch( choice )
{
case VOLTAGE_DIVIDER:
voltage_divider();
break;

case EMITTER:
emitter();
break;

case COLLECTOR_FEEDBACK:
collector_feedback();
break;

default: /* Prevenir l'utilisateur qu'il n'a pas brille sur ce coup-la... */
printf("Mauvais choix !\n");
break;
}
}

Les deux premières fonctions pourraient ressembler à ceci (n'oubliez pas que vous devrez ultérieurement ajouter leur prototype) :

void give_directions( void )
{
printf("Polarisation de transistors en courant continu\n");
printf("Calcul du point de repos Q\n\n");
printf("Choix de la polarisation :\n");
printf("1. Par diviseur de tension\n");
printf("2. Par l'emetteur avec deux sources de tension\n");
printf("3. Par reaction de collecteur\n");
}

int get_choice( void )
{
int ch;
printf("Entrez le numero de votre choix :");
scanf("%d", &ch);
return( ch );
}

Maintenant, il est temps d'écrire les fonctions de calcul de polarisation. Voici à quoi la fonction voltage_divider() pourrait ressembler :

void voltage_divider( void )
{
double vcc, vth, r1, r2, rth, re, rc, beta, ic, icsat, vce;
printf("Entrez la tension de l'emetteur en volt");
scanf("%lf", &vcc);
printf("Entrez le gain en courant (beta ou hfe)");
scanf("%lf", &beta);
printf("Toutes les valeurs de resistance doivent être saisies en ohm\n");
printf("Entrez la valeur de la resistance superieure du diviseur");
scanf("%lf", &r1);
printf("Entrez la valeur de la resistance inferieure du diviseur");
scanf("%lf", &r2);
printf("Entrez la valeur de la resistance du collecteur");
scanf("%lf", &rc);
printf("Entrez la valeur de la resistance de l'emetteur");
scanf("%lf", &re);

vth = vcc*r2/(r1+r2);
rth = r1*r2/(r1+r2);
ic = (vth-VBE)/(re+rth/beta);
icsat = vcc/(rc+re);

if( ic >= icsat )
{
printf("Le circuit est en saturation !\n");
printf("Ic = %lf amp et Vce = 0 volt\n", icsat );
}
else
{
Vce = vcc-ic*(re+rc);
printf("Ic = %lf amp et Vce = %lf volt\n", ic, vce );
}
}

Les deux autres fonctions de polarisation devraient être semblables à celle-ci. Quelques points à noter : afin d'obtenir la valeur absolue, envisagez d'utiliser la fonction fabs() (valeur absolue d'un nombre en virgule flottante). Une autre approche du programme serait de rendre les variables vce, icsat et ic comme des variables globales, et de déplacer la section d'affichage finale dans le programme principal main(), car la comparaison finale et l'affichage seront identiques dans les trois fonctions de calcul de polarisation. Il est également possible que les fonctions renvoient les valeurs via des pointeurs, en évitant le besoin de variables globales.

Complétez les deux autres fonctions de polarisation, créez et testez le programme. Pour éviter les avertissements du compilateur, vous devez placer les prototypes de fonction avant le programme principal main(). Vous pouvez les placer dans un fichier d'en-tête séparé, mais ce n'est pas vraiment nécessaire. Pour créer les prototypes, il suffit de copier et coller les déclarations de fonction, et d'ajouter un point-virgule à la fin. Par exemple :

#define VBE .7

void give_directions( void );

int get_choice( void );
/* et ainsi de suite */

void main( void )
{
...

Le compilateur ne « connaîtra » pas les fonctions utilisées dans le programme principal main(), ce qu'elles prennent comme arguments, ni quel type de variable elles retournent si leur prototype n'est pas déclaré auparavant. Sans prototype, le compilateur suppose un argument void par défaut et s'attend à une valeur de retour de type int. En découvrant l'appel de la fonction, le compilateur vous avertira alors que les types ne correspondent pas aux types attendus. Ce mécanisme croisé de vérification peut sembler redondant, mais il peut empêcher de nombreux bogues.

Modifications

Ce programme pourrait être utile à un étudiant apprenant les transistors, mais il peut être un peu lourd à utiliser. Après tout, le programme doit être redémarré à chaque nouveau circuit. En d'autres termes, l'utilisateur y gagnerait en confort s'il pouvait démarrer le programme et l'exécuter continuellement pour 100 circuits s'il le souhaite. Comment modifieriez-vous le programme afin de demander à l'utilisateur s'il souhaite ou non essayer un autre circuit ? Peut-être plus intéressant, qu'est-ce qui serait nécessaire pour que le programme puisse servir à la génération d'« exercices » ? C'est-à-dire pour que le programme puisse créer des circuits appropriés à l'aide de nouvelles valeurs de composants à chaque fois, et en informer l'utilisateur. Il demanderait ensuite à l'utilisateur de calculer le courant et la tension à la main et de les saisir. Le programme déterminerait alors si les réponses de l'utilisateur sont correctes (avec un certain pourcentage d'erreur pour compenser les erreurs d'arrondi). Une autre idée serait de présenter à l'utilisateur une liste à choix multiples de réponses possibles, au lieu de lui faire entrer des valeurs précises. Enfin, comment pouvez-vous essayer de « dessiner » les circuits afin que les composants soient visuellement identifiés ?

VI - Les boucles

Dans cet exercice, on s'attardera sur l'utilisation des boucles ou comment itérer. Quelques fonctions utiles dans la bibliothèque standard seront étudiées. Les ordinateurs sont évidemment idéals pour les calculs répétitifs comme ceux nécessaires pour remplir des tables de données.

Si un nombre d'itérations précis est requis, une construction avec la boucle for est généralement le meilleur choix. Dans les situations où le nombre d'itérations n'est pas connu à l'avance, ou quand la condition de fin de boucle peut être exprimée de façon plus aisée, la boucle while() est préférable. Cet exercice se limitera à utiliser les constructions avec la boucle while(), bien qu'une fois le code saisi, vous seriez tenté de le modifier au profit d'une boucle for().

Ce programme servira à produire une table de valeurs concernant des circuits RC en série (NDLR Circuits analogiques du 1er ordre permettant la réalisation de filtres passe-bas). Il calculera et affichera les valeurs qui pourront servir à tracer un diagramme de Bode. Ce diagramme montre le gain ou l'atténuation en sortie d'un circuit en réponse à la fréquence. Dans cet exemple, la tension aux bornes du condensateur sera prise comme grandeur de sortie. On utilise plus couramment le gain en décibel (ou dB) au lieu du gain absolu calculé à partir du rapport des tensions. Ce programme ne permettra pas de tracer la courbe, mais plutôt de produire les coordonnées des points de la courbe pour que vous puissiez la tracer vous-même.

Premièrement, le programme aura besoin d'obtenir les valeurs de résistance et capacité de la part de l'utilisateur. Avec ces valeurs, il calculera et affichera la fréquence de coupure, fc. L'utilisateur devra alors saisir la plage de fréquences pour le calcul des gains. Il lui sera demandé de donner une fréquence de départ, une fréquence de fin et le nombre de points de fréquence à calculer par décade (une décade correspond à un facteur 10 entre deux nombres).

Habituellement, l'échelle linéaire sur l'axe des abscisses (l'axe des fréquences) n'est pas privilégiée, et on lui préfère une représentation sur une échelle logarithmique. Avec cette nouvelle échelle, deux graduations dont le rapport vaut une valeur donnée sont à distance constante. Par exemple, si la plage du tracé est entre 100 Hz et 1000 Hz et que l'on veut deux points par décade, on ne retiendra pas les valeurs 100, 550 et 1000 (1000 débutant une nouvelle décade, 100 et 550 sont alors considérés comme les « deux points par décade »).

550 est à égale distance de 100 et 1000, mais le rapport 550/100=5,5 est plus grand avec la graduation de départ, qu'avec la graduation de fin, car 1000/550≈1,82. En fait, on retiendra les graduations 100, 316 et 1000, car les rapports 316/100 et 1000/316 sont égaux tous les deux à 3,16. Simple, non ! Réfléchissez à la spécification « deux points par décade ». On veut un nombre qui lorsqu'il est multiplié par lui-même vaut 10, c'est-à-dire égal à la racine carrée de 10 (3,16). Si on veut cinq points par décade, on utilisera la racine cinquième de 10, et ainsi de suite.

Note de la rédaction

550 est à mi-distance entre 100 et 1000, car 550-100=1000-550=450. Sur une échelle logarithmique, le milieu géométrique entre les graduations 100 et 1000 est à la graduation 316, car log(316)-log(100)=log(316/100) est égal à log(1000)-log(316)=log(1000/316).

Voici un pseudocode pour commencer :

1. Donner des indications à l'utilisateur ;

2. Obtenir de l'utilisateur les valeurs de la résistance en ohms et de la capacité en farads ;

3. Calculer et afficher la fréquence de coupure ;

4. Obtenir de l'utilisateur les fréquences de départ et de fin (en Hz) ainsi que le nombre de points par décade ;

5. Afficher l'en-tête de la table, c'est-à-dire fréquence (Hz) et gain (dB) ;

6. Déterminer le facteur multiplicateur de la fréquence à partir du nombre de points par décade ;

7. Initialiser la fréquence à la valeur de départ ;

8. Commencer à boucler sur la fréquence tant qu'elle est inférieure à la fréquence de fin ;

9. Calcul du gain en dB ;

10. Afficher la fréquence et la valeur du gain correspondant ;

11. Multiplier la fréquence par le facteur calculé au 6. pour obtenir la valeur suivante de la fréquence ;

12. Fin de la boucle, puis fin du programme.

La seule chose qui pourrait rester un peu floue est le calcul du gain et sa conversion en décibels (dB). La conversion en décibels est définie par la formule suivante :

En gros, le gain s'obtient grâce à un diviseur de tension réalisé entre la résistance et la réactance capacitive. Ainsi, on peut détailler l'étape 9 :

9a. Déterminer la réactance capacitive XC.

9b. Calculer le gain basé sur le diviseur de tension : , (Rappel : le module de l'expression est )

9c. Calculer le gain en dB avec la formule : .

La bibliothèque de fonctions mathématiques (math.h) nous sera très utile pour les fonctions usuelles comme les logarithmes, racines carrées et autres. Les fonctions qui nous concernent ici sont : pow(), log10() et sqrt().

Le programme

Premièrement, remarquez l'utilisation de #define pour le nombre pi et les définitions de prototypes de fonctions. Les fonctions, bien que de petites tailles, rendent le programme principal beaucoup plus lisible.

#include <stdio.h>
#include <math.h>

#define M_PI 3.141592653

void give_directions( void );
double find_fc( double res, double cap );
double find_xc( double freq, double cap );
double find_dB( double gain );

int main( void )
{
double r, c, xc, gain, dB, steps;
double fc, f, fstart, fstop, ffactor;
give_directions();
printf("Entrez la resistance en ohms :");
scanf("%lf", &r);
printf("Entrez la capacite en Farads :");
scanf("%lf", &c);
fc = find_fc( r, c );
printf("\nLa frequence de coupure est %lf Hertz.\n\n", fc);
printf("Entrez la frequence de depart en Hertz :");
scanf("%lf", &fstart);
printf("Entrez la frequence de fin en Herz :");
scanf("%lf", &fstop);
printf("Entrez le nombre de pas a afficher par decade :");
scanf("%lf", &steps);
printf("Frequence (Hz)\t\t\tGain (dB)\n"); /* \t is a tab */
ffactor = pow( 10.0, 1.0/steps );
f = fstart;
while( f <= fstop )
{
xc = find_xc( f, c );
gain = xc/sqrt(r*r + xc*xc); /* On pourrait utiliser pow() ici pour elever au carre, mais la multiplication du nombre par lui-meme s'execute plus rapidement */
dB = find_dB( gain );
printf("%10.1lf\t\t%10.1lf\n", f, dB ); /* %10.1lf 10 espaces avec 1 chiffre apres la virgule */
f *= ffactor; /* raccourci pour f=f*ffactor; */
}
}

void give_directions( void )
{
printf("Generation tableau de Bode\n\n");
printf("Ce programme affichera les gains en dB pour un circuit RC serie\n");
}

double find_fc( double res, double cap )
{
return( 1.0/(2.0*M_PI*res*cap) );
}

double find_xc( double freq, double cap )
{
return( 1.0/(2.0*M_PI*freq*cap) );
}

double find_dB( double gain )
{
return( 20.0 * log10( gain ) );
}

Saisissez ce programme et testez-le pour différents couples de valeurs en entrée. Examinez ce qui pourrait mal tourner avec ce programme, et comment vous pourriez contourner les problèmes. Par exemple, considérez ce qui pourrait arriver si l'utilisateur saisissait une valeur nulle pour la résistance, ou si la fréquence de fin était plus petite que la fréquence de départ.

Note de la rédaction
Exemple de sortie console obtenue dans l'EDI Pelles C :

À la fréquence de coupure, le gain est de -3 dB.
Remarquez que pour des fréquences élevées, la courbe tend asymptotiquement vers une droite de pente -20 dB par décade. Et l'on retrouve les résultats classiques d'un filtre passe-bas du 1er ordre de la figure ci-dessous.

Aménagements et évolutions

Il y a deux évolutions possibles de ce programme une fois que vous aurez complété cet exercice. La première, déjà mentionnée plus tôt, consiste à aménager la boucle en passant par une construction avec une boucle for(). La deuxième consiste à rajouter une troisième colonne pour le calcul de la phase pour chaque valeur de fréquence. En effet, un diagramme de Bode complet comporte deux courbes, une pour le gain en fonction de la fréquence, et l'autre pour la phase en fonction de la fréquence.

VII - Introduction aux pointeurs

Toutes les variables et fonctions ont une adresse (emplacement de mémoire) qui leur est associée. C'est là qu'elles résident. Pour les données plus grandes qu'un octet, comme les tableaux, l'adresse est celle du premier élément du bloc de données. L'adresse de toute variable (à l'exception des variables définies avec la classe de stockage register) peut être trouvée avec l'opérateur &. Selon le système d'exploitation, l'adresse sera généralement contenue dans 2, 4 ou 8 octets. C'est-à-dire que toutes les adresses possibles du système peuvent être décrites. Si une adresse est placée dans une autre variable, on dit que cette autre variable est un pointeur (c'est-à-dire qu'il stocke l'adresse, ou « pointe vers » la « première » variable). Afin de profiter de la vérification des types et des opérations arithmétiques sur les pointeurs par le compilateur, les pointeurs sont déclarés avec le type d'élément qu'ils pointent, même si tous les pointeurs sont eux-mêmes de la même taille. Ainsi, un pointeur est correctement déclaré en tant qu'un pointeur vers un caractère (char) ou un pointeur vers un nombre à virgule flottante (float). L'opérateur * permet d'indiquer que la déclaration qui suit est un pointeur. Par exemple :

int *pc;

déclare un pointeur vers un type int.

int c, *pc;

c = 12;
pc = &c;

Ce morceau de code déclare une variable de type int et un pointeur vers un type int, attribue 12 à la variable c, puis place l'adresse de c dans le pointeur pc. La valeur de l'élément pointé peut être récupérée via l'opérateur de déréférencement *. Considérons l'ajout suivant à ce qui précède :

printf("La valeur de c est %d, l'adresse de c est %d\n", c, &c );
printf("La valeur de pc est %d, La valeur pointee par pc est %d\n", pc, *pc );

Notez que c et *pc ont la même valeur (12) mais aussi, que si la valeur de c est modifiée, *pc reflète cette modification. Ainsi, en ajoutant :

c = 36;
printf("La nouvelle valeur de c est %d, l'adresse de c est %d\n", c, &c );
printf("La valeur de pc est %d, La valeur pointee par pc est %d\n", pc, *pc );

affichera 36 pour *pc ainsi que pour c, et les adresses ne seront pas modifiées.

Exercice 1 : créez un petit programme basé sur le code ci-dessus et exécutez-le. Comparez vos résultats avec ceux de vos collègues. Que remarquez-vous ?

Que se passe-t-il si l'adresse du pointeur est stockée dans un autre pointeur ? Que signifie l'adresse d'un pointeur stockée dans un autre pointeur ? C'est ce qu'on appelle un pointeur de pointeur. Testez le morceau de code suivant :

int c, *pc, **ppc;
c = 12;
pc = &c;
ppc = &pc;
printf("pc = %d, la valeur pointee par pc est %d\n", pc, *pc );
printf("ppc = %d, la valeur pointee par ppc est %d\n", ppc, *ppc );

Exercice 2 : modifiez votre programme pour implémenter ce qui précède. Exécutez-le et comparez vos résultats. Que pensez-vous de la valeur **ppc ?

Les adresses peuvent être envoyées en tant qu'arguments aux fonctions. C'est ainsi qu'une fonction peut renvoyer plus d'une valeur, c'est un concept important à retenir. Considérez ce qui suit :

int main( void )
{
int a, b, c, *pc;
pc = &c;
assign_it( &a, &b, pc );
printf("Les valeurs dans \"main\" sont : %d %d %d\n", a, b, c);
}
void assign_it( int *x, int *y, int *z )
{
*x = 1;
*y = 20;
*z = 300;
}

Notez que assign_it() peut être appelée en utilisant soit l'adresse via l'opérateur (&) sur une variable existante, soit en passant un pointeur sur une variable (comme dans le cas de &c ou pc). En outre, la déclaration de assign_it() indique qu'elle accepte des pointeurs vers le type int, et non la valeur d'un int.

Exercice 3 : en utilisant ce qui précède comme exemple, créez un programme pour tester le retour de plusieurs valeurs à partir d'une fonction.

Exercice 4 : modifiez ce qui précède pour afficher les adresses des variables reçues par assign_it(), ainsi que &a, &b, et pc dans la fonction main(). Qu'est-ce que cela montre ?

Remarque : si vous n'êtes pas sûr de la taille des pointeurs dans un système d'exploitation donné, utilisez simplement la fonction sizeof(). Cela renverra la taille de l'élément en octets. Par exemple, le morceau de code,

int x, *pc;
x = sizeof( pc );
printf("Les pointeurs font %d octets\n", x);

peut afficher 2 (Windows 3.1), 4 (Windows 95/XP, plusieurs systèmes UNIX) ou 8 (vrais systèmes d'exploitation 64 bits). Notez que pc peut être un pointeur vers un float, double, char ou n'importe quel autre type, il sera toujours de la même taille. C'est un point très important à comprendre. Essayez d'utiliser sizeof() pour vérifier cela.

VIII - Hello Arduino

VIII-A - Installation

Une raison pour laquelle le système Arduino a été choisi pour ce cours est son faible coût. Le logiciel est gratuit et les cartes de développement sont disponibles pour moins de 30 €. Si vous avez déjà installé le logiciel Arduino sur votre ordinateur ou si vous n'avez pas l'intention de le faire, passez au chapitre suivant sur l'« EDI ».

Pour l'installation, allez d'abord tout simplement sur le site http://arduino.cc/en/Main/Software et téléchargez la dernière version du logiciel compatible avec votre système d'exploitation. Il est disponible pour Windows, Mac et Linux. Pour Windows, le logiciel est compressé dans un fichier zip. Il doit être décompressé avec un programme tel que WinZip. Une fois décompressé, exécutez simplement le fichier d'installation. Cela installera tous les fichiers nécessaires en un rien de temps. Il n'y a qu'un seul problème qui survient au moment de l'installation des pilotes (ou drivers) de la carte. Une fois que le logiciel est installé, branchez votre carte Arduino avec un câble USB-A vers USB-B. À un moment donné, Windows vous informera qu'il a trouvé un nouveau composant et qu'il recherche son driver. Si vous sélectionnez « détails », Windows affichera quelque chose comme ceci (Windows 7) :

Le problème est que Windows n'est pas capable de trouver le pilote et vous informe de la manière suivante :

Ne vous inquiétez pas, vous pouvez installer les pilotes manuellement. Le processus varie un peu entre Windows XP, Vista et Windows 7, mais il va généralement comme suit. D'abord, ouvrez le gestionnaire de périphériques (via Panneau de configuration >> Système et sécurité >> Système). Descendez jusqu'à l'élément « Ports ». Vous devriez voir une entrée de type « Arduino R3 (COM 1) » (vous pouvez également trouver « Matériel inconnu » au lieu de « Ports »). Sélectionnez-la et allez vers l'onglet « Pilotes ». Cliquez sur « Mise à jour / Installation du pilote ». Parcourez la liste jusqu'à trouver votre pilote. Il devrait se trouver dans le dossier « drivers » du dossier d'installation du logiciel Arduino. Pour la carte UNO, il devrait se nommer « Arduino Uno R3.inf ». Sélectionnez-le et laissez Windows l'installer. Si vous avez des difficultés, un guide décrit l'installation étape par étape : https://www.arduino.cc/en/Guide/Windows#

Note de la rédaction
Sous Windows 7 et les versions ultérieures, avec les dernières versions de l'EDI, le problème évoqué ne devrait pas se produire. En revanche, ce qui peut se passer est que si vous débranchez la carte puis la rebranchez ou si vous changez de port USB, Windows réinstallera les pilotes et affectera un nouveau port COM à la carte ! Pensez à toujours vérifier dans l'EDI le port COM dédié à la carte Arduino utilisée avant le téléversement du programme.

Note de la rédaction
L'installation sous Linux est tout aussi simple. Téléchargez le fichier tar.xz correspondant à votre système. Décompressez-le dans le dossier de votre choix puis lancez l'installation en exécutant le script install.
Cependant, pour que la communication entre l'EDI et l'Arduino s'opère convenablement, vous devez modifier les droits sur les ports série. Sans cette opération, vous êtes obligé de lancer l'EDI en mode administrateur (via sudo). Pour débloquer le port série sur lequel l'Arduino est connecté, procédez comme suit :
$ sudo usermod -aG dialout votre_login
Source : http://www.leunen.com/linux/2012/11/autoriser-lacces-aux-ports-series-sous-ubuntudebian/

VIII-B - EDI Arduino

Une fois la carte installée, il est temps d'ouvrir l'EDI Arduino. Il est assez simple comparé aux autres systèmes de développement en C.

Vous verrez un simple éditeur de texte avec les habituelles fonctions de copier-couper-coller. La zone noire en dessous de l'éditeur est dédiée aux messages du compilateur ou téléverseur. Autrement dit, c'est là où les erreurs du compilateur apparaissent avec les messages d'états. Le contenu des menus « Fichier » et « Édition » est tout ce qu'il y a de classique. Le menu « Outils » est particulièrement important.

Les deux plus importants items sont « Type de carte » (à partir duquel vous sélectionnez votre carte) et « Port » (à partir duquel vous sélectionnez le port COM où votre carte est connectée). Ne pas configurer cela correctement conduira à une programmation infructueuse de votre carte. Notez que la carte et le port COM sélectionnés sont affichés en bas à droite de la fenêtre de l'EDI.

Il y a une simple barre d'outils sous le menu. Le premier item (symbole check) est appelé « Vérifier » et sert à compiler votre code. Le second item (flèche à droite) téléverse le code vers la carte cible. Les autres boutons servent à ouvrir et enregistrer les fichiers.

VIII-C - Codage

D'ordinaire, tout le code sera contenu dans un seul fichier. Quand vous créez un nouveau projet, ou « sketch » dans Arduino, un nouveau dossier sera créé pour lui. Contrairement aux fichiers sources en C, les sketches Arduino utilisent l'extension « .ino » en lieu et place de « .c ». De plus, le point d'entrée habituel main() n'est pas utilisé. En substance, le système Arduino a écrit le main() à votre place pour ressembler à quelque chose comme ça :

void main()
{
init();
setup();
while(1)
loop();
}

La première fonction appelée est là où l'initialisation des différents systèmes se produit comme l'allocation, le préréglage des horloges, le système de conversion analogique-numérique (ADC), etc. Tout ceci est déjà écrit pour vous. Les deux autres appels, setup() et loop(), sont pour vous. setup() est votre fonction d'initialisation (c.-à-d. les choses que vous devez faire juste une fois au démarrage) et loop() est la partie qui sera répétée indéfiniment. Vous allez donc commencer à coder avec quelque chose qui ressemble au code suivant :

void setup() {

}

void loop() {

}

Une des difficultés avec la programmation embarquée est que vous n'avez pas de console pour les entrées et les sorties via scanf() et printf(). Normalement, un programme embarqué n'a pas besoin de ça mais cela reste très utile quand il s'agit de déboguer un programme. Sans printf(), comment insérer des points d'arrêt dans le programme pour tester la progression, les valeurs, etc. ? Une des astuces est de simplement allumer une LED à un endroit spécifique dans le programme.

La carte Arduino possède une petite LED montée en surface de la carte et connectée à l'un de ses ports dans ce but. Bien que pratique, ceci est limité, ainsi une seconde et plus puissante méthode implique une communication bidirectionnelle avec l'ordinateur hôte. Avec l'Arduino, ceci peut se faire avec la bibliothèque Serial. L'EDI inclut un « Moniteur Série » dans le menu « Outils ». Le sélectionner ouvrira une fenêtre. Vous pouvez l'utiliser pour envoyer des données vers la carte ou recevoir des données provenant de la carte.

Regardons un exemple de communication via le Moniteur Série. Premièrement, considérez l'écriture de données de la carte vers l'hôte. Voici un programme « Hello World » :

void setup()
{
Serial.begin(9600);
}
void loop()
{
Serial.print("Hello World\n");
delay(1000);
}

Dans la fonction setup(), nous ouvrons le port série et nous fixons la vitesse de communication à 9600 bauds (vitesse plus ou moins standardisée pour l'Arduino) en utilisant la fonction Serial.begin(). Il y a la fonction complémentaire Serial.end() si vous décidez d'utiliser les connecteurs dédiés pour la communication série pour d'autres objectifs. Notez que tous les appels vers des fonctions de cette bibliothèque débutent par le nom de la bibliothèque. La fonction loop() écrit votre message en utilisant Serial.print() et attend environ 1 seconde en utilisant la fonction delay(), l'argument de celle-ci étant en millisecondes.

La carte UNO ne possède qu'un seul port série matériel que l'on retrouve sur les connecteurs 0 (Rx) et 1 (Tx). Ce port est utilisé lors des échanges grâce à la classe Serial et également lors du téléversement. Il faut donc faire attention aux conflits si vous désirez connecter des périphériques sur ces connecteurs. Si vous avez besoin de plusieurs ports série, il reste la solution des ports séries virtuels avec quelques limitations ou passer par une carte Arduino comme la Mega qui possède trois ports séries matériels.
En savoir plus sur les ports série virtuels :
https://www.arduino.cc/en/Reference/SoftwareSerial

Une fois la fonction loop() exécutée, le programme retourne dans la fonction principale main(), qui rappelle aussitôt la fonction loop(), et ce indéfiniment. Votre message est affiché une seconde fois, puis une troisième fois et ainsi de suite.

Tapez le morceau de code précédent, compilez et téléversez-le dans la carte. Une fois fait, ouvrez le Moniteur Série et observez la sortie. Toutes les secondes, vous devez voir le message « Hello World » s'afficher encore et encore.

C'est tout ce que le programme du microcontrôleur fait. Pas très utile en soi, mais la possibilité d'afficher un message sur l'hôte est très utile une fois que nous le maîtrisons.

Habituez-vous à utiliser la documentation en ligne. Deux bons points de départ sont la page « Reference » à l'adresse http://arduino.cc/en/Reference/HomePage et la page « Tutorial » (tutoriel) à la page http://arduino.cc/en/Tutorial/HomePage.

La référence inclut des informations sur tous les opérateurs et fonctions disponibles dans le système Arduino. Les tutoriels donnent des informations sur des concepts de programmation et des idées. Par exemple, voici la copie (traduite) de l'aide en ligne sur la fonction Serial.print().

Serial.print()

Description

Affiche les données sur le port série au format ASCII. Cette commande peut prendre plusieurs formes. Les nombres sont affichés en utilisant le caractère ASCII de chaque chiffre. Les nombres flottants sont affichés de la même façon, avec, par défaut, deux chiffres après la virgule. Les octets sont envoyés comme de simples caractères. Les caractères et les chaînes sont envoyés tels quels.

Exemples

- Serial.print(78) donne « 78 »

- Serial.print(1.23456) donne « 1.23 »

- Serial.print('N') donne « N »

- Serial.print("Hello world.") donne « Hello world. »

Un second et optionnel paramètre spécifie la base (format) à utiliser ; les valeurs permises sont BIN (binaire ou base 2), OCT (octal ou base 8), DEC (décimal ou base 10), HEX (hexadécimal ou base 16). Pour les nombres à virgule flottante, ce paramètre spécifie le nombre de décimales à afficher. Par exemple :

- Serial.print(78, BIN) donne « 1001110 »

- Serial.print(78, OCT) donne « 116 »

- Serial.print(78, DEC) donne « 78 »

- Serial.print(78, HEX) donne « 4E »

- Serial.println(1.23456, 0) donne « 1 »

- Serial.println(1.23456, 2) donne « 1.23 »

- Serial.println(1.23456, 4) donne « 1.2346 »

Vous pouvez exploiter les chaînes de caractères avec Serial.print() directement depuis la mémoire Flash où elles restent stockées en passant les chaînes avec F(). Par exemple :

Serial.print(F("Hello World"))

Pour envoyer un octet, utilisez Serial.write().

Syntaxe

- Serial.print(val)

- Serial.print(val, format)

Paramètres

- val : la valeur à afficher. Tout type de donnée.

- format : spécifie la base du nombre (pour des entiers) ou le nombre de décimales (pour les nombres à virgule flottante).

Retour

la taille (entier long) : print() retourne le nombre d'octets écrits bien que la lecture de ce nombre soit optionnelle.

/**** Fin de la copie de la documentation de référence. ****/

Ce qui est bien avec cette fonction est le paramètre optionnel qui vous permet de spécifier le format de l'information. La possibilité d'afficher des valeurs en hexadécimal ou en binaire est particulièrement pratique, par exemple pour examiner les valeurs des bits. Modifiez votre programme tel qu'il est présenté ci-dessous, compilez et téléversez.

void setup()
{
int i = 27;  // essayer différentes valeur

Serial.begin(9600);

// println est équivalent à print avec un saut de ligne
Serial.println(i, DEC);
Serial.println(i, BIN);
Serial.println(i, HEX);
}

void loop()
{
// rien à faire ici
}

Moins commun mais qui reste utile de temps en temps pour le débogage est la possibilité de passer des valeurs au programme. La fenêtre du Moniteur Série possède un champ de saisie en haut associé avec un bouton « Envoyer ». Les fonctions de la classe Serial les plus intéressantes sont Serial.available(), Serial.parseFloat() et Serial.parseInt() :

Serial.available()
Description
Récupère le nombre d'octets (caractères) disponibles pour la lecture sur le port série. Ce sont des données déjà arrivées et stockées dans le buffer de réception du port série (qui contient 64 octets). La fonction available() est héritée de la classe utilitaire Stream.
Syntaxe
Serial.available()
Paramètres
aucun
Retour
le nombre d'octets disponibles pour la lecture.

Serial.parseFloat()
Description
Serial.parseFloat() retourne le premier nombre flottant valide contenu dans le buffer série. Les caractères qui ne sont pas des nombres (ou le signe -) sont ignorés. ParseFloat() se termine par le premier caractère qui n'est pas un nombre à virgule flottante. La fonction parseFloat() est héritée de la classe utilitaire Stream.
Syntaxe
Serial.parseFloat()
Paramètres
aucun
Retour
flottant
/**** Fin de la copie de la documentation de référence. ****/

Serial.parseInt() est similaire à Serial.parseFloat() mais retourne une valeur entière au lieu d'une valeur à virgule flottante. Ces deux fonctions analysent la chaîne d'entrée et commencent par supprimer les caractères non numériques. Dès qu'un numéral est trouvé, elles continuent de regarder les caractères dans la chaîne jusqu'à trouver un non numéral. Ensuite, les codes ASCII numéraux sont traduits en valeur entière (ou en flottant selon le cas). Notez qu'il est possible d'envoyer plusieurs valeurs en même temps.

Voici un exemple sur la façon dont elles pourraient être utilisées pour créer un convertisseur décimal vers hexadécimal. Entrez ce bout de code, compilez et téléversez.

int i1, i2; // variables globales pour mémoriser les données

void setup()
{
Serial.begin(9600);
}
void loop()
{

// Nouvelles données disponibles ?
if (Serial.available() > 0)
{
i1 = Serial.parseInt();
i2 = Serial.parseInt();
//
// Réaffiche ce que vous avez tapé mais en hexa

Serial.println("En hexa, cela donne : ");
Serial.println(i1, HEX);
Serial.println(i2, HEX);
}
}

Une fois le programme téléversé, ouvrez le Moniteur Série et entrez « 10␣27 » dans le champ de texte en haut et cliquez ensuite sur le bouton « Envoyer » à côté (vous pouvez aussi appuyer sur la touche « Entrée » de votre clavier). Le résultat devrait être :

En hexa, cela donne :
A
1B

Vous pouvez aussi bien essayer avec d'autres valeurs. Si vous entrez seulement une valeur, i2 sera considéré comme un 0. Si vous entrez plusieurs valeurs, de multiples retours vont se succéder. Essayer les deux possibilités avant de continuer.

Si vous entrez un nombre négatif, quelque chose d'intéressant va se produire. Essayez en tapant « -1␣-2 ». Que voyez-vous et pourquoi ?

Maintenant que vous avez des connaissances basiques sur l'EDI et que vous savez comment créer un simple texte pour débogage, nous pouvons passer à l'examen de la lecture et de l'écriture sur des circuits externes.

IX - Les sorties numériques

Dans cet exercice, on manipulera les sorties numériques en utilisant à la fois les bibliothèques de l'environnement Arduino et plus directement par « manipulation bit à bit » des registres. Le but sera de contrôler une LED interfacée à l'Arduino avec les circuits électroniques adéquats.

Premièrement, on utilise une version modifiée du programme exemple Blink. Celle-ci fait clignoter la LED montée en surface de la carte et reliée au connecteur 13 de l'Arduino. Périodiquement, la LED sera allumée pendant deux secondes, puis éteinte pendant une seconde. Des messages seront produits en sortie via la liaison série pour rendre compte du résultat.

Tapez le code suivant dans l'éditeur :

/*
MyBlink
Faire clignoter une LED en boucle
avec affichage sur le moniteur série
*/

// Le connecteur 13 est connecté à une LED montée en surface
// sur la plupart des cartes Arduino. On lui attribue un nom :
#define LEDPIN 13

void setup()
{
// Configurer le connecteur en sortie
pinMode(LEDPIN, OUTPUT);
// Configurer la communication série à la vitesse de 9600 bits par seconde
Serial.begin(9600);
}
void loop()
{
Serial.println("On");
digitalWrite(LEDPIN, HIGH); // Allumer la LED
delay(2000); // Temporiser 2 secondes
Serial.println("Off");
digitalWrite(LEDPIN, LOW); // Éteindre la LED
delay(1000); // Temporiser 1 seconde
}

Compilez le code et téléversez-le dans la carte. Ouvrez le Moniteur Série. Vous devriez voir des messages s'afficher, reflétant l'état de la LED montée en surface de la carte.

Le code est plutôt évident, mais quelques précisions s'imposent. Tout d'abord, le connecteur 13 de l'Arduino Uno est connecté à une petite LED montée en surface de la carte. Le connecteur est relié à une résistance de limitation du courant, elle-même reliée à l'anode de la LED, la cathode retournant à la masse. Ainsi, la mise en tension du connecteur allume la LED. La routine setup configure le connecteur en sortie et établit le système de communication série. La routine loop utilise digitalWrite() pour basculer la sortie du connecteur alternativement à l'état haut ou bas, ce qui permet d'allumer ou éteindre la LED. La fonction delay() est utilisée pour maintenir l'état temporairement, et permettre ainsi d'observer le clignotement de la LED à l’œil nu. Cette fonction de temporisation fonctionne très bien ici, mais ce n'est qu'une simple boucle d'attente qui fait qu'aucune autre tâche utile ne peut être exécutée durant ce laps de temps. C'est une sérieuse limitation pour des programmes avec des tâches intensives, mais on s'en contentera pour l'instant.

On quitte maintenant l'utilisation de la LED interne à la carte pour une LED externe. Tandis que l'AVR 328P peut délivrer un courant de 20 mA par broche, si on choisit de piloter plusieurs LED, il est préférable de passer par un transistor afin de limiter le courant demandé au microcontrôleur. Le schéma général est le suivant :

Le transistor NPN saturera et la LED s'allumera avec une tension élevée sur sa base, tandis que dans la version avec le transistor PNP, celui-ci saturera avec une tension basse sur sa base. Dans les deux cas, le courant qui traverse la LED est le courant de saturation du transistor qui est approximativement égal à la tension d'alimentation Vcc, moins la tension de seuil de la LED, le tout divisé par la résistance du collecteur Rc. Par exemple, pour obtenir 10 mA avec une alimentation 5 V et une LED rouge (tension de seuil à 2 V environ), la résistance Rc nécessaire est d'environ 300 Ω. La résistance de base Rb doit être environ égale à 20 fois cette valeur pour un courant de pilotage d'environ 700 microampères. Cela devrait garantir la saturation pour les valeurs courantes de gain en courant β. Les transistors comme les 2N3904 ou 2N3906 sont des choix classiques, mais si un grand nombre de composants doivent être pilotés, il peut être intéressant de se tourner vers un circuit intégré comme l'ULN2003.

Modifiez maintenant le code de l'exemple précédent en utilisant cette fois un transistor NPN piloté avec le connecteur 8 de l'Arduino. Pour anticiper la suite, on précise que ce connecteur correspond au port B, bit 0 (PB0) de l'ATmega328P. Auparavant, il faut câbler les composants sur une plaque d'essais en prenant des valeurs de résistance standards (330 Ω et 6,8 kΩ feront l'affaire). Reliez Vcc au connecteur 5 V de l'Arduino, et la masse au connecteur GND (les deux connecteurs étant sur le même port POWER de la carte).

Reliez maintenant la broche de pilotage (Vbb) au connecteur D8 de l'Arduino. Le code modifié est le suivant :

/*  Clignotement (Blink) d'une LED externe */

#define LEDPIN 8

void setup()
{
pinMode(LEDPIN, OUTPUT);
}

void loop()
{
digitalWrite(LEDPIN, HIGH);
delay(2000);  // temporiser 2 secondes
digitalWrite(LEDPIN, LOW);
delay(1000);  // temporiser 1 seconde
}

Compilez le code, et téléversez-le dans la carte. À l’exécution, vous devriez observer un magnifique clignotement de la LED. Observez également le cadencement. Maintenant, préparez le nouveau montage avec le transistor PNP, puis reliez-le au connecteur D8 à la place de la version NPN du transistor (sans défaire le montage NPN, juste en le déconnectant du microcontrôleur). Ne changez rien au code. Vous devriez noter une différence sur le cadencement allumé/éteint de la LED. Si vous ne constatez aucun changement, reconnectez le NPN et observez à nouveau.

Considérons maintenant l'accès direct aux ressources matérielles, sans passer par les bibliothèques de l'environnement Arduino. Il y a deux méthodes pour faire cela : la première consiste à chercher les adresses réelles des ports matériels et des registres, et de leur assigner des pointeurs. La seconde méthode (un peu plus facile) implique d'utiliser des macros prédéfinies pour les ports et les registres.

Ce qui suit est une variation du programme Blink de clignotement de LED en utilisant l'accès direct au port grâce aux pointeurs. Notez que les variables pour le port et les registres de direction des données sont déclarées, puis se voient assigner en dur les valeurs numériques des adresses.

Tout ceci varie d'un processeur à l'autre, rendant ainsi le code non portable, et ce n'est pas souhaitable. Pour autant, ce processus permet d'illustrer ce qui se passe réellement au niveau du microcontrôleur et il en résulte un code d'assemblage compact. Notez l'utilisation du masque LEDMASK. Changer sa valeur vous permet d'accéder aux différents bits du port. Notez également l'utilisation des opérateurs bit à bit OR, AND et COMPLEMENT (|, &, ~) pour configurer le ou les bits appropriés du registre.

/* Direct Blink: Accès direct et bit à bit via des pointeurs */
// Le port B bit 0 est relié au connecteur D8

#define LEDMASK 0x01

void setup()
{
unsigned char *portDDRB;
portDDRB = (unsigned char *)0x24;
// Configurer la broche en sortie
*portDDRB |= LEDMASK;
}

void loop()
{
unsigned char *portB;
portB = (unsigned char *)0x25;

// Allumer la LED
*portB |= LEDMASK;
delay(2000);

// Éteindre la LED
*portB &= (~LEDMASK);
delay(1000);
}

Tapez le code ci-dessus et reliez le montage avec le transistor NPN au connecteur 8 de l'Arduino. Compilez le code, puis téléversez-le pour le tester. Il devrait fonctionner comme le programme précédent.

Note de la rédaction : Accès aux registres
D'après la documentation (datasheet) de l'ATmega 328P :
Les registres I/O sont situés entre les adresses 0X0020 et 0x005F :

Le connecteur 8 de l'Arduino Uno est relié au Port B, bit 0 :

Pour configurer le connecteur 8 de l'Arduino Uno en sortie, il faut donc accéder au registre de sélection de direction des données à l'adresse 0x0024 :

Si le bit DDB0 est à 1, PB0 est configurée en sortie numérique.

On peut alors configurer l'état du connecteur à l'état haut ou bas grâce au registre de données à l'adresse 0x0025 :

Si le bit PORTB0 est à 1, la sortie PB0 est à l'état haut.

On peut améliorer le programme Direct Blink avec une nouvelle version utilisant des macros prédéfinies comme PORTB. Celles-ci sont définies dans un fichier d'entête, et peuvent ainsi être ajustées pour tout dispositif. Cela rend le code portable sur toute une famille de microcontrôleur plutôt que d'avoir un code spécifique à chaque unité. Et comme vous allez le constater, le code tend à être plus simple à lire.

/* Direct Blink 2: Accès direct et bit à bit via des variables prédéfinies.
*/

// Masque correspond au port B bit 0, connecteur D8 de l'Arduino
#define LEDMASK 0x01

void setup()
{
// Configurer la broche en sortie
DDRB |= LEDMASK;
}

void loop()
{
// Allumer la LED
PORTB |= LEDMASK;
delay(2000);

// Éteindre la LED
PORTB &= (~LEDMASK);
delay(1000);
}

Tapez le code ci-dessus, compilez et téléversez-le dans l'Arduino pour le tester. Il devrait aussi fonctionner comme précédemment.

Toutes les versions de ce programme peuvent être améliorées et étendues au contrôle de plusieurs sorties logiques, chacune étant connectée à un périphérique différent. Par exemple, une sortie pourrait contrôler une LED tandis qu'une autre pilote un relais ou un transistor de puissance pour actionner un moteur. En utilisant la version Direct Blink 2, vous pourrez contrôler plusieurs sorties en une seule instruction, simplement par une opération de masquage appropriée. Tentez la rapide expérience suivante : modifiez LEDMASK avec la valeur 0x21 dans le programme Direct Blink 2 pour piloter en tandem la LED montée en surface de la carte et la LED externe.

Note de la rédaction

Le connecteur D8 qui pilote la LED externe est relié au port B bit 0 (PORTB0) du microcontrôleur, et le connecteur D13 qui pilote la LED montée en surface de la carte Arduino est relié au port B bit 5 (PORTB5).

Le masque approprié pour piloter les deux LED en tandem est :

#define LEDMASK (0b00000001 | 0b00100000)
// soit LEDMASK=0x21

On allume alors les deux LED simultanément en écrivant :

PORTB |= LEDMASK; // Allumer les deux LED simultanément

Il est assez courant de vouloir piloter plusieurs sorties simultanément, mais il est également possible de configurer individuellement chaque sortie en manipulant les bits un par un en insérant une temporisation entre chaque bit modifié. C'est une méthode quelque peu maladroite, qui n'est ni particulièrement précise, ni flexible lorsque de nombreuses sorties doivent être pilotées. Mais nous examinerons les recours plus tard.

Note de la rédaction
Si on reprend le pilotage des deux LED précédentes, on peut allumer individuellement chaque LED :

// masque pour la LED externe, connecteur D8
#define LEDMASK1 0b00000001

// masque pour la LED montée en surface, connecteur D13
#define LEDMASK2 0b00100000

Pour allumer les deux LED, chacune leur tour :

PORTB |= LEDMASK1; // allumer la LED externe
// delay(10);
PORTB |= LEDMASK2; // allumer la LED montée en surface

Défi

Créez un programme qui va faire clignoter deux LED, une rouge et une verte alternativement. Quand la LED rouge s'allume, la verte devra s'éteindre et inversement, quand la LED verte s'allume, la rouge devra s'éteindre. La LED rouge devra rester allumée pendant deux secondes, et la verte devra rester allumée pendant une seconde. À aucun moment les deux LED devront être allumées simultanément ou éteintes simultanément (ou du moins, cela ne doit pas être perceptible par l’œil humain ordinaire). Ce schéma de clignotement devra se répéter encore et encore : la LED rouge allumée pendant deux secondes, la verte pendant une seconde, la rouge pendant deux secondes, la verte pendant une seconde, et ainsi de suite.

Joignez votre code et un schéma électrique avec les caractéristiques des composants. Il est recommandé d'utiliser Multisim ou tout autre logiciel de conception de circuits pour générer le schéma.

X - Les entrées numériques

Dans cet exercice, on s'attache à étudier les entrées numériques du point de vue du code et du point de vue matériel pour le microcontrôleur. Typiquement, le microcontrôleur aura besoin de réagir à un changement d'état (vrai/faux) provenant d'un événement extérieur, suite à une action d'un utilisateur ou d'un capteur. Par exemple, un utilisateur peut appuyer sur un bouton-poussoir dans l'intention de démarrer ou stopper un processus comme couper un signal audio ou mettre en marche un moteur électrique. Les broches d'entrées numériques d'un contrôleur sont conçues précisément pour faire réagir à ce type de signaux (NDLR en tout-ou-rien).

Les signaux en entrée doivent être compatibles avec les niveaux logiques haut et bas du contrôleur afin de déclencher un événement. Dans le cas de l'Arduino Uno, les signaux doivent obéir aux niveaux standards 0 V et 5 V. Les sources des signaux peuvent être classées en deux grandes catégories : active et passive. Une source active pourrait être un composant tels une porte logique ou un comparateur qui amène la broche d'entrée au niveau de tension 5 V ou 0 V. Pour certaines applications, cette solution est trop compliquée, et on cherchera des solutions plus simples, à savoir des solutions dites passives. Pour ces dernières, une résistance de tirage interne est activée sur la broche d'entrée du contrôleur. Cette résistance de valeur assez élevée est connectée entre l'alimentation et la broche d'entrée. Elle amènera le niveau logique haut à l'entrée (on dit qu'elle « tire » l'entrée à l'état haut, NDLR d'où son nom de « résistance de tirage »). Pour amener au niveau bas, on pourra utiliser un simple interrupteur placé entre l'entrée et la masse. Aucun circuit avec une alimentation externe n'est indispensable ici. Bien entendu, la résistance de tirage doit être désactivée lorsque l'entrée est reliée à une source active.

Note de la rédaction

Schéma de principe - activation de la résistance de tirage sur l'entrée D8

La résistance de tirage (entre 20 et 50 kΩ d'après la datasheet) est entourée en rouge sur la figure ci-dessous. Pour l'activer grâce au transistor, il faut PUD=LOW, WDx=LOW, WRx=HIGH :

Extrait datasheet Atmel ATmega 328P - General Digital I/O

Pour lire le niveau logique sur une entrée, deux choses doivent être faites. Premièrement, le registre de direction de données (data direction register) doit être défini de façon à configurer la broche concernée en entrée afin de la préparer en lecture (et, optionnellement, pour y activer la résistance de tirage interne). Une fois cela fait, on pourra lire l'état du bit de données sur le port concerné. Il convient de remarquer que sur certains microcontrôleurs, le même registre est employé pour à la fois lire en entrée et écrire en sortie (par exemple le registre PORTB de l'exercice sur l'écriture sur une sortie numérique). Sur les Atmel AVR (comme l'ATmega 328P de la Uno), deux adresses différentes sont utilisées : PORTx pour l'écriture, et PINx pour la lecture. L'erreur classique pour les nouveaux développeurs sur AVR est d'essayer de lire l'état depuis PORTB alors qu'ils souhaiteraient en fait le lire depuis PINB.

Dans notre premier exemple, on fera basculer l'état d'une broche d'entrée avec un simple interrupteur SPST (single-pole, single-throw – le plus basique des interrupteurs on-off) et en activant la résistance de tirage interne. L'état de l'entrée sera visualisé dans le Moniteur Série. Au départ, les bibliothèques de l'environnement Arduino seront utilisées mais ensuite on regardera en détail des formes plus génériques.

Soudez deux fils aux bornes d'un interrupteur, puis reliez-les au connecteur 8 et la masse de l'Arduino. Plus simplement, vous pouvez insérer un fil au connecteur 8 puis vous insérerez l'autre extrémité à une des masses au moment utile (NDLR pour simuler la fermeture de l'interrupteur).

Note de la rédaction

Montage d'un interrupteur avec résistance de tirage pull-up interne activée

Tapez le code suivant dans l'éditeur :

/* Read Digital Input V1. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série */

void setup()
{
pinMode(8, INPUT_PULLUP);
Serial.begin(9600);
}
void loop()
{
int i;
i = digitalRead(8);
Serial.println(i);
}

C'est aussi simple que cela. Dans un premier temps, le port série est initialisé et la fonction pinMode() initialise le registre de direction des données afin de configurer le connecteur 8 en entrée, avec la résistance de tirage activée (avec le mode INPUT seul, la résistance est désactivée). Dans la boucle, on lit l'état du connecteur 8 et on reporte son état pour affichage dans le Moniteur Série. Compilez et téléversez le code dans la carte Arduino. Ouvrez le Moniteur Série, vous devriez voir défiler une série de « 1 » marquant l'état logique haut. Maintenant, insérez l'autre extrémité du fil à une des masses disponibles sur le port de la carte. Vous devriez maintenant voir défiler des « 0 », marquant l'état logique bas. En connectant et déconnectant le fil (ou en basculant l'interrupteur), vous devriez voir les valeurs basculer.

/* Read Digital Input V2. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série, forme générique */

// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01

void setup()
{
DDRB &= (~BITMASK); // Configurer le connecteur en entrée
PORTB |= BITMASK; // Activer la résistance de tirage pull-up
Serial.begin(9600);
}

void loop()
{
int i;
i = PINB & BITMASK;
Serial.println(i);
}

Par rapport au code original de la première version, les deux appels pour configurer l'entrée semblent demander un peu plus de travail. Pourtant, le code machine généré est remarquablement plus compact (sous la v1.0.4 de l'EDI, la barre d'état donne 2726 octets pour la première version du programme contre 2340 seulement dans la seconde version).

À noter que dans le cas général, le calcul PINB & BITMASK retourne 0 si l'entrée est à la masse et une valeur non nulle si l'entrée est au 5 V (et pas forcément la valeur 1). Ce n'est pas un problème ici car en langage C toute valeur non nulle est traitée comme étant à Vrai. On ne testera donc pas si une variable est à Vrai en la comparant avec la valeur 1 mais plutôt en testant si elle « existe ».

if (v==1) // Ne faites pas cela pour tester si v est Vrai
if (v)    // Faites plutôt ceci
if (!v)   // et cela pour tester si v est Faux

Tapez la seconde version du programme Read Digital Input, compilez et téléversez le programme afin de le tester. Le défilement très rapide dans le Moniteur Série est quelque peu pénible. Actuellement, c'est comme si la Uno vous informait : « l'interrupteur est fermé », « l'interrupteur est toujours fermé », « … toujours fermé », encore et encore… Ce serait plus approprié pour l'utilisateur s'il était prévenu seulement lorsqu'il y a changement d'état. Pour cela, le programme doit mémoriser l'état en cours de la broche afin d'être comparé à son état un instant plus tard. Considérez l'évolution suivante du programme :

/* Read Digital Input V3. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série, forme générique, seuls les changements d'état sont notifiés */

// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01

int priorstate = BITMASK;

void setup()
{
DDRB &= (~BITMASK); // Configurer le connecteur en entrée
PORTB |= BITMASK; // Activer la résistance de tirage pull-up
Serial.begin(9600);
}

void loop()
{
int state;
state = PINB & BITMASK;
if( state != priorstate )
{
Serial.println(state);
priorstate = state;
}
}

On initialise la variable priorstate à BITMASK puisque c'est l'état initial de l'entrée. Le contenu de la variable state est affiché dans le Moniteur Série seulement s'il y a eu un changement d'état. Saisissez le code, téléversez-le et testez la nouvelle version. Remarquez que le déferlement de valeurs est maintenant remplacé par un affichage plus sobre et bien plus pratique. Si vous basculez plusieurs fois l'interrupteur dans les deux sens en regardant dans le Moniteur Série, vous devriez constater quelque chose d'étrange. Vous noterez probablement qu'il y a davantage de changements d'état que de basculements de l'interrupteur. Mais d'où viennent ces changements d'état aléatoires supplémentaires ?

Suppression des rebonds (debounce)

Le comportement réel des boutons et interrupteurs mécaniques n'est pas idéal. Quand un interrupteur ou un bouton commute, après la fermeture initiale du contact se produit une succession de « rebonds » mécaniques conduisant à une série de microruptures des contacts. Cette phase peut durer 10 millisecondes, voire plus selon la conception de l'interrupteur ou du bouton. Des exemples de rebonds d'interrupteur sont visualisés à l'oscilloscope sur les figures ci-dessous.

Rebonds d'un bouton ou interrupteur

Le problème est que le microcontrôleur peut lire l'état de la broche d'entrée en une fraction de milliseconde (NDLR et dans une boucle loop(), l'état de l'entrée peut être relevé plusieurs fois pendant la succession de rebonds), ainsi, chaque rebond peut être interprété comme une fermeture/ouverture très rapide de l'interrupteur. Dans certaines situations ce n'est pas gênant, par exemple quand il s'agit d'allumer ou éteindre une LED en accord avec l'état pressé ou relâché d'un bouton (La LED reste allumée tant que le bouton est pressé, et s'éteint lorsque le bouton est relâché). Dans ce cas, les rebonds du bouton se traduiront seulement par des clignotements de la LED imperceptibles par l’œil humain. Mais dans d'autres situations, on pourrait par exemple avoir à programmer et faire en sorte d'augmenter un niveau sonore ou un niveau de luminosité à chaque appui sur un bouton. Ici, un seul appui pourrait faire franchir plusieurs niveaux d'un coup à cause des multiples rebonds du bouton, et le système produirait un résultat incohérent en réponse à l'utilisateur.

Il existe deux systèmes « anti-rebond » simples. La première façon de faire est purement logicielle, tandis que la seconde est matérielle. Jetons un œil à la première solution.

La clé pour un anti-rebond logiciel consiste à étendre la durée d'observation de l'état du bouton. En d'autres termes, on veut observer l'état sur une durée suffisamment longue, le temps de laisser passer les rebonds. Par exemple, si on remarque que l'état n'a pas changé pendant une certaine durée, disons dix millisecondes, on peut assumer sans risque que l'on n'est pas en train de lire une entrée pendant la phase des rebonds. Considérez la variante suivante du code. Il ressemble beaucoup à la version V3, mais une nouvelle fonction get_state(), essentielle pour gérer les rebonds, a été ajoutée.

/* Read Digital Input V4. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série, forme générique, seuls les changements d'état sont notifiés, avec système anti-rebond logiciel */

// prototype de fonction
int get_state(void);

// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01

int priorstate = BITMASK;

void setup()
{
DDRB &= (~BITMASK); // Configurer le connecteur en entrée
PORTB |= BITMASK; // Activer la résistance de tirage pull-up
Serial.begin(9600);
}

void loop()
{
int state;
state = get_state();
if( state != priorstate )
{
Serial.println(state);
priorstate = state;
}
}

int get_state(void)
{
int priorstate, state, count=0, i=0;
priorstate = PINB & BITMASK;
while( i < 30 )
{
state = PINB & BITMASK;
if( state == priorstate )
count++;
else
{
count = 0;
priorstate = state;
}
if( count >= 10 )
break;
delay(1);
i++;
}
return( state );
}

L'idée derrière cette nouvelle fonction est de continuellement relever l'état de l'entrée et de continuer jusqu'à obtenir un état consistant pendant dix millisecondes. À chaque fois que l'état courant est identique à l'état précédent, on incrémente un compteur. Quand ce compteur atteint la valeur 10, on sait qu'une durée de dix millisecondes sans changement d'état du signal a été obtenue (grâce à la temporisation delay() d'une milliseconde lancée en fin de boucle). On sort alors de la boucle en retournant l'état de l'entrée. Si l'état de l'entrée bascule à cause d'un rebond, state n'est pas égal à priorstate et le compteur count est remis à zéro. Si l'interrupteur ou le bouton est défectueux, la phase de rebonds pourrait durer plus longtemps, et afin d'empêcher le code d'être bloqué à l'intérieur de la fonction, un maximum de 30 itérations (c'est-à-dire une période de 30 millisecondes) est institué grâce à la variable i.

Saisissez la version 4 et sauvegardez-la sous un nouveau nom, car on reviendra un peu plus tard sur la version 3. Compilez et téléversez le code et faites des tests en utilisant le Moniteur Série. Cette version devrait produire un résultat propre et cohérent en sortie dans le Moniteur Série, sans les rebonds parasites de la version 3 précédente. Il est aussi important de remarquer que le code machine produit est encore plus compact que le code original avec la bibliothèque Arduino malgré son avance en matière de fonctionnalités.

La seconde méthode pour vaincre les rebonds d'un interrupteur passe par des moyens matériels. En général, cela prend la forme d'un circuit RC connecté à l'interrupteur suivi d'une bascule de Schmitt (ou Trigger de Schmitt) (74HC14). Le circuit RC va filtrer les variations rapides du signal et l’hystérésis de la bascule de Schmitt s'occupera des basculements intempestifs autour du seuil. Le montage de la figure ci-dessous est un exemple classique (notez l'inverseur en sortie de bascule) :

Montage anti-rebond avec filtre RC suivi d'une bascule de Schmitt

Note de la rédaction
Allure des signaux en sortie du circuit RC, puis en sortie de bascule de Schmitt :

Les valeurs typiques pour ce genre de circuit devraient être autour de 4,7 kΩ pour R1, 470 kΩ pour R2 et 100 nF pour C. La constante de temps τ du circuit RC (τ=RC) vaut environ 50 millisecondes et les seuils de la bascule seront atteints en 200 millisecondes (moins de 5τ), ce qui permettra même de prendre en charge le pire des boutons mécaniques. Remarquez aussi que le signal d'entrée est actif et donc que la résistance de tirage (pull-up) n'est pas requise ici. Retournons à la version 3 (sans anti-rebond logiciel) et apportons une modification pour l’adapter au système anti-rebond matériel :

/* Read Digital Input V5. Lecture en boucle du connecteur D8 et envoi des valeurs au Moniteur Série, forme générique, seuls les changements d'état sont notifiés, avec système anti-rebond matériel */

// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01

int priorstate = 0;

void setup()
{
DDRB &= (~BITMASK); // Configurer le connecteur en entrée
// Ici, la résistance de tirage pull-up n'est pas activée
Serial.begin(9600);
}

void loop()
{
int state;
state = PINB & BITMASK;
if( state != priorstate )
{
Serial.println(state);
priorstate = state;
}
}

Retirez le bouton de la plaque d'essais. Réalisez le circuit anti-rebond de la figure précédente et reliez-le au connecteur 8 de l'Arduino. Compilez et téléversez le code. Ouvrez le Moniteur Série et constatez les résultats. Ils devraient être similaires à ceux obtenus par la version 4 avec l'anti-rebond logiciel. Remarquez également que le code occupe 300 octets de moins. On a effectivement confié la gestion anti-rebond à un circuit extérieur matériel.

Il y a des avantages à gérer les rebonds de façon logicielle plutôt que par le matériel. Premièrement, il y a de la souplesse à pouvoir modifier le code pour affiner les performances. Deuxièmement, il n'y a pas de coût additionnel du fait de composants ou de cartes additionnelles supplémentaires pour les accueillir. Il y a également quelques désavantages, comme la mémoire supplémentaire requise due à la fonction de gestion des rebonds (souvenez-vous que les microcontrôleurs, contrairement aux PC, ont une capacité mémoire très modeste). L'autre problème est le temps gaspillé à attendre que le signal du bouton s'installe. Dix ou vingt millisecondes peuvent paraître courts mais pour certaines applications cela peut être une éternité. Le choix d'un anti-rebond logiciel ou matériel dépend en fait d'autres facteurs propres au système.

Évidemment, suivre l'état de la broche dans le Moniteur Série est bien pratique pour déboguer ou apprendre comment fonctionne le composant mais ce n'est pas ce que l'on rencontre le plus souvent dans une application. Essayons d'évoluer vers quelque chose d'un peu plus utile.

Plutôt que d'envoyer l'état de la broche en sortie dans le Moniteur Série, on va refléter cet état en allumant ou en éteignant une LED externe reliée au connecteur 12 de l'Arduino, c'est-à-dire le bit 4 du port B. Le circuit anti-rebond continuera d'être utilisé avec le nouveau code suivant :

/* Read Digital Input V6. Lire en boucle le connecteur D8 et refléter l'état à une LED extérieure, forme générique, avec système anti-rebond matériel au niveau du connecteur d'entrée. */

// Connecteur D8 connecté au Port B, bit 0
#define BITMASK 0x01

// LED sur connecteur D12 de l'Arduino, c'est-à-dire Port B, bit 4
#define LEDMASK 0x10

int priorstate = 0;

void setup()
{
DDRB &= (~BITMASK); // Configurer le connecteur relié au 74HC14 en entrée
DDRB |= LEDMASK; // Configurer le connecteur relié à la LED en sortie
delay(1);
}

void loop()
{
int state;
state = PINB & BITMASK;
if( state != priorstate ) // Si changement d'état
{
priorstate = state;
if( state )
PORTB |= LEDMASK; // Allumer la LED
else
PORTB &= (~LEDMASK); // Éteindre la LED
}
}

Préparez un circuit de pilotage de LED (les versions avec un transistor NPN et PNP sont représentées sur la figure ci-dessous) et reliez-le au connecteur 12 de l'Arduino. Déterminez la valeur de Rc pour un courant qui traverse la LED de 10 milliampères environ, en supposant que sa tension de seuil est de 2 volts. Déterminez Rb pour un courant sur la base d'environ 0,5 milliampère.

LED Driver Circuits

Une fois le circuit monté, entrez le code, compilez-le et téléversez-le. L'état de la LED devrait être le reflet de la position de l'interrupteur. Si vous ne l'avez pas encore remarqué, jetez un œil dans la barre d'état de l'éditeur. Comme le Moniteur Série n'est plus utilisé ainsi que la bibliothèque associée, la taille du code est descendue à un petit 510 octets !

Défi
Il peut arriver dans certains cas que l'état de la LED ne reflète pas directement la position de l'interrupteur. Comme cela a été mentionné plus tôt, un bouton-poussoir (NDLR à appui momentané) peut être utilisé pour basculer l'état d'un dispositif. Par exemple, on pousse le bouton pour allumer le dispositif et on réappuie pour l'éteindre. Proposez une modification du code de façon à ce que le bouton fasse basculer l'état de la LED plutôt qu'une LED qui indique la position enfoncée ou relâchée du bouton. Autrement dit, la LED devra changer d'état seulement sur un front montant du signal d'entrée délivré par le bouton (ou seulement sur un front descendant). Vous pourrez commencer par dessiner le schéma électrique avec Multisim ou un logiciel de conception de schéma électrique équivalent.

XI - Les entrées analogiques

Dans cet exercice, nous allons examiner la méthode d’acquisition des signaux analogiques tels que ceux délivrés par divers capteurs à mesure continue. On utilise ces capteurs délivrant un signal de manière continue pour mesurer des températures, forces, accélérations, etc. Cette capacité doit être combinée aux techniques d’entrées/sorties numériques déjà vues afin de bâtir un système indiquant la force appliquée par une série de LED. Plus la force appliquée est grande, plus il y aura de LED allumées.

Les microcontrôleurs utilisent des Convertisseurs Analogique-Numérique (CAN ou ADC pour Analog to Digital Converter) pour mesurer des tensions d’entrée variables en continu. L’ATmega 328P possède un CAN 10 bits précédé d’un multiplexeur à six canaux. Ces six lignes sont reliées au port des entrées analogiques de l’Arduino, connecteurs A0 à A5. La tension de référence par défaut du convertisseur est le 5 V de la carte. Elle représente la tension maximale qui peut être numérisée/mesurée. La tension minimale est la masse ou zéro volt (si des signaux négatifs sont nécessaires, leur niveau devra être décalé d’une façon ou d’une autre). Un convertisseur 10 bits distingue 2^10 soit 1024 niveaux discrets. Cela signifie que chaque pas représente un peu moins de cinq millivolts (NDLR quantum=5/1024≈4,9 mV ; la résolution du convertisseur).

Pour utiliser le CAN, une tension de référence doit être initialement configurée. Après cela, à chaque fois qu’une valeur/mesure est requise, il suffira d’appeler la fonction analogRead(). Le seul argument de la fonction est le numéro du canal d’entrée désiré (de zéro à cinq pour la Uno). La fonction retourne la valeur numérisée issue de la conversion sous forme d’un entier entre 0 et 1023. Bien qu’il soit possible d’accéder directement aux registres du CAN, la bibliothèque Arduino est assez efficace et nous ne gagnerions au mieux que quelques octets à ne pas l’utiliser.

Nous allons commencer par quelque chose de simple pour montrer comment lire une broche d’entrée analogique. Notez que, à la différence des entrées numériques, l’initialisation par pinMode() ou par un registre de direction (DDRx) n’est pas nécessaire lors de l’utilisation de la fonction analogRead().

Connectez les extrémités d’un potentiomètre de 10 kΩ aux +5 V et masse GND de la carte. Connectez le curseur au connecteur d’entrée analogique 0 (A0).

Note de la rédaction

Entrez le code ci-dessous, compilez-le et transférez-le dans la carte.

/* Lecture analogique V1. Lit la tension issue d’un potentiomètre et l’envoie sur le moniteur série. Valeurs de 0 à 1023 */

#define ANALOG_IN_PIN 0

int prior = 0;

void setup()
{
Serial.begin(9600);
analogReference( DEFAULT );
}

void loop()
{
int a;

a = analogRead( ANALOG_IN_PIN );

if( a != prior )
{
Serial.println(a);
prior = a;
}
}

Le code est explicite. Sa particularité intéressante est que, plutôt que d'afficher directement la valeur issue de la conversion, on la compare à la valeur précédente et, seulement si elle est différente, le résultat est affiché. Cela économise du temps de traitement et accélère les choses.

Ouvrez le moniteur série. Tournez le curseur du potentiomètre dans le sens horaire et anti-horaire. Les valeurs doivent changer dans le moniteur série lorsqu’on tourne le curseur. Elles doivent évoluer entre 0 et 1023 (il peut manquer une valeur à cause du bruit). Littéralement, on peut déterminer la position réelle du curseur en multipliant la valeur lue par le quantum (5 V/1024, soit environ 4,883 millivolts), et réaliser ainsi un voltmètre très rudimentaire.

XI-A - Capteur de force

Remplaçons le potentiomètre par un capteur analogique. Pour cet exercice, nous utiliserons une jauge de déformation (souvent appelée abusivement « jauge de contrainte », FSR en anglais pour Force Sensing Resistor). Une jauge de déformation est un composant à deux bornes, prenant le plus souvent la forme d’un film plat, dont la résistance dépend de la force qui lui est appliquée.

Note de la rédaction

Force Sensitive Resistor FSR400 - image Sparkfun CC BY 2.0

Si aucune force n’est appliquée, la résistance est très élevée, généralement largement supérieure au mégaohm. Avec la force maximum (telle qu’une forte pression des doigts sur la partie sensible), la résistance peut chuter à une valeur aussi basse que 100 Ω. Retirez le potentiomètre et reliez une borne de la jauge de déformation au +5 V et l’autre à une résistance de 10 kΩ. Le point commun à la jauge et la résistance devra être relié à l’entrée A0, tandis que la patte libre de la résistance ira à la masse. Ce montage n’est rien d’autre qu’un diviseur de tension dont l’une des branches est la jauge de déformation.

Note de la rédaction

Lorsqu’on applique une force, la résistance diminue, donc la tension au point commun augmente. Ne modifiez pas le code. Notez simplement comment les valeurs changent dans le moniteur série lorsqu’une force est appliquée sur la jauge : plus la pression est grande, plus la valeur est élevée. Pour finir, échangez les positions de la résistance et de la jauge. À une pression correspond maintenant une baisse des valeurs dans le moniteur série.

XI-B - Affichage à LED

Maintenant que le capteur et la fonction de lecture d'entrée analogique ont été testés, combinons cela à un affichage à LED. Plutôt que d'afficher les valeurs sur le moniteur série, nous allons allumer une série de LED. Cela nécessitera de programmer des sorties sur le port numérique, comme vu dans un chapitre précédent chapitre précédent précédent. Pour rester simple et juste « vérifier le concept », nous allumerons jusqu’à quatre LED. Chacune nécessitera un pilotage par un transistor (ou un ensemble de Darlington tel qu’un ULN2003 qui contient sept amplificateurs dans un seul composant). Nous utiliserons les quatre bits de poids faible du port B pour les sorties, connecteurs 8 à 11 de l’Arduino. Voici le code ci-dessous :

/* Lecture analogique V2. Utilise la jauge de déformation (reliée a la masse) et la 10k (connectée au 5V), sur la broche A0. Allumage de 4 LED sur le port B bits 0 a 3 pour indiquer la force. Allumage à l’état haut. *//

#define ANALOG_IN_PIN 0

// Bits à utiliser sur le port B pour les LED
#define LEDMASK0      0x01
#define LEDMASK1      0x02
#define LEDMASK2      0x04
#define LEDMASK3      0x08
#define LEDMASK       (LEDMASK0 | LEDMASK1 | LEDMASK2 | LEDMASK3)

void  lightLEDsBar( int a );

int prior=0;

void setup()
{
Serial.begin(9600);
analogReference( DEFAULT );
// Configure les bits de pilotage des LED en sorties
DDRB |= LEDMASK;
}

void loop()
{
int a;

a = analogRead( ANALOG_IN_PIN );
if( a != prior )
{
Serial.println(a);
lightLEDsBar( a );
prior = a;
}
}

void  lightLEDsBar( int a )
{

if( a > 820 )
PORTB |= LEDMASK; // Allume tout
else
{
if( a > 615 )
{
PORTB &= (~LEDMASK3); // Éteint le haut
PORTB |= (LEDMASK2 | LEDMASK1 | LEDMASK0); // Allume tout en dessous
}
else
{
if( a > 410 )
{
PORTB &= (~(LEDMASK3 | LEDMASK2));
PORTB |= (LEDMASK1 | LEDMASK0);
}
else
{
if( a > 205 )
{
PORTB &= (~(LEDMASK3 | LEDMASK2 | LEDMASK1));
PORTB |= LEDMASK0;
}
else
PORTB &= (~LEDMASK);
}
}
}
}

Il est ici primordial de configurer les bits 0 à 3 de DDRB en sorties, elles piloteront nos LED. Des masques de bits sont définis pour chaque sortie afin de faciliter la maintenance du code. Une nouvelle fonction a été créée pour gérer l’allumage : lightLEDsBar(). Cette fonction créera un bargraphe fondé sur la valeur qu’on lui aura passée. Comme il y a quatre LED, nous pouvons diviser les 1024 points de mesure en cinq segments. Si la valeur est en dessous de 20 % (env. 205), aucune LED n’est allumée. De 20 % à 40 %, seule la première LED sera allumée. Avec l’augmentation des valeurs, de plus en plus de LED seront allumées, jusqu’à 80 % et plus, où toutes les LED seront allumées. La fonction est un peu plus évoluée qu’une cascade de if/else. Pour une zone donnée, les LED du haut sont d’abord éteintes, puis les LED restantes en bas sont allumées. Remarquez que la méthode ci-dessus est plus efficace que l’utilisation de la fonction digitalWrite(), car nous pouvons modifier l’état de plusieurs bits en une fois, en accédant directement au port B. Au contraire, digitalWrite() nécessiterait un appel pour chaque bit/LED. Le code du moniteur série a été conservé, vous pourrez ainsi vérifier les résultats et vous assurer que tout fonctionne comme prévu.

Tout en laissant la jauge de déformation en place, câblez les quatre circuits de pilotage des LED sur les broches 8 à 11. Un courant de 10 mA dans les LED sera suffisant. Entrez le code ci-dessus, compilez et transférez-le. Pour tester, ouvrez simplement le moniteur série et appuyez sur la partie sensible de la jauge. Plus la force appliquée augmente, plus les valeurs sur le moniteur série diminuent et moins les LED s’allument. Vous pouvez envisager cela comme un « absence-de-force-mètre ». Bien que d’une logique assez déroutante, cela peut être très utile. Il arrive qu’on souhaite garder les choses « sous pression », une force appliquée trop faible devenant un signal d’alarme (autrement dit, plus de LED allumées signifient un plus gros problème).

Parfois, les LED sont disposées pour imiter un indicateur analogique. Dans un pareil cas, on peut vouloir n’allumer qu’une seule LED plutôt que toutes celles qui sont en dessous. Nous appellerons cela le « mode point », à l’opposé du « mode barre ». La version trois du programme présente le mode point sous une forme intéressante. La partie moniteur série a été retirée, vous pourrez donc noter une diminution de la taille du programme.

/* Lecture analogique V3. Utilise la jauge de déformation (reliée a la masse) et la 10k (au 5V), sur la broche A0. Allumage d’une seule LED sur le port B bits 0 a 3 pour indiquer la force. Visualisation en mode point. Allumage a l’état haut. */

#define ANALOG_IN_PIN 0
// Bits à utiliser sur le port B pour les LED
#define LEDMASK0      0x01
#define LEDMASK1      0x02
#define LEDMASK2      0x04
#define LEDMASK3      0x08
#define LEDMASK       (LEDMASK0 | LEDMASK1 | LEDMASK2 | LEDMASK3)

void  lightLEDsDot( int a );

int prior=0;
void setup()
{
analogReference( DEFAULT );
DDRB |= LEDMASK;  // Configure les bits de pilotage des LED en sorties
}

void loop()
{
int a;

a = analogRead( ANALOG_IN_PIN );
if( a != prior )
{
lightLEDsDot( a );
prior = a;
}
}

int thresh[] = {820, 615, 410, 205};

void  lightLEDsDot( int a )
{
int *p;
p = thresh;

if( a > *p++ )
{
PORTB &= (~(LEDMASK2 | LEDMASK1 | LEDMASK0)); // Éteint les trois du bas
PORTB |= (LEDMASK3);                  // allume la plus haute
}
else
{
if( a > *p++ )
{
PORTB &= (~(LEDMASK3 | LEDMASK1 | LEDMASK0));
PORTB |= (LEDMASK2);
}
else
{
if( a > *p++ )
{
PORTB &= (~(LEDMASK3 | LEDMASK2 | LEDMASK0));
PORTB |= LEDMASK1;
}
else
{
if( a > *p )
{
PORTB &= (~(LEDMASK3 | LEDMASK2 | LEDMASK1));
PORTB |= LEDMASK0;
}
else
PORTB &= (~LEDMASK);
}
}
}
}

La variante en mode point est similaire à celle avec un bargraphe, sauf que toutes les LED sont éteintes, sauf la plus haute. Vous pourriez vous demander pourquoi chercher la complication à vouloir éteindre les LED individuellement, plutôt que les éteindre toutes (faire un ET logique avec 0xF0), puis allumer celle que l’on désire. La raison est que cela peut provoquer un basculement fugitif (ON-OFF-ON) de la LED en question. Dans cette application, vous ne verrez probablement jamais la différence, mais ce basculement peut poser problème dans certaines applications. Quoiqu’il en soit, la quantité de code machine créé est à peu près la même dans les deux cas.

La modification la plus intéressante de la fonction est qu’on utilise un tableau de valeurs pour les seuils plutôt que des valeurs « en dur ». Cela rend le programme beaucoup plus flexible s’il fallait modifier les seuils ultérieurement. Pour utiliser un jeu de seuils différent, il suffit de faire pointer p sur la première valeur du tableau de seuils créé au début de la fonction. Notez également en particulier l’utilisation de la post-incrémentation du pointeur p. C’est généralement plus efficace que la classique indexation de tableau.

Défi

Modifiez le programme afin d’inverser la logique d’allumage, c.à.d. faites en sorte que plus forte est la pression, plus le nombre de LED allumées est grand et inversez le sens en mode point. Ne modifiez pas le câblage de la jauge. Faites-le uniquement en modifiant le programme. Ensuite, ajoutez un interrupteur sur la broche 7 de l’Arduino (port D.7). La position de cet interrupteur permettra à l’utilisateur de choisir un affichage en mode barre ou en mode point. Un traitement anti-rebond de cet inverseur ne sera pas nécessaire. En bonus, ajoutez un second interrupteur sur la broche 6 de l’Arduino (Port D.6), qui permettra de choisir entre l’actuelle échelle linéaire et une échelle logarithmique de 6 dB par LED (soit une réduction du niveau de moitié, la LED supérieure s’allumant au-dessus de 512). Proposez votre code et un schéma adéquat avec les valeurs des composants.

XII - Mesure de temps de réaction

Dans cet exercice, nous utiliserons les entrées et sorties numériques, les fonctions de gestion du temps et la génération de nombres aléatoires pour créer un jeu simple. Le but sera de tester le temps de réaction du joueur (techniquement, nous mesurons le temps de réponse qui est la somme du temps de réaction plus le temps du mouvement du joueur). Au lieu de simplement regarder le code, cet exercice nous permettra également de prendre du recul, de regarder la conception du système et les problèmes d’une interface utilisateur simple.

Le jeu donnera au joueur cinq essais pour atteindre son meilleur temps de réaction. Avant chaque tentative, un premier voyant rouge sera activé pour signaler au joueur de se préparer (c.-à-d. « prêt »). Ensuite, un deuxième voyant vert sera activé pour dire au joueur d’y aller (c.-à-d. « partez »). Le temps entre l’allumage des voyants rouge et vert sera toujours compris entre une et dix secondes mais sera aléatoire entre les tentatives. Une fois que le voyant vert s'allume, le joueur doit frapper sur un interrupteur. Le temps qui s’est écoulé entre le feu vert allumé et l'activation de l’interrupteur est le temps de réaction du joueur. Le meilleur temps sera affiché à la fin des cinq essais. Le jeu doit également se prémunir contre la tricherie, c'est-à-dire qu’il doit interdire l’anticipation du feu vert.

Avant de considérer les aspects matériels et logiciels, nous devons avoir une idée plus précise de ce que nous mesurons. D’une manière générale, quel est le temps de réponse du corps humain ? Il s'avère que cela est en partie fonction du stimulus (les signaux audibles sont traités plus rapidement par le cerveau humain que les signaux visuels). Les temps de réponse typiques sont d'environ 160 millisecondes pour un stimulus auditif et 190 millisecondes pour un stimulus visuel, bien que les athlètes professionnels tels les sprinteurs olympiques puissent parfois atteindre une valeur de seulement 2/3 de celle-ci. De toute évidence, le matériel et le logiciel doivent être très rapides si nous voulons obtenir un niveau de précision acceptable.

L'interface utilisateur se compose de quatre éléments : un voyant rouge, un voyant vert, l’interrupteur du joueur et un moyen pour afficher du texte. Pour simplifier et faciliter le prototypage, nous utiliserons le Moniteur Série (Serial Monitor) pour l’affichage du texte. Si cela devait devenir un produit fini, le moniteur pourrait être remplacé par un écran LCD dédié. En ce qui concerne les lumières, les LED semblent être un choix évident peut-être en raison de leur familiarité et de leur taille adaptée à ce jeu individuel (si plusieurs personnes faisaient un essai chacun, nous aurions besoin d'une source de lumière beaucoup plus grande ou plus lumineuse qu'une LED ordinaire). Un autre élément à considérer est la rapidité d’allumage des voyants. Les LED s'allument en une fraction de milliseconde tandis que les lampes à incandescence peuvent ne pas atteindre une luminosité totale pendant des dizaines ou même des centaines de millisecondes. Comme nous nous attendons à des temps de réponse aux alentours de 200 millisecondes, une lampe à incandescence peut ne pas être assez rapide. C'est-à-dire que le délai nécessaire pour atteindre une luminosité totale pourrait fausser les résultats, ralentissant le temps de réponse apparent (mesuré) du joueur. Donc, il semble que de simples LED soient un bon choix ici.

L'élément final est l’interrupteur du joueur. Il pourrait frapper assez fort, car il voudra déplacer sa main aussi vite que possible pour minimiser le temps de mouvement. Par conséquent, l'interrupteur doit être assez robuste. Il devrait également avoir une taille décente pour le joueur, c'est-à-dire assez large pour que le joueur ne soit pas obligé de faire un mouvement très précis. Alors qu'un simple bouton poussoir momentané pourrait sembler un choix décent, un capteur de force résistif pourrait s'avérer meilleur. Il ne contient pas de pièces mobiles et offre une grande cible (NDLR Voir le capteur de force du chapitre précédent). Il répond également presque instantanément et ne souffre pas de rebond. L'interconnexion est également simple : nous pouvons simplement le connecter entre une broche d'entrée et la masse, en activant la résistance de pull-up interne de l'ATmega328P. Sans pression sur le capteur sa résistance est très élevée et lorsqu'il est sous pression, la valeur baisse à environ 100 Ohms. Comme il sera en série avec le pull-up connecté à l'alimentation, sans aucune pression dessus la règle du diviseur de tension indique que la tension à ses bornes sera vue comme un état logique haut. Lorsqu’une pression sera exercée sur le capteur, la tension à ses bornes sera vue comme un état logique bas. Le seul inconvénient de celui-ci est l’absence de ressentis physiques lorsqu’on appuie dessus. De nombreux interrupteurs font un « clic » lorsqu'ils sont enfoncés et cela fournit une information auditive au joueur confirmant un appui franc. Nous pouvons apporter une information aux joueurs en allumant une LED, voire les deux, au moment où l’appui est détecté.

Nous avons maintenant une idée des potentiels besoins matériels. Qu'en est-il du logiciel ? De toute évidence, les techniques de gestion des entrées et sorties numériques étudiées précédemment peuvent être utilisées pour lire et contrôler le capteur de force et les LED. La fonction delay() peut être utilisée pour gérer la temporisation entre l'allumage des LED pour lancer le départ. Deux nouveaux éléments sont cependant nécessaires : une façon de générer des valeurs aléatoires (dans notre cas, entre 1000 millisecondes et 10 000 millisecondes) et une méthode de mesure du temps entre deux événements. Considérons d'abord le nombre aléatoire.

D’une manière générale, les circuits numériques ne sont pas adaptés pour générer de véritables valeurs aléatoires, car ils sont déterministes. Concrètement, nous essayons de « fabriquer » tout le potentiel du hasard, car nous avons besoin d'une opération hautement prévisible. Les valeurs véritablement aléatoires ne sont en aucun cas corrélées entre elles. Autrement dit, il n'y a rien sur une suite de valeurs aléatoires qui vous permettra de déterminer la valeur suivante. Il s'avère cependant que nous pouvons créer des séquences pseudo-aléatoires sans trop d'effort. Une séquence pseudo-aléatoire semble être aléatoire, mais si vous exécutez la séquence assez longtemps, elle se répétera. Celles-ci peuvent ne pas être suffisamment aléatoires pour des recherches scientifiques, mais elles sont suffisantes pour quelque chose comme notre jeu. Le processus de tirage de nombres pseudo-aléatoires sort du cadre de cet exercice, mais on peut dire qu’il implique un traitement assez rudimentaire, tel qu'un décalage de bits, sur une valeur de départ appelée une graine (seed). Le résultat de ce calcul est ensuite utilisé comme donnée d’entrée pour calculer la valeur suivante et ainsi de suite.

La bibliothèque Arduino comprend une fonction random(), qui renverra une valeur entière pseudo-aléatoire de type entier long. La fonction peut également être appelée avec des limites supérieure et inférieure pour contraindre les valeurs renvoyées comme random(30, 200). Cette suite pseudo-aléatoire est initialisée par la fonction randomSeed() qui prend un entier long comme argument. Un cas de figure parfois utile est que si l’initialisation de départ est la même pour chaque exécution du programme, la suite pseudo-aléatoire résultante se répétera exactement de la même manière. Cela permet le débogage d'une fonctionnalité supposément aléatoire. Mais cela soulève un problème : comment pouvons-nous obtenir une graine aléatoire pour initialiser une suite pseudo-aléatoire ? Bien qu'il soit possible de demander à l'utilisateur une valeur de départ, cela ouvre la porte à une tricherie éventuelle. Une meilleure façon est d'utiliser une tension issue d’un bruit. Par sa nature même, le bruit est aléatoire. Si on regarde une broche d'entrée analogique qui n'est connectée à rien, on verra une faible tension de bruit. Nous pouvons numériser cette valeur vraiment aléatoire avec la fonction analogRead() et l'utiliser pour la graine. Cette tension ne changera pas sur une très large plage, mais elle changera en quantité suffisante pour que notre séquence pseudo-aléatoire semble réellement aléatoire.

Le programme suivant montre comment fonctionne le système de nombres aléatoires. Entrez le code, compilez-le et transférez-le, puis exécutez-le. Tout d'abord, il affichera la tension de bruit, puis des valeurs aléatoires entre 50 et 1000. Après avoir vu un nombre suffisant de valeurs, appuyez sur le bouton reset de la carte pour redémarrer le programme. Faites-le plusieurs fois. Ces redémarrages devraient produire différentes séquences. De temps en temps, vous pourriez avoir la même tension de bruit. Si vous voyez cela, notez que la séquence de nombres pseudo-aléatoires sera répétée exactement de la même manière.

void setup()
{
long i;
Serial.begin(9600);

i=analogRead(0); // ou tout autre entrée analogique libre
randomSeed(i);
Serial.print("Noise voltage: ");
Serial.println(i);
}
void loop()
{
long a;
a = random(50,1000);
Serial.println(a);
delay(2000);
}

Cette technique devrait fonctionner parfaitement pour notre application. Tout ce que nous devons faire c’est borner la fonction aléatoire entre 1000 et 10 000 afin de pouvoir utiliser directement les valeurs en tant qu’argument, en millisecondes, pour la fonction delay() qui servira pour l'allumage aléatoire de la LED verte « partez ».

La deuxième partie dont nous avons besoin consiste à mesurer le délai entre l'éclairage de la LED « partez » et l’appui sur le capteur de force résistif. Si vous le faisiez à partir de zéro, vous devriez initialiser l'un des timers de l'ATmega328P et vérifier la valeur qu’il contient au moment voulu. Les événements de synchronisation sont un besoin tellement commun dans le domaine de l’embarqué que, lors de la routine d'initialisation Arduino, un timer est créé pour vous. À l'instant où vous allumez ou réinitialisez la carte, ce timer interne commence à incrémenter un registre qui a été initialisé à zéro. Le registre est incrémenté à chaque milliseconde. Ce timer continue de fonctionner indépendamment du code principal qui est en cours d'exécution (à moins que le code principal ne reprogramme le timer, bien sûr). Par conséquent, la valeur du registre indique combien de millisecondes se sont écoulées depuis la mise sous tension ou la réinitialisation de la carte. Le contenu de ce registre peut être lu via la fonction millis(). C'est-à-dire que millis() renvoie le nombre de millisecondes qui se sont écoulées depuis la dernière réinitialisation ou mise sous tension. Après environ 50 jours, ce registre débordera et repassera à 0 puis l’incrémentation reprendra à nouveau. Comme il est très peu probable qu’un individu joue à ce jeu pendant aussi longtemps, ce débordement ne devrait pas être problématique. L'extrait de code suivant fera ce dont nous avons besoin :

starttime = millis();
// Allumer la LED  "Partez !"
/* Scruter l'état de l'entrée où est connecté le capteur de force, puis se mettre
en attente jusqu'à la réponse du joueur qui appuie sur le capteur */
finishtime = millis();
responsetime = finishtime - starttime;

Donc, nous pouvons maintenant trouver un pseudocode pour la boucle du jeu :

1. Initialiser le meilleur temps de réponse en tant que valeur la plus élevée (long int)
2. Demander à l'utilisateur de lancer le jeu en appuyant sur le capteur de force
3. Attendre la réponse du joueur par l’appui sur le capteur de force
4. Démarrer une boucle de cinq essais, pour chaque essai :
a. Générer un nombre aléatoire entre 1000 et 10 000
b. Attendre une seconde ou deux pour permettre à l'utilisateur de se préparer
c. Allumer une LED rouge une ou plusieurs fois rapidement pour indiquer le début de cet essai
d. Attendre pendant un délai aléatoire défini en 4.a
e. Enregistrer le temps millis() de départ
f. Faire un Flash sur la LED verte « partez ! »
g. Attendre la réponse du joueur en scrutant le bit FSR (image du capteur de force)
h. Enregistrer le temps millis() de fin et calculer le temps de réponse (4.h - 4.e)
j. Faire un Flash à la fois sur la LED rouge et la LED verte pour un retour visuel
k. Si le temps de réponse est inférieur à 100 ms, déclarer une tricherie et réinitialiser le temps de réponse à une valeur de pénalité élevée
l. Afficher le temps de réponse
m. Comparer le meilleur temps de réponse jusqu'à présent, ne garder que la valeur la plus petite (c'est-à-dire la meilleure) (boucle complète)
5. Afficher la meilleure valeur
6. Attendre un moment avant de commencer la prochaine partie

Certaines de ces étapes pourraient nécessiter des éclaircissements ou des ajustements supplémentaires. À l'étape 4.k, une « tricherie » est détectée si le temps de réponse est inférieur à 100 millisecondes. Ceci est probablement exagéré compte tenu des statistiques vues précédemment et pourrait être ajusté à une valeur plus élevée. La valeur de pénalité ici pourrait être identique à la valeur d'initialisation, peut-être 10 000 millisecondes ou plus. Après tout, si cela nécessite plus de 10 secondes pour appuyer sur le capteur de force, on peut supposer que le joueur est en état d’ébriété, étourdi ou en train d’écrire un SMS.

Un autre élément important est le code pour les étapes 3 et 4.g. Concrètement, il doit « attendre » que le joueur frappe sur le capteur de force résistif. Si celui-ci est câblé sur une broche d'entrée utilisant une résistance de pull-up interne, en appuyant dessus, l'entrée va passer d’un état haut à un état bas. (Rappelez-vous, en appuyant sur le capteur, sa résistance devient faible et comme le capteur est en série avec la résistance de pull-up, la règle du diviseur de tension montre que la tension aux bornes du capteur diminue et atteint une valeur égale à un état logique bas). Par conséquent, le code peut « attendre » sur cette broche et en sortir quand la broche est à l’état bas. Nous pouvons câbler le capteur de force sur la broche 8 de l'Arduino qui est le bit 0 du port B. En outre, nous pourrions éviter de coder ceci en dur en utilisant un #define :

#define FSRMASK 0x01

Donc, le code de test ressemble à ceci :

while( PINB & FSRMASK ); // PINB, et non PORTB !!

Rappelez-vous, pour une entrée, nous regardons PINx et pour une sortie, nous utilisons PORTx. Cette boucle continue à fonctionner aussi longtemps que le bit d'entrée FSR est à l'état logique haut. Dès que le joueur presse le capteur avec une force raisonnable, la tension sur la broche diminue et la boucle se termine.

Nous pouvons rationaliser un peu plus notre code sur les étapes 4.c, 4.f et 4.h qui consiste à faire clignoter une LED. Nous pouvons connecter les LED verte et rouge sur les broches 6 et 7 de l'Arduino (ports D.6 et D.7) et utiliser un #define avec une fonction qui allume ou éteint une LED rapidement :

// LED verte, port D.6
#define GREENLED 0x40
// LED rouge, port D.7
#define REDLED 0x80
// Délai on/off en millisecondes
#define LEDTIME 30
void FlashLED( int mask )
{
PORTD |= mask;    // Allumer la ou les LED
delay(LEDTIME);
PORTD &= (~mask); // Éteindre la ou les LED
delay(LEDTIME);
}

Cette fonction serait appelée pour un seul flash rapide :

FlashLED(GREENLED);

Elle pourrait également être placée dans une boucle pour effectuer une série de flashs rapides et vous pourriez modifier un bit ou les valeurs du masque pour faire flasher les deux LED simultanément.

Le dernier point concerne l'étape 6, « Attendre un moment avant de commencer une nouvelle partie ». Pourquoi inclure cela ? La raison n'est pas évidente. Rappelez-vous que le microcontrôleur traitera tout le programme en un temps très court, peut-être que quelques millisecondes se passeront entre le moment où le joueur frappe le capteur, les valeurs finales sont affichées et jusqu’à ce que la fonction loop() se termine, pour être rappelée à nouveau par la fonction invisible main(). Lors de la réintroduction, dans la fonction loop(), le programme indique au joueur d'appuyer sur le capteur de force pour démarrer la prochaine partie, puis il attend dans une boucle que bit FSR passe à l’état bas. Le problème est que l’appui sur le capteur va démarrer une nouvelle partie mais le joueur n’aura peut-être pas le temps de retirer son doigt que l’essai sera enregistré puisque le bit FSR sera vu comme un état bas, tellement rapidement, que la boucle d’attente se terminera tout aussi vite ! Pour éviter cela, nous pouvons entrer un petit délai, car en principe personne ne continuera à appuyer sur le capteur de force une fois le temps de réponse enregistré, ou nous pourrions utiliser un autre capteur indépendant pour débuter la partie. Un seul capteur semble plus adapté.

Selon les extraits de code précédents, l’initialisation pourrait ressembler à ceci :

// LED verte sur D.6, rouge sur D.7, FSR sur B.0, délai flash en millisecondes
#define GREENLED 0x40
#define REDLED 0x80
#define FSRMASK 0x01
#define LEDTIME 30
void setup()
{
Serial.begin(9600);

// Configuration des connecteurs 6 et 7 en sortie pour les LED
DDRD |= (GREENLED | REDLED);
// Configuration connecteur 8 en entrée pour le capteur de force FSR
DDRB &= (~FSRMASK); // Initialiser le connecteur en entrée
PORTB |= FSRMASK;   // Activer la résistance de pull-up interne
// Initialiser la graine d'après le signal bruité de l'entrée analogique A0
randomSeed(analogRead( 0 ));
}

Défi

Sur la base du pseudocode présenté précédemment, créez la fonction loop() pour le jeu et testez-la plusieurs fois. Entrez votre code complété avec un schéma approprié montrant toutes les interconnexions au microcontrôleur. Les LED doivent être alimentées par un circuit de commande approprié pour minimiser le courant de sortie du contrôleur. Une petite modification pour un bonus supplémentaire serait que le programme détermine et affiche le temps de réponse total pour chaque partie de cinq essais ainsi que le temps de réponse moyen. Il devrait également indiquer le nombre de « tricheries » s'il y a lieu. Notez que les fraudes entraîneront une augmentation considérable du temps de réponse total et de la moyenne.

XIII - Arduino Reaction Timer Redux

En cours de traduction

XIV - Arduino Analog Output via PWM

En cours de traduction

XV - Arduino Event Counter

En cours de traduction

XVI - Arduino Arbitrary Waveform Generator

En cours de traduction

XVII - Notes de la Rédaction Developpez.com

Ce livre est la traduction du livre Laboratory Manual for Embedded Controllers Using C and Arduino (Version 2.1.0 du 16 janvier 2017) par le professeur James M. Fiore du Mohawk Valley Community College (Utica, État de New York).

Nous remercions les membres de la Rédaction de Developpez.com pour le travail de traduction et de relecture qu'ils ont effectué, en particulier : f-leb, Auteur, Vincent PETIT, Delias, Aegnor1975, Malick Seck, Claude Leloup, Maxy35 et genthial.