/******************************************************** Voltmeter/Ammeter with LCD display recording measurement results into the log file at fixed time intervals. v.1.5 February 2020 --------------------------------------------------------- Hardware: - Arduino NANO - 16-bit ADC module based on ADS1115 - Logger module from Deek-Robot incl. DS1307 real time clock on I2C bus and SD card reader on SPI bus - LCD display module 1602 on I2C bus - Two current sensor modules based on ACS712 ------------------------------------------------------- Version history 1.0 January 2018 Initial version 1.1 September 2018 Start button is blocked when SD card is not inserted 1.2 October 2018 New step in SETUP mode - zero setup routine 1.3 October 2018 Settings are stored to EEPROM to keep them when power is off 1.4 December 2018 Calculation and display of the energy in Wh accumulated and returned by the battery 1.5 February 2021 Calculation and display of charge/discharge level (battery capacity) in Ah ****************************************************/ #include #include #include #include #include #include #include #define voltageSensitivity 0.12486 //ADS1115 sensitivity = 0.125 for GAIN_ONE (see ADS1115 datasheet for details) #define currentSensitivity 0.26320 //Sensitivity of two ACS712 sensors connected in opposite direction //#define NOISE_SUPPRESSOR 1 // Comment this line to turn noise supressor off #define MODE_MEASUREMENT 0 // Device measures input values and displays voltage and current #define MODE_ENERGY 1 // Device displays accumulated and returned energy #define MODE_ALERT 2 // Device displays "No SD card" alert #define MODE_RECORDING 3 // Device measures input values and records results into SD card every minutes #define MODE_SETUP 4 // Measurement is stopped, user changes settings interactively #define setupButtonPin 2 // SETUP pushbutton pin #define startButtonPin 3 // START/UP/OK pushbutton pin #define stopButtonPin 4 // STOP/DOWN/CANCEL pushbutton pin #define ADC_I2C_ADDR 0x48 // Analog-digital converter I2C address = 72 #define DSP_I2C_ADDR 0x3F // Display module I2C address = 63 #define RTC_I2C_ADDR 0x68 // Real time clock I2C address = 104 #define INTERVAL_EE_ADDR 0 // Bytes 0 and 1 in EEPROM are used to keep interval value (int) #define VOLTAGE_CORR_EE_ADDR 2 // Bytes 2,3,4,5 in EEPROM are used to keep voltageZeroCorrection (float) #define CURRENT_CORR_EE_ADDR 6 // Bytes 6,7,8,9 in EEPROM are used to keep currentZeroCorrection (float) float voltageZeroCorrection = 0.0; //compensates zero point bias in ADC channel 0 (voltage channel) float currentZeroCorrection = 0.0; //compensates zero points difference between two ACS712 modules bool RecordingIsActive = false; bool CardIsInTheSocket = false; const int intervals[] = {1,3,10,30}; //intervals in minutes between log records int interval = 2; //index in intervals[] array, 10 mins by default File logFile; //Log file object Adafruit_ADS1115 ads(ADC_I2C_ADDR); //ADC object LiquidCrystal_I2C lcd(DSP_I2C_ADDR,16,2); //Display object: 16 symbols in a row, 2 rows RTC_DS1307 rtc; //Real Time Clock object float Voltage = 0.0; // Voltage measurement result float Current = 0.0; // Current measurement result float ChargedWh = 0.0; // Accumulated (charged) and returned float DischargedWh = 0.0; // (discharged) energy in Watt-hours float ChargedAh = 0.0; // Accumulated (charged) and returned float DischargedAh = 0.0; // (discharged) capacity in Ampere-hours boolean display_energy = false; //***** Execute analog-digital conversion on one of the ADS1115 channels ****** // 1-st argument - ADC IC channel number (0 - Voltage channel, 1 and 2 - Current channels) // 2-nd argument - input voltage divider ratio (5 - Voltage channel, 1 - Current channel) // 3-rd argument - number of iterations (repeat measurement for times and return average) // Returns measured voltage in Volts float measureSingleEnded(uint8_t channel, uint8_t divider, uint8_t iterations) { int16_t adc; // 16-bit integer obtained from the ADC long accum = 0; // sum of measurements for (int i = 0; i < iterations; i++) { adc = ads.readADC_SingleEnded(channel); accum += adc; } /* Voltage = accum * divider * voltageSensitivity / 1000 / iterations * where: * accum - sum of measurements accumulated in accum variable * divider - input voltage divider ratio * voltageSensitivity - 0.1875 for GAIN_TWOTHIRDS, 0.125 for GAIN_ONE, * 0.0625 for GAIN_TWO (see ADS115 datasheet for details) * 1000 - convert mV to V * iterations - number of measurements to be averaged */ return accum * divider * voltageSensitivity / 1000 / iterations; } //***** Measure voltage and current, write them to log ***** // MeasurementInterval - interval in milliseconds // since previous measurement void measure(unsigned long MeasurementInterval) { static long lastRecordTime = 0; //time in seconds since 1/1/2000 of last log record //++++ measure voltage ++++ // Channel 0, input divider ratio = 5, average of 10 iterations // adding correction to compensate zero point bias of ADC input Voltage = measureSingleEnded(0, 5, 10) + voltageZeroCorrection; //++++ cut negative values ++++ Voltage = (Voltage > 0) ? Voltage : 0; //++++ measure current ++++ // Channel 1 and 2, no divider, average of 10 iterations float Chan1 = measureSingleEnded(1, 1, 10); float Chan2 = measureSingleEnded(2, 1, 10); // Calculate current value adding correction to compensate zero difference of two ACS712 modules Current = (Chan1 - Chan2) / currentSensitivity + currentZeroCorrection; //++++ calculate accumulated and returned energy ++++ /* Capacity = Current * MeasurementInterval / 3600000.0 * Energy = Capacity * Voltage * where: * Capacity - accumulated (charge) or returned (discharge) battery capacity in Ampere*hours * Energy - accumulated (charge) or returned (discharge) battery energy in Watt*hours * Current - charge (if positive) or discharge (if negative) current in Ampers * MeasurementInterval - time since previous measurement in milliseconds * 3600000.0 - conversion of milliseconds to hours * Voltage - battery voltage in Volts */ float additive = Current * MeasurementInterval / 3600000.0; if (Current > 0) { //Accum is being charged ChargedAh += additive; ChargedWh += additive * Voltage; } else { //Accum is being discharged DischargedAh -= additive; DischargedWh -= additive * Voltage; } //++++ write record to log ++++ if (RecordingIsActive && logFile) { //Recording mode is active and we have file to write into it // Take current date and time from RTC DateTime now = rtc.now(); if (now.secondstime() >= (lastRecordTime + intervals[interval] * 60)) { //store last record time lastRecordTime = now.secondstime(); //construct date/time string as "YYYY/MM/DD HH:mm:ss" String dateStr = String(now.year()) + "/" + now.month() + "/" + now.day() + " " + normalize(now.hour()) + ":" + normalize(now.minute()) + ":" + normalize(now.second()); //write record to log file logFile.println(dateStr + ", " + String(Voltage,4) + ", " + String(Current,4)); } } #ifdef NOISE_SUPPRESSOR //++++ treat Voltage values less than 3mV as zero to suppress noise near zero ++++ if (Voltage < 0.003) { Voltage = 0; } //++++ treat Current values less than 3mA as zero to suppress noise near zero ++++ if (abs(Current) < 0.003) { Current = 0; } #endif } //**** Display static simbols depending on mode **** void setDisplayMode(int mode) { static int oldMode = -1; if (mode != oldMode) { oldMode = mode; lcd.clear(); switch (mode) { case (MODE_MEASUREMENT): lcd.print("U: "); lcd.setCursor(0,1); lcd.print("I: "); lcd.noBlink(); break; case (MODE_ENERGY): lcd.print("C: "); lcd.setCursor(0,1); lcd.print("D: "); lcd.noBlink(); break; case (MODE_ALERT): break; case (MODE_RECORDING): lcd.print("U: "); lcd.setCursor(0,1); lcd.print("I: "); //show blinking "R" at the right of upper row lcd.setCursor(15,0); lcd.print("R"); lcd.setCursor(15,0); lcd.blink(); break; case (MODE_SETUP): lcd.print("Setup"); break; } } } //**** Display Voltage and Current **** void showResults(void) { //++++ display Voltage ++++ lcd.setCursor(3,0); lcd.print(" "); lcd.setCursor(alignment(Voltage) - 1,0); lcd.print(Voltage,3); lcd.print(" V"); //++++ display Current ++++ lcd.setCursor(3,1); lcd.print(" "); lcd.setCursor(alignment(Current) - 1,1); lcd.print(Current,3); lcd.print(" A"); lcd.setCursor(15,0); //blinking "R" position in recording mode } //**** Display Charged and Discharged battery energy **** void showEnergy(void) { //++++ display accumulated energy ++++ lcd.setCursor(3,0); lcd.print(" "); lcd.setCursor(alignment(ChargedWh),0); lcd.print(ChargedWh,4); lcd.print(" Wh"); //++++ display returned energy ++++ lcd.setCursor(3,1); lcd.print(" "); lcd.setCursor(alignment(DischargedWh),1); lcd.print(DischargedWh,4); lcd.print(" Wh"); } //**** Display Charged and Discharged battery charge **** void showCapacity(void) { //++++ display accumulated charge ++++ lcd.setCursor(3,0); lcd.print(" "); lcd.setCursor(alignment(ChargedAh),0); lcd.print(ChargedAh,4); lcd.print(" Ah"); //++++ display returned charge ++++ lcd.setCursor(3,1); lcd.print(" "); lcd.setCursor(alignment(DischargedAh),1); lcd.print(DischargedAh,4); lcd.print(" Ah"); } int alignment(float Inp){ if (Inp >= 100.0) { return 3; } else if (Inp >= 10.0) { return 4; } else if (Inp >= 0.0) { return 5; } else { return 4; } } //****** Clear display line ******* // 0 - top line, 1 - bottom line void clearLine(int line) { lcd.setCursor(0,line); lcd.print(" "); } //***** Wait until button is pressed ***** int waitButtonPress() { while (true) { delay(2); if (digitalRead(setupButtonPin) == LOW) { return setupButtonPin; } if (digitalRead(startButtonPin) == LOW) { return startButtonPin; } if (digitalRead(stopButtonPin) == LOW) { return stopButtonPin; } } } //***** Wait until button is released ***** void waitButtonRelease(int buttonPin) { do { delay(2); } while (digitalRead(buttonPin) == LOW); } // Add leading '0' to day, month, hours or minutes if it is less than 10 String normalize(uint8_t number) { if (number>9) { return String(number); } else { return (String('0') += number); } } //**** Retrive current date/time and display date or time **** // dateOrTime = true -> display date // dateOrTime = false -> display time DateTime displayDateOrTime(bool dateOrTime) { DateTime now = rtc.now(); lcd.setCursor(5,1); if (dateOrTime) { lcd.print(normalize(now.day())); lcd.print("-"); lcd.print(normalize(now.month())); lcd.print("-"); lcd.print(now.year(),DEC); } else { lcd.print(normalize(now.hour())); lcd.print(":"); lcd.print(normalize(now.minute())); } return now; } // ********* Set date ********* void dateSetup(void) { DateTime now; waitButtonRelease(startButtonPin); lcd.setCursor(10,0); lcd.print(":"); clearLine(1); lcd.blink(); //---> setup day while (true) { now = displayDateOrTime(true); // true -> display date lcd.setCursor(6,1); int buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button -> increase day rtc.adjust(DateTime(now.year(), now.month(), (now.day()==31?1:now.day()+1), now.hour(), now.minute(), now.second())); waitButtonRelease(startButtonPin); } if (buttonPressed == stopButtonPin) { // DN button -> decrease day rtc.adjust(DateTime(now.year(), now.month(), (now.day()==1?31:now.day()-1), now.hour(), now.minute(), now.second())); waitButtonRelease(stopButtonPin); } if (buttonPressed == setupButtonPin) { // SETUP button -> finish day setup, continue to month setup waitButtonRelease(setupButtonPin); break; } } //---> setup month while (true) { now = displayDateOrTime(true); // true -> display date lcd.setCursor(9,1); int buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button -> increase month rtc.adjust(DateTime(now.year(), (now.month()==12?1:now.month()+1), now.day(), now.hour(), now.minute(), now.second())); waitButtonRelease(startButtonPin); } if (buttonPressed == stopButtonPin) { // DN button -> decrease month rtc.adjust(DateTime(now.year(), (now.month()==1?12:now.month()-1), now.day(), now.hour(), now.minute(), now.second())); waitButtonRelease(stopButtonPin); } if (buttonPressed == setupButtonPin) { // SETUP button -> finish month setup, continue to year setup waitButtonRelease(setupButtonPin); break; } } //---> setup year while (true) { now = displayDateOrTime(true); // true -> display date lcd.setCursor(14,1); int buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button -> increase year rtc.adjust(DateTime((now.year()==99?0:now.year()+1), now.month(), now.day(), now.hour(), now.minute(), now.second())); waitButtonRelease(startButtonPin); } if (buttonPressed == stopButtonPin) { // DN button -> decrease year rtc.adjust(DateTime((now.year()==0?99:now.year()-1), now.month(), now.day(), now.hour(), now.minute(), now.second())); waitButtonRelease(stopButtonPin); } if (buttonPressed == setupButtonPin) { // SETUP button -> finish year setup waitButtonRelease(setupButtonPin); break; } } lcd.noBlink(); } // ********* Set time ********* void timeSetup(void) { DateTime now; waitButtonRelease(startButtonPin); lcd.setCursor(10,0); lcd.print(":"); clearLine(1); lcd.blink(); //---> setup hours while (true) { now = displayDateOrTime(false); // false -> display time lcd.setCursor(6,1); int buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button -> increase hours rtc.adjust(DateTime(now.year(), now.month(), now.day(), (now.hour()==23?0:now.hour()+1), now.minute(), now.second())); waitButtonRelease(startButtonPin); } if (buttonPressed == stopButtonPin) { // DN button -> decrease hours rtc.adjust(DateTime(now.year(), now.month(), now.day(), (now.hour()==0?23:now.hour()-1), now.minute(), now.second())); waitButtonRelease(stopButtonPin); } if (buttonPressed == setupButtonPin) { // SETUP button -> finish hours setup, continue to minutes setup waitButtonRelease(setupButtonPin); break; } } //---> setup minutes while (true) { now = displayDateOrTime(false); // false -> display time lcd.setCursor(9,1); int buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button -> increase minutes rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), (now.minute()==59?0:now.minute()+1), now.second())); waitButtonRelease(startButtonPin); } if (buttonPressed == stopButtonPin) { // DN button -> decrease minutes rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), (now.minute()==0?59:now.minute()-1), now.second())); waitButtonRelease(stopButtonPin); } if (buttonPressed == setupButtonPin) { // SETUP button -> finish minute setup waitButtonRelease(setupButtonPin); break; } } lcd.noBlink(); } // ********* Set interval ********* void intervalSetup(void) { waitButtonRelease(startButtonPin); lcd.setCursor(14,0); lcd.print(":"); clearLine(1); while (true) { lcd.setCursor(5,1); lcd.print(intervals[interval]); lcd.print("min "); int buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button -> increase interval interval++; if (interval > 3) {interval = 0;} waitButtonRelease(startButtonPin); } if (buttonPressed == stopButtonPin) { // DN button -> decrease interval interval--; if (interval < 0) {interval = 3;} waitButtonRelease(stopButtonPin); } if (buttonPressed == setupButtonPin) { // SETUP button -> store new value to EEPROM and that's it int oldInterval; EEPROM.get(INTERVAL_EE_ADDR, oldInterval); if (interval != oldInterval) { EEPROM.put(INTERVAL_EE_ADDR, interval); } return; } } } //*********** Set zero points ************ void zeroPointsSetup(void) { waitButtonRelease(startButtonPin); //------ show "Wait" message ------ lcd.clear(); lcd.setCursor(0,0); lcd.print("Wait..."); // measure voltage and current assuming that I input is // disconnected, U input is short-circuited and then // calculate zero correction values (50 iterations each) voltageZeroCorrection = - measureSingleEnded(0, 5, 50); currentZeroCorrection = -(measureSingleEnded(1, 1, 50) - measureSingleEnded(2, 1, 50)) / currentSensitivity; //------ show "Done" message ------ lcd.setCursor(0,0); lcd.print("Done "); //------ store calculated values to EEPROM ------ EEPROM.put(VOLTAGE_CORR_EE_ADDR, voltageZeroCorrection); EEPROM.put(CURRENT_CORR_EE_ADDR, currentZeroCorrection); //------ display calculated values for 4s ------ lcd.setCursor(0,1); lcd.print("U="); lcd.print(voltageZeroCorrection,3); lcd.print(" I="); lcd.print(currentZeroCorrection,3); delay(4000); lcd.setCursor(0,0); lcd.print("Setup"); } void showMsg(const char *msg, bool displayButtonPrompt) { lcd.setCursor(6,0); lcd.print(msg); if (displayButtonPrompt) { lcd.setCursor(0,1); lcd.print("UP\176OK DN\176Cancel"); // \176 is right arrow symbol } else { clearLine(1); } } //******* SETUP mode handling ******** // returns: // true if completed (all steps passed) // false if aborted (red button was pressed) bool doSetup(void) { //----- Date setup ----- showMsg("date?", true); waitButtonRelease(setupButtonPin); int buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button - setup date dateSetup(); } if (buttonPressed == stopButtonPin) { // DN button - cancel setup, return to measurement return false; } //----- Time setup ----- showMsg("time?", true); waitButtonRelease(setupButtonPin); buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button - setup time timeSetup(); } if (buttonPressed == stopButtonPin) { // DN button - cancel setup, return to measurement return false; } //----- Interval setup ----- showMsg("interval?", true); waitButtonRelease(setupButtonPin); buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button - setup interval intervalSetup(); } if (buttonPressed == stopButtonPin) { // DN button - cancel setup, return to measurement return false; } //----- Zero points setup ----- showMsg("zero? ", true); waitButtonRelease(setupButtonPin); buttonPressed = waitButtonPress(); if (buttonPressed == startButtonPin) { // UP button - setup zero points zeroPointsSetup(); } if (buttonPressed == stopButtonPin) { // DN button - cancel setup, return to measurement return false; } //----- Finalize setup ------ showMsg("completed!", false); delay(2500); return true; } //***** Callback function for file timestamps ***** void dateTimeCB(uint16_t* date, uint16_t* time) { DateTime now = rtc.now(); // return date using FAT_DATE macro to format fields *date = FAT_DATE(now.year(), now.month(), now.day()); // return time using FAT_TIME macro to format fields *time = FAT_TIME(now.hour(), now.minute(), now.second()); } // ******** Start recording to log file ******** void startRecording(void){ waitButtonRelease(startButtonPin); //take date and time from RTC and construct filename DateTime now = rtc.now(); String Fname = normalize(now.month()) + normalize(now.day()) + normalize(now.hour()) + normalize(now.minute()) + String(".CSV"); logFile = SD.open(Fname, FILE_WRITE); delay(500); } // ******** Stop recording to log file ********* void stopRecording(void){ waitButtonRelease(stopButtonPin); logFile.close(); delay(500); } //*** Display message if SD card is not inserted **** void noSdCardWarning() { lcd.print("SD card is not"); lcd.setCursor(5,1); lcd.print("found!"); delay(2500); } //******************************* // INITIALIZATION //******************************* void setup(void) { //**************************** // initialize button pins //**************************** pinMode(setupButtonPin, INPUT_PULLUP); pinMode(startButtonPin, INPUT_PULLUP); pinMode(stopButtonPin, INPUT_PULLUP); //**************************** // read settings from EEPROM //**************************** EEPROM.get(INTERVAL_EE_ADDR, interval); // for the very first launch when setings aren't // stored in EEPROM yet, set default values if ((interval < 0) || (interval > 3)) { interval = 2; voltageZeroCorrection = 0.0; currentZeroCorrection = 0.0; EEPROM.put(INTERVAL_EE_ADDR, interval); EEPROM.put(VOLTAGE_CORR_EE_ADDR, voltageZeroCorrection); EEPROM.put(CURRENT_CORR_EE_ADDR, currentZeroCorrection); } else { EEPROM.get(VOLTAGE_CORR_EE_ADDR, voltageZeroCorrection); EEPROM.get(CURRENT_CORR_EE_ADDR, currentZeroCorrection); } //**************************** // initialize ADC module //**************************** ads.begin(); ads.setGain(GAIN_ONE); //4.096V full-scale range //**************************** // initialize RTC module //**************************** rtc.begin(); if (!rtc.isrunning()) { rtc.adjust(DateTime(2017, 8, 22, 3, 0, 0)); //set any date/time to launch RTC if it isn't running } //**************************** // initialize LCD module //**************************** lcd.begin(); lcd.backlight(); //********************************** // set date time callback function //********************************** SdFile::dateTimeCallback(dateTimeCB); //**************************** // initialize SD card module //**************************** if (SD.begin()) { CardIsInTheSocket = true; } else { setDisplayMode(MODE_ALERT); noSdCardWarning(); } //**************************** // prepare screen //**************************** setDisplayMode(MODE_MEASUREMENT); } //******************************** // MAIN LOOP //******************************** void loop(void) { static unsigned long loopBegin = 0; unsigned long currentMillis; // *************************************** // Measure voltage and display results // *************************************** currentMillis = millis(); measure(currentMillis - loopBegin); loopBegin = currentMillis; showResults(); // *************************************** // Check if any of the buttons is pressed // Repeat it for 400 times (about 800 ms) // *************************************** for(int i = 0; i < 400; i++) { //SETUP button if ((!RecordingIsActive) && (digitalRead(setupButtonPin) == LOW)) { setDisplayMode(MODE_SETUP); bool SetupFinished = doSetup(); setDisplayMode(MODE_MEASUREMENT); if (!SetupFinished){ //if setup mode was aborted then wait for red button release waitButtonRelease(stopButtonPin); } } //START/OK/UP button if ((!RecordingIsActive) && (digitalRead(startButtonPin) == LOW)) { if (CardIsInTheSocket) { startRecording(); RecordingIsActive = true; setDisplayMode(MODE_RECORDING); } else { setDisplayMode(MODE_ALERT); noSdCardWarning(); setDisplayMode(MODE_MEASUREMENT); } } //STOP/CANCEL/DOWN button if (digitalRead(stopButtonPin) == LOW) { if (RecordingIsActive) { stopRecording(); RecordingIsActive = false; } else { setDisplayMode(MODE_ENERGY); // Display either Energy in Wh or Capacity in Ah // depending on display_energy flag value if (display_energy) { showEnergy(); } else { showCapacity(); } // Invert flag to switch display from Energy to // Capacity and back on each new Stop button press display_energy = !display_energy; waitButtonRelease(stopButtonPin); } setDisplayMode(MODE_MEASUREMENT); } delay(2); } }