Arduino Uno (ATmega328) Input Capture Unit Example

Hi Everyone,

My post is to big, so I’ll just link to the code
http://epccs.org/hg/epccs/Software/file/tip/Embeded/Capture

and post one of the files

InputCapture.ino

/*
    This file (InputCapture.ino) is part of Capture.
    
    Timer 1 Input Capture, Arduino Uno pin 8 ICP1 facility, 
    referance
    https: //gist.github.com/mpflaga/4404996
    http://forum.arduino.cc/index.php/topic,146497.0.html
    note: volatile will force compiler to load/store variable every time it is used.
    
    This capture method will skip pulses if the duty is about 300 counts (or less), 
    about 19%@10kHz or 1.9%@1kHz (assuming a 16MHz MCU).
    The counter is 32 bits 
    
    command format for rising (falling) edge to rising (falling) edge period count
    period? [rise|fall]

    pulse duty measured in clock counts. l2h is inactive time, falling edge to rising edge. h2l is active time, rising edge to falling edge
    duty? [h2l|l2h]

    command format for channel pulse count of current channel, read befor changing channel
    count?

*/
#include "Arduino.h"
#include <avr/interrupt.h>
 
volatile uint8_t rising; //true is rising edge that will cause capture event, false is falling edge
 
// ring or circular buffer for capture events http://en.wikipedia.org/wiki/Circular_buffer
#define MAX_EVENT_BUFF 4
volatile uint8_t ring; // head of ring buffer, don't need a tail, use active

typedef struct {
      uint32_t event;
      uint32_t duty;
      uint32_t period;
      uint8_t rising;
      uint8_t active;
  }  capture;

volatile capture icp[MAX_EVENT_BUFF];

uint32_t chCount; // ch pulse count
//int32_t sumt, bogon_count;
volatile union twoword { uint32_t dword; uint16_t word[2]; } t1vc;   // timer 1 virtual counter

// Interrupt capture handler
//
ISR(TIMER1_CAPT_vect) {
  union twobyte { uint16_t word; uint8_t byte[2]; } timevalue;
  
  timevalue.byte[0] = ICR1L;	// grab captured timer1 low byte
  timevalue.byte[1] = ICR1H;	// grab captured timer1 high byte
  t1vc.word[0] = timevalue.word;

  // put next timestamp onto the ring buffer
  ring++;
  if (ring >= MAX_EVENT_BUFF) ring = 0;
  icp[ring].event = t1vc.dword;
  icp[ring].rising = rising;
  icp[ring].active = true;
  if (rising) {
    chCount++;
  }
  if (chCount > MAX_EVENT_BUFF) { // buff is full of new readings
    if (ring == 0) { // cast to int uses two's complement math giving correct result at roll over 
      icp[ring].duty = (int32_t)icp[ring].event - (int32_t)icp[MAX_EVENT_BUFF-1].event;
      icp[ring].period = (int32_t)icp[ring].event - (int32_t)icp[MAX_EVENT_BUFF-2].event;
    }  
    if (ring == 1) {
      icp[1].duty = (int32_t)icp[ring].event - (int32_t)icp[ring - 1].event;
      icp[1].period = (int32_t)icp[ring].event - (int32_t)icp[MAX_EVENT_BUFF-1].event;
    }
    if (ring > 1) {
      icp[ring].duty = (int32_t)icp[ring].event - (int32_t)icp[ring - 1].event;
      icp[ring].period = (int32_t)icp[ring].event - (int32_t)icp[ring - 2].event;
    }
  } else {
    icp[ring].duty = 0;
    icp[ring].period = 0;
  }
  // rising or falling edge tracking
  // 328 datasheet said to clear ICF1 after edge direction change 
  // switch edge setup to catch duty
  // setup to catch rising edge: TCCR1B |= (1<<ICES1); TIFR1 |= (1<<ICF1); rising = 1;
  // setup to catch falling edge: TCCR1B &= ~(1<<ICES1); TIFR1 |= (1<<ICF1); rising = 0;
  if (rising) { TCCR1B &= ~(1<<ICES1); TIFR1 |= (1<<ICF1); rising = 0; }
  else {TCCR1B |= (1<<ICES1); TIFR1 |= (1<<ICF1); rising = 1;}
}

// Virtual timer counts 2^32 clocks (about 4.3E9, thus events must be less than 4.4 minutes apart) 
// Maintain the high order 16 bits here by incrementing the virtual timer
// when timer1 overflows. The low order 16 bits are captured above.
//
ISR(TIMER1_OVF_vect) {
  ++t1vc.word[1];
}

void initCapture(void) {
  // Input Capture setup
  // ICNC1: Enable Input Capture Noise Canceler
  // ICES1: =1 for trigger on rising edge
  // CS10: =1 set prescaler to 1x system clock (F_CPU)
  TCCR1A = 0;
  TCCR1B = (0<<ICNC1) | (0<<ICES1) | (1<<CS10);
  TCCR1C = 0;
   
  // initialize to catch Falling Edge
  { TCCR1B &= ~(1<<ICES1); TIFR1 |= (1<<ICF1); rising = 0; }
   
  // Interrupt setup
  // ICIE1: Input capture
  // TOIE1: Timer1 overflow
  TIFR1 = (1<<ICF1) | (1<<TOV1);	// clear pending interrupts
  TIMSK1 = (1<<ICIE1) | (1<<TOIE1);	// enable interupts

  // Set up the Input Capture pin, ICP1, Arduino Uno pin 8
  pinMode(8, INPUT);
  digitalWrite(8, 0);	// floating may have 60 Hz noise on it.
  //digitalWrite(8, 1); // or enable the pullup
}

//period is the time it takes a signal to repeat
void Period() {
  if((argumentCount(findArgument()) == 1)) {
    String arg1;
    arg1 = command.substring(argOffset,argOffsetEnd);
    uint32_t val=0;
    boolean found =false;
    if (chCount > MAX_EVENT_BUFF) { // buff is full of new readings
      uint8_t oldsreg = SREG;     // save global interrupt flag
      cli();                      // clear global interrupts
      if(arg1.equalsIgnoreCase("rise")){
        uint8_t i = 0;
        if (!icp[i].rising) {
          i++;
        }  
        val = icp[i].period;
        found =true; 
      }
      if(arg1.equalsIgnoreCase("fall")) {
        uint8_t i = 0;
        if (icp[i].rising) {
          i++;
        }
        val = icp[i].period;
        found =true; 
      }
      if (found) chCount = 0;
      SREG = oldsreg;             // restore global interrupts
    } else {
      char c;
      for(int i = 0; c = pgm_read_byte(&(errNoCapture[i])); i++) { // + "ERR no-capture\n"
        Serial.write(c);
      }
      c = pgm_read_byte(&(comment[2])); // + "\n"
      Serial.write(c);
    }
    if (found) {
      char c;
      Serial.print (String(val,DEC));
      c = pgm_read_byte(&(comment[2])); // + "\n"
      Serial.write(c); 
    }
  }
}
  
//duty cycle is the percentage of a period a signal is active
void Duty() {
  if((argumentCount(findArgument()) == 1)) {
    String arg1;
    arg1 = command.substring(argOffset,argOffsetEnd);
    uint32_t val=0;
    boolean found =false;
    if (chCount > MAX_EVENT_BUFF) { // buff is full of new readings
      uint8_t oldsreg = SREG;     // save global interrupt flag
      cli();                      // clear global interrupts
      if(arg1.equalsIgnoreCase("h2l")){
        uint8_t i = 0;
        if (!icp[i].rising) { //falling edge is high to low
          i++;
        }  
        val = icp[i].duty;
        found =true; 
      }
      if(arg1.equalsIgnoreCase("l2h")) {
        uint8_t i = 0;
        if (icp[i].rising) {
          i++;
        }
        val = icp[i].duty;
        found =true; 
      }
      if (found) chCount = 0;
      SREG = oldsreg;             // restore global interrupts
    } else {
      char c;
      for(int i = 0; c = pgm_read_byte(&(errNoCapture[i])); i++) { // + "ERR no-capture\n"
        Serial.write(c);
      }
      c = pgm_read_byte(&(comment[2])); // + "\n"
      Serial.write(c);
    }
    if (found) {
      char c;
      Serial.print (String(val,DEC));
      c = pgm_read_byte(&(comment[2])); // + "\n"
      Serial.write(c); 
    }
  }
}

void Count(){
  if((argumentCount(findArgument()) == 0)) {
    uint8_t oldsreg = SREG;     // save global interrupt flag
    cli();                      // clear global interrupts
    uint32_t val=chCount;
    SREG = oldsreg;             // restore global interrupts
    char c;
    Serial.print (String(val,DEC));
    c = pgm_read_byte(&(comment[2])); // + "\n"
    Serial.write(c); 
  }
}

Thanks

Just to make the code much more light, faster and so working on the whole range of PWM from 1 to 254.

Nota, I was unable to use reliably virtual counter incremented by timer1 overflow.

I believe interrupt sometimes conflict together and I get an old value for virtual counter mixed with new value for timer1. Probably it’s possible to avoid this by inhibit/authorise interrupt correctly but I believe 16 bit is more than enough for PWM accuracy so better remove virtual counter.

Prescaler can be used to adapt to the right PWM base frequency.

Edit1: Just added a little trick to be able to handle from PWM 0 to 255.

Edit2: Double the size of ring buffer so acquisition of new timestamp for incoming pulses can continue during reading of timestamp without overwriting data being processed. It’s not necessary to inhibit interrupts anymore. Ultra-short pulses can be catched.

Edit3: Remove one stupid pesky bug.

#include "Arduino.h"
#include <avr/interrupt.h>

// circular buffer for capture events
#define   MAXR      8     // must be a power of 2. 4/8/16 ...
volatile  uint8_t   ring;
volatile  int8_t    prot;
volatile  uint16_t  icp[MAXR];

volatile  uint16_t  pulseCount;
volatile  uint8_t   intCount, edgeCount;

void initCapture(void)
{
  // Input Capture setup
  // ICNC1: Enable Input Capture Noise Canceler
  // ICES1: =1 for trigger on rising edge
  // CS12 CS11 CS10
  //   0    0    1  : /1    No prescaler, CPU clock
  //   0    1    0  : /8    prescaler
  //   0    1    1  : /64   prescaler
  //   1    0    0  : /256  prescaler
  //   1    0    1  : /1024 prescaler

  TCCR1A = 0;
  TCCR1B = (1<<ICNC1) | (0<<ICES1) | (1<<CS11);
  TCCR1C = 0;

  // initialize to catch Falling Edge
  { TCCR1B &= ~(1<<ICES1); TIFR1 |= (1<<ICF1); }
  ring = MAXR-1; // so ring+1 -> MAXR -> 0
  intCount = 0;
  edgeCount = 0;

  // Interrupt setup
  // ICIE1: Input capture
  // TOIE1: Timer1 overflow
  TIFR1 = (1<<ICF1) | (1<<TOV1);    // clear pending interrupts
  TIMSK1 = (1<<ICIE1) | (1<<TOIE1); // enable interupts

  // Set up the Input Capture pin, ICP1, Arduino Uno pin 8
  pinMode(8, INPUT);
  digitalWrite(8, 0); // floating may have 50 Hz noise on it.
  //digitalWrite(8, 1); // or enable the pullup
}


// to check there is a least 4 input change every 65536 count of timer
//
ISR(TIMER1_OVF_vect)
{
  edgeCount = intCount; 
  intCount = 0;
}

// Interrupt capture handler
//
ISR(TIMER1_CAPT_vect)
{
  ring = (ring+1)&(MAXR-1);

  icp[ring] = ICR1;

  if (prot==ring)       // if (prot==ring) there is a big problem. data will be overwritten. 
    prot = -abs(ring);  // prot set to negative as error flag.

  // setup to catch falling edge
  if (ring&1) { TCCR1B &= ~(1<<ICES1); TIFR1 |= (1<<ICF1); }
  // setup to catch rising edge
  else { TCCR1B |= (1<<ICES1); TIFR1 |= (1<<ICF1); }

  pulseCount += (ring&1) && pulseCount<0xFFFF;  //branch is bad when execution time is precious
  intCount += intCount<0xFF;
}



//period is the time it takes a signal to repeat
uint16_t Period(boolean edge)
{
  uint16_t val=0;

  if (intCount>4 || edgeCount>4)  // buff is full of readings
  {
    uint8_t i = ring;             // last writing in circular buffer
    prot = (i+(MAXR-4))&(MAXR-1); // protected area. TIMER1_CAPT_vect should not write here

    if(edge ^ (i&1))           // if edge XOR rising, it's not the right one take previous
      i = (i+(MAXR-1))&(MAXR-1);  // (i+3)&3 = i-1 range 0-3

    val = icp[i] - icp[(i+(MAXR-2))&(MAXR-1)];  // (i+2)&3 = i-2 range 0-3

    if (prot<0)                   // interruption occurs and will overwrite my data
      val = 0;                    // return 0 as error flag
  }

  return val;
}


//percentage of a period a signal is active 0-255 = 0-100%
int16_t Duty(boolean edge)
{
  uint32_t val=0;

  if (intCount>4 || edgeCount>4)  // buff is full of readings
  {
    uint8_t i = ring;             // last writing in circular buffer
    prot = (i+(MAXR-4))&(MAXR-1); // protected area. TIMER1_CAPT_vect should not write here

    if(!edge ^ (i&1))          // if edge XOR rising, it's not the right one take previous
      i = (i+(MAXR-1))&(MAXR-1);  // (i+3)&3 = i-1 range 0-3

    val = icp[i] - icp[(i+(MAXR-1))&(MAXR-1)];  // (i+3)&3 = i-1 range 0-3
    val = (val<<8) / (icp[i] - icp[(i+(MAXR-2))&(MAXR-1)]); // (i+2)&3 = i-2 range 0-3

//    delay(5); // test for overwritting error
           
    if (prot<0)                   // interruption occurs and will overwrite my data
      val = -1;                   // return -1 as error flag
  }
  else
  {
    if(!edge ^ digitalRead(8))
      val = 255;
    else
      val = 0;
  }
  
  return val;
}

uint16_t Count()
{
  uint16_t val=0;

  val = pulseCount;
  pulseCount = 0;

  return val;
}

Interesting... I'm working on other hardware at the moment but will try to look into this soon. I think it was working with Arduino 1.6.3 but have recently got to the point I will be tinkering with capture hardware again. I was capturing flow meter and sensor pulses. The frequency they operate at is less than 10kHz, and the signal must be clean. If it has noise then false capture events can happen. The virtual timer may not have been used much, so perhaps it is impossible... Thanks for the update by the way.

New version with 2 measurement on 2 consecutives pulse for measurement validation. Duty returned in 12 bits. 0-4095 = 0-100%.

#include "Arduino.h"
#include <avr/interrupt.h>

// circular buffer for capture events
#define   MAXR      8     // must be a power of 2. 8/16/32 ...
#define   VALD      4     // to be considered as valid 2 successive measurement must have difference lower than VALD

volatile  uint8_t   ring;
volatile  int8_t    prot;
volatile  uint16_t  icp[MAXR];

volatile  uint16_t  pulseCount;
volatile  uint8_t   intCount, edgeCount;

void initCapture(void)
{
  // Input Capture setup
  // ICNC1: Enable Input Capture Noise Canceler
  // ICES1: =1 for trigger on rising edge
  // CS12 CS11 CS10
  //   0    0    1  : /1    No prescaler, CPU clock
  //   0    1    0  : /8    prescaler
  //   0    1    1  : /64   prescaler
  //   1    0    0  : /256  prescaler
  //   1    0    1  : /1024 prescaler

  TCCR1A = 0;
  TCCR1B = (1<<ICNC1) | (0<<ICES1) | (1<<CS11);
  TCCR1C = 0;

  // initialize to catch Falling Edge
  { TCCR1B &= ~(1<<ICES1); TIFR1 |= (1<<ICF1); }
  ring = MAXR-1; // so ring+1 -> MAXR -> 0
  intCount = 0;
  edgeCount = 0;

  // Interrupt setup
  // ICIE1: Input capture
  // TOIE1: Timer1 overflow
  TIFR1 = (1<<ICF1) | (1<<TOV1);    // clear pending interrupts
  TIMSK1 = (1<<ICIE1) | (1<<TOIE1); // enable interupts

  // Set up the Input Capture pin, ICP1, Arduino Uno pin 8
  pinMode(8, INPUT);
  digitalWrite(8, 0); // floating may have 50 Hz noise on it.
  //digitalWrite(8, 1); // or enable the pullup
}


// to check there is a least 4 input change every 65536 count of timer
//
ISR(TIMER1_OVF_vect)
{
  edgeCount = intCount; 
  intCount = 0;
}

// Interrupt capture handler
//
ISR(TIMER1_CAPT_vect)
{
  ring = (ring+1)&(MAXR-1);

  icp[ring] = ICR1;

  if (prot==ring)       // if (prot==ring) there is a big problem. data will be overwritten. 
    prot = -abs(ring);  // prot set to negative as error flag.
    
  // setup to catch falling edge
  if (ring&1) { TCCR1B &= ~(1<<ICES1); TIFR1 |= (1<<ICF1); }
  // setup to catch rising edge
  else { TCCR1B |= (1<<ICES1); TIFR1 |= (1<<ICF1); }

  pulseCount += (ring&1) && pulseCount<0xFFFF;  //branch is bad when execution time is precious
  intCount += intCount<0xFF;
}



//period is the time it takes a signal to repeat
uint16_t Period(boolean edge)
{
  uint8_t   i;
  uint16_t  val1,val2;

  if ((intCount+edgeCount)>4)           // buff is full of readings
  {
    i = ring;                           // last writing in circular buffer
    prot = (i+(MAXR-5))&(MAXR-1);       // protected area. TIMER1_CAPT_vect should not write here

    if(edge ^ (i&1))                    // if edge XOR rising, it's not the right one take previous
      i = (i+(MAXR-1))&(MAXR-1);        // (i+3)&3 = i-1 range 0-3

    val1 = icp[i] - icp[(i+(MAXR-2))&(MAXR-1)]; // (i+2)&3 = i-2 range 0-3
    i = (i+(MAXR-2))&(MAXR-1);                  // (i+2)&3 = i-2 range 0-3
    val2 = icp[i] - icp[(i+(MAXR-2))&(MAXR-1)]; // (i+2)&3 = i-2 range 0-3

    if (prot>=0 && abs(val1-val2)<VALD) // No overwritting and 2 measurements quite same
    {
      return ((uint32_t)val1+(uint32_t)val2)>>1;  // Return mean        
    }
    else
      return 0;                         // return 0 as error flag
  }
  else
    return 0;                           // return 0 as error flag
}


//percentage of a period a signal is active 0-4095 = 0-100%
int16_t Duty(boolean edge)
{
  uint8_t   i;
  uint16_t  val1,val2;

  if ((intCount+edgeCount)>4)           // buff is full of readings
  {
    i = ring;                           // last writing in circular buffer
    prot = (i+(MAXR-5))&(MAXR-1);       // protected area. TIMER1_CAPT_vect should not write here

    if(!edge ^ (i&1))                   // if edge XOR rising, it's not the right one take previous
      i = (i+(MAXR-1))&(MAXR-1);        // (i+3)&3 = i-1 range 0-3

    val1 = icp[i] - icp[(i+(MAXR-1))&(MAXR-1)]; // (i+3)&3 = i-1 range 0-3
    val1 = (((uint32_t)val1)<<12) / (icp[i] - icp[(i+(MAXR-2))&(MAXR-1)]);  // (i+2)&3 = i-2 range 0-3
    i = (i+(MAXR-2))&(MAXR-1);          // (i+2)&3 = i-2 range 0-3
    val2 = icp[i] - icp[(i+(MAXR-1))&(MAXR-1)]; // (i+3)&3 = i-1 range 0-3
    val2 = (((uint32_t)val2)<<12) / (icp[i] - icp[(i+(MAXR-2))&(MAXR-1)]);  // (i+2)&3 = i-2 range 0-3

// delay(4); // test for overwritting error
           
    if (prot>=0)                        // No overwritting
    {
      if (abs(val1-val2)<VALD)          // 2 measurements quite same
      {
        return ((uint32_t)val1+(uint32_t)val2)>>1;  // Return mean        
      }
      else
        return -2;                      // return -2 as error flag
    }
    else
    {
      return -1;                        // return -1 as error flag
    }
  }
  else if (!(intCount+edgeCount))
  {
    if(!edge ^ digitalRead(8))
      return 4095;
    else
      return 0;
  }
  else
    return -3;                      // return -3 as error flag
}

uint16_t Count()
{
  uint16_t val=0;

  val = pulseCount;
  pulseCount = 0;

  return val;
}

I have a redo of my software, it is not yet fully tested.

I have kept the virtual timer, it clearly can't work if anything blocks the interrupt for a brief time. That means the use of serial ports needs to be well understood, and delays are simply not acceptable. Doing non-blocking code is a pain, but I think it is needed for this.

I've increased the capture buffer size to 32 events, which is held in four uint8_t arrays. That is the maximum where the compiler can use the AVR ldd instruction in the ISR, I have not looked to verify it is doing so. Also removed other stuff from the ISR.

I am trying to get better at Makefiles so this new software is not going to work in the Arduino IDE, someone may find it useful anyway. It was compiled with the toolchain (gcc-avr, avr-libc, avrdude, make) on Ubuntu 16.04 which seems to work fine.

More updates.

Data is now returned in two formats.

/0/capture? icp1,3
{"icp1":{"count":"598068","low":"1731","high":"530"}}
{"icp1":{"count":"598066","low":"1729","high":"530"}}
{"icp1":{"count":"598064","low":"1732","high":"530"}}

capture? returns ICP1 timer delta(s) as a pair of low and high timer values from the buffered capture events.

/0/event? icp1,3
{"icp1":{"count":"2124622","event":"2128443871","rising":"1"}}
{"icp1":{"count":"2124621","event":"2128442275","rising":"0"}}
{"icp1":{"count":"2124620","event":"2128441867","rising":"1"}}

event? returns ICP1 event timer values from a 32 bit unsign integer.

A problem with occasional data corruption is ongoing, it may have been related to how the virtual counter ISR was trying to increment external volatiles directly without copying them to local registers. I don't understand why that would be a problem but inside the wiring.c file of Arduino I find the ISR(TIMER0_OVF_vect) function that does the nearest thing I have seen. Inside that ISR they copy all external volatiles to local variables and have a comment that said: "volatile variables must be read from memory on every access". Clearly I'm grasping but why is that needed in Arduino's wiring ISR.

Yet more updates

I have now added status that shows if the overflow flag is set while running the capture ISR and if the capture flag is set while running the overflow ISR. It is not yet clear if this can be used to fix a broken event but I think it points out the cause of the problem.

/0/capture? icp1,3
{"icp1":{"count":"602193","low":"1645","high":"435","status":"64"}}
{"icp1":{"count":"602191","low":"1645","high":"437","status":"4"}}
{"icp1":{"count":"602189","low":"1645","high":"436","status":"0"}}

The status of 64 means the last event of the capture report had an ICF1 (capture) flag set while running the overflow ISR. The status of 4 means the first event of the capture report had its ICF1 flag set while running the overflow ISR. It takes three events to aggregate the data for a capture report, so the flag status can show in two reports (as the first and last event). The flag will only show once if it is in a middle event.

/0/event? icp1,3
{"icp1":{"count":"12300654","event":"4236314232","status":"1"}}
{"icp1":{"count":"12300653","event":"4236312588","status":"4"}}
{"icp1":{"count":"12300652","event":"4236312143","status":"1"}}

Corrupted Captures look very much like the result of the two interrupts competing with each other, this is twisting my brain into a knot.