ATTiny capteur universel en I2C

Le lun. 05 mars 2012 par Kasey

J’utilise depuis pas mal de temps des ATTinys (précisément des Attiny85) dans mes projets, j'y ai d’ailleurs déjà consacré une page de mon blog http://kasey.fr/article/ATTiny-et-IDE-Arduino. De plus en plus fou des ces microcontrôleurs, je pense qu'il est maintenant temps de partager un peux les connaissances que j'ai acquises sur ces derniers.

En effet, s'il est vrai que le core arduino a été porté sur ces derniers, son utilisation est régie par des règles rappelant le côté hack de la chose Le tout étant bien sur saupoudré d'une documentation bien inexistante. Je laisse donc ceux que ça intéresse lire le reste de l'article, j'y expose un peu de mon expérience capitalisé au travers d'un exemple général, lecture d'une valeur via les ADC de l'attiny et remontée de l'information en I2C, bref la base d'un bon capteur :)

ATTiny85 et I2C

Première chose a savoir, les ATTiny ne disposent pas a proprement parler d'une stack I2C ou TWI, mais intègrent un USI (Universal Serial Interface) qui peut se programmer pour communiquer en I2C. Bien heureusement la communauté a bien généreusement travaillé, et vous pouvez récupérer la stack au complet implémenté en tant que salve et master a cette adresse :

Step 1 : lire une valeur analogique avec les ATTiny85

Bien je reprends mon exemple final issu de mon précédent article :

/*
         +-\/-+
A0 PB5  1|    |8  Vcc
A3 PB3  2|    |7  PB2 D0 A1
A2 PB4  3|    |6  PB1 D1
   GND  4|    |5  PB0 D2
         +----+        
*/

#define IN  3
#define OUT PB1

short lock;

void setup() {
  pinMode(IN,INPUT);
  pinMode(OUT,OUTPUT);
  lock = 5;
}

void loop() {
  if(analogRead(IN) > 30) {
    if(lock <= 10) lock++;
    if(lock >= 10) { digitalWrite(OUT,HIGH); delay(1000);}
  }
  if(analogRead(IN) < 10) {
    if(lock > 0)   lock--;
    if(lock == 0)  { digitalWrite(OUT,LOW); }
  }
}

Notez bien une chose, la pin nommée IN est passée de PB3 a 3, pourquoi ? Et bien en fait la déclaration du pinMode ne respecte pas du tout la même logique que sur un arduino, aussi si vous voulez lire une valeur analogique, déclarer comme numéro de patte de lecture le numéro de l'ADC, soit 3 comme ADC3 ici.

Pour les autres pattes je vous laisse vous reporter au schéma que vous vous pouvez vous faire tatouer :

ATTiny Mapping

Juste pour confirmer, si vous vouliez lire la valeur sur la pin 3 vous utiliseriez le code suivant :

#define IN 2
pinMode(IN,INPUT);
analogRead(IN);

Step 2 : communiquer en I2C entre Slave ATTiny et Master Arduino : ordre simple

Boooooon, ici on va supposer que vous avez correctement installé :

  • vous avez la version 1.0 de l'IDE arduino disponible ici : arduino.cc
  • vous avez la version 1.0 du code arduino disponible là : arduino-tiny
  • vous avez la librairie TinyWireS a minima

On va commencer par le code simple côté Arduino :

#include <Wire.h>

#define SLAVE_ADDR 0x26

void setup(){
  Wire.begin();                            // on rejoint le bus en tant que maitre
  Serial.begin(38400);                     // petit debug via le port série
  Serial.println("I2C Test Application");  // hop on affiche le start (pratique sur les reset)
}

void loop(){
  Serial.println("Envoi commande");        // on log la commande envoyée

  Wire.beginTransmission(SLAVE_ADDR);      // on déclare l'adresse de l'esclave qui va recevoir le code
  Wire.write(0xDE);                        // on envoie la donnée à proprement parler
  Wire.endTransmission();                  // on est content on arrête la transmission

  delayMicroseconds(1000);                 // on glande une seconde
}

Ok, je pense que le code est suffisamment clair tel qu'il est commenté.

On passe maintenant a la partie ATTiny85 en I2C slave :

#include "TinyWireS.h"                  // on inclus la lib I2C Slave pour ATTiny

#define I2C_SLAVE_ADDR  0x26            // On note l'adresse I2C
#define LED_PIN         PB3             // On déclare l'adresse de la LED de debug sur la pin 2

byte byteRcvd;

void setup(){
  pinMode(LED_PIN,OUTPUT);              // on n'oublie pas de passer la pin relié a la LED en sortie
  TinyWireS.begin(I2C_SLAVE_ADDR);      // on rejoint le bus avec une adresse d'esclave (similaire a la lib wire)
  byteRcvd = 0;
}

void loop(){
  if (TinyWireS.available()){           // si on revoit quelque chose sur le bus I2C
    byteRcvd = TinyWireS.receive();     // on l'enregistre
    Blink(LED1_PIN);                    // on blink un coup pour montrer que l'on est content
  }
}

void Blink(byte led) {                  // blinker du pauvre :)
 digitalWrite(led,HIGH);
 delay(200);
 digitalWrite(led,LOW);
}

La vous devriez être en tain de vouloir flasher votre ATTiny et vous devez vous demander, hum, mais quel profil dois-je utiliser. Sans suspense, utilisez le profil "ATTiny85 @ 1Mhz" il s'agit des paramètres de fusibles pour lesquels la librairie a été conçue.

Step 3 : communiquer en I2C entre Slave ATTiny et Master Arduino : ordre et réponse d'une moyenne en float

Bon la je saute une étape qui est de répondre un byte, pour passer directement au truc vraiment intéressant, la réponse d'un flottant réalisé sur une moyenne de mesures. Ben oui, en règle générale, si on utilise un microcontrôleur c'est pour faire un peu plus qu'un ADC I2C, sinon on utilise un circuit fait pour ça :).

On va commencer par le code coté ATTiny, car de lui découle un peu le code côté master, je laisse les commentaires en anglais, car je les trouve plus explicites :

/*
    ATTiny85 slave, sample a sensor and send a float value to Arduino UNO master
    Copyright (C) <2012>  <Lucas Fernandez>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

                  +-\/-+
        (RESET)  1|    |8  VCC (2.7-5.5V)
      Temp Read  2|    |7  I2C   SCK -> Uno A5
          DEBUG  3|    |6  (PB1) NA
            GND  4|    |5  I2C   SDA -> Uno A4
                  +----+ 
*/

#include "TinyWireS.h"      // ATTiny wire lib
#include "Statistic.h"      // Arduino Statistic lib

#define I2C_SLAVE_ADDR 0x27 // I2C slave address

#define TEMP_READ        3  // Temperature read on pin 2 (ADC3)
//#define DEBUG          PB4  // Temperature read on pin 2 (ADC3)

#define DELAY_ADC       10  // time before re-read ADC 10 us

Statistic temp;
float     result;

byte*     floatPtr;
byte      byteRcvd;
byte      i;

void setup() {
  pinMode(TEMP_READ,INPUT);
//pinMode(DEBUG,OUTPUT);

  TinyWireS.begin(I2C_SLAVE_ADDR);

  temp.clear();
  result  = 0;
  byteRcvd= 0;
  i       = 0;
}

void loop() {
    if (TinyWireS.available()){          // if we get an I2C message
    byteRcvd = TinyWireS.receive();      // do nothing with the message

    for(i=0;i<50;i++) {                  // make 50 temperature sample
      temp.add(analogRead(TEMP_READ));   // read and store temperature sensor output
      delayMicroseconds(DELAY_ADC);      // wait 10 us to stabilise ADC
//    digitalWrite(DEBUG,HIGH);
//    digitalWrite(DEBUG,LOW);
    }

    result = ((temp.average()*0.0049)-0.5)*100 ; // convert the result into a humain readable output

    if(result < 0) result = 0;           // avoid eratic datas
    temp.clear();                        // clear statistics to avoid leack and data stacking

    floatPtr = (byte*) &result;
                TinyWireS.send( *floatPtr );  // send first byte
    ++floatPtr; TinyWireS.send( *floatPtr );  // the second
    ++floatPtr; TinyWireS.send( *floatPtr );  // third
    ++floatPtr; TinyWireS.send( *floatPtr );  // fourth and final byte
  }
}

Note 1 : ce code nécessite d'installer la librairie Statistic disponible ici : arduino.cc, petit point intéressant, cette librairie propose de calculer l'écart type sur la mesure. C'est super pour avoir une image de la précision de la mesure :)

Note 2 : ce code fonctionne actuellement dans un ATTiny qui prend à intervalle régulier des mesures de température, ceci explique la licence GPLv3.

Le côté master :

/*
    UNO Master I2C, read a float value from ATTiny85 slave
    Copyright (C) <2012>  <Lucas Fernandez>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <Wire.h>

#define I2C_SLAVE_ADDR_TEMP   0x27         // I2C Temperature slave address

#define DELAY_TEMP_READ   140              // wait 140 ms before ask temp value
#define DELAY_NEW_SAMPLE 1000              // time before new sample (1s)

float tempValue;
byte* floatPtr;

void setup(){
  Wire.begin();                            // start I2C Bus as Master
  Serial.begin(38400);                     // start serial to send dust output
  tempValue = 0;
}

void loop(){
  tempValue = 0;

  Wire.beginTransmission(I2C_SLAVE_ADDR_TEMP);  // transmit to slave device
  Wire.write(0xFF);                             // sends usuless data 
  Wire.endTransmission();                       // stop transmitting

  delay(DELAY_TEMP_READ);

  Wire.requestFrom(I2C_SLAVE_ADDR_TEMP, 4);     // request 4 byte from slave (float = 4 bytes)

  if (Wire.available()) {
    floatPtr = (byte*) &tempValue;
    (*floatPtr) = Wire.read(); ++floatPtr;
    (*floatPtr) = Wire.read(); ++floatPtr;
    (*floatPtr) = Wire.read(); ++floatPtr;
    (*floatPtr) = Wire.read();
    Serial.print("Temp = ");      // print out slave byte to Serial monitor
    Serial.print(tempValue,DEC);  // print out slave byte to Serial monitor
    Serial.println(" C");         // print out slave byte to Serial monitor
  } else {
    Serial.println("NaN");  
  }

  delay(DELAY_NEW_SAMPLE);
}

Quand ça fonctionne avec deux ATTinys et un Arduino sur le même bus :

image

Debug

Les pins à utiliser

Je l'ai noté dans un de mes codes, mais je vous le redonne textuellement. Pour vous relier sur le bus I2C, tirez les entrées analogique A4 et A5 d'un arduino a 5v avec des résistances de 4,7 ou 10 k. Connectez ensuite les pin 5 de vos ATTinys a la pin A4 de votre arduino master et la pin 7 sur la pin A5.

Le bus fonctionne bizarrement

Alors il y a une chose à savoir avec le bus I2C et la stack arduino, elle fonctionne par pile. Il faut donc quand on envoie une réponse a un master depuis un slave, remplir la stack du slave puis lire cette valeur via l'arduino Master. Si le master demande un résultat avant qu'il soit présent dans la stack du slave, un blocage peut se produire jusqu'au timeout du master. En cas de soucis de ce type, commencez par un reset du slave, puis du master. Inversez si ça ne fonctionne pas mieux. De manière générale, essayez de garder des timmings de pulling cool, pour vous prémunir de se genre de chose. Personnellement, je garde dans les 200 ms de marge.

Conclusion

Voilà, vous savez maintenant un peu plus de choses sur les ATTiny mis en oeuvre en I2C au travers du core Arduino. J'ai mis pas mal de temps à comprendre toutes les finesses exposées ici. J'espère que cet article vous fera gagner le temps que j'ai passé à rechercher toute ces informations.