« SE3 2025/2026 EC3 » : différence entre les versions
| Ligne 173 : | Ligne 173 : | ||
OCR2A = 249; // 1ms | OCR2A = 249; // 1ms | ||
TIMSK2 |= (1 << OCIE2A); // Active l'interruption | TIMSK2 |= (1 << OCIE2A); // Active l'interruption | ||
</syntaxhighlight> | |||
<br> | |||
<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. | |||
<br> | |||
<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). | |||
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. | |||
<span style="color:#800020;font-size: 1.2em; font-weight: bold;"> 3. Communication avec les puces </span> | |||
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. | |||
<br> | |||
<span style="color:#800020;font-size: 1.2em; font-weight: bold;">3. Extrait de code : Logique d'affichage </span> | |||
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) : | |||
<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> | |||
Ensuite, la fonction suivante permet d'envoyer ces codes aux puces bit par bit : | |||
<syntaxhighlight lang="c"> | |||
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> | </syntaxhighlight> | ||
Version du 5 mars 2026 à 20:45
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.
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)
}
}
Document utilisés
- https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf
- https://forum.arduino.cc/t/multifunction-shield/1007847/3
- https://ww1.microchip.com/downloads/en/DeviceDoc/AVR-InstructionSet-Manual-DS40002198.pdf
- https://cdn.sparkfun.com/assets/1/1/f/a/3/HCS_HS-422_Specsheet_v2.2__102623.pdf