1

I have a counter attached to ISR:

void ISR_impulse() {
  CNT++;
}

and I want to calculate rolling count per minute with arbitrary update rate.

However I can only get it to update every minute:

void loop() {    
  if (t - t0 > 60000) {
    CPM = CNT - CNT0;
    t0 = t;
    CNT0 = CNT;      
  }

  t = millis();
  delay(100);
}

Requirement:

  • Rolling count with a window of 1 minute, and arbitrary update rate like 1 second ?
  • CPU has other tasks to do, which might take a significant amount of time.
  • SAMD21 has pulse period measurement in TC, but I have no idea how to set it up.

Partial solution one by edgar bonet:

  static uint8_t buf[60];
  static uint8_t idx;
  static uint32_t t0;
  static uint16_t CNT0;
  static uint8_t cpm;

  if (millis() - t0 >= 1000) {
    t0 = millis();
    uint16_t cps = CNT - CNT0;
    CNT0 = CNT;
    cpm += cps - buf[idx];
    buf[idx] = cps;
    if (++idx >= 60) idx = 0;
  }

How ever this only works if the loop code finishes in less than 1ms.

2
  • set up a FIFO buffer ... the ISR pushes decimal 60 into the buffer .... every second, a separate program routine decrements each of the values in the buffer by one, counts non-zero values and returns the count
    – jsotola
    Jun 23, 2020 at 17:16
  • 1
    While rewriting the code of my answer, you introduced a couple of errors: 1. By updating t0 with t0 = millis();, you make the tiny timing errors cumulative. If you update it with t0 += 1000; they are not cumulative: they only create a small amount of jitter, which is somewhat averaged-out by the subsequent running average. 2. By reading CNT twice with interrupts enabled, you create a race condition. If CNT is updated by the ISR between the two reads, you miss one pulse. Dec 31, 2021 at 9:00

2 Answers 2

2

If you want to update every second a one minute running average, you have no choice but store in memory the counts you got the previous 60 seconds. You store them in a ring buffer, so that every new reading replaces the oldest one. Then, every second:

  • you add to the CPM count the number of counts you got this second
  • you subtract the number of counts you got on the second that just got out of the sliding window.

I would implement that logic this way:

const uint32_t ONE_SECOND = 1000;

uint16_t counts_per_minute;
uint16_t previous_counts[60];
size_t counts_pos;

void loop() {
    static uint32_t previous_second;
    if (millis() - previous_second >= ONE_SECOND) {
        previous_second += ONE_SECOND;
        noInterrupts();
        uint16_t CNT_copy = CNT;
        interrupts();
        static uint16_t CNT_previous;
        uint16_t second_count = CNT_copy - CNT_previous;
        CNT_previous = CNT_copy;
        counts_per_minute += second_count - previous_counts[counts_pos];
        previous_counts[counts_pos] = second_count;
        if (++counts_pos >= 60) counts_pos = 0;
    }
}

Note that:

  • The timing variable is updated with previous_second += ONE_SECOND rather than previous_second = now in order to avoid the accumulation of small timing errors.
  • The counter that is updated in the ISR has to be read only once, with interrupts disabled. Otherwise the interrupt may change it while you are in the middle of reading it.
9
  • So it's a moving average with count-per-update-period as elements ? Wouldn't the noInterrupts() cause the counter to miss some counts ?
    – 7E10FC9A
    Jun 22, 2020 at 19:32
  • 1
    It would be perfect of you could add code to account for millis() < 60000 situation.
    – 7E10FC9A
    Jun 22, 2020 at 20:01
  • 1
    @7E10FC9A: noInterrupts() will not make you miss an interrupt. If an interrupt request fires while interrupts are disabled, it is put on hold and served right after the interrupts() call. Only if two instances of the same request fire within this period do you miss an interrupt. But since they are been disabled for less than half a microsecond, this could only happen with an interrupt rate way above what the Arduino can handle. Jun 22, 2020 at 20:32
  • 1
    @7E10FC9A: If you program in a non blocking fashion, your loop() should always execute very fast. If it takes more than a millisecond, you are most likely doing something wrong. With a very fast loop(), the inaccuracies will be tiny, and likely irrelevant. If your accuracy requirement is really so high that a fraction of a millisecond does matter, then program a timer to fire an interrupt every millisecond, and update counts_per_minute within the ISR. Dec 30, 2021 at 14:23
  • 1
    @7E10FC9A: In such a case I would use a timer interrupt. My point is that it is uncommon to actually need 1 ms of CPU time at once. If loop() takes more than 1 ms, it is more than likely that most of these CPU cycles are waisted delaying or busy-waiting for some event (i.e. “blocking”). Then you can make it non blocking by using a finite state machine and/or the technique shown in the Blink Without Delay tutorial. Dec 31, 2021 at 8:37
0

You need a First-In, First-Out buffer (FIFO) so that the oldest data drops off to make room for the newest. This is usually done with a circular buffer - an array whose beginning / end point change as data is added. (For comparison, the simplistic way is an array whose contents gets moved down (toward i = 0) each time you need to add a datum. But moving data is slow, so remembering where the oldest entry is, replacing it with the newest one, and updating the remembered oldest-data location gives a huge permormance improvement.

Secondly, you need to define how responsive your counter needs to be - how many updates per minute you'll need. That's the number of elements you need in the array.

Evaluating the immediately-passed minute's count is as simple as summing the array, which can be done without caring where the begin/end point is. Sum from 0 to N-1; the result will be the same.

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.