0

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() */
7
  • 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
    Commented Jun 2, 2020 at 15:42
  • Trying it tomorrow
    – Guntis
    Commented Jun 2, 2020 at 16:01
  • @Majenko: can that be used with Arduino?
    – dandavis
    Commented 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
    Commented 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
    Commented Jun 2, 2020 at 21:56

2 Answers 2

1

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

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.