3. PWM: un type de sortie intéressant


Dans le chapitre précédent décrivant l'Arduino, je vous ai présenté rapidement ce que l'on pouvait faire avec une sortie digitale "tout-ou-rien". Ce type est équivalent au fonctionnement d'un interrupteur. Dans ce nouvel exemple, je vous présente un autre type de sortie: le PWM pour Pulse Width Modulation, ce qui signifie: "impulsion à largeur variable". On parlera aussi de signal rectangulaire.

Qu'est-ce que cela signifie? Habituellement, lorsqu'on parle de signal continu, on vous informe que la valeur est constante au cours du temps. Par exemple: à la sortie d'une pile électrique, vous mesurerez idéalement une valeur constante de 9V. Dans le cas d'un signal rectangulaire, la valeur mesurée en amplitude est aussi de 9V, mais pas en permanence soit à interval régulier. Par exemple, pendant un intervalle de référence, la valeur du signal est 9V pendant 1/10ème de cet intervalle. La largeur de l'impulsion peut donc varier de 0 à la totalité de l'intervalle de référence. Dans le cas de l'Arduino, celui-ci est capable d'envoyer un signal ou "valeur" sur une sortie. Cette valeur varie de 0 à 255 et correspond à une impulsion de longueur 0/255 = 0 à 255/255 = 1.

Maintenant, essayons de vulgariser cet effet et voir comment il agit. Considèrons un composant comme une led. Si on envoie un signal rectangulaire de largeur 1, alors la led éclairera en permanence. Par contre, si on lui envoie de petites impulsions de largeur 1/10, la led n'a pas le temps de s'allumer ou de "réagir", qu'elle s'éteint déjà à cause de la durée d'impulsion trop petite. Elle s'allume pendant 1/10ème de l'intervalle alors qu'elle est éteinte pendant les 9/10ème de l'intervalle. L'observateur aura l'impression que cette même led éclaire moins fortement, puisqu'elle est plus souvent éteinte qu'allumée... Bien évidement, il faut que cet intervalle de référence soit suffisamment court (ou qu'il soit éxecuté de nombreuses fois par seconde) pour que l'on ne perçoive pas un effet de clignotement rapide...

Voilà pour la "théorie" très simplement résumée. Si vous n'avez pas bien compris mes explications, ce n'est pas grave... ;-) Retenez seulement que: "faire varier la valeur de sortie sur un port PWM est équivalent à faire varier l'intensité lumineuse d'une led" même si ce n'est pas tout à fait ça... Il faut en fait interpréter cela comme une "information digitale" qui est envoyée au port de sortie, mais qui est rendue visible grâce à une led...

Passons maintenant à un premier exemple concret.




3.1. Exemple du fader

Le principe est celui du fader ou "variateur d'intensité lumineuse". Il est très simple. A l'aide d'une sortie PWM, nous allons alimenter une led en faisant varier progressivement la longueur de l'impulsion de 0 à 255 puis à 0 et ainsi de suite... Tout d'abord le montage:


Comme vous pouvez le voir, il y a peu de différence par rapport au montage précédent: la led est verte ;-) et la sortie utilisée est la "3". Mais identifiez bien cette sortie: c'est en fait "~3". Le "~" indique que la sortie est capable de générer un signal PWM. Si on branche la led sur la sortie 2, l'intensité ne variera pas... On prendra aussi un instant pour vérifier qu'il y a 6 sorties PWM sur un Arduino Uno: 3, 5, 6, 9, 10 et 11.

Le code se résume ainsi:

int led = 3;
int i = 0;

void setup()
{
  pinMode( led, OUTPUT );
}

void loop()
{
  for ( i = 0; i < 256; i = i + 1 )
  {
    analogWrite( led, i );
    delay( 10 );
  }
}

Examinons ce programme en détails...

Au départ, deux variables (led et i) représentant des nombres entiers (int) sont déclarées et initialisées par une valeur.

Puis, deux fonctions sont déclarées. Elles sont requises pour que le programme fonctionne: setup() et loop(). Le contenu de chaque fonction est appelé le corps et cette zone est délimitée par les symboles { et }.

La première (setup()) est exécutée une seule fois dès la mise sous tension de l'Arduino, ou lorsque le bouton reset est pressé. Dans le corps de cette fonction, on configure les types de ports E/S à utiliser. Par exemple, on utilise l'instruction pinMode() pour initialiser le pin digital led (=3) en mode OUTPUT (sortie).

La seconde (loop()) est exécutée en permanence et le code qui s'y trouve boucle indéfiniement. Le corps de cette fonction contient une instruction for() que l'on peut interpréter ainsi:
  • dans le corps de l'instruction, exécute le code qui s'y trouve pour la valeur courante de la variable i.
  • la première valeur de i est 0 (i = 0). C'est l'étape d'initalisation.
  • ré-exécute ce code pour toutes les valeurs de i tant que cette valeur est strictement inférieure à 256 (i < 256), donc sans jamais atteindre cette dernière valeur. C'est ce qu'on appelle la condition d'arrêt.
  • à chaque itération ajoute 1 à la valeur courante de i (i = i + 1) après que le corps ait été exécuté. C'est l'itérateur.

Donc, la suite équivalente d'instructions serait:
analogWrite( led, 0 );
delay( 10 );
analogWrite( led, 1 );
delay( 10 );
analogWrite( led, 2 );
delay( 10 );
jusqu'à:
analogWrite( led, 255 );
delay( 10 );

Comme cette instruction for() se trouve dans la fonction loop(), celle-ci est ré-exécutée indéfiniement... Les valeurs qui sont envoyées à analogWrite() sont: 0, 1, 2... 254, 255, 0, 1, 2... etc... N'oublions pas qu'un délais d'attente de 10ms est imposé entre chaque changement de valeur de la sortie avec l'instruction delay( 10 ). Sans ça, la led semblerait allumée en permanence.

Voyons le résultat:


Comme on le voit, l'intensité lumineuse passe du maximum au minimum entre chaque appel à loop(), puisque la fin de l'instruction for() se termine par analogWrite( led, 255 ), alors que l'appel suivant débute par analogWrite( led, 0 ). Maintenant, nous allons modifier un peu le programme pour insérer une exctinction progressive de la led... Pour cela nous avons besoin d'une seconde instruction for() qui va modifier la valeur de la sortie selon une courbe décroissante. Nous insérons les changements de code suivants, alors que le reste du programme demeure inchangé:

void loop()
{
  for ( i = 0; i < 256; i = i + 1 )
  {
    analogWrite( led, i );
    delay( 10 );
  }

  for ( i = 254; i > 0; i = i - 1 )
  {
    analogWrite( led, i );
    delay( 10 );
  }
}

Remarquez:
  • la valeur initiale 254 (puisque l'instruction précédente se terminait par 255),
  • la condition i > 0 (permettant de terminer la suite de valeurs par ...3, 2, 1 sans atteindre 0)
  • et enfin la variation décroissante de la variable i de 1 en 1 avec i = i - 1.
Ainsi, pour l'ensemble du programme nous obtenons la suite de valeurs:
  • premier for(): 0, 1, 2... 254, 255
  • second for(): 254, 253... 2, 1
  • premier for(): 0, 1, 2...
  • etc...





3.2. Exemple du phare côtier

Ce projet est très semblable au fader puisqu'il nécessite aussi de faire varier l'intensité lumineuse de la led. Cependant, il y a une petite subtilité... Un phare côtier est normalement représenté par une lampe munie d'une lentille de Fresnel rotative. Bien évidemment, notre but est de simuler cet effet sans avoir à introduire de mécanisme rotatif.

Dans ce cas, examinons l'effet réel. Lorsqu'on fixe un tel phare, on observe deux caractéristiques:
  • l'intensité croit progressivement mais non linéairement (de plus en plus rapidement et de plus en plus intensément)...
  • ... jusqu'à aboutir à un simili flash. Ce flash est en fait le point où la lentille de Fresnel est orientée dans la direction de l'observateur.
  • (puis l'intensité décroit selon la même courbe)

La première caractéristique de cette simulation est la représentation d'un accroissement (et d'une diminution) de l'intensité lumineuse selon une courbe non-linéaire. Le calcul de la valeur à envoyer à analogWrite() est différent du calcul linéaire effectué dans l'exemple du fader. Nous allons pour cela pré-définir une liste de valeurs qui correspond à la courbe désirée plutôt que de la calculer à l'aide d'une formule... (ce qui serait possible en utilisant une fonction trigonométrique).

Introduisons la notion de tableau dans notre programme. Il se définit ainsi: int lum[ 4 ] = { 11, 22, 33, 44 } et signifie que 4 valeurs entières (11, 22, 33 et 44) sont stockées dans un tableau nommé lum et chaque valeur est implicitement indexée par un numéro: 0, 1, 2 et 3. Dans notre exemple de phare côtier, nous définissons la progression de l'intensité par une série de 32 valeurs pouvant varier de 0 à 255.
int lum[ 32 ] = { 1, 1, 1, 1, 2, 2, 2, 5, 5, 9, 14, 30, 50, 100, 140, 255, 140, 100, 50, 30, 14, 9, 5, 2, 1, 0, 0, 0, 0, 0, 0, 0 }

Bien évidemment, j'ai établi cette suite de valeurs un peu au hasard, mais en essayant de suivre une progression croissante qui s'accélère jusqu'à un point de pleine intensité (255) pour redescendre ensuite jusqu'à 0. En observant attentivement le trait de lumière réel, on a l'impression que ce trait s'éloigne plus rapidement qu'il ne s'approche. J'expliquerais cela par la persistence rétinienne du "flash". En effet, le temps que le flash se dissipe, l'oeil n'a plus le temps de distinguer le reste du trait qui s'éloigne. Il lui semble alors plus court... C'est pourquoi j'ai choisi une courbe décroissante plus rapide que la courbe montante... Mais ce n'est qu'une interprétation personnelle. Libre à chacun de choisir des courbes de valeurs différentes.

La seconde caractéristique est la simulation du flash. Le fait d'utiliser la valeur maximale est importante dans cette suite, mais aussi les valeurs adjacentes (140) doivent être beaucoup plus faibles afin d'accentuer l'effet. Voici le code que nous obtenons:

int led = 3;
int i = 0;
int lum[ 32 ] = { 1, 1, 1, 1, 2, 2, 2, 5, 5, 9, 14, 30, 50, 100, 140, 255, 140, 100, 50, 30, 14, 9, 5, 2, 1, 0, 0, 0, 0, 0, 0, 0 };

void setup()
{
  pinMode( led, OUTPUT );
}

void loop()
{
  for ( i = 0; i < 32; i = i + 1 )
  {
    analogWrite( led, lum[ i ] );
    delay( 20 );
  }
}

Remarque: dans l'exemple du fader, nous avions utilisé la valeur de la variable i comme valeur d'intensité pour la fonction analogWrite(). Ici, nous utilisons cette valeur comme indice dans le tableau lum. Les intensités envoyées à analogWrite() sont le contenu de chacun des éléments du tableau lum. Ainsi, la fonction analogWrite() reçoit les valeurs contenues dans lum[ 0 ], lum[ 1 ], lum[ 2 ], etc... jusqu'à lum[ 31 ].

Mais notre simulation n'est pas tout à fait complète. Un phare possède une signature qui l'identifie et le distingue des autres phares. Par exemple, on observera deux flashs espacés d'une seconde environ, sur un cycle de 5 secondes. Ceci est simulé dans notre programme d'une part par l'exécution d'une boucle permettant deux affichages consécutifs du flash, et d'autre part par l'introduction de pauses supplémentaires. Et tout simplement:

int led = 3;
int i = 0;
int j = 0;
int lum[ 32 ] = { 1, 1, 1, 1, 2, 2, 2, 5, 5, 9, 14, 30, 50, 100, 140, 255, 140, 100, 50, 30, 14, 9, 5, 2, 1, 0, 0, 0, 0, 0, 0, 0 };

void setup()
{
  pinMode( led, OUTPUT );
}

void loop()
{
  for ( j = 0; j < 2; j = j + 1 )
  {
    for ( i = 0; i < 32; i = i + 1 )
    {
      analogWrite( led, lum[ i ] );
      delay( 20 );
    }
    delay( 1000 );
  }
  delay( 5000 );
}

Nous avons ici deux boucles/instructions for() imbriquées. La plus extérieure indique qu'il faut exécuter 2 fois la boucle/instruction à l'intérieur... et ces deux exécutions sont espacées de 1000ms par delay( 1000 ). Enfin, une fois que ces deux boucles ont été exécutées, un délais de 5000ms est imposé avant de reprendre le cours du programme...



Lorsque vous testerez ce programme avec votre Arduino, essayer de modifier le delai de 20ms entre chaque itération de l'instensité et examiner le résultat. Aussi, essayer d'ajouter des valeurs d'intensité dans le tableau. Ces deux modifications auront une incidence sur la vitesse de l'animation...





3.3. Exemple du signal de passage à niveau type U.S.

Dans ce dernier exemple, nous allons complexifier un peu le code en introduisant deux leds qui vont clignoter alternativement. Tout d'abord, les branchements:


La seconde led est ajoutée sur le pin 5, lui aussi PWM. Le programme est très similaire au précédent. Cependant, pour simplifier les choses, nous allons introduire un deuxième tableau de valeurs lum2. Celles-ci seront les mêmes que dans le premier tableau lum1 mais les valeurs seront décalées de façon à simuler l'allumage des deux feux alternativement.

int led1 = 3;
int led2 = 5;
int i = 0;

int lum1[ 38 ] = { 0, 0, 0, 2, 5, 8, 11, 15, 20, 25, 30, 35, 40, 50, 70, 100, 130, 180, 220, 255, 220, 180, 130, 100, 70, 50, 40, 35, 30, 25, 20, 15, 11, 8, 5, 2, 0, 0 };

int lum2[ 38 ] = { 255, 220, 180, 130, 100, 70, 50, 40, 35, 30, 25, 20, 15, 11, 8, 5, 2, 0, 0, 0, 0, 0, 2, 5, 8, 11, 15, 20, 25, 30, 35, 40, 50, 70, 100, 130, 180, 220 };

void setup()
{
  pinMode( led1, OUTPUT );
  pinMode( led2, OUTPUT );
}

void loop()
{
  for ( i = 0; i < 38; i = i + 1 )
  {
    analogWrite( led1, lum1[ i ] );
    analogWrite( led2, lum2[ i ] );
    delay( 30 );
  }
}

Et le résultat obtenu (imaginez-le à l'horizontal):


Bien évidemment, nous aurions pu utiliser des sorties "tout-ou-rien" sans mode PWM, comme dans le premier programme du clignotant, mais l'effet n'aurait pas été aussi réaliste...

Voilà qui conclu ce chapitre sur des utilisations simples d'une ou plusieurs sorties PWM et les exemples des projets associés: le fader, le phare côtier et le signal de passage à niveau style US. Nous avons pu examiner quelques-unes des instructions du langage de programmation, ainsi que les fonctions principales qui permettent de structurer un programme pour l'Arduino. Ce qui est frappant est la facilité avec laquelle il est possible de développer et modifier un programme et de voir les résultats rapidement en ajustant quelques instructions...





3.4. Utilisation d'un séquenceur

Dans les exemples précédents, nous avons pu voir l'utilisation de l'instruction delay() qui crée un délai dans l'exécution du programme. Cependant, si on désire animer l'éclairage du réseau comme dans l'article jour et nuit..., il n'est pas vraiment concevable (tout en étant possible malgré tout...) d'imposer des délais de plusieurs secondes, voire minutes, pour une telle animation... Par exemple:

#define LED_PWM 11

void setup()
{
  pinMode( LED_PWM, OUTPUT );
}

void loop()
{
  // nuit pendant 100s
  analogWrite( LED_PWM, 0 );
  delay( 100000 );

  // lever du jour sur 25s
  for ( int i = 0; i < 255; ++i )
  {
    analogWrite( LED_PWM, i );
    delay( 100 );
  }

  // jour pendant 100s
  analogWrite( LED_PWM, 255 );
  delay( 100000 );

  // coucher du soleil sur 25s
  for ( int i = 255; i > 0; --i )
  {
    analogWrite( LED_PWM, i );
    delay( 100 );
  }
}

En effet, on peut vouloir utiliser l'Arduino pour d'autres fonctionnalités pendant les longs moments d'attente...

Donc, nous allons définir ce que j'appelle un "séquenceur". C'est une manière de signaler les "évènements" auxquels des changements se produisent (lever du soleil, coucher du soleil, etc...) et de définir des "régimes" pendant lesquels une action est effectuée sans avoir à stopper le cours du programme. Chaque évènement est déterminé en comparant une "horloge" courante à des "temps" pré-déterminés.

Ainsi, nous commençons par définir les temps auxquels les évènements ont lieux. Dans l'exemple suivant:
  • la nuit dure de 0 à 100s
  • le soleil se lève de 100s à 125s
  • le jour dure de 125s à 225s
  • le soleil se couche de 225s à 250s
  • et l'horloge repart à 0.
Ce qui se traduit par un tableau de 4 événements, avec des indications en milli-secondes pour satisfaire à la définition de l'instruction millis():

#define EVENEMENTS 4
unsigned long evenements[ EVENEMENTS ] =
{
  100000, // lever
  125000, // jour
  225000, // coucher
  250000  // nuit
};

Ensuite, il suffit de tester si l'horloge courante (horloge) se situe entre deux évènements et appliquer le régime qui y correspond. Par exemple, dans le cas de la nuit, si l'horloge indique un temps inférieur à l'évènement "lever du jour", alors nous sommes au milieu de la nuit... (sortie PWM à 0: analogWrite( LED_PWM, 0 ))

horloge = millis();
if ( horloge < debut + evenements[ 0 ] )
{
  analogWrite( LED_PWM, 0 );
}

La variable debut sert à sauvegarder le temps du début du cycle et est mise à jour à chaque fin de cycle. Toutes les comparaisons seront effectuées en séquence, d'où le nom de "séquenceur". Si l'évènement X est dépassé par l'horloge alors l'évènement suivant Y est testé. Et ainsi de suite...

Le programme sera ainsi écrit:

#define LED_PWM 11

#define EVENEMENTS 4
unsigned long evenements[ EVENEMENTS ] =
{
  100000, // lever
  125000, // jour
  225000, // coucher
  250000  // nuit
};
unsigned long debut;
unsigned long horloge;

void setup()
{
  pinMode( LED_PWM, OUTPUT );
  // on n'oublie pas d'initialiser la référence au début du cycle
  debut = millis();
}

void loop()
{
  // lecture de l'horloge
  horloge = millis();

  // description de la séquence des événements
  if ( horloge < debut + evenements[ 0 ] )
  {
    // nuit
    analogWrite( LED_PWM, 0 );
  }
  else if ( horloge < debut + evenements[ 1 ] )
  {
    // lever du soleil
    float lumiere =
      (float)( horloge - ( debut + evenements[ 0 ] ) ) /
      (float)( evenements[ 1 ] - evenements[ 0 ] );

    analogWrite( LED_PWM, (int)( 255 * lumiere ) );
  }
  else if ( horloge < debut + evenements[ 2 ] )
  {
    // jour
    analogWrite( LED_PWM, 255 );
  }
  else if ( horloge < debut + evenements[ 3 ] )
  {
    // coucher du soleil
    float lumiere =
      (float)( horloge - ( debut + evenements[ 2 ] ) ) /
      (float)( evenements[ 3 ] - evenements[ 2 ] );

    analogWrite( LED_PWM, (int)( 255 * ( 1.0f - lumiere ) ) );
  }
  else
  {
    // retour au début, remise à zéro du compteur
    debut = millis();
  }
}

Pour simuler le lever et le coucher du soleil, il suffit de faire varier une sortie PWM en fonction du temps qui passe... Ceci est effectué en déterminant la position de l'horloge entre le début et la fin du régime et en calculant un simple ratio (représenté par la variable lumiere) entre 0 et 1 que l'on multipliera à la valeur maximale de l'intensité 255. Dans le cas du coucher de soleil, il suffira de calculer le ratio entre 1 et 0 pour simuler une valeur décroissante...

Voilà, il n'y a rien de bien sorcier dans ce petit programme. Vous devriez pouvoir ajouter une seconde série de leds (comme dans l'animation que j'ai créée sur mon réseau) de façon à mélanger les tonalités lumineuses. Si vous avez l'intention d'utiliser des rubans de leds pour éclairer votre réseau, il suffit de se reporter au montage incluant le transistor de puissance TIP122 comme décrit dans l'article 8.2.1.

On peut aussi penser à quelques variantes afin de personnaliser le montage:
  • ajout de sorties supplémentaires qui permettront de simuler l'allumage de l'éclairage urbain pendant la nuit
  • ajout d'un second bandeau de leds comme précisé un peu plus haut
  • utilisation d'une variation non linéaire de l'intensité lors du lever et du coucher du soleil
J'ai pu créer de nombreuses animations différentes pour mon réseau. Les possibilités sont multiples. Je vous laisse réfléchir à tout cela... Bon montage!



8 commentaires:

  1. Merci pour cet article.

    Je suis en train de réfléchir au programme de mon éclairage. j'ai acquis le power-shield Sparkfun. Plus qu'à y mettre des refroidisseurs.
    par contre, j'ai regardé le shéma de celui-ci avec le logiciel Eagle. A priori, il utilises les broches 3-5-6-9-10-11, donc les sorties PWM. est-il possible d'utiliser les autres broches 2-4-7-8-12-13 afin de créer un petit séquenceur qui allumerai durant les phases de nuit ou jours d'autres leds?
    En gros: le jour se lève, les lumières sont toutes allumées grâce aux broches PWM. La nuit tombe, je veux allumer trois leds de lampadaires via la sortie 2 par exemple. Est-ce possible? Dis moi juste oui ou non et je verrai comment modifier le programme.

    Merci beaucoup.

    RépondreEffacer
  2. Oui c'est possible. Normalement le shield n'utilise pas les autres sorties (non PWM)

    Par contre:
    1) je ne sais pas exactement le nombre de mA consommés par le shield par sortie. Cela doit être assez peu (quelques mA). Mais il faut toujours garder en tête que la consommation totale supportée par l'Arduino est d'environ 150mA...
    2) utiliser les autres sorties de l'Arduino vient augmenter ce total... Je n'utiliserais pas toutes les autres sorties!
    3) 3 leds/lampadaires sur une seule sortie est trop, mais cela dépend des leds bien sû... Une led tire environ 20mA sur une sortie. 3 leds dépasseraient alors le max de 40mA par sortie... Utilises plutôt plusieurs sorties...

    Voilà,
    Patrick

    RépondreEffacer
  3. OK. C'est bien ce qu'il me semblait. Il va falloir que je réfléchisse à cela alors. D'après toi, je peux commander un petit circuit qui serait alimenté par l'arduino et qui lui servirai de "séquenceur"?

    RépondreEffacer
  4. Je ne comprends pas ce que tu désires faire: "D'après toi, je peux commander un petit circuit qui serait alimenté par l'arduino et qui lui servirai de "séquenceur"? "

    L'arduino est le séquenceur. Pourquoi voudrais-tu un circuit supplémentaire???

    RépondreEffacer
  5. Bonjour,
    Sur mon réseau, j'ai installé des rubans de leds blanc chaud et aussi des rubans de ledsRVB pour créer différentes ambiances ( plus rouge pour les couchers de soleil,etc..).
    Avec votre séquenceur, est-il possible de commander ces différents rubans: par exemple quand les leds blanches sont en train de décroitre,a mi-parcours, est-ce que je peux faire croitre mes leds rouge?
    J'ai bien compris que je peux créer d'autres évènements mais quand je passe au else if suivant, le précédent se bloque. Les ratios ne sont actifs que dans le if concerné.

    RépondreEffacer
  6. Bonjour,
    Hmmm... Je ne suis pas sur de bien comprendre le probleme. Pourriez-vous me faire un petit graphique avec le temps qui croit de gauche a droite par exemple, et decrire quel ruban s'allume ou s'eteint?
    Je crois voir que vous desirez "superposer" l'allumage et l'exctinction d'un ruban par rapport a l'autre, mais je n'en suis pas sur...
    Patrick

    RépondreEffacer
  7. En fait... pourriez-vous m'envoyer votre code?

    RépondreEffacer
  8. je souhaite fermer un passage a niveau dans les deux sens de circulation et ouverture après le passage du train.je débute avec arduino est ce possible de faire avec une arduino uno

    RépondreEffacer