4 Jan 20

Capteur optique pour frigo (partie 1)

Catégorie : hack
Étiquettes :

La porte du frigo est encore resté légèrement entrouverte cette nuit. C'est énervant parce que la lampe est restée allumée toute la nuit, et le fromage qui est juste à côté a légèrement transpiré ! Bref, il va falloir faire quelque chose. L'idéal serait un capteur qui nous indique par exemple par un signal sonore, que la porte du frigo n'est pas totalement fermée, et que la lumière est restée allumée à l'intérieur.

Le cahier des charges ressemblerait à quelque chose comme ça :

  • lorsque la porte du frigo est ouverte, on détecte l'allumage de la lampe, et un compteur se lance
  • si au bout d'une minute, la lampe est toujours allumée, il est probable que la porte du frigo soit restée légèrement entrouverte. Il faut alors activer une alarme sonore pour indiquer le problème
  • si la lampe s'est éteinte avant la minute du compteur, alors on annule tout
  • il faut prévoir un bouton pour mettre l'alarme en veille pour une nouvelle minute si besoin, par exemple lorsque l'on range les courses dans le frigo (ce qui prend généralement plus d'une minute)
  • il faut que le capteur ne coûte pas très cher, et qu'il puisse fonctionner avec une simple pile de 3V
  • il faut que cette pile dure longtemps. La consommation du capteur devra donc être réduite au minimum.

Bien, voilà en gros pour les contraintes. Le cœur du capteur sera piloté par un microcontrôleur ATTINY85 de chez Microchip. C'est vraiment tout petit, pas cher, et dispose de suffisamment d'entrées/sorties pour notre application. En plus, on peut le mettre en mode sommeil lorsqu'il ne se passe rien pour limiter la consommation. Il sera ensuite réveillé par une interruption lorsque la lumière s'allume, pour démarrer le compteur et au final déclencher l'alarme. Si la lumière s'éteint entre temps, alors le microcontrôleur retourne en hibernation. Côté coût, on peut trouver ces microcontrôleurs chez Aliexpress pour moins d'un euro pièce. Imbattable !

Partons sur cette configuration. Pour détecter le changement de luminosité dans le frigo, une simple photorésistance LDR (Light Dependent Resistor) fera l'affaire. Il s'agit d'un composant dont la résistance dépend de la quantité de lumière qu'il reçoit. Il suffira alors d'utiliser un pont diviseur de tension relié à une entrée du microcontrôleur pour détecter un changement d'environnement lumineux. Là encore, c'est très bon marché puisque ce composant ne revient qu'à quelques centimes pièce.

Datasheet du microcontrôleur

Avant toute chose, nous aurons besoin des spécifications techniques (datasheet) du microcontrôleur. Ce document est téléchargeable sur le site microchip.com. Je ferai régulièrement référence au contenu de ce document sous la forme [DATASHEET xxx], où xxx est le numéro de page en question dans ce document.

Les différentes broches des microcontrôleurs sont habituellement regroupées dans des ports. L'ATTINY85 ne dispose que d'un seul port, le PORTB, correspondant à six broches du microcontrôleur, numérotées de PB0 à PB5. Les deux dernières broches correspondent à l'alimentation (VCC) et à la masse (GND). Attention à ne pas confondre le numéro de broche (de 1 à 8 sur le schéma ci-dessous [DATASHEET 2]), et le port correspondant . Par exemple, le port PB3 correspond à la broche n°2.

Un premier montage simple

Dans ce premier billet, nous allons nous contenter de mettre en place le pont diviseur de tension associé à la photorésistance et une résistance de 10 kΩ pour détecter la présence de lumière ou non, et une simple LED (en série avec une résistance de 330 Ω pour limiter le courant) que nous ferons clignoter, et qui nous servira à vérifier que tout se passe comme prévu (une sorte de monitoring rudimentaire). Le pont diviseur de tension sera relié à la broche PB2, qui sera alors configurée en entrée numérique (mesure en tout ou rien), et la LED sera connectée à la broche PB1, qui sera elle configurée en sortie numérique (pour allumer ou éteindre la LED).

Voici donc le schéma du montage qui est mis en place :

Création du projet PlatformIO

Côté logiciel, je vais utiliser l'environnement de développement PlatformIO associé à l'éditeur Visual Studio Code. Il suffit de cliquer sur le bouton « + New Project », de donner un nom au projet, et de choisir la carte : ici « Generic ATtiny85 (Atmel) ».

Une arborescence est alors créée dans le dossier « test_allumage_interruption », contenant entre autre :

  • un fichier platformio.ini contenant les paramètres du projet. Complétons ce fichier par quelques lignes :
[env:attiny85]
platform = atmelavr
board = attiny85
framework = arduino
; on utilise le protocole usbasp pour téléverser les programmes dans le microcontrôleur
upload_protocol = usbasp
; et on fixe sa fréquence d'horloge à 8 MHz
board_build.f_cpu = 8000000L
  • un fichier src/main.cpp contenant un squelette de code source pour le projet. Là, c'est plus radical. On supprime tout et on va repartir d'une page blanche car on ne va pas réellement utiliser le framework arduino, mais directement la bibliothèque avr-libc.
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

#define LDR PB2
#define LED PB1

int main(void)
{

  // boucle principale
  while(1)
  {

  }

  return 0;
}

Quelques fichiers d'en-tête nous sont nécessaires :

  • avr/io.h : qui contient les définitions des registres d'entrée/sortie et les valeurs des bits de ces registres tels que définis dans la documentation du microcontrôleur (par exemple les variables PB1, PB2…)
  • util/delay.h : qui contient des fonctions permettant d'attendre pendant une certaine durée
  • avr/interrupt.h : qui permet de manipuler les interruptions, que nous utiliserons pour réveiller notre capteur lorsque la lumière va s'allumer dans le frigo.

La boucle principale restera vide, et nous ferons en sorte que tous les événements qui se produisent soient gérés uniquement par des interruptions.

Pour simplifier le code, nous définissons également LDR, que nous utiliserons en lieu et place de PB2, et LED qui remplacera également PB1.

Définition des entrées / sorties

Nous allons maintenant indiquer quels ports sont en entrée ou en sortie. Nous allons pour cela utiliser le registre de direction du port B, DDRB [DATASHEET 64] :

Le principe est très simple. Par défaut, toutes les broches sont en entrée (Initial Value 0). Pour placer une broche en sortie, il faut mettre le bit correspondant à 1 dans le registre. Donc pour mettre la broche PB1 en sortie (sur laquelle on connecte la LED), on écrira :

  // LED en sortie
  DDRB |= (1 << LED);

Mise en place des interruptions

Les interruptions sont des mécanismes qui permettent d'interrompre le déroulement normal d'un programme lorsqu'un certain événement se produit, et d'exécuter des instructions spécifiques en fonction de l'événement qui s'est produit [DATASHEET 48].

Dans notre cas, il faudra générer une interruption lorsque la luminosité sur la photorésistance LDR varie significativement.

Il y a deux façons de déclencher des interruptions sur les différentes broches du microcontrôleur. Ces deux façons peuvent d'ailleurs être utilisées simultanément. Elles sont gérées par le registre GIMSK (General Interrupt Mask Register) [DATASHEET 51] :

  • en mettant à 1 le bit n°6 de ce registre (INT0), une interruption sera déclenchée lors d'un changement d'état sur la broche INT0 (qui correspond à PB2)
  • en mettant à 1 le bit n°5 (PCIE), une interruption sera déclenchée lors d'un changement d'état sur n'importe laquelle des broches PCINT0 (broche PB0) à PCINT5 (PB5). Attention, une fois que l'interruption est déclenchée, on ne sait pas quelle est la broche qui en est à l'origine.

Pour notre application, surveiller la broche INT0 devrait suffire si l'on souhaite uniquement détecter un changement de luminosité (le pont diviseur de tension est relié à la broche PB2, donc INT0). En revanche, je voudrais dans la suite que l'on puisse arrêter l'alarme en appuyant sur un bouton poussoir, et que cela se fasse également par une interruption. Il sera donc nécessaire d'utiliser plusieurs broches en entrée pour les interrutions et c'est donc la seconde façon de faire qui sera retenue.

On passe donc à 1 le bit PCIE (Pin Change Interrupt Enable) du registre GIMSK :

  // on active les interruptions par changement d'état des 
  // broches du port B. PCIE = Pin Change Interrupt Enable
  GIMSK |= ( 1 << PCIE );

Mais cela ne suffit pas. En effet, il faut indiquer quelles broches sont surveillées pour les interruptions, grâce au registre PCMKS (Pin Change Mask Register) [DATASHEET 52] :

Ici c'est très simple : on passe à 1 les bits correspondant aux broches que l'on souhaite utiliser pour les interruptions. Dans notre cas, il s'agit de PB2, soit PCINT2, ou encore LDR (toutes ces notations sont équivalentes). Nous nous occuperons plus tard du bouton poussoir. On écrit donc :

  // on surveille les interruptions sur la broche LDR
  PCMSK |= ( 1 << LDR );

Bien, nous avons bien avancé. Il ne reste plus qu'à activer globalement les interruptions grâce à la fonction sei(), qui met à 1 le bit n°7 (bit I) du registre SREG (AVR Status Register). Tous les réglages précédents concernant les interruptions prennent alors effet.

Tout est maintenant en place pour nos interruptions. Il faut maintenant indiquer quel code exécuter lorsqu'une interruption est détectée sur la broche LDR. Ce code est géré par la macro ISR (Interrupt Service Routine), qui prend comme paramètre le vecteur d'interruption. Dans notre cas, le vecteur d'interruption correspondant aux changements d'état sur les broches se nomme PCINT0_VECT [DATASHEET 48]. On écrira donc le code suivant :

// code gérant les interruptions sur les broches
ISR( PCINT0_vect )
{
  // on suspend les interruptions
  cli();

  // petit délai pour éviter les rebonds
  _delay_ms(100);

  // on fait clignoter la LED

    PORTB |= ( 1 << LED);
    _delay_ms(300);
    PORTB &=~ ( 1 << LED);

  // on réactive les interruptions
  sei();
}

Dans ces quelques lignes de code, on commence par suspendre les interruptions avec cli() pour éviter que d'autres interruptions surviennent pendant le traitement de l'interruption. Un petit délai de 100 ms permet également de laisser le temps aux interruptions d'être suspendues (tout n'est pas clair sur ce point, mais sans ce léger délai, deux interruptions successives peuvent se produire).

Ensuite, on fait clignoter la LED en l'allumant pendant 300 ms, puis en l'éteignant. Pour allumer la LED, il suffit de mettre le bit PORTB2 (équivalent à PB2 ou encore LED dans notre cas) du registre PORTB à 1

on termine le code en réactivant les interruptions avec sei().

Avec ce code, la led clignote une fois lorsque la lumière s'allume, ou bien qu'elle s'éteint. Nous sommes donc bien capables de détecter un changement d'état lumineux par le biais d'interruptions.

Dans un prochain article, nous ajouterons la minuterie qui permettra de déclencher l'alarme lorsque la lumière reste allumée trop longtemps, et le bouton poussoir pour suspendre manuellement cette alarme pour une durée déterminée.