Réaliser une carte avec KiCad pour mesurer la capacité de vos accus NiMH AA/AAA et Li-Ion 18650

Ce tutoriel reprend les principes détaillés dans mon tutoriel « Mesurer la capacité d’une pile Ni-MH », mais avec deux modifications principales :

  • Design d’une carte autonome basée sur le micro-contrôleur ATmega328P (plus besoin d’Arduino Uno)
  • Décharge de l’accumulateur à courant constant, et possibilité de choisir la valeur de ce courant

Schéma du montage

schéma carte testeur accus 18650 KiCad
Schéma du montage de mesure de capacité d’accus

Description du schéma

Le micro-contrôleur ATmega328P contrôle le circuit de décharge de l’accu et mesure la tension aux bornes de cet accu. L’utilisateur interagit avec le montage au travers de 3 boutons poussoir et d’un écran OLED. Le montage est alimenté en 5 V via un connecteur micro USB. Un connecteur USB / UART permet de téléverser le logiciel sur le micro avec un programmateur FTDI.

Décharge à courant constant

Dans mon précédent tutoriel, la pile était déchargée sans contrôle du courant de décharge. Le courant étant simplement dépendant de la tension de l’accumulateur. Or nous savons que la tension d’un accu diminue lorsqu’il se décharge. Par exemple pour une pile Ni-MH, la tension est égale à environ 1,4 V lorsqu’elle vient d’être rechargée, et elle diminue au cours de la décharge. On arrête la décharge lorsque sa tension de seuil de 0,8 V est atteinte afin de ne pas l’endommager. Si la pile se décharge dans une résistance de 10 Ω, le courant de décharge sera donc de 140 mA au début de la décharge pour finir à 80 mA en fin de décharge.

Dans ce montage, l’objectif est donc d’avoir une décharge de l’accu à courant constant, et de pouvoir choisir la valeur de ce courant de décharge. Dans le schéma, la gestion du courant de décharge est effectuée au travers de 3 blocs fonctionnels.

Premier bloc – Convertisseur numérique-analogique

Le premier bloc est un convertisseur numérique-analogique (CNA) composé de sorties numériques du micro-contrôleur, d’un réseau R-2R et d’un ampli-op. Les sorties PD3 et PD4 du micro-contrôleur contrôlent les deux étages du réseau R-2R. Nous avons ainsi un CNA ayant 4 tensions différentes en sortie : 0 V, 1.3 V, 2.5 V et 3.7 V. L’ampli-op suiveur agit comme un étage tampon de tension et permet d’éviter que le bloc suivant perturbe le réseau R-2R.

Second bloc – Pont diviseur de tension

Le second bloc est un pont diviseur qui permet d’ajuster la tension de sortie du CNA à l’entrée du bloc suivant afin de régler (une fois pour toutes) les différentes valeurs possibles pour le courant de décharge. Les valeurs du pont diviseur sont choisies afin d’avoir un courant constant sur toute la plage de valeurs de tensions de l’accu (entre 0,8 V pour une pile Ni-MH déchargée et 4,2 V pour une batterie Li-Ion chargée).

Troisième bloc – Gestion du courant de décharge constant

Le dernier bloc est composé d’un ampli-op, d’un transistor bipolaire et d’une résistance de 1 Ω. L’objectif de ce bloc est d’assurer que la tension aux bornes de la résistance soit égale à la tension d’entrée de ce bloc. Lorsque la tension aux bornes de l’accu diminue (puisque il se décharge), l’ampli-op agit alors sur le transistor dont la tension VCE augmente et compense la baisse de tension de l’accu. Ainsi la tension aux bornes de la résistance ne varie pas et reste bien égale à la tension d’entrée du bloc.

L’accu se décharge donc bien à courant constant dans cette résistance. Le courant de décharge est proportionnel à la tension d’entrée de ce bloc, qui est contrôlée par les deux blocs précédents.

Calcul du courant de décharge en fonction de l’état des sorties du micro-contrôleur

Notre réseau R-2R est composé de 2 étages et une tension d’entrée de 5 V, il offre donc une résolution de ΔV = 5 x 1 / 2² = 1.25 V (voir mon article sur les réseau R-2R).

En sortie du réseau R-2R, nous avons donc 4 tensions possibles : 0 V, 1.25V, 2.5 V et 3.75 V. La tension d’alimentation de l’ampli-op est de 5 V et sa sortie est limitée à 3.5 V, donc la tension de 3.75 V de sortie du réseau R-R2 sera réduite à 3.5 V en entrée du pont diviseur.

Le pont diviseur 68k-10k va diviser la tension avec le rapport 10 / (10 + 68). Les tensions de sortie du pont diviseur et d’entrée du troisième bloc seront donc 0 V, 0.160 V, 0.320 V et 0.448 V.

Le troisième bloc assure que la tension aux bornes de la résistance de 1 Ω sera égale à la tension d’entrée du bloc. En appliquant la loi d’Ohm (I = U / R = U / 1 Ω), nous pouvons calculer le courant que l’accu va générer dans cette résistance, soit par exemple 320 mA pour une tension d’entrée du troisième bloc de 0.320 V.

Le tableau ci-dessous qui reprend les différents courants de décharge en fonction de l’état des sorties du micro-contrôleur. Nous voyons que nous pouvons décharger nos accus avec un courant de décharge de 160 mA, 320 mA ou 448 mA. Un courant de 0 mA permet de mesurer la tension à vide de l’accumulateur, alors qu’il ne génère aucun courant.

PD3PD4Courant de décharge
000 mA
10160 mA
01320 mA
11448 mA
Courant de décharge en fonction des sorties numériques de l’ATmega328P

Enfin un dernier pont diviseur permet d’assurer que la tension mesurée aux bornes de l’accu soit bien convertie en une tension inférieure à la référence de tension du convertisseur analogique-numérique du micro-contrôleur. La référence de tension interne de 1.1 V est utilisé dans ce montage.

Projet KiCad

Les fichiers du projet KiCad sont disponibles sur mon GitHub.

PCB KiCad d'une carte qui mesure la capacité des accus NiMH AA/AAA et Li-Ion 18650
PCB de la carte réalisé sous KiCad

Simulation du circuit sur falstad

Simulation du circuit de mesure de capacité des accus NiMH et Li-Ion 16850 avec Falstad

Lien vers la simulation du circuit.

Programme de l’ATmega328P

Le code du projet développé avec l’IDE Arduino est disponible sur mon GitHub.

// 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"
#include <PinChangeInt.h>

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

// Declaration de l'ecran OLED
SSD1306AsciiAvrI2c oled;

// variables utilisees pour les routines
// d'interruptions pour la gestion des boutons
unsigned long last_interrupted = 0;
volatile bool bouton_haut_appuye = false;
volatile bool bouton_bas_appuye = false;
volatile bool bouton_ok_appuye = false;

// Le bouton Haut est relie a la broche PC1 (15)
#define brocheBoutonHaut 15
// Le bouton Bas est relie a la broche PC2 (16)
#define brocheBoutonBas 16
// Le bouton Ok est relie a la broche PC3 (17)
#define brocheBoutonOk 17

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

// Machine a etats pour la gestion du menu
enum machineEtatType_t: uint8_t {
    ME_MENU_TENSION,
    ME_MENU_CAPACITE,
    ME_MENU_ETALONNAGE,
    ME_MENU_CAPA_NIMH,
    ME_MENU_CAPA_LIION
};
machineEtatType_t machineEtat = ME_MENU_TENSION;


// Constantes etalonnees 
// ---------------------
// Valeurs de courants de decharge en mA
#define COURANT1 151
#define COURANT2 293
#define COURANT3 434
// Valeur de la tension de reference
#define VREF 1.075
// Rapport pont diviseur utilise sur A0
#define PDIV0 4.15

// Seuils bas en tension des accus
// Ne pas descendre sous ces seuils lors de la
// mesure de capacite, sous peine de les endommager
#define SEUIL_BAS_TENSION_ACCU_NIMH     0.80
#define SEUIL_BAS_TENSION_ACCU_LIION    2.00

// Mesure de la tension d'un accumulateur
void mesureTension() {
  float u;

  // La mesure en tension se fait à faible
  // courant de decharge COURANT1
  digitalWrite(3, LOW);
  digitalWrite(4, HIGH);
  
  while (bouton_ok_appuye == false) {
    oled.clear();
    oled.set2X();    
    oled.println("Tension");
    oled.set1X();    
    u = PDIV0*analogRead(A0)*VREF/1023.0;
    oled.println("");
    oled.println("U = " + String(u) + " V");  
    oled.println("");
    oled.println("ok -> terminer");
    delay(2000);
  }
  
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  bouton_ok_appuye = false;
}

void mesureCapacite(float seuil) {
  float u;
  uint16_t q;
  unsigned long initTimeSec, dureeMesureSec;

  initTimeSec = millis()/1000;

  // La mesure de la capacite de l'accu se fait à 
  // fort courant de decharge COURANT3 
  digitalWrite(3, HIGH);
  digitalWrite(4, HIGH);
  
  u = PDIV0*analogRead(A0)*VREF/1023.0;
  
  while ((u >= seuil) &amp;&amp; (bouton_ok_appuye == false)) {
    oled.clear();
    oled.set2X();
    oled.println("Capacite");
    oled.set1X(); 
    u = PDIV0*analogRead(A0)*VREF/1023.0;
    oled.println("");
    oled.println("U = " + String(u) + " V");  
    dureeMesureSec = millis()/1000-initTimeSec;
    q = COURANT3*dureeMesureSec/3600;
    oled.println("Q = "+String(q)+" mAh");  
    delay(2000);
  }

  oled.println("mesure terminee");
  oled.println("ok -> fin");
  
  Serial.println("mesure terminee");
  Serial.println("Q = "+String(q)+" mAh");  

  bouton_ok_appuye = false;
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  
  while (bouton_ok_appuye == false) {};
  bouton_ok_appuye = false;
}


// Etalonnage du circuit
//
void etalonnage() {
  float U0;

  oled.clear();
  oled.set2X(); 
  oled.println("Etalonnage");
  oled.set1X(); 
  oled.println("");
  oled.println("Broche PD3 : Low ");
  oled.println("Broche PD4 : Low ");
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  delay(500);
  U0 = PDIV0*analogRead(A0)*VREF/1023.0;
  oled.println("U = " + String(U0) + " V");  
  oled.println("");
  oled.println("ok -> suivant");  
  while (bouton_ok_appuye == false) {};
  bouton_ok_appuye = false;
    
  oled.clear();
  oled.set2X(); 
  oled.println("Etalonnage");
  oled.set1X(); 
  oled.println("");
  oled.println("Broche PD3 : Low ");
  oled.println("Broche PD4 : High ");
  digitalWrite(3, LOW);
  digitalWrite(4, HIGH);
  delay(500);  
  U0 = PDIV0*analogRead(A0)*VREF/1023.0;
  oled.println("U = " + String(U0) + " V");  
  oled.println("");
  oled.println("ok -> suivant");  
  while (bouton_ok_appuye == false) {};
  bouton_ok_appuye = false;
  
  oled.clear();
  oled.set2X(); 
  oled.println("Etalonnage");
  oled.set1X(); 
  oled.println("");
  oled.println("Broche PD3 : High ");
  oled.println("Broche PD4 : Low ");
  digitalWrite(3, HIGH);
  digitalWrite(4, LOW);
  delay(500);  
  U0 = PDIV0*analogRead(A0)*VREF/1023.0;
  oled.println("U = " + String(U0) + " V");  
  oled.println("");
  oled.println("ok -> suivant");  
  while (bouton_ok_appuye == false) {};
  bouton_ok_appuye = false;
  
  oled.clear();
  oled.set2X(); 
  oled.println("Etalonnage");
  oled.set1X(); 
  oled.println("");
  oled.println("Broche PD3 : High ");
  oled.println("Broche PD4 : High ");
  digitalWrite(3, HIGH);
  digitalWrite(4, HIGH);
  delay(500);  
  U0 = PDIV0*analogRead(A0)*VREF/1023.0;
  oled.println("U = " + String(U0) + " V");  
  oled.println("");  
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);
  oled.println("ok -> fin");  
  while (bouton_ok_appuye == false) {};
  bouton_ok_appuye = false;
}

// Affichage des menus
void afficheMenuPrincipal() {
  oled.clear();
  oled.set2X();
  oled.println("Menu");
  oled.set1X();
  oled.println("");
  oled.println("* Mesure tension");
  oled.println("  Mesure capacite");
  oled.println("  Etalonnage");  
}
void afficheMenuCapacite() {
  oled.clear();
  oled.set2X();
  oled.println("Menu");
  oled.set1X();
  oled.println("");
  oled.println("  Mesure tension");
  oled.println("* Mesure capacite");
  oled.println("  Etalonnage");  
}
void afficheMenuEtalonnage() {
  oled.clear();
  oled.set2X();
  oled.println("Menu");
  oled.set1X();
  oled.println("");
  oled.println("  Mesure tension");
  oled.println("  Mesure capacite");
  oled.println("* Etalonnage");  
}
void afficheMenuNiMh() {
  oled.clear();
  oled.set2X();
  oled.println("Capacite");
  oled.set1X();
  oled.println("");
  oled.println("* Accu Ni-Mh");
  oled.println("  Accu Li-Ion");
}
void afficheMenuLiIon() {
  oled.clear();
  oled.set2X();
  oled.println("Capacite");
  oled.set1X();
  oled.println("");
  oled.println("  Accu Ni-Mh");
  oled.println("* Accu Li-Ion");
}

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("");
  
  // Positionne la référence de tension 
  // sur la référence interne à 1,1 V
  analogReference(INTERNAL);

  // Les entrees PC1 PC2 et PC3 sont les
  // boutons poussoirs, on y attache des
  // routines d'interruption
  pinMode(brocheBoutonOk, INPUT_PULLUP);
  pinMode(brocheBoutonHaut, INPUT_PULLUP);
  pinMode(brocheBoutonBas, INPUT_PULLUP);
  PCintPort::attachInterrupt(brocheBoutonHaut, isr_up, RISING);
  PCintPort::attachInterrupt(brocheBoutonBas, isr_down, RISING);
  PCintPort::attachInterrupt(brocheBoutonOk, isr_ok, RISING);

  // Les sortie PD3 et PD4 sont les 2 bits 
  // de controle du reseau R-2R
  pinMode(3, OUTPUT);
  pinMode(4, OUTPUT);
  digitalWrite(3, LOW);
  digitalWrite(4, LOW);

  // Attente d'appui sur le bouton ok
  while (bouton_ok_appuye == false) {};
  bouton_ok_appuye = false;

  afficheMenuPrincipal();

}

// Boucle principale
void loop() {
  switch (machineEtat) {
    case ME_MENU_TENSION:
      if (bouton_haut_appuye == true) {
        bouton_haut_appuye = false;
      }
      if (bouton_bas_appuye == true) { 
        bouton_bas_appuye = false;
        machineEtat = ME_MENU_CAPACITE;
        afficheMenuCapacite();        
      }
      if (bouton_ok_appuye == true) {
        bouton_ok_appuye = false;
        mesureTension();
        machineEtat = ME_MENU_TENSION;        
        afficheMenuPrincipal();        
      }
      break;
    case ME_MENU_CAPACITE:
      if (bouton_haut_appuye == true) {
        bouton_haut_appuye = false;
        machineEtat = ME_MENU_TENSION;
        afficheMenuPrincipal();        
      }
      if (bouton_bas_appuye == true) { 
        bouton_bas_appuye = false;
        machineEtat = ME_MENU_ETALONNAGE;
        afficheMenuEtalonnage();        
      }
      if (bouton_ok_appuye == true) {
        bouton_ok_appuye = false;        
        machineEtat = ME_MENU_CAPA_NIMH;
        afficheMenuNiMh();        
      }    
      break;
    case ME_MENU_CAPA_NIMH:
      if (bouton_haut_appuye == true) {  
        bouton_haut_appuye = false;
      }
      if (bouton_bas_appuye == true) { 
        bouton_bas_appuye = false;
        machineEtat = ME_MENU_CAPA_LIION;
        afficheMenuLiIon();        
      }
      if (bouton_ok_appuye == true) {
        bouton_ok_appuye = false;        
        mesureCapacite(SEUIL_BAS_TENSION_ACCU_NIMH);
        machineEtat = ME_MENU_TENSION;
        afficheMenuPrincipal();                
      }   
      break; 
    case ME_MENU_CAPA_LIION:
      if (bouton_haut_appuye == true) {  
        bouton_haut_appuye = false;
        afficheMenuNiMh();   
        machineEtat = ME_MENU_CAPA_NIMH;                     
      }
      if (bouton_bas_appuye == true) { 
        bouton_bas_appuye = false;
      }
      if (bouton_ok_appuye == true) {
        bouton_ok_appuye = false;        
        mesureCapacite(SEUIL_BAS_TENSION_ACCU_LIION);
        machineEtat = ME_MENU_TENSION;
        afficheMenuPrincipal();                
      }    
      break;      
    case ME_MENU_ETALONNAGE:
      if (bouton_haut_appuye == true) {
        bouton_haut_appuye = false;
        afficheMenuCapacite();        
        machineEtat = ME_MENU_CAPACITE;
      }
      if (bouton_bas_appuye == true) { 
        bouton_bas_appuye = false;
      }
      if (bouton_ok_appuye == true) {
        bouton_ok_appuye = false;                
        etalonnage();
        machineEtat = ME_MENU_TENSION;        
        afficheMenuPrincipal();                
      }    
      break;
    default:
      break;
  }
  delay(100);
}

// Routine d'interruption du bouton haut
void isr_up() {
  if ((millis() - last_interrupted) > 100)
    bouton_haut_appuye = true;
  last_interrupted = millis();
}

// Routine d'interruption du bouton bas
void isr_down() {
  if ((millis() - last_interrupted) > 100)
    bouton_bas_appuye = true;      
  last_interrupted = millis();
}

// Routine d'interruption du bouton ok 
void isr_ok() {
  if ((millis() - last_interrupted) > 100)
    bouton_ok_appuye = true;
  last_interrupted = millis();
}