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 | naute | 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, naute, kicadder

- Parution : 8 mai 2017

- Mise à jour : 7 mai 2019

- 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 standards

- 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. Mesure de temps de réaction, le retour

- XIV. Sortie « analogique » via des signaux PWM

- XV. Compteur d’événements…

- XV-A. …ou la revanche du fils du retour de la mesure du temps de réaction

- XV-B. Nous interrompons le déroulement normal du programme…

- XV-C. Le choix du commutateur

- XVI. Générateur de formes d’onde arbitraires

- XVI-A. Un générateur de signal sinusoïdal simple

- XVI-B. Changement de forme d’onde

- XVI-C. Changement de fréquence

- XVI-D. Utilisation d’un convertisseur numérique-analogique intégré

- XVI-E. Conclusion

- XVI-F. Annexe

- 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 le choix entre plusieurs 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 pseudo-code. Le pseudo-code 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'ajout 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 standards

Dans cet exercice, on étudiera plus en détail les fonctions d'entrées-sorties standards, 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 pseudo-code ou un diagramme. Voici un pseudo-code 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 pseudo-code 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 de réduire 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 entreprise. 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 pseudo-code :

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 pseudo-code 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 du 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, le code supplémentaire suivant :

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 ne 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 à variation 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. 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. Quoi qu’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 et du 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 ceux-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 caractère familier et de leur taille adaptée à ce jeu individuel (si plusieurs personnes faisaient un essai chacune, 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 impré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 fonctionmillis(). 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 pseudo-code 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 les 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 consistent à 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 le moment où 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 le 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 pseudo-code 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 - Mesure de temps de réaction, le retour

Parfois, il est bien de revisiter un projet passé pour y étudier une approche différente. Ici, on va retravailler l’exercice sur la mesure du temps de réaction. Même si celui-ci il fût nécessaire comme étape d’apprentissage, la limitation évidente de ce programme en lui-même est le fait que la carte de développement nécessite d’être raccordée à un ordinateur exécutant l’EDI Arduino, car il repose sur le Moniteur Série pour l’affichage des valeurs numériques. À la place, on propose de réaliser un chronomètre autonome, et qui fonctionnera grâce à une source d’alimentation extérieure. Il y a plusieurs méthodes pour afficher une valeur numérique, la plus directe est probablement celle utilisant un afficheur 7 segments à LED. Ces composants peuvent afficher les 10 chiffres arabes et quelques lettres latines (sans accent, et non sans quelque ambiguïté).

Note de la rédaction : afficheur 7 segments

16 caractères représentés sur un afficheur

Désignation des segments

Images CC BY-SA 3.0

Afin de simplifier le codage, notre jeu sera privé de quelques « extras », comme la détermination du meilleur score parmi les cinq tentatives, de la moyenne des temps de réaction, etc. Ceci permettra de se concentrer sur l'objectif premier qui sera d'interfacer un système d'affichage autonome pour simplement afficher le temps de réponse de chaque tentative. Le fonctionnement général du jeu sera similaire : l’affichage se mettra à clignoter rapidement pour indiquer à l’utilisateur de se tenir prêt, et après une attente de durée aléatoire entre une et dix secondes, un nouveau message donnera le top départ et le joueur devra en réponse frapper le bouton (un capteur de force résistif) au plus vite. Le temps de réponse sera affiché pendant quelques secondes et, après une courte pause, le jeu recommencera. Le principe général, ou le pseudo-code du jeu, restent fidèles au jeu original. La modification majeure, aussi bien sur le plan matériel que logiciel, concerne le système d'affichage puisqu'un ensemble d’afficheurs à LED 7-segments sera utilisé à la place du Moniteur Série de l'EDI Arduino.

En ce qui concerne le reste du matériel, on n'aura plus besoin des LED de signalisation, mais l'interrupteur du joueur est conservé (le capteur de force). Un temps de réaction inférieur à 100 ms étant improbable, pas plus qu'un temps supérieur à la seconde pour quelqu'un en bonne santé, à moins que la personne ne soit distraite ou sous l'emprise d'une quelconque substance chimique et récréative affectant temporairement (espérons-le) son système nerveux. Ainsi, un ensemble de trois afficheurs devrait suffire pour rendre compte des temps entre 0 et 999 millisecondes.

Un ensemble de trois afficheurs 7-segments nécessite normalement 21 sorties si chaque segment est piloté individuellement. C'est plus que n'en comporte l'Arduino Uno et cette solution n'est donc pas viable. Au lieu de cela, on peut multiplexer les afficheurs dans le temps. Dans le principe, on affiche seulement le chiffre des centaines sur l’afficheur le plus à gauche pendant quelques millisecondes, puis vient le chiffre des dizaines sur l’afficheur du milieu pendant quelques millisecondes, et enfin le chiffre des unités sur l’afficheur de droite là aussi pendant quelques millisecondes. On répète alors le processus en boucle, les afficheurs s'allumant chacun leur tour. Tant que la boucle principale s'exécute en moins de 25 millisecondes environ, l’œil et le cerveau n'y voient que du feu (NDLR du fait de la persistance rétinienne) et les trois afficheurs semblent actifs simultanément. Avec ce mécanisme de multiplexage, seules 10 sorties de la carte seront nécessaires : sept pour commander les segments et trois pour piloter chaque afficheur individuellement en mode multiplexé. Si les afficheurs sont à anode commune, le schéma du montage est celui de la figure ci-dessous :

Configuration d'un ensemble de 3 afficheurs 7-segments (anode commune). Un bit 0 supplémentaire peut être utilisé au besoin pour piloter le point décimal.

Avec cette configuration à anode commune, c'est un niveau logique bas à la cathode qui va permettre d'allumer le segment, et donc un niveau logique haut qui va l'éteindre. Notez que les circuits de pilotage des LED ne sont pas représentés ici. Au lieu de cela, les cathodes des segments sont connectées directement aux sorties de l'Arduino en passant par des résistances Rs de limitation de courant. Les sorties de l'Arduino pourront absorber le courant nécessaire si le courant qui traverse chaque LED reste à un niveau peu élevé. Par exemple, pour une alimentation à 5 V et une tension de seuil de la LED (Forward voltage) à environ 2 V, une résistance Rs de 470 Ω permet de limiter le courant aux alentours de 6 ou 7 milliampères par segment, ce qui rend l'éclairage des segments suffisant pour une utilisation en intérieur. Chaque afficheur peut être activé grâce à un transistor PNP. Un niveau logique bas sur sa base sature le transistor, et le courant peut alors circuler à travers les segments de l’afficheur. Pour cette application, un transistor de faible puissance comme le 2N3906 suffira. Si tous les segments de l’afficheur sont allumés, c'est un courant maximum d'environ 50 milliampères qui arrive au collecteur. Ainsi, une résistance à la base du transistor de l'ordre du kiloohm fonctionnera parfaitement, avec un courant de pilotage à la base du transistor de quelques milliampères.

En pratique, il sera plus facile de dédier tout le port D au pilotage des sept segments (D.1:7, connecteurs 1 à 7 de l'Arduino). Cela signifie par contre qu'on ne pourra plus utiliser le Moniteur Série (ports D.0:1), mais ce ne sera pas un problème. On peut utiliser les ports B.0:2 (connecteurs 8 à 10 de l'Arduino) pour piloter les trois afficheurs en mode multiplexé et B.3 (connecteur 11) pour le capteur de force. Les ports utilisés peuvent être déclarés dans des directives #define. On aura également besoin d'un tableau de valeurs codées en binaire pour le dessin des chiffres et lettres de l'afficheur :

// Port B.0:2 pour le multiplexage
#define DIGIT1   0x01
#define DIGIT10  0x02
#define DIGIT100 0x04
#define DIGIT111 0x07

#define FSRMASK  0x08

unsigned char numeral[]={
//ABCDEFG,dp
0b00000011,   // 0
0b10011111,   // 1
0b00100101,   // 2
0b00001101,   // 3
0b10011001,
0b01001001,
0b01000001,
0b00011111,
0b00000001,
0b00011001,   // 9
0b11111111,   // espace
0b01100001,   // E
0b01110011,   // r
0b00001001,   // g
0b00111001    // o
};

#define LETTER_BLANK  10
#define LETTER_E      11
#define LETTER_R      12
#define LETTER_G      13
#define LETTER_O      14
#define MSG_GO        -1
#define MSG_ERR       -2

Les dessins des chiffres et caractères sont codés dans un tableau numeral[] d'octets dont les valeurs sont initialisées en binaire. Chaque bit donne l'état du segment : le bit de poids fort donne l'état du segment A (le segment horizontal tout en haut de l’afficheur), et le bit de poids faible l'état du point décimal (non employé ici). Si on prend par exemple le dessin du chiffre « 0 », il faut activer tous les segments excepté le G (le segment horizontal du milieu) et le point décimal. Avec la logique active au niveau bas, il faut donc mettre à 0 chaque bit correspondant à un segment allumé. Le procédé est le même pour les autres chiffres. On a en plus ajouté certains caractères spéciaux comme le caractère « espace », ainsi que les lettres « E », « r », « g » et « o ». Avec les deux premières lettres, on pourra former le message « Err » pour signaler une erreur (ou si l'utilisateur « triche » par exemple), et avec les deux lettres suivantes on pourra former le message « go » pour donner le top départ à l'utilisateur à la place de la LED verte. Deux variables MSG qui seront examinées au moment propice ont également été définies.

Note de la rédaction : documentation typique d'un ensemble avec trois afficheurs 7-segments (anode commune) adapté au multiplexage

Pour allumer un segment sur un afficheur en particulier, il faut mettre l'anode de l’afficheur au niveau logique haut, et la cathode du segment correspondant au niveau logique bas. En mode multiplexé, on configure l'état des segments au niveau des cathodes puis on allume l’afficheur en mettant l'anode correspondante au niveau logique haut. Pour donner l'illusion des trois afficheurs allumés simultanément, on active tour à tour chaque afficheur à une fréquence suffisante pour que la persistance rétinienne joue pleinement son rôle.

Une vidéo plutôt démonstrative ci-dessous.

Animation en format "youtube" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://www.youtube.com/v/pMJ_5iTbNzc

Après ces quelques définitions, on peut regarder les changements apportés à la fonction setup(). Les configurations des entrées-sorties sont modifiées, mais la définition de la graine (seed) pour la fonction random() est toujours là. Notez que comme toutes les broches du port D sont configurées en sorties, on remplit le registre de direction directement avec l'octet 0xff plutôt que de définir les bits un par un avec une succession de OU logique. Il est aussi important d'initialiser les bits pilotant les transistors des trois afficheurs (DIGIT111) du port B au niveau logique haut. Sinon, comme les broches du port D seront au niveau logique bas par défaut, tous les segments des trois afficheurs apparaîtraient allumés lors du démarrage de la carte. Le courant absorbé par la carte serait considérable à ce moment-là. Mettre les bits pilotant les transistors des trois afficheurs à l'état haut permet d'assurer que l'ensemble des afficheurs sera désactivé avec tous les segments éteints au démarrage de la carte.

void setup()
{
// Configuration des connecteurs 0 à 7 (port D.0:7) en sortie pour le pilotage des segments
DDRD = 0xff;

// Configuration des connecteurs 8 à 10 (port B.0:2) en sortie pour le pilotage des afficheurs en mode multiplexé
DDRB |= DIGIT111;

// Les afficheurs sont actifs par défaut au niveau logique bas, donc on les désactive au démarrage
PORTB |= DIGIT111;

// Connecteur 11, port B.3 comme entrée pour le capteur de force
DDRB &= (~FSRMASK);   // Configuration en entrée
PORTB |= FSRMASK;     // Activation de la résistance pull-up

// Initialiser la graine à partir du bruit sur l'entrée analogique A0
randomSeed(analogRead( 0 ));
}

Voici le code de la boucle principale loop(), légèrement modifié. On peut le comparer à la version originale ligne par ligne et noter les quelques modifications. Les commentaires dans le code devraient être suffisamment explicites. Les plus gros changements concernent évidemment le code d'affichage dans le Moniteur Série qui est remplacé par une nouvelle fonction DisplayValue() décrite plus bas.

void loop()
{
unsigned long starttime, finishtime;
int i, j, nettime;
long a;

for(i=0;i<5;i++)
{
a = random(1000, 10000);

// Attendre deux secondes avant de débuter ce tour
delay(2000);

// Clignotement de l'afficheur pour signaler au joueur de se tenir prêt
for(j=0;j<5;j++)
{
DisplayValue( 888, 100 );
delay(100);
}

// Temporisation de durée aléatoire
delay(a);

// Message Go, pour donner le top départ
starttime = millis();
DisplayValue( MSG_GO, 50 );

// Attente de la réponse, la frappe du capteur de force va mettre l'entrée au niveau logique bas
while( PINB & FSRMASK );

finishtime = millis();
nettime = finishtime - starttime;

// Si la frappe a été anticipée irrégulièrement
if( nettime < 100 )
nettime = MSG_ERR;

DisplayValue( nettime, 3000 );
}
}

La fonction DisplayValue() est assez simple à utiliser. Le premier argument de la fonction est le nombre à afficher, entre 0 et 999. Le second argument est la durée de l'affichage en millisecondes. Par exemple, DisplayValue(888, 100) signifie : « Afficher le nombre 888 pendant 100 millisecondes ». On affichera ce nombre pour signifier au joueur de se tenir prêt. Outre les nombres à afficher, d'autres valeurs particulières sont aussi autorisées comme premier argument de la fonction DisplayValue(), et celles-ci sont négatives pour les distinguer des durées en millisecondes. Les temps de réactions négatifs sont bien entendu impossibles, à moins de disposer de facultés à remonter dans le temps. Si la valeur du premier argument est MSG_ERR, le message « Err » sera affiché. Si la valeur est MSG_GO, c'est le message « go » qui s'affichera. Ainsi, l'ancienne LED verte qui se mettait à clignoter pour lancer le top départ est remplacée par un DisplayValue(MSG_GO, 50) qui affichera le message « go » pendant 50 millisecondes.

La première chose que doit réaliser la fonction est de vérifier les valeurs des arguments passés. D'abord, les codes des messages spéciaux doivent être valides, puis les durées affichées doivent être dans la plage admissible (NDLR entre 0 et 999 millisecondes). Lorsqu'un nombre correspondant à la durée en millisecondes est passé, il faut le décomposer en trois nouvelles valeurs correspondant au chiffre à afficher sur chacun des trois afficheurs. Ces nouvelles valeurs sont stockées dans les variables h, t et u correspondant respectivement au chiffre des centaines (hundreds), des dizaines (tens) et des unités (units). Pour faire cette décomposition, on utilise la division entière et le modulo. On utilise le modulo par 10 pour récupérer le chiffre des unités à droite, puis la division entière par 10 pour décaler le nombre d'un rang vers la droite, et on recommence. Prenez un nombre quelconque à trois chiffres, et effectuez les calculs vous-mêmes sur papier pour vous approprier la logique.

void DisplayValue( int v, int msec )
{
unsigned char i, h, t, u;

if( (v <= MSG_ERR) || (v > 999) ) // Si valeur non valide, erreur
{
h = LETTER_E;
t = u = LETTER_R;
}
else
{
if( v == MSG_GO )
{
h = LETTER_G;
t = LETTER_O;
u = LETTER_BLANK;
}
else
{
u = v%10;
v = v/10;
t = v%10;
h = v/10;
}
}

À ce stade, le message doit être affiché pendant le temps requis en bouclant autant de fois que nécessaire. Le code fait en sorte que chaque passage dans la boucle dure environ 15 millisecondes, où les trois afficheurs sont alors allumés chacun leur tour pendant 5 millisecondes. Une simple division par 15 suffira alors pour calculer le nombre d'itérations, mais il faut faire attention au cas où la durée d'affichage demandée accidentellement ne serait que de quelques millisecondes (et où la division retournerait une valeur nulle).

// Calcul du nombre d'itérations pour la durée requise
msec = msec/15;
if( msec < 1 )
msec = 1;

On crée ensuite une boucle de la durée requise, et qui sera nécessairement un multiple de 15 millisecondes. À l'intérieur de cette boucle, on commence par éteindre tous les afficheurs. On prépare les segments à l'aide du tableau numeral avant d'activer l’afficheur souhaité. Notez que l'état des segments codé en binaire est saisi dans un tableau en suivant un ordre de telle sorte que l'indice du tableau évoquant l'état des segments correspond justement au chiffre à afficher. Comme quoi, une bonne anticipation permet d'économiser des efforts de codage et de la place mémoire. Chaque afficheur est maintenu actif pendant 5 millisecondes. En sortant de la fonction, on désactive tous les afficheurs.

// Affichage de la valeur pendant la durée spécifiée
for( i=0; i<msec; i++ )
{
// On efface tout, puis activation des afficheurs chacun leur tour
PORTB |= DIGIT111;
PORTD = numeral[h];
PORTB &= ~DIGIT100;
delay(5);

PORTB |= DIGIT111;
PORTD = numeral[t];
PORTB &= ~DIGIT10;
delay(5);

PORTB |= DIGIT111;
PORTD = numeral[u];
PORTB &= ~DIGIT1;
delay(5);
}

// On efface tout
PORTB |= DIGIT111;

Défi

Cet exercice requiert beaucoup de câblage et une quantité significative de code. Beaucoup de choses peuvent dysfonctionner si vous sautez dedans à pieds joints en essayant de tout faire en même temps. Par exemple, si un segment ou même un afficheur ne s'allume jamais, comment saurez-vous si l'origine du problème est matérielle ou logicielle ? Trouver le bug n'est pas une mince affaire dans ce cas. Il vaut mieux s'assurer d'abord que chaque bout de code fait ce qu'on attend de lui, puis se baser dessus pour faire le reste. Par exemple, vous pouvez commencer par mettre au point le code qui allume un seul afficheur, puis l'étendre pour l'affichage multiplexé. Considérez le code suivant :

// port B.0:2 pour le pilotage des afficheurs en mode multiplexé
#define DIGIT1   0x01
#define DIGIT10  0x02
#define DIGIT100 0x04
#define DIGIT111 0x07

#define FSRMASK  0x08

unsigned char numeral[]={
//ABCDEFG,dp
0b00000011,   // 0
0b10011111,   // 1
0b00100101,   // 2
0b00001101,   // 3
0b10011001,
0b01001001,
0b01000001,
0b00011111,
0b00000001,
0b00011001,   // 9
0b11111111,   // espace
0b01100001,   // E
0b01110011,   // r
0b00001001,   // g
0b00111001    // o
};

#define LETTER_BLANK  10
#define LETTER_E      11
#define LETTER_R      12
#define LETTER_G      13
#define LETTER_O      14
#define MSG_ERR       -2

void loop()
{
// Jeu d'essais
DisplayValue(123);
DisplayValue(456);
DisplayValue(12);
DisplayValue(3);
DisplayValue(100);
DisplayValue(-2);
DisplayValue(50);
}

void DisplayValue( int v )
{
unsigned char i, h, t, u; // centaines, dizaines, unités

if( (v <= MSG_ERR) || (v > 999) ) // Erreur
{
h = LETTER_E;
t = u = LETTER_R;
}
else
{
u = v%10;
v = v/10;
t = v%10;
h = v/10;
}

// Afficher la valeur pendant approximativement 1s (66 x 15 millisecondes)
for( i=0; i<66; i++ )
{
// On efface tout, puis activation des afficheurs chacun leur tour
PORTB |= DIGIT111;
PORTD = numeral[h];
PORTB &= ~DIGIT100;
delay(5);

PORTB |= DIGIT111;
PORTD = numeral[t];
PORTB &= ~DIGIT10;
delay(5);

PORTB |= DIGIT111;
PORTD = numeral[u];
PORTB &= ~DIGIT1;
delay(5);
}

// On efface tout
PORTB |= DIGIT111;
}

Une fois que le montage est prêt, testez-le en situation ! Mettez en œuvre le programme et réalisez un schéma complet du montage. On vous demande également de décrire les éventuelles modifications à apporter pour des afficheurs 7-segments à cathode commune à la place de ceux à anode commune.

XIV - Sortie « analogique » via des signaux PWM

Dans cet exercice, on examinera les « sorties analogiques » à travers l'utilisation des signaux PWM ou Pulse Width Modulation (NDLR en français, MLI pour Modulation en Largeur d'Impulsion). Les signaux PWM permettent de générer des signaux analogiques à l'aide de sorties numériques, à défaut de disposer de véritables sorties variables en continu. Certains microcontrôleurs et cartes à microcontrôleur intègrent en effet des DAC (NDLR Digital-Analogic Converter ou Convertisseur Numérique-Analogique) comme l'Arduino Due, mais comme la plupart n'en disposent pas, un signal numérique impulsionnel modulé en largeur d'impulsion avec un rapport cyclique variable peut être utilisé à la place.

Note de la rédaction

Un signal PWM est un signal logique, rectangulaire et périodique, dont on peut faire varier le rapport cyclique (le pourcentage de temps où le signal est à l'état haut sur une période du signal).

Allure d'un signal modulé en largeur d'impulsion ou PWM

Le signal n’est nullement analogique, mais on peut l'adapter en lui faisant subir un filtrage de type passe-bas.

Image Wikipédia

Filtrer ce signal carré permet d’obtenir la tension moyenne VMOY (et parfois, la charge se comporte avantageusement comme un filtre passe-bas). Si la fréquence porteuse est significativement supérieure à la fréquence de coupure du filtre, l'ondulation résiduelle reste faible.

Rapport cyclique :

Tension moyenne :

Mathématiquement, la tension « analogique » utile produite en sortie du filtre passe-bas est proportionnelle à l'aire sous la courbe du train d'impulsions modulé. Si vous avez pensé à moyenner le signal du train d'impulsions avec un filtre passe-bas, un rapport cyclique petit (proche de 0 %) produira une tension moyenne basse, et inversement un rapport cyclique grand (proche de 100 %) produira une tension moyenne élevée. Certains composants vont naturellement filtrer les hautes fréquences alors que d'autres non. Dans cet exercice, on commencera par étudier le comportement d'une simple LED.

Il est facile d'allumer ou éteindre une LED. Considérez le code suivant :

/* PWM V1 Test luminosité LED, version manuelle */

#define LEDBIT   0x40

// on/off temporisation en millisecondes
#define LEDONTIME   300
#define LEDOFFTIME  300

void setup()
{
// Configuration connecteur 6 en sortie vers la LED
DDRD |= LEDBIT;
}

void loop()
{
PORTD |= LEDBIT;    // allumer la LED
delay(LEDONTIME);

PORTD &= (~LEDBIT); // éteindre la LED
delay(LEDOFFTIME);
}

Dans cet exemple, on allume la LED pendant 300 millisecondes, on l'éteint pendant 300 millisecondes, puis on perpétue le cycle périodiquement, toutes les 600 millisecondes. Pour contrôler la LED, réalisez un circuit à base de transistor NPN piloté par la sortie 6 de l'Arduino. Saisissez le code, compilez-le et transférez-le afin de tester le programme. Réduisez les temporisations on/off des LED à 30 millisecondes chacune et répétez l'opération. La LED devrait clignoter plus rapidement. Réduisez à nouveau les temporisations on/off des LED, cette fois à 5 millisecondes chacune et répétez l'opération. Une chose curieuse arrive, la LED ne semble plus clignoter, mais sa luminosité est plus faible. Tandis que les temporisations descendent en dessous de 20 millisecondes, l’œil humain ne distingue plus ces flashs lumineux et les assimile en niveau d'intensité lumineuse continue(1), comme s'il moyennait le signal lumineux. Comme sur une période du signal, la LED est allumée la moitié du temps, elle apparaîtra allumée en continu, mais avec sa luminosité atténuée de moitié par rapport à sa luminosité maximum. Vous pouvez vérifier le phénomène en changeant la temporisation on de la LED à 1 milliseconde, et la temporisation off à 9 millisecondes, perpétuant ainsi un cycle périodique de période 10 millisecondes. La luminosité de la LED sera davantage atténuée puisque celle-ci ne sera allumée que 10 % du temps.

Plutôt que de régler les temporisations manuellement, on peut utiliser la fonction analogWrite() sur une des sorties capables de générer des signaux PWM (les sorties dédiées de la carte sont repérées avec un symbole « ~ » (tilde) sérigraphié au niveau du connecteur). Il se trouve que le connecteur 6 offre cette possibilité, et donc nous n'avons qu'un léger changement à effectuer au niveau du code :

/* PWM V2 Test luminosité LED, version avec analogWrite */

#define LEDPIN 6

// luminosité entre 0 et 255
#define LEDBRIGHTNESS 230

void setup()
{
}

void loop()
{
analogWrite(LEDPIN, LEDBRIGHTNESS);
}

Remarquez que le connecteur 6 n'a pas besoin d'être configuré en sortie puisque la fonction analogWrite() s'en charge automatiquement (voir analogWrite()). Si vous relevez le signal du connecteur 6 à l'oscilloscope, vous verrez un signal carré à 490 Hz environ, avec un rapport cyclique de 90 % (NDLR car 230/255 ≈ 0,90). Notez que le paramètre LEDBRIGHTNESS pourrait être remplacé par une variable résultant d'une lecture d'entrée analogique avec analogRead(). On peut donc connecter un potentiomètre à une entrée analogique et récupérer la valeur issue de la conversion analogique-numérique comprise entre 0 et 1023. Il reste alors à rééchelonner la plage de valeurs vers une autre plage de valeurs entre 0 et 255 (unsigned char), compatible avec le paramètre en entrée de la fonction analogWrite(), et jouer ainsi sur la luminosité de la LED. Ce changement d'échelle peut être effectué avec la fonction map() de la bibliothèque Arduino :

result = map(value, fromLow, fromHigh, toLow, toHigh);

Dans notre cas, on utilisera :

result = map(value, 0, 1023, 0, 255);

Par la suite, connectez un potentiomètre 10 kΩ entre l'alimentation et la masse. Connectez son curseur au connecteur A0 de l'Arduino. Saisissez le code suivant, compilez-le et transférez-le :

/* PWM V3 Gradateur LED utilisant analogWrite et un potentiomètre */

#define ANALOG_IN_PIN 0
#define LEDPIN 6

void setup()
{
analogReference( DEFAULT );
}

void loop()
{
int a;

a = analogRead( ANALOG_IN_PIN );

a = map(a, 0, 1023, 0, 255);
analogWrite(LEDPIN, a);
}

Jouer avec des LED est bien beau, mais on pourrait souhaiter piloter des choses un peu plus intéressantes, par exemple un moteur. Bien entendu, notre circuit de pilotage de LED sera sous-dimensionné pour la plupart des moteurs, mais il sera suffisant moyennant quelques modifications mineures avec un petit moteur à courant continu pour hobbyiste à faible consommation (10 à 15 mA en fonctionnement à vide). Un exemple de circuit de pilotage d'un tel moteur est montré dans la figure ci-dessous :

Circuit de pilotage d’un moteur avec un transistor bipolaire

Notez tout d'abord qu'une alimentation externe plus puissante est nécessaire. Dans ce cas, une alimentation 15 V à courant continu serait appropriée (NDLR une alimentation réglable à CC régulée de laboratoire 0-15 V et 0-2 A par exemple, soit 30 W au maximum). R1 est choisi aux alentours de 6,8 kΩ pour garantir la saturation du transistor, et C1 aux alentours de 100 nF. D1 est une diode de type 1N4002. Son rôle est d'éviter, ou d'amortir les pics de tension dus à l'inductance du moteur lorsque le transistor bascule de l'état saturé à l'état bloqué(2).

Note de la rédaction

Une première remarque sur la présence du condensateur C1 dans le schéma précédent. On retrouve le même montage avec ce condensateur C1 dans un autre ouvrage du même auteur, avec la seule mention : « The resistor and capacitor at the base are used to shape the incoming pulse to improve performance. » Si la présence de la résistance R1 est nécessaire pour contrôler le courant à la base du transistor, la justification de la présence du condensateur C1 pour « améliorer les performances » ne nous paraît pas évidente a priori. Assurément, vous ne risquez rien à omettre ce condensateur dans un premier temps.

La valeur de la résistance de base R1 doit être calculée, mais l'avantage d'utiliser le transistor en « tout ou rien » (état bloqué ou saturé pour faire de la commutation), c'est qu'elle se calcule très facilement grâce à la datasheet du transistor.

Transistor NPN : symboles et conventions

Si on prend les caractéristiques du transistor BC547 :

Extrait datasheet BC547

Le constructeur ne garantit plus le fonctionnement du transistor si :

- la tension entre le collecteur et l'émetteur dépasse 45 V ;

- le courant demandé au niveau du collecteur dépasse 100 mA ;

- la puissance dissipée, sans dissipateur et à la température ambiante 25 °C, dépasse 0,625 W.

D’après le constructeur du transistor, celui-ci est complètement saturé si le courant de base IB est 20 fois inférieur au courant du collecteur IC. Ensuite, le constructeur donne au moins deux exemples concrets : si IC = 100 mA (IC = IC maxi) et IB = 5 mA (vingt fois inférieur) alors le transistor sera saturé, sa tension collecteur-émetteur ne sera évidemment pas de 0 V comme un interrupteur fermé, mais elle s'en approchera, 0,2 V typiquement et jusqu'à 0,6 V maxi garanti dans les conditions de courant données juste avant.

Calcul de la résistance de base

Il faut que le courant de base IB soit 20 fois inférieur au courant de collecteur IC.

Imaginons une charge qui consomme 30 mA, donc IC = 30 mA, il faut que IB soit égal ou très proche de 1,5 mA (idéalement, il faut même être un peu au-dessus des 1,5 mA pour « sursaturer »).
Résistance de base = (Tension de sortie de l'Arduino - VBE(SAT)) / IB (application de la loi des mailles et de la loi d’Ohm)
Résistance de base = (5 – 0,7) / 1,5 mA = 2,8 kΩ, et on mettra une résistance normalisée de 2,7 kΩ.

Pour des charges requérant plus de puissance, un transistor à effet de champ (FET pour Field Effect Transistor) peut être utilisé comme montré dans la figure ci-dessous, par exemple avec un ventilateur de PC dans un boîtier standard. Typiquement, ce genre de moteur s'alimente en 12 V et peut dissiper 1 ou 2 watts. Le transistor EMOSFET ZVN4206A pourrait convenir avec une telle charge puisqu''il peut conduire un courant jusqu'à 600 mA en continu et 8 A en pointe (avec Vdd = 12 V CC).

Circuit de pilotage d'un moteur de ventilateur avec un transistor EMOSFET (NDLR Mais où est passée la résistance de base ?)

Préparez un circuit de pilotage de moteur en prototype en lieu et place du circuit de pilotage de LED précédent, le connecteur 6 de l'Arduino est conservé pour le pilotage (en mettant de côté provisoirement le circuit de pilotage de LED afin d'y revenir dessus plus tard). Aucune modification du code n'est nécessaire. Une fois le circuit connecté, la vitesse du moteur devrait maintenant varier selon la position du potentiomètre. (NDLR Un tel moteur filtre les hautes fréquences et se comporte comme s'il était alimenté en continu à la tension moyenne déterminée par la position du curseur du potentiomètre.)

Défi

Parfois, on a besoin d'un retour visuel sur la valeur d'une variable, de réaliser un afficheur en quelque sorte. Dans ce cas, on peut réaliser un simple bargraphe à LED pour indiquer la vitesse du moteur, plus la vitesse est élevée et plus il y a de barres allumées. Inutile de réinventer la roue puisqu'un tel dispositif a déjà été réalisé précédemment. Il suffit d'adapter la fonction lightLEDsBar() de l'exercice sur les entrées analogiques de l'Arduino. Vous aurez besoin d'un circuit relié au port B de l'Arduino pour piloter les quatre LED.

Finalement, vous pouvez fixer la vitesse du moteur en fonction d'une variable environnementale comme la température ou la luminosité ambiante plutôt qu'utiliser un potentiomètre actionné manuellement. Le potentiomètre peut être remplacé par un simple capteur, tel un LM34 pour acquérir la température, ou bien par une photorésistance avec une résistance pour former un diviseur de tension et acquérir le niveau de luminosité.

Il sera relativement simple et sans danger de faire varier aux extrêmes le niveau de luminosité en utilisant une lampe ou votre doigt pour éclairer ou obscurcir une photorésistance. En comparaison, la plage de variation de la tension d'un capteur de température analogique est plus restreinte et il faudra probablement adapter la plage de tension en conséquence. Vous pouvez aussi souhaiter éloigner le capteur de température des autres composants du circuit afin de les protéger des très hautes ou très basses températures. Par sécurité, n'utilisez pas de flamme en laboratoire pour créer de la chaleur !

Inclure votre code et un schéma de montage approprié, comme d'habitude, et joindre une description des tests réalisés.

XV - Compteur d’événements…

XV-A - …ou la revanche du fils du retour de la mesure du temps de réaction

Il semblerait que notre microcontrôleur ait souvent besoin de faire plusieurs choses en même temps, comme scruter ses entrées tandis qu'il doit opérer des changements sur ses sorties. Un simple contrôleur ne peut faire ça au sens littéral, mais il peut opérer si rapidement qu'il semble effectuer plusieurs tâches simultanément. Par contre, dans certaines situations, cela peut être compliqué à mettre en œuvre si l’on code avec une technique d'attente active (polling ou busy-waiting).

Par exemple, considérez l'opération de multiplexage de plusieurs afficheurs 7-segments. Alors qu’une simple LED ne nécessite que la modification ponctuelle d’un bit, les affichages multiplexés nécessitent, quant à eux, une succession permanente de modifications. Dans un exercice précédent sur la mesure du temps de réaction, l'affichage nécessitait 15 millisecondes par cycle, soit 5 millisecondes pour chacun des trois afficheurs. Si l'on voulait seulement afficher une valeur pendant quelques secondes, il fallait simplement boucler sur cette partie du code. Bien que ce principe a fonctionné pour cette application, tel n'est pas toujours le cas.

Considérez maintenant le cas d'un compteur d'événements. Il s'agit d'un dispositif qui compte des choses et qui affiche la valeur courante du compteur. On pourrait envisager ce genre de dispositif sur une ligne d'assemblage pour garder une trace du nombre d'articles qui sont détectés, grâce à un capteur mécanique, photoélectrique, magnétique ou autre détecteur qui se déclencherait lors du passage de l'article. On voudrait en plus que ce dispositif affiche en permanence la valeur courante du compteur, tout en scrutant constamment l'état du détecteur. Comment faire les deux à la fois ? Dans le contexte de notre système de mesure de temps de réaction, on pourrait ajouter du code à l'intérieur de la boucle qui gère l'affichage pour relever l'état du détecteur, mais si la procédure de détection dure moins longtemps que la procédure d’affichage, soit 15 millisecondes, on pourrait louper une détection si celle-ci avait lieu pendant la procédure d’affichage. Fort heureusement, il y a une solution élégante pour gérer ce genre de problème.

XV-B - Nous interrompons le déroulement normal du programme…

La solution passe par l'utilisation d'une interruption. Imaginez un système par lequel un bout de code appelé « gestionnaire d'interruption » (interrupt handler) ou « routine de service d'interruption » (ISR pour Interrupt Service Routine) est activé pour s'exécuter sous certaines conditions. L'exécution du code courant est suspendu jusqu'à ce que la routine d'interruption ISR soit terminée. L'interruption peut être activée par un événement soit logiciel, soit matériel.

Un exemple typique pourrait être le cas d'une interruption déclenchée par un changement d'état d'un signal sur une entrée. La plupart des microcontrôleurs ont différents niveaux d'interruption qui peuvent être classés par ordre de priorité, c'est-à-dire que certains événements peuvent être prioritaires sur d'autres, et qu'il est possible qu'une routine d'interruption ISR soit elle-même interrompue par une interruption de priorité plus élevée. En général, les routines d'interruption ISR sont courtes, exécutant rapidement des bouts de code qui ne vont pas rentrer en conflit avec le programme principal.

Dans le cas d'un compteur d'événements, notre détecteur pourrait être connecté à une entrée qui déclenche une interruption. La routine d'interruption ISR n'aurait rien d'autres à faire qu'à incrémenter une variable globale qui ferait office de compteur d'articles. Le programme principal devra scruter périodiquement cette variable globale et répondre en conséquence (ici, mettre à jour l'affichage). Tout se passe comme si un mécanisme s'exécutant en tâche de fond scrutait en permanence la broche d'entrée. Ce mécanisme nous exonère d’avoir à disséminer, au cœur du programme principal, des fragments de code destinés à scruter l’état de la broche.

Habituellement, aucune des interruptions disponibles n'est active, à l'exception du signal de remise à zéro (reset). Elles doivent être activées individuellement avant d'être utilisées. Le système de développement attribue un nom pour chaque routine d'interruption, donc tout ce qu'il nous reste à faire est d'activer le ou les bits appropriés dans le ou les registres de contrôle des interruptions (interrupt control register) et de compléter le code de la routine d'interruption ISR.

Pour le compteur d'événements, on va réutiliser une grande partie du code et du matériel de cet exercice précédent sur la mesure du temps de réaction, plus particulièrement les afficheurs 7-segments et le code gérant le multiplexage. Le capteur de force qui faisait office de bouton pour le joueur sera notre déclencheur d’événement. On exploitera « l'interruption sur changement d'état » des broches de l'ATmega 328P (PCINT pour Pin Change INTerrupt). Correctement configurée, chacune des broches en entrée peut déclencher une interruption quand le signal bascule de l'état haut à l'état bas, ou inversement de l'état bas à l'état haut. Ces broches sont groupées en blocs (banks) correspondant aux ports de l'ATmega 328P, par exemple le bloc 0 (bank 0) correspond au port B (NDLR voir les interruptions sur Arduino pour plus de détails).

Chaque bloc a son vecteur d'interruption associé. Pour le bloc bank 0, ce sera PCINT0_vect (Pin Change INTerrupt Bank 0 Vector). Techniquement, un vecteur d'interruption est une adresse vers une fonction d'interruption prédéfinie. Chacune des broches d'un port en particulier est activée par son propre bit d'interruption. Pour le bloc bank 0, ces bits sont nommés PCINT0 à PCINT7. Donc, si les bits 0 et 1 du port sont activés pour interruptions (via PCINT0 et PCINT1), un changement d'état sur n'importe laquelle des deux broches générera une interruption du bloc bank 0 et la routine d'interruption ISR du vecteur PCINT0_vect s'exécutera. Une fois la routine ISR terminée, l'exécution normale du code principal reprend là ou elle a été interrompue.

Avant de jeter un œil au matériel, regardons du côté du code. Comme mentionné plus tôt, on peut modifier le code existant du système de mesure de temps de réaction, nouvelle formule. On n'a plus besoin du message spécial « go », et on peut donc supprimer certaines lignes du tableau formant les lettres sur l’afficheur. Les autres modifications du code resteront minimes si on conserve le même câblage. On utilisera le terme générique « capteur » (sensor) à la place de FSR (Force Sensor Resistive), et on doit encore déterminer comment le comptage sur événement sera implémenté. Remarquez que documenter son code en décrivant notamment le câblage nous évite de réinventer la roue. Tout bon programmeur, technicien ou ingénieur doit le faire !

/* Compteur d’événements avec un ensemble de 3 afficheurs 7-segments anode commune.
Segments sur D1:7, afficheurs sur B0:2 (chiffre des centaines sur B.2)
Pilotage des afficheurs par transistors PNP
Capteur sur B.3. Valeurs>999 ==> "Err"
Segments et afficheurs activés à l’état bas */
// Port B.0:2 pour le multiplexage
#define DIGIT1   0x01
#define DIGIT10  0x02
#define DIGIT100 0x04
#define DIGIT111 0x07

#define SENSORMASK  0x08

unsigned char numeral[]={
//ABCDEFG,dp
0b00000011,   // 0
0b10011111,   // 1
0b00100101,   // 2
0b00001101,   // 3
0b10011001,
0b01001001,
0b01000001,
0b00011111,
0b00000001,
0b00011001,   // 9
0b11111111,   // espace
0b01100001,   // E
0b01110011    // r
};

#define LETTER_BLANK  10
#define LETTER_E      11
#define LETTER_R      12

À ce stade, on doit déclarer la variable globale mentionnée plus tôt et modifier la fonction setup().

unsigned int g_count = 0;

void setup()
{
// Configuration des connecteurs 0 à 7 (port D.0:7) en sortie pour le pilotage des segments
DDRD = 0xff;

// Configuration des connecteurs 8 à 10 (port B.0:2) en sortie pour le pilotage des afficheurs en mode multiplexé
DDRB |= DIGIT111;

// Les afficheurs sont actifs par défaut au niveau logique bas, donc on les désactive au démarrage
PORTB |= DIGIT111;

// Connecteur 11, port B.3 comme entrée pour le capteur de force
DDRB &= (~SENSORMASK);   // Configuration en entrée
PORTB |= SENSORMASK;     // Activation de la résistance pull-up

// activation pin change interrupt bank 0
bitSet(PCICR, PCIE0);

// activation pin change interrupt sur port B.3
bitSet(PCMSK0, PCINT3);
}

Les deux dernières lignes du code ci-dessus sont particulièrement importantes. La première permet d'autoriser les interruptions sur changement d'état du bloc bank 0 (bit PCIE0 pour Pin Change Interrupt Enable bank 0) dans le registre de contrôle des interruptions (PCICR pour Pin Change Interrupt Control Register). La seconde spécifie alors la ou les broches concernées (PCINT3 pour Pin Change INTerrupt pin 3) dans le registre PCMSK0 (PCMSK0 pour Pin Change MaSK for bank 0)

Une fois l'interruption activée, il reste à écrire la routine d'interruption ISR. Comme dit précédemment, cette routine a déjà un nom (PCINT0_vect), et tout ce qu'elle doit faire, c'est d'incrémenter la variable globale qui fait office de compteur.

/* gestionnaire d’interruption sur bloc 0 (bank 0).
Trigger sur tout changement d’état de toute broche du bloc 0 (bank 0, c-à-d port B). État Haut->Bas, ou Bas→Haut) */

ISR(PCINT0_vect)
{
// incrémentation uniquement sur front descendant pour éviter un double comptage

if( !(PINB & SENSORMASK) )
g_count++;
}

La boucle loop() doit être entièrement réécrite. À l’intérieur, on y placera le contenu de l'ancienne fonction DisplayValue() qui contrôlait l'affichage multiplexé. Contrairement à la fonction originale, il n’y a plus besoin de maintenir un affichage pendant un laps de temps déterminé. Au lieu de cela, elle effectue en permanence un simple balayage des trois afficheurs sur une durée totale de 15 millisecondes par cycle. La boucle loop() reprend alors au début, et le processus se répète indéfiniment.

Au début de la boucle, le contenu de la variable globale g_count, le compteur, est copié dans une variable locale traitée séparément pour déterminer les indices du tableau comme auparavant. Remarquez une petite amélioration, à savoir ce bout de code supplémentaire qui affiche des « blancs » au niveau des zéros non significatifs à gauche.

Il reste à répondre à une question : pourquoi se donner la peine de faire une copie locale de la variable globale ? La raison est qu'une interruption peut surgir à tout moment. Ce qui signifie que des traitements pourraient n’être effectués que partiellement sur les variables h, t et u alors qu'une interruption modifie la valeur de la variable globale. Si cela se produit, les indices calculés peuvent prendre des valeurs fausses, car le code a fait le traitement sur une valeur qui a été modifiée une fois, voire plus, entre temps. Si on fait une copie locale, cela ne se produira pas. Seule la variable globale changera et l’affichage sera rafraîchi proprement au prochain passage dans la boucle loop().

void loop()
{
unsigned int v;
unsigned char h, t, u; // centaines, dizaines, unités

// copie dans une variable locale
v = g_count;

if( v > 999 ) // erreur
{
h = LETTER_E;
t = u = LETTER_R;
}
else
{
u = v%10;
v = v/10;
t = v%10;
h = v/10;

// zéros non significatifs à gauche remplacés par des blancs
if( !h )
{
h = LETTER_BLANK;
if( !t )
t = LETTER_BLANK;
}
}

// On efface tout, puis activation des afficheurs chacun son tour
PORTB |= DIGIT111;
PORTD = numeral[h];
PORTB &= ~DIGIT100;
delay(5);

PORTB |= DIGIT111;
PORTD = numeral[t];
PORTB &= ~DIGIT10;
delay(5);

PORTB |= DIGIT111;
PORTD = numeral[u];
PORTB &= ~DIGIT1;
delay(5);

// On efface tout
PORTB |= DIGIT111;
}

À ce stade, le code est fonctionnel. Si le matériel dont on disposait précédemment n'est pas encore en place, recâblez-le. Saisissez le code ci-dessus, compilez-le et transférez-le dans la carte. Chaque pression sur le bouton du joueur devrait incrémenter la valeur affichée d'une unité. Remarque : si aucun dispositif antirebond n'a été prévu, il se peut que chaque pression augmente la valeur du compteur de deux unités ou plus, de façon aléatoire.

XV-C - Le choix du commutateur

L'astuce consiste maintenant à implémenter le commutateur approprié pour compter les articles ou événements en question. Le commutateur doit établir ou rompre un contact, ou produire une transition d'état : état haut-état bas-état haut. Imaginez qu'on doive compter des articles convoyés par tapis roulant. L'implémentation dépendra des articles eux-mêmes. Ont-ils tous la même taille et la même orientation ? Est-ce qu'ils sont lourds ? Sont-ils répartis de manière aléatoire et à intervalle régulier ? Dans certains cas, un simple contacteur mécanique peut suffire : l'article qui vient en contact avec un mécanisme à poussoir, le déplace en poussant lors de son passage et produit un signal. Un ressort de rappel ramène le mécanisme en position initiale une fois l'article éloigné, le contacteur est ainsi prêt à détecter l'article suivant. Mais cette détection par contact peut ne pas être adaptée selon la taille, le poids ou la forme de l'objet détecté ou si les objets sont disposés trop irrégulièrement sur le tapis.

Une autre approche consiste à utiliser un capteur photoélectrique. Ici, un faisceau de lumière visible ou infrarouge traverse le tapis roulant (NDLR Barrage optique). À l'opposé de l'émetteur est placé un détecteur optique. Quand un article coupe le faisceau lors de son passage, un signal est généré. Ce principe fonctionnera avec des formes atypiques d'objet, quel que soit son placement sur la bande du tapis et même si l'objet est très léger puisqu'il n'y a aucun contact physique entre l'objet et le capteur. Il y aura toutefois quelques difficultés de détection avec des matériaux transparents et autres morceaux de fromages suisse découpés finement (sérieusement, un objet de forme atypique présentant des trous pourrait être détecté plusieurs fois ! NDLR Et plusieurs objets se masquant partiellement pourraient ne donner lieu qu’à une seule détection. De même, un objet mince pourrait passer sous le faisceau. La fiabilité de ce système de détection peut être facilement mise en défaut, surtout si l’on veut comptabiliser le passage d’objets différents.)

La fabrication des contacteurs mécaniques est complexe, mais leur principe de fonctionnement est assez simple. Pour la solution photoélectrique, deux possibilités se présentent à vous. La première est d'utiliser une photorésistance (cellules à bas coût au sulfure de cadmium ou CdS) et une source de lumière assez puissante. Ce genre de cellules a sa résistivité qui augmente sous faible éclairage, et qui diminue sous un fort éclairage. La photorésistance peut être connectée directement à un connecteur d'entrée avec la résistance de tirage au plus (pull-up) activée. La difficulté majeure ici provient de la lumière ambiante. L'autre possibilité passe par une paire émetteur-détecteur infrarouge (LED infrarouge et phototransistor infrarouge). Le circuit électrique est un peu plus complexe, mais ce capteur est beaucoup moins sensible à la luminosité ambiante.

L'alignement de l'émetteur et du récepteur photoélectrique, ainsi que la distance qui les sépare doit être précis pour déclencher un signal à bon escient. Pour faciliter les tests en laboratoire, il est préférable de valider le concept avec l'émetteur et le récepteur distant de seulement quelques centimètres, et en simulant le passage des articles avec une carte de couleur sombre entre les deux.

Défi

Retirez l'ancien capteur FSR et insérez votre nouveau détecteur d'articles à la place. Ajoutez un nouveau bouton qui remettra le compteur d'articles à zéro (bien entendu, sans utiliser le bouton reset de la carte Arduino). Incluez une description et le schéma de câblage de votre nouveau détecteur.

XVI - Générateur de formes d’onde arbitraires

Ce chapitre met en pratique une grande partie de ce que nous venons de voir dans les chapitres précédents, notamment l’utilisation des entrées/sorties numériques (GPIO) et des timers. Un nouvel élément majeur va résider dans la production de tensions de sortie variables sans avoir recours au variateur de rapport cyclique PWM. Une fois ce chapitre achevé, nous disposerons d’un générateur de formes d’ondes basse fréquence opérationnel, offrant le choix entre trois formes d’ondes de base : sinusoïdale, carrée et rampe (choix pouvant être étendu à d’autres formes), de fréquence variable. La qualité du signal sinusoïdal sera au moins aussi bonne que celle fournie par les générateurs de fonction standard offrant un THD (Total Harmonic Distorsion) inférieur à 1 % (NDLR : à comparer malgré tout aux générateurs de fonctions professionnels qui offrent des distorsions harmoniques inférieures à 0,001 %).

Une réalisation de cette importance va nous amener à aborder de nombreux domaines et demandera beaucoup de temps pour la mise au point du logiciel et la réalisation du montage matériel correspondant. Une approche méthodique du problème sera donc incontournable.

Commençons par le concept de base de la synthèse numérique directe (DDS pour Direct Digital Synthesis). Plutôt que d’utiliser des oscillateurs analogiques, on peut concevoir des formes d’onde en entrant les valeurs appropriées dans un convertisseur numérique-analogique (CNA). Il est possible d’obtenir ces valeurs soit en numérisant un signal analogique, soit par le calcul. Quelle que soit la source des données, nous pouvons les stocker dans un tableau puis les envoyer une par une à un convertisseur numérique-analogique. Une fois la fin du tableau atteinte, nous retournons à son début et répétons le processus. Ce tableau est couramment appelé « table d’onde » (NDLR : il y sera souvent fait référence ici sous le simple nom de « tableau »).

Théoriquement, en utilisant un tableau de taille appropriée, nous pouvons reproduire n’importe quelle onde de notre choix. La première question qui se pose est : « Quelle est la taille appropriée pour le tableau ? ». Pour faire simple, ça dépendra essentiellement de la complexité de l’onde, de la précision désirée, et d’éventuelles autres considérations pratiques. Pour une onde de type sinusoïdal, un résultat de qualité correcte sera obtenu avec quelques centaines de valeurs codées sur 8 bits. Pour une très haute qualité, quelques milliers de valeurs codées sur 16 bits pourront être nécessaires.

Considérant le simple cas d’une fonction sinusoïdale, on peut se poser la question de l’intérêt de l’utilisation d’une table d’onde dans la mesure où la fonction sin() peut nous fournir toutes les valeurs nécessaires. Cet intérêt réside dans le fait que, bien que la détermination des valeurs par le calcul prenne moins de place en mémoire que le tableau équivalent, l’accès aux valeurs contenues dans ce tableau est beaucoup plus rapide que leur détermination par le calcul.

Par exemple, sur un Arduino UNO, le calcul d’un point de la courbe, sur 8 bits, avec la fonction sin(), nécessite environ 140 µs : si le tableau comprend 100 points, cela conduit à une période de 14 millisecondes, soit une fréquence maximale de 71 Hz seulement. L’accès au même point dans un tableau de valeurs précalculées ne prendrait qu’une fraction de microseconde et permettrait d’atteindre, même pour des tableaux plus importants, des fréquences de l’ordre du kilohertz.

Alors, comment remplir le tableau ? D’abord, pour des raisons pratiques, le nombre de points contenus dans le tableau est une puissance de deux : n’oublions pas que nous travaillons en base deux. 256, soit 2^8, est une taille intéressante, car elle permet d’utiliser un entier non signé sur 8 bits (un octet) comme index, et de l’incrémenter en continu. Une fois la valeur 255 atteinte, l’incrément suivant provoquera un débordement, donc le retour à zéro, ce qui rend inutile un test de fin de tableau (NDLR : c’est toujours une bonne idée de se placer, quand c’est possible, dans des conditions qui permettent de simplifier le code).

Il est possible d’effectuer le calcul des points dans la fonction setup() et de remplir le tableau avec les résultats obtenus. Toutefois, ça consommera de la mémoire, et il sera préférable de le faire séparément. Bien qu’il soit possible d’utiliser une calculatrice pour déterminer ces valeurs à la main, il sera plus rentable d’écrire un programme dédié qui calculera ces valeurs et les écrira dans un fichier texte. Il sera simple, par la suite, de copier/coller ces données dans le programme C. Python sera un bon choix pour écrire ce programme.

XVI-A - Un générateur de signal sinusoïdal simple

Voici un petit programme Python destiné à remplir un tableau avec des valeurs représentant les ordonnées des points d’une courbe sinusoïdale. Chacune de ces valeurs consiste en un entier non signé codé sur 8 bits (octet), variant donc théoriquement de 0 à 255 (256 valeurs), grâce à un décalage positif de 128. Ça fonctionnera donc parfaitement avec un convertisseur numérique-analogique (CNA) unipolaire 8 bits.

# Création d’une table d’onde en Python 3.3 pour un tableau de données en langage C utilisant des octets non signés
# Tableau à 256 entrées, plage d’amplitude 0-255
# Note : renommer le tableau en fonction de l’onde générée

import math

fn = input("Entrer le nom du fichier tableau à créer: ")
#Pour les utilisateurs de Python 2.x, remplacer la ligne ci-dessus par la ligne ci-dessous
#fn = raw_input("Entrer le nom du fichier tableau à créer: ")
fil = open( fn, "w+" )
fil.write( "unsigned char sinetable[256]={" )

for x in range( 256 ):

# Chaque donnée représente 1/256ème du cycle, la valeur de crête est 127
f = 127.0 * math.sin( 6.28319 * x / 256.0 )

# Conversion en entier et transposition de la courbe vers des valeurs positives
v = int( round( f, 0 ) ) + 128

# Empêche de sortir de l’intervalle valide au cas où
if v > 255:
v = 255
if v < 0:
v = 0

if x < 255:
fil.write( str(v) + ", " )
else:
fil.write( str(v) + "};" )

fil.close()

NDLR : le calcul de f fournit des valeurs arrondies comprises entre -127 et +127, ce qui, en comptant la valeur « 0 », produit en fait 255 valeurs. Le convertisseur numérique-analogique ne pouvant pas traiter les valeurs négatives, on ajoute 128 pour transposer la courbe vers des valeurs comprises entre 1 et 255.

Pour générer ce tableau, il nous suffit d’exécuter le programme ci-dessus, d’entrer un nom de fichier valide à la demande et d’attendre une seconde l’achèvement du processus. Nous n’avons plus alors qu’à ouvrir le fichier ainsi créé dans un éditeur de texte pour copier ensuite son contenu dans l’éditeur de l’EDI Arduino. Ce programme peut être utilisé en tant que programme générique pour créer, si besoin, d’autres tables d’onde.

NDLR : si vous êtes sous GNU-Linux, pensez à ajouter la ligne suivante au début de votre script :

#!/usr/bin/python3

Maintenant que nous disposons des valeurs représentatives de notre onde, quel type de convertisseur numérique-analogique allons-nous utiliser ? Naturellement, nous pourrions utiliser un convertisseur du commerce, mais nous pouvons également obtenir des résultats exploitables en utilisant un simple réseau en échelle de type R-2R comme celui de la figure ci-dessous. La valeur de R pourrait se situer aux alentours de 10 kΩ, ce qui entraîne naturellement une valeur de 20 kΩ pour les résistances 2R. Bien que la tolérance des valeurs des résistances et les fluctuations de la tension de sortie entraînent un manque de linéarité de ce dispositif, les résultats obtenus sur des données codées sur 8 bits sont suffisants dans un premier temps.

Réseau en échelle R-2R

Choisissez une valeur appropriée pour R et réalisez le réseau en échelle. Connectez le réseau au port D de l’Arduino comme indiqué sur la figure en faisant correspondre Bit0 avec la broche 0 (LSB) jusqu’à Bit7 avec la broche 7 (MSB). La sortie analogique devrait être analysée avec un oscilloscope. (NDLR : voir le tableau en annexe pour les correspondances entre les broches de l’Arduino UNO et les ports de l’ATmega 328P.)

Le code destiné à générer l’onde sinusoïdale est assez simple : il est en fait très succinct. La plus grosse partie est occupée par le tableau de données. Voici la première version :

unsigned char sinetable[256]={128, 131, 134, 137, 140, 144, 147, 150, 153, 156, 159, 162, 165, 168, 171, 174, 177, 179, 182, 185, 188, 191, 193, 196, 199, 201, 204, 206, 209, 211, 213, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 235, 237, 239, 240, 241, 243, 244, 245, 246, 248, 249, 250, 250, 251, 252, 253, 253, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 253, 253, 252, 251, 250, 250, 249, 248, 246, 245, 244, 243, 241, 240, 239, 237, 235, 234, 232, 230, 228, 226, 224, 222, 220, 218, 216, 213, 211, 209, 206, 204, 201, 199, 196, 193, 191, 188, 185, 182, 179, 177, 174, 171, 168, 165, 162, 159, 156, 153, 150, 147, 144, 140, 137, 134, 131, 128, 125, 122, 119, 116, 112, 109, 106, 103, 100, 97, 94, 91, 88, 85, 82, 79, 77, 74, 71, 68, 65, 63, 60, 57, 55, 52, 50, 47, 45, 43, 40, 38, 36, 34, 32, 30, 28, 26, 24, 22, 21, 19, 17, 16, 15, 13, 12, 11, 10, 8, 7, 6, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 6, 6, 7, 8, 10, 11, 12, 13, 15, 16, 17, 19, 21, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 43, 45, 47, 50, 52, 55, 57, 60, 63, 65, 68, 71, 74, 77, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 116, 119, 122, 125};

void setup()
{
// Configurer tout le port D en sortie
DDRD = 0xff;
}
void loop()
{
unsigned char u=0;
unsigned char *p;

p = sinetable;

// Boucle indéfiniment
while( 1 )
{
// Écrit la donnée et introduit l’intervalle d’échantillonnage
PORTD = p[u++];
delayMicroseconds(100);
}
}

Ce code commence par configurer le port D en sortie. La fonction loop() est une boucle while() infinie. Il est intéressant de noter que cette fonction n’a pas l’occasion de boucler, dans la mesure où une fonction while() infinie également (while(1)) empêche le programme principal de continuer. Le contenu du programme principal pourrait tout aussi bien être placé dans la fonction setup(), cela fonctionnerait parfaitement. Le fait de mettre ce code dans la section loop() permet de séparer le programme proprement dit des initialisations, et est de plus cohérent avec la structure d’un sketch Arduino.

Un pointeur est défini et pointe sur le début du tableau. Ce n’est techniquement pas nécessaire ici, car nous pourrions tout aussi bien utiliser sinetable[]. La raison de ce choix va devenir évidente dans un instant. La première valeur du tableau est copiée sur le port D. Le réseau en échelle transforme la combinaison des huit tensions 0 ou 5 volts résultantes en une tension continue unique. Il est intéressant de noter l’efficacité de l’écriture « en bloc » d’une valeur sur le port D comparée à l’écriture séquentielle de la même valeur à l’aide de la fonction orientée bit digitalWrite() examinée plus tôt. Écrire les bits un par un sera source de désordre car cela entraînera la génération de sept tensions successives différentes en sortie du réseau en échelle avant d’aboutir à la bonne tension quand les huit bits seront écrits. Quoi qu’il en soit, après écriture de la valeur, le programme fait une pause de 100 µs : c’est la période d’échantillonnage. Après cette pause, la valeur suivante contenue dans le tableau est envoyée à son tour vers le port D et le processus recommence. Abstraction faite de la durée de la boucle et de celle de l’accès/écriture, le processus complet de lecture du tableau nécessite 256 processus unitaires de 100 µs soit 25,6 ms, ce qui donne une fréquence de sortie d’approximativement 39 Hz.

Saisissez le code et téléversez-le sur la carte. Connectez un oscilloscope sur la sortie analogique Analog Output du réseau R-2R. Celui-ci devrait afficher une sinusoïde d’une fréquence approximative de 39 Hz, d’une amplitude d’environ 5 volts de crête à crête, décalée de 2,5 volts. Étirez la base de temps pour analyser la courbe. Les marches d’escalier induites par le réseau en échelle devraient être visibles, ainsi que la pause de 100 µs à chaque étape.

On peut augmenter ou diminuer la fréquence en modifiant la valeur fournie à la fonction delayMicroseconds(). Essayez une valeur plus élevée pour voir ce que ça donne. 10 µs devrait générer une fréquence d’environ 390 Hz. Pour cette fréquence, mesurez le THD (taux de distorsion harmonique) résiduel de la sinusoïde. En utilisant des résistances à 5 %, le THD devrait descendre en dessous de 1 %. Une partie de cette distorsion harmonique est due à la tolérance dans la valeur des résistances ainsi qu’à des valeurs légèrement différentes de la tension de sortie présente sur chacune des broches du port D.

Différentes formes d’onde peuvent être générées et incorporées au programme, la seule modification à effectuer dans celui-ci étant au niveau de l’affectation du pointeur p, lequel devra pointer sur d’autres tableaux. Vous pourriez par exemple dupliquer le tableau actuel en modifiant son nom, disons wackysinetable[], et en modifier quelques valeurs d’une quantité suffisante pour que ce soit visible à l’oscilloscope. Pointez sur la nouvelle table en faisant p= wackysinetable; et testez le résultat. Si vous disposez d’un petit amplificateur, vous pourriez envisager de le connecter à la sortie du réseau en échelle pour écouter sur un casque le rendu de l’onde nouvellement modifiée. Vous avez devant vous des heures d’expérimentations distrayantes ! Appelez maintenant votre famille, vos amis, rameutez les voisins et lâchez les chiens : tous conviendront que c’est une meilleure façon de se distraire que de s’abrutir devant la télé.

XVI-B - Changement de forme d’onde

Bien qu’il soit amusant de changer manuellement le délai (donc la période et par conséquent la fréquence), d’éditer manuellement les tableaux et d’écouter les résultats obtenus, il serait plus utile de pouvoir contrôler ces paramètres à l’aide de commandes externes, comme celles présentes sur les générateurs de fonctions standards qu’on trouve dans les laboratoires.

Comment fournir à l’utilisateur un contrôle sur la forme de l’onde ? Beaucoup de générateurs de fonctions de laboratoire disposent de barrettes de boutons-poussoirs mutuellement exclusifs, c’est-à-dire qu’un seul bouton peut être enfoncé à un moment donné : toute pression sur un autre bouton libérera le bouton précédemment enclenché. Cela offre un bon retour visuel et tactile. Une solution plus simple et moins coûteuse consiste à utiliser un unique bouton permettant de se déplacer séquentiellement parmi les choix offerts, en revenant sur le premier choix en fin de séquence. C’est très efficace quand le nombre de choix est relativement restreint. Nous pourrions coupler ce dispositif et une série de LED pour visualiser le choix effectué. En arrière-plan, une unique variable mémorisera ce choix et sera mise à jour à chaque utilisation de ce bouton. Ce bouton devrait disposer d’un dispositif anti-rebond matériel car un anti-rebond logiciel prendra trop de temps. Nous pouvons vérifier le choix en cours au début de chaque cycle. Ceci offre potentiellement deux avantages par rapport à une vérification effectuée après écriture sur le port :

- diminution des traitements à effectuer ;

- si la configuration des tableaux est correcte, nous pouvons garantir que la commutation entre deux formes d’onde aura toujours lieu lors d’une variation positive de la tension au passage du 0 volt, évitant des régimes transitoires potentiellement dangereux.

Traitons le cas avec les trois ondes suivantes : sinusoïdale, carrée et rampe. Nous aurons besoin de trois LED, une par onde, pour afficher la sélection en cours, et donc de trois broches configurées en sortie. Nous aurons de plus besoin d’une broche configurée en entrée pour le commutateur d’ondes. Bien que la variable mémorisant le choix puisse se contenter de stocker les valeurs 1, 2 ou 3, par exemple, pour représenter chacune des ondes possibles, il sera plus judicieux de s’épargner du travail en faisant coïncider ces valeurs avec les adresses des broches sur le port utilisé. De cette manière, la même variable servira à sélectionner l’onde et la LED correspondante. Nous utiliserons ici quatre des huit broches reliées au port B :

- broche 9 : port B bit 1 pour le commutateur ;

- broches 10, 11 et 12: port B bits 2, 3 et 4 pour les LED correspondant respectivement aux ondes sinusoïdale, carrée et rampe.

Nous pourrions utiliser ce qui suit :

// Tout sur le port B
#define WAVEBIT        0x02
#define SINEWAVE        0x04
#define SQUAREWAVE                0x08
#define RAMPWAVE        0x10

Nous devons alors modifier la fonction setup() comme suit :

// Configure B.2:4 en sortie
DDRB |= (SINEWAVE | SQUAREWAVE | RAMPWAVE);

// Configure B.1 en entrée, active le pull-up
DDRB &= (~WAVEBIT);
PORTB |= WAVEBIT;

Dans la fonction loop(), nous aurons besoin de nouvelles variables :

unsigned char wavechoice=SINEWAVE;
unsigned char waveswitch=PINB & WAVEBIT, currentwaveswitch=PINB & WAVEBIT;

wavechoice est la variable qui mémorise l’onde courante choisie par l’utilisateur : elle est initialisée à SINEWAVE (onde sinusoïdale). Il est important de garder à l’esprit que nous changerons d’onde lors d’une transition (changement d’état) du bouton-poussoir et non sur son état « haut » ou « bas » en lui-même. Il est donc nécessaire de connaître son état antérieur aussi bien que son état courant. Les variables waveswitch et currentwaveswitch sont utilisées pour mémoriser ces deux états : pressé ou relâché. Plutôt que de les positionner à 1/0 (true/false), on utilisera directement la valeur actuelle du bit correspondant. Cela rendra le code un peu plus transparent. Aussi, avant d’entrer dans la boucle while(), il nous faudra positionner le pointeur de tableau sur le tableau par défaut.

// Sélectionne la sinusoïde par défaut et allume la LED correspondante à cette onde
PORTB |= wavechoice;
p = sinetable;

Comme la variable wavechoice stocke l’état des bits du port correspondant aux LED, il suffit de la copier sur le port de sortie pour synchroniser les LED (souvenez-vous que tous les autres bits sont mis par défaut à zéro lors du reset) : simple et de bon goût.

Jetons maintenant un coup d’œil sur le processus de commutation. Nous devons d’abord déterminer l’état courant :

currentwaveswitch = PINB & WAVEBIT;

La commutation n’aura lieu qu’à l’appui du bouton (c’est-à-dire lors de la transition haut/bas). Notez que si vous utilisez un circuit anti-rebond matériel inverseur, la logique devra être inversée elle aussi (supprimer le « not »).

if( !currentwaveswitch ) // Activé (bas)
{
if( currentwaveswitch != waveswitch ) // Transition
{

Nous vérifions alors l’état courant de la variable wavechoice et bouclons parmi les valeurs valides, en mettant à jour le pointeur de tableau par la même occasion :

if( wavechoice == SINEWAVE )
{
wavechoice = SQUAREWAVE;
p = squaretable;
}
else
{
if( wavechoice == SQUAREWAVE )
{
wavechoice = RAMPWAVE;
p = ramptable;
}
else
{
wavechoice = SINEWAVE;
p = sinetable;
}
}

On éteint toutes les LED et on applique le nouveau choix :

// Éteint toutes les LED
PORTB &= (~(SINEWAVE| SQUAREWAVE| RAMPWAVE));

// Allume la LED sélectionnée
PORTB |= wavechoice;
}
}

Enfin, on met à jour la variable waveswitch afin que la logique de détection de transition fonctionne correctement :

waveswitch = currentwaveswitch;

Tout ce qui reste à faire est d’insérer cette portion de code dans une clause if() afin de n’effectuer le contrôle du commutateur qu’en début de tableau. Ceci se produit quand la variable index u est égale à zéro.

if( !u )
{
// Gestion du commutateur d’ondes
}

Créez deux nouveaux tableaux pour les formes d’onde carrée et rampe puis ajoutez le code ci-dessus. Câblez également les trois LED ainsi que le commutateur de forme d’onde. Téléversez le sketch et testez l’utilisation à l’aide d’un oscilloscope. Le programme Python vu plus haut pourra être utilisé pour créer les nouveaux tableaux :

- le tableau de l’onde carrée contiendra 128 entrées à 255 suivies de 128 entrées à 0 ;

- le tableau de l’onde rampe contiendra 256 entrées commençant à 0 et se terminant à 255, chacune des entrées valant la précédente augmentée de 1, soit [0, 1, 2, 3, …/… , 243, 244, 255].

XVI-C - Changement de fréquence

Permettre à l’utilisateur de changer de forme d’onde est certainement utile, mais si l’on pouvait également lui permettre de modifier la fréquence, ce serait parfait. De prime abord, il peut sembler classique d’utiliser un potentiomètre pour modifier la fréquence. Utilisé en diviseur de tension, il fournit une tension variable que l’on peut utiliser avec la fonction analogRead() dont le résultat permettra de modifier le paramètre fourni à la fonction delayMicroseconds(). Le problème qui se pose avec cette solution est le même que celui qui se posait avec le calcul en temps réel de la sinusoïde : c’est trop long. Le temps d’acquisition du convertisseur analogique-numérique n’est pas négligeable au regard de la valeur de temporisation et influera fortement sur la fréquence de l’onde créée. Par contre, le test de l’état d’un bouton-poussoir est un processus très rapide. Ajouter un second bouton-poussoir identique à celui utilisé pour changer de forme d’onde ne pose aucun problème. Chaque pression sur ce bouton incrémentera une variable globale qui sera utilisée par la fonction delayMicroseconds() comme suit :

unsigned int g_period=100; // Initialisé à 100 µs

// Écrit la donnée et introduit l’intervalle d’échantillonnage
PORTD = p[u++];
delayMicroseconds( g_period );

À chaque pression sur le commutateur de fréquence, on augmentera la temporisation comme suit :

BumpPeriod( 1 );

La fonction BumpPeriod() masque la variable globale et permet de délimiter la plage d’utilisation en bouclant une fois la limite fixée atteinte.

void BumpPeriod( char c )
{
g_period += c;

// Boucle sur la plage de valeurs autorisées, c peut être négatif
if( g_period < 1 )    g_period = 200;
if( g_period > 200 )    g_period = 1;
}

Une temporisation de 1 µs entraîne une période de 256 µs (si on néglige la durée des processus) pour une fréquence maximale d’environ 3,9 kHz. De la même manière, une temporisation de 200 µs produira une fréquence minimale d’à peu près 20 Hz. Retenez que g_period est une variable globale et que, comme telle, elle est définie en dehors et avant toute fonction. Dans cette mesure, toutes les fonctions peuvent y avoir accès.

Ce code fonctionne et nous pourrions l’utiliser tel quel pour gérer le commutateur de fréquences, mais il pose un problème pratique. Chaque pression sur le bouton incrémentera la fréquence d’échantillonnage de seulement 1 µs. Nous disposons potentiellement d’une plage de 200 valeurs à parcourir ce qui, potentiellement, peut amener à appuyer 200 fois sur le bouton si on veut reculer d’une unité. Ce n’est probablement pas ce que souhaite faire l’utilisateur.

Une solution courante à ce type de problème réside dans l’utilisation d’un bouton à incrémentation automatique lorsqu’on maintient la pression suffisamment longtemps. Ce temps de maintien peut être de l’ordre de 500 ms. Il ne faut pas que l’incrémentation soit trop rapide, au risque de ne pas pouvoir incrémenter d’une seule unité : une période de 100 ms fera l’affaire. C’est l’équivalent de 10 pressions par seconde (ou le résultat de trop d’expressos). À cette vitesse, on parcourt la plage en 20 s juste en laissant le doigt appuyé sur le bouton. L’ergonomie peut encore être améliorée en ajoutant à côté un bouton effectuant l’opération inverse (décrémentation). Le code de ce bouton sera exactement le même, à ceci près que le paramètre fourni à la fonction BumpPeriod() sera négatif. Si la plage était plus étendue, on pourrait considérer l’ajout d’un bouton effectuant une incrémentation (ou une décrémentation) de 10 unités au lieu d’une, par exemple, afin d’approcher plus rapidement la période (donc la fréquence) souhaitée. Il suffirait pour cela de donner la valeur 10 (ou -10) au paramètre fourni à la fonction BumpPeriod(). Une autre solution, classique également, consiste à modifier le facteur d’incrémentation en fonction du temps de maintien du bouton. Par exemple, l’incrémentation pourrait être de une unité toutes les 100 ms quand le bouton est maintenu appuyé entre 0,5 et 2 secondes, puis passer à 10 unités si le maintien du bouton se prolonge au-delà des 2 secondes.

// Tout sur le port B
#define FREQBIT        0x01
#define WAVEBIT        0x02
#define SINEWAVE        0x04
#define SQUAREWAVE                0x08
#define RAMPWAVE        0x10

void BumpPeriod( char c );
unsigned int g_period=100; // Initialisée à 100 µs

void setup()
{
// Configure tout le port D en sortie
DDRD = 0xff;

// Configure B.2:4 en sortie
DDRB |= (SINEWAVE | SQUAREWAVE | RAMPWAVE);

// Configure B.0:1 en entrée, active le pull-up
DDRB &= (~(FREQBIT | WAVEBIT));
PORTB |= (FREQBIT | WAVEBIT);
}

La commutation des fréquences est traitée de la même manière que celle des formes d’onde, à ceci près qu’ici nous devons en plus gérer le temps. En effet, la fonctionnalité auto-incrément nécessite de détecter les 500 ms de maintien du bouton pour démarrer puis, à partir de là, de détecter des écoulements successifs de 100 ms pour effectuer les incréments. Par conséquent, en plus des variables destinées à mémoriser l’état actuel et l’état précédent du commutateur, nous aurons besoin de variables pour mémoriser la transition, c’est-à-dire le moment de l’appui sur le bouton (transitiontime), l’instant présent (currenttime) et la consigne, c’est-à-dire la durée de maintien avant déclenchement (bumptime).

unsigned char freqswitch=PINB & FREQBIT, currentfreqswitch=PINB & FREQBIT;
unsigned long transitiontime, currenttime, bumptime=500;

Les variables « xxxtime » doivent toutes être des entiers longs non signés pour pouvoir être utilisées par la fonction millis().

Voici comment fonctionne cet algorithme :

- on récupère l’état courant du commutateur : on ne fait rien tant que le commutateur n’est pas à l’état « bas » ;

- quand il est à l’état bas, on mémorise l’instant présent et on regarde si son état antérieur était l’état « haut » ;

- si oui, il s’agit d’une transition : on mémorise alors le moment de cette transition et on incrémente la période ;

- on en profite pour initialiser la consigne (bumptime) avec la valeur de maintien souhaitée (500 ms) avant le démarrage de l’auto-incrémentation.

// Vérifie si le commutateur de fréquence est activé
currentfreqswitch = PINB & FREQBIT;

if( !currentfreqswitch ) // Activé (bas)
{
currenttime = millis();
if( currentfreqswitch != freqswitch ) // Transition
{
transitiontime = currenttime;
BumpPeriod( 1 );
bumptime = 500;    // Nombre de millisecondes  avant une autoincrémentation
}

Si les états du commutateur (précédent et courant) sont les mêmes, c’est que le commutateur est maintenu dans son état courant. Avant de commencer l’incrémentation automatique, il faut savoir si la durée de ce maintien a respecté la consigne (bumptime, ou 500 ms). En soustrayant le moment de la transition de l’instant courant, nous saurons combien de temps le bouton est resté pressé. Si ce temps est au moins égal à la consigne, on peut incrémenter la période. Comme il ne faut pas réincrémenter la période pendant les prochaines 100 ms, il faudra également incrémenter la consigne (bumptime) de 100 ms.

else    // Si maintenu, la durée de maintien est-elle suffisante?
{
if( currenttime-transitiontime > bumptime )
{
BumpPeriod( 1 );
bumptime += 100;
}
}
}

Enfin, et quel que soit l’état du commutateur, on met à jour la variable stockant l’état antérieur du commutateur (freqswitch) avec la valeur contenue dans la variable stockant son état actuel (currentfreqswitch) pour détecter une nouvelle transition.

freqswitch = currentfreqswitch;

Notez qu’une fois le bouton relâché, il est ignoré jusqu’à sa prochaine utilisation. À ce moment-là, une nouvelle transition sera détectée et la consigne (bumptime) sera réinitialisée à 500 ms afin que le processus puisse se répéter comme décrit ci-dessus. Si le bouton est relâché avant que la consigne ne soit atteinte, aucune auto-incrémentation n’aura lieu, et la pression suivante sur le bouton réinitialisera la transition et la consigne.

Comme pour la commutation des formes d’onde, il est important de noter que si vous utilisez un circuit anti-rebond matériel inverseur, le « switch selected » logique sera également inversé (supprimez le « not »).

Le code du processus de commutation de fréquence doit être placé au même endroit que celui concernant le processus de commutation de forme d’onde (dans le même bloc), soit immédiatement avant, soit immédiatement après. Cette partie de la fonction loop() devrait donner quelque chose comme :

// Sélectionne la sinusoïde par défaut et allume la LED correspondante à cette onde

while( 1 )
{
// Ne vérifie l’état du commutateur qu’au début de chaque cycle
if( !u )
{
// Gestion du commutateur d’ondes
// Gestion du commutateur de fréquences
}

// Écrit la donnée et introduit l’intervalle d’échantillonnage
PORTD = p[u++];
delayMicroseconds( g_period );
}
}

Saisissez et téléversez ce code puis testez-le. Si vous vous en sentez le courage, vous pouvez de la même manière implémenter un bouton de décrémentations.

// Tout sur le port B
#define FREQBIT     0x01
#define WAVEBIT     0x02
#define SINEWAVE    0x04
#define SQUAREWAVE  0x08
#define RAMPWAVE    0x10

void BumpPeriod( char c );

unsigned char sinetable[256]={128, 131, 134, 137, 140, 144, 147, 150, 153, 156, 159, 162, 165, 168, 171, 174, 177, 179, 182, 185, 188, 191, 193, 196, 199, 201, 204, 206, 209, 211, 213, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 235, 237, 239, 240, 241, 243, 244, 245, 246, 248, 249, 250, 250, 251, 252, 253, 253, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 253, 253, 252, 251, 250, 250, 249, 248, 246, 245, 244, 243, 241, 240, 239, 237, 235, 234, 232, 230, 228, 226, 224, 222, 220, 218, 216, 213, 211, 209, 206, 204, 201, 199, 196, 193, 191, 188, 185, 182, 179, 177, 174, 171, 168, 165, 162, 159, 156, 153, 150, 147, 144, 140, 137, 134, 131, 128, 125, 122, 119, 116, 112, 109, 106, 103, 100, 97, 94, 91, 88, 85, 82, 79, 77, 74, 71, 68, 65, 63, 60, 57, 55, 52, 50, 47, 45, 43, 40, 38, 36, 34, 32, 30, 28, 26, 24, 22, 21, 19, 17, 16, 15, 13, 12, 11, 10, 8, 7, 6, 6, 5, 4, 3, 3, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 6, 6, 7, 8, 10, 11, 12, 13, 15, 16, 17, 19, 21, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 43, 45, 47, 50, 52, 55, 57, 60, 63, 65, 68, 71, 74, 77, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 116, 119, 122, 125};
unsigned char squaretable[256]={0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255};
unsigned char ramptable[256]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253,  255};
unsigned int g_period=100; // Initialisé à 100 µs

void setup()
{
// Configure B.2:4 en sortie
DDRB |= (SINEWAVE | SQUAREWAVE | RAMPWAVE);
// Configure B.0:1 en entrée, active le pull-up
DDRB &= (~(FREQBIT | WAVEBIT));
PORTB |= (FREQBIT | WAVEBIT);
// Configure tout le port D en sortie
DDRD = 0xff;
}

void loop()
{
unsigned char u=0;
unsigned char *p;
unsigned char wavechoice=SINEWAVE;
unsigned char waveswitch=PINB & WAVEBIT, currentwaveswitch=PINB & WAVEBIT;
unsigned int g_period=100; // Initialisé à 100 µs
unsigned char freqswitch=PINB & FREQBIT, currentfreqswitch=PINB & FREQBIT;
unsigned long transitiontime, currenttime, bumptime=500;
// Sélectionne la sinusoïde par défaut et allume la LED correspondante à cette onde
PORTB |= wavechoice;
p = sinetable;
// Boucle indéfiniment
while( 1 )
{
if( !u )
{
currentwaveswitch = PINB & WAVEBIT;
if( !currentwaveswitch ) // Activé (bas)
{
if( currentwaveswitch != waveswitch ) // Transition
{
if( wavechoice == SINEWAVE )
{
wavechoice = SQUAREWAVE;
p = squaretable;
}
else
{
if( wavechoice == SQUAREWAVE )
{
wavechoice = RAMPWAVE;
p = ramptable;
}
else
{
wavechoice = SINEWAVE;
p = sinetable;
}
}
// Éteint toutes les LED
PORTB &= (~(SINEWAVE| SQUAREWAVE| RAMPWAVE));

// Allume la LED sélectionnée
PORTB |= wavechoice;
}
}
waveswitch = currentwaveswitch;
// Vérifie si le commutateur de fréquence est activé
currentfreqswitch = PINB & FREQBIT;
if( !currentfreqswitch ) // Activé (bas)
{
currenttime = millis();
if( currentfreqswitch != freqswitch ) // Transition
{
transitiontime = currenttime;
BumpPeriod( 1 );
bumptime = 500; // Nombre de millisecondes  avant une autoincrémentation
}
else  // Si maintenu, la durée de maintien est-elle suffisante?
{
if( currenttime-transitiontime > bumptime )
{
BumpPeriod( 1 );
bumptime += 100;
}
}
}
freqswitch = currentfreqswitch;
}
// Écrit la donnée et introduit l’intervalle d’échantillonnage
PORTD = p[u++];
delayMicroseconds( g_period );
}

} // Fin de void Loop()

void BumpPeriod( char c )
{
g_period += c;

// Boucle sur la plage de valeurs autorisées, c peut être négatif
if( g_period < 1 )  g_period = 200;
if( g_period > 200 )  g_period = 1;
}

XVI-D - Utilisation d’un convertisseur numérique-analogique intégré

Si le réseau en échelle R-2R est tout à fait utilisable pour cette application, on ne peut certainement pas dire qu’il soit très précis. Si une résolution supérieure en bits est souhaitée, l’accumulation d’erreurs dues à l’imprécision des résistances et des tensions pourrait rendre le bit de poids faible LSB non significatif. De plus, pour un fonctionnement correct, le réseau R-2R ne doit pas être trop sollicité. Il serait intéressant de rendre la sortie insensible à une sollicitation importante et d’augmenter la précision du système. Ce but peut être atteint par une modification raisonnable du câblage, avec l’utilisation d’un simple convertisseur numérique-analogique (CNA) à entrée parallèle 8 bits, alimentant un buffer de sortie à amplificateur opérationnel. Un bon exemple de ce produit est le DAC0808 de Texas Instrument dont vous pouvez télécharger la fiche technique ici.

Le DAC8080 est un convertisseur numérique-analogique (CNA, DAC en anglais) à entrées parallèles 8 bits, dont la sortie est modulée en courant (intensité). Son utilisation nécessite un minimum de composants externes. Les huit broches de sortie du microcontrôleur sont connectées directement aux huit broches d’entrée du CNA (en ce qui nous concerne, la totalité du réseau R-2R peut être supprimée). Comme le CNA utilise une sortie modulée en courant, il est naturel d’utiliser un amplificateur opérationnel en mode conversion courant/tension (source de tension contrôlée en courant) pour fournir une tension de sortie stable. Un bon exemple est proposé dans la fiche technique sous la référence « Typical application » (voir figure ci‑dessous).

L’amplificateur opérationnel LF351 peut être remplacé par le circuit LF411 plus récent. Notez également que l’alimentation du circuit est absente du schéma et qu’un condensateur de couplage peut être requis en sortie pour compenser un éventuel décalage de tension. Câblez cette variante et testez-la. Portez la fréquence à 390 Hz avec une onde sinusoïdale et mesurez le THD résiduel. Comparez-le avec celui mesuré précédemment avec le réseau R-2R.

Application typique du DAC8080©National Semiconductor

XVI-E - Conclusion

En imaginant qu’un bouton de décrémentation ait été ajouté à cette réalisation, la totalité des ports B et D est utilisée sur l’Arduino UNO, et il ne reste que six broches disponibles, les broches analogiques (de A0 à A5). Si d’autres ports étaient disponibles, l’affichage de la fréquence en cours pourrait être une option intéressante. Cela peut être réalisé avec sept broches supplémentaires (ou huit si le point décimal est requis) pour piloter les sept segments d’un afficheur, auxquelles s’ajouteront trois ou quatre broches (en fonction de la précision désirée pour l’affichage de la fréquence) destinées à réaliser le multiplexage. Plusieurs cartes de la famille Arduino, comme la carte Arduino MEGA, sont capables de gérer ça.

La technique utilisée ici pour changer la fréquence est connue sous le nom de « fréquence d’échantillonnage variable ». Elle est relativement simple mais offre quelques inconvénients, dont le moindre n’est pas la difficulté à filtrer la sortie (processus destiné à supprimer le phénomène « marches d’escalier » affectant l’onde, dont on n’a pas parlé).

Une autre technique consiste à utiliser une fréquence d’échantillonnage élevée et constante. La fréquence est modifiée lors de la génération de l’onde désirée par interpolation dans un tableau plus grand. La charge de calcul est un peu plus élevée, mais même une simple interpolation linéaire peut aboutir à un résultat de grande qualité sans beaucoup de temps de traitement supplémentaire.

Pour finir, ce type d’application peut bénéficier de l’utilisation d’interruptions temporisées. Elles peuvent être vues comme des bribes de code s’exécutant à des intervalles de temps bien définis, contrôlés dans ce cas par les timers/counters embarqués. Le timer sera utilisé pour déclencher une interruption logicielle, afin d’éviter d’avoir recours à la fonction bloquante delayMicroseconds(). La routine d’interruption n’aura guère d’autre objet que de copier la valeur suivante du tableau sur le port de sortie. Cette technique permettra la génération d’une onde régulière et libérera la fonction loop() pour scruter l’état des commutateurs et effectuer les gestions associées.

XVI-F - Annexe

Relations entre les broches de l’Arduino UNO et les registres de l’ATmega 328P

Broche

Port

Alias

Adr.Reg.

Bit

Alias

Conf.I/O

Adr.Reg.

Alias

Input

Adr.Reg

Alias

0

D

PORTD

0x2B

0

PORTD0

DDRD

0x2A

DDD0

PIND

0x29

PIND0

1

1

PORTD1

DDD1

PIND1

2

2

PORTD2

DDD2

PIND2

3

3

PORTD3

DDD3

PIND3

4

4

PORTD4

DDD4

PIND4

5

5

PORTD5

DDD5

PIND5

6

6

PORTD6

DDD6

PIND6

7

7

PORTD7

DDD7

PIND7

8

B

PORTB

0x25

0

PORTB0

DDRB

0x24

DDB0

PINB

0x23

PINB0

9

1

PORTB1

DDB1

PINB1

10

2

PORTB2

DDB2

PINB2

11

3

PORTB3

DDB3

PINB3

12

4

PORTB4

DDB4

PINB4

13

5

PORTB5

DDB5

PINB5

A0

C

PORTC

0x28

0

PORTC0

DDRC

0x27

DDC0

PINC

0x26

PINC0

A1

1

PORTC1

DDC1

PINC1

A2

2

PORTC2

DDC2

PINC2

A3

3

PORTC3

DDC3

PINC3

A4

4

PORTC4

DDC4

PINC4

A5

5

PORTC5

DDC5

PINC5

Correspondances Arduino-ATmega 328P

Les deux programmes Python 3 pour générer les tables d’ondes « carrée et rampe » :

#!/usr/bin/python3

# Création de la table d'ondes carrées, 256 entrées,
# amplitude variant de 0 à 255.

fn = input("Entrez le nom du fichier table d'onde à créer: ")
fil = open( fn, "w+" )

fil.write( "unsigned char squaretable[256]={" )

for x in range(128):
fil.write('0, ')
for x in range(127):
fil.write('255, ')
fil.write('255};')

fil.close()

#!/usr/bin/python3

# Création de la table d'ondes en rampe, 256 entrées,
# amplitude variant de 0 à 255.

fn = input("Entrez le nom du fichier table d'onde à créer: ")
fil = open( fn, "w+" )

fil.write( "unsigned char ramptable[256]={" )

for x in range(254):
fil.write(str(x) + ", ")
fil.write("255};")

fil.close()

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, genthial, naute et jacques_jean.

(1) Si l’œil humain ne fonctionnait pas comme ça, nous ne pourrions pas avoir l'impression de visualiser une animation fluide et continue en regardant la télévision ou un film au cinéma alors que la fréquence des trames vidéo est de 30 et 24 images par seconde respectivement. La fréquence élevée à laquelle défilent les images trompe notre cerveau en créant l'illusion d'un mouvement continu.

(2) Souvenez-vous, le courant qui traverse une inductance ne peut pas changer instantanément. Si le transistor se bloque, la tension aux bornes de l'inductance s'inverse et atteint des valeurs élevées, ce qui provoquera un pic de tension entre le collecteur et l'émetteur du transistor (lois de Kirchhoff). La diode (dite « diode de roue libre ») telle qu'elle est placée sur la figure permet de limiter cette surtension et donc de protéger le transistor.