In short. ESP32, two water meters (hot and cold). Booth, hot and cold, some interrupts are missed. When i connect ESP32 to serial monitor and watch then all interrupts are detected (about 5-10 minutes). But if i leave to 5v power only, then some interrupts are missed (in one day it can be up to 50 or even more).
About water counter - one pulse per liter. Correct info about sensor is in PDF Original Operating Instructions. Here is image of technical info
Sensor connections. Brown wire to GND, white to PIN 32(hot),33(cold).
Board. Olimex ESP32-POE Rev C.
- Powered via 5v Samsung phone charger (standard 5V charger without 9v)
- WiFi enabled (For NTP and to see counter readings)
- OTA enabled
- WEB server running (plain html, ESPAsyncWebServer)
- Bluetooth disabled
- SD card attached (stores SQLite database only)
- Uses siara-cc/esp32_arduino_sqlite3_lib library
Water usage in last month (May)
- hot - 4777 liters (counted with sensor - 2675)
- cold - 9289, with sensor - 5528
As we can see, difference is huge. Maybe ISR routine exec time is too long and some interrupts are skipped? I know that WiFi too uses interrupts... Is my interrupt code wrong? Water counter reading is stored in SQLite database when 10 liters are used. Thanks for anyone for your time in advance!
Here is image of water counter and sensor image of water counter and sensor
Here is some code, related to interrupts (debug disabled).
struct WaterCounter
{
const uint8_t PIN; // microcontroller pin...
volatile uint32_t liters_total; // max 4294967295
volatile uint16_t liters_in_session; // max 65535
volatile uint32_t session_started; // unix timestamp
volatile uint32_t session_last_pulse; // unix timestamp
const char display_name[5];
volatile unsigned long pulse_start;
volatile unsigned long pulse_stop;
volatile uint32_t isr_prev_time;
volatile uint32_t liters_last_written;
volatile uint32_t liters_used_today;
};
WaterCounter wc_hot = { PIN_WC_HOT, 0, 0, 0, 0, "Hott", 0, 0, 0, 0, 0 };
WaterCounter wc_cold = {PIN_WC_COLD, 0, 0, 0, 0, "Cold", 0, 0, 0, 0, 0 };
void IRAM_ATTR handle_wc(WaterCounter *wc)
{
uint32_t t = micros();
uint32_t dt = t - wc->isr_prev_time;
if (dt<1500) return;
wc->isr_prev_time = t;
if (wc->pulse_start == 0)
{
wc->pulse_start = millis();
#ifdef DEBUG_ISR
//Serial.printf("Started %s pulse!\n", wc->display_name);
ets_printf("Started %s pulse!\n", wc->display_name);
#endif
}else
{
wc->pulse_stop = millis();
unsigned long pulsew = wc->pulse_stop - wc->pulse_start;
wc->pulse_start = 0;
if (pulsew > 40 && pulsew < 55)
{
wc->liters_total++;
wc->liters_in_session++;
wc->liters_used_today++;
if (wc->session_started == 0)
{
wc->session_started = now;
}
wc->session_last_pulse = now;
}
#ifdef DEBUG_ISR
//Serial.printf("%s: pulse_width: %ld\n", wc->display_name, pulsew);
ets_printf("%s: pulse_width: %ld\n", wc->display_name, pulsew);
#endif
}
}
void IRAM_ATTR isr_wc_cold()
{
handle_wc(&wc_cold);
}
void IRAM_ATTR isr_wc_hot()
{
handle_wc(&wc_hot);
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Serial.println("Boot...");
// allow some time to pull up pin, interrupt is attached below
pinMode(wc_cold.PIN, INPUT_PULLUP);
pinMode(wc_hot.PIN, INPUT_PULLUP);
Serial.printf("Watchdog timeout: %dms\n", WATCHDOG_TIMEOUT);
timerWD = timerBegin(0, 80, true); //timer 0, div 80
timerAttachInterrupt(timerWD, &resetModule, true); //attach callback
timerAlarmWrite(timerWD, WATCHDOG_TIMEOUT * 1000, false); //set time in us
timerAlarmEnable(timerWD); //enable interrupt
timerWrite(timerWD, 0); //reset timer (feed watchdog)
SD_MMC.begin();
enqRC = sqlite3_initialize();
if (enqRC == SQLITE_OK)
{
openDb();
initSensorDatabase();
timerWrite(timerWD, 0); //reset timer (feed watchdog)
SetupWiFi();
closeDb();
}
timerWrite(timerWD, 0); //reset timer (feed watchdog)
pollNtp();
timerWrite(timerWD, 0); //reset timer (feed watchdog)
openDb();
initTotalValues(&wc_cold);
timerWrite(timerWD, 0); //reset timer (feed watchdog)
initTotalValues(&wc_hot);
closeDb();
timerWrite(timerWD, 0); //reset timer (feed watchdog)
attachInterrupt(digitalPinToInterrupt(wc_cold.PIN), &isr_wc_cold, FALLING);
attachInterrupt(digitalPinToInterrupt(wc_hot.PIN), &isr_wc_hot, FALLING);
timer_session = timerBegin(1, 80, true);
timerAttachInterrupt(timer_session, &onSessionTimer, true);
timerAlarmWrite(timer_session, 1000000, true); // every 1 second
timerAlarmEnable(timer_session);
EnableOTA();
//* async web
webServer.on("/", HTTP_GET, handleRootAsync);
webServer.on("/heap", HTTP_GET, heapInfoAsync);
webServer.on("/post", HTTP_POST, handlePostAsync);
webServer.on("/params", HTTP_GET, handleParameterPage);
webServer.onNotFound(onNotFoundAsync);
webServer.begin();
time(&now);
}
void loop() {
// put your main code here, to run repeatedly:
loop_task();
}
void loop_task()
{
if (time_every_second <= now)
{
writeWcTotalToDb(&wc_cold);
writeWcTotalToDb(&wc_hot);
time_every_second = now + 1;
}
if (time_every_30_seconds <= now)
{
pollNtp();
time_every_30_seconds = now + 30;
}
}
void IRAM_ATTR finishSession(WaterCounter *wc, time_t tt)
{
if (wc->session_started == 0)
{
return;
}
if (tt - wc->session_last_pulse > SESSION_LENGHT)
{
//finish session
#ifdef DEBUG_ISR
//Serial.printf("# %s liters in session: %d, duration %d(s)\n", wc->display_name, wc->liters_in_session, wc->session_last_pulse - wc->session_started);
ets_printf("# %s liters in session: %d, duration %d(s)\n", wc->display_name, wc->liters_in_session, wc->session_last_pulse - wc->session_started);
#endif
wc->session_started = 0;
wc->liters_in_session = 0;
}
}
/* every 1s */
void IRAM_ATTR onSessionTimer()
{
timerWrite(timerWD, 0); //reset timer (feed watchdog)
//ets_printf("\nonSessionTimer()\n");
time(&now);
getLocalTime(&timeinfo);
strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%d %H:%M:%S", &timeinfo);
if (now % 86400 == 75600) /* works if this function is called every second */
{
ets_printf("\n!!!midnight!!\n");
wc_cold.liters_used_today = 0;
wc_hot.liters_used_today = 0;
}
finishSession(&wc_cold, now);
finishSession(&wc_hot, now);
}
void writeWcTotalToDb(WaterCounter *wc)
{
#ifdef DEBUG_WRITE
Serial.printf("writeWcTotalToDb(%s): begin\n", wc->display_name);
#endif
uint8_t ldiff = wc->liters_total - wc->liters_last_written;
if (ldiff == 0)
{
#ifdef DEBUG_WRITE
Serial.println("writeWcTotalToDb(): diff zero");
#endif
return;
}
if (ldiff <= LITERS_BEFORE_WRITE && wc->session_started > 0)
{
#ifdef DEBUG_WRITE
Serial.println("writeWcTotalToDb(): ldiff <= LITERS_BEFORE_WRITE && wc->session_started > 0");
#endif
return;
}
#ifdef DEBUG_WRITE
Serial.println("writeWcTotalToDb(): start writting procedure!");
#endif
if (!openDb())
{
char *sql = "INSERT INTO `water_counter` VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
sqliteResultCode = sqlite3_prepare_v2(sqliteDb, sql, strlen(sql), &sqliteRes, NULL);
if (sqliteResultCode != SQLITE_OK)
{
#ifdef DEBUG
Serial.printf("writeWcTotalToDb(): ERROR preparing sql: %d -> %s\n", sqliteResultCode, sqlite3_errmsg(sqliteDb));
#endif
}else
{
#ifdef DEBUG
Serial.print("writeWcTotalToDb(): Prepare OK\n");
#endif
//getLocalTime(&timeinfo); // inside every 1 sec
//strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%d %H:%M:%S", &timeinfo);
sqlite3_bind_int(sqliteRes, 1, now); // datums
sqlite3_bind_int(sqliteRes, 2, wc->liters_total);
sqlite3_bind_text(sqliteRes, 3, timeStringBuff, strlen(timeStringBuff), SQLITE_STATIC);
sqlite3_bind_text(sqliteRes, 4, wc->display_name, strlen(wc->display_name), SQLITE_STATIC);
sqlite3_bind_int(sqliteRes, 5, 0);
sqlite3_bind_int(sqliteRes, 6, wc->liters_in_session);
sqlite3_bind_int(sqliteRes, 7, wc->session_started);
sqlite3_bind_int(sqliteRes, 8, 0); /* currently not used */
if (sqliteResultCode = sqlite3_step(sqliteRes) != SQLITE_DONE)
{
#ifdef DEBUG
Serial.printf("writeWcTotalToDb(): ERROR executing stmt: %d -> %s\n", sqliteResultCode, sqlite3_errmsg(sqliteDb));
#endif
}else
{
#ifdef DEBUG
Serial.print("writeWcTotalToDb(): data saved!\n");
#endif
wc->liters_last_written = wc->liters_total;
sqlite3_clear_bindings(sqliteRes);
sqliteResultCode = sqlite3_reset(sqliteRes);
if (sqliteResultCode != SQLITE_OK)
{
#ifdef DEBUG
Serial.printf("writeWcTotalToDb(): sqlite3_reset(res) result code = [%s] %s\n", sqliteResultCode, sqlite3_errmsg(sqliteDb));
#endif
}else
{
#ifdef DEBUG
Serial.print("writeWcTotalToDb(): sqlite3_reset(res) OK\n");
#endif
}
sqlite3_finalize(sqliteRes);
}
}
closeDb();
}else
{
#ifdef DEBUG
Serial.println("writeWcTotalToDb(): Failed to open database!");
#endif
}
#ifdef DEBUG_WRITE
Serial.printf("writeWcTotalToDb(%s): finished!\n", wc->display_name);
#endif
} /* writeWcTotalToDb() */
-
You should switch to using the "pulse counter" peripheral in the ESP32. Read more here: docs.espressif.com/projects/esp-idf/en/latest/esp32/…Majenko– Majenko06/02/2020 15:42:03Commented Jun 2, 2020 at 15:42
-
Trying it tomorrowGuntis– Guntis06/02/2020 16:01:25Commented Jun 2, 2020 at 16:01
-
@Majenko: can that be used with Arduino?dandavis– dandavis06/02/2020 21:42:04Commented Jun 2, 2020 at 21:42
-
a binary counter IC would make reliability easy, but if there's a built-in counter we can use, that's even better.dandavis– dandavis06/02/2020 21:44:05Commented Jun 2, 2020 at 21:44
-
Yes. Arduino used the IDF to control the chip. You'd just be skipping the Arduino layer and calling the IDF directly.Majenko– Majenko06/02/2020 21:56:08Commented Jun 2, 2020 at 21:56
2 Answers 2
As @Majenko suggested, i created solution with pulse counter.
Here is working code, based on espressif PCNT Example. Arduino studio, Olimex ESP32-POE board.
Each water meter uses separate pulse counter unit. Now I am testing this code and after several days i will accept answer if all works like i want :)
#include "freertos/queue.h"
#include "driver/pcnt.h"
#include "driver/periph_ctrl.h"
#include "driver/gpio.h"
#include "esp_attr.h"
#define PCNT_H_LIM_VAL 1
#define PCNT_L_LIM_VAL -1
#define PCNT_THRESH1_VAL 0
#define PCNT_THRESH0_VAL -0
#define PCNT_INPUT_SIG_WC_HOT 32 // hot water counter
#define PCNT_INPUT_SIG_WC_COLD 33 // cold water counter
/*
Cold meter uses PCNT_UNIT_1
hot meter uses PCNT_UNIT_0
*/
xQueueHandle pcnt_evt_queue; // A queue to handle pulse counter events
pcnt_isr_handle_t user_isr_handle = NULL; //user's ISR service handle
/* A sample structure to pass events from the PCNT
* interrupt handler to the main program.
*/
typedef struct {
pcnt_unit_t unit; // the PCNT unit that originated an interrupt
uint32_t status; // information on the event type that caused the interrupt
} pcnt_evt_t;
/* Decode what PCNT's unit originated an interrupt
* and pass this information together with the event type
* the main program using a queue.
*/
static void IRAM_ATTR pcnt_example_intr_handler(void *arg)
{
uint32_t intr_status = PCNT.int_st.val;
int i;
pcnt_evt_t evt;
portBASE_TYPE HPTaskAwoken = pdFALSE;
for (i = 0; i < PCNT_UNIT_MAX; i++) {
if (intr_status & (BIT(i))) {
evt.unit = (pcnt_unit_t)i;
/* Save the PCNT event type that caused an interrupt
to pass it to the main program */
evt.status = PCNT.status_unit[i].val;
PCNT.int_clr.val = BIT(i);
xQueueSendFromISR(pcnt_evt_queue, &evt, &HPTaskAwoken);
if (HPTaskAwoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
}
}
/* Initialize PCNT functions:
* - configure and initialize PCNT
* - set up the input filter
* - set up the counter events to watch
*/
static void pcnt_example_init()
{
//Serial.printf("pcnt_example_init(%d, %d)\n", _unit, _pin);
/* Prepare configuration for the PCNT unit */
pcnt_config_t pcnt_config = {};
pcnt_config.pulse_gpio_num = PCNT_INPUT_SIG_WC_HOT;
pcnt_config.ctrl_gpio_num = PCNT_PIN_NOT_USED;
pcnt_config.channel = PCNT_CHANNEL_0;
pcnt_config.pos_mode = PCNT_COUNT_INC;
pcnt_config.neg_mode = PCNT_COUNT_DIS;
pcnt_config.lctrl_mode = PCNT_MODE_KEEP;//PCNT_MODE_REVERSE;
pcnt_config.hctrl_mode = PCNT_MODE_KEEP;
pcnt_config.counter_h_lim = PCNT_H_LIM_VAL;
pcnt_config.counter_l_lim = PCNT_L_LIM_VAL;
pcnt_config.unit = PCNT_UNIT_0;
/* Initialize PCNT unit */
ESP_ERROR_CHECK(pcnt_unit_config(&pcnt_config));
/* cold water meter */
pcnt_config.pulse_gpio_num = PCNT_INPUT_SIG_WC_COLD;
pcnt_config.ctrl_gpio_num = PCNT_PIN_NOT_USED;
pcnt_config.channel = PCNT_CHANNEL_0;
pcnt_config.pos_mode = PCNT_COUNT_INC;
pcnt_config.neg_mode = PCNT_COUNT_DIS;
pcnt_config.lctrl_mode = PCNT_MODE_KEEP;//PCNT_MODE_REVERSE;
pcnt_config.hctrl_mode = PCNT_MODE_KEEP;
pcnt_config.counter_h_lim = PCNT_H_LIM_VAL;
pcnt_config.counter_l_lim = PCNT_L_LIM_VAL;
pcnt_config.unit = PCNT_UNIT_1;
ESP_ERROR_CHECK(pcnt_unit_config(&pcnt_config));
/* Configure and enable the input filter */
ESP_ERROR_CHECK(pcnt_set_filter_value(PCNT_UNIT_0, 1000)); // filter_val is a 10-bit value, so the maximum filter_val should be limited to 1023.
ESP_ERROR_CHECK(pcnt_filter_enable(PCNT_UNIT_0));
/* Set threshold 0 and 1 values and enable events to watch */
ESP_ERROR_CHECK(pcnt_set_event_value(PCNT_UNIT_0, PCNT_EVT_THRES_1, PCNT_THRESH1_VAL));
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_THRES_1));
ESP_ERROR_CHECK(pcnt_set_event_value(PCNT_UNIT_0, PCNT_EVT_THRES_0, PCNT_THRESH0_VAL));
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_THRES_0));
/* Enable events on zero, maximum and minimum limit values */
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_ZERO));
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_H_LIM));
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_L_LIM));
/* Initialize PCNT's counter */
ESP_ERROR_CHECK(pcnt_counter_pause(PCNT_UNIT_0));
ESP_ERROR_CHECK(pcnt_counter_clear(PCNT_UNIT_0));
/* Register ISR handler and enable interrupts for PCNT unit */
ESP_ERROR_CHECK(pcnt_isr_register(pcnt_example_intr_handler, NULL, 0, &user_isr_handle));
ESP_ERROR_CHECK(pcnt_intr_enable(PCNT_UNIT_0));
/* Everything is set up, now go to counting */
ESP_ERROR_CHECK(pcnt_counter_resume(PCNT_UNIT_0));
/* PCNT_UNIT_1 configuration */
/* Configure and enable the input filter */
ESP_ERROR_CHECK(pcnt_set_filter_value(PCNT_UNIT_1, 1000)); // filter_val is a 10-bit value, so the maximum filter_val should be limited to 1023.
ESP_ERROR_CHECK(pcnt_filter_enable(PCNT_UNIT_1));
/* Set threshold 0 and 1 values and enable events to watch */
ESP_ERROR_CHECK(pcnt_set_event_value(PCNT_UNIT_1, PCNT_EVT_THRES_1, PCNT_THRESH1_VAL));
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_1, PCNT_EVT_THRES_1));
ESP_ERROR_CHECK(pcnt_set_event_value(PCNT_UNIT_1, PCNT_EVT_THRES_0, PCNT_THRESH0_VAL));
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_1, PCNT_EVT_THRES_0));
/* Enable events on zero, maximum and minimum limit values */
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_1, PCNT_EVT_ZERO));
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_1, PCNT_EVT_H_LIM));
ESP_ERROR_CHECK(pcnt_event_enable(PCNT_UNIT_1, PCNT_EVT_L_LIM));
/* Initialize PCNT's counter */
ESP_ERROR_CHECK(pcnt_counter_pause(PCNT_UNIT_1));
ESP_ERROR_CHECK(pcnt_counter_clear(PCNT_UNIT_1));
ESP_ERROR_CHECK(pcnt_intr_enable(PCNT_UNIT_1));
/* Everything is set up, now go to counting */
ESP_ERROR_CHECK(pcnt_counter_resume(PCNT_UNIT_1));
}
void setup() {
Serial.begin(115200);
Serial.println("Boot...");
// put your setup code here, to run once:
pinMode(PCNT_INPUT_SIG_WC_HOT, INPUT_PULLUP);
pinMode(PCNT_INPUT_SIG_WC_COLD, INPUT_PULLUP);
/* Initialize PCNT event queue and PCNT functions */
pcnt_evt_queue = xQueueCreate(10, sizeof(pcnt_evt_t));
pcnt_example_init();
// pcnt_example_init(PCNT_UNIT_WC_HOT, PCNT_INPUT_SIG_WC_HOT);
//pcnt_example_init(PCNT_UNIT_WC_COLD, PCNT_INPUT_SIG_WC_COLD);
Serial.printf("Setup done\n");
}
int16_t count = 0;
pcnt_evt_t evt;
portBASE_TYPE res;
char * chToName(int ch)
{
if (ch == 1) return "Cold";
if (ch == 0) return "Hot";
return "Unc";
}
void loop() {
// put your main code here, to run repeatedly:
/* Wait for the event information passed from PCNT's interrupt handler.
* Once received, decode the event type and print it on the serial monitor.
*/
res = xQueueReceive(pcnt_evt_queue, &evt, 1000 / portTICK_PERIOD_MS);
if (res == pdTRUE) {
pcnt_get_counter_value(evt.unit, &count);
Serial.printf("Event PCNT unit[%d=%s]; cnt: %d, status: %d\n", evt.unit, chToName(evt.unit), count, evt.status);
if (evt.status & PCNT_EVT_THRES_1) {
Serial.printf("THRES1 EVT\n");
}
if (evt.status & PCNT_EVT_THRES_0) {
Serial.printf("THRES0 EVT\n");
}
if (evt.status & PCNT_EVT_L_LIM) {
Serial.printf("L_LIM EVT\n");
}
if (evt.status & PCNT_EVT_H_LIM) {
Serial.printf("H_LIM EVT\n");
}
if (evt.status & PCNT_EVT_ZERO) {
Serial.printf("ZERO EVT\n");
// here we are increment liter count
}
} else {
pcnt_get_counter_value(PCNT_UNIT_0, &count);
Serial.printf("Current counter value: %s=%d,", chToName(PCNT_UNIT_0), count);
pcnt_get_counter_value(PCNT_UNIT_1, &count);
Serial.printf(" %s=%d\n", chToName(PCNT_UNIT_1), count);
//pcnt_get_counter_value(PCNT_UNIT_WC_COLD, &count);
//Serial.printf(", %d on unit: %d\n", count, PCNT_UNIT_WC_COLD);
}
/*
if(user_isr_handle) {
Serial.printf("Clearing user_isr_handle\n");
//Free the ISR service handle.
esp_intr_free(user_isr_handle);
user_isr_handle = NULL;
}*/
}
You can use Pulse Counter(PCNT) feature in ESP32 to count the number of pulse in background, Its also possible to configure event when number of counts reached certain threshold and had lot of options,
For get information and available Interfaces and API's for Pulse Counter(PCNT) please follow below link, https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/pcnt.html
Initially I faced lot of issue to make Pulse Counter(PCNT) work in Adrino IDE for ESP-32, After multiple attempt I make it working, And same sample code is uploaded in GitHub for reference. I have not use all the API's in the official documentation but but used few of them and are working..
I have created a simple sample program for a water flow meter, there also we use to get pulse which needs to count to measure the water flow rate.
GitHub Sample code Path:- https://github.com/Embedded-Linux-Developement/Arduino_Sample_Programs/tree/main/ESP_32/Water_Flow_Pulse_counter_WithOut_Interrupt_Using_PCNT
I have not placing the code here, because its there in GitHub and a simile one and can use it. Its a working code I tested in ESP32 HW.
Hopes Its helpful, Regards, Jerry James