/* Dual Impulse Counter Arduino/AVR software to detect 2 impulse inputs, increment counters and save counter values to EEPROM. With power loss detection / saving. Copyright 2020 Florian Krauter This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #define SERIALBAUD 57600 #define SERIALCONF SERIAL_8N1 //#define INCLUDE_TESTFUNCTIONS #define PROJECT_NAME "DualS0ImpCounter" #define PROJECT_VERSION "1.0" #include #include // DIGITAL OUTPUTS #define PIN_OUT_HARDRESET 6 #define PIN_OUT_LED 13 // DIGITAL INPUTS #define PIN_IN_PULSE1 2 #define PIN_IN_PULSE2 3 // NUMBER OF COUNTERS #define COUNTERS_COUNT 2 // ANALOG INPUTS #define PIN_IN_ADC_PWRGOOD A3 // global conf variables // POWER GOOD - monitors voltage in front of the onboard voltage regulator powering the AVR/Arduino // in order to save all data to EEPROM before power is really gone // to enable this, supply voltage is much higher than needed for an AVR/Arduino (9-12V) // connected to a huge (2200uF) capacitor over a decoupling diode (voltage drop 0.6-0.7V) // and regulated to Vcc=5V by the Arduino onboard regulator // Vin is measured through a voltage divider of 56k/24k (after the diode) // a value of 420 seems to be the minimum at ~7.5V supply voltage (in front of the decoupling diode) // below this the value increases again due to voltage regulator drop out and therefore sinking Vref // so a reasonable safety margin is mandatory here // as we use a 12V supply we set this to 500 for now ( ~= 9.0V) unsigned int powerGoodMinValue = 500; int debounceDelay = 40; int debounceRecoveryDelay = 70; bool debug = true; int debuglevel = 1; bool output_json = true; // --- END global conf vars unsigned long currentReading[COUNTERS_COUNT]; uint16_t currentReadingImpulses[COUNTERS_COUNT]; uint16_t currentReadingImpulses_saved[COUNTERS_COUNT]; byte pulseInPins[] = {PIN_IN_PULSE1, PIN_IN_PULSE2}; byte pulseInLastState[2]; unsigned long pulseInLastDebounceTime[] = {0, 0}; bool pulseAlreadyDetected[COUNTERS_COUNT]; unsigned long pulseInLastMillis[COUNTERS_COUNT]; // config vars per counter uint16_t meter_impPerUnit[] = {1000, 100}; uint16_t meter_noImpulseTimeout_seconds[] = {0, 300}; uint16_t meter_savePulsesOnInterval_mins[] = {15, 0}; //uint16_t meter_savePulsesEveryImpulses[] = {0, 0}; // global vars unsigned long pulsesLastSaved_seconds[COUNTERS_COUNT]; unsigned long lastDataSent_seconds[COUNTERS_COUNT]; unsigned long lastSaveTime_pulses = 0; unsigned long millis_everysecond = 0; unsigned long seconds=0; // global static vars // serial commands #define MAX_CMD_LEN 120 #define MAX_CMDDATA_LEN 90 char cmdBuffer[MAX_CMD_LEN + 1]; // Serial Input-Buffer int cmdBufferCount = 0; // Anzahl der eingelesenen Zeichen bool cmdComplete = false; char cmdDataBuffer[MAX_CMDDATA_LEN + 1]; // EEPROM addresses // configuration values in EEPROM addr 0-19 (avoid using 0 so starting at 2) uint16_t eeprom_addr_debug = 2; // 1 byte uint16_t eeprom_addr_output_json = 3; // 1 byte uint16_t eeprom_addr_debounce1 = 4; // 2 byte uint16_t eeprom_addr_debounce2 = 6; // 2 byte uint16_t eeprom_addr_powerGoodMinValue = 8; // 2 byte uint16_t eeprom_addr_impPerUnit[] = {10, 12}; // 2 bytes uint16_t eeprom_addr_noImpTout[] = {14, 16}; // 2 bytes uint16_t eeprom_addr_saveInt[] = {18, 20}; // 2 bytes // 4 bytes (unsigned long) for current unit readings // use 3 EEPROM for each reading addresses (2 as backup, so that the actual value can hopefully always be restored when 2 of them hold the same value) // as the value ranges from 0 to max. 100000 here we can write this directly on every value change // reserved EEPROM addr: 20-68 (4 counters a 3 values a 4 bytes uint16_t eeprom_addr_currentReading[2] = {32, 44}; uint16_t eeprom_addr_currentReading_backup1[2] = {36, 48}; uint16_t eeprom_addr_currentReading_backup2[2] = {40, 52}; // different situation on the impulses where 100 imp ^= 1 reading // these are updated quite often, so directly writing will bring troubles with EEPROM endurance // this value has to be written: // - after every increment of main reading value (set to 0, to ensure it will not hold a higher than real count) // - after some time of no impulse counter change (to save from time to time so that we don´t loose too many impulses) // -> When logging a gas meter this should be sufficient, as these typically stand still for a longer period between consuming periods as the heating // is switched in intervals. // as these typically have 10 or 100 pulses per unit (m^3), it should be safe to just write every reading rollover // (without decimals/single impulses) and + write the current impulse count when there was no activity for some minutes. // When logging i.e. a power meter with 1000 imp/kWh, it would get more complicated. These typically don´t ever stand still, so only writing // the current pulse count when there is no activity would just not work. Also 1000 pulses are a lot to loose, so for this case it is necessary // to also write i.E. every 50st or 100st pulse or every 10-60 min additionally. // -> this all leads to a somehow higher write load // so using wear leveling is advisable // a simple approach to that (ring buffer): // - define an address area on the EEPROM for this use // - preformat the entire EEPROM area (write 0x00 to every byte) // - before every EEPROM write, increase a 2 byte (uint16_t) write counter // - use this counter as offset to point to the actual most current data (has to multiplied with the length one dataset uses for sure) // - memorize last address written and write next dataset to the next address // - store the writecounter and the data next to it (examples for 2 byte counter and 2 byte data): // EEPROM area start address: 128 // counter=0 -> addr 128+129, value -> addr 130+131 (=start_addr + (counter * 4) => 128 + (0*4) = 128 // counter=1 -> addr 132+133, value -> addr 134+135 (=start_addr + (counter * 4) => 128 + (1*4) = 132 // counter=2 -> addr 136+137, value -> addr 138+139 (=start_addr + (counter * 4) => 128 + (2*4) = 136 // - repeat writing at offset 0 when the entire address space is used // - when restoring data, scan the entire area to find the position where the writecounter value is > than writecounter in the next field // and restore data from that position // static byte eeprom_datasetsize_currentReadingImpulses = 4; // 2 bytes data, 2 bytes write counter uint16_t eeprom_addr_area_start_currentReadingImpulses[2] = {128, 640}; uint16_t eeprom_addr_area_length_currentReadingImpulses[2] = {512, 128}; uint16_t eeprom_writecounter_currentReadingImpulses[COUNTERS_COUNT]; uint16_t eeprom_nextoffset_currentReadingImpulses[COUNTERS_COUNT]; /** Setup. */ void setup() { // immediately disable watchdog timer so we will not get interrupted wdt_disable(); // init global vars/arrays for (int i = 0; i < COUNTERS_COUNT; i++) { eeprom_writecounter_currentReadingImpulses[i] = 0; eeprom_nextoffset_currentReadingImpulses[i] = 0; currentReading[i] = 0; pulseInLastMillis[i] = 0; pulseAlreadyDetected[i] = false; } // PIN_OUT_HARDRESET is wired to /RESET PIN // used to hard reset the MCU from software // when there is no DTS pin connected and a firmware update should be installed // normally set to INPUT mode (without pullup, so "no" connection to the actual RESET pin) pinMode(PIN_OUT_HARDRESET, INPUT); pinMode(PIN_IN_PULSE1, INPUT_PULLUP); pinMode(PIN_IN_PULSE2, INPUT_PULLUP); // the following forces a pause before enabling WDT. This gives the IDE a chance to // call the bootloader in case something dumb happens during development and the WDT // resets the MCU too quickly. Once the code is solid, remove this. delay(2L * 1000L); // enable the watchdog timer. There are a finite number of timeouts allowed (see wdt.h). // Notes I have seen say it is unwise to go below 250ms as you may get the WDT stuck in a // loop rebooting. // The timeouts I'm most likely to use are: // WDTO_1S // WDTO_2S // WDTO_4S // WDTO_8S wdt_enable(WDTO_8S); Serial.begin(SERIALBAUD, SERIALCONF); Serial.print(PROJECT_NAME); Serial.print(" v"); Serial.print(PROJECT_VERSION); Serial.print(F(" starting...")); // read config from EEPROM loadConfig(); // restore counters from EEPROM restoreAllLastData(); // output current counter values printAllCurrentReadings(); } void loop() { if ((millis() - millis_everysecond) >= 1000) { millis_everysecond = millis(); everySecond(); } checkInputStates(); checkPowerGood(); wdt_reset(); } void everySecond() { onNoImpulseTimeout(); saveDataOnInterval(); seconds++; }