Micro contrôleurs AVR/Arduino
L' arduino est un nom générique donné à un ensemble de platines ayant une connectique très spécifique. Son but est de pouvoir recevoir des "shields" qui ne sont autres que des périphériques spécifiques. Le nombre de shields est en constante augmentation. Des périphériques relativement complexes sont ainsi proposés :
- WIFI
- USB maître
- Commandes de moteurs pour la robotique
et bien d'autres encore ...
L'environnement de programmation est basé sur un Environnement de développement intégré (IDE dans la suite de ce chapitre) très simple à utiliser. Le langage utilisé est un dérivé du C, en fait du C++ dont on cache certaines parties.
Internet permet de trouver facilement de la documentation sur Arduino. Pour ne pas faire de redondance avec ce genre de documentation, nous allons présenter des problèmes assez techniques dans ce chapitre. Le genre de problème qu’il est difficile de trouver résolu. Nous avons, par exemple, décidé d'ajouter en fin de ce chapitre l’utilisation de platines Arduino avec le compilateur avr-gcc, autrement dit sans l'environnement ARDUINO. Nous aborderons aussi le problème des claviers USB, complexe à cause de nos claviers AZERTY et de nos caractères accentués.
Des platines libres
modifierLes platines sont naturellement vendues mais elles sont toujours proposés aux amateurs (de grandes sensations) de manière complètement libre. Il est donc possible en principe de fabriquer sa propre carte Arduino, mais cela devient de plus en plus difficile avec les évolutions des processeurs. On est passé des boîtiers DIP du processeur AVR ATMega8 aux boîtiers CMS ou SMD (pour Surface-Mount Device) des AVR ATMega 328 (32 broches). Travailler avec ce genre de boîtier est devenu difficile pour l'amateur, aussi éclairé soit-il.
S'il vous est possible de fabriquer des platines même à échelles industrielles, il vous est en revanche interdit de leur donner "Arduino" comme nom. C'est pour cela que l’on a vu fleurir des noms concurrents assez évocateurs : Freeduino, Sanguino, Seeduino, Uduino, Diduino, etc.
Nous vous présentons ci-contre un exemple typique de platine Arduino. Regardez ses connecteurs typiques (entourés de rouges), c’est avec eux que l’on connecte les "shields".
Le langage Arduino
modifierComme nous avons déjà eu l’occasion de le dire, le langage de programmation est basé sur le langage C/C++. Voyons cela un peu plus en détail.
Voici en image comment se présente l’Environnement de développement intégré ci-dessous à droite.
La référence du langage Arduino peut facilement être trouvée ici.
Langage C et Arduino
modifierLa compréhension de cette section nécessite une connaissance du langage C. Rappelons qu’il existe une Introduction au langage C dans la Wikiversité. On peut voir la programmation en langage arduino de la manière suivante : un programme en C ayant la structure :
void setup(void); // prototype de la fonction setup()
void loop(void); // prototype de la fonction loop()
int main(){
setup();
while(1) {
loop();
}
return 0;
}
Dans ce programme vous n'aurez qu’à écrire la fonction loop() et la fonction setup(). Autrement dit tout ce qui est présenté dans le listing ci-dessus vous est caché mais est ajouté de manière automatique.
On remarquera que setup() est appelé une seule fois alors que loop() est appelé dans une boucle infinie. Rappelons enfin que la traduction du mot anglais "loop" est "boucle".
Nous espérons que vous avez bien remarqué que la seule chose qui manque au programme ci-dessus pour être complet est naturellement :
void setup(void){
// votre code ici de la fonction setup()
}
void loop(void){
// votre code ici de la fonction loop()
}
et donc que c’est la seule chose que vous avez à faire en programmant votre Arduino.
Un exemple de programme
modifierL'entrée anglaise de wikipédia sur le sujet propose l'exemple suivant :
#define LED_PIN 13
void setup () {
pinMode (LED_PIN, OUTPUT); // autorise pin 13 comme sortie
}
void loop () {
digitalWrite (LED_PIN, HIGH); // allume la LED
delay (1000); // attend une seconde (1000 milliseconds)
digitalWrite (LED_PIN, LOW); // éteint la LED
delay (1000); // attend une seconde
}
Ce programme tout simple fait clignoter une LED reliée à la broche 13 (avec une résistance bien sûr) toutes les secondes.
Exemples de réalisations
modifierNous allons présenter dans cette section un exemple de réalisation et ses programmes associés. Nous espérons que des collègues viendront compléter cette partie pour diversifier les matériels utilisés.
Présentation du matériel utilisé
modifierUn shield (bouclier) a été réalisé à l’IUT de Troyes pour l'enseignement des bases de la programmation Arduino. Sa documentation peut être consultée dans ce lien (WIKI interne à GEII Troyes).
Afficher sur deux afficheurs sept segments
modifierLes programmes de cette section sont dépendants de la partie matérielle présentée plus haut et surtout ici.
Connexion du shield utilisé aux sept segments
modifierLes 2 afficheurs ne peuvent pas être utilisés simultanément. L'état de la sortie mux (arduino port 4 ou PD4) permet de sélectionner l'un ou l'autre. En allumant successivement l'un puis l'autre rapidement, on a l'illusion qu’ils sont tous 2 allumés.
Les segments des afficheurs sont câblés de façon analogue comme décrit ci-dessous :
Segment | pt | g | f | e | d | c | b | a |
---|---|---|---|---|---|---|---|---|
Arduino Pin | 11 | 9 | 10 | 8 | 7 | 6 | 12 | 13 |
Port UNO | PB3 | PB1 | PB2 | PB0 | PD7 | PD6 | PB4 | PB5 |
Voici sous forme schématique la documentation correspondante :
Cet ordre pour le brochage peut sembler étrange. Mais elle est liée à des contraintes de routage du shield.
Utiliser une interruption pour allumer une LED
modifierEncore une fois ce programme est très dépendant de la partie matérielle présentée plus haut et surtout ici.
Un appui sur le bouton allume la LED tandis qu'un nouvel appui l’éteint :
int pin = 13;
volatile int state = LOW;
void setup()
{
pinMode(pin, OUTPUT);
// attachInterrupt(0, blink, CHANGE);
attachInterrupt(0, blink, RISING);
}
void loop()
{
digitalWrite(pin, state);
}
void blink()
{
state = !state;
}
Ce programme est intéressant pour montrer comment utiliser une interruption avec l'Arduino. Cette interruption se déclenche sur un front montant ("RISING") mais peut l'être sur tout changement de niveau ("CHANGE"), ou sur front descendant ("FALLING").
Utiliser "Processing" pour afficher des données graphiques
modifierProcessing est un environnement de programmation permettant de réaliser du graphisme avec un environnement de type Arduino. La seule différence est qu'au lieu de programmer les célèbres "setup()" et "loop()" on trouvera ici "setup()" et "draw()". L'autre grande différence c’est que le langage utilisé est Java. Voici donc un exemple destiné à réaliser l’affichage sous forme de bar-graphe des valeurs provenant d'un seul capteur.
D'abord le programme Arduino
modifierNous n'utilisons pas de protocole mais envoyons directement la valeur d'un seul capteur en continu.
void setup()
{
Serial.begin(9600);
}
void loop()
{
Serial.println(analogRead(A3),DEC);
delay(500);
}
Ensuite le programme "processing"
modifierLe programme processing est un peu plus complexe :
// D'après G.Spanner : "personnalisez vos montages Arduino" Editions Elektor (2013)
// Modifié pour nos besoins
int x_max = 561;
int y_max = 600;
int offset = 60;
int distX = 80;
int distBot = 40;
int colWidth = 20;
int i = 0;
PFont fontA;
int[] sensorValues = new int[6];
import processing.serial.*;
// serial connection
Serial port;
String message = null;
String elements[] = null;
color graphColor = color(25,25,250);
PFont fontGraph;
void draw_grid(){
background(200);
for(int x=0;x<=x_max;x+=distX)
line(x,offset,x,x_max);
for(int y=0;y<=y_max;y+=50)
line(0,y+offset,y_max,y+offset);
textFont(fontA,32);
fill(graphColor);
text("AnalogMonitor",x_max/2-100,40);
textFont(fontA,16);
}
void serialEvent(Serial p){
// message = port.readStringUntil(13);
message = p.readString();
if (message != null) {
try {
// elements = splitTokens(message);
// for (int i=0; i<elements.length && i<6;i++){
// sensorValues[i] = int (elements[i]);
// }
sensorValues[i] = int (message);
i++;
if (i==6) i=0;
}
catch (Exception e) {}
}
}
void setup() {
size(x_max,y_max);
noStroke();
fontGraph =loadFont("ArialMT-12.vlw");
textFont(fontGraph,12);
println(Serial.list());
//port = new Serial(this,"ttyACM0",115200); //doesn't work !!!
port = new Serial(this, Serial.list()[0], 9600);
fontA = loadFont("ArialMT-12.vlw"); // need this file in sketch directory
textFont(fontA,16);
}
void draw(){
draw_grid();
for (int i=0;i<6;i++){
fill(graphColor);
rect(i*distX+distX-colWidth/2,height-distBot,colWidth,-sensorValues[i]/2);
text(sensorValues[i],i*distX+distX-colWidth/2,height-distBot+20);
}
}
Nous reviendrons sur cet exemple en le complétant plus tard.
Voir aussi
modifierLa programmation des composants
modifierUne fois le programme réalisé et compilé, il est temps de l'envoyer dans l'EPROM du microcontrôleur. C'est à ce problème que nous allons nous attaquer maintenant.
Le programmateur avrdude
modifierLe programmateur avrdude est un programmateur en ligne de commande assez connu dans le monde des AVR. L'IDE de l'Arduino l'utilise de manière transparente. Nous n'allons donc pas insister sur le sujet. Vous pouvez cependant lire la section sur l’utilisation de platine Arduino avec le compilateur C (sans l'IDE Arduino) pour voir un peu la ligne de commande "avrdude" !
Les bootloaders
modifierLa notion de bootloader a été abordée dans le chapitre précédent. Nous y revenons ici pour les replacer dans le contexte de l’Arduino.
Le comportement des bootloaders dépendent fortement du type de carte Arduino utilisé. Par exemple la carte UNO et MEGA2560 il n'y a rien de particulier à faire pour appelé le bootloader tandis qu'avec la carte LEONARDO un bouton de reset doit êtrte activé pour lancer celui-ci.
Le protocole utilisé par les bootloaders est en général le protocole STK500.
Le Timer 0
modifierNous avons déjà décrit auparavant dans ce livre le timer 0 et nous invitons le lecteur à relire le chapitre correspondant. Nous allons simplement traduire ce que nous avons déjà fait en langage Arduino. Pour ceux qui sont habitués à nos schéma nous présentons de nouveau le fonctionnement du timer 1 dans l'ATMega328 :
Tout travail direct sur le timer 0 provoquera obligatoirement des dysfonctionnements des primitives Arduino :
|
Rappel de la documentation du shield utilisé
modifierPuisque nous allons utiliser les LEDs pour afficher une valeur binaire sur 8 bits, voici la documentation correspondante :
Numéro | f5 | f4 | f3 | f2 | f1 | f0 | p1 | p0 |
---|---|---|---|---|---|---|---|---|
Couleur | r | o | v | r | o | v | v | r |
Arduino Pin | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 |
Port Arduino UNO | PB5 | PB4 | PB3 | PB2 | PB1 | PB0 | PD7 | PD6 |
Port Arduino LEONARDO | PC7 | PD6 | PB7 | PB6 | PB5 | PB4 | PE6 | PD7 |
Seule la ligne correspondant à l'Arduino LEONARDO nous intéresse dans la suite.
Exercice 1
modifierCet exercice est une variation d'un exercice un peu similaire dans le chapitre sur le timer 0.
Question 1
modifierLe site : convert base vous propose un algorithme de division par 10 que voici :
unsigned int A;
unsigned int Q; /* the quotient */
Q = ((A >> 1) + A) >> 1; /* Q = A*0.11 */
Q = ((Q >> 4) + Q) ; /* Q = A*0.110011 */
Q = ((Q >> 8) + Q) >> 3; /* Q = A*0.00011001100110011 */
/* either Q = A/10 or Q+1 = A/10 for all A < 534,890 */
Sans chercher à comprendre l'algorithme de division, on vous demande de le transformer en une fonction de prototype :
unsigned int div10(unsigned int A);
Indication
modifierunsigned int div10(unsigned int A){
// a compléter ICI : ceci a été réalisé en TD en salle !!!
}
Ne pas chercher à réaliser un test pour le moment mais réaliser une compilation pour retirer les fautes de syntaxe.
Question 2
modifierLes LEDs du shield maison sont couplées à un arduino UNO. Écrire un sous-programme capable d'afficher un nombre sur 8 bits sur les LEDs. Réaliser un programme de test à l'aide d'un compteur par exemple et/ou mieux qui utilise la division par 10 et sort sur les LEDs.
Indication
modifierComme l'indique le commentaire ci-dessous, le sous-programme que vous réaliserez ne devra en aucun cas changer d'autres bits que ceux que l’on utilise pour l’affichage des LEDs !!!
//************************************
// Ne modifie que les bits concernés
// pour les deux PORTs concernés
//************************************
void afficheLeds(unsigned char ch){
// A compléter ici. On n'utilisera que des décalages
//et des masques pour réaliser cette fonction
}
Question 3
modifierÉcrire un programme complet qui mesure le temps d'exécution du sous programme de division par 10 avec le timer 0, puis modifier le programme pour qu’il puisse comparer avec une division par 10 normale. On pourra utiliser un front montant sur le bouton A pour choisir le type de division réalisé.
Indications
modifierLa mise au point peut être longue pour choisir correctement le nombre de boucles de calculs nécessaires pour avoir une affichage correct sur 8 bits... ainsi que la valeur du pré-scaler.
Nous trouvons sur la page de documentation le tableau suivant :
Bouton | Position | Arduino Pin | Port | Interruption | Niveau logique de l'entrée arduino si bouton appuyé |
---|---|---|---|---|---|
A | Bas Gauche | 2 | PD2 | int0 | 1 |
D | Haut Gauche | 3 | PD3 | int1 | 1 |
B | Bas Droite | A0 | PC0 | 0 | |
C | Haut Droite | A1 | PC1 | 0 |
Un programme gérant la détection de front ressemblera à :
char etatPresent=0,etatPasse=0;
unsigned char etatSortie=0;
void setup()
{
..... // configuration des e/s
}
void loop()
{
etatPasse=etatPresent; // mémorise l'état précédent (le présent devient le passé)
etatPresent=digitalRead(??); // lecture de la valeur actuelle
if ( ( etatPresent == ?? ) && ( etatPasse == ?? ) ) // si appui alors ....
{
.....
}
}
Question 4
modifierModifier le programme de la question 3 pour qu'au lieu d'afficher sur des LEDs, l’affichage se fasse par la liaison série. On prendra soin d'afficher en même temps le type d'algorithme utilisé en même temps que la durée. On pourra améliorer l’affichage en affichant la durée en ms. Ceci sera fait dans un premier temps en utilisant directement le timer 0, puis dans un deuxième temps en utilisant la primitive Millis.
Indication
modifier- Vous ne pouvez pas utiliser la primitive delay pour ne pas saturer la liaison série car toute manipulation du timer 0 l'empêchera de fonctionner normalement.
- Serial est la documentation sur la liaison série
- Millis donne les millisecondes écoulées depuis le début du programme
La primitive Millis utilise le timer 0 de manière transparente. Mais vous ne pourrez pas l’utiliser si vous utilisez déjà le timer 0.
Question 5 : le mode de scrutation du flag
modifierNous devons savoir à ce niveau, que tout débordement du timer0 (passage de 0xFF à 0x00) entraîne le positionnement du flag TOV0, bit b0 du registre TIFR0. Vous pouvez donc utiliser ce flag pour déterminer si vous avez eu débordement du timer0, ou, en d’autres termes, si le temps programmé est écoulé. Cette méthode à l’inconvénient de vous faire perdre du temps inutilement dans une boucle d'attente.
Petit exemple :
while ((TIFR0 & 0x01) == 0); //attente passive
Réaliser un chenillard sur les 4 LEDs en utilisant le sous programme de la question 2 et le timer 0 (à régler correctement pour un chenillard visible).
Indications
modifierOn donne le programme suivant concernant le timer 0 :
int main(void){
// initialisation du timer division par 8
TCCR0 = 0x02; // prescaler 8 , entrée sur quartz
TCNT0 = 0x00; // tmr0 : début du comptage dans 2 cycles
// bit PB0 du PORTB en sortie
DDRB |= 0x01; //PB0 as output
while(1) {
TIFR0 |= 0x01; // clr TOV0 with 1 : obligatoire !!!
while ((TIFR0 & (0x01<<TOV0)) == 0);
// ce qui est fait ici est fait tous les 256 comptages de TCNT0
PORTB ^= 0x01; // on bascule avec ou exclusif
// TIFR0 &= ~(1 << TOV0); // reset the overflow flag
}
return 0;
}
Pouvez-vous donner la fréquence d'oscillation du bit b0 du PORTB avec quelques explications ? Modifiez-le pour le transformer avec un setup() et un loop() et une fréquence visible à l’œil si votre quartz est à 16 MHz et que votre œil ne peut distinguer que les fréquences inférieures à 25 Hz (2 à 5 Hz serait très bien pour ce chenillard).
Question 1
unsigned int div10(unsigned int A){
unsigned int Q; /* the quotient */
Q = ((A >> 1) + A) >> 1; /* Q = A*0.11 */
Q = ((Q >> 4) + Q) ; /* Q = A*0.110011 */
Q = ((Q >> 8) + Q) >> 3; /* Q = A*0.00011001100110011 */
/* either Q = A/10 or Q+1 = A/10 for all A < 534,890 */
return Q; // ne pas oublier le return pour une fonction
}
Question 2
//************************************
// Ne modifie que les bits concernés
// pour les deux PORTs concernés
// Version pour LEONARDO
//************************************
void afficheLeds(unsigned char ch){
unsigned char ch_partie;
if (ch & 0x80) PORTC |= 0x80; else PORTC &= ~0x80;
if (ch & 0x40) PORTD |= 0x40; else PORTD &= ~0x40;
ch_partie = (ch << 2) & 0xF0;
PORTB &= ~0xF0; // effacement de PB7 à PB4
PORTB |= ch_partie; // seuls les 1 seront écrits
if (ch & 0x02) PORTE |= 0x40; else PORTE &= ~0x40;
if (ch & 0x01) PORTD |= 0x80; else PORTD &= ~0x80;
}
Ce programme complet a été vérifié avec une Leonardo :
//*******************************
// Leonardo + shield OK
//*******************************
void setup() {
DDRB |= 0xF0;
DDRC |= 0x80;
DDRD |= 0xC0;
DDRE |= 0x40;
}
//************************************
// Ne modifie que les bits concernés
// pour les deux PORTs concernés
// Version pour LEONARDO
//************************************
void afficheLeds(unsigned char ch){
unsigned char ch_partie;
if (ch & 0x80) PORTC |= 0x80; else PORTC &= ~0x80;
if (ch & 0x40) PORTD |= 0x40; else PORTD &= ~0x40;
ch_partie = (ch << 2) & 0xF0;
PORTB &= ~0xF0; // effacement de PB7 à PB4
PORTB |= ch_partie; // seuls les 1 seront écrits
if (ch & 0x02) PORTE |= 0x40; else PORTE &= ~0x40;
if (ch & 0x01) PORTD |= 0x80; else PORTD &= ~0x80;
}
void loop() {
static unsigned char cmpt=0;
afficheLeds(cmpt);
cmpt++;
delay(100);
}
Question 3 Ce programme a été vérifié sur Leonardo
void setup() {
// les bits correspondants en sortie
DDRB |= 0xF0;
DDRC |= 0x80;
DDRD |= 0xC0;
DDRE |= 0x40;
// le bouton A en entrée
pinMode(2,INPUT);
// initialisation du timer avec prescaler 256 ,
TCCR0B |= _BV(CS02);
TCCR0B &= ~(_BV(CS01) | _BV(CS00));
}
//************************************
// Ne modifie que les bits concernés
// pour les deux PORTs concernés
// Version pour LEONARDO
//************************************
void afficheLeds(unsigned char ch){
unsigned char ch_partie;
if (ch & 0x80) PORTC |= 0x80; else PORTC &= ~0x80;
if (ch & 0x40) PORTD |= 0x40; else PORTD &= ~0x40;
ch_partie = (ch << 2) & 0xF0;
PORTB &= ~0xF0; // effacement de PB7 à PB4
PORTB |= ch_partie; // seuls les 1 seront écrits
if (ch & 0x02) PORTE |= 0x40; else PORTE &= ~0x40;
if (ch & 0x01) PORTD |= 0x80; else PORTD &= ~0x80;
}
unsigned int div10(unsigned int A){
unsigned int Q; /* the quotient */
Q = ((A >> 1) + A) >> 1; /* Q = A*0.11 */
Q = ((Q >> 4) + Q) ; /* Q = A*0.110011 */
Q = ((Q >> 8) + Q) >> 3; /* Q = A*0.00011001100110011 */
/* either Q = A/10 or Q+1 = A/10 for all A < 534,890 */
return Q;
}
void loop() {
static char etatPresent=0,etatPasse=0,algo=0;
unsigned char temps,i;
unsigned int res;
etatPasse=etatPresent; // mémorise l'état précédent (le présent devient le passé)
etatPresent=digitalRead(2); // lecture de la valeur actuelle
if ( ( etatPresent == LOW ) && ( etatPasse == HIGH ) )
algo++; // sera utilisé pour son poids faible
TCNT0 = 0x00;
// algorithme de calcul ici plusieurs fois
for(i=0;i<55;i++) {
if (algo & 0x01)
res = div10(i+15558);
else
res = (i+15558)/10;
PORTC = res; // autrement simplifié
}
// fin de la mesure : lecture du timer
temps=TCNT0;
afficheLeds(temps);
delay(100);
}
Exercice 2
modifierReprendre l'exercice du chapitre sur le timer 0 sur la comparaison simple en utilisant le langage Arduino.
Le Timer 1
modifierNous avons déjà décrit auparavant dans ce livre le timer 1 et nous invitons le lecteur à relire le chapitre correspondant. Nous allons simplement traduire ce que nous avons déjà fait en langage Arduino. Pour ceux qui sont habitués à nos schéma nous présentons de nouveau le fonctionnement du timer 1 dans l'ATMega328 :
Le timer 1 en interruption
modifierNous allons chercher à déclencher une interruption par le timer 1. Cette interruption va faire basculer la broche 11 de la maquette Arduino qui est relée par l'intermédiaire d'un shield à une LED.
#define ledPin 11
void setup(){
pinMode(ledPin, OUTPUT);
// initialize Timer1
noInterrupts(); // disable all interrupts
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 34286; // preload timer 65536-16MHz/256/2Hz
TCCR1B |= (1 << CS12); // 256 prescaler
TIMSK1 |= (1 << TOIE1); // enable timer overflow interrupt
interrupts(); // enable all interrupts
}
ISR(TIMER1_OVF_vect) {// interrupt service routine that wraps a user defined function supplied by attachInterrupt
TCNT1 = 34286; // preload timer
digitalWrite(ledPin, digitalRead(ledPin) ^ 1);
}
void loop(){
// your program here...
}
Même si ce programme fonctionne très bien, permettez-nous de faire les remarques suivantes.
Ce programme ci-dessus doit être analysé plus avant. Nous ferons donc les remarques suivantes :
- une interruption est un programme court
- La mesure du temps d'exécution d'une instruction est présentée ici Mesure du temps d'exécution à l'aide du timer2
- une mesure avec timer2 de l'instruction "digitalWrite" nous a montré qu'elle était 20 fois plus lente que sa cousine en C.
Ces remarques ne remettent pas en cause l’utilisation des fonctions Arduino mais il faut garder en tête que celles-ci sont utiles pour débuter. Une fois formé débarrassez-vous en pour programmer en C le plus vite possible.
Utilisation du timer 1 en comparaison
modifierLe matériel s'occupe de tout
modifierNous donnons la version qui fonctionne sur la platine MEGA2560. Pour une autre platine il faut probablement changer le numéro de broche.
//*********** Pour platine MEGA2560 mais fonctionne probablement avec platine UN0
// probablement à changer pour une autre platine
#define ledPin 11
void setup(){
// initialisation pour comparaison
// DDRB |= (1<<DDB5); // 1 = sortie
pinMode(11,OUTPUT);
noInterrupts(); // disable all interrupts
TCCR1B = 0x00;
TCCR1A = 0x00;
TCNT1 = 0x0000;
digitalWrite(11,1);// OCR pin
//TIFR0 |= 0x04; // clr TOV1 with 1 : inutile
// Prescaler 1024 (Clock/1024)
TCCR1B |= (1<<CS12) | (1<<CS10);
TCCR1B |= (1<<WGM12); //RAZ timer quand comparaison
TCCR1A |= (1<<COM1A0); //bascule sortie a chaque comparaison
// TCCR1A &= ~(1<<COM1A1); //inutile : tout est déjà à 0
OCR1A = 7812;//16 000 000 /(1024*2)
}
void loop() {
// Your program here...
}
Vous pouvez constater qu'aucune interruption n'est lancée. C'est le matériel qui s'occupe d'absolument tout, tout seul comme un grand.
Comparaison et interruption
modifierPour nous la patte 9 est reliée à ne LED. On peut donc visualiser son clignotement.
#define ledPin 9
void setup(){
pinMode(ledPin, OUTPUT);
// initialize Timer1
noInterrupts(); // disable all interrupts
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 31250; // compare match register 16MHz/256/2Hz
TCCR1B |= (1 << WGM12); // CTC mode
TCCR1B |= (1 << CS12); // 256 prescaler
TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt
interrupts(); // enable all interrupts
}
ISR(TIMER1_COMPA_vect) // timer compare interrupt service routine
{
digitalWrite(ledPin, digitalRead(ledPin) ^ 1); // toggle LED pin
}
void loop(){
// your program here...
}
Le Timer 2
modifierMesure de temps d'exécution
modifierNous allons profiter du timer 2 pour mesurer le temps d'exécution de certaines fonctions Arduino, et se rendre compte de l'une de leur principale limite.
Le temps écoulé sera mesuré à l'aide du Timer 2.
Le principe est toujours le même :
- Initialiser le timer
- Répéter plusieurs fois l'instruction souhaitée
- Observer la valeur du timer
- Afficher cette valeur
Le timer 2 et ses registres (sans interruption)
modifierVoici ci-contre, avec les conventions schématiques habituelles, le schéma de fonctionnement du timer 2.
On distingue ainsi le bit b0 du registre TIFR2 appelé TOV2 qui est mis à un automatiquement par le matériel. Ce ne sera pas la matériel qui le mettra à 0 s'il n'y a pas d'interruptions. C'est à vous programmeur de le faire ! Mais pour le faire il vous faut écrire un 1 dans ce bit b0 !
Les habituels bits de configuration de la division se trouvent dans le registre TCCR2B et fonctionnent exactement comme pour timer 0.
Le registre ASSR sert à choisir la source de l'horloge du timer 2. Pour nous, sauf mention contraire, ce sera toujours le quartz. Ce registre doit être configuré dans ce mode de fonctionnement par défaut.
Configuration des entrées/sorties
modifierNous souhaitons ici évaluer le temps mis par la fonction pinMode().
L'affichage du résultat sera tout simplement transmis sur la liaison série en utilisant les fonctions suivantes :
- Serial.begin(debit)
- Serial.print("valeur = ")
- Serial.println(valeur,DEC)
Question : Compléter le programme suivant en choisissant les bonnes valeurs pour évaluer le temps d'exécution de pinMode()
void setup() {
// Variables éventuelles
// Initialisation du port série
Serial.begin(9600);
// Configuration du timer 2 : Attention, chaque bit doit être configuré à '0' ou '1'
TCCR2A ??? (1<<WGM20); // mettre à 0
TCCR2A ??? (1<<WGM21); // mettre à 0
TCCR2B ??? (1<<WGM22); // mettre à 0
// choix du pré-diviseur :
TCCR2B ??? (1<<CS22);
TCCR2B ??? (1<<CS21);
TCCR2B ??? (1<<CS20);
// Initialisation du Timer : acquittement et valeur de départ
TIFR2|=1<<TOV2;
TCNT2=0;
for (i=0;i<1;i++) // Le nombre d'itérations peut/doit être adapté !
{
// Fonction à évaluer : il est intéressant de répéter la fonction plusieurs fois
pinMode(6,OUTPUT);
}
// Récupérer la valeur du timer et l'afficher seulement si le timer n'a pas débordé !!!
}
void loop() {
}
Attention ! Ne prenez pas ce que vous donne le timer pour argent comptant. Cette valeur doit avoir certaines propriétés :
- ne pas être accompagnée d'un débordement de TCNT2. Vous seriez donc très inspiré d'écrire la valeur du flag TOV2 avec la liaison série en plus de la valeur de temps
- augmenter lorsqu'on augmente le nombre de boucles exécutées sur l'instruction testée (autre manière de dire les choses, on double le temps si on double le nombre de boucles).
Ce travail nécessite donc un peu de soin et beaucoup d'essais pour trouver la bonne valeur de la division dans le pré-diviseur à choisir. Il peut être utile de mettre les tests dans loop() pour prendre son temps pour ouvrir la liaison série.
Question : Comparer en déclarant une sortie directement en configurant le registre DDRx
Interruptions
modifierL'attente passive peut être évitée à l'aide des interruptions. On imagine un programme qui fait des tas de choses mais qui sera interrompu régulièrement par un timer.
Question : En utilisant la documentation ci-dessus, on vous demande de générer un signal d'un kilo Hertz à l'aide d'une interruption.
Indication
modifierVoici un premier programme à compléter :
void setup()
{
DDRB = ; // Pin 6 as output
// Using timer 2
// Set to Normal mode, Pin OC0A disconnected
TCCR2A = ;
// Prescale clock by 1024
// Interrupt every 256K/16M sec = 1/64 sec
TCCR2B = ;
// Turn on timer overflow interrupt flag
TIMSK2 = ;
// Turn on global interrupts
sei();
}
volatile char timer = 0;
ISR( _vect) {
timer++;
PORTB = ;
}
void loop()
{
// Nothing to do
}
La comparaison
modifierDocumentation
modifierVoici la documentation correspondante sous forme de schéma (ci-contre).
Ce mode de fonctionnement s’appelle la comparaison. L’idée générale est que lorsque le timer 2 (TCNT2) arrive à la même valeur que celle qui est contenue dans un registre (OCR2A ou OCR2B) une logique interne est capable de changer (ou pas) une sortie qui s’appelle OC2A ou OC2B.
Ce mode est essentiellement géré par les deux bits COM2A1 et COM2A0 comme indiqué dans le tableau ci-dessous :
- Mode non PWM pour la comparaison
COM2A1 | COM2A0 | Description |
---|---|---|
0 | 0 | Opération Normale PORT, OC2A déconnecté |
0 | 1 | Bascule OC2A sur la comparaison |
1 | 0 | Mise à 0 de OC2A sur la comparaison |
1 | 1 | Mise à 1 de OC2A sur la comparaison |
Nous avons ajouté les informations correspondant à la carte UNO entre parenthèse. Par exemple la sortie correspondante au bit b3 du PORTD est OC2A et correspond au numéro 11 de la carte UNO. L'autre broche gérée par le comparateur B est la broche 3 et se trouve entre parenthèse avec le registre de comparaison.
Utilisation du timer 2 en mode CTC
modifierLe mode CTC du timer est un mode qui veut dire : Clear Timer on Compare match.
Question : Faire un programme qui génère un signal de fréquence 1kHz sur la patte PB3, en utilisant le timer 2 et la comparaison.
Indication : l’idée est de placer le timer en mode CTC (Clear Timer on Compare match) et la génération de signaux en mode basculement de OC2A.
- Description des bits pour le mode de fonctionnement du timer 2 CTC
Mode | WGM22 | WGM21 | WGM20 | Mode de fonctionnement | Bas si | Mise à jour de OCRAx si | Drapeau TOV2 positionné si |
---|---|---|---|---|---|---|---|
2 | 0 | 1 | 0 | CTC | OCR2A | immédiatement | MAX |
Utilisation du timer 2 pour générer deux signaux
modifierIl est possible de générer deux signaux d'un coup avec le timer2 cat il y a un seul timer mais deux registres de comparaison.
Question : Ajouter au programme précédent une génération de signal de fréquence 2kHz sur la patte PD3, en utilisant le timer 2 et la comparaison. À ce stade on aura deux fréquences générées avec aucune occupation du processeur (loop complètement vide).
Interruption sur comparaison
modifierIl est possible aussi de déclencher une interruption sur une comparaison. Voici un squelette de programme à compléter pour cela :
void setup()
{
DDRB = ; // Pin 13 OUTPUT
// Using timer 2
// Set to CTC mode, Pin OC0A disconnected
TCCR2A = ;
// Prescale clock by 1 (no prescale)
TCCR2B = ;
// Set compare register
OCR2A = ;
// Turn on timer compare A interrupt flag
TIMSK2 = ;
// Turn on global interrupts
sei();
}
char timer = 0;
ISR( _vect) {
timer++;
PORTB = ;
}
void loop(){
// Nothing to do
}
La suite et parfois des compléments peuvent être trouvés dans le WIKI de l'IUT de Troyes.
Clavier USB avec la platine Leonardo
modifierLa platine Leonardo est architecturée autour d'un processeur ATMega32U4, le U signifiant USB. Cette version de processeur permet de gérer deux protocoles USB clients en même temps, nous voulons dire par la même prise USB. Nous verrons dans le projet de cette section (et dans l'exemple keyboardSerial de la rubrique USB) qu’il est possible d’utiliser ensemble la liaison série et un clavier USB par le même fil USB.
En fonctionnement normal de la platine Leonardo, la liaison série est utilisée pour la programmation à l'aide d'un bootloader à travers l'USB. Les connaisseurs d'autres platines Arduino (comme la UNO et bien d'autres) diront qu’à première vue, il n'y a rien de nouveau ! Sauf qu'ici le processeur se débrouille seul alors que dans les premières versions de la platine UNO il y avait un FTDI2232 qui transformait l'USB en RS232. Les nouvelles versions de cette platine ont abandonné le FTDI au profit d'un processeur ATMega8u2 ou ATMega16u2... et tout cela en plus du processeur ATMega328 que l’on cherche à programmer. La Leonardo ne comporte que le processeur à programmer et rien d'autre, ni FTDI ni autre processeur.
Nous allons examiner les performances de la Leonardo par rapport à cette liaison USB. Pour ce faire, nous allons simuler une clé USB destinée à entrer un identifiant et un mot de passe automatiquement. C'est un projet assez courant sur Internet mais nous n'avons trouvé que peu de code source (Celui-ci fait exception mais est très loin de réaliser notre cahier des charges).
Cahier des charges
modifierCe que nous cherchons à faire peut être appelé "USB keylogger" en anglais (ou à peu près car le mot keylogger est associé parfois à l'enregistrement pirate de frappes au clavier). Aussi le mot anglais USB EasyLogger semble plus approprié. Nous le traduisons par clé de connexion USB. Nous allons donc développer la partie logicielle sur une platine Leonardo. La partie matérielle finale devra être compatible Leonardo. Elle est décrite plus loin. Nous voulons être capable de :
- stoker plusieurs identifiants et mots de passes en EEPROM par la liaison série/USB
- effacer certains couples identifiants/mots de passes plus utilisés
- choisir lequel est actif par défaut
- choisir la séquence par défaut
Le dernier point mérite quelques explications. Les comptes éloignés nécessitent en général un identifiant et un mot de passe. Les deux sont séparés par une tabulation et après le mot de passe on valide par un ENTER. Mais sur votre ordinateur local, le bon utilisateur est parfois choisi par défaut et donc seul le mot de passe est demandé. C'est cela que nous appelons séquence et qui pourra être choisi.
Notre description de cette clé de connexion USB nécessitera une utilisation fréquente des expressions :
- Mot de Passe que nous résumerons le plus possible par MdP
- Utilisateur (ou mieux identifiant) que nous appellerons aussi par l'anglicisme login
- couple "Identifiant/Mot de passe" sera souvent appelé login/MdP
Pour respecter ce cahier des charges, nous envisageons d’utiliser quatre boutons :
- un bouton appelé ENTER pour envoyer le couple login/MdP,
- un bouton appelé MODIF qui devra permettre d'entrer dans un mode modification à l'aide d'un hyperterminal,
- un bouton appelé CHOIX qui devra aussi permettre des modifications en utilisant uniquement les trois LEDs présentes.
- un reset comme il y a sur le Leonardo
Dans l'état actuel seuls les boutons ENTER et MODIF sont utilisés.
Pourquoi prendre une Leonardo comme point de départ ?
modifierNous sommes parti d'une platine Leonardo, car elle fait partie des platines qui gèrent le périphérique clavier en natif. Cela est dû essentiellement au processeur ATMega32u4 utilisé. En fait toute platine compatible Arduino avec ce processeur peut être utilisée. Vous avez par exemple :
- Platine "Pro Micro - 5V/16MHz"
- Platine "Pro Micro - 3,3 V/8MHz"
et d'autres encore du fabricant Sparkfun. Celles-ci sont de très petites tailles (3,4 cm x 1,7 cm environ) mais possèdent un connecteur micro USB.
La platine Leonardo munie d'un shield avec trois boutons peut être utilisée pour le développement logiciel. Le développement de cette partie logicielle est assez longue. Ce ne serait pas le cas si vous n'aviez comme objectif d’utiliser un login et mot de passe seulement. Mais si vous voulez pouvoir les mettre à jour avec un micro-ordinateur Windows, Linux ou MAC c’est une autre histoire.
Le développement logiciel avec l'environnement Arduino est particulièrement simple mais présente un inconvénient pour la réalisation finale : le bootloader. En effet celui-ci attend 8s avant de lancer le programme. Cela veut dire que chaque fois que vous allez brancher votre clé de connexion USB, il vous faudra attendre 8s avant de pouvoir envoyer votre login et mot de passe. Cela peut être considéré comme un inconvénient grave. Si c’est le cas il faut pouvoir modifier le fusible qui gère le RESET (sur bootloader ou pas) et ceci nécessite une programmation ICSP en dehors de l'environnement Arduino !
Il est grand temps d’aborder les problèmes spécifiques des claviers USB.
Clavier QWERTY
modifierNous avions cru lire, il y a quelques temps sur Internet, que les claviers USB ne retournent plus les scancodes comme leurs cousins PS/2 mais surtout que le code retourné était absolu : 'a' codé 0x04, 'b' codé 0x05. Mais ceci est malheureusement faux. Cela prouve que notre anglais n’est pas encore assez pointu pour distinguer ce genre de détail. Il faut dire qu’il est très difficile de trouver de la documentation technique sur le sujet.
Nous avons lu aussi qu'avec Leonardo on était condamné au clavier QWERTY contrairement à son concurrent Olimexino. Et bien comment dire, c’est aussi faux !
Un théorème dit qu' en programmation tout est possible et son corollaire dit tout dépend du temps que l’on désire y passer. Il n'y a pas de mathématiques là-dedans mais nous avons toujours adoré utiliser de la terminologie mathématique là où il n'y en avait pas besoin !
L'instruction qui permet d'écrire comme un clavier sera détaillée plus tard, mais elle s’appelle Keyboard.println(). Et bien figurez-vous que si vous utilisez Keyboard.println("mouton") sur appui d'un bouton et que vous ouvrez un éditeur de texte et appuyez sur ledit bouton vous obtiendrez ",outon" ! Tout simplement parce que le m (AZERTY) est remplacé par une virgule sur clavier QWERTY ! C'est une mauvaise nouvelle car pour corriger, ce n’est pas difficile mais long. La bonne nouvelle c’est que nous allons essayer d'expliquer comment faire, le faire, et finalement le publier ici.
Le problème AZERTY/QWERTY qui est présenté ci-après n'est que partiellement résolu avec un tableau de transcodage. Il est possible d'éviter ce tableau sur Linux avec la commande :
setxkbmap fr/us
et il existe probablement sous windows une commande à peu près similaire. Mais comme nous désirons faire une carte autonome qui se branche pour faire "utilisateur/mot de passe" pour un login automatisé, et qu'à ce stade du démarrage nous n'avons pas accès à une ligne de commande, cela nous oblige à utiliser un tableau de transcodage.
Le fait que le programme Arduino soit automatiquement reconnu comme un clavier US est probablement lié aux données qui sont envoyées par le code pendant l'énumération. Il existe donc probablement aussi un autre moyen de résoudre ce problème en modifiant le processus d'énumération mais nous ne nous sommes pas risqué à l'entreprendre !
Tableau de transcodage QWERTY
modifierChercher le fichier HID.cpp de votre distribution Arduino. Chez nous, sous Linux, il est dans "/usr/share/arduino/hardware/arduino/cores/arduino". Chez vous, sous Windows, il est probablement dans quelque chose comme "Program File\arduino\hardware\arduino\cores\arduino".
Dans le fichier HID.cpp vous trouverez un tableau de transcodage :
#define SHIFT 0x80
const uint8_t _asciimap[128] =
{
0x00, // NUL
0x00, // SOH
0x00, // STX
0x00, // ETX
0x00, // EOT
0x00, // ENQ
0x00, // ACK
0x00, // BEL
0x2a, // BS Backspace
0x2b, // TAB Tab
0x28, // LF Enter
0x00, // VT
0x00, // FF
0x00, // CR
0x00, // SO
0x00, // SI
0x00, // DEL
0x00, // DC1
0x00, // DC2
0x00, // DC3
0x00, // DC4
0x00, // NAK
0x00, // SYN
0x00, // ETB
0x00, // CAN
0x00, // EM
0x00, // SUB
0x00, // ESC
0x00, // FS
0x00, // GS
0x00, // RS
0x00, // US
0x2c, // ' '
0x1e|SHIFT, // !
0x34|SHIFT, // "
0x20|SHIFT, // #
0x21|SHIFT, // $
0x22|SHIFT, // %
0x24|SHIFT, // &
0x34, // '
0x26|SHIFT, // (
0x27|SHIFT, // )
0x25|SHIFT, // *
0x2e|SHIFT, // +
0x36, // ,
0x2d, // -
0x37, // .
0x38, // /
0x27, // 0
0x1e, // 1
0x1f, // 2
0x20, // 3
0x21, // 4
0x22, // 5
0x23, // 6
0x24, // 7
0x25, // 8
0x26, // 9
0x33|SHIFT, // :
0x33, // ;
0x36|SHIFT, // <
0x2e, // =
0x37|SHIFT, // >
0x38|SHIFT, // ?
0x1f|SHIFT, // @
0x04|SHIFT, // A
0x05|SHIFT, // B
0x06|SHIFT, // C
0x07|SHIFT, // D
0x08|SHIFT, // E
0x09|SHIFT, // F
0x0a|SHIFT, // G
0x0b|SHIFT, // H
0x0c|SHIFT, // I
0x0d|SHIFT, // J
0x0e|SHIFT, // K
0x0f|SHIFT, // L
0x10|SHIFT, // M
0x11|SHIFT, // N
0x12|SHIFT, // O
0x13|SHIFT, // P
0x14|SHIFT, // Q
0x15|SHIFT, // R
0x16|SHIFT, // S
0x17|SHIFT, // T
0x18|SHIFT, // U
0x19|SHIFT, // V
0x1a|SHIFT, // W
0x1b|SHIFT, // X
0x1c|SHIFT, // Y
0x1d|SHIFT, // Z
0x2f, // [
0x31, // bslash
0x30, // ]
0x23|SHIFT, // ^
0x2d|SHIFT, // _
0x35, // `
0x04, // a
0x05, // b
0x06, // c
0x07, // d
0x08, // e
0x09, // f
0x0a, // g
0x0b, // h
0x0c, // i
0x0d, // j
0x0e, // k
0x0f, // l
0x10, // m
0x11, // n
0x12, // o
0x13, // p
0x14, // q
0x15, // r
0x16, // s
0x17, // t
0x18, // u
0x19, // v
0x1a, // w
0x1b, // x
0x1c, // y
0x1d, // z
0x2f|SHIFT, //
0x31|SHIFT, // |
0x30|SHIFT, // }
0x35|SHIFT, // ~
0 // DEL
};
Nous le donnons ici car c’est la version originale du fichier HID.cpp et il vous faut le mettre de côté.
Changer QWERTY en AZERTY
modifierC'est le tableau de transcodage de la section précédente qu’il faut changer. Voici comment procéder techniquement pour le changer.
Essayez de vous procurer une photo d'un clavier QWERTY. Nous en dessinons un (très simplifié) ci-dessous.
Le point essentiel est de comprendre que votre tableau de transcodage doit correspondre au clavier du bas alors que celui que vous avez au départ correspond à celui du haut ! Les échanges de codes sont faciles lorsque vous savez à quelle touche QWERTY correspond telle lettre. Vous la cherchez sur le clavier AZERTY et notez le numéro rouge s'il est différent et le placez à l'endroit du commentaire correspondant.
Les choses sont plus délicates lorsque vous n'avez pas la lettre correspondante sur le clavier QWERTY. Prenez le é par exemple, quel est son code ? Ce n’est pas 0X1F comme le laisserait penser le dessin du bas ! ... et de toute façon il n'est même pas en commentaire dans le fichier HID.cpp !!!
#define SHIFT 0x80
const uint8_t _asciimap[128] =
{
0x00, // NUL
0x00, // SOH
0x00, // STX
0x00, // ETX
0x00, // EOT
0x00, // ENQ
0x00, // ACK
0x00, // BEL
0x2a, // BS Backspace
0x2b, // TAB Tab
0x28, // LF Enter
0x00, // VT
0x00, // FF
0x00, // CR
0x00, // SO
0x00, // SI
0x00, // DEL
0x00, // DC1
0x00, // DC2
0x00, // DC3
0x00, // DC4
0x00, // NAK
0x00, // SYN
0x00, // ETB
0x00, // CAN
0x00, // EM
0x00, // SUB
0x00, // ESC
0x00, // FS
0x00, // GS
0x00, // RS
0x00, // US
0x2c, // ' '
0x38, // !
0x20, // "
0x20, // # :TODO
0x30, // $
0x34|SHIFT, // %
0x1E, // &
0x21, // '
0x22, // (
0x2d, // )
0x31, // * : done
0x2e|SHIFT, // +
0x10, // ,
0x23, // -
0x36|SHIFT, // .
0x37|SHIFT, // /
0x27|SHIFT, // 0
0x1e|SHIFT, // 1
0x1f|SHIFT, // 2
0x20|SHIFT, // 3
0x21|SHIFT, // 4
0x22|SHIFT, // 5
0x23|SHIFT, // 6
0x24|SHIFT, // 7
0x25|SHIFT, // 8
0x26|SHIFT, // 9
0x37, // :
0x36, // ;
0x64, // < Done
0x2e, // =
0x64|SHIFT, // > Done
0x10|SHIFT, // ? 0x38 -> 0x10 OK
0x1f, // @ TODO
0x14|SHIFT, // A
0x05|SHIFT, // B
0x06|SHIFT, // C
0x07|SHIFT, // D
0x08|SHIFT, // E
0x09|SHIFT, // F
0x0a|SHIFT, // G
0x0b|SHIFT, // H
0x0c|SHIFT, // I
0x0d|SHIFT, // J
0x0e|SHIFT, // K
0x0f|SHIFT, // L
0x33|SHIFT, // M
0x11|SHIFT, // N
0x12|SHIFT, // O
0x13|SHIFT, // P
0x04|SHIFT, // Q
0x15|SHIFT, // R
0x16|SHIFT, // S
0x17|SHIFT, // T
0x18|SHIFT, // U
0x19|SHIFT, // V
0x1d|SHIFT, // W
0x1b|SHIFT, // X
0x1c|SHIFT, // Y
0x1a|SHIFT, // Z
0x0c, // [ TODO 2F
0x31, // bslash
0x0d, // ] TODO 30
0x2F, // ^
0x25, // _
0x35, // ` TODO
0x14, // a
0x05, // b
0x06, // c
0x07, // d
0x08, // e
0x09, // f
0x0a, // g
0x0b, // h
0x0c, // i
0x0d, // j
0x0e, // k
0x0f, // l
0x33, // m
0x11, // n
0x12, // o
0x13, // p
0x04, // q
0x15, // r
0x16, // s
0x17, // t
0x18, // u
0x19, // v
0x1d, // w
0x1b, // x
0x1c, // y
0x1a, // z
0x2f|SHIFT, //
0x31|SHIFT, // | TODO
0x30|SHIFT, // } TODO
0x35|SHIFT, // ~ TODO
0 // DEL
};
Ce tableau de transcodage permet d'exécuter un programme du genre :
void setup() {
pinMode(2,INPUT);
}
void loop() {
static char new_button,old_button;
new_button=digitalRead(2);
if ((old_button==0)&&(new_button==1)) { //detection front montant
Keyboard.begin();
Keyboard.println("DEBUT AUTORISE");
Keyboard.println("abcdefghijklmnopqrstuvwxyz");
Keyboard.println("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
Keyboard.println("0123456789");
Keyboard.println(" !\"$%&'()");
Keyboard.println("%,;:!?./+=");
Keyboard.println("FIN AUTORISE");
Keyboard.println("DEBUT NON AUTORISE");
Keyboard.println("<>*#{[|`\^@]}");
Keyboard.println("FIN NON AUTORISE");
Keyboard.end();
}
old_button = new_button;
delay(100);
}
avec comme résultat :
DEBUT AUTORISE abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 !"$%&'() %,;:!?./+= FIN AUTORISE DEBUT NON AUTORISE −− "ïµ²ếj£ FIN NON AUTORISE
où l’on voit que les caractères non autorisés ne fonctionnent pas correctement. Pour le constater, comparer ce qui est affiché avec ce qui est mis dans les println du programme correspondant (au-dessus) ! En clair, ce que nous avons noté NON AUTORISE correspond aux caractères que vous ne pouvez pas utiliser dans vos mots de passe.
Passer ces caractères non autorisés comme autorisés demande un travail supplémentaire prévu dans le futur mais pas réalisé pour le moment. Il faut pour cela changer le code source de HID.cpp, temporairement au moins pour examiner les scancodes.
En modifiant temporairement HID.cpp pour qu’il envoie directement le scancode et non le transcodé du caractère ASCII (un simple court-circuit du tableau de transcodage), nous avons réussi à récupérer <>* comme caractères autorisés supplémentaires. Mais pour la suite, c’est une autre histoire !!!! La version présentée ci-dessus doit avoir pris en compte ces modifications.
Récupérer #{[\@]} par exemple, nécessiterait de faire un autre modificateur que SHIFT, ALT Gr en l’occurrence. Mais ceci ne convient pas à un codage sur 7 bits comme utilisé dans HID.cpp. Affaire à suivre....
... et qui est suivie dans la section suivante.
Plus loin pour changer QWERTY en AZERTY
modifierLe code ci-dessous force systématiquement l’utilisation de l'envoi d'un scancode au lieu d'un code ASCII à cause du #define ASCII_EN en commentaire au début. Il force aussi systématiquement le modificateur Alt droit qui est pour nous français un Alt Gr. Le résultat est enfin les caractères ~#{[|`\^@]}. Mais il n'est bati que pour du test. À modifier pour poursuivre.
//#define ASCII_EN
//Retiré beaucoup de code de HID.cpp pour simplifier
// press() adds the specified key (printing, non-printing, or modifier)
// to the persistent key report and sends the report. Because of the way
// USB HID works, the host acts like the key remains pressed until we
// call release(), releaseAll(), or otherwise clear the report and resend.
// octet 0 : modifiers, avec :
// bit 0 : ctrl gauche
// bit 1 : shift gauche
// bit 2 : alt gauche
// bit 3 : GUI gauche
// bit 4 : ctrl droit
// bit 5 : shift droit
// bit 6 : alt droit
// bit 7 : GUI droit
size_t Keyboard_::press(uint8_t k)
{
uint8_t i;
#ifdef ASCII_EN
if (k >= 136) { // it's a non-printing key (not a modifier)
k = k - 136;
} else if (k >= 128) { // it's a modifier key
_keyReport.modifiers |= (1<<(k-128));
k = 0;
} else { // it's a printing key
k = pgm_read_byte(_asciimap + k);
if (!k) {
setWriteError();
return 0;
}
if (k & 0x80) { // it's a capital letter or other character reached with shift
_keyReport.modifiers |= 0x02; // the left shift modifier
k &= 0x7F;
}
}
#endif
_keyReport.modifiers |= 0x40; // modificateur Alt droit systématique
// Add k to the key report only if it's not already present
// and if there is an empty slot.
if (_keyReport.keys[0] != k && _keyReport.keys[1] != k &&
_keyReport.keys[2] != k && _keyReport.keys[3] != k &&
_keyReport.keys[4] != k && _keyReport.keys[5] != k) {
for (i=0; i<6; i++) {
if (_keyReport.keys[i] == 0x00) {
_keyReport.keys[i] = k;
break;
}
}
if (i == 6) {
setWriteError();
return 0;
}
}
sendReport(&_keyReport);
return 1;
}
// release() takes the specified key out of the persistent key report and
// sends the report. This tells the OS the key is no longer pressed and that
// it shouldn't be repeated any more.
size_t Keyboard_::release(uint8_t k)
{
uint8_t i;
#ifdef ASCII_EN
if (k >= 136) { // it's a non-printing key (not a modifier)
k = k - 136;
} else if (k >= 128) { // it's a modifier key
_keyReport.modifiers &= ~(1<<(k-128));
k = 0;
} else { // it's a printing key
k = pgm_read_byte(_asciimap + k);
if (!k) {
return 0;
}
if (k & 0x80) { // it's a capital letter or other character reached with shift
_keyReport.modifiers &= ~(0x02); // the left shift modifier
k &= 0x7F;
}
}
#endif
_keyReport.modifiers &= ~(0x40); // modificateur Alt droit systématiquement retiré
// Test the key report to see if k is present. Clear it if it exists.
// Check all positions in case the key is present more than once (which it shouldn't be)
for (i=0; i<6; i++) {
if (0 != k && _keyReport.keys[i] == k) {
_keyReport.keys[i] = 0x00;
}
}
sendReport(&_keyReport);
return 1;
}
Autre piste en lisant attentivement le code ci-dessus pour le press et release : on peut atteindre directement le modificateur AltGr avec le code 134 comme ci-dessous :
Keyboard.press(134);
Keyboard.write('"');
Keyboard.release(134);
Keyboard.write(',');
Ceci n'écrira pas ", mais #, et c’est bien ce que l’on cherche à faire. Cette technique nous permet de récupérer tous les caractères AltGr soit ~#{[|`\^@]} Pas mal non ? Mais il faut écrire un programme à nous qui fasse les bons appels en fonction des caractères. Et ce travail n'a pas encore été fait.
Nous venons de découvrir que les modifications que nous proposons ci-dessus ne fonctionnent pas avec Windows 8 ! Le clavier reste désespérément en QWERTY. |
Reste aussi le cas des caractères accentués : là, nous ne voyons aucune piste simple pour le moment !
Nous sommes absolument stupéfait par la difficulté de répondre à cette simple question : que faut-il envoyer au PC pour qu’il interprête cela comme un é ? Après des heures et des heures de recherche sur Internet, nous sommes incapable de répondre à la question !!!
Ceci est un appel à l'aide !!!
HELP HELP HELP !!!
HILFE HILFE HILFE !!!
Nous ne connaissons pas d’autre langues !
Travail Logiciel
modifierNous allons donner sous forme d'exercices l’ensemble du travail logiciel à réaliser.
Stocker les différents utilisateurs/ mots de passe dans l'EEPROM
modifierNous allons utiliser l'abréviation MdP pour désigner les mots de passe dans la suite. Ainsi le couple "identifiants/mots de passe" pourra s'écrire de manière simplifiée : identifiants/MdP
On vous demande de réaliser un sous-programme capable de mettre à jour l'EEPROM avec des couples identifiants/MdP. La convention est la suivante :
- les mots de passes comme les identifiants seront des mots de 15 caractères maximum terminés systématiquement par un zéro (de fin de chaîne).
- Le premier octet de l'EEPROM désigne le mot de passe par défaut : 0 est le premier mot de passe, 1 le deuxième etc ...
- Le deuxième octet de l'EEPROM désigne le nombre maximum de mots de passe enregistrés dans l'EEPROM (ce nombre ne devra pas dépasser 7)
- Le troisième octet désigne le type de séquence utilisée : soit "Login TAB MdP ENTER", soit "MdP ENTER" pour le moment
- Le premier identifiant se trouve en adresse 0x10, le deuxième en 0x30 ....
- Le premier mot de passe se trouve en adresse 0x20, le deuxième en 0x40 ....
Voici un dessin représentant le contenu de l'EEPROM pour un stockage de deux identifiants/mots de passe :
char eeprom1[]={'t','z','W','t','Y','u','9','2',0};
char eeprom2[]={'X','b','=','7','Z','?',0};
char eeprom3[]={'z','t','o','t','r','o','w',0};
char eeprom4[]={'d','E','w','a','Z','d','t','=','M','y',0};
void init_eeprom(){
char i;
// debut EEPROM = information vitale
EEPROM.write(0x00,0); // login/mot de passe par défaut de 0 à n-1
EEPROM.write(0x01,2); // maxi : nb maxi de mots de passe enregistrés
// sequence 00 : login tab mot de passe Enter
// sequence 01 : mot de passe Enter (pas de login)
EEPROM.write(0x02,0); //sequence
// maintenant la suite des mots de passe et login
i=0;
do {
EEPROM.write(0x10+i,eeprom1[i]);
i++;
}while(eeprom1[i]!=0);
EEPROM.write(0x10+i,0);
i=0;
do {
EEPROM.write(0x20+i,eeprom2[i]);
i++;
}while(eeprom2[i]!=0);
EEPROM.write(0x20+i,0);
i=0;
do {
EEPROM.write(0x30+i,eeprom3[i]);
i++;
}while(eeprom3[i]!=0);
EEPROM.write(0x30+i,0);
i=0;
do {
EEPROM.write(0x40+i,eeprom4[i]);
i++;
}while(eeprom4[i]!=0);
EEPROM.write(0x40+i,0);
}
Le premier octet étant à zéro, cela veut dire que c’est le premier login/MdP qui sera utilisé.
Le deuxième octet confirme que nous avons deux paires login/MdP
Le troisième octet à 0 nous dit que c’est la séquence "Utilisateur TAB MdP ENTER" qui sera utilisée.
Vous devez naturellement changer le contenu des tableaux eeprom1[] à eeprom4[] pour y mettre vos utilisateurs/MdP si vous voulez avoir l’occasion de tester.
Ce sous-programme peut alors être utilisé par un programme du genre :
#include <EEPROM.h>
void setup() {
init_eeprom();
}
void loop() {
}
Donnons une petite mise en garde sur l’utilisation de l'EEPROM avant de continuer.
N'écrivez pas pour un oui et pour un non dans l'EEPROM. C'est une ressource qui a un nombre d'écritures limité. Cela ira très bien pour une clé de connexion USB si vous n'en abusez pas. Dès que vous avez lancé ce programme vous devriez mettre init_eeprom() du setup en commentaire ! N'oubliez pas que le setup() est réalisé après chaque programmation ! |
Utiliser le login/MdP de l'EEPROM pour un essai
modifierOn vous demande maintenant d’utiliser le contenu de l'EEPROM pour tenter de vous connecter à un de vos compte. Le sous programme correspondant aura comme prototype : void fakeKeyboard(). Le mot anglais "fake" se traduit par "faire comme si".
void fakeKeyboard(){ //joue le role de clavier par défaut
char nbDefaut,adresse,i,sequence;
char login[16],motDePasse[16];
nbDefaut = EEPROM.read(0x00);
sequence = EEPROM.read(0x02);
adresse = 0x10 + nbDefaut * 0x20; // passage en adresse EEPROM
i=0;
do {
login[i] = EEPROM.read(adresse);
i++;adresse++;
} while ((login[i-1] != 0)&& (i<0x10));
login[i]=0;
adresse = 0x20 + nbDefaut * 0x20; // passage en adresse EEPROM
i=0;
do {
motDePasse[i] = EEPROM.read(adresse);
i++;adresse++;
} while ((motDePasse[i-1] != 0)&& (i<0x10));
motDePasse[i]=0;
Keyboard.begin();
switch(sequence) {
case 0x00 : Keyboard.print(login);
Keyboard.write(0x09);//tabulation
Keyboard.println(motDePasse);break;
case 0x01 : Keyboard.println(motDePasse);break;
}
Keyboard.end();
}
Ouvrez un éditeur de texte pour confirmer ou infirmer son fonctionnement correct. Si vous voulez l’utiliser pour un essai grandeur nature sur un de vos comptes, il vous faut conditionner l'appel de ce sous-programme à un front montant sur une entrée quelconque.
Un sous-programme pour afficher complètement le contenu de l'EEPROM par la liaison série
modifierÉcrire un sous-programme capable d'afficher le contenu de l'EEPROM par la liaison série. Un paramètre option lui sera passé. Si un 1 est présent sur le bit b7 de option alors les mots de passe seront masqués par des '*'. L'identifiant/MdP par défaut devra être précédé de ">>" et la séquence sera affichée en clair. Il n'y a pas beaucoup d’intérêt à afficher l'octet contenant le nombre de couples login/MdP.
// option = 0x80 brouille l’affichage des mots de passe
void afficheContenu(char option){
char nbDefaut,maxi,adresse,i,pos,sequence;
char chaine[17];
nbDefaut = EEPROM.read(0x00);
maxi = EEPROM.read(0x01);
sequence = EEPROM.read(0x02);
nbDefaut = 0x10 + nbDefaut * 0x20; // passage en adresse EEPROM
maxi = 0x10 + maxi * 0x20; // passage en adresse EEPROM
Serial.println("****Login / Mots de Passe ******");
for(i=0x10;i<maxi;i+=0x20){
pos=0;adresse = 0x00;
if (i==(nbDefaut+(option&0x07))) Serial.print(">> ");
do {
chaine[pos] = EEPROM.read(i+adresse);
adresse++;pos++;
} while ((chaine[pos-1] != 0)&& (adresse<0x10));
chaine[pos]=0;
Serial.print(chaine);Serial.write(' ');
adresse = 0x10;
pos=0;
//login est trouvé, au mot de passe maintenant
do {
chaine[pos] = EEPROM.read(i+adresse);
adresse++;pos++;
} while ((chaine[pos-1] != 0) && (adresse<0x20)); // mot de passe trouvé
chaine[pos]=0;
if (option & 0x80) {
--pos;--pos;
while(pos>=0)
chaine[pos--]='*';
}
Serial.println(chaine);
}
Serial.println("***** Sequence utilisee :");
switch (sequence) {
case 0x00 : Serial.println("Login TAB MotPasse ENTER");break;
case 0x01 : Serial.println("MotPasse ENTER");break;
}
}
Liaison série pour faire un choix
modifierLa mise à jour des logins/MdP doit pouvoir se réaliser à l'aide d'un simple hyperterminal et non pas à l'aide d'un environnement Arduino.
Au fur et à mesure des difficultés rencontrées à mettre au point le programme complet, nous avons abandonné petit à petit une gestion trop complexe de menus et sous-menus. Ainsi, le nombre de choix s'est limité au fur et à mesure. Nous avons cependant mis au point un sous-programme de choix série qui peut être utile à d'autres projets.
Partant du principe qu'un utilisateur auquel on demande de choisir entre deux valeurs max et min essaiera toujours un nombre en dehors de cet intervalle, pouvez-vous mettre au point un sous-programme de prototype "char readChoixSerie(char maxmin)" qui retournera le choix de l’utilisateur que s'il est entre min et max ? Si le choix est en dehors, il redemandera à l'utilisateur de choisir et ce dernier finira bien par abandonner. Les deux nombres max et min sont codés sur 4 bits et dans un même octet (max dans les 4 bits de poids fort et min dans les 4 bits de poids faible).
// lecture du choix de l'opérateur
char readChoixSerie(char maxmin){
char choix;
do {
Serial.print("Entrez votre choix entre ");
Serial.print(maxmin & 0x0F);
Serial.print(" et ");
Serial.println(maxmin >> 4);
while(Serial.available()==0); // attente de données
choix=Serial.read();
choix-='0';
while(Serial.available() > 0){
Serial.read();//perdu mais vide buffer
}
} while((choix > (maxmin>>4)) || (choix < (maxmin & 0x0F)));
return choix;
}
Liaison série pour mettre à jour
modifierLe sous-programme correspondant n’est pas encore complètement au point.
Réalisation matérielle
modifierJean-Michel Gérard, ingénieur d'étude à l'IUT de Troyes jusqu'en aout 2014, nous a réalisé une partie matérielle que nous allons présenter maintenant.
Le travail a consisté à prendre le schéma électronique libre de la platine Leonardo, à retirer tout ce qui est inutile et à ajouter trois boutons (en plus du reset). L'alimentation du Leonardo a été simplifiée. En effet, l’utilisation comme une clé nous impose systématiquement une alimentation par l'USB hôte. Pas besoin de rendre cette carte autonome avec une alimentation autre que l'USB.
Schéma de principe
modifierVoici le schéma de principe retenu :
Ce schéma réalisé avec Eagle est de qualité suffisante pour pouvoir être téléchargé et consulté à l'aide de zooms importants.
Les trois boutons (nommés plus loin) sont clairement visibles en haut du schéma où l’on découvre clairement qu’ils sont montés à l'aide d'une pull-up externe (pas forcément utile avec la famille AVR).
Trois LEDs sont aussi visibles en bas à droite du schéma.
Circuit imprimé
modifierLe circuit a été réalisé en deux couches. Nous en présentons une image même si celle-ci ne peut pas être utilisée telle quelle.
Recherche des noms Arduino des boutons et des LEDs
modifierLe standard Arduino pour les shields a été abandonné puisque l’on veut un circuit de la taille d'une clé USB (enfin un peu plus grand quand même). Ainsi les boutons ont été réalisés avec des pull-up externes mais reliés aux PORTs qui simplifiaient au mieux le circuit imprimé. Il s'agit de PB4, PB5 et PB6. Comme le nom de ces bits n’est pas utilisé en programmation Arduino, il nous faut trouver les correspondants symboliques (du langage Arduino). La technique pour cela est assez simple : il suffit de lire le schéma libre de la carte Léonardo dans cette page. C'est là qu'on voit l’intérêt d'un hardware libre.
nom des boutons | CR = ENTER | MODIF | CHOIX |
---|---|---|---|
PORT | PB4 | PB5 | PB6 |
Nom Arduino | 8 | 9 | 10 |
Trois LEDs sont présentes et doivent, elles-aussi, être associées à un nom Arduino.
PORT | PF5 | PF6 | PF7 |
---|---|---|---|
Nom Arduino | A2 | A1 | A0 |
- Le bouton ENTER sert à envoyer le login/MdP comme on le ferait avec un clavier
- le bouton MODIF sert à ajouter un login/MdP et à modifier le login/MdP par défaut et la séquence à l'aide d'un hyperterminal
- le bouton CHOIX fait la même chose que MODIF mais sans hyperterminal mais avec les trois LEDs (non encore réalisé en logiciel à ce jour)
Mise en place du bootloader Leonardo
modifierPuisque nous désirons programmer à partir de l'environnement Arduino il nous faut mettre le bootloader Arduino dans l'ATMega32U4. Cette opération est déjà réalisée lorsqu'on achète une platine Arduino mais pas lorsqu'on achète un 32U4 et qu'on le met sur un circuit que l’on a conçu. C'est à cause de cette opération qu'a été ajouté un programmateur ICSP 10 points. Une fois cette opération réalisée, le programmateur pourra être retiré si l’on veut un peu moins de volume à notre clé.
Mettre un bootloader dans un processeur n’est pas une opération difficile pour qui a déjà programmé un AVR en dehors de l'environnement Arduino. Par contre configurer correctement les fusibles est une autre histoire. Pour le moment nous allons nous contenter d'une copie d'écran concernant l'ATMega32U4 avec un environnement assez lisible :
Cette image devra être discutée pour une bonne compréhension. Mais ce sera fait plus tard.
Voir aussi
modifierNous avons rédigé un énoncé de projet sur ce thème dans le WIKI de l'IUT de Troyes :
- Clef USB reprend la problématique ci-dessus en commençant par travailler en RAM plutôt qu'en EEPROM. C'est quand les programmes seront au point que l’on passe à l’utilisation de l'EEPROM. Le but est d'économiser un peu l'EEPROM.
Et maintenant un site qui nous doit beaucoup :
- Utiliser un keyboard AZERTY avec Arduino site qui a pris pas mal de choses publiées ici, en particulier la partie pour passer de QWERTY en AZERTY.
- Lisez aussi cet article qui va un peu plus loin que le travail présenté ici.
Nous allons maintenant passer à un tout autre domaine : la réalisation d'un algorithme CORDIC dans un Arduino.
CORDIC sur Arduino
modifierNous allons évoquer le calcul de cosinus et sinus en format virgule fixe et ses performances.
Les besoins de tels calculs se retrouvent dans la régulation d'un moteur synchrone comme dans la robotique mobile. Mais l’utilisation de flottants avec un Arduino est très coûteux. Il faut donc l'éviter à tout prix. Pour cela on dispose du format virgule fixe que nous allons expliciter maintenant.
La représentation virgule fixe
modifierLe format Virgule fixe n’est pas normalisé contrairement au format virgule flottante. Le seul point commun que l’on retrouve dans tous les formats virgule fixe est qu’il est signé. Il peut donc représenter des nombres positifs ou négatifs en complément à 2. La virgule étant placée par défaut à un endroit fixe on utilise en général une notation du genre Qm.n pour le désigner où m représente le nombre de bits avant la virgule et n le nombre de bit après la virgule et donc m+n est la taille de la représentation.
Par exemple nous allons utiliser un format Q3.13 dans la suite de ce chapitre.
S |
Il s'agit d'un format 16 bits (3+13) et adapté à la trigonométrie en radian.
Puisque nous sommes intéressé par la conversion de ce format en flottant, nous allons présenter maintenant ce format qui est normalisé.
La représentation virgule flottante
modifierL'article Virgule_flottante de wikipédia est suffisamment complet pour ne pas être repris ici. Il peut d'ailleurs être complété par la lecture de l’article détaillé sur la norme IEEE 754 correspondante.
Voici en figure récapitulative le codage des nombres flottants.
Il existe essentiellement deux formats :
Encodage | Signe | Exposant | Mantisse | Valeur d'un nombre | Précision | Chiffres significatifs | |
Simple précision | 32 bits | 1 bit | 8 bits | 23 bits | 24 bits | environ 7 | |
Double précision | 64 bits | 1 bit | 11 bits | 52 bits | 53 bits | environ 16 |
Sur ces deux formats, seul le format simple précision sur 32 bits nous intéresse. Il peut se résumer avec la figure :
Transformation virgule fixe vers nombre réel et vice versa
modifierSi le cœur vous en dit, il vous est possible d'expérimenter l'écriture directe de nombre flottant en hexadécimal avec ensuite un affichage. Cela permet de comprendre un peu la mécanique des flottants. Voici par exemple un programme complet :
typedef union {
float f_temp; //taille : 4 octets
long int li_temp; //taille : 4 octets
} t_u;
void setup(){
Serial.begin(9600);
}
void loop(){
t_u u;
u.li_temp = 0X3F400000;
Serial.println(u.f_temp,4);
delay(5000);
}
Remarquez que l’on utilise une "union" pour définir en même temps 32 bits manipulables directement et le flottant associé. Cette façon de faire sera utilisée lors des conversions. Ce code affiche sans arrêt 0.7500 : auriez-vous été capable de prévoir cela ?
Le code présenté maintenant est lié au travail réalisé pour un autre livre sur VHDL (chapitre CORDIC). Nous présentons donc un code compact adapté à l'embarqué.
void setup() {
Serial.begin(9600);
}
float e=1.76;
void loop() {
int i;
char chaine[10];
i= float2HexQ3_13_AVR(e);
HexQ3_13ToString(i,chaine);
Serial.print("Version 1 : ");
Serial.println(chaine);
Serial.print("Version 2 : ");
e=HexQ3_13ToFloat_AVR(i);
Serial.println(e,4);
e -= 0.1;
delay(5000);
}
//************************************************************************
// function HexQ3_13ToFloat_AVR()
// purpose: transformation of a 16-bit Q3.13 number into 32-bit float number
// arguments:
// corresponding Q3.13 number
// return: 32-bit float
// note: This function works on Arduino Mega 2560 and UNO
//************************************************************************
float HexQ3_13ToFloat_AVR(int val){
union {
float f_temp; //taille : 4 octets
long int li_temp; //taille : 4 octets
} u;
long int i_temp;
unsigned char exposant=129;//,*p;
signed char i;
if (val < 0) i_temp = -val; else i_temp = val;
for (i=15;i>=0;i--) {
if (i_temp & (1<<i)) {
// on efface le '1' trouvé :
i_temp= i_temp & ~(1<<i);
break;// on sort de la boucle
}
exposant--;
}
u.li_temp = exposant;
u.li_temp <<=23;
u.li_temp = u.li_temp|(i_temp << (23-i));
if (val < 0)
u.f_temp = -u.f_temp;//-u.f_temp;
return u.f_temp;
}
//************************************************************************
// function float2HexQ3_13()
// purpose: transformation of a 32-bit float number into Q3.13
// arguments:
// corresponding float number and the returned string
// return: integer in Q3.13 format
// note:
//************************************************************************
int float2HexQ3_13(float val){ //conversion float vers Q3.13
int temp;
char i;
float f_temp;
if (val < 0) f_temp = -val; else f_temp = val;
temp = ((int) floor(f_temp)<<13);
f_temp = f_temp - floor(f_temp);
for (i=0;i<13;i++) {
temp|=((int)floor(2*f_temp)<<(12-i));
f_temp = 2*f_temp - floor(2*f_temp);
}
if (val < 0) return -temp; else return temp;
}
//************************************************************************
// function float2HexQ3_13_AVR()
// purpose: transformation of a 32-bit float number into Q3.13
// arguments:
// corresponding float number and the returned string
// return: integer in Q3.13 format
// note: idem to float2HexQ3_13 but without float library
// utilise 500 octets de moins que la précédente
//************************************************************************
int float2HexQ3_13_AVR(float val){ //conversion float vers Q3.13
union {
float f_temp; //taille : 4 octets
long int li_temp; //taille : 4 octets
} u,v;
unsigned char exposant;
long int mantisse;
int result=0;
u.f_temp = val;
if (val < 0) u.f_temp = - u.f_temp;
v = u;
// recupération de l'exposant
v.li_temp >>= 23;
exposant = v.li_temp;
// recuperation mantisse
if (exposant < 129) {
mantisse = (u.li_temp & 0X007FFFFF);
// mise à 1 du bit manquant
result |= (1 << (exposant-114)); // (15-129+exposant)); semble OK
mantisse >>= (137-exposant); //(22-(14-129+exposant));
result |= mantisse;
if (val < 0) return -result;
else return result;
} else {
//**** "Erreur conversion"*****
return 0;
}
}
//************************************************************************
// function HexQ3_13ToString()
// purpose: transformation of a Q3.13 number into string
// arguments:
// corresponding Q3.13 number and the returned string
// return:
// note: only four decimal digit of too lengthy numbers are calculated !!
//************************************************************************
void HexQ3_13ToString(int valQ3_13,char str[]){
unsigned int valQ3_13b;
char digit;
if (valQ3_13 < 0) { // eviter problèmes de signe !!!
str[0] = '-';
valQ3_13 = -valQ3_13;
} else
str[0] = '+';
digit = valQ3_13 >> 13;
str[1]= digit + '0';
str[2]= '.';
valQ3_13 &= 0x1FFF; // on retire les 3 bits de poids fort
valQ3_13 = valQ3_13 * 5; // *5 pour tenir dans 16 bits
valQ3_13b = valQ3_13;
valQ3_13b >>= 12; // on ne garde que les 4 bits de poids fort
str[3] = valQ3_13b +'0';
valQ3_13 &= 0x0FFF; // on retire les 4 bits de poids fort
valQ3_13 = valQ3_13 * 10; //enfin le *10
valQ3_13b = valQ3_13;
valQ3_13b >>= 12; // on ne garde que les 4 bits de poids fort
str[4] = valQ3_13b +'0';
valQ3_13 &= 0x0FFF; // on retire les 4 bits de poids fort
valQ3_13 = valQ3_13 * 10;
valQ3_13b = valQ3_13;
valQ3_13b >>= 12; // on ne garde que les 4 bits de poids fort
str[5] = valQ3_13b +'0';
valQ3_13 &= 0x0FFF; // on retire les 4 bits de poids fort
valQ3_13 = valQ3_13 * 10;
valQ3_13b = valQ3_13;
valQ3_13b >>= 12; // on ne garde que les 4 bits de poids fort
str[6] = valQ3_13b +'0';
str[7]=0;
}
Le calcul CORDIC et son test associé
modifierL'article CORDIC de wikipédia donne un morceau de code en C. Ce code de principe utilise le type float ainsi que des opérations sur ce flottants. Nous avons déjà eu l’occasion d’en présenter une transformation dans le chapitre sur la base robotique mobile ASURO. En voici maintenant une version complètement Arduino qui utilise la liaison série pour demander un angle et utilise cette même liaison pour afficher le résultat du calcul.
void setup() {
Serial.begin(9600);
}
void loop() {
int i;
int sinus, cosinus;
char chaine[10];
float angle;
Serial.println("Entrez un angle : ");
if (Serial.available()) {
angle=Serial.parseFloat();
i= float2HexQ3_13(angle); // ou i= float2HexQ3_13_2(angle); pour gagner 500 octets
cordic(i,&sinus,&cosinus);
Serial.print("Sinus=");
HexQ3_13ToString(sinus,chaine);
Serial.println(chaine);
Serial.print("Cosinus=");
HexQ3_13ToString(cosinus,chaine);
Serial.println(chaine);
}
delay(1000);
}
//************************************************************************
// function HexQ3_13ToFloat_AVR()
// purpose: transformation of a 16-bit Q3.13 number into 32-bit float number
// arguments:
// corresponding Q3.13 number
// return: 32-bit float
// note: This function works on Arduino Mega 2560
//************************************************************************
float HexQ3_13ToFloat_AVR(int val){
union {
float f_temp; //taille : 4 octets
long int li_temp; //taille : 4 octets
} u;
long int i_temp;
unsigned char exposant=129;//,*p;
signed char i;
if (val < 0) i_temp = -val; else i_temp = val;
for (i=15;i>=0;i--) {
if (i_temp & (1<<i)) {
// on efface le '1' trouvé :
i_temp= i_temp & ~(1<<i);
break;// on sort de la boucle
}
exposant--;
}
u.li_temp = exposant;
u.li_temp <<=23;
u.li_temp = u.li_temp|(i_temp << (23-i));
if (val < 0)
u.f_temp = -u.f_temp;//-u.f_temp;
return u.f_temp;
}
//************************************************************************
// function float2HexQ3_13()
// purpose: transformation of a 32-bit float number into Q3.13
// arguments:
// corresponding float number and the returned string
// return: integer in Q3.13 format
// note:
//************************************************************************
int float2HexQ3_13(float val){ //conversion float vers Q3.13
int temp;
char i;
float f_temp;
if (val < 0) f_temp = -val; else f_temp = val;
temp = ((int) floor(f_temp)<<13);
f_temp = f_temp - floor(f_temp);
for (i=0;i<13;i++) {
temp|=((int)floor(2*f_temp)<<(12-i));
f_temp = 2*f_temp - floor(2*f_temp);
}
if (val < 0) return -temp; else return temp;
}
//************************************************************************
// function float2HexQ3_13_AVR()
// purpose: transformation of a 32-bit float number into Q3.13
// arguments:
// corresponding float number and the returned string
// return: integer in Q3.13 format
// note: idem to float2HexQ3_13 but without float library
// utilise 500 octets de moins que la précédente
//************************************************************************
int float2HexQ3_13_AVR(float val){ //conversion float vers Q3.13
union {
float f_temp; //taille : 4 octets
long int li_temp; //taille : 4 octets
} u,v;
unsigned char exposant;
long int mantisse;
int result=0;
u.f_temp = val;
if (val < 0) u.f_temp = - u.f_temp;
v = u;
// recupération de l'exposant
v.li_temp >>= 23;
exposant = v.li_temp;
// recuperation mantisse
if (exposant < 129) {
mantisse = (u.li_temp & 0X007FFFFF);
// mise à 1 du bit manquant
result |= (1 << (exposant-114)); // (15-129+exposant)); semble OK
mantisse >>= (137-exposant); //(22-(14-129+exposant));
result |= mantisse;
if (val < 0) return -result;
else return result;
} else {
//**** "Erreur conversion"*****
//Serial.println("Erreur conversion");
return 0;
}
}
//************************************************************************
// function HexQ3_13ToString()
// purpose: transformation of a Q3.13 number into string
// arguments:
// corresponding Q3.13 number and the returned string
// return:
// note: only four decimal digit of too lengthy numbers are calculated !!
//************************************************************************
void HexQ3_13ToString(int valQ3_13,char str[]){
unsigned int valQ3_13b;
char digit;
if (valQ3_13 < 0) { // eviter problèmes de signe !!!
str[0] = '-';
valQ3_13 = -valQ3_13;
} else
str[0] = '+';
digit = valQ3_13 >> 13;
str[1]= digit + '0';
str[2]= '.';
valQ3_13 &= 0x1FFF; // on retire les 3 bits de poids fort
valQ3_13 = valQ3_13 * 5; // *5 pour tenir dans 16 bits
valQ3_13b = valQ3_13;
valQ3_13b >>= 12; // on ne garde que les 4 bits de poids fort
str[3] = valQ3_13b +'0';
valQ3_13 &= 0x0FFF; // on retire les 4 bits de poids fort
valQ3_13 = valQ3_13 * 10; //enfin le *10
valQ3_13b = valQ3_13;
valQ3_13b >>= 12; // on ne garde que les 4 bits de poids fort
str[4] = valQ3_13b +'0';
valQ3_13 &= 0x0FFF; // on retire les 4 bits de poids fort
valQ3_13 = valQ3_13 * 10;
valQ3_13b = valQ3_13;
valQ3_13b >>= 12; // on ne garde que les 4 bits de poids fort
str[5] = valQ3_13b +'0';
valQ3_13 &= 0x0FFF; // on retire les 4 bits de poids fort
valQ3_13 = valQ3_13 * 10;
valQ3_13b = valQ3_13;
valQ3_13b >>= 12; // on ne garde que les 4 bits de poids fort
str[6] = valQ3_13b +'0';
str[7]=0;
}
//************************************************************************
// function cordic()
// purpose: calculation of sine and cosine of an Q3.13 angle
// arguments:
// corresponding Q3.13 number and the returned Q3.13
// return:
// note:
//************************************************************************
void cordic(int beta,int *sinus,int *cosinus) {
unsigned char i;
int K = 0x136F; //=0.6073 en Q3.13
int atantb[14]={0x1921,0xED6,0x7D6,0x3FA,0x1FF,0xFF,0x7F,0x3F,0x1F,0xF,0x7,0x3,0x2,0x1};
int x_Nouveau;
*cosinus = K;
*sinus=0;
// itération CORDIC proprement dite :
for(i = 0; i < 14; i++) {
// Si beta<0 rotation dans le sens trigo
if(beta < 0) {
x_Nouveau = *cosinus + (*sinus>>i);
*sinus -= *cosinus>>i;
beta += atantb[i];
}
// sinon dans l'autre sens
else {
x_Nouveau = *cosinus - (*sinus>>i);
*sinus += (*cosinus>>i);
beta -= atantb[i];
}
*cosinus = x_Nouveau;
}
}
Des comparaisons avec une calculette montrent trois chiffres significatifs corrects ce qui est un bon résultat. Pour information, la taille du binaire correspondant à ce code est de 5784 octets contre plus de 8000 pour le code initial. Retirer "int float2HexQ3_13(float val)" qui n’est pas toujours utile permet de descendre à 3900 octets... et ajouter un "Serial.println(cos(angle));" nous amène tout de suite à 7148 octets ! Ces chiffres ne sont pas importants en soi mais nous montrent que tout programmeur doit avoir un peu de réticence à utiliser des types flottants.
Voir aussi sur CORDIC
modifier- CORDIC
- Réalisation d'un coprocesseur CORDIC en VHDL dans la Wikiversité.
SPI maître/esclave avec Arduino
modifierNous allons laisser ici un exemple de code pour connecter deux Arduinos :
- le premier est un Arduino nano et sera considéré comme le maître
- le deuxième est un Arduino UNO et sera considéré comme esclave
Le type de carte Arduino utilisé n'a en fait que peu de répercussion sur le code présenté plus bas. Seuls les branchements sont dépendants du type de carte Arduino utilisée.
Broches SPI | MOSI | MISO | SCK | SS |
---|---|---|---|---|
Arduino UNO Pin (esclave) | 11 | 12 | 13 | 10 |
Arduino Nano Pin (Maître) | P11 | P12 | P13 | P10 ou autre |
Notre objectif est de réaliser une configuration telle que bien évidemment c'est le maître qui décide la demande à l'esclave de données. L'esclave est donc supposé faire des calculs sans arrêt et livrer ses données seulement quand le maître le demande.
Réalisation du maître
modifierLa réalisation du maître ne pose aucune difficulté car la librairie Arduino standard peut être utilisée pour cela. Voici donc le code.
#include <SPI.h>
const int slaveSelectPin = 10; //n'importe quel bit de port peut être utilise ici
void setup() {
// set the slaveSelectPin as an output:
Serial.begin(9600);
pinMode(slaveSelectPin, OUTPUT);
// initialize SPI:
SPI.begin();
}
void loop() {
uint8_t dataSlave;
digitalWrite(slaveSelectPin, LOW);
dataSlave=SPI.transfer(0);
digitalWrite(slaveSelectPin, HIGH);
Serial.println(dataSlave);
delay(1000);
}
Réalisation de l'esclave
modifierLa réalisation de l'esclave demande un peu plus d'attention. Nous avons en effet décidé d'utiliser la fonction standard d'initialisation de la librairie SPI (à savoir SPI.begin()) qui ne sait qu'initialiser en SPI Maître. Il ne faut donc pas oublier de passer MISO en sortie (sur l'esclave) et MOSI en entrée.
Il faut aussi naturellement passer le bit MSTR du registre SPCR à 0 pour passer en mode esclave.
L'esclave fonctionnera en interrruption et sera occupé à faire des calculs le reste du temps.
#include <SPI.h>
const int slaveSelectPin = 10; // absolument obligatoire pour un esclave !!!!
volatile boolean process;
uint8_t cmpt=0;
volatile uint8_t c=0;
void setup() {
// set the slaveSelectPin as an output:
pinMode(slaveSelectPin, INPUT);
// initialize SPI:
SPI.begin();
// slave select
//SPCR |= (1<<SPE)|(1<<SPIE);
SPCR &= ~(1<<MSTR); // turn on SPI in slave mode
SPI.attachInterrupt(); // turn on interrupt
pinMode(12,OUTPUT); //12=MISO pas fait par SPI.begin
pinMode(11,INPUT); //11=MOSI pas fait par SPI.begin
process = false;
sei();
}
ISR (SPI_STC_vect) // SPI interrupt routine
{
c = SPDR; // read byte from SPI Data Register
process = true;
}
void loop()
{
if (process) {
cmpt++;
process = false;
SPDR = cmpt;
}
}
Tout cela fonctionne correctement sans grande difficultés. Les difficultés arrivent quand on veut transmettre des données plus grandes que l'octet. C'est à ce sujet que nous allons nous intéresser maintenant.
Transmissions de données de plusieurs octets
modifierNous restons dans les conditions particulières que nous avons eu à affronter en robotique mobile : c'est l'esclave qui a des données à transmettre au maître, mais ceci se fait à l'initiative du maître. Le problème à résoudre dans ce cas est un problème de synchronisation. Si l'on veut que l'esclave donne ses données les plus récentes il lui faut un moyen pour savoir s'il a à livrer l'octet de poids fort ou l'octet de poids faible. Si l'on se contente de livrer une fois sur deux l'octet de poids faible et l'octet de poids fort il est certain que vous aurez des problèmes comme le montre la figure. Pour bien comprendre le problème il vous faut remarquer plusieurs choses :
- le maître exécute sa vie. C'est lui qui décide quand il va envoyer les deux interruptions pour lire les données des capteurs.
- le temps entre les deux interruptions pour lecture de données et les prochaine n'est pas du tout égal. Cela peut être 10 ms contre 0.2 ms soit un rapport de 50
- l'écriture SPR = quelque chose dans les interruptions n'est pris en compte que l'interruption suivante ! Et c'est là qu'est le problème !
Si vous réalisez ce qui est montré dans la figure, les poids faible et fort de la variable pourront donc avoir 10 ms d'écart. Si les données capteurs ne varient pas beaucoup cela peut n'avoir que des conséquences négligeables. Mais si les données varient rapidement les conséquences peuvent être désastreuses.
Pour information, le contenu de la partie code du maître se décomposant en "lecture capteur", "planification" et "action" est typique de la robotique. Elle n'a pas une importance capitale ici et les problèmes évoqués sont indépendants de ce que fait le maître. Certains roboticiens découpent ces actions de manière plus fine encore :
- Observe Orient Decide Act (OODA) (en français Observe s'Oriente Décide Agit a son article dans Wikipédia)
Pour commencer, nous allons quand même nous intéresser au cas de données 16 bits en essayant de résoudre le problème de synchronisation tout juste évoqué.
Cas d'une donnée 16 bits
modifierPour réaliser la synchronisation, le maître fait trois écritures pour récupérer deux octets. C'est ce que montre la figure ci-contre. La première interruption permet d'écrire le poids faible dans SPR qui sera lu par la deuxième lecture qui écrit le poids fort dans SPR qui sera pris en compte dans la troisième lecture.
Nous avons découvert qu'il faut un délai minimum entre les écritures SPI pour un fonctionnement correct. Il est probable que la raison en soit l'attente de la fin de la réalisation de l'interruption sur l'esclave avant d'envoyer d'autres données... sans quoi l'interruption suivante peut ne pas se réaliser.
- SPI maître pour transmettre 2 octets :
#include <SPI.h>
#define START 128
const int slaveSelectPin = 10; // ou autre
uint16_t data=0;// donnée a transmettre
void setup() {
// set the slaveSelectPin as an output:
Serial.begin(9600);
pinMode(slaveSelectPin, OUTPUT);
// initialize SPI:
SPI.begin();
}
void loop() {
uint8_t dataSlave;
digitalWrite(slaveSelectPin, LOW);
dataSlave=SPI.transfer(START);
//digitalWrite(slaveSelectPin, HIGH);
data=0;
//digitalWrite(slaveSelectPin, LOW);
delayMicroseconds(200);
dataSlave=SPI.transfer(8);
//digitalWrite(slaveSelectPin, HIGH);
data = dataSlave;
data <<= 8;
//digitalWrite(slaveSelectPin, LOW);
delayMicroseconds(200);
dataSlave=SPI.transfer(0);
data += dataSlave;
digitalWrite(slaveSelectPin, HIGH);
Serial.println(data);
delay(1000);
}
- SPI esclave pour transmettre 2 octets :
#include <SPI.h>
const int slaveSelectPin = 10;
volatile boolean process;
uint16_t cmpt=0;
volatile uint8_t c=0;
void setup() {
Serial.begin(9600);
// set the slaveSelectPin as an output:
pinMode(slaveSelectPin, INPUT);
// initialize SPI:
SPI.begin();
// slave select
//SPCR |= (1<<SPE)|(1<<SPIE);
SPCR &= ~(1<<MSTR); // turn on SPI in slave mode
SPI.attachInterrupt(); // turn on interrupt
pinMode(12,OUTPUT); //12=MISO pas fait par SPI.begin
pinMode(11,INPUT); //11=MOSI pas fait par SPI.begin
process = false;
sei();
}
ISR (SPI_STC_vect) // SPI interrupt routine
{
c = SPDR; // read byte from SPI Data Register
if (c==128) SPDR = (cmpt >> 8); else
if (c==8) SPDR = (cmpt); else
SPDR = 0xFF;
process = true;
}
void loop()
{
//cmpt++;
//delay(500);
if (process) {
cli(); // Ne pas déranger
cmpt++;
sei();
process = false;
//SPDR = cmpt;
Serial.println(c);
}
//SPDR = 10;
}
Cas d'une donnée de type float
modifierLes données de type float sont sur 32 bits (4 octets) sur à peu près toutes les architectures. C'est le cas sur les AVR avec le compilateur avrgcc.
Pour réaliser la synchronisation nous allons utiliser aussi une écriture supplémentaire à savoir ici 5 lectures par le maître.
- Esclave Float
#include <SPI.h>
const int slaveSelectPin = 10;
volatile boolean process;
uint16_t cmpt=0;
volatile uint8_t c=0;
union {
float f_temp;
uint32_t temp;
} Bx;
void setup() {
//Serial.begin(9600);
// set the slaveSelectPin as an output:
pinMode(slaveSelectPin, INPUT);
// initialize SPI:
SPI.begin();
// slave select
//SPCR |= (1<<SPE)|(1<<SPIE);
SPCR &= ~(1<<MSTR); // turn on SPI in slave mode
SPI.attachInterrupt(); // turn on interrupt
pinMode(12,OUTPUT); //12=MISO pas fait par SPI.begin
pinMode(11,INPUT); //11=MOSI pas fait par SPI.begin
process = false;
sei();
Bx.f_temp=0.0;
}
ISR (SPI_STC_vect) // SPI interrupt routine
{
c = SPDR; // read byte from SPI Data Register
if (c==0) SPDR = (Bx.temp >> 24); else
if (c==1) SPDR = (Bx.temp >> 16); else
if (c==2) SPDR = (Bx.temp >> 8); else
if (c==3) SPDR = (Bx.temp); else
SPDR = 0xFF;
process = true;
}
void loop()
{
//cmpt++;
//delay(500);
if (process) {
cli(); // en aucun cas être dérangé !!!!
Bx.f_temp += 0.1;
sei();
process = false;
//SPDR = cmpt;
//Serial.println(c);
}
//SPDR = 10;
}
- Maître float :
#include <SPI.h>
#define START 0
const int slaveSelectPin = 10;
uint16_t data=0;
union {
float f_temp;
uint32_t temp;
} Bx;
void setup() {
// set the slaveSelectPin as an output:
Serial.begin(9600);
pinMode(slaveSelectPin, OUTPUT);
// initialize SPI:
SPI.begin();
}
void loop() {
uint8_t dataSlave;
digitalWrite(slaveSelectPin, LOW);
dataSlave=SPI.transfer(START);
//digitalWrite(slaveSelectPin, HIGH);
Bx.f_temp=0.0;
//digitalWrite(slaveSelectPin, LOW);
delayMicroseconds(200);
//delay(1);
dataSlave=SPI.transfer(1);
//digitalWrite(slaveSelectPin, HIGH);
Bx.temp += dataSlave;
Bx.temp <<= 8;
//digitalWrite(slaveSelectPin, LOW);
delayMicroseconds(200);
//delay(1);
dataSlave=SPI.transfer(2);
Bx.temp += dataSlave;
Bx.temp <<= 8;
delayMicroseconds(200);
//delay(1);
dataSlave=SPI.transfer(3);
//digitalWrite(slaveSelectPin, HIGH);
Bx.temp += dataSlave;
Bx.temp <<= 8;
//digitalWrite(slaveSelectPin, LOW);
delayMicroseconds(200);
//delay(1);
dataSlave=SPI.transfer(4);
//digitalWrite(slaveSelectPin, HIGH);
Bx.temp += dataSlave;
//digitalWrite(slaveSelectPin, LOW);
digitalWrite(slaveSelectPin, HIGH);
Serial.println(Bx.f_temp);
delay(1000);
}
Pour pouvoir utiliser correctement le découpage des flottants en octets on a élé obligé d'utiliser une union qui permet de superposer deux types différents sur une même donnée. Par exemple quand on travaille sur le flottant on utilise le champ .f_temp et quand on veut réaliser des décalages on utilise le champ .temp
Encore de la synchronisation
modifierNous avons montré comment résoudre le problème de synchronisation des données entre un esclave et un maître. Mais tous les problèmes sont-ils résolus pour autant ?
La réponse est négative malheureusement. Pourquoi ? A cause de l'interruption dans l'esclave !
En effet l'esclave a pour objectif de lire des capteurs et éventuellement de faire des calculs (qui peuvent être compliqués pour un accéléromètre). Une fois ces calculs réalisé, le programme mettra à jour la variable qui sera découpée en tranche par l'interruption. Que se passe-t-il si l'interruption a lieu en pleine mis à jour de cette variable ? Cela peut être aussi dramatique que ce que l'on a cherché à résoudre avec une lecture supplémentaire des données SPI. La seule différence est qu'il y en a une qui peut arriver systématiquement (c'est pour cela qu'on a résolu le problème) et l'autre qui n'a qu'une petite probabilité d'arriver. En informatique théorique, on chercherait à résoudre ce problème même si la probabilité est très faible. Pour nous, que dire, on prend le risque.
Voici cependant le résultat donné par notre programme de transmission d'un flottant :
9242.57 9243.07 9243.57 9244.06 9244.56 9245.06 9245.56 9246.06 9216.50 9247.20 9247.70 9248.20 8256.50 8256.50
On y distingue quelques dysfonctionnements en particulier les deux dernières lignes ! On attend en effet un compteur qui compte de 0.5 en 0.5. Mais nous ne savons pas dire pour le moment si cela est dû au problème évoqué dans cette section ou à un autre. Il nous faudra investiguer plus loin plus tard.
Pour aller plus loin sur le domaine, lire Parallélisme Informatique dans Wikipédia.
Complément : utiliser un compilateur C avec une carte Arduino
modifierNous allons examiner comment compiler un programme C et le télécharger dans une carte Arduino. Nous supposerons que l'environnement Arduino est installé et que l’on travaille sous Linux. Ceci nous permet de supposer que le compilateur C avr-gcc est correctement installé ainsi que le logiciel de téléchargement avrdude.
Pour simplifier nous donnons directement un script bash capable de réaliser l’ensemble :
#!/bin/bash
avr-gcc -g -mmcu=atmega2560 -Wall -Os -c Mega2560Demo1.c
avr-gcc -g -mmcu=atmega2560 -o Mega2560Demo1.elf -Wl,-Map,Mega2560Demo1.map Mega2560Demo1.o
#avr-objdump -h -S hello.elf > hello.lss
#avr-objcopy -O binary -R .eeprom hello.elf hello.bin
avr-objcopy -R .eeprom -O ihex Mega2560Demo1.elf Mega2560Demo1.hex
/usr/share/arduino/hardware/tools/avrdude -C/usr/share/arduino/hardware/tools/avrdude.conf -v -v -v -v -patmega2560 -cwiring -P/dev/ttyACM0 -b115200 -D -V -Uflash:w:Mega2560Demo1.hex:i
Ce script fonctionne pour un Arduino MEGA2560 et vous devrez l'adapter en changeant le processeur pour les autres cartes.
Voici un programme d'essai pour la carte MEGA2560 :
//**** OK : Arduino MEGA2560 + shield
#include <avr/io.h>
main(void)
{
DDRB = 0xFF; // 8 sorties pour B
DDRE = 0x00; // 8 entrées pour E
while(1)
PORTB = PINE; // recopie du PORTE dans le PORTB qui allume les LEDs
}
Ce programme fonctionne correctement mais nécessite un shield qui est décrit ICI (WIKI interne à GEII Troyes)
Exercice 1
modifierLes LEDs du shield présenté dans la section précédente sont reliées aux broches 6 à 13 de l'arduino MEGA2560. Une lecture de son schéma fait apparaître la correspondance :
Numéro | f5 | f4 | f3 | f2 | f1 | f0 | p1 | p0 |
---|---|---|---|---|---|---|---|---|
Couleur | r | o | v | r | o | v | v | r |
Arduino Pin | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 |
Port | PB7 | PB6 | PB5 | PB4 | PH6 | PH5 | PH4 | PH3 |
Réaliser en C un programme réalisant un chenillard simple.
Solution de l'exercice 1
modifierVoici le programme :
//**** OK : Arduino MEGA2560 + shield
#include <avr/io.h>
#undef F_CPU
#define F_CPU 16000000UL
#include <util/delay.h>
void afficheLeds(unsigned char ch){
unsigned char ch_partie;
ch_partie = (ch << 3) & 0x78;
PORTH = ch_partie;
ch_partie = ch & 0xF0;
PORTB = ch_partie;
}
void main(void)
{
unsigned char ch,i;
DDRH = 0x78; // 4 sorties pour H
DDRB = 0xF0; // 4 sorties pour B
while(1) {
ch = 1;
for (i=0;i<8;i++) {
afficheLeds(ch);
_delay_ms(1000);
ch <<=1;
}
}
}
Sur notre shield, la led de poids fort ne fonctionne pas correctement avec le MEGA2560 car une LED jaune est déjà reliée sur PB7. Mais ce programme fonctionne parfaitement si vous utilisez une plaque d'essai sans soudure avec votre led et votre résistance.
Exercice 2
modifierMettre en œuvre le timer0 sur le MEGA2560 avec un prescaler à 1024 et une attente active du bit T0V0 du registre TIFR0. On basculera le bit b4 du PORTB qui est relié à une LED. Quelle est la fréquence de basculement du bit b4 si la fréquence du quartz est 16 MHz ?
Solution de l'exercice 2
modifierLe nom des registres est un peu changé par rapport à un ATMega8.
//**** OK : Arduino MEGA2560 + shield
#include <avr/io.h>
void main(void){
// initialisation du timer division par 1024
TCCR0B = 0x05; // prescaler 1024 , entrée sur quartz
TCNT0 = 0x00; // tmr0 : début du comptage dans 2 cycles
// bit RB4 du PORTB en sortie
DDRB |= 0x10; //RB4 as output
while(1) {
TIFR0 |= 0x01; // clr TOV0 with 1
while ((TIFR0 & (0x01<<TOV0)) == 0);
PORTB ^= 0x10; // on bascule avec ou exclusif
TIFR0 &= ~(1 << TOV0); // reset the overflow flag
}
}
Fréquence 16 000 000 /(1024*256*2) = 32 Hz
D'après ce résultat une division par 32 doit donner 1 Hz. On le fait par exemple par interruption, toujours avec un MEGA2560 :
//**** OK : Arduino MEGA2560 + shield
#include <avr/io.h>
#include <avr/interrupt.h>
// compteur
volatile unsigned char cpt=0;
// Fonction de traitement Timer 0 OverFlow
ISR(TIMER0_OVF_vect){
cpt++;
if(cpt==31) {
PORTB ^=(1<<PB4);
cpt=0;
}
}
void main(){
// IT Timer0 Over Flow Active
TIMSK0=(1<<TOIE0);
// Prescaler 1024 (Clock/1024)
TCCR0B = (1<<CS02) | (1<<CS00);
//Configuration PORTB.4 en sortie
DDRB |= (1<<DDB4);
PORTB &= ~(1<<PB4); // PORTB.4 <-0
//activation des IT (SREG.7=1)
// sei();
SREG |= 0x80; // equivalent à sei()
while(1);
}
Nous n'avons pas utilisé "sei()" mais il est naturellement possible de le faire.
Améliorations
modifierUn makefile serait probablement plus adapté à la situation car ici une erreur de compilation chargera le fichier HEX (qui sera de ce fait un fichier plus ancien).
Pour commencer nous donnons un makefile trouvé sur Internet qu’il nous faudra essayer, adapter et éventuellement traduire :
# tools
AVRDUDE=avrdude -F -V
OBJCOPY=avr-objcopy
CC=avr-gcc
RM=rm -f
# parameters
MCU=atmega328
F_CPU=16000000UL
BIN_FORMAT=ihex
PORT=/dev/cuaU0
BAUD=57600
PROTOCOL=arduino
PART=ATMEGA3290
CFLAGS=-Wall -Os -DF_CPU=$(F_CPU) -mmcu=$(MCU)
PROG=addr
SUDO=sudo
.SUFFIXES: .elf .hex
.c.elf:
$(CC) $(CFLAGS) -o $@ $<
.elf.hex:
$(OBJCOPY) -O $(BIN_FORMAT) -R .eeprom $< $@
.PHONY: all
all: ${PROG}.hex
${PROG}.hex: ${PROG}.elf
${PROG}.elf: ${PROG}.c
.PHONY: clean
clean:
$(RM) ${PROG}.elf ${PROG}.hex
.PHONY: upload
upload: ${PROG}.hex
${SUDO} $(AVRDUDE) -c $(PROTOCOL) -p $(PART) -P $(PORT) \
-b $(BAUD) -U flash:w:${PROG}.hex
Rester dans l'environnement Arduino pour faire du C
modifierNous avons découvert qu’à priori on peut écrire du C dans l'environnement Arduino. Nous voulons dire un programme avec un main() et sans setup() ni loop(). Ceci se fait dans l'éditeur Arduino et la compilation comme le téléversement se fait comme pour un sketch Arduino. C'est pratique cela évite de lancer "avrdude" en ligne de commande par exemple et de trop se poser des questions sur les commandes à réaliser.
En clair, chaque fois que vous mettez un main() dans un programme vous court-circuitez partiellement l'initialisation qui est faite avant le setup() ! Et du coup certains #include ne sont plus nécessaires.
Voici un exemple qui a été testé sur une carte UNO.
Création de 1 kHz sur PB3 par attente passive du drapeau de débordement du timer 2
modifierPour information PB3 est repéré par l'entrée/sortie numéro 11 sur la platine UNO comme le montre le schéma officiel.
On choisit une division par 128, qui demande alors un comptage de 62,5 arrondi ici à 63. Ce programme a été testé correct avec un oscilloscope sur une carte UNO et a été écrit tel quel dans l'environnement Arduino.
//*************** Testé OK pour UNO
// le #include <avr/io.h> habituel est complètement inutile
int main() {
// variables
// e/S
DDRB |= (1<<PB3);
// configuration du timer2
TCCR2A &= ~(1<<WGM20);
TCCR2A &= ~(1<<WGM21);
TCCR2B &= ~(1<<WGM22);
// prescaler à (101) soit division par 128
TCCR2B |= (1<<CS22);
TCCR2B &= ~(1<<CS21);
TCCR2B |= (1<<CS20);
TIFR2 |= (1<<TOV2);
TCNT2=0;
for (;;) {
// Attendre un débordement du timer
while (!(TIFR2 & (1<<TOV2))) ;
// Acquitter le débordement
TIFR2 |= (1<<TOV2);
TCNT2 += 193; //256-63 on peut aussi utiliser TCNT2 = 193;
// Changer l'état de la sortie
PORTB ^= (1<<PB3);
}
}
La valeur 193 peut être ajustée pour être le plus proche possible de 1kHz.
Voir aussi
modifier- Premiers Pas en Informatique Embarquée document de cours (de Simon Landrault et al.)
- Arduino et moteur à courant continu
- Les entrées analogiques de l'Arduino
- Réaliser un télémètre à ultrason
- Le chapitre sur le robot pololu de ce livre qui peut utiliser l'environnement Arduino pour sa programmation.
- La page officielle en anglais de l'Arduino
- Documents ATMEL sur le 32U4
- Sparkfun tutorial sur le HID
- Un manuel processing en français
- Communication Arduino/Processing