Monitorizarea unui string de 48 V format din 4 baterii înseriate de 12V

Într-un sistem off-grid de alimentare a diverselor aparate electrice suntem nevoiți să monitorizăm tensiunea de alimentare a acestor aparate de la un grup de baterii.
Deoarece rata de descărcare nu este în mod egal din cele 4 baterii ce alimentează sistemul se impune a se monitoriza toate cele 4 baterii înseriate.
În acest scop putem folosi placa de dezvolatre Espduino, al cărui caracteristici sunt prezentate aici.

Schema eletrică

Schema electrică de principiu este redată mai jos:

din care putem constata că pentru tema propusă este necesar de următoarele blocuri:

  • 4 divizoare de tensiune R1-R2,R3-R4,R5-R6, R7-R8;
  • placa dezvolatare - Espduino;
  • senzor pentru măsurarea mărimelor analogice de tensiune - ADS1115;
  • senzor de temperatură și umiditate - DHT22;
  • LCD 2x16;
  • convertor serial-paralel - i2c - realizat cu PCF8574;

Divizoarele de tensiune

Sunt în număr de 4 divizoare de tensiune cu raportul de transformare, aproximativ:
Vbat1/V1=5;
Vbat2/V2=10;
Vbat3/V3=15;
Vbat4/V4=16;
Aceste rapoarte se vor determina exact, în procedura de calibrare software, prin măsurarea cu voltmetru iar rezulatatele obținute vor fi folosite la completarea următoarelor constante din cadrul software-ului:

//
//Calibrare
//
//Raportul dintre tensiunea maximă pe bateria Vbat1=Vbat 
//și tensiunea de la ieșire din divizor V1 (Vbat/V1)
const float calDivRez0 =  5;
//Raportul dintre tensiunea maximă pe bateria (Vbat+Vbat2)=Vbat
//și tensiunea de la ieșire din divizor V2 (Vbat/V2)
const float calDivRez1 = 10;
//Raportul dintre tensiunea maximă pe bateria (Vbat+Vbat2+Vbat3)=Vbat
//și tensiunea de la ieșire din divizor V3 (Vbat/V3)
const float calDivRez2 = 15;
//Raportul dintre tensiunea maximă pe bateria (Vbat+Vbat2+Vbat3+Vbat4)=Vbat
//și tensiunea de la ieșire din divizor V4 (Vbat/V4)
const float calDivRez3 = 20;

Convertorul analog digital ADS1115

ADS1115 este un convertor analogic digital de 16 biți realizat de către Texas Instruments. Comunicare cu acesta se face folosind protocolul I2C.
Conform figurii de mai jos se pot configura până la 4 adrese posibile:

Convertorul dispune de 4 intrări iar tensiunile de intrare nu pot depăși tensiunea de alimentare a modulului.
Cele 4 intrări pot fi configurate și în modul diferențial (A0-A1;A2-A3) dar pentru a monitoriza 4 baterii este necesar încă un modul.

Citirea și calculul celor 4 tensiuni aferente bateriilor s-a realizat cu funcția:

void citesc_tens()
{
  adc0 = ads.readADC_SingleEnded(0);//citesc intrarea adc0
  adc_0 = ((adc0*bitRate)/1000)*calDivRez0;//transform val adc0 în V și multiplic adc_0 cu, const. de calibrare
  Vbat_1 = adc_0; //calculez tens. pe Bat1
//
  adc1 = ads.readADC_SingleEnded(1);//citesc intrarea adc1
  adc_1 = ((adc1*bitRate)/1000)*calDivRez1;//transform val adc1 în V și multiplic adc_1 cu, const. de calibrare
  Vbat_2 = adc_1-adc_0; //calculez tens. pe Bat2
//
  adc2 = ads.readADC_SingleEnded(2);//citesc intrarea adc2
  adc_2 = ((adc2*bitRate)/1000)*calDivRez2;//transform val adc2 în V și multiplic adc_2 cu, const. de calibrare
  Vbat_3 = adc_2-adc_1;  // calculez tens. pe Bat3
//
  adc3 = ads.readADC_SingleEnded(3);//citesc intrarea adc3
  adc_3 = ((adc3*bitRate)/1000)*calDivRez3;//transform val adc3 în V și multiplic adc_3 cu, const. de calibrare
  Vbat_4 = adc_3-adc_2; //calculez tens. pe Bat4
//
}

Măsurarea temperaturii și a umidității

Avân în vedere că, capacitatea unei baterii este puternic influiențată de temperatură s-a prevăzut un senzor digital pentru măsurarea temperaturii și a umidității DHT22. Senzorul ne conferă informății legate de starea vremii la locația unde se află bateriile.

Afișarea tensiunilor

În vederea afișarii tensiunilor măsurate și calculate pentru ficare baterie în parte sistemul conține un afișor LCD16x2 conectat la placa Espduino prin două fire folosind comunicația I2C realizată cu ajutorul convertorul serial-paralel realizat cu PCF8574. Pentru a scădea consumul afișajul nu este iluminat decât la apăsarea lung (>1s) a butonului conecatat la GPIO0 (flash).

Funcția folosită la afișarea tensiunilor determinate este următoarea:

//
void afisare_lcd()
{
  //Iluminare afișaj
stareButon=digitalRead(Buton); // citesc starea butonului 
  if (stareButon==1)//dacă butonul nu este apăsat
  {
    lcd.setBacklight(0);;//ledul de la afișaj nu este alimentat
    delay(200);//o mică întârziere de 200ms
  }
  if (stareButon==0)//dacă butonul este apăsat
  {
    lcd.setBacklight(255);;//alimentez LED-ul de la afișaj
    delay(200);//o mică întârziere de 200ms
  }
//LCD linia 1
lcd.setCursor(0, 0);
lcd.print("V1=");
lcd.setCursor(3,0);
lcd.print(Vbat_1,1);
lcd.setCursor(9,0);
lcd.print("V2=");
lcd.setCursor(12,0);
lcd.print(Vbat_2,1);
//LCD linia 2
lcd.setCursor(0, 1);
lcd.print("V3=");
lcd.setCursor(3,1);
lcd.print(Vbat_3,1);
lcd.setCursor(9,1);
lcd.print("V4=");
lcd.setCursor(12,1);
lcd.print(Vbat_4,1);
}  

Pentru monitorizarea de la distanță s-a prevăzut în cod publicarea în cele trei topicuri:

const char* topic_umiditate = "sensors/esp8266/umiditate";//topicul pentru umiditate
const char* topic_temperatura = "sensors/esp8266/temperatura";//topicul pentru temperatura
const char* topic_tensiune = "sensors/esp8266/tensiune";//topicul pentru tensiune

către serviciul/serverul mosquitto folosind protocol MQTT.
Pentru a stoca datele în baza de date funcție de timp InfluxDB folosim Telegraf, pentru ca în final Grafana să prezinte datele stocate în baza de date într-un mod elegant.
O imagine a protopului folosit la teste:

Pentru realizare codului s-a folosit Visual Studio Code:

la care s-a activat PlatformIO.
Codul sursă în totalitate folosit, este următorul:

#include <Arduino.h> //încarc biblioteca Arduino
#include <Wire.h> //încarc biblioteca pentru i2c
#include <Adafruit_ADS1015.h>//încarc biblioteca senzorului ADC
#include <LiquidCrystal_PCF8574.h>//încarc biblioteca conv. com. serial-paralel
#include <ESP8266WiFi.h>//încarc biblioteca pentru com. WiFi
#include <PubSubClient.h>//înccarc biblioteca pentru com. MQTT
#include <Adafruit_Sensor.h>//încarc biblioteca necesara lui DHT22
#include <DHT.h>//încarc biblioteca necesara lui DHT22
#include <DHT_U.h>//încarc biblioteca necesara lui DHT22
//
//comunicația seriala pentru debug 1
//anulez comunicația seriala pentru debug 0 
#define debug 1
//
//Definesc constantele
//
//pentru senzor
#define DHTPIN 2 //Pinul D2 unde este conectată ieșire digitală de la senzorul DHT22
#define DHTTYPE DHT22 // definim modelul DHT22 (AM2302)
//
// pentru WIFI
const char* ssid = "xxx";//SSID unde mă conectez
const char* wifiPassword =  "xxx";//parola aferentă SSID-ului
//
//pentru serverul MQTT
const char* mqttServer = "192.168.1.25";//adresa serverului MQTTT
const int mqttPort = 1883;//portul de acces la serverul MQTT
const char* mqttUser = "esp8266";//utilizatorul ce are acces la serverul MQTT
const char* mqttPassword = "parola";//parola utilizatorului
const char* topic_umiditate = "sensors/esp8266/umiditate";//topicul pentru umiditate
const char* topic_temperatura = "sensors/esp8266/temperatura";//topicul pentru temperatura
const char* topic_tensiune = "sensors/esp8266/tensiune";//topicul pentru tensiune
//
//Definesc funcțiile
void setup_wifi();
void reconect();
void citesc_tens();
void afisare_lcd();
void citesc_temp(); 
//Declar obiectele
//declar obiectul ads
Adafruit_ADS1115 ads(0x48);  /* Use this for the 16-bit version */
//declar obiectul lcd
LiquidCrystal_PCF8574 lcd(0x27);  // setez adresa LCD-ului la 0x27
DHT_Unified dht(DHTPIN, DHTTYPE); //definesc obiectul dht
WiFiClient espClient;//definesc obiectul espClient
PubSubClient client(espClient);//definesc obiectul client
//declar variabilele
int16_t adc0, adc1, adc2, adc3;//variabile adc
float adc_0, adc_1, adc_2, adc_3;//variabilele adc în mV
float Vbat_1, Vbat_2, Vbat_3, Vbat_4;//variabilele tens. bateriilor
int error;//variabilă pt. eroarea LCD-ului
int Buton = 0; //declar Butonul să folosescă intrarea de la GPIO 0 (butonul de flash)
int stareButon = 1; // declar starea inițială a butornului
float T, H;//definesc variabile necesare temperaturii și umidității
//
//Calibrare
//
//Raportul dintre tensiunea maximă pe bateria Vbat1=Vbat 
//și tensiunea de la ieșire din divizor V1 (Vbat/V1)
const float calDivRez0 =  5;
//Raportul dintre tensiunea maximă pe bateria (Vbat+Vbat2)=Vbat
//și tensiunea de la ieșire din divizor V2 (Vbat/V2)
const float calDivRez1 = 10;
//Raportul dintre tensiunea maximă pe bateria (Vbat+Vbat2+Vbat3)=Vbat
//și tensiunea de la ieșire din divizor V3 (Vbat/V3)
const float calDivRez2 = 15;
//Raportul dintre tensiunea maximă pe bateria (Vbat+Vbat2+Vbat3+Vbat4)=Vbat
//și tensiunea de la ieșire din divizor V4 (Vbat/V4)
const float calDivRez3 = 20;
//constanta bitRate aferentă GAIN_ONE (0,125mV)
float bitRate = 0.125;
//
void setup(void) 
{
//funcția setup este rulată o singură dată
  delay(10); //o mica intarziere
  Wire.begin();//inițializez i2c
  Wire.beginTransmission(0x27);//trimit adresa LCD-ului
//
  #if debug == 1
//
  Serial.begin(115200);
  while (! Serial);//dacă nu se inițializează com. serială stau aici la infinit
  Serial.println("Hello!");
  Serial.println("Getting single-ended readings from AIN0..3");
  Serial.println("ADC Range: +/- 4.096V (1 bit = 2mV/ADS1015, 0.125mV/ADS1115)");
//
  Serial.println("LCD...");
  Serial.println("Verific LCD");
  error = Wire.endTransmission();
  Serial.print("Eroare: ");
  Serial.print(error);
  if (error == 0) {
    Serial.println(": am gasit LCD.");
  } else {
    Serial.println(": nu am gasit LCD.");
  }
//
  #endif
  //Domeniul de intrare ADC (sau câștigul) poate fi schimbat prin următoarele funcții, 
  //dar aveți grijă să nu depășiți niciodată VDD + 0.3V max
  //sau să depășiți limitele superioare și inferioare
  //dacă ajustați domeniul de intrare!
  //Setarea acestor valori incorect poate distruge ADC-ul dvs.!
  //                                                                ADS1015  ADS1115
  //                                                                -------  -------
  // ads.setGain(GAIN_TWOTHIRDS);  // 2/3x gain +/- 6.144V  1 bit = 3mV      0.1875mV (default)
     ads.setGain(GAIN_ONE);        // 1x gain   +/- 4.096V  1 bit = 2mV      0.125mV
  // ads.setGain(GAIN_TWO);        // 2x gain   +/- 2.048V  1 bit = 1mV      0.0625mV
  // ads.setGain(GAIN_FOUR);       // 4x gain   +/- 1.024V  1 bit = 0.5mV    0.03125mV
  // ads.setGain(GAIN_EIGHT);      // 8x gain   +/- 0.512V  1 bit = 0.25mV   0.015625mV
  // ads.setGain(GAIN_SIXTEEN);    // 16x gain  +/- 0.256V  1 bit = 0.125mV  0.0078125mV
  //
  ads.begin(); // initializez senzorul ADS1115
  lcd.begin(16, 2); // initializez lcd-ul pentru 16 caractere și 2 linii
  dht.begin();// inițializez senzorul DHT22
  setup_wifi();//apelez funcția
  client.setServer(mqttServer, mqttPort);//inițilizez conexiunea la serverul MQTT
}
//
void loop(void) 
{
// funcția loop este rulată în mod continuu
  citesc_tens();
  afisare_lcd();
//
  #if debug ==1
  Serial.print("AIN0: "); Serial.print(adc0); Serial.print("\tVbat_1= "); Serial.print(Vbat_1,2); Serial.print(" V"); Serial.print("\tadc_0= "); Serial.print(adc_0,2); Serial.println(" V");
  Serial.print("AIN1: "); Serial.print(adc1); Serial.print("\tVbat_2= "); Serial.print(Vbat_2,2); Serial.print(" V"); Serial.print("\tadc_1= "); Serial.print(adc_1,2); Serial.println(" V");
  Serial.print("AIN2: "); Serial.print(adc2); Serial.print("\tVbat_3= "); Serial.print(Vbat_3,2); Serial.print(" V"); Serial.print("\tadc_2= "); Serial.print(adc_2,2); Serial.println(" V");
  Serial.print("AIN3: "); Serial.print(adc3); Serial.print("\tVbat_4= "); Serial.print(Vbat_4,2); Serial.print(" V"); Serial.print("\tadc_3= "); Serial.print(adc_3,2); Serial.println(" V");
  Serial.println(" ");
  #endif
//
  delay(1000);
  citesc_temp();
//
if (!client.connected()) {
  reconect();//dacă utilizatorul nu s-a conectat la MQTT, reincerc
}
//
  String MSG_T = "TempCamera Temp=";//construiesc tabela și câmpul - pentru InfluxDb
  String MSG_H = "UmidCamera Umid=";//construiesc tabela și câmpul - pentru InfluxDb
  String StrT = String(T).c_str();//transform în String datele de tip float
  String StrH = String(H).c_str();//transform în String datele de tip float
  String Temp = MSG_T+StrT;//concatenez mesajul pentru temperatura
  String Umid = MSG_H+StrH;//concatenez mesajul pentru umiditate
  client.publish(topic_temperatura, Temp.c_str(), true);//public temperatura
  client.publish(topic_umiditate, Umid.c_str(), true);//public umiditatea
//
//
  String MSG_Vbat1 = "TensBat Vbat1=";//construiesc tabela și câmpul - pentru InfluxDb
  String StrVbat1 = String(Vbat_1).c_str();//transform în String datele de tip float
  String Svbat_1 = MSG_Vbat1+StrVbat1;//concatenez mesajul pentru tensiune
  client.publish(topic_tensiune, Svbat_1.c_str(), true);//public tensiunea
//
  String MSG_Vbat2 = "TensBat Vbat2=";//construiesc tabela și câmpul - pentru InfluxDb
  String StrVbat2 = String(Vbat_2).c_str();//transform în String datele de tip float
  String Svbat_2 = MSG_Vbat2+StrVbat2;//concatenez mesajul pentru tensiune
  client.publish(topic_tensiune, Svbat_2.c_str(), true);//public tensiunea
//
  String MSG_Vbat3 = "TensBat Vbat3=";//construiesc tabela și câmpul - pentru InfluxDb
  String StrVbat3 = String(Vbat_3).c_str();//transform în String datele de tip float
  String Svbat_3 = MSG_Vbat3+StrVbat3;//concatenez mesajul pentru tensiune
  client.publish(topic_tensiune, Svbat_3.c_str(), true);//public tensiunea
//
  String MSG_Vbat4 = "TensBat Vbat4=";//construiesc tabela și câmpul - pentru InfluxDb
  String StrVbat4 = String(Vbat_4).c_str();//transform în String datele de tip float
  String Svbat_4 = MSG_Vbat4+StrVbat4;//concatenez mesajul pentru tensiune
  client.publish(topic_tensiune, Svbat_4.c_str(), true);//public tensiunea
//
}
//
void citesc_tens()
{
  adc0 = ads.readADC_SingleEnded(0);//citesc intrarea adc0
  adc_0 = ((adc0*bitRate)/1000)*calDivRez0;//transform val adc0 în V și multiplic adc_0 cu, const. de calibrare
  Vbat_1 = adc_0; //calculez tens. pe Bat1
//
  adc1 = ads.readADC_SingleEnded(1);//citesc intrarea adc1
  adc_1 = ((adc1*bitRate)/1000)*calDivRez1;//transform val adc1 în V și multiplic adc_1 cu, const. de calibrare
  Vbat_2 = adc_1-adc_0; //calculez tens. pe Bat2
//
  adc2 = ads.readADC_SingleEnded(2);//citesc intrarea adc2
  adc_2 = ((adc2*bitRate)/1000)*calDivRez2;//transform val adc2 în V și multiplic adc_2 cu, const. de calibrare
  Vbat_3 = adc_2-adc_1;  // calculez tens. pe Bat3
//
  adc3 = ads.readADC_SingleEnded(3);//citesc intrarea adc3
  adc_3 = ((adc3*bitRate)/1000)*calDivRez3;//transform val adc3 în V și multiplic adc_3 cu, const. de calibrare
  Vbat_4 = adc_3-adc_2; //calculez tens. pe Bat4
//
}
//
void afisare_lcd()
{
  //Iluminare afișaj
stareButon=digitalRead(Buton); // citesc starea butonului 
  if (stareButon==1)//dacă butonul nu este apăsat
  {
    lcd.setBacklight(0);;//ledul de la afișaj nu este alimentat
    delay(200);//o mică întârziere de 200ms
  }
  if (stareButon==0)//dacă butonul este apăsat
  {
    lcd.setBacklight(255);;//alimentez LED-ul de la afișaj
    delay(200);//o mică întârziere de 200ms
  }
//LCD linia 1
lcd.setCursor(0, 0);
lcd.print("V1=");
lcd.setCursor(3,0);
lcd.print(Vbat_1,1);
lcd.setCursor(9,0);
lcd.print("V2=");
lcd.setCursor(12,0);
lcd.print(Vbat_2,1);
//LCD linia 2
lcd.setCursor(0, 1);
lcd.print("V3=");
lcd.setCursor(3,1);
lcd.print(Vbat_3,1);
lcd.setCursor(9,1);
lcd.print("V4=");
lcd.setCursor(12,1);
lcd.print(Vbat_4,1);
}
//
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Ma conectez la ");
Serial.println(ssid);
//
WiFi.begin(ssid, wifiPassword);//inițializez conexiunea wifi
//
while (WiFi.status() != WL_CONNECTED) {
  delay(500);//aștept jumătate de secundă
  Serial.print(".");
}
//
Serial.println("");
Serial.println("S-a stabilit conexiune WiFi");
Serial.println("Adresa IP: ");
Serial.println(WiFi.localIP());
}
//
void reconect(){
  while(!client.connected()) {
    Serial.println("Astept sa ma conectez la serverul MQTT...");
    if (client.connect("ESP8266Client", mqttUser, mqttPassword)) {//trimit id-ul, utilizatorul și parola pentru conectare la MQTT
      Serial.println("m-am conectat");
    }
    else {
      Serial.println("eroare, nr= ");
      Serial.print(client.state());//scot nr erorii, in caz de aparitie se va invetiga motivul
      Serial.println(" incerc din nou in 5 secunde");
      delay(5000);//astept 5 secunde
    }
  }
}
//
void citesc_temp()
{
  sensors_event_t event;
  dht.temperature().getEvent(&event);
  T = event.temperature;
  dht.humidity().getEvent(&event);
  H = event.relative_humidity;
//
  Serial.print("Temperatura: ");
  Serial.print(T);
  Serial.print(" C; ");
//
  Serial.print("Umiditatea: ");
  Serial.print(H);
  Serial.println(" %;");
//
}
//