Counting RPM through Input Capture Pin

Dear all,

in my current project I am using a Noctua NF-A14 DC 5V DC fan.
Thanks to the help on timer2 I managed to drive the fan on the required 25kHz frequency. In the code below, by setting the "var" variable to a value between 0 and 40 it is possible to set different fan speeds.
Now, I would like to calculate the RPM by using the input capture pin and timer1. Using some tutorials and this example I combined the following code:

/* 12.06.2021
 * (Mini pro 168 5V 16MHz)
 * Using PWM @ 25 KHz to drive a Noctua 5V PWM fan
 * For Control want to use 16 MHz on Timer2 (without library)
 * For measuring the RPM Signal of the fan use input capture pin with timer1
 * 
 */

#define PWM_FAN_PIN 3 // blue cable
#define RPM_FAN_PIN 8 //green cable = Pin 8 ist der Input Capture Pin
//yellow is 5V



//Serial Communication speed
#define BAUDRATE 115200

const unsigned int top = 40; //defines the Frequency 24,4 kHz
const unsigned int timer1End = 62499; 
volatile bool RPM_MEASURE = false;
volatile unsigned int timer1_capture_counter = 0;
unsigned int RPMspeed = 0;

byte var = 0;

void setupTimer2();
void setupTimer1();
void setFanSpeed(unsigned int val);
void calcFanSpeed();
 
//gets triggered every 1s - see setup of timer1 in CTC! mode
ISR(TIMER1_COMPA_vect){
  RPM_MEASURE = true;
}

ISR(TIMER1_CAPT_vect){
  timer1_capture_counter ++;
}

void setupTimer1(){
/*Set detection to 1 Hz
* f = 16MHz / (prescaler x (65536 + TopVal))
* With prescaler 256 -> TopVal = 62499 for 1Hz
*/

    //Clear Timer1 control and Count registers
  TCCR1A = 0; // undo configurations on Timer1 pin 9
  TCCR1B = 0; // undo configurations on Timer1 pin 10
  TCNT1 = 0;  // Reset Timer1

  TCCR1A = 0; //CTC mode = 4 -> WGM(13,12,11,10) = (0,1,0,0)
  TCCR1B = (1<<ICES1) +(1<<WGM12)+ (1<<CS12); // CS12 = 1 -> prescaler 256; ICES1 = 1 means rising edge; ICNC1 = noise canceler 
  TIMSK1 =  (1<<ICIE1) + (1<<OCIE1A); // ICIE1 -> activates input capture interrupt, OCIE1A -> Output Compare A Match Flag 
  OCR1A = timer1End;
  
  return;
}

void setupTimer2(){
   //Clear Timer2 control and Count registers
  TCCR2A = 0; // undo configurations on Timer2 pin 11
  TCCR2B = 0; // undo configurations on Timer2 pin 3
  TCNT2 = 0;  // Reset Register of Timer2 

  //Setting wave form generation mode (WGM) to PWM, phase correct mode 5 
  // set prescaler to 8 -> CS12=0; CS11=1; CS10=0;
  
  TCCR2A =  (1<<COM2B1) | (1<<WGM20);
  TCCR2B = (1<<WGM22) | (1<<CS21);
  OCR2A = top;

  return;
}



void setFanSpeed( unsigned int val){    
      if(val <= top)
        OCR2B = val;
      else{
        Serial.println(F("Invalid fan speed"));
        OCR2B = 0; 
      }
      return;
}

void calcFanSpeed(){
  RPM_MEASURE = false;
  RPMspeed = (unsigned int)timer1_capture_counter / 2;
  timer1_capture_counter = 0;
  return;
}


void setup() {
  
  while(!Serial);
  
  Serial.begin(BAUDRATE);
  Serial.println(F("Start Operation - setup"));


  setupTimer2();
  setupTimer1();
  pinMode(PWM_FAN_PIN, OUTPUT);
  pinMode(RPM_FAN_PIN, INPUT);

}


void loop() {

  var = 20;
  
  
    setFanSpeed(var);
    if(RPM_MEASURE == true){

      calcFanSpeed();
      
      Serial.print(F("PWM Speed: "));
      Serial.print(var);
      Serial.print(F(" corres. to RPM: "));
      Serial.println(RPMspeed);
 
    }

}

The program compiles and works great on all var values, but the measured RPM is wrong.
If I set a low speed of var = 10 the µC captures in "timer1_capture_counter" a higher value than at var = 20 (where the fan runs obviously faster).
Ridiculously, at a quite high rotation value of var = 35, I get an output of timer1_capture_counter = 0 counts (within one second).
According to the Noctua description I should get 25kHz x 2 = 50.000 counts. (Two rising flags per cycle).
The problem is independent of the used mode (4 = CTC above). I measured same wrong values in normal mode ( 0, with correspondingly addapted settings of the timer1).

Where is my mistake? Or is it on the electronics side, that the output waveform is unreadable for the arduino? Unfortunately, I do not have an oscilloscope to check the generated waveform...

Thank you very much in advance!

PS: I avoided to use an ext. Interrupt to count the pulses, because teh gain was to run it in the background without performance losses.

You seem to be using the Input Capture as just another external interrupt. You are just counting how many time you get interrupted.

The Input Capture Interrupt is there to tell you that the timer count was captured in the Input Capture Register. You can use that value, subtracted from the next value, to precisely measure the time between pulses.

1 Like

You need to disable interrupts when using your volatile variables in your main code or else you risk having them updated mid-calculation. Since timer1_capture_counter is 16 bits, it can not be accessed atomically.

void calcFanSpeed(){
  RPM_MEASURE = false;
  noInterrupts();
  RPMspeed = (unsigned int)timer1_capture_counter / 2;
  timer1_capture_counter = 0;
  interrupts();
  return;
}
1 Like

Thanks a lot for your replies!
Indeed, (@johnwasser , you are right! ) I entirely misunderstood the concept of Input capture.
I should use the register ICR1, where the last "time-stamp" is written.

I modified the script using:

  • the important hint of @blh64
  • running timer1 in mode 4 but at 250 Hz with prescaler 1
    (This way the counter counts until 63999 until an oveflow happens, which means I am resolving 1/250 = 4ms with the mentioned number of steps;
  • the average of 4 measurements; meaning I want to capture 4 ICR1 values and calculating 3 time-differences for these consecuitve pulses for observing an average.
  • the example of Nick Gammon Timing an interval using the input capture unit

Actually, I decided to modify my program using the last mentioned link as a guide.
The example of Nick Gammon uses the normal mode at 16MHz. I want to try the same procedure in CTC mode with the settings above.
Now, for slow rotations I will also have overflows (multiple TIMER1_COMPA_vect "triggers").
Therefore, I would have to add the number of overflows to the capture-ICR1-value within TIMER1_CAPT_vact.

With the following code below I encountered multiple issues:

/* 13.06.2021
 * (Mini pro 168 5V 16MHz)
 * Using PWM @ 25 KHz to drive a Noctua 5V PWM fan
 * For Control want to use 16 MHz on Timer2 (without library)
 * For measuring the RPM Signal of the fan use input capture pin with timer1
 * 
 */

#define PWM_FAN_PIN 3 // blue cable
#define RPM_FAN_PIN 8 //green cable = Pin 8 ist der Input Capture Pin
//yellow is 5V



//Serial Communication speed
#define BAUDRATE 115200

const unsigned int top = 40; //defines the Frequency 24,4 kHz
const unsigned int timer1End = 63999; 
volatile bool RPM_MEASURE = false;
volatile byte RPM_CAPT_NUM = 0;

volatile unsigned int timer1_capture_counter[4]; // taking only 4 measures
volatile unsigned long overflowCount;
byte SIZEOF_timer1_cap_counter;

unsigned int RPMspeed = 0;

byte var = 0;

void resetCaptureArray();
void setupTimer2();
void setupTimer1();
void setFanSpeed(unsigned int val);
void calcFanSpeed();
 
//gets triggered every 1s - see setup of timer1 in CTC! mode
ISR(TIMER1_COMPA_vect){
    overflowCount++;
}

//Captures the values when where is a trigger on the ICP
ISR(TIMER1_CAPT_vect){

  unsigned long timer1_IC_val;
  timer1_IC_val = ICR1;

  if(RPM_CAPT_NUM < (SIZEOF_timer1_cap_counter)){
    timer1_capture_counter[RPM_CAPT_NUM] =  timer1_IC_val;
    RPM_CAPT_NUM ++;
  }
    
  else{
      TIMSK1 = 0;
      RPM_MEASURE = true;
      return;
  }
  
}

void setupTimer1(){
/*Set detection to 1 Hz
* f = 16MHz / (prescaler x (1 + timer1End))
* !Set Prescaler to 1 to have full 16 MHz resolution and 250 Hz 
* => timer1End = 63999
*/

  noInterrupts();
  //Clear Timer1 control and Count registers
  TCCR1A = 0; // undo configurations on Timer1 pin 9
  TCCR1B = 0; // undo configurations on Timer1 pin 10
  TCNT1 = 0;  // Reset Timer1 counter

  TCCR1A = 0; //CTC mode = 4 -> WGM(13,12,11,10) = (0,1,0,0)
  TCCR1B =(1<<ICNC1)+ (1<<ICES1) +(1<<WGM12)+ (1<<CS10); // CS10 = 1 -> prescaler 1; ICES1 = 1 means rising edge; ICNC1 = noise canceler 
  TIMSK1 =  (1<<ICIE1) + (1<<OCIE1A); // ICIE1 -> activates input capture interrupt, OCIE1A -> Output Compare A Match Flag 
  OCR1A = timer1End;

  interrupts();
  return;
}

void setupTimer2(){

  noInterrupts();
   //Clear Timer2 control and Count registers
  TCCR2A = 0; // undo configurations on Timer2 pin 11
  TCCR2B = 0; // undo configurations on Timer2 pin 3
  TCNT2 = 0;  // Reset Register of Timer2 

  //Setting wave form generation mode (WGM) to PWM, phase correct mode 5 
  // set prescaler to 8 -> CS12=0; CS11=1; CS10=0;
  
  TCCR2A =  (1<<COM2B1) | (1<<WGM20);
  TCCR2B = (1<<WGM22) | (1<<CS21);
  OCR2A = top;

  interrupts();
  return;
}



void setFanSpeed( unsigned int val){    
      if(val <= top)
        OCR2B = val;
      else{
        Serial.println(F("Invalid fan speed"));
        OCR2B = 0; 
      }
      return;
}

void resetCaptureArray(){
    
    for(byte i=0; i<=SIZEOF_timer1_cap_counter; i++)
    timer1_capture_counter[i]=0;
    overflowCount = 0;
    RPM_CAPT_NUM = 0;
    
    return;
}


void calcFanSpeed(){

  noInterrupts();

  Serial.println(timer1_capture_counter[0]);
  Serial.println(timer1_capture_counter[1]);
  Serial.println(timer1_capture_counter[2]);
  Serial.println(timer1_capture_counter[3]);
  
  unsigned long val=0;
  byte numVal=0;
  float RPmS = 0.0; 
  
    for(byte i=0; i<(RPM_CAPT_NUM-1); i++){
        if(timer1_capture_counter[i]!=0 && timer1_capture_counter[i+1]!=0){
          Serial.println(timer1_capture_counter[i+1]-timer1_capture_counter[i]);
          val= val + (timer1_capture_counter[i+1]-timer1_capture_counter[i]);//one pulse duration
          numVal++;
        }    
    }
    RPmS = (float) ((timer1End/4)/(val/numVal));
    // timer1End corresponds to 250 Hz, 4ms
    Serial.println(RPmS);
    RPMspeed = (unsigned int)(RPmS*60000.0/2.); //devided by 2 since 2 duty cacle corresp. to one turn for Noctua
    Serial.println(RPMspeed);
    Serial.println(RPMspeed);
  resetCaptureArray();
  TIMSK1 =  (1<<ICIE1) + (1<<OCIE1A);
  RPM_MEASURE = false;
  interrupts();
  return;
}


void setup() {
  
  while(!Serial);
  Serial.begin(BAUDRATE);
  Serial.println(F("Start Operation - setup"));

  SIZEOF_timer1_cap_counter = (sizeof(timer1_capture_counter) / sizeof(timer1End));

  pinMode(PWM_FAN_PIN, OUTPUT);
  pinMode(RPM_FAN_PIN, INPUT);

  setupTimer2();

  var = 32;
  setFanSpeed(var);

  
  resetCaptureArray();
  setupTimer1();

}


void loop() {
  
    if(RPM_MEASURE == true){
      
      calcFanSpeed();
      Serial.print(F(" PWM Speed: "));
      Serial.print(var);
      Serial.print(F(" corres. to RPM: "));
      Serial.println(RPMspeed);

    }

}

The output is of course meaningless, because I am missing the adding of the overflowCount to the captured ICR-values

timer1_capture_counter[RPM_CAPT_NUM] =  timer1_IC_val;

Now to do so, I require to define my 4 slot array timer1_capture_counter as unsigned long instead of unsigned int.
(Following the example of Nick Gammon I would like to use

timer1_capture_counter[RPM_CAPT_NUM] =(overflowCount << 16) +  timer1_IC_val;

)

Unfortunately, just by changing the definition of this array from "unsigned int" to "unsigned long" the programm crahes: The serial output freezes entirely.
Is there any reason why?

Maybe important information: At the setting of var = 32 the fan rotates at about 1200 RPM -> Pulses going at twice the speed 2400 RPM -> 40 pulses per second, 25 ms between two pulses.

Dear all,

now I really struggle to understand, what is going on.
I tried another approach by using the solution mentioned before from Nick Gammons' forum
Timing an interval using the input capture unit but I implemented additionally timer2.
This way I was able to drive different speed values for the fan and have a look on the performance of the original code.

// Frequency timer using input capture unit
// Author: Nick Gammon
// Date: 31 August 2013
//MODIFIED by MM 13.06.2021

// Input: Pin D8 

#define PWM_FAN_PIN 3 // blue cable
#define RPM_FAN_PIN 8 //green cable = Pin 8 ist der Input Capture Pin
//yellow is 5V

const unsigned int top = 40; //defines the Frequency 24,4 kHz

volatile boolean first;
volatile boolean triggered;
volatile unsigned long overflowCount;
volatile unsigned long startTime;
volatile unsigned long finishTime;




void setupTimer2(){

  noInterrupts();
   //Clear Timer2 control and Count registers
  TCCR2A = 0; // undo configurations on Timer2 pin 11
  TCCR2B = 0; // undo configurations on Timer2 pin 3
  TCNT2 = 0;  // Reset Register of Timer2 

  //Setting wave form generation mode (WGM) to PWM, phase correct mode 5 
  // set prescaler to 8 -> CS12=0; CS11=1; CS10=0;
  
  TCCR2A =  (1<<COM2B1) | (1<<WGM20);
  TCCR2B = (1<<WGM22) | (1<<CS21);
  OCR2A = top;

  interrupts();
  return;
}

void setFanSpeed( unsigned int val){    
      if(val <= top)
        OCR2B = val;
      else{
        Serial.println(F("Invalid fan speed"));
        OCR2B = 0; 
      }
      return;
}




// timer overflows (every 65536 counts)
ISR (TIMER1_OVF_vect) 
{
  overflowCount++;
}  // end of TIMER1_OVF_vect

ISR (TIMER1_CAPT_vect)
  {
  // grab counter value before it changes any more
  unsigned int timer1CounterValue;
  timer1CounterValue = ICR1;  // see datasheet, page 117 (accessing 16-bit registers)
  unsigned long overflowCopy = overflowCount;
  
  // if just missed an overflow
  if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 0x7FFF)
    overflowCopy++;
  
  // wait until we noticed last one
  if (triggered)
    return;

  if (first)
    {
    startTime = (overflowCopy << 16) + timer1CounterValue;
    first = false;
    return;  
    }
    
  finishTime = (overflowCopy << 16) + timer1CounterValue;
  triggered = true;
  TIMSK1 = 0;    // no more interrupts for now
  }  // end of TIMER1_CAPT_vect
  
void prepareForInterrupts ()
  {
  noInterrupts ();  // protected code
  first = true;
  triggered = false;  // re-arm for next time
  // reset Timer 1
  TCCR1A = 0;
  TCCR1B = 0;
  
  TIFR1 = bit (ICF1) | bit (TOV1);  // clear flags so we don't get a bogus interrupt
  TCNT1 = 0;          // Counter to zero
  overflowCount = 0;  // Therefore no overflows yet
  
  // Timer 1 - counts clock pulses
  TIMSK1 = bit (TOIE1) | bit (ICIE1);   // interrupt on Timer 1 overflow and input capture
  // start Timer 1, no prescaler
  TCCR1B =  bit (CS10) | bit (ICES1);  // plus Input Capture Edge Select (rising on D8)
  interrupts ();
  }  // end of prepareForInterrupts
  

void setup () 
  {
  Serial.begin(115200);       
  Serial.println("Frequency Counter");
  pinMode(PWM_FAN_PIN, OUTPUT);
  setupTimer2();
  setFanSpeed(5);
  
  // set up for interrupts
  prepareForInterrupts ();   
  } // end of setup

void loop () 
  {
  // wait till we have a reading
  if (!triggered)
    return;
 
  // period is elapsed time
  unsigned long elapsedTime = finishTime - startTime;
  // frequency is inverse of period, adjusted for clock period
  float freq = F_CPU / float (elapsedTime);  // each tick is 62.5 ns at 16 MHz
  
  Serial.print ("Took: ");
  Serial.print (elapsedTime);
  Serial.print (" counts. ");

  Serial.print ("Frequency: ");
  Serial.print (freq);
  Serial.println (" Hz. ");

  // so we can read it  
  delay (500);

  prepareForInterrupts ();   
}   // end of loop

At different fan speeds (e.g. setFanSpeed(5)) the results are completely wrong:

14:02:43.909 -> Took: 640 counts. Frequency: 25000.00 Hz. 
14:02:44.511 -> Took: 1280 counts. Frequency: 12500.00 Hz. 
14:02:45.012 -> Took: 640 counts. Frequency: 25000.00 Hz. 
14:02:45.560 -> Took: 5760 counts. Frequency: 2777.78 Hz.

Only at the max-speed setFanSpeed(40) there are some reasonable results:

14:06:37.588 -> Took: 312820 counts. Frequency: 51.15 Hz. 
14:06:38.237 -> Took: 328306 counts. Frequency: 48.74 Hz. 
14:06:38.991 -> Took: 2243794 counts. Frequency: 7.13 Hz. 
14:06:39.640 -> Took: 336776 counts. Frequency: 47.51 Hz. 
14:06:40.442 -> Took: 2560056 counts. Frequency: 6.25 Hz. 
14:06:41.096 -> Took: 322454 counts. Frequency: 49.62 Hz. 

Still having strange values inbetween.

The fan runs at full speed, without the modified blocks for the fan-operation through timer2 .
Using the original code from Nick Gammon I got simillar values as posted in the last output.

I woul conclude, that there is a serious issue of timer2 running in parallel to timer1 during the input capture.
What would be the proper way to measure the time-distance between the pulses of the fan?

Thank you very much in advance!

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.