A water pulse meter using interrupts and the ATMEGA328P "power save" mode

  • Hello everyone!

    I'm back here with an exciting project and an exciting issue ๐Ÿ˜› , after my post regarding the "pilot wire" (which works well by the way)!

    First, a bit of context, as usual:
    My new goal is know how much water is consumed in the house, still via HomeAssistant. The specifications I imposed to myself:

    1. The node will be powered on batteries. It should consume the minimum amount of power and therefore sleep as much as possible.
    2. I would like to send the amount of water consumed "reliably" every 5 minutes to avoid spamming the gateway and to save energy. And 5 minutes, no more (>= 5'01"), no less (<= 4'59").

    Similarly to my previous project, I am not using any Arduino board. Only an ATMEGA328P using its own internal 8MHz clock.

    To measure the amount of water I am using, I chose to buy a second-hand two-wires "Itron Cyble sensor", where "K=1". It is a very basic sensor which outputs a "signal equivalent to a dry contact signal (e.g. reed switch)".
    More information about it can be found in its brochure: https://www.itron.com/-/media/feature/products/documents/brochure/cyble-sensor-water-brochure-english.pdf

    To consume as less energy as possible, the circuit must be interrupt-driven. I can see two interrupts here:

    • On the water sensor side: whenever the sensor simulates a pushed button (meaning I've consumed a liter of water), this has to increment a variable. Once done, the chip should go back to sleep.

    • To measure 5 minutes: I've spent quite a bit of time searching for solutions to reliably measure 5 minutes, without using external components (an RTC for instance).
      It seems I wasn't the only one to wonder how to have interrupts and a timer and I found this post for instance: https://forum.mysensors.org/topic/7263/remainder-sleep-time-upon-an-interrupt. However, I didn't like the solution given, which didn't look elegant to me...
      Moreover, the sleep() function provided by the MySensors API wakes the radio up after sleeping and I don't need waking it up when I simply have to increment the "water consumed" variable!

    And then, I discovered that the ATMEGA328P can run an asynchronous timer (called Timer2) and trigger interrupts on overflows / compare matches. But the really nice thing here is that this timer continues running in the power saving mode "SLEEP_MODE_PWR_SAVE", if a 32.768 kHz quartz is connected!

    Where I am right now:
    Right now, I have finished wiring the circuit on a breadboard and developing the code. Regarding the wiring, I have:

    • An ATMEGA328P flashed with my code (with encryption enabled) and using its internal 8 MHz clock to operate.
    • An RFM69W radio connected to the chip.
    • A button connected to the arduino-equivalent pin #3 (pin 2 is used by the RFM69W radio, DI0), to simulate my Itron sensor.
    • A 32.768 kHz quartz connected on the XTAL pins of the ATMEGA.
    • A few other capacitors here and there, where they are needed.

    Now, here is the code of my sketch:

    // MySensors configuration
    #define MY_RADIO_RFM69
    #define MY_RFM69_NEW_DRIVER
    // Include the libraries
    #include <avr/power.h>
    #include <avr/sleep.h>
    #include <MySensors.h>
    // Global settings
    #define CHILD_ID 0
    // Global variables
    volatile bool time_elapsed = true;
    volatile int liters_consumed = 0;
    MyMessage measure_msg(CHILD_ID, V_VOLUME);
    // Interrupt triggered on Timer2 overflow
        // variable to count the number of overflows
        static byte timer2_ovfs = 0;
        // Increment the counter
        // Set the flag and reset the counter
        if (timer2_ovfs >= 40) {
            time_elapsed = true;
            timer2_ovfs = 0;
    void sensor_interrupt() {
    void presentation() { 
        sendSketchInfo("Compteur d'eau", "1.0");
        present(CHILD_ID, S_WATER);
    void setup() {
        // Setup Timer2
        ASSR = (1<<AS2);                        // Make Timer2 asynchronous
        TCCR2A = (1<<WGM21);                    // CTC mode
        TCCR2B = (1<<CS22)|(1<<CS21)|(1<<CS20); // Prescaler of 1024
        OCR2A = 239;                            // Count up to 240 (zero relative!)
        TIMSK2 = (1<<OCIE2A);                   // Enable compare match interrupt
        // Setup interrupt on pin (INT1)
        pinMode(3, INPUT);
        attachInterrupt(digitalPinToInterrupt(3), sensor_interrupt, RISING);
        // Disable unneeded peripherals
        power_adc_disable(); // Analog to digital converter
        power_twi_disable(); // I2C
        // Set the sleep mode as "Power Save"
        // Enable interrupts
    void loop() {
        // The 5 minutes period has passed
        if(time_elapsed) {
            // Wake up the radio
            // Send the value
            // Reset the timer
            time_elapsed = false;
            // Make the radio sleep
        // Go back to sleep

    The problem:
    However, there is a problem right now (otherwise, I wouldn't post in the "troubleshooting" section ๐Ÿ˜› ).

    On the gateway side, I can see that the node successfully registers and gets its ID. But for an unknown reason, after a few seconds, it keeps sending data as if time_elapsed never changed to false. And it never goes to sleep.
    Here are the gateway logs after startup:

    Aug 18 17:01:05 INFO  Starting gateway...
    Aug 18 17:01:05 INFO  Protocol version - 2.3.1
    Aug 18 17:01:05 DEBUG Serial port /dev/ttyMySensorsGateway (115200 baud) created
    Aug 18 17:01:05 DEBUG MCO:BGN:INIT GW,CP=RPNGLS-X,REL=255,VER=2.3.1
    Aug 18 17:01:05 DEBUG TSF:LRT:OK
    Aug 18 17:01:05 DEBUG TSM:INIT
    Aug 18 17:01:05 DEBUG TSF:WUR:MS=0
    Aug 18 17:01:05 DEBUG TSM:INIT:TSP OK
    Aug 18 17:01:05 DEBUG TSM:INIT:GW MODE
    Aug 18 17:01:05 DEBUG TSM:READY:ID=0,PAR=0,DIS=0
    Aug 18 17:01:05 DEBUG MCO:REG:NOT NEEDED
    Aug 18 17:01:05 DEBUG MCO:BGN:STP
    Aug 18 17:01:05 DEBUG MCO:BGN:INIT OK,TSP=1
    Aug 18 17:01:05 DEBUG TSM:READY:NWD REQ
    Aug 18 17:01:05 DEBUG TSF:MSG:SEND,0-0-255-255,s=255,c=3,t=20,pt=0,l=0,sg=0,ft=0,st=OK:
    Aug 18 17:01:06 DEBUG TSF:MSG:READ,2-2-0,s=255,c=3,t=21,pt=1,l=1,sg=1:0
    Aug 18 17:01:15 DEBUG TSF:MSG:READ,3-3-255,s=255,c=3,t=7,pt=0,l=0,sg=0:
    Aug 18 17:01:15 DEBUG TSF:MSG:BC
    Aug 18 17:01:15 DEBUG TSF:MSG:FPAR REQ,ID=3
    Aug 18 17:01:15 DEBUG TSF:PNG:SEND,TO=0
    Aug 18 17:01:15 DEBUG TSF:CKU:OK
    Aug 18 17:01:15 DEBUG TSF:MSG:GWL OK
    Aug 18 17:01:16 DEBUG TSF:MSG:SEND,0-0-3-3,s=255,c=3,t=8,pt=1,l=1,sg=0,ft=0,st=OK:0
    Aug 18 17:01:17 DEBUG TSF:MSG:READ,3-3-0,s=255,c=3,t=24,pt=1,l=1,sg=0:1
    Aug 18 17:01:17 DEBUG TSF:MSG:PINGED,ID=3,HP=1
    Aug 18 17:01:17 DEBUG TSF:MSG:SEND,0-0-3-3,s=255,c=3,t=25,pt=1,l=1,sg=0,ft=0,st=OK:1
    Aug 18 17:01:17 DEBUG TSF:MSG:READ,3-3-0,s=255,c=3,t=15,pt=6,l=2,sg=0:0100
    Aug 18 17:01:18 DEBUG TSF:MSG:SEND,0-0-3-3,s=255,c=3,t=15,pt=6,l=2,sg=0,ft=0,st=OK:0100
    Aug 18 17:01:18 DEBUG TSF:MSG:READ,3-3-0,s=255,c=0,t=17,pt=0,l=5,sg=0:2.3.1
    Aug 18 17:01:18 DEBUG TSF:MSG:READ,3-3-0,s=255,c=3,t=6,pt=1,l=1,sg=0:0
    Aug 18 17:01:18 DEBUG TSF:MSG:SEND,0-0-3-3,s=255,c=3,t=6,pt=0,l=1,sg=0,ft=0,st=OK:M
    Aug 18 17:01:18 DEBUG TSF:MSG:READ,3-3-0,s=255,c=3,t=11,pt=0,l=14,sg=0:Compteur d'eau
    Aug 18 17:01:18 DEBUG TSF:MSG:READ,3-3-0,s=255,c=3,t=12,pt=0,l=3,sg=0:1.0
    Aug 18 17:01:18 DEBUG TSF:MSG:READ,3-3-0,s=0,c=0,t=21,pt=0,l=0,sg=0:
    Aug 18 17:01:18 DEBUG TSF:MSG:READ,3-3-0,s=255,c=3,t=26,pt=1,l=1,sg=0:2
    Aug 18 17:01:18 DEBUG TSF:MSG:SEND,0-0-3-3,s=255,c=3,t=27,pt=1,l=1,sg=0,ft=0,st=OK:1
    Aug 18 17:01:18 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:34 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:36 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:37 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:38 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:39 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:41 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:42 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:43 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:0
    Aug 18 17:01:44 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:1
    Aug 18 17:01:46 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:3
    Aug 18 17:01:47 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:7
    Aug 18 17:01:48 DEBUG TSF:MSG:READ,3-3-0,s=0,c=1,t=35,pt=2,l=2,sg=0:7

    As you can see, a few seconds after registering, the node spams the gateway, sending the liters_consumed variable every second.

    Do you guys know why? What can be wrong in this code?
    Also, does the MySensor library generate other kind of interrupts?

    Thanks in advance for your help!


    Appendix / bonus: how I get the 5 minutes period.

    As mentioned, a 32.768 kHz quartz generates 32,768 clock cycles per second.

    The clock prescaler has been set to 1024, meaning that I need 1024 clock cycles to add "1" to Timer2. This means that each second, 32 is added to timer2.

    The "output compare register A" was set to 239, meaning that it will trigger an "Output Compare A interrupt" every time the counter reaches 239 (starting from 0 in CTC mode). So, such an interrupt will be triggered every 240 / 32 = 7.5s, waking up the microcontroller from sleep mode and adding 1 to the timer2_ovfs variable.

    Finally, when I reach 40 output compare interrupts, I'm (supposed) to send the data to the controller. Since 7.5s * 40 = 300s = 5 minutes! ๐Ÿ™‚

  • If you are using watch crystal (32khz) connected to TOSC1/2, I have an "rtc" one second tick counter you can use (from an unrelated project):

    You can extend this to different tick periods with different prescalers.

    But i think any interrupt will bring mysensors out of wait? Which means you will need to enter sleep yourself, and decide whether to go back to sleep or hand control back to main loop for mysensors to do reporting thing.

    Here are some examples i put together for directly controlling entering/exiting low-power sleep:

    You can also use watchdog timer as (less accurate) wakeup timer.
    (But you said you need more exact, <1 sec, accuracy to send at [exactly] 5 sec boundaries, which means you'll need timer2/rtc/watch crystal for that level of accuracy)

    You can use a separate counter, e.g. timer1 in counter mode to capture sensor pulses as an external clock for the counter. You probably only need interrupt on counter overflow, so you can capture overflow counts. Then when you wake up on an interval and check the number of pulses you can evaluate overflows + remaining counter value, and determine number of pulses since previous wakeup, etc. (without needing wakeup/cpu to keep track of individual pulses)

  • also, declaring static var inside isr is strange to me (maybe it works?) but I have always seen that you should declare any vars to be manipulated in isr to be global and volatile

  • Hello @Jens-Jensen!

    Thanks for your answer, I'm asking a kind of tricky question here it seems! ๐Ÿ˜›

    Thanks for your link regarding the RTC project.
    I actually more or less based my code on the Sparkfun's BigTime watch project (https://www.sparkfun.com/products/11734) in which they handle interrupts the same way.

    As I explained in my first post, I'm indeed generating a Timer2 compare interrupt every 7.5s, waking up the microcontroller, making it add 1 to the overflows count and then it is (supposed) to go back to sleep. But it doesn't, for some reason.

    I may have found a clue regarding my problem, after spending more time Googling. There may be some kind of method to call before entering into the AVR sleep mode, to make the "MySensors state machine" aware of that.
    Here is the link about the post I found: https://forum.mysensors.org/topic/9590/timer2-does-not-work-after-including-mysensors-h

    @scalz may have an idea about my issue if he is around? He seems to have a very good knowledge about what's happening under the hood...


  • Also, regarding the static keyword in the ISR, it should be working fine.

    I first played with the asynchronous mode of Timer2 before integrating MySensors, the code successfully toggled an LED every 5 minutes. ๐Ÿ™‚

  • @encrypt K=1 means a pulse every litre, the Itron may be a fet type head rather than a reed..
    No idea why you are doing this at the Node, the increased wakeups will reduce battery run-time IMHO... The 2xAA alkaline powered (pro-mini) water meter Node here is still at 2.8v after nearly 18 months, but that is on a boosted supply sucking the batteries to below 2v (ie dead). It is on a fet triggered Class D meter on the domestic supply, the updated value sent on every litre passing, the Controller does the rest, in this case Domoticz, no idea on HomeAssistant.
    Admitedly this summates the readings into hourly volumetric consumptions, but there are other ways to sift the data (NodeRed?) if it is essential to have finer time based data.

  • @Encrypt
    yes, interesting... i also had strange issues trying to implement rtc first time, but it was due to arduino lib mucking with Timer2 registers (due to Tone libs). The answer was to make sure all registers were not assumed to be default, but explicitly set in setup(). You seem to have that here.
    I did a quick scan of mysensors repo code, but I don't see where it is touching any of Timer2's registers, so not sure.
    Did you toggle led/pin in the current code's ISR, to be 100% sure your the issue is that the isr is getting triggered at unexpected interval? (maybe sanity check that with scope.) That should bifurcate the problem either to the timer implementation/configuration, or something else.

    @zboblamont I also agree here. But it should be totally possible to setup Timer1 as externally clocked counter, i.e. pulse from meter can increment Timer1 counters without interrupting CPU from sleep, and only interrupt and increment another counter variable on Timer1 overflow (if it happens) so you can be sure you have complete, accurate count when you wake up from your long sleep to do your accounting and reporting.

    Briefly looking at the datasheet for the water sensor, I didn't get a sense of what the unit of water volume/flow a single HF pulse means. Depending on what this unit is, and how often you wakeup and check the T1 counter value, it may not even be necessary to bother with handling counter overflow, e.g. pulses = 1 liter; would you ever consume >65kL in <10 mins?
    In this case, you only simply need to worry about waking up from long sleep interval, polling and clearing counter, sending, and going back to sleep.

  • Hello everyone!

    @zboblamont: well, I guess that sending the number of liters consumed "in batch" should consume less energy, since it reduces the number of radio wake-ups and transmissions.
    According to the datasheet:

    • The ATMEGA328P "typically" consumes 5.2 mA in Active mode.
    • The RFM69W consumes 45 mA (when RFOP = 13 dBm) when it transmits data

    So I believed that doing so was a better option.

    Also, the SparkFun documentations regarding the BigTime Watch Kit (which basically does what I am trying to do) states that: ยซ Thanks to some low-level hackery, the ATMega is running at super low power and should get an estimated 2 years of run time on a single CR2032 coin cell! ยป.

    @Jens-Jensen: I read somewhere that:

    • Timer0 is used for functions like delay(), millis()...
    • Timer1 is used for sound generation
    • Timer2 isn't used by defaults in Arduino libraries

    So if the MySensors library uses time somewhere, it probably uses Timer0. So it should be safe playing with Timer2 ๐Ÿ™‚
    Regarding my tests, I toggled LEDs both in the ISR and in the loop() function.

    I've had a closer look at how MySensors implements the sleep() function.
    It seems that @tekka is the one that essentially worked on it according to the history of the code.
    So, @tekka, if you're around, would you have an idea? ๐Ÿ˜›

  • So this is where I see for atmega 168/328, Tone lib is modding Timer2:
    So that's why it's important to explicitly set every Timer2 register.

    Reviewing AVR130 app note, I think I was incorrect about being able to sleep and externally clock T1 (to count pulses), as T0/T1 seem to be synchronous, meaning they require the system (i/o) clock to be running to latch in the edges of external source, and only IDLE power save mode supports running the clk_io. This mode will use way more current, e.g. w/ internal rc osc at 8MHz at 3.3v about 700uA.

    Seems that only Timer2 can allow an "asynchronous" external clocking event (pulse counting) with cpu clocks off (in very low power modes).

    Just blabbering, yet another aproach could be to, instead of bothering with RTC crystal on Timer2:

    1. use watchdog timer as a wakeup (interrupt only) source, for either an approximately 4 or 8 second interval (5 mins = 300 secs, 300 / 4 = 75 wakeups)
    2. use Timer2 as external event counter to count the pulses (without interrupt/wakeup?)

    regarding the mysensors lib, I'm not an expert, but reviewing the code, seems like you covered it:
    mysensors sleep overloads call _sleep in MySensorsCore.cpp
    which call transportDisable() like you do then call hwSleep() variants in MyHwAVR.cpp (depending upon how you call sleep, e.g. with pin interrupt setups or not), which eventually call hwPowerDown()
    which seems to do pretty much what you are doing (maybe a few other things, etc..) to call sleep. ref: https://www.microchip.com/webdoc/AVRLibcReferenceManual/group__avr__sleep.html

    So I dont really see any additional state machine stuff happening w.r.t. MySensors itself.

  • This post is deleted!

  • @encrypt said in A water pulse meter using interrupts and the ATMEGA328P "power save" mode:

    well, I guess that sending the number of liters consumed "in batch" should consume less energy, since it reduces the number of radio wake-ups and transmissions.

    I thought similarly but practice demonstrates minimal if any measurable saving, ignoring the dual method wakeup you are examining. Have not looked into how the radio is handled (rfm69), but battery life is astonishing on the MySensors sleep (dominant condition).
    eg - The gas node was initially set up to accumulate 5 pulses before updating the gateway, the water node does so every pulse.
    There was no appreciable difference in battery decay over the winter months when both were running regularly, and the water node is over 92,000 notifications in on the same 2xAA, but this is a higher mAh than a CR2032.

  • @jens-jensen said in A water pulse meter using interrupts and the ATMEGA328P "power save" mode:

    Briefly looking at the datasheet for the water sensor, I didn't get a sense of what the unit of water volume/flow a single HF pulse means. Depending on what this unit is, and how often you wakeup and check the T1 counter value, it may not even be necessary to bother with handling counter overflow, e.g. pulses = 1 liter; would you ever consume >65kL in <10 mins?
    In this case, you only simply need to worry about waking up from long sleep interval, polling and clearing counter, sending, and going back to sleep.

    The K value is an industry standard usually printed on gas and water meters for volume passing per pulse. eg - On my treated water Elster is stamped "K1" with a FET head, the outside raw water Zenner is "100L/imp" but not currently fitted with a reed, the gas meter has "1imp=0.01m3" with a reed. The gas meter threw me with interrupts until a continuity check revealed the reed was on for ca 6 seconds in full flow, therafter handled by a timer loop/sleep until it changed state, then going deep sleep.
    I get the curiosity aspect over timers, simply not the reasons for using a specific form at the Node. As I understood it, Sleep and timing are in reality low energy loops of the CPU, the interrupt triggered by the sensor is what begins the higher energy process, hence maximum Sleep = long battery life.
    Your Controller will usually expect a cumulative value (eg Domoticz), the only unforeseen I encountered was when the cumulative total incremented and overflowed giving Domoticz a brainfart. Changing the numeric type (long?) sent by the node rectified that, and is now well beyond the 65k..

  • Mod

    Chipping in for @tekka ๐Ÿ˜‰

    @encrypt said in A water pulse meter using interrupts and the ATMEGA328P "power save" mode:

    On the gateway side, I can see that the node successfully registers and gets its ID. But for an unknown reason, after a few seconds, it keeps sending data as if time_elapsed never changed to false. And it never goes to sleep.

    Please be aware that sleeping on interrupts is rather tricky to get right!
    When going to sleep, pending interrupts will immediately wake the AVR again. See the MySensors code from https://github.com/mysensors/MySensors/blob/development/hal/architecture/AVR/MyHwAVR.cpp#L183 on.

    In your case pending interrupts are very likely, as you use a mechanical button that hasn't been debounced.

    Furthermore you are using MySensors internal functions that might work for now, but are not guaranteed to work in the future.
    I would suggest to stick to the official API whenever possible.

    MySensors development branch offers the ability to sleep for a fixed time and report remaining sleep time. Your node should just go back to sleep until 5 minutes have passed; see my post here: https://forum.mysensors.org/topic/9595/interrupted-sleep/11
    Only drawback on AVR is the usage of the watchdog as a sleep timer, which makes it impossible to exactly get 5 minutes sleep interval.

  • I think mysensors sleep function is not suitable for this purpose.
    Here is example from avr/sleep.h:

      #include <avr/interrupt.h>
      #include <avr/sleep.h>
        if (some_condition)