Monitoring a 48V string made up of 4 series-connected 12V batteries
In an off-grid power system supplying various electrical devices, we need to monitor the supply voltage from this group of batteries.
Since the discharge rate is not equal among the 4 batteries powering the system, it is necessary to monitor all 4 series-connected batteries.
For this purpose, we can use the Espduino development board, whose features are presented here .
Electrical diagram
The basic electrical diagram is shown below:
from which we can conclude that for the proposed topic the following blocks are necessary:
- 4 voltage dividers R1-R2, R3-R4, R5-R6, R7-R8;
- development board - Espduino;
- sensor for measuring analog voltage values - ADS1115;
- temperature and humidity sensor - DHT22;
- LCD 2x16;
- serial-parallel converter - i2c - implemented with PCF8574;
Voltage Dividers
There are 4 voltage dividers with a transformation ratio of approximately:
Vbat1/V1=5;
Vbat2/V2=10;
Vbat3/V3=15;
Vbat4/V4=16;
These ratios will be determined exactly in the software calibration procedure by measuring with a voltmeter, and the results obtained will be used to complete the following constants within the software:
//
//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;
ADS1115 Analog-to-Digital Converter
The ADS1115 is a 16-bit analog-to-digital converter made by Texas Instruments . Communication with it is done using the I2C protocol.
According to the figure below, up to 4 possible addresses can be configured:
The converter has 4 inputs, and the input voltages must not exceed the supply voltage of the module.
The 4 inputs can also be configured in differential mode (A0-A1; A2-A3), but to monitor 4 batteries, an additional module is required.
Reading and calculating the 4 voltages corresponding to the batteries was done using the function:
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
//
}
Measuring temperature and humidity
Considering that the capacity of a battery is strongly influenced by temperature, a digital sensor for measuring temperature and humidity, DHT22, has been provided. The sensor gives us information related to the weather conditions at the location where the batteries are located.
Displaying voltages
In order to display the measured and calculated voltages for each battery individually, the system contains an LCD16x2 display connected to the Espduino board via two wires using I2C communication facilitated by the serial-parallel converter made with PCF8574. To reduce power consumption, the display is only illuminated when the button connected to GPIO0 (flash) is pressed for a long time (>1s).
The function used to display the determined voltages is as follows:
//
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);
}
For remote monitoring, the code includes publishing to the three topics:
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
to the mosquitto service/server using the MQTT protocol.
To store the data in a time-series database InfluxDB , we use Telegraf , so that ultimately Grafana can present the stored data in an elegant manner.
An image of the prototype used for testing:
For the implementation of the code, Visual Studio Code was used:
where PlatformIO was activated.
The complete source code used is as follows:
#include <Arduino.h> //load the Arduino library
#include <Wire.h> //load the library for i2c
#include <Adafruit_ADS1015.h>//load the ADC sensor library
#include <LiquidCrystal_PCF8574.h>//load the serial-parallel communication library
#include <ESP8266WiFi.h>//load the library for WiFi communication
#include <PubSubClient.h>//include the library for MQTT communication
#include <Adafruit_Sensor.h>//include the library necessary for DHT22
#include <DHT.h>//include the library necessary for DHT22
#include <DHT_U.h>//include the library necessary for DHT22
//
//comunicația seriala pentru debug 1
//anulez comunicația seriala pentru debug 0
#define debug 1
//
//Definesc constantele
//
//pentru senzor
#define DHTPIN 2 //Pin D2 where the digital output from the DHT22 sensor is connected
#define DHTTYPE DHT22 // define the model 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(" %;");
//
}
//