/*
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++;
}