Réaliser un shield Arduino Uno avec KiCad

Dans ce tutoriel nous allons créer un shield (carte d’extension qui se branche sans soudure) pour Arduino Uno qui permet de tester l’état de vos piles ou batteries et d’en mesurer leur capacité. Après avoir validé le prototype du circuit sur un Protoshield Arduino, nous réaliserons un PCB (Printed Cirtcuit Board = circuit imprimé) du Shield avec le logiciel KiCad.

Ce tutoriel utilise les concepts expliqués dans les autres tutoriels ‘Testeur de piles alcalines‘ et ‘Mesurer la capacité d’une pile rechargeable Ni-MH, mais l’ajout de quelques fonctionnalités permet de tester tout type de pile ou batterie dont la tension est inférieure à 6,25 V.

Vous pourrez par exemple vérifier la capacité réelle de votre batterie externe utilisée pour recharger votre smartphone ou encore vérifier l’état de vos piles alcalines ou de vos piles rechargeables Ni-MH.

L’objectif est d’avoir un montage autonome. Grâce à une afficheur OLED il ne sera plus nécessaire de relier l’Arduino Uno à un ordinateur pour lire le résultat des mesures.

Voici le schéma du Shield, composé de 5 blocs fonctionnels :

schema shield arduino uno kicad - final

Bloc 1 : tension de référence

L’objectif est d’avoir une mesure précise de la tension de la pile. Pour cela la tension de référence du convertisseur analogique/numérique doit être la plus précise possible.

En fonction de la façon dont vous alimentez l’Arduino (via le 5 V du port USB par exemple), le tension de référence peut ne pas être précise. C’est pourquoi l’utilisation d’une tension de référence externe est privilégiée dans ce tutoriel.

La tension de référence externe est générée par un régulateur de tension TL431. Cette tension de 2,5 V est reliée à l’entrée AREF de l’Arduino.

Attention : L’Arduino Uno possède un condensateur de 100 nF entre AREF et GND, ce qui rend le TL431 totalement instable. Ce comportement du TL431 est parfaitement décrit dans cette note de TI. Avec un Arduino Uno, il faut donc ajouter un condensateur de 10 μF entre la sortie VREF du composant et la masse.

Bloc 2 : bouton de configuration

Un switch permet d’interagir avec le programme afin de sélectionner les différentes options :

  • Type de pile ou de batterie : Alcaline, Ni-MH, Li-Ion ou Li-Po
  • Type de mesure à effectuer : mesure de la tension , mesure de la capacité

Ce switch est relié à la broche D2 de l’Arduino et utilise la résistance de rappel « pull-up » interne à l’Arduino.

Bloc 3 : transistor de contrôle du type de mesure

La sortie D7 de l’Arduino contrôle l’état (saturé ou bloqué) du transistor 2N2222. Il est ainsi possible de choisir de mesurer la tension de la pile à vide ou en charge lorsqu’elle débite du courant. Voir le tutoriel « Testeur de piles alcalines » pour plus de détails sur cette fonctionnalité.

Bloc 4 : afficheur OLED

Un petit afficheur OLED est utilisé comme interface graphique avec l’utilisateur. Il permet de configurer le type de mesure à réaliser (sélection par le bouton) et de visualiser le résultat des mesures.

L’afficheur et de relié à l’Arduino par un bus I2C via ses broches SDA et SCL. Attention à ne pas oublier les résistances pull-up de 4,7 kΩ sur les deux fils SDA et SCL de ce bus.

J’ai utilisé la librairie https://github.com/greiman/SSD1306Ascii car elle permet de bien géré mon afficheur OLED 64×128 en I2C sans consommer trop de mémoire comme les librairies Sparkfun ou Adafruit.

Bloc 5 : module de mesure des tensions

Deux ponts diviseur de tension constitués chacun de 2 résistance de 200 kΩ et 300 kΩ divisent les tensions mesurées aux bornes de la résistance de décharge. Voir l’article « A quoi ça sert… un pont diviseur de tension« . Ce pont permet de mesurer des piles ou batteries jusqu’à 6,25 V tout en limitant la tension des entrées A0 et A1 de l’Arduino à 2,5 V. Il ne faut en effet pas dépasser le tension de référence sur les entrées du convertisseur analogique/numérique (2,5 V).

Une amélioration pourrait être de rajouter un jumper pour mesurer AREF au travers des ponts diviseurs afin de calibrer par logiciel (une fois pour toutes) le rapport du pont diviseur de tension. En effet les résistances ont une certaines précision (5% par exemple) et ne valent donc pas exactement 200 kΩ et 300 kΩ. Comme AREF a une précision de 1%, une mesure de AREF au travers du pont permettra de gagner en précision sur les mesures de la tension des piles.

Mesure de la tension de la pile à vide et en charge

Lorsque la sortie D7 de l’Arduino est à LOW, le transistor du bloc 3 est dans son état bloqué. Aucun courant ne circule dans la résistance de décharge (R17). La pile ne débite pas de courant et c’est bien sa tension à vide qui est mesurée à ses bornes.

Lorsque la sortie D7 de l’Arduino est à HIGH, le transistor du bloc 3 est dans son état saturé et le pile débite du courant au travers de la résistance de décharge de 5 Ω (R17). On mesure ainsi sa tension en charge, très importante pour évaluer l’état de la pile.

Voir tutoriel ‘Testeur de piles alcalines‘.

Mesure de la capacité de la pile

On mesure les tensions aux 2 bornes de la résistance afin de pouvoir en déduire le courant qui y circule. Cela permet de calculer la quantité de courant que le pile débite et ainsi de pouvoir mesurer sa capacité.
La décharge de la batterie dans la résistance de 5 Ω va faire chauffer cette résistance, surtout avec une batterie de 5V. Dans ce tutoriel j’utilise une résistance de 10 W, qui encaissera bien la puissance générée par la batterie et qui ne deviendra pas trop chaude. Ne pas utiliser une résistance 1/4 W dans tous les cas afin d’éviter tout risque de brûlure.

Voir le tutoriel ‘Mesurer la capacité d’une pile rechargeable Ni-MH’.

Validation du circuit sur un Protoshield Arduino

Protoshield Arduino
Protoshield Arduino utilisé pour le prototypage

Avant de concevoir le PCB avec KiCad, il est utile de vérifier le bon fonctionnement du circuit. Une carte de type Protoshield Arduino s’avère très intéressante pour valider notre prototype de shield.

Par contre j’ai dessoudé les connecteurs car je ne voulais pas utiliser de câbles Dupont mais souder les composants et les fils sur la carte.

La résistance de 10 W est trop grosse pour tenir sur le protoshield, j’ai donc utilisé une résistance de 27 Ω de puissance inférieure (2,5 W) que j’avais en stocke. Avec une résistance de 27 Ω la décharge complète de la pile prend beaucoup plus de temps car l’intensité débitée est plus faible (I = U / R = 1,2 / 27 = 44 mA).

prototype sur carte protoshield

Réalisation du PCB avec KiCad

J’ai choisi le logiciel KiCad pour la réalisation du PCB. KiCad est en effet le logiciel de conception de circuit électronique open source le plus complet à ce jour.

Vous trouvez ci-dessous le projet KiCad en téléchargement depuis mon GitHub.

1/ Conception du schéma

La première étape est la création du schéma du shield. Il faut utiliser le template « Arduino as Uno – Expansion », cela permet d’éviter d’avoir à créer les connecteurs du shield ainsi que la forme du PCB. Ensuite il suffit de dessiner les différents blocs du montage comme sur l’image ci-dessous.

schema shield arduino uno kicad - final

2/ Conception du circuit intégré (PCB)

La seconde étape est la création du PCB lui-même à partir des empreintes des composants utilisés. C’est une phase qui nécessite un peu d’habitude mais qui révélera votre côté « créatif » 😉 .

pcb du shield

Parce qu’une vidéo vaut mieux qu’un long discours, voici la réalisation du premier prototype sous KiCad (attention il comportait quelques erreurs corrigées depuis)…

https://youtu.be/B6VE6SSA2wk

Après avoir réalisé le premier prototype, voici les changements qui ont été nécessaires :

  • Pull-up pour le bouton à la place du pull-down pour éviter l’utilisation de la résistance externe R2 en utilisant la résistance interne de l’Arduino configurée en pull-up. Un petit condensateur de 10 nF sur le bouton pour éviter les rebonds de façon matérielle et non plus logicielle.
  • Ajout du condensateur de 10 µF sur AREF pour stabiliser la tension de référence TL431.
  • L’emplacement de l’écran pose un léger problème car les soudures au dos de la carte sont trop proches du connecteur USB de l’Arduino Uno, il faudra donc changer la position de l’écran sur le PCB.

Voilà, je vous souhaite de vous amuser autant que moi sur la partie placement et routage dans KiCad, c’est un peu fastidieux au début mais je trouve que c’est vraiment sympa d’avoir la liberté de créer sa propre carte. 😉

Le programme de l’Arduino

// Mesure de la tension et/ou de la capacité d'un accumulteur
// https://tutoduino.fr/
// Copyleft 2020

// Librairie pour l'afficheur OLED
// https://github.com/greiman/SSD1306Ascii
#include "SSD1306Ascii.h"
#include "SSD1306AsciiAvrI2c.h"

// Adresse de l'afficheur OLED sur le bus I2C
#define I2C_ADDRESS 0x3C

// Declaration de l'ecran OLED
SSD1306AsciiAvrI2c oled;

// Boolean qui indique que la configuration 
// par le menu est terminee
bool configOk = false;

// Le bouton est relie a la broche 2 de l'Arduino Uno
#define brocheBouton 2

// Le bouton est conçu en mode pull-up, l'appel a la fonction
// digitalRead retourne la valeur 0 lorsque le bouton est appuye. 
#define BOUTON_APPUYE 0

// Declaration de type enum pour le type de mesure et d'accumulateur
enum mesureType_t: uint8_t {
    MESURE_TENSION = 0, 
    MESURE_CAPACITE = 1
};
enum accuType_t: uint8_t {
    ALCA = 0,
    NIMH = 1, 
    LIION = 2
};

// Variables globales au programme 
mesureType_t  mesure; // Type de mesure à effectuer (tension ou capacite)
accuType_t    accu;   // Type d'accumulateur à mesurer (Alcaline, Li-Ion, Ni-MH,...)

// Constantes de calibration 
// -------------------------
// Valeur de la resistance de decharge
#define R 5
// Valeur de la tension de reference
#define VREF 2.5
// Rapport pont diviseur utilise sur A0
#define PDIV0 2.49
// Rapport pont diviseur utilise sur A1
#define PDIV1 2.49

// Caracteristiques des accumulateurs
// 
// Pile alcaline
// 
// Tension nominale = 1,5 V
// Etats :
//  Bon si tension en charge > 1,3 V
//  Faible si 1,30 V > tension en charge > 1.00 V
//  A changer si tension en charge < 1.00 V 
// Pas de seuil bas car ce n'est pas une batterie rechargeable
#define SEUIL_BON_TENSION_ACCU_ALCA     1.30
#define SEUIL_FAIBLE_TENSION_ACCU_ALCA  1.00
#define SEUIL_BAS_TENSION_ACCU_ALCA     0.00
//
// Batterie Ni-MH
//
// Tension nominale = 1,2 V
// Etats :
//  Bon si tension en charge > 1,15 V
//  Faible si 1,15 V > tension en charge > 1.00 V
//  A changer si tension en charge < 1.00 V 
// Seuil bas 0.80 V
#define SEUIL_BON_TENSION_ACCU_NIMH     1.15
#define SEUIL_FAIBLE_TENSION_ACCU_NIMH  1.00
#define SEUIL_BAS_TENSION_ACCU_NIMH     0.80
//
// Batterie Li-Ion
//
// Tension nominale = 3,7 V
// Etats :
//  Bon si tension en charge > 3,7 V
//  Faible si 3,70 V > tension en charge > 3.00 V
//  A changer si tension en charge < 3.00 V 
// Seuil bas 2.00 V
#define SEUIL_BON_TENSION_ACCU_LIION    3.70
#define SEUIL_FAIBLE_TENSION_ACCU_LIION 3.00
#define SEUIL_BAS_TENSION_ACCU_LIION    2.00

// Détection de seuil bas de la tension de l'accumulateur
bool seuilBasAccuAtteint = false;
float seuilBasAccu = 0;

// Heure de début de la mesure
unsigned long initTime;

// Quantité d'électricité générée par l'accumulateur
// lors de la dernière seconde
float quantiteElectricite = 0;

// Quantité d'électricité totale générée par l'accumulateur 
// depuis le lancement du programme
float quantiteElectriciteTotale = 0;


// Verifie si le bouton est appuye
// pendant le temps d'attente specifie
// Le temps d'attente est un multiple de 
// 50 ms
bool appuiBouton() {
  uint8_t cpt=40;
  
  while (cpt != 0) {
    // verifie si le bouton est appuyé pendant 
    // au moins 50 millisecondes afin d'éviter
    // un etat instable
    if (digitalRead(brocheBouton) == BOUTON_APPUYE) {
      delay(50);
      if (digitalRead(brocheBouton) == BOUTON_APPUYE) {
        // le bouton a bien ete appuye pendant 
        // le temps d'attente
        // on attend que le bouton soit relache
        while (digitalRead(brocheBouton) == BOUTON_APPUYE) {
          delay(100);
        }
        return true;
      }
    }
    // si le bouton n'a pas ete appuye
    // attend 50 ms et teste le bouton
    // de nouveau tant que le temps total
    // d'attente n'est pas ecoule
    delay(50);
    cpt = cpt - 1;  
  }

  // le bouton n'a pas ete appuye pendant
  // le temps d'attente
  return false;
}

bool menuTypeMesure() {
  oled.clear();
  oled.set2X();
  oled.println("Config");
  oled.set1X();
  oled.println("");
  oled.println("Mesure tension ?");
  oled.println();
  oled.println("oui -> Appuyez");  
  if (appuiBouton() == true) {
    mesure = MESURE_TENSION;
    return true;
  }

  oled.clear();
  oled.set2X();
  oled.println("Config");
  oled.set1X();
  oled.println("");  
  oled.println("Mesure capacite ?");
  oled.println();
  oled.println("oui -> Appuyez");  
  if (appuiBouton() == true) {
    mesure = MESURE_CAPACITE;
    return true;
  }
  
  return false;

}

bool menuAccu() {

  oled.clear();
  oled.set2X();
  oled.println("Config");
  oled.set1X();
  oled.println("");  
  oled.println("Pile Alcaline ?");
  oled.println();
  oled.println("oui -> Appuyez");   
  if (appuiBouton() == true) {
    accu = ALCA;
    seuilBasAccu = SEUIL_BAS_TENSION_ACCU_ALCA;    
    return true;
  }
  
  oled.clear();
  oled.set2X();  
  oled.println("Config");
  oled.set1X();
  oled.println("");  
  oled.println("Accu Ni-MH ?");
  oled.println();
  oled.println("oui -> Appuyez");   
  if (appuiBouton() == true) {
    accu = NIMH;
    seuilBasAccu = SEUIL_BAS_TENSION_ACCU_NIMH;    
    return true;
  }

  oled.clear();
  oled.set2X();
  oled.println("Config");
  oled.set1X();
  oled.println("");  
  oled.println("Accu Li-Ion ?");
  oled.println();
  oled.println("oui -> Appuyez");   
  if (appuiBouton() == true) {
    accu = LIION;
    seuilBasAccu = SEUIL_BAS_TENSION_ACCU_LIION;    
    return true;
  }
  
  return false;

}


void configMenu() {

  // reset global variables
  seuilBasAccuAtteint = false;
  quantiteElectriciteTotale = 0;

  while (menuTypeMesure() != true) {
    delay(100);
  } 

  while (menuAccu() != true) {
    delay(100);
  } 
 
  oled.clear();
  oled.set2X();
  oled.println("Validez !");
  oled.set1X();
  if (mesure == MESURE_TENSION) {
    oled.println("Mesure tension");
  } else {
    oled.println("Mesure capacite");
    // La pile doit debiter du courant
    digitalWrite(7, HIGH);
  }
 switch (accu) {
   case NIMH:
      oled.println("Ni-MH");
      break;  
   case LIION:
      oled.println("Li-Ion");
      break;  
   case ALCA:
      oled.println("Alcaline");
      break;        
    default:
      break;
   }   

  oled.println("");
  oled.println("ok -> Appuyez");  

  // attente de l'appui sur le bouton
  // pour pouvoir lancer la mesure
  while (appuiBouton() == false) {
  }
  
  // La configuration est finie
  configOk = true;
}


// Fonction qui doit être appellée toutes les secondes
// La fonction mesure la tension aux bornes de la résistance
// et en déduit le courant qui y circule. 
// Elle mesure la capacité de la pile en additionnant
// le courant de décharges toutes les secondes jusqu'à ce
// que la pile soit totalement déchargée
void mesureQuantiteElectricite() {
  float U0,U1;
  float tensionResistance;
  float courant;
  
  // Lit la tension aux bornes de la pile et aux bornes 
  // de la résistance
  U0 = PDIV0*analogRead(A0)*VREF/1023.0;
  U1 = PDIV1*analogRead(A1)*VREF/1023.0;
  
  tensionResistance = U0-U1;
  Serial.println("tensionResistance="+String(tensionResistance));
  
  // Verifie que la tension de la pile n'est
  // pas inferieure à son seuil bas afin de ne pas 
  // l'endommager
  if (U0 < seuilBasAccu) {
    seuilBasAccuAtteint = true;
  }

  // Calcul du courant qui circule dans la résistance
  courant = (U0-U1)/R;

  // La quantité d'électricité est la quantité de charges électriques
  // déplacées par les électrons. Elle est égale à l'intensité
  // multipliée par le temps (1 seconde ici).
  // Afin d'avoir la quantité d'électricité en mAh, il faut
  // utiliser le facteur 1000/3600
  quantiteElectricite = courant/3.6;

  // On additionne cette quantité d'électricité aux précédentes
  // afin de connaître la quantité totale d'électricité
  // à la fin de la mesure
  quantiteElectriciteTotale = quantiteElectriciteTotale+quantiteElectricite;

  // Affichage sur l'afficheur OLED de la tension de la pile, 
  // du courant débité ainsi que de la quantité d'électricité débitée 
  // par la pile depuis le lancement du programme
  oled.clear();
  oled.set2X();
  oled.println("Capacite");
  oled.set1X();
  oled.println("U = "+String(U0,3)+" V");
  oled.println("I = "+String(courant*1000,0)+" mA");
  oled.println("Q = "+String(quantiteElectriciteTotale,3)+" mAh");

  delay(1000);
 
}

void mesureCapacite() {
  // Mesure de la quantité d'électricité débitée toutes les 
  // secondes jusqu'à ce que la tension de l'accu soit inférieure
  // à son seuil bas afin de ne pas l'endommager.
  if (seuilBasAccuAtteint == false) {
    mesureQuantiteElectricite();
  } else {
    // Une fois que le seuil bas de l'accu est atteint, 
    // on positionne le transistor en mode bloqué afin
    // d'arrêter la décharge de l'accu et on affiche
    // sa capacite
    digitalWrite(7, LOW);
    oled.println("Mesure terminee");
    oled.println("");
    oled.println("reset -> Appuyez");  
    
    // attente de l'appui sur le bouton
    // pour pouvoir relancer le programme
    while (appuiBouton() == false) {
    }
    configOk = false;
    }
}

String etatAccu(accuType_t typeAccu, float tensionCharge) {
  
 switch (typeAccu) {
   case NIMH:
      if (tensionCharge > SEUIL_BON_TENSION_ACCU_NIMH) {
        return("Accu ok");
      } else if (tensionCharge > SEUIL_FAIBLE_TENSION_ACCU_NIMH) {
        return("Accu faible");
      } else {
        return("Accu a recharger");
      }        
      break;  
   case LIION:
      if (tensionCharge > SEUIL_BON_TENSION_ACCU_LIION) {
        return("Accu ok");
      } else if (tensionCharge > SEUIL_FAIBLE_TENSION_ACCU_LIION) {
        return("Accu faible");
      } else {
        return("Accu a recharger");
      }        
      break;    
   case ALCA:
      if (tensionCharge > SEUIL_BON_TENSION_ACCU_ALCA) {
        return("Pile ok");
      } else if (tensionCharge > SEUIL_FAIBLE_TENSION_ACCU_ALCA) {
        return("Pile faible");
      } else {
        return("Pile a remplacer");
      }  
      break;        
    default:
      break;
   }   
}

void mesureTension() {
  float tensionVide;
  float tensionCharge;

  oled.println("Mesure en cours...");

  // Lecture de la tension à vide de la pile
  // Bloque le transistor en positionnant la sortie D7 à LOW
  digitalWrite(7, LOW);
  delay(500);
  
  tensionVide = (PDIV0*analogRead(A0)*VREF)/1023.0;
  Serial.println("tensionVide="+String(tensionVide));
  delay(500);

  // Lecture de la tension en charge de la pile
  // Sature le transistor en positionnant la sortie D7 à HIGH
  digitalWrite(7, HIGH);
  delay(500);

  tensionCharge = (PDIV0*analogRead(A0)*VREF)/1023.0;
  Serial.println("tensionCharge="+String(tensionCharge));
  delay(500);

  // Arrete la decharge de la pile
  digitalWrite(7, LOW);

  oled.clear();
  oled.set2X();
  oled.println("Tension");
  oled.set1X();  
  oled.println("U_vide = "+String(tensionVide)+" V");
  oled.println("U_charge = "+String(tensionCharge)+" V");
  oled.println(etatAccu(accu, tensionCharge));
  oled.println("");
  oled.println("reset -> Appuyez");  
  
  // attente de l'appui sur le bouton
  // pour pouvoir relancer le programme
  while (appuiBouton() == false) {
  }
  configOk = false;
}

void setup() {
  
  Serial.begin(9600);

  oled.begin(&amp;Adafruit128x64, I2C_ADDRESS);
  oled.setFont(Adafruit5x7);  
  oled.clear();
  oled.set2X();
  oled.println("Tutoduino");
  oled.set1X();
  oled.println("Apprendre");
  oled.println("l'electronique");
  oled.println("avec un Arduino");
  oled.println("** Shield 5 Ohm **");

  delay(2000);
  
  // Positionne la référence de tension 
  // sur la référence externe à 2,5 V
  analogReference(EXTERNAL);
  
  pinMode(brocheBouton, INPUT_PULLUP);

  // La sortie D7 sera utilisé pour contrôler 
  // l'état du transistor
  pinMode(7, OUTPUT);
  delay(100);

  // Passe le transistor dans son état bloqué
  // afin de ne pas décharger l'accumulateur
  digitalWrite(7, LOW);  

}
void loop() {

  // Affiche le menu de configuration lors de la première 
  // boucle (une fois la configuration terminee le flag
  // configOk passe à true 
  if (configOk == false) {
    configMenu();
  }
  
  if (mesure == MESURE_CAPACITE) {
    mesureCapacite();
  } else {
    mesureTension();
  }

}