Le microcontrôleur - partie 3
Exercices de programmation en langage C (bases)



Après avoir vu les bases du Microcontrôleur (voir les pages "ça fait quoi ?", "comment ça se branche?", "les registres") , nous allons aborder la programmation en langage C.
Tous les exemples ci-dessous ont pour but d'illustrer la façon d'utiliser les registres nécessaires à la création de notre décodeur. Chaque programme est très court et est largement commenté.
Je conseille de regarder les exemples dans l'ordre ci-dessous car certains réutilisent les notions vues dans les exemples précédents.

Sommaire de cette page :

1/ On va commencer tout doux avec l'utilisation des ports d'entrées-sorties
- Exemple 1 : allumage d'une LED
- Exemple 2 : utilisation d'un bouton-poussoir
- Exemple 3 : Faire clignoter la LED
- Exemple 4 : Clignotement avec un seul BP
- Exemple 5 : Bouton "Start" / Bouton "Stop" (mémorisation)


2/ Puis on rentrera dans les choses TRES sérieuses avec les timers
(c'est le moins simple)
 Présentation des avantages du Timer
- Exemple 6 : Faire clignoter la LED
- Exemple 7 : Clignotement avec un seul BP
- Exemple 8 : Variation de l'intensité d'éclairage d'une LED (MLI)
- Exemple 9 : Commande d'un seul servomoteur
(MLI)
- Exemple 10 : Utilisation de la MLI automatique (utilisation des sorties OC1A)

3/ Un peu plus simple mais utile : Détection d'évènements extérieurs

- Exemple 11 : Utilisation des interruptions INT0 et INT1
- Exemple 12 : Chronométrage

4/ Très facile et très utile aussi : Utilisation de l'EEPROM
- Exemple13 : Programmation initiale del'EEPROM
- Exemple14 : Lecture de l'EEPROM
- Exemple15 : Ecriture dans l'EEPROM







1/ Utilisation des ports d'entrées-sorties :

C'est l'utilisation la plus simple, et la plus utile...car on souhaite justement agir sur quelque-chose.
Exemple 1 : Je veux allumer une LED qui est branchée sur la patte A0.

Rien de plus facile ! Seuls 2 registres sont nécessaires :
- DDRA (pour dire que la broche A0 est en sortie)
- PORTA (pour écrire la valeur souhaitée sur le port)

Telle qu'elle est branchée, la LED sera allumée quand le port A0 sera à 1.

- On commence par initialiser le port en précisant la direction de chaque broche :  DDRA=0x01 ( "0x" c'est pour indiquer un nombre hexadécimal)

- Et dans la boucle principale "while(1)" on  passe à l'action :
PORTA0_bit = 1   Pour affecter la valeur 1 sur  la broche 0

"While(1)" permet de boucler éternellement la suite d'instructions située entre les accolades {...}. La LED reste allumée en permanence.


Le µC cherche donc à allumer la LED en permanence. Il ne fait que ça, en boucle !



Plutôt que de s'allumer en permanence, on peut aussi utiliser un bouton-poussoir pour commander l'allumage de la LED
Exemple 2 :
Détection d'un bouton poussoir pour allumer la LED.
La LED doit rester allumée tant qu'on appuie sur le BP

- Le bouton poussoir est branché sur le port A2.
- La LED est branchée sur le port A4.

solution :

On utilise le registre d'entrée PINA2 pour affecter sa valeur au registre de sortie PORTA4.

Si le BP est appuyé, alors PINA2_bit vaut "1"



Mais bon... utiliser un µC pour allumer une LED....j'ai déjà vu plus simple !!! On va la faire clignoter :
Exemple 3 :

Je veux allumer la LED, attendre approximativement 0.5s puis l'éteindre pendant approximativement 0.5s .

- Le montage 1 est légèrement modifié. Cette fois, j'ai utilisé le port A4 (la cinquième broche du port A) pour brancher la LED.
- Pas de B.P.
Je me suis amusé à changer légèrement les instructions :

- Pour DDRA : j'ai utilisé la notation binaire. Le "1" est à la cinquième place en partant de la droite ==> broche A4
- L'instruction "PORTA=16"  écrit "00010000" sur toutes les pattes du port A. (Rappel : 16(décimal) = 00010000(binaire)
- Delay_ms(500) : délai de 500ms   (après chaque changement) pour patienter 0.5s
-  l'extinction de la patte 0 :  PORTA4_bit = 0; Seule, la patte 4 est concernée par le changement.

ATTENTION : l'utilisation de delay_ms(500) pose problème :  c'est ok quand on n'a pas besoin de précision ET quand le µC n'a que ça à faire. Car pendant qu'il attend, le µC ne fait RIEN d'autre.
DONC ça n'est PAS DU TOUT la bonne solution quand on a besoin de précision et qu'il faut faire autre-chose en même temps
.

(Voir la solution à ce problème dans l'exercice 6)




On peut aussi clignoter uniquement lorsque le BP est enfoncé :
Exemple 4 :

Très proche du précédent, on souhaite que la LED clignote tant que le BP reste sollicité. La LED ne se rallume pas  après que le BP soit relâché.
Pour cet exemple :
- LED branchée sur A4
- BP branché A2.


DDRA = 16 ( cinquième broche du port A en sortie)

Il faut que le PINA2 soit égal à 1 pour autoriser le clignotement.


L'utilisation de Delay_ms(500) pose le même problème que précédemment.

Remarque : Si on relâche le BP un poil de microseconde trop tard, la LED repart pour un cycle de clignotement
.




On peut utiliser 2 boutons si on veut :  un "bouton start" et un "bouton stop".

Exemple 5 :

- Le bouton "start" est branché sur A0
- Le bouton "stop" est branché sur A1

- La LED est branchée sur A7 (huitième broche du portA)

La variable "Memoire" est créée au début du programme, avant l'initialisation. On déclare que c'est un entier (pas de virgule) avec "unsigned int".

Cette variable va servir à mémoriser l'appui sur un bouton ou sur l'autre.

L'appui sur start (PINA0) règle "Mémoire" à 1.
L'appui sur stop (PINA1) règle "Mémoire" à 0.
Si "Mémoire" est à 1 alors on clignote.

L'utilisation de Delay_ms(500) pose le même problème que précédemment. La prise en compte du bouton stop ne se fait que pendant une fraction de seconde, toute les secondes. Un appui bref passera inaperçu.






2/ Utilisation du Timer :


Dans les exemples précédents, les instructions "Delay_ms" ont occupé le coeur pendant toute l'attente. Donc, grosso-modo, pendant une seconde d'attente, il y a 16 millions de cycles qui ne servent à rien d'autre qu'à attendre. C'est comme si vous restiez réveillé face à votre montre pour surveiller l'heure du lever... Stupide !

Le Timer soulage énormément le travail du coeur en prenant le rôle du réveil. Il se charge de surveiller le temps en permanence. Ainsi, pendant 99,99 % du temps, le coeur peut faire autre-chose. La réactivité est améliorée et la précision aussi.

Notre travail consiste juste à "régler le réveil"
(zone orange ci-dessous) pour qu'il "sonne" quand c'est utile. Ce réglage s'effectue au début du programme, juste avant la boucle éternelle "While(1)"

Ensuite, lorsque le "réveil sonne" il faut exécuter les bonnes actions. C'est le rôle du sous-programme d'interruption (en jaune ci-dessous)

Exemple 6 :
On reprend les objectifs de l'exercice 2, mais cette fois, c'est le Timer0 qui va gérer le clignotement car on souhaite avoir un clignotement précis.
- La LED est connectée à la patte A7 (et non A4 )


Donnée importante : le quartz qui est branché au µC est cadencé à 16Mhz (soit une impulsion toutes les 0.0625µs)

Le programme est divisé en plusieurs zones :

- D'abord, AVANT la boucle principale (zone en jaune), on doit trouver tous les sous-programmes. Ici, c'est l'interruption provoquée par la comparaison "COMPA" du Timer0.
 Elle remet le compteur0 à zéro, puis change l'état du bit A7.


- Le programme (à partir de "void main()") commence par faire les réglages initiaux des registres. La zone orange règle le Timer0  :
TCCR0A : le bit 7 permet d'utiliser les 16 bits du Timer0 (au lieu de 8 normalement).
TCCR0B : Les trois derniers bits règlent la division d'horloge pour un pas du timer : division par 256
OCR0A et OCR0B : indiquent la valeur à laquelle le compteur doit déclencher l'interruption.
TIMSK : Ici, il autorise uniquement l'interruption de comparaison A réussie.
SREG : Autorise le déclenchement des interruptions

- On retrouve la boucle "While(1)"....complètement vide ! ==> le coeur n'a rien à faire pendant que le compteur compte !! cool !


Remarque : pour TCCR0A, TCCR0B et TIMSK, il
faut lire le datasheet du composant pour savoir quels
bits mettre à "1". C'est différent d'un µC à l'autre.
Ici c'est pour l'ATtiny861.

Petite explication quand au calcul des valeurs
de la zone orange (initialisation) :
L'interruption doit se déclencher exactement toutes les 0.5 secondes. Le Timer doit donc être réglé pour "sonner" toutes les 0.5s.

D'abord, on règle la durée du pas du Timer en divisant l'horloge principale (celle du quartz) par 256 (TCR0B=0b00000100) :
  256 x 0.0625µs = 16µs .
Le timer augmente donc d'un pas toutes les 16µs
On peut donc déterminer le nombre de pas pour une demi seconde : 500000µs / 16µs = 31250 pas .
Il faut donc compter jusqu'à 31250 pour obtenir précisément une demi seconde.
On convertit cette valeur en binaire :   31250 ==> 01111010 000100010
Cela ne peut pas tenir dans un seul octet, on a besoin de 16 bits (TCCR0A=0b10000000).
Je répartis donc le chiffre obtenu dans les registres de comparaison OCR0A et OCR0B, qui sont combinés dans le cas du timer 16 bits (OCR0B= 01111010 et OCR0A=000100010).

Lorsque le compteur atteint la valeur 31250, la comparaison avec la valeur de OCR0A/OCR0B est validée, et cela déclenche le sous-programme "COMPA".
==> le réveil "sonne" !



Exemple 7 :
On reprend le schéma de l'exemple 4.
Le BP est connecté à la patte A0.
La LED ne s'allume que si  le BP est sollicité
.



On retrouve les mêmes zones que précédemment :

- AVANT la boucle principale, le sous-programme d'interruption provoqué par la comparaison "COMPA" du Timer0.
Il remet le compteur0 à zéro, puis vérifie que le BP est appuyé pour changer l'état du bit A7 sinon le bit A7 est mis à zéro.



- Au début du programme "void main()", on retrouve les mêmes réglages que précédemment afin que l'interruption "COMPA" soit exécutée une fois toutes les 0.5s.




- La boucle "While(1)"...reste complètement vide ! Ici encore, la gestion du clignotement ne prend pas de ressource pendant le comptage. Le coeur peut faire autre-chose.


Remarque : Pendant que le sous programme s'exécute, le compteur reprend le comptage en temps masqué. Il n'y a aucune perte de temps.



Exemple 8 :
Intensité d'éclairage d'une LED

Chaque appui sur BP+ augmente l'éclairage.
Chaque appui sur BP- diminue l'éclairage.

BP+ est branché dans B6 (=Int0)
BP- est branché dans A2 (=Int1)



L'astuce de ce programme repose sur le réglage du seuil de comparaison OCR0A : plus OCR0A est grand, plus t0 est long (et t1 est court)

- La comparaison réussie entre OCR0A et le Compteur permet de déclencher le sous-programme qui "allume" la LED.
- Le débordement du compteur (Overflow) à 255, déclenche le sous-programme "OVF" qui "éteint" la LED

Lors du débordement, on en profite pour actualiser la valeur de OCR0A en fonction du bouton qui est sollicité.

Dans la partie d'initialisation, remarquez que :
- TCCR0A : le timer est réglé sur "8 bits" de façon à être beaucoup plus rapide malgré la division par 1024.
- TIMSK possède 2 bits à "1" afin d'autoriser l'interruption du débordement du timer 0
- OCR0A est préréglé à un seuil de 128 afin d'avoir un éclairage moyen lors du démarrage.





Exemple 9 : Commande de servomoteur

Le principe est strictement identique au cas précédent mais les valeurs du Timer doivent être réglées de façon stratégique :
- La durée totale du comptage ne doit pas dépasser 20ms
- le créneau "1" doit durer entre 0.5ms et 2.5ms selon la position souhaitée
(0.5ms pour 0°...2.5ms pour 180°)

BP+ est branché dans B6 (=Int0)
BP- est branché dans A2 (=Int1)
Solution :
Compte tenu des possibilités du µC, j'ai choisi d'utiliser le Timer1 qui peut compter sur 10 bits maxi (soit 1024 pas).
- Avec une prédivision par 256, chaque pas va durer 16µs.
- La durée totale du cycle vaudra 1024 x 16µs = 16,384ms
- Un créneau "court" sera  déclenché à 32 pas (soit  0.512 ms)
- Un créneau "long" sera déclenché à 156 pas (soit  2,496ms)

Réglage des registres du Timer 1:
TCCR1A = 0  (comptage 8bits, et aucune influence  sur les ports
"automatiques")
TCCR1B = 0b00001001 (division par 256) => 1pas =16µs
TC1H : C'est un registre utilisé pour accéder aux octets supérieurs à 8 bits. C'est notre cas puisqu'on est sur 10 bits lors du comptage jusqu'à 1024. Il doit être réglé juste avant d'écrire la valeur souhaitée sur l'un des registres 10 bits.
OCR1C = valeur maxi du compteur à combiner avec TC1H

OCR1A
= 32 (réglage initial à la valeur mini du créneau)
TIMSK = 44 autorisation interruptions COMPA et OVF
On obtient un joli signal qui varie en fonction des appuis sur BP+ ou BP- : le servo tourne sur 180°

                           créneau de 0.5ms                                               créneau de 2.5ms
Evidemment, si vous n'avez pas d'oscilloscope, vous ne pourrez pas "voir" les créneaux. En revanche, vous verrez la LED éclairer plus ou moins suivant la durée du créneau. Si vous branchez un servomoteur, il fonctionnera d'une butée à l'autre (180°).



Les créneaux qui ont été générés dans les deux exemples précédents sont propres et relativement précis... C'est bien... à condition de ne pas avoir d'autres signaux à générer en même temps (par exemple pour le décodeur à 4 servomoteurs !!). Car malgré tout, même si l'interruption soulage le processeur, au final, c'est quand-même lui qui doit exécuter l'interruption en gérant le début et la fin de chaque créneau. Et pourtant, on peut aussi confier cela à 100% au Timer 1 ! Ce qui allège encore la tâche du coeur !! Ca vous botte ? OK, on essaye !
Exemple 10 :
Utilisation de la sortie "MLI automatique" : OC1A

Schéma identique à l'exercice précédent.

- Attention, la sortie OC1A est située sur la patte B1 (voir le Datasheet du µC)

-On met les entrées d'interrupteurs sur B2 et B3
Solution :

L'interruption ne se déclenchera que lors du débordement (OVF pour "Overflow") pour mettre à jour OCR1A (en fonction des BP sollicités)


Cette fois, c'est le port B qui a sa patte "1" en sortie



TCCR1A et TCCR1D définissent le comportement du port B1. Avec ces réglages, il sera mis à 1 lors du débordement puis mis à zéro lors de la comparaison avec OCR1A.   Tout cela AUTOMATIQUEMENT !



==> TIMSK =0b00000100 (ok pour interruption  débordement)

Pendant ce temps, le µC est LIBRE !

On obtient EXACTEMENT le même comportement qu'à l'exemple 9.
Mais cette fois ci, ce n'est plus le coeur qui gère les mises à "1" ou à "0". C'est le timer qui fait cela en automatique. Le coeur du µC est libre et il n'y a aucun risque de "petit" contretemps.





3/ Utilisation des interruptions externes : INT0, INT1....


La logique est la même que pour le chronomètre : Au lieu de surveiller les entrées tout le temps, le coeur confie cette tâche aux registres qui sont spécialisés pour cela. Le registre GIMSK offre la possibilité de surveiller les pattes B6 (INT0) et A2 (INT1). On ne va pas se gêner ! C'est tellement facile !
Exemple 11 : Détection des évènements externes

On reprend l'exercice 5, mais cette fois,
- Le BP "Start"  devra être relié à B6 (INT0).
- Le BP "Stop"  devra être relié à A2 (INT1).
(Le schéma ci-contre doit donc être modifié en conséquence !!!!)



(schéma de l'exemple 5)
La variable "Memoire" servira à mémoriser le dernier appui effectué.







Deux sous-programmes d'interruptions (un pour INT0 et un pour INT1) vont permettre de gérer différemment chaque évènement.
Ils sont TRES courts !!!!






- Au moment de l'initialisation, on a besoin d'une ligne supplémentaire pour autoriser le déclenchement des interruptions INT0 et INT1. Avec le registre GIMSK, le premier "1" c'est pour INT1, le second c'est pour INT0.
- MCUCR=3  permet de ne réagir qu'aux fronts montants.


La boucle principale est encore disponible pour d'autres tâches de calcul ! Cool !


Tout appui, aussi bref qu'il soit, à n'importe quel moment, sera détecté et géré immédiatement !!



L'intérêt des interruptions externes réside surtout dans la rapidité de réaction face à un évènement ponctuel. Ainsi la mesure d'une durée peut être ultra précise puisque le Timer peut être lu immédiatement lors de l'arrivée de l'évènement. Exemple pour la détection du signal DCC :
Exemple 12 : Mesure d'une durée.
Dans le montage ci-contre, l'optocoupleur permet de détecter les créneaux du signal DCC
- Un créneau "long" dure 220µs ==> DCC=0
- Un créneau "court" dure 116µs ==> DCC=1

Il faut donc mesurer la durée du créneau pour connaître la valeur de l'info transmise.

Dès que l'interruption est déclenchée :
- on consulte le chronomètre. S'il est inférieur à 160µs alors c'est un créneau court : DCC=1. Sinon DCC=0

- On remet le compteur à zéro pour chronométrer le créneau suivant.




- TCCR1B=5 : On règle le Timer 1 de façon à augmenter d'un cran à chaque microseconde. La valeur du Timer est ainsi égale au temps en microseconde.
- MCUCR =3 : Pour que l'interruption INT0 (et donc la mesure) ne soit déclenchée que par les fronts montants.

 


Aperçu du signal avec l'oscilloscope :
Un créneau long dans les rails provoque une information  "0" tandis qu'un créneau court provoque une sortie à "1"





4/ Utilisation de l'EEPROM :


L'EEPROM est une mémoire non volatile. C'est-à-dire qu'elle conserve ses informations même si une coupure de courant survient. C'est pratique pour garder en mémoire l'adresse de votre décodeur ou les réglages effectués pendant l'arrêt de votre réseau.


 Exemple 13 : Programmation initiale del'EEPROM (pas obligatoire mais pratique)

Ce n'est pas du langage C puisque c'est le logiciel de programmation qui effectue l'écriture initiale. Attention, je vous montre cette partie telle qu'elle se présente dans mon logiciel. Il se peut que chez vous ça se présente différemment si vous utilisez un autre compilateur.


Au départ, l'EEPROM est vierge :
Les valeurs qui sont inscrites dans l'EEPROM s'affichent en hexadécimal.
Tout est réglé à "0xFF" = 0b11111111 = 255
Adresse de la case identifiée :

"0000" & "01"

==> 0x000001   = 1
   (hexadécimal)    (décimal)
Chaque case possède une adresse. Les lignes et les colonnes sont numérotées aussi en hexadécimal. Sur l'image ci-dessus, on pointe sur la case située à l'adresse 0x000001

On peut modifier individuellement chaque case pour y placer la valeur souhaitée :
Exemple :
 Valeurs placées :
hexadécimal=> binaire
   0x01      => 0000 0001
   0x02      => 0000 0010
   0x04      => 0000 0100
   0x08      => 0000 1000
   0x10      => 0001 0000
   0x20      => 0010 0000
   0x40      => 0100 0000
   0x80      => 1000 0000
Il ne reste plus qu'à sauvegarder cette modification.
C'est fini ! Vous pouvez passer à l'écriture du code.
 L'EEPROM sera automatiquement chargée lors de la mise en place du programme dans le µC



Lecture de l'EEPROM avec l'instruction EEPROM_Read(Adresse)
 Exemple14 : Lecture de l'EEPROM

On souhaite afficher successivement les 7 valeurs
de l'EEPROM (rentrées ci-dessus) vers le portA.

Le changement s'effectue toutes les 0.5s grâce au Timer0 réglé comme dans l'exemple 5.

Les valeurs programmées ci-dessus vont afficher un chenillard qui va de A0 vers A7 et qui recommence.

On commence par définir une variable qui servira de "pointeur" d'adresse.

Le sous-programme effectue les actions suivantes
- RAZ du compteur (pour le respect du temps)
- Incrémentation de la variable  "pointeur"
- Ecriture sur le port A de la valeur située en EEPROM à l'adresse du "pointeur"


Remarquez que :
- Tout le port A est en sortie DDRA=255
- Il est possible d'accélérer le rythme en diminuant les valeurs de OCR0A et OCR0B.
OCR0B & OCR0A = 0000010001111010
L'octet OCR0B étant l'octet de poids fort, il aura beaucoup plus d'influence que OCR0A


Et en attendant, le coeur peut faire autre-chose.





Ecriture dans l'EEPROM avec l'insstruction EEPROM_Write (adresse,Valeur)
 Exemple15 : Ecriture dans l'EEPROM

Chaque appui sur le bouton poussoir fait avancer le chenillard d'un cran.

En cas de disparition de l'alimentation, on doit mémoriser l'état des LEDs en EEPROM.
Le bouton poussoir est branché sur la broche B6 pour solliciter l'interruption Int0.

L'interruption exécute trois actions :
- "while(PINB6_bit)" permet d'attendre que l'utilisateur relâche le bouton poussoir pour éviter les rebonds de l'interrupteur.
- La valeur du PORTA est incrémentée pour déplacer l'allumage d'un cran
- On écrit dans l'EEPROM,  à l'adresse 0x01 la valeur du portA.



Au moment de l'initialisation, on va lire le contenu de l'EEPROM pour retrouver la dernière valeur écrite à l'adresse 0x01.


Et en attendant, le corps du programme peut s'occuper d'autre chose. car tout se passe pendant les interruptions.





BRAVO !!! Vous êtes arrivé au bout de cette initiation.
J'espère que toutes ces lignes de code ne vous auront pas rebuté. Si vous avez tout suivi, alors aucun souci ! Vous êtes prêt pour attaquer la programmation de votre propre décodeur DCC.
Ce sera l'objet de la prochaine page.

Maintenant, le meilleur exercice consisterait à faire vous-même des petits programmes élémentaires pour "voir" ce qui marche et ce qui ne marche pas.
Evidemment, je reste à votre écoute. Si vous souhaitez des compléments d'information ou si vous avez des remarques sur cette page, ecrivez-moi un message (comme d'hab, enlevez toutes les lettres "z" qui sont dans l'adresse)