Un coup d'oeil dans les coulisses
Avant de continuer avec la présentation du code pour aujourd'hui on va d'abord jeter un coup d'oeil dans les coulisses. Atmel Studio et GCC nous rendent la vie plus facile en générant du code supplémentaire pour nous. Pour voir le code généré par GCC il faut ouvrir le fichier listing produit par le compilateur. Heureusement il n'est pas nécessaire de chercher bien loin. Il se trouve dans la fenêtre solution explorer dans output files Double-cliquez dessus pour l'ouvrir.
Pourquoi ce petit programme occupe-t-il 192 octets de flash? Moins de 90 aurait du suffire!. Si on regarde le listing on constate que GCC a initialisé 10 vecteurs d'interruptions. Mais notre programme n'utilise aucune interruption donc notre code pourrait commencé à l'adresse 0. 20 octets perdus.
La routine __ctors_end occupe 8 octets, elle initialise le registre R1 comme générateur de constante avec la valeur '0'. Ensuite elle initialise le pointeur de pile à l'adresse 0x9F. Rien à reprocher à cette procédure sauf qu'elle pourrait-être installée à l'adresse 0x00 plutôt que 0x14.
La routine __do_copy_data copie les variables globales initialisées au début de la mémoire RAM à partir de la mémoire flash. Mais notre programme n'utilise pas de variables globales initialisées. 22 octets perdus.
La routine __do_clear_bss initialise à zéro toutes le reste de la RAM c'est à dire celle qui n'a pas été initialisée par __do_copy_data. Notre programme n'utilise aucune variable RAM, cette routine est donc aussi inutile. 40 octets de perdus.
Pour le reste il n'y a rien à reprocher au code que nous avons écris et qui a été compilé efficacement. Ça fait quand même 82 octets perdus par rapport à ce qu'on aurais obtenu si on avait programmé en assembleur.
Mais c'est le prix à payer pour se rendre la vie plus facile par la programmation en 'C' plutôt qu'en assembleur.
Bug dans le code d'hier
Retour sur le code d'hier, il y avait un bug et la seule raison pour laquelle ça fonctionnait est du à une particularité des AVR. Le bug est le suivant, comme je programme rarement en 'C' j'avais oublié que l'opérateur '-' a précédence sur l'opérateur '<<'. La ligne:
La conséquence de cette erreur est que la broche PORTB0 que j'avais configurée en sortie dans une instruction précédente était revenue en mode entrée. Lorsqu'une broche est en mode entrée sur les AVR et qu'on écris un '1' sur cette broche ça active le pullup. Le courant passant à travers le pullup était suffisant pour commuter le transistor en conduction et allumer la LED. Lorsque le programme remettait '0' sur la broche le pullup était retiré et le transistor cessait de conduire. Ainsi le bug est passé inaperçu. Jusqu'à ce que je veuille utiliser le PWM et que je me gratte la tête en me demandant: pourquoi ça ne fonctionne pas?
Selon ma logique les opérateurs '<<' et '>>' devraient avoir la précédence sur les opérateurs '+' et '-' parce que se sont des opérateurs de multiplication et division par puissance de 2. Mais il semble que Kernighan et Ritchie, les auteurs du language 'C', non pas la même logique que moi.
Le code modifié pour aujourd'hui
Comme je l'ai expliqué à la fin de la chronique précédente il s'agit d'ajouter 4 niveaux d'intensité à la LED en utilisant un périphérique PWM. Tous les ATtiny même le plus petit ATtiny4 ont des périphériques PWM implantés. l'ATtiny13a possèdes 2 canaux PWM fonctionnants sur la même minuterie. Nous n'en utiliserons qu'un seul. Puisque pour cette application la fréquence PWM pourrait être aussi basse que 70hz sans que l'oeil ne percoive de scintillement on aurait tout aussi bien put le faire entièrement en software. Allons-y quand même pour la version hardware.
Modulation en largeur d'impulsion
Puisque cette série de chroniques est une introduction et que les périphériques Pulse Width Modulation (PWM) sont des plus utilisés sur les microcontrôleurs je vais expliquer ce que c'est, comment ça fonctionne et à quoi ça sert.
Les sorties numériques des MCU, comme leur nom l'indique, n'ont que 2 niveaux de tensions, soit la sortie est à 0 volt, soit elle est à Vdd, Vdd étant la tension d'alimentation du MCU. Donc si on commute cette sortie entre ces 2 niveaux on obtient une onde rectangulaire. La modulation PWM ou en français Modulation en largeur d'impulsion consiste à faire varier le rapport de durée entre la partie haute et la partie basse de l'onde. Par exemple si la partie haute a la même durée que la partie basse on obtient une onde carrée. En anglais le rapport entre ces 2 alternances du cycle s'appelle le duty cycle que reverso traduit par rapport cyclique. Donc si la partie haute dure 25% la partie basse durera 75% du cycle.
Presque tous les MCU ont un périphérique PWM. Voici comment ça fonctionne dans sa forme la plus simple. Un compteur binaire appellé timer reçoit un signal d'horloge qui incrémente le compte à chaque tick. Dans le cas du ATtiny13a ce compteur s'appelle TCNT0 et c'est un compteur 8 bits. Il peut donc compter de 0 à 255.
Lorsqu'il arrive à 255 il retombe à zéro et le cycle recommence. De plus il faut un registre qui lui sert à déterminer le rapport cyclique. Dans le cas du ATtiny13a il y en a 2 parce qu'il y a 2 cannaux PWM, OCR0A et OCR0B. Supposons qu'on utilise OCR0A et qu'on met la valeur 0x7F dans ce registre. A chaque tick de l'horloge qui alimente TCNT0 la valeur de TCNT0 est comparée à celle de OCR0A et si TCNT0 égale OCR0A l'état de la sortie est inversée. Donc supposons qu'au départ TCNT0 a la valeur zéro et que la sortie est à 1. lorsque le compte dans TCNT0 atteindra 0x7F la sortie passera à zéro et le restera jusqu'à ce que TCNT0 débute un nouveau cycle. Le rapport cyclique de la sortie dépend donc de la valeur de OCR0A. Si ce dernier contient zéro la sortie sera toujours à 0 mais si la valeur de OCR0A=0xFF la sortie sera toujours à 1. On peut donc faire varier le rapport cyclique de 0 à 100% par incrément de 1/256.
Maintenant à quoi ça peut servir. Si presque tous les MCU en ont au moins 1 c'est que justement c'est très utile. On peut s'en servir pour positionner un servo-moteur, contrôler la vitesse d'un moteur en rotation continue, dans les circuits régulateurs ou convertisseurs de tension. Comme convertisseur digital/analogique ou encore pour contrôler l'intensité d'une LED comme nous allons faire.
Si on veut que la LED est une intensité de 75% il suffit de mettre le rapport cyclique à 75% donc on mettra dans OCR0A la valeur int(75/100*256). Il suffit que la période du cycle soit supérieur à 70Hz pour que l'oeil ne se rende pas compte du clignotement de la LED. En fait à partir d'environ 20Hz l'oeil ne perçoit déjà plus un clignotement mais un scintillement. A 60Hz on peut percevoir encore un léger scintillement si on observe du coin de l'oeil. A 70hz il n'y a vraiment plus de scintillement perçu même si le rapport cyclique est très bas, on perçois une LED toujours allumée mais à faible intensité.
Voici une capture d'écran prise à l'oscilloscope de la sortie PWM sur PB0. A gauche le rapport cyclique est de 12% et à droite de 50%.
code source
Voilà ce que notre programme va faire, il va faire clignoter la LED très rapidement et on va faire varier le rapport cyclique de 100% en diminuant la valeur de OCR0A de moitié à chaque étape. J'ai testé plusieurs variantes, voici celle que j'ai retenu.
notes sur l'optimisation du code
Le compilateur a beau être optimisant le programmeur doit aussi faire ça part. Voici quelques commentaires à ce sujet.
Cette variante utilise 222 octets de mémoire flash et aucune mémoire RAM. J'ai constaté que lorsqu'on défini une variable globale (variable en dehors des fonctions), cette variable est stockée dans la RAM plutôt que dans la banque de registres ce qui a des conséquences sur la taille du code et la vitesse d'accès à cette variable. Donc autant que possible on défini les variables à l'intérieur des functions. De toute façon c'est une bonne pratique en programmation de réduire les variables globales au mininum. Autre considération, notez que la variable rapport_cyclique est de type uint8_t. Les variables de type int sont de 16 bits donc occupent 2 octets et de plus augmente la taille du code car les AVR étant des processeurs 8 bits doivent manipuler les entiers 16 bits en 2 opérations plutôt qu'une seule. Deux instructions pour l'affectation d'une valeur. Deux instructions pour la copie d'une variable, etc. Donc encore une fois en autant que c'est possible utilisez les types int8_t ou uint8_t.
Pourquoi est-ce que j'ai utilisé une variable rapport_cyclique au lieu de manipuler la valeur directement dans le registre OCR0A? J'ai essayé les deux variantes. La version avec variable réduit le code de 6 instructions. En effet la variable étant enregistrée directement dans la banque de registres il n'est pas nécessaire d'aller la cherchée pour manipuler sa valeur, tandis que pour OCR0A il faut une instruction in pour transférer son contenu dans un registre avant toute manipulation et un out pour retourner le résultat dan OCR0A.
J'ai lu quelque part qu'il était plus efficace de créer une boucle infinie en utilisant l'instruction:
Qu'avons nous appris
- Ce qu'est un périphérique PWM et comment l'utiliser dans sa version la plus simple, ce qui est suffisant pour une introduction.
- Dans les expresssions il vaut mieux mettre plus de parenthèses pour grouper les sous-expressions que moins. Les parenthèses ne coûtent rien à la compilation et ça évite les bugs du à une erreur d'interprétation des précédences des opérateurs.
- Le programmeur même s'il progamme en 'C' doit lire le datasheet et bien comprendre le fonctionnement du micro-processeur sur lequel il travaille. Chaque détail compte.
- Le programmeur ne peut compter sur le compileur seul pour optimiser le code, là aussi il doit faire sa part.
prochaine étape
On va ajouter une fonction de clignotement à 3Hz pleine intensité. Ça va nécessité une modification à la fonction attend_pression_bouton() car la lumière va entrée dans ce mode seulement si le bouton est maintenu enfoncé plus de 2 secondes.
Aucun commentaire:
Publier un commentaire
Remarque : Seuls les membres de ce blogue sont autorisés à publier des commentaires.