123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- /*
- 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 <http://www.gnu.org/licenses/>.
- */
- #define SERIALBAUD 57600
- #define SERIALCONF SERIAL_8N1
- //#define INCLUDE_TESTFUNCTIONS
- #define PROJECT_NAME "DualS0ImpCounter"
- #define PROJECT_VERSION "1.0"
- #include <avr/wdt.h>
- #include <EEPROM.h>
- // 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++;
- }
|