Ardu? No!Initiation ≫ Le PWM

Le PWM

Nous savons allumer la led par

digitalWrite(LED_BUILTIN, HIGH);

nous savons l'éteindre, nous allons voir maintenant comment faire pour faire varier l'intensité.

Pour cela il faudrait pouvoir faire varier la tension sur la broche entre 0V et 5V. Mais on a dit que ce n'était pas possible, et qu'on ne pouvait mettre que du 0V ou du 5V. Sur une Uno, la broche correspondant à LED_BUILTIN est une sortie numérique uniquement. Mais nous sommes plus malin qu'une Uno (heureusement!). Nous ne pouvons pas faire varier la tension instantanée, mais nous pouvons faire varier la tension moyenne.

Période et fréquence

On va avoir besoin de deux notions électroniques: la période et la fréquence. Pour un signal électrique la période souvent noté T est le plus petit temps pour lequel le signal à l'instant t est le même que le signal à l'instant T+t. En langage plus clair, c'est le temps le plus petit pour lequel le signal se répète. La période est un temps et se mesure en secondes.

Image: Période d'un signal

La fréquence d'un signal c'est l'inverse de la période. En termes plus clairs, c'est le nombre de fois que le signal se répète par seconde. L'unité est le Hertz (abréviation Hz), et cela correspond à des s-1.

Pendant qu'on est dans les définitions, profitons-en même si tout ne nous servira pas pour cet exercice.

Le niveau haut (pour un signal digital), c'est quand la tension est haute. Le niveau bas, c'est quand la tension est basse. On est donc sur un niveau un temps non nul.

Un front montant c'est quand le signal passe de l'état bas à l'état haut. Un front descendant c'est quand le signal passe de l'état haut à l'état bas. Dans un modèle simplifié un front n'a pas de durée. En pratique ce n'est évidemment pas possible.

Image: Front montant, descendant, niveau haut et bas

Pour un signal simple, la période est donc le temps qui sépare deux fronts montant ou deux fronts descendant, et c'est aussi le temps de l'état haut plus de l'état bas.

Enfin le rapport cyclique c'est le temps de l'état haut divisé par la période. C'est donc un nombre sans dimension qui peut varier entre 0 (signal toujours bas) à 1 (signal toujours haut).

On parlera de signal carré pour ce type de signal simple et régulier, même si le niveau haut est différent du niveau bas. En fait le carré à 4 côtés égaux, mais ici deux côtés sont des temps, et deux sont des tensions. Cela n'a pas de sens de dire qu'ils sont égaux ou pas. On emploie quand même carré, rectangulaire serait plus approprié. Tant pis.

Pulse Width Modulation?

Pour faire varier la tension moyenne, on va allumer la diode pendant un temps t1, et l'éteindre pendant un temps t2. Si t1 est petit devant t2, la led ne va pas être allumée longtemps, et on aura un faible éclairement. Si c'est l'inverse, la led brillera un maximum. Si on va assez vite, l'œil ne perçoit plus que la moyenne de l'éclairement.

Image: Chronogrammes de PWM

La tension moyenne ne dépend pas de la période du signal, et donc ne dépend pas de la fréquence. Elle ne dépend que du rapport cyclique.

Choix du rapport cyclique

Pour la variation de l'éclairement, on peut se contenter de 256 niveaux, c'est déjà pas mal. Si je choisis cette valeur, c'est parce qu'un type byte contient un nombre de 0 à 255. C'est plus simple à coder. Il nous faut une variable pour garder en mémoire ce rapport. Nous l'appellerons rapportCyclique. Si rapportCyclique est nul, la led est éteinte. Si rapportCyclique vaut 255, la led est allumée à son maximum. On peut donc faire:
- allumer la led
- attendre rapportCyclique ms
- éteindre la led
- attendre (255 - rapportCyclique) ms
Ainsi, la période est constante et est égale à la somme des deux attentes, soit exactement 255 ms. Dans un premier temps, on va utiliser cette période de 255ms

Bien entendu, si on utilise un rapport cyclique fixe, on ne verra pas que l'on peut faire varier la lumière. Nous allons donc faire une rampe, la led est éteinte, s'allume progressivement et quand on atteint le maximum, on recommence.

On pourrait penser que le "on recommence", c'est loop() qui nous le fournirait, et à l'intérieur de loop() on ferait juste une rampe lumineuse. Mais ici il y a plus simple (moins cher en fait). Si on incrémente rapportCyclique régulièrement, quand il atteindra la valeur 255, l'incrémentation le fera repasser à 0. Pour un byte 255+1=0 car on ne peut pas représenter le nombre 256 sur 8 bits. On dit qu'il y a débordement. Le plus souvent cela pose un problème, mais dans le cas précis, c'est une opportunité. Il suffit donc de faire uniquement dans loop() les 4 ordres décrits plus haut et de rajouter une incrémentation de rapportCyclique. Ne pas oublier dans setup de bien mettre la pin en sortie.

Ça clignote!

je vous engage donc à réaliser ce programme. Il n'est pas parfait, mais il montre bien le fonctionnement du PWM. Aude?byte rapportCyclique; // De 0 à 255! void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); // Allumée ... delay(rapportCyclique); //... pendant quelques ms digitalWrite(LED_BUILTIN, LOW); // Eteinte ... delay(255 - rapportCyclique); // ... pendant le reste du temps rapportCyclique++; // Pour faire la rampe lumineuse }

Voici ce que l'on observe: la led clignote avec de brefs éclairs au début, pour finir quasiment tout le temps allumée, puis cela recommence toutes les 65 secondes. En fait le clignotement n'est pas assez rapide et l'œil le voit. Il faut donc aller plus vite. Mais au moins avec ce ralenti, on peut comprendre ce qui se passe.

Aller plus vite

Notre œil ne perçoit plus les variations si la fréquence est supérieure à 50Hz. C'est pour cela que le réseau Français est à 50Hz. En dessous certaines lampes clignoteraient, et il vaut mieux choisir une fréquence basse pour transmettre de l'énergie. 50Hz est un bon compromis.

Nous avons dit que notre boucle durait 255ms. C'est la période. Pour avoir du 50Hz, soit une période de 20ms (1/50Hz), il faut aller environ 10 fois plus vite. Remplacez delay(rapportCyclique); par delay(rapportCyclique/10); et delay(255-rapportCyclique); par delay((255-rapportCyclique)/10); Aide?byte rapportCyclique; // De 0 à 255! void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); // Allumée ... delay(rapportCyclique / 10); //... pendant quelques ms digitalWrite(LED_BUILTIN, LOW); // Eteinte ... delay((255 - rapportCyclique) / 10); // ... pendant le reste du temps rapportCyclique++; // Pour faire la rampe lumineuse }

C'est beaucoup mieux, mais du coup nous n'avons que 25 niveaux. Cela se voit surtout dans les faibles éclairements; la lumière monte par crans.

Notez aussi que l'œil n'est pas linéaire. On a l'impression que la lumière monte vite au début puis stagne à la fin. Mais c'est l'œil humain. Le flux lumineux est sensiblement proportionnel. Je dis sensiblement car le rendement d'une led diminue si la puissance augmente. Mais ce n'est qu'annexe.

Profitez-en pour essayer d'autre facteurs de divisions, 5 par exemple.

Pour retrouver les 256 niveaux, retirons les deux divisions par 10 et remplaçons delay() par delayMicroseconds(). On va aller 1000 fois plus vite, cela ne devrait plus clignoter. Aide?byte rapportCyclique; // De 0 à 255! void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); // Allumée ... delayMicroseconds(rapportCyclique); //... pendant quelques ms digitalWrite(LED_BUILTIN, LOW); // Eteinte ... delayMicroseconds(255 - rapportCyclique); // ... pendant le reste du temps rapportCyclique++; // Pour faire la rampe lumineuse }

Pas vrai dites-vous? Ce qui se passe est qu'au début, on parcourait toutes les valeurs de 0 à 255 en 62s environ, mais en allant 1000 fois plus vite, la rampe de lumière dure 62ms. Et c'est cela que l'on voit papilloter. Il faut donc rester sur un niveau de rapport cyclique non plus 255µs, mais au moins 100 fois plus. Pour cela, il suffit (on va croire que c'est élémentaire!) d'incrémenter rapportCyclique une fois sur 100.

Une fois sur 100

"A la main", on ferait 1, 2, 3, ..., 98, 99, 100: j'incrémente, 1, 2, ... Comme on fait de l'informatique, on préfère compter plutôt à partir de 0. C'est une habitude, ce n'est pas une obligation. Nous avons 100 boucles à faire avant d'incrémenter rapportCyclique, il nous faut donc une variable boucle par exemple qui va compter de 0 à 99. A chaque tour de loop(), on va l'incrémenter et quand elle arrivera à 100, on la remet d'une part à 0, d'autre part on incrémente rapportCyclique.

Remplacez donc

rapportCyclique++; // Augmentation de la lumière 

par

boucle++;
if (boucle == 100) { // Ceci est fait une fois sur 100
  boucle = 0;
  rapportCyclique++; // Augmentation de la lumière 
}

Aide?byte rapportCyclique; // De 0 à 255! byte boucle; // Pour incrémenter 1 fois sur 100 void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); // Allumée ... delayMicroseconds(rapportCyclique); //... pendant quelques ms digitalWrite(LED_BUILTIN, LOW); // Eteinte ... delayMicroseconds(255 - rapportCyclique); // ... pendant le reste du temps boucle++; if (boucle == 100) { // Ceci est fait une fois sur 100 boucle = 0; rapportCyclique++; // Augmentation de la lumière } }

Quelques remarques:
- n'oubliez pas de déclarer boucle, par exemple comme un byte (256 valeurs, plus qu'il n'en faut).
- j'aurais pu mettre ++rapportCyclique; cela aurait été pareil, mais rapportCyclique++; est plus courant.
- en fait boucle va prendre 101 valeurs alors qu'on avait dit 100. C'est vrai, mais dès qu'elle arrive à 100, elle passe immédiatement à 0. On a donc bien 100 fois le passage pour chaque valeur de rapportCyclique
- on pourrait aussi écrire:

if (++boucle == 100) { // Ceci est fait une fois sur 100
  boucle = 0;
  rapportCyclique++; // Augmentation de la lumière 
}

c'est équivalent, peut être un peu moins clair au début.