« SE3 2025/2026 EC3 » : différence entre les versions

De wiki-se.plil.fr
Aller à la navigation Aller à la recherche
 
(18 versions intermédiaires par 2 utilisateurs non affichées)
Ligne 1 : Ligne 1 :
= Objectifs =
= Objectifs =


Réaliser le programme du devoir surveillé concernant la programmation en C sur microcontrôleur AVR :
Réaliser le programme "capteurs et actionneurs" du cours/TP concernant la programmation en C sur microcontrôleur AVR :
Réaliser le programme "capteurs et actionneurs" du cours/TP concernant la programmation en C sur microcontrôleur AVR :
* contrôlez un servo-moteur en utilisant le minuteur 0 ;
* contrôlez un servo-moteur en utilisant le minuteur 0 ;
Ligne 17 : Ligne 16 :


=Historique=
=Historique=
*02/03/2026:Branchement du matériel (Arduino,Shield et servo-moteur) et code pour faire bouger le moteur avec les deux boutons.
* 02/03/2026: Branchement du matériel (Arduino,Shield et servo-moteur) et code pour faire bouger le moteur avec les deux boutons.
* 03/03/2026 : Configuration du Timer 2 et étude de l'afficheur 7-segments .
* 05/03/2026 : Création de la logique d'affichage .
* 09/04/2026 : Correction du code et de la mise en page du Wiki.


=Travail réalisé=
== Travail réalisé ==
<br>
 
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">Jour 1 : Moteur et Boutons</span>
=== Jour 1 : Moteur et Boutons ===


Pour cette première étape, j’ai choisi d’avancer progressivement. L’objectif était simplement de lire les boutons S1 et S2 afin de commander la rotation du servo-moteur dans les deux sens.
Pour cette première étape, j’ai choisi d’avancer progressivement. L’objectif était simplement de lire les boutons S1 et S2 afin de commander la rotation du servo-moteur dans les deux sens.


<br>
==== Configuration des boutons ====
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">Configuration des boutons</span>


En consultant la datasheet ATmega328P,la section 13.2.1 (page 59),
En consultant la datasheet ATmega328P, la section 13.2.1 (page 59) :


* Lorsqu’une broche est configurée en entrée DDR = 0 et que l’on écrit un 1 dans le registre PORT correspondant, la résistance pull-up interne est activée. Cela maintient la broche à 5V au repos.
Lorsqu’une broche est configurée en entrée (DDR = 0) et que l’on écrit un 1 dans le registre PORT correspondant, la résistance pull-up interne est activée. Cela maintient la broche à 5V au repos.


on a donc :
on a donc :
* Bouton non appuyé : niveau 1
 
* Bouton appuyé : niveau 0
Bouton non appuyé : niveau 1
 
Bouton appuyé : niveau 0


C’est pour cette raison que, dans la boucle principale, je teste un état logique bas 0 pour détecter un appui.
C’est pour cette raison que, dans la boucle principale, je teste un état logique bas 0 pour détecter un appui.


<br>
==== Désactivation du buzzer du shield ====
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">Désactivation du buzzer du shield</span>


Au démarrage, le buzzer du shield émettait un signal sonore en continu.
Au démarrage, le buzzer du shield émettait un signal sonore en continu. Après vérification, celui-ci est piloté par un transistor PNP Q1. Pour le désactiver, il faut imposer un niveau logique haut (5V) sur la broche de commande. D’après la table 13-1 (page 60), cela se fait en configurant la broche en sortie DDR = 1 et en forçant le bit correspondant du registre PORT à 1. J’ai donc configuré la broche PD3 en sortie et fixé son état à 1 dès l’initialisation du programme.
Après vérification, celui-ci est piloté par un transistor PNP Q1. Pour le désactiver, il faut imposer un niveau logique haut (5V) sur la broche de commande. D’après la table 13-1 (page 60), cela se fait en configurant la broche en sortie DDR = 1 et en forçant le bit correspondant du registre PORT à 1.
J’ai donc configuré la broche PD3 en sortie et fixé son état à 1 dès l’initialisation du programme.


<br>
==== Génération du signal PWM pour le servo ====
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">Génération du signal PWM pour le servo</span>


Le servo-moteur nécessite un signal PWM d’environ 60 Hz.  
Le servo-moteur nécessite un signal PWM d’environ 60 Hz. J’ai utilisé le Timer 0 en mode Fast PWM (Mode 3), en activant les bits WGM01 et WGM00, conformément à la table 14-8 (page 86). Dans ce mode, le compteur évolue de 0 à 255. Pour que le signal soit transmis sur la broche PD6 (OC0A), j’ai activé le bit COM0A1 (table 14-3, page 84).
 
J’ai utilisé le Timer 0 en mode Fast PWM (Mode 3), en activant les bits WGM01 et WGM00, conformément à la table 14-8 (page 86).
* Dans ce mode, le compteur évolue de 0 à 255.
* Pour que le signal soit transmis sur la broche PD6 (OC0A), j’ai activé le bit COM0A1 (table 14-3, page 84). ''"If one or both of the COM0A1:0 bits are set, the OC0A output overrides the normal port functionality of the I/O pin it is connected to."''


Étant donné que le microcontrôleur fonctionne à 16 MHz, j’ai appliqué un prescaler de 1024 via les bits CS02 et CS00 (page 87). Le calcul de la fréquence obtenue est le suivant :
Étant donné que le microcontrôleur fonctionne à 16 MHz, j’ai appliqué un prescaler de 1024 via les bits CS02 et CS00 (page 87). Le calcul de la fréquence obtenue est le suivant :


<math>f = \frac{16\,000\,000}{1024 \times 256} = 61 \text{ Hz}</math>
f = 16,000,000 / (1024 * 256) = 61 Hz


La fréquence obtenue est donc d’environ 61 Hz, ce qui est compatible avec les exigences d’un servo-moteur standard.
La fréquence obtenue est donc d’environ 61 Hz, ce qui est compatible avec les exigences d’un servo-moteur standard.


<br>
==== Justification des valeurs matérielles pour le servo Hitec HS-422 ====
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">Justification des valeurs matérielles pour le servo Hitec HS-422</span>


Le moteur utilisé pour ce projet est un servo '''Hitec HS-422'''. En étudiant ses spécifications techniques officielles, le constructeur définit la plage de fonctionnement suivante :
Le moteur utilisé pour ce projet est un servo Hitec HS-422. En étudiant ses spécifications techniques officielles, le constructeur définit la plage de fonctionnement suivante :
* Le point central (neutre) est fixé à '''1500 µs (1,5 ms)'''.
* La plage d'impulsion garantie (Pulse Width) s'étend de '''900 µs (0,9 ms) à 2100 µs (2,1 ms)'''.


'''Calcul des valeurs du registre OCR0A :'''
Le point central (neutre) est fixé à 1500 µs (1,5 ms).
Notre horloge Arduino fonctionne à 16 MHz. Avec un prescaler de 1024, la durée d'une seule incrémentation (un "tic") du Timer 0 est de :
<math>T_{tic} = \frac{1024}{16\,000\,000} = 0,000064 \text{ s} = 0,064 \text{ ms}</math>


Pour trouver la valeur technique à envoyer au registre, on divise le temps d'impulsion du constructeur par la durée de notre tic :
La plage d'impulsion garantie (Pulse Width) s'étend de 900 µs (0,9 ms) à 2100 µs (2,1 ms).


* '''Point central (1,5 ms) :'''
Calcul des valeurs du registre OCR0A :
<math>\frac{1,5}{0,064} = 23,43</math>. En arrondissant, on obtient notre position de démarrage 23.
Notre horloge Arduino fonctionne à 16 MHz. Avec un prescaler de 1024, la durée d'une seule incrémentation (un tic) du Timer 0 est de :
T_tic = 1024 / 16,000,000 = 0,064 ms


* '''Butée minimale constructeur (0,9 ms) :'''
Pour trouver la valeur technique à envoyer au registre, on divise le temps d'impulsion du constructeur par la durée de notre tic :
<math>\frac{0,9}{0,064} = 14,06</math>. La limite officielle basse est de 14.


* '''Butée maximale constructeur (2,1 ms) :'''
Point central (1,5 ms) : 1,5 / 0,064 = 23,43. En arrondissant, on obtient 23.
<math>\frac{2,1}{0,064} = 32,81</math>. La limite officielle haute est de 33.


'' Dans le code final, les limites ont été étendues de 8 à 38, correspondant environ à 0,5 ms et 2,4 ms pour exploiter le maximum la mécanique du servo.''
Butée minimale constructeur (0,9 ms) : 0,9 / 0,064 = 14,06. La limite basse est de 14.


<br>
Butée maximale constructeur (2,1 ms) : 2,1 / 0,064 = 32,81. La limite haute est de 33.
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">Code du Jour 1</span>


<syntaxhighlight lang="c" line="1">
Dans le code final, les limites ont été étendues de 8 à 38 pour exploiter le maximum la mécanique du servo.
#ifndef F_CPU
#define F_CPU 16000000UL // On définit la vitesse de l'horloge
#endif


#include <avr/io.h>
=== Jour 2 : Affichage et Timer 2 ===
#include <util/delay.h>


int main(void) {
Aujourd'hui, j'ai travaillé sur l'afficheur 7-segments du shield.
    // On définit les pins comme des sorties (DDR = 1)
    DDRD |= (1<<PD6) | (1<<PD3);
   
    // On force le buzzer au silence
    PORTD |= (1<<PD3);
 
    // On active les résistances internes pour les boutons
    PORTC |= (1<<PC1) | (1<<PC2);


    // On crée un signal PWM rapide pour piloter le servo sur la pin D6
==== 1. Comment on parle à l'afficheur ====
    TCCR0A = (1<<COM0A1) | (1<<WGM01) | (1<<WGM00);
    TCCR0B = (1<<CS02) | (1<<CS00); // On ralentit le chrono pour avoir du 61Hz
   
    uint8_t pos = 23; // Valeur de démarrage (moteur à l'arrêt)
 
    while (1) {
        // Lecture des boutons 
        if (!(PINC & (1<<PC1)) && pos > 8)  pos--; // Bouton S1 diminue
        if (!(PINC & (1<<PC2)) && pos < 38) pos++; // Bouton S2 augmente
       
        OCR0A = pos; // On envoie l'ordre de vitesse au moteur
       
        _delay_ms(100); // Délai pour que le réglage du servo ne soit pas trop rapide
    }
}
</syntaxhighlight>
 
<br>
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">Jour 2 : Affichage et Timer 2 </span>
 
Aujourd'hui, j'ai travaillé sur l'afficheur 7-segments du shield. C'est plus dur que le moteur parce qu'il faut gérer plusieurs chiffres en même temps avec peu de fils.
 
<br>
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">1. Comment on parle à l'afficheur </span>


Le shield utilise deux puces (74HC595) pour contrôler les segments. On utilise seulement 3 broches :
Le shield utilise deux puces (74HC595) pour contrôler les segments. On utilise seulement 3 broches :


*DATA (PB0) : pour envoyer les infos bit par bit.
DATA (PB0) : pour envoyer les infos bit par bit.


*CLOCK (PD7) : pour donner le rythme.
CLOCK (PD7) : pour donner le rythme.


*LATCH (PD4) : pour dire à l'écran d'afficher ce qu'on a envoyé.
LATCH (PD4) : pour dire à l'écran d'afficher ce qu'on a envoyé.


Comme il faut envoyer du courant vers l'afficheur, j'ai mis ces broches en Sortie dans le registre DDR (Table 13-1, page 60).
==== 2. L'affichage (Multiplexage) ====


<br>
On utilise la persistance rétinienne : on allume chaque chiffre l'un après l'autre très vite. L'œil ne voit pas le clignotement et on a l'impression que tout est allumé.
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">2. L'affichage (Multiplexage) </span>
On ne peut pas allumer les 3 chiffres en même temps normalement. On utilise la persistance rétinienne : on allume chaque chiffre l'un après l'autre très vite. L'œil ne voit pas le clignotement et on a l'impression que tout est allumé.


<br>
==== 3. Pourquoi le Timer 2 et le mode CTC ? ====
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">3. Pourquoi le Timer 2 et le mode CTC ? </span>


Pour que l'affichage soit propre, j'utilise le Timer 2 pour faire une interruption toutes les 1 ms.
Pour que l'affichage soit propre, j'utilise le Timer 2 pour faire une interruption toutes les 1 ms.
J'ai choisi le mode CTC (Clear Timer on Compare Match). Le timer se remet à zéro quand il atteint la cible OCR2A. Pour avoir 1 ms, j'ai mis 249 dans OCR2A.


Pourquoi le Timer 2 ?
=== Jour 3 : Calcul de l'angle et logique d'affichage ===


*J'ai laissé le Timer 0 car il gère déjà le moteur.
==== 1. Calcul de l'angle ====


*Je n'ai pas pris le Timer 1 car c'est un timer 16 bits très puissant. L'affichage est une tâche simple.
La valeur pos varie entre 8 (0°) et 38 (180°). La plage est de 30 unités.
La formule mathématique intégrée est : angle = (uint32_t)(pos - 8) * 180 / 30;
L'utilisation de uint32_t permet d'éviter un dépassement de capacité en mémoire avant la division.


Pourquoi le mode CTC ?
==== 2. Communication avec les puces ====


J'ai choisi le mode CTC (Clear Timer on Compare Match) au lieu du mode normal.
Le Shield utilise des registres à décalage. On envoie d'abord l'octet qui définit les segments à allumer, puis l'octet pour choisir l'afficheur. Une fois les 16 bits envoyés, on active la broche LATCH.


En mode normal, le timer compte jusqu'à 255 et on ne peut pas régler la durée précisément.
=== Jour 4 : L'assemblage final et les interruptions ===


En mode CTC, le timer se remet à zéro pile quand il atteint la cible qu'on lui donne dans le registre OCR2A.
==== 1. La fonction d'interruption : ISR ====


Pour avoir 1 ms, j'ai mis 249 dans OCR2A. Comme le compteur commence à 0, ça fait exactement 250 étapes. Avec le prescaler de 64, ça nous donne un rafraîchissement parfait.
Pour que l'affichage se fasse seul en arrière-plan, j'utilise la fonction ISR(TIMER2_COMPA_vect). Elle met à jour un seul afficheur à chaque appel pour ne pas bloquer le processeur. On utilise une variable static uint8_t digit pour alterner entre les 3 chiffres.


<syntaxhighlight lang="c">
==== 2. Le programme principal libéré ====
// Timer 2 pour l'affichage (toutes les 1ms)
TCCR2A = (1 << WGM21);  // Mode CTC
TCCR2B = (1 << CS22);  // Prescaler 64
OCR2A = 249;            // 1ms
TIMSK2 |= (1 << OCIE2A); // Active l'interruption
</syntaxhighlight>


<br>
Grâce à cette interruption, la boucle principale while(1) s'occupe uniquement de la logique utilisateur : lecture des boutons et mise à jour de la position.
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">Jour 3 : Calcul de l'angle et logique d'affichage  </span>


Maintenant que le Timer 2 est prêt à rafraîchir l'écran, j'ai travaillé sur la partie logique : comment transformer la position du moteur en un angle lisible (0 à 180°) et comment l'envoyer physiquement aux afficheurs.
==== 3. Pourquoi le delay ne fait pas clignoter l'écran ? ====


<br>
Le _delay_ms(100) bloque le programme principal mais pas le Timer 2 qui est matériel. Toutes les 1 ms, l'interruption se déclenche quand même pour rafraîchir l'écran.
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">1. Calcul de l'angle et logique d'affichage </span>
Dans le code du Jour 1, j'utilisais une variable pos pour le registre OCR0A. Cette valeur varie entre 8 (0°) et 38 (180°).


On a une plage de 30 unités (38 - 8 = 30).
=== Bilan et rendu ===


Chaque unité ajoutée à pos représente donc 180 / 30 = 6 degrés.
Le projet répond à toutes les contraintes (deux timers, Fast PWM, multiplexage et interruptions).


La formule mathématique que j'ai intégrée est :
==== 1. Fonctions d'initialisation ====
angle = (uint32_t)(pos - 8) * 180 / 30;


Pourquoi (uint32_t) ?
J'ai extrait la configuration des registres dans deux fonctions dédiées afin de rendre le main() plus lisible :
J'ai forcé la conversion en entier 32 bits  car lors de la multiplication par 180, le résultat intermédiaire peut monter jusqu'à 5400 (30 * 180). L'ATmega328P étant un microcontrôleur 8 bits , utiliser (uint32_t) permet d'éviter un dépassement de capacité (overflow) en mémoire avant que la division par 30 ne réduise le résultat final.


Pour afficher un angle, le microcontrôleur doit envoyer les chiffres un par un. J'utilise Modulo (%) pour isoler chaque chiffre :
init_timer0_pwm() : Configure le mode Fast PWM pour le servo.


Centaines : On divise l'angle par 100.
init_timer2_irq() : Configure le mode CTC pour l'interruption d'affichage.


Dizaines : On divise par 10, puis on prend le reste de la division par 10 (angle / 10) % 10.
==== 2. Démonstration ====


Unités : On prend le reste de la division par 10 angle % 10.
Le système est fonctionnel. Une vidéo et le fichier de code sont joints.


C'est une étape essentielle car chaque afficheur du Shield est indépendant.
<syntaxhighlight lang="c">
#define LATCH PD4
#define CLOCK PD7
#define DATA  PB0


volatile uint16_t angle = 90;
const uint8_t segments[] = {~0x3F,~0x06,~0x5B,~0x4F,~0x66,~0x6D,~0x7D,~0x07,~0x7F,~0x6F};


<span style="color:#800020;font-size: 1.2em; font-weight: bold;"> 3. Communication avec les puces </span>
void init_timer0_pwm(void) {
    TCCR0A = (1<<COM0A1) | (1<<WGM01) | (1<<WGM00);
    TCCR0B = (1<<CS02) | (1<<CS00);
}


Le Shield utilise des registres à décalage. Pour envoyer les données, j'ai créé une fonction qui respecte le protocole série :
void init_timer2_irq(void) {
    TCCR2A = (1<<WGM21);
    TIMSK2 |= (1<<OCIE2A);
    OCR2A = 249;
    TCCR2B = (1<<CS22);
}


On envoie d'abord l'octet qui définit quels segments allumer (A, B, C, D, E, F, G). J'utilise un tableau segments pour savoir quel bit correspond à quel chiffre.
void envoyer(uint8_t val) {
    for (uint8_t i = 0; i < 8; i++) {
        if (val & (1 << (7 - i))) PORTB |= (1 << DATA);
        else PORTB &= ~(1 << DATA);
       
        PORTD |= (1 << CLOCK);
        PORTD &= ~(1 << CLOCK);
    }
}


On envoie ensuite un deuxième octet pour choisir quel afficheur doit s'allumer parmi les quatre disponibles.
ISR(TIMER2_COMPA_vect) {
    static uint8_t digit = 0;
    uint8_t val = (digit == 0) ? (angle/100) : (digit == 1) ? (angle/10)%10 : (angle%10);
   
    PORTD &= ~(1 << LATCH);
    envoyer(segments[val]);
    envoyer(1 << digit);
    PORTD |= (1 << LATCH);
   
    digit = (digit + 1) % 3;
}


Pour chaque bit, on fait passer la broche CLOCK de 0 à 1 pour pousser la donnée dans la puce.
int main(void) {
    DDRD |= (1<<LATCH) | (1<<CLOCK) | (1<<PD6) | (1<<PD3);
    DDRB |= (1<<DATA);
    PORTD |= (1<<PD3);
    PORTC |= (1<<PC1) | (1<<PC2);


Une fois les 16 bits envoyés, on active la broche LATCH pour confirmer l'affichage.
    init_timer0_pwm();
    init_timer2_irq();


Tout ce processus (découpage du nombre et envoi des bits) prend du temps. Avec le Timer 2, l'affichage devient automatique.
    sei();
Le programme principal peut continuer de lire les boutons et de bouger le moteur sans s'arrêter. Grâce à la vitesse du Timer 2, l'œil humain ne voit pas le multiplexage et l'affichage semble fixe et brillant. C'est le principe de la persistance rétinienne.
    uint8_t pos = 23;


Comme pour le moteur, j'ai vérifié dans la Table 13-1 (page 60) de la datasheet que les broches de communication (PB0, PD7 et PD4) sont configurées en sortie dans le registre DDR. Sans cela, aucun signal ne parviendrait aux puces de l'afficheur.
    while (1) {
 
        if (!(PINC & (1<<PC1)) && pos > 8)  pos--;
<br>
        if (!(PINC & (1<<PC2)) && pos < 38) pos++;
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">3. Extrait de code : Logique d'affichage </span>
       
 
        OCR0A = pos;
Voici la partie du code qui gère la transformation des chiffres en signaux électriques.
        angle = (uint32_t)(pos - 8) * 180 / 30;
 
       
J'ai d'abord créé un tableau qui contient les codes pour chaque chiffre (0-9) en suivant le schéma des segments (avec le symbole ~ car le shield s'allume avec du 0V) :
        _delay_ms(100);
<syntaxhighlight lang="c">
    }
// Codes pour afficher les chiffres 0 à 9 sur le shield
}
const uint8_t segments[] = {
~0x3F, ~0x06, ~0x5B, ~0x4F, ~0x66, ~0x6D, ~0x7D, ~0x07, ~0x7F, ~0x6F
};
</syntaxhighlight>
</syntaxhighlight>


Ensuite, la fonction suivante permet d'envoyer ces codes aux puces bit par bit :
[[Fichier:Demonstration Finale.mp4]]
<syntaxhighlight lang="c">
[[Fichier:Correction servo.zip]]
void envoyer(uint8_t val) {
for (uint8_t i = 0; i < 8; i++) {
// On vérifie chaque bit de l'octet du plus grand au plus petit
if (val & (1 << (7 - i))) PORTB |= (1 << PB0); // DATA = 1 (5V)
else PORTB &= ~(1 << PB0);                    // DATA = 0 (0V)
 
    PORTD |= (1 << PD7);  // Coup de CLOCK (Haut)
    PORTD &= ~(1 << PD7); // Coup de CLOCK (Bas)
}
}
</syntaxhighlight>


=Document utilisés=
=Document utilisés=

Version actuelle datée du 23 juin 2026 à 09:06

Objectifs

Réaliser le programme "capteurs et actionneurs" du cours/TP concernant la programmation en C sur microcontrôleur AVR :

  • contrôlez un servo-moteur en utilisant le minuteur 0 ;
  • l’utilisateur utilise deux boutons pour changer l’angle ;
  • affichez l’angle de rotation sur les 7-segments :
    • 3 afficheurs doivent être utilisés ;
    • utilisez la persistence rétinienne ;
    • utilisez les interruptions générées par un second minuteur.

Matériel nécessaire

  • Arduino Uno avec câble USB ;
  • bouclier multi-fonctions ;
  • servo-moteur.

Historique

  • 02/03/2026: Branchement du matériel (Arduino,Shield et servo-moteur) et code pour faire bouger le moteur avec les deux boutons.
  • 03/03/2026 : Configuration du Timer 2 et étude de l'afficheur 7-segments .
  • 05/03/2026 : Création de la logique d'affichage .
  • 09/04/2026 : Correction du code et de la mise en page du Wiki.

Travail réalisé

Jour 1 : Moteur et Boutons

Pour cette première étape, j’ai choisi d’avancer progressivement. L’objectif était simplement de lire les boutons S1 et S2 afin de commander la rotation du servo-moteur dans les deux sens.

Configuration des boutons

En consultant la datasheet ATmega328P, la section 13.2.1 (page 59) :

Lorsqu’une broche est configurée en entrée (DDR = 0) et que l’on écrit un 1 dans le registre PORT correspondant, la résistance pull-up interne est activée. Cela maintient la broche à 5V au repos.

on a donc :

Bouton non appuyé : niveau 1

Bouton appuyé : niveau 0

C’est pour cette raison que, dans la boucle principale, je teste un état logique bas 0 pour détecter un appui.

Désactivation du buzzer du shield

Au démarrage, le buzzer du shield émettait un signal sonore en continu. Après vérification, celui-ci est piloté par un transistor PNP Q1. Pour le désactiver, il faut imposer un niveau logique haut (5V) sur la broche de commande. D’après la table 13-1 (page 60), cela se fait en configurant la broche en sortie DDR = 1 et en forçant le bit correspondant du registre PORT à 1. J’ai donc configuré la broche PD3 en sortie et fixé son état à 1 dès l’initialisation du programme.

Génération du signal PWM pour le servo

Le servo-moteur nécessite un signal PWM d’environ 60 Hz. J’ai utilisé le Timer 0 en mode Fast PWM (Mode 3), en activant les bits WGM01 et WGM00, conformément à la table 14-8 (page 86). Dans ce mode, le compteur évolue de 0 à 255. Pour que le signal soit transmis sur la broche PD6 (OC0A), j’ai activé le bit COM0A1 (table 14-3, page 84).

Étant donné que le microcontrôleur fonctionne à 16 MHz, j’ai appliqué un prescaler de 1024 via les bits CS02 et CS00 (page 87). Le calcul de la fréquence obtenue est le suivant :

f = 16,000,000 / (1024 * 256) = 61 Hz

La fréquence obtenue est donc d’environ 61 Hz, ce qui est compatible avec les exigences d’un servo-moteur standard.

Justification des valeurs matérielles pour le servo Hitec HS-422

Le moteur utilisé pour ce projet est un servo Hitec HS-422. En étudiant ses spécifications techniques officielles, le constructeur définit la plage de fonctionnement suivante :

Le point central (neutre) est fixé à 1500 µs (1,5 ms).

La plage d'impulsion garantie (Pulse Width) s'étend de 900 µs (0,9 ms) à 2100 µs (2,1 ms).

Calcul des valeurs du registre OCR0A : Notre horloge Arduino fonctionne à 16 MHz. Avec un prescaler de 1024, la durée d'une seule incrémentation (un tic) du Timer 0 est de : T_tic = 1024 / 16,000,000 = 0,064 ms

Pour trouver la valeur technique à envoyer au registre, on divise le temps d'impulsion du constructeur par la durée de notre tic :

Point central (1,5 ms) : 1,5 / 0,064 = 23,43. En arrondissant, on obtient 23.

Butée minimale constructeur (0,9 ms) : 0,9 / 0,064 = 14,06. La limite basse est de 14.

Butée maximale constructeur (2,1 ms) : 2,1 / 0,064 = 32,81. La limite haute est de 33.

Dans le code final, les limites ont été étendues de 8 à 38 pour exploiter le maximum la mécanique du servo.

Jour 2 : Affichage et Timer 2

Aujourd'hui, j'ai travaillé sur l'afficheur 7-segments du shield.

1. Comment on parle à l'afficheur

Le shield utilise deux puces (74HC595) pour contrôler les segments. On utilise seulement 3 broches :

DATA (PB0) : pour envoyer les infos bit par bit.

CLOCK (PD7) : pour donner le rythme.

LATCH (PD4) : pour dire à l'écran d'afficher ce qu'on a envoyé.

2. L'affichage (Multiplexage)

On utilise la persistance rétinienne : on allume chaque chiffre l'un après l'autre très vite. L'œil ne voit pas le clignotement et on a l'impression que tout est allumé.

3. Pourquoi le Timer 2 et le mode CTC ?

Pour que l'affichage soit propre, j'utilise le Timer 2 pour faire une interruption toutes les 1 ms. J'ai choisi le mode CTC (Clear Timer on Compare Match). Le timer se remet à zéro quand il atteint la cible OCR2A. Pour avoir 1 ms, j'ai mis 249 dans OCR2A.

Jour 3 : Calcul de l'angle et logique d'affichage

1. Calcul de l'angle

La valeur pos varie entre 8 (0°) et 38 (180°). La plage est de 30 unités. La formule mathématique intégrée est : angle = (uint32_t)(pos - 8) * 180 / 30; L'utilisation de uint32_t permet d'éviter un dépassement de capacité en mémoire avant la division.

2. Communication avec les puces

Le Shield utilise des registres à décalage. On envoie d'abord l'octet qui définit les segments à allumer, puis l'octet pour choisir l'afficheur. Une fois les 16 bits envoyés, on active la broche LATCH.

Jour 4 : L'assemblage final et les interruptions

1. La fonction d'interruption : ISR

Pour que l'affichage se fasse seul en arrière-plan, j'utilise la fonction ISR(TIMER2_COMPA_vect). Elle met à jour un seul afficheur à chaque appel pour ne pas bloquer le processeur. On utilise une variable static uint8_t digit pour alterner entre les 3 chiffres.

2. Le programme principal libéré

Grâce à cette interruption, la boucle principale while(1) s'occupe uniquement de la logique utilisateur : lecture des boutons et mise à jour de la position.

3. Pourquoi le delay ne fait pas clignoter l'écran ?

Le _delay_ms(100) bloque le programme principal mais pas le Timer 2 qui est matériel. Toutes les 1 ms, l'interruption se déclenche quand même pour rafraîchir l'écran.

Bilan et rendu

Le projet répond à toutes les contraintes (deux timers, Fast PWM, multiplexage et interruptions).

1. Fonctions d'initialisation

J'ai extrait la configuration des registres dans deux fonctions dédiées afin de rendre le main() plus lisible :

init_timer0_pwm() : Configure le mode Fast PWM pour le servo.

init_timer2_irq() : Configure le mode CTC pour l'interruption d'affichage.

2. Démonstration

Le système est fonctionnel. Une vidéo et le fichier de code sont joints.

#define LATCH PD4
#define CLOCK PD7
#define DATA  PB0

volatile uint16_t angle = 90;
const uint8_t segments[] = {~0x3F,~0x06,~0x5B,~0x4F,~0x66,~0x6D,~0x7D,~0x07,~0x7F,~0x6F};

void init_timer0_pwm(void) {
    TCCR0A = (1<<COM0A1) | (1<<WGM01) | (1<<WGM00);
    TCCR0B = (1<<CS02) | (1<<CS00);
}

void init_timer2_irq(void) {
    TCCR2A = (1<<WGM21);
    TIMSK2 |= (1<<OCIE2A);
    OCR2A = 249;
    TCCR2B = (1<<CS22);
}

void envoyer(uint8_t val) {
    for (uint8_t i = 0; i < 8; i++) {
        if (val & (1 << (7 - i))) PORTB |= (1 << DATA);
        else PORTB &= ~(1 << DATA);
        
        PORTD |= (1 << CLOCK);
        PORTD &= ~(1 << CLOCK);
    }
}

ISR(TIMER2_COMPA_vect) {
    static uint8_t digit = 0;
    uint8_t val = (digit == 0) ? (angle/100) : (digit == 1) ? (angle/10)%10 : (angle%10);
    
    PORTD &= ~(1 << LATCH);
    envoyer(segments[val]);
    envoyer(1 << digit);
    PORTD |= (1 << LATCH);
    
    digit = (digit + 1) % 3;
}

int main(void) {
    DDRD |= (1<<LATCH) | (1<<CLOCK) | (1<<PD6) | (1<<PD3);
    DDRB |= (1<<DATA);
    PORTD |= (1<<PD3);
    PORTC |= (1<<PC1) | (1<<PC2);

    init_timer0_pwm();
    init_timer2_irq();

    sei();
    uint8_t pos = 23;

    while (1) {
        if (!(PINC & (1<<PC1)) && pos > 8)  pos--;
        if (!(PINC & (1<<PC2)) && pos < 38) pos++;
        
        OCR0A = pos;
        angle = (uint32_t)(pos - 8) * 180 / 30;
        
        _delay_ms(100);
    }
}

Fichier:Correction servo.zip

Document utilisés