SE3 2025/2026 EC3

De wiki-se.plil.fr
Aller à la navigation Aller à la recherche

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 :

  • 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 .

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). "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 :

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 :

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) :

. En arrondissant, on obtient notre position de démarrage 23.

  • Butée minimale constructeur (0,9 ms) :

. La limite officielle basse est de 14.

  • Butée maximale constructeur (2,1 ms) :

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


Code du Jour 1

#ifndef F_CPU
#define F_CPU 16000000UL // On définit la vitesse de l'horloge 
#endif

#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    // 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
    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
    }
}


Jour 2 : Affichage et Timer 2

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.


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é.

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)

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é.


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.

Pourquoi le Timer 2 ?

  • J'ai laissé le Timer 0 car il gère déjà le moteur.
  • 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.

Pourquoi le mode CTC ?

J'ai choisi le mode CTC (Clear Timer on Compare Match) au lieu du mode normal.

En mode normal, le timer compte jusqu'à 255 et on ne peut pas régler la durée précisément.

En mode CTC, le timer se remet à zéro pile quand il atteint la cible qu'on lui donne dans le registre OCR2A.

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.

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


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

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.


1. Calcul de l'angle et logique d'affichage

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).

Chaque unité ajoutée à pos représente donc 180 / 30 = 6 degrés.

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

Pourquoi (uint32_t) ? 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 :

Centaines : On divise l'angle par 100.

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

Unités : On prend le reste de la division par 10 angle % 10.

C'est une étape essentielle car chaque afficheur du Shield est indépendant.


3. Communication avec les puces

Le Shield utilise des registres à décalage. Pour envoyer les données, j'ai créé une fonction qui respecte le protocole série :

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.

On envoie ensuite un deuxième octet pour choisir quel afficheur doit s'allumer parmi les quatre disponibles.

Pour chaque bit, on fait passer la broche CLOCK de 0 à 1 pour pousser la donnée dans la puce.

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

Tout ce processus (découpage du nombre et envoi des bits) prend du temps. Avec le Timer 2, l'affichage devient automatique. 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.

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.


3. Extrait de code : Logique d'affichage

Voici la partie du code qui gère la transformation des chiffres en signaux électriques.

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) :

// Codes pour afficher les chiffres 0 à 9 sur le shield
const uint8_t segments[] = {
~0x3F, ~0x06, ~0x5B, ~0x4F, ~0x66, ~0x6D, ~0x7D, ~0x07, ~0x7F, ~0x6F
};

Ensuite, la fonction suivante permet d'envoyer ces codes aux puces bit par bit :

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)
}
}


Jour 4 : L'assemblage final et les interruptions

Aujourd'hui, c'était l'étape la plus délicate : réunir le code du moteur et le code de l'affichage . Le défi principal était de faire fonctionner les deux en même temps. Si le programme est occupé à faire des pauses pour le moteur, comment peut-il rafraîchir l'écran assez vite pour que l'œil ne voie pas de clignotement ? C'est là que les interruptions entrent en jeu.


1. La fonction d'interruption : ISR

Pour que l'affichage se fasse tout seul en arrière-plan, j'utilise une fonction appelée `ISR(TIMER2_COMPA_vect)`. Ce nom est imposé par la librairie `<avr/interrupt.h>` : il indique au microcontrôleur de lancer cette fonction précisément quand le Timer 2 atteint la cible que j'ai fixée (249) pour faire 1 milliseconde.

À chaque fois que cette fonction s'allume, elle ne met à jour qu'un seul des 3 afficheurs pour ne pas perdre de temps. J'ai utilisé une variable `static uint8_t digit = 0;`. Le mot `static` permet à la variable de garder en mémoire l'afficheur actuel (0, 1 ou 2) entre chaque appel de la fonction.

Voici les actions de l'interruption en une fraction de milliseconde :

  • Elle bloque l'écran en mettant le LATCH à 0 pour qu'on ne voie pas les bits bouger.
  • Elle envoie le code des segments via la fonction envoyer().
  • Elle envoie un deuxième code pour choisir le bon afficheur.
  • Elle débloque l'écran en remettant le LATCH à 1 : le chiffre apparaît.
  • Elle prépare le prochain tour en faisant digit = (digit + 1) % 3. (le modulo permet de faire tourner le compteur 0, 1, 2, 0...).


2. Le programme principal libéré

Grâce à cette fonction ISR qui s'occupe du multiplexage en tâche de fond, ma boucle principale while(1) est devenue très simple à gérer.

Dans cette boucle, le programme s'occupe uniquement de la logique utilisateur :

  • Il vérifie si les boutons S1 ou S2 sont pressés.
  • Si oui, il augmente ou diminue la variable pos.
  • Il calcule le nouvel angle en degrés sur 32 bits pour le préparer pour l'affichage.


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

Dans ma boucle principale, j'ai ajouté un _delay_ms(100) à la fin. Ce délai est obligatoire, sans lui, si je reste appuyé sur le bouton, la variable 'pos' augmente tellement vite que le moteur saute d'un coup à la fin de sa course. Le délai permet de ralentir cette lecture.

Cependant, la fonction `delay` "bloque" le microcontrôleur pendant 100 millisecondes.

  • Le problème : Si j'avais géré l'affichage dans cette même boucle principale, l'écran resterait figé pendant ces 100 ms à chaque tour. Le multiplexage serait détruit et l'écran clignoterait atrocement.
  • La solution du Timer 2 : Comme l'affichage est géré par l'interruption, il est totalement indépendant. Même quand le programme principal est bloqué par le `delay`, le Timer 2 (qui est matériel) continue de tourner. Toutes les 1 ms, il force le microcontrôleur à faire une pause dans son attente, à changer le chiffre de l'afficheur, et à reprendre son délai. L'écran reste donc parfaitement fluide !


4. Bilan et rendu

Le projet est maintenant fonctionnel et répond à toutes les contraintes du sujet (utilisation de deux timers, signal Fast PWM, multiplexage, et interruptions). L'intégralité de mon code commenté se trouve dans le fichier `main.c` que j'ai déposé avec ce compte-rendu.

Fichier:Main.zip

Document utilisés