Le texte équivalent pour Energia/MSP430G se trouve sous http://www.pyr.ch/coursera/LC5-msp.pdf
Les sources des exemples sont à disposition sous www.didel.com/coursera/LC5ino.zip
Un tableau de variables (array) correspond à une suites de positions mémoires de même dimension numérotées à partir de 0. La dimension du tableau ou table est le nombre d’éléments
int table [4];
réserve la place pour 4 éléments dont le numéro (index) est 0,1,2,3
On peut définir le contenu de ces éléments s’ils doivent être différents de 0 :
int table [4] = {57, -3, , 4}; // c’est mieux d’écrire le 0 pour l’index 2!
Si le type est char
, le tableau peut être formé de caractères ASCII
char message[] = "hello";
On voit qu’il n’a pas été nécessaire de compter les lettres, le compilateur sait le faire et peut vous donner son résultat avec l’instruction sizeof (message)
.
On peut déclarer des tableaux multidimensionnels. A 2 dimensions, on visualise les données en ligne et colonne. Le premier index est la ligne, le second la colonne.
int table [3][2] = { {1,2},
{10,20},
{100,200}
};
Pour calculer la somme des nombres de la table précédente, il faut deux boucles On remarque que compactifier le tableau 2D sur une ligne n’est pas une bonne idée. |
//A511 Tableau 2D void setup() { Serial.begin(9600); } int table [3][2] = {{1,2},{10,20},{100,200}}; int result =0; void loop () { for (byte i=0; i<3; i++) { for (byte j=0; j<2; j++) { result += table [i][j]; } } Serial.println(result); while (1) {} } |
Comme pour les variables, le prefix La table pour un dé électronique par exemple dépend du câblage. Son écriture est facile et les erreurs faciles à corriger. |
|
On peut faire une table avec des numéro de pins Arduino. Ces numéros sont traités par une fonction assez complexe (section 4.2).
Pour initialiser nos 4 leds et la led 13, formons un tableau avec leurs numéros. Une boucle Exécuter 10000 fois ces instructions avec un |
//A521 Initialisation à la Arduino byte pinsOut [] = {4,5,6,7,13}; void setup () { for (byte i=0; i<sizeof(pinsOut); i++) { pinMode (pinsOut [i],1); } } void loop () { } |
Ce type de programmation est intéressant dans le monde Arduino ; il permet de passer d’une carte à l’autre sans se préoccuper du microcontrôleur. La correspondance entre le no de pin et le bit sur un port est faite par une fonction dans la librairie, d’où le choix “Type de carte” à sélectionner au début.
Avec un autre microcontrôleur que l’Atmega328, il faut pour programmer en C retrouver le schéma de câblage pour agir sur les bons registres. C’est 20 fois plus rapide (vérifiez, on a vu les instructions) mais ce n’est plus portable dans le monde Arduino, où c’est facile d’acheter un processeur plus rapide avec plus de mémoire.
Une mémoire tampon reçoit de l’information et livre cette informations dans le même ordre, mais pas à la même vitesse. On parle de premier entré-premier sorti (FIFO FirstInFirstOut), à ne pas confondre avec une pile (stack) qui est un LIFO (LastInFirstOut).
On a vu un exemple avec le terminal : les caractères tapés sont mémorisés dans un tampon et transférés à une vitesse différente sur l’écran.
Un tampon est un tableau. L’astuce est dans les index qui pointent l’information qui entre et l’information qui sort. On parle de tampon circulaire, car le bas de la zone mémoire continue en haut : si le pointeur arrive en bas, on le mets en haut. On a deux index, en face l’un de l’autre si le tampon est vide. On ne s’intéresse pas au nombre de mots en attente, mais on peut l’obtenir par différence circulaire.
Il faut déclarer un tableau tampon de longueur La fonction On peut écrire si On peut lire si |
|
Choisissons un exemple simple pour comprendre la synchronisation entre deux tâches asynchrones. On presse irregulièrement sur un poussoir. Chaque pression doit générer un clignotement de 1 seconde (500 ms allumé, 500 ms éteint). Une solution est de faire communiquer les 2 tâches non bloquantes par une variable globale Chaque pression augmente le compteur. Chaque clignotement complet décompte. La fonction |
|
Le clignotement non bloquant a été vu en 4.61. C’est un peu plus compliqué ici parce qu’il faut maintenant voir 3 états : allumé 0.5 s, éteint 0.5 s, terminé. Un On voudrait envoyer cnt sur le terminal. Où faut-il le faire ? Cela prend 5 ms, acceptable. |
//A531 Cligote asynchrone #include "LcDef.h" void setup() { LcSetup (); } int cnt=0 ; // variable globale void Pous1Edge () { volatile byte prevPous1; if (Pous1On && (prevPous1==0)) { cnt++; Led1Toggle; } prevPous1=Pous1On; } #define DurOn 1000/20 #define DurOff 1000/20 void CliLed2 () { // appelé toutes les 20ms 25+25 cycles byte etat; switch (etat) { static byte cntLed2; { case 0: Led2On; if (cntLed2++ > DurOn) { cntLed2 = 0; etat = 1; } break; case 1: Led2Off; if (cntLed2++ > DurOff) { cntLed2 = 0; etat = 2; } break; case 2: cnt--; etat = 2; break; } } void loop () { delay (20); Pous1Edge (); if (cnt>0) { CliLed2(); } } |
On a vu (3.31) comment moyenner les valeurs d’un capteur. La fréquence des mesures utilisables est divisée pas le nombre d’échantillons intervenant dans la moyenne. La moyenne glissante se calcule à chaque mesure. A l’initialisation, on fait comme si les dernières mesures étaient identiques à la première mesure. |
|
A défaut de capteur, introduisons des valeurs au terminal (voir 4.51) et affichons la moyenne glissante. Sans initialisation, on voit que les 3 premières moyennes sont fausses. Compléter le setup pour que la première moyenne soit égale à la première valeur entrée. On voit que les variables concernées ont déjà été déclarées avant le setup. Comment faire pour que cette initialisation soit faites dans la fonction ? Indication : ajouter un flag qui est à zéro au premier appel de |
//5.42 Moyenne glissante #include "LcDef.h" int taGlis [4]; byte iTag; // index à taGlis void setup() { LcSetup (); Serial.begin(9600); } // fonction GetMoyGlis (valeur); //Rend la moyenne des 4 valeurs précédentes int GetMoyGlis (int vv) { int mm = 0; taGlis [iTag++] = vv; // le ++ se fait après l’écriture if (iTag > 3) {iTag = 0; } for (byte i=0; i<4; i++) { mm += taGlis [i]; } return mm/4; } int valeur, moyenne; void loop() { while (Serial.available() > 0) { valeur = Serial.parseInt(); Serial.print(valeur); Serial.print(" "); moyenne = GetMoyGlis (valeur); Serial.println(moyenne); } } |
Dans sa forme la plus simple, un timer est un registre compteur que l’on peut lire et écrire comme une variable. Il compte à une fréquence qui est un diviseur de la fréquence du processeur. Le compteur, appelé TCNT
, a une longueur limitée, 8 ou 16 bits, et est associé à une bascule appelée TOVF
(Timer Overflow Flag).
Le timer compte sans cesse. La bascule |
|
On peut à chaque dépassement charger le compteurs à une valeur quelconque, pour que le TOVF
revienne plus vite. En chargeant 200, l’overflow aura lieu 56 impulsions plus tard. Mais attention, tout ne se fait pas simultanément. Le microcontrôleur doit surveiller TOVF
directement (ou par interruption, on verra plus tard). Quand il agit pour réinitialiser le timer, quelques impulsions de comptage ont passé, qu’il faut soustraire. On chargera 56-2 au lieu de 56 par exemple pour tenir compte de ce temps de réponse.
Ce timer 8 bits est piloté par 3 registres. On peut lire et écrire le compteur 8 bits Le registre |
|
La bascule est remise à zéro en écrivant un 1 dans le bit correspondant de TIFR2: bitSet(TIFR2,TOV2)
. Cela peut surprendre, mais le câblage des registres de flag est spécial : on ne peut par écrire dans TIFR2
, on peut seulement agir sur le reset de ses flags.
Clignotons de nouveau pour changer! Corriger les paramètres pour avoir comme prédiviseur Programmer une sirène en changeant la valeur initialisant le compteur-timer à chaque cycle. |
//A552 Clignote via le timer2 #include "LcDef.h" void setup () { LcSetup (); TCCR2A = 0; TCCR2B = 0b00000111; // clk/1024 16kHz 62,5us } int cntLed; void loop () { if ( TIFR2 & (1<<TOV2) ) { TCNT2 = -16 ; // pour 1 kHz bitSet (TIFR2,TOV2); Led1Toggle; HPToggle; if (cntLed++ > 500) { // pour période 1Hz cntLed = 0; Led2Toggle; } } } |
Une interruption ressemble à un appel de fonctions déclenché par un flag, lui-même activé par une entrée du microcontrôleur ou une condition sur un registre timer ou autre. C’est un peu plus compliqué parce que cela arrive n’importe quand dans le programme. Il faut sauver l’état des registres sur une pile et les rétablir en fin de routine d’interruption. La fonction (on utilise plus fréquemment le terme de routine) appelée par un signal d’interruption commence toujours par interdire les interruptions, et dans le cas des AVR, le flag qui a déclenché l’interruptions est remis à zéro automatiquement.
Un bit dans un registre, activé par la fonction Arduino sei();
permet d’autoriser les interruptions, cli();
permet de les ignorer quand le programme principal doit respecter des timings précis, ou que l’on change des conditions liées aux interruptions.
Il y a en général plusieurs sources d’interruption (timers, entrées du processeur, série, etc).
Chaque source a un flag, et un vecteur d’interruption propre, c’est à dire une adresse pour la routine à exécuter. Arduino a des noms réservés pour les routines d’interruption, par exemple ISR(TIMER2_OVF_vect) (InterruptServiceRoutine)
.
On voit sur la figure que toutes les demandes d’interruption vienne sur une porte OU et sont éventuellement bloquées pas le signal GIE
, activé par sei();
Les interruptions individuelles sont chacunes autorisée par un bit “interrupt enable” qui laisse passer la demande du flag actif. Pour le Timer2, TOIE2
et GIE
doivent être actifs pour que, au moment ou TOV2
passe à 1, la routine TIMER2_OVF_vect
soit appelée. Tous ces noms sont connus du compilateur, il y a dans les fichiers du dossier Arduino des milliers de #define
liés à tous les bits et registres du microcontrôleur.
Pour clignoter par interruption, les instructions de l’exemple 5.52 sont reportées dans la routine d’interruption du timer. Le programme principal ne fait plus rien. S’il clignote une autre led à la vieille mode, il est ralenti de ~5 microsecondes à chaque interruption, ceci toutes les millisecondes avec ce qui a été programmé, soit 0.5 % du temps. Que se passe-t-il si le programme principal et l’interruption clignotent la même led à la même fréquence ? Essayez. |
//A571.ino Clignote par interruption timer #include "LcDef.h" void setup () { LcSetup (); TCCR2A = 0; //default TCCR2B = 0b00000111; // clk/1024 TIMSK2 = 0b00000001; // TOIE2 sei(); } int cntLed; ISR (TIMER2_OVF_vect) { TCNT2 = 16; // pour 1 kHz if (cntLed++ > 500) { cntLed = 0; Led1Toggle; } } void loop () { Led2Toggle; delay (500); } |
Les timers ont une fonctionnalité très riche. On en reparlera dans le prochain chapitre.
Tous les microcontrôleurs peuvent créer une interruption si une ou plusieurs pins changent d’état. Des ou-exclusifs sont câblés sur des ports pour reconnaître un changement, ce qui facilite par exemple l’interface avec un clavier.
L’Atmega328 a deux entrées avec une logique sensible au changement, sur les pins 2 et 3 (P1 et P2). On retrouve un flag, un mask pour autoriser les interruptions et un registre de mode. Pour plus de détails, voir www.didel.com/C/ArduinoInterruptPins.pdf.
Arduino propose une fonction qui fait tout et que l’on a avantage à utiliser.
La fonction attachInterrupt(pin,function,mode)
a trois paramètres.
pin // 0 ou 1 selon la pin 2 ou 3 ( 3 ou 2 selon la carte)
function // le nom de la fonction appelée
mode // 0,1,2,3 ou LOW, CHANGE, RISING, FALLING
Ce qui est appelé pin n’est donc pas le numéro de la pin, mais le no du flag. La correspondance avec les pins dépends de l’AVR. Pour le 328, le paramètre 0 est associé à la pin 2, poussoir P1.
La fonction est le nom de la fonction à exécuter.
Le mode agit sur des inverseurs et ou-exclusif entre l’entrée et le flag d’interruption.
Clignotons en passant par les interruptions créées par P1, à l’instant ou l’on presse, c’est à dire quand le signal sur processeur passe de 1 à 0 (le 0 est actif sur le poussoir). Le mode est donc 3 ou L’interruption réagit dans la microseconde, le Toggle led aussi, donc s’il y a un nombre impair de rebonds avant le contact, il n’y aura pas de basculement. S’il y a un nombre impair de rebonds au relâchement, il y aura basculement. |
//A581 Toggle par interruption P1 #include "LcDef.h" void setup () { LcSetup (); attachInterrupt(0,CliUneFois,FALLING); } void CliUneFois() { Led1Toggle; } void loop () { } |
On voit que le poussoir a parfois des rebonds, qui doivent être très court (~5 µs).
Les microcontrôleurs ont des entrées analogiques multiplexées sur le port C dans le cas de l’Atmega328, donc sur les pins 14 à 19, connues aussi sous les noms A0
à A6
.
A l’intérieur du 328, il faut préparer plusieurs registres avant de lancer la conversion et attendre qu’elle soit finie. Arduino cache cette complexité avec la fonction analogRead ()
qui rend une valeur de 0 à 1023 (10 bits) pour une tension entre 0 et la tension d’alimentation.
Ce n’est pas nécessaire de déclarer la pin en entrée, Arduino force la pin en entrée et désactive la pull-up (sauf erreur) à chaque lecture. Cette lecture est bloquante (~120 µs).
Si vous avez un breadboard, enlevez le LearnCbot et prenez l’un des innombrables tutoriaux sur Arduino pour jouer avec un pot ou une LDR.
On peut stocker des bytes en EEPROM et les retrouver après coupure de courant. L’écriture est lente (10 ms) mais la lecture est aussi rapide que la lecture d’une variable.
Les instructions AVR 328 pour écrire en EEPROM sont compliquées, pour diminuer la probabilité d’une écriture intempestive si le programme déraille. Arduino propose une librairie facile à utiliser.
Le programme ci-contre initialise un contenu dans le setup. Commentez cette partie à la 2e exécution et coupez l’alimentation. La boucle demande de taper l’adresse 0 1 2..9 au terminal (suivi d’un retour à la ligne). Modification : afficher le contenu complet avec une boucle |
//A5101.ino Eeprom On écrit 00 11 22 33 // en 0 1 2 3 . . . 9 et on relit en tapant l’adresse #include "LcDef.h" #include <EEPROM.h> byte ad=0,data=0; void setup () { LcSetup (); Serial.begin(9600); for (byte i=0; i<10; i++) { EEPROM.write (ad,data); ad++; data +=11; } } void loop () { if (Serial.available() > 0) { ad = Serial.read(); ad -= "0"; // ou code du 0 = 48 décimal = 0x30 data = EEPROM.read (ad); Serial.print(data); Serial.print(" "); } } |
jdn 140526