Using ISR or ICP for pulse counting

Hi.

I am trying to create a program that with count the time taken for 4 pulses and then convert that to to RPM and store it in a variable for use later.

I have tried using INT0 to capture the 4 pulse and record the time taken but when i add in the code to show the result on serial monitor it seems to take a while to show the correct rpm for a pulse at 66Hz.

Here is the code I have. Please feel free to tell me any stupid mistakes. I'm still learning.

I'm testing with a NANO but will be moving to a ATTINY85

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <SwitecX12.h>


//#define F_CPU 8000000UL
#define TIMER_INTERVAL_MS 200
#define PULSE_THRESHOLD 4
#define RPM_THRESHOLD 2000
#define AVERAGE_COUNT 5
const int STEPS = 265 * 12;

// Define X12 control pins
const int A_STEP = 8;
const int A_DIR = 9;
const int RESET = 11;

SwitecX12 motor1(STEPS, A_STEP, A_DIR);

volatile uint16_t pulse_count = 0;
volatile uint8_t pulse_flag = 0;
volatile uint16_t rpm = 0;
volatile uint16_t rpm_average = 0;
uint16_t rpm_buffer[AVERAGE_COUNT] = { 0 };
uint8_t rpm_index = 0;

// Function prototypes
void timer1_init();
void int0_init();
void uart_init();
void uart_transmit(char data);
void uart_print(const char* str);
void uart_print_number(uint16_t num);

// Timer1 compare match interrupt (200 ms interval)
ISR(TIMER1_COMPA_vect) {
  // Calculate RPM (pulses in 200ms * 5 * 60 = pulses per minute)
  rpm = (pulse_count * (60.0 / 0.66644) / 1.5);  // Convert to RPM
  pulse_count = 0;                               // Reset pulse count

  // Store RPM in the buffer for averaging
  rpm_buffer[rpm_index] = rpm;
  rpm_index = (rpm_index + 1) % AVERAGE_COUNT;

  // Calculate average RPM
  uint16_t sum = 0;
  for (uint8_t i = 0; i < AVERAGE_COUNT; i++) {
    sum += rpm_buffer[i];
  }
  rpm_average = sum / AVERAGE_COUNT;

  // Set LED on if average RPM is above threshold
  if (rpm_average > RPM_THRESHOLD) {
    PORTB |= (1 << PORTB5);  // Turn LED on
  } else {
    PORTB &= ~(1 << PORTB5);  // Turn LED off
  }
  int motorPosition = map(rpm_average, 0, 5000, 0, STEPS);  // Adjust the steps according to your motor's range
  motor1.stepTo(motorPosition);
  motor1.update();
  
}

// External Interrupt on INT0 (pin PD2)
ISR(INT0_vect) {
  pulse_count++;

  // Set flag every 4 pulses
  if (pulse_count % PULSE_THRESHOLD == 0) {
    pulse_flag = 1;
  }
}

void timer1_init() {
  // Configure Timer1 for CTC mode with 200ms interval
  TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10);                         // CTC mode, prescaler 64
  OCR1A = (uint16_t)((F_CPU / (64 * 1000)) * TIMER_INTERVAL_MS / 1000 - 1);  // 200ms interval
  TIMSK1 |= (1 << OCIE1A);                                                   // Enable Timer1 compare match A interrupt
}

void int0_init() {
  // Configure INT0 to trigger on rising edge
  EICRA |= (1 << ISC01) | (1 << ISC00);  // Rising edge on INT0
  EIMSK |= (1 << INT0);                  // Enable external interrupt INT0
}

void uart_init() {
  // Set baud rate to 9600
  uint16_t ubrr_value = F_CPU / 16 / 9600 - 1;
  UBRR0H = (uint8_t)(ubrr_value >> 8);
  UBRR0L = (uint8_t)ubrr_value;
  UCSR0B = (1 << TXEN0);                   // Enable transmitter
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);  // 8-bit data
}

void uart_transmit(char data) {
  while (!(UCSR0A & (1 << UDRE0)))
    ;           // Wait for empty transmit buffer
  UDR0 = data;  // Send data
}

void uart_print(const char* str) {
  while (*str) {
    uart_transmit(*str++);
  }
}

void uart_print_number(uint16_t num) {
  char buffer[10];
  itoa(num, buffer, 10);
  uart_print(buffer);
}

void setup() {

  

  // Configure PB5 as output for LED
  DDRB |= (1 << DDB5);
  DDRB |= (1 << DDB3);
  PORTB |= (1 << PORTB3);

  motor1.zero();
  motor1.stepTo(STEPS);
  motor1.stepTo(0);
}

int main(void) {

 

  // Initialize Timer1, INT0, and UART
  timer1_init();
  int0_init();
  uart_init();
  setup();
  

  

  // Enable global interrupts
  sei();

  while (1) {
    // Check if the pulse flag is set
    if (pulse_flag) {
      pulse_flag = 0;  // Reset flag

      // Print RPM to serial
      uart_print("RPM: ");
      uart_print_number(rpm_average);
      uart_print("\r\n");

      
    }
  }

 

}

Move as much math out of the ISR as possible.
ISR's should be as short as possible to stay reactive.
Do that math in loop()

Thinking out loud
Why are you using 4 pulses as the time difference between two pulses (current and previous) should be enough to calculate the RPM.
With every new pulse you get a new duration since the previous pulse.
Multiply by 4 and you have the time of a single rotation.
RPM = 1.0/(60 * duration).
(assumption the 4 pulses are 90 degrees apart).

I just did it because there was 4 pulses per revolution.

  • the ISR just increments a a count
  • loop reports an rpm every some period, T (e.g. 100 msec)
  • loop determines the difference in the count between each update, delta = cnt - cntLast (cnt is unsigned)
  • rpm = 60 * delta / 4 / T

may need to diable interrupts when capturing pulse count

Here is the new code that the ISR is working faster.

#include <SwitecX12.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdio.h>


#define F_CPU 16000000UL   // 16 MHz clock speed

// Define X12 control pins
const int A_STEP = 8;
const int A_DIR = 9;
const int RESET = 11;

// Define the potentiometer pin
const int POT_PIN = A0;

const int STEPS = 265*12; // Max movement 265° (12 steps/deg)

// Create a SwitecX25 object
SwitecX12 motor1(STEPS, A_STEP, A_DIR);  // steps for 265-degree movement

volatile uint16_t pulse_duration = 0; // Store time between two pulses in microseconds
volatile uint8_t pulse_count = 0;     // Count the number of pulses

void setup_timer1() {
    // Configure Timer1 (16-bit) in normal mode
    TCCR1A = 0;                  // Normal mode
    TCCR1B = (1 << CS11);        // Prescaler = 8 (16 MHz / 8 = 2 MHz, 1 tick = 0.5 us)
    TCNT1 = 0;                   // Reset Timer1 Counter
}

void setup_interrupt() {
    // Enable External Interrupt on INT0 (PD2)
    EICRA |= (1 << ISC01) | (1 << ISC00); // Rising edge on INT0 triggers interrupt
    EIMSK |= (1 << INT0);                 // Enable INT0 interrupt
}

ISR(INT0_vect) {
    if (pulse_count == 0) {
        // First pulse: Start Timer1
        TCNT1 = 0; // Reset Timer1 Counter
        pulse_count++;
    } else if (pulse_count == 1) {
        // Second pulse: Capture Timer1 value
        pulse_duration = TCNT1; // Save the time between two pulses
        pulse_count = 0;        // Reset pulse count for next measurement
    }
}

void setup() {

  
   // Initialize rest pin of X12
  pinMode(RESET, OUTPUT);
  digitalWrite(RESET, HIGH);

  // Initialize motor

  motor1.zero();
  motor1.stepTo(STEPS);
  motor1.stepTo(0);
}

void loop() {
    
    uint32_t rpm = 0;

  

    // Initialize Timer1, and Interrupts
    
    setup_timer1();
    setup_interrupt();

    // Enable global interrupts
    sei();

    while (1) {
        if (pulse_duration > 0) {
            // Calculate RPM based on the pulse duration
            // Each tick is 0.5 us (due to prescaler of 8)
            uint32_t pulse_time_us = pulse_duration * 0.5; // Convert to microseconds
            if (pulse_time_us > 0) {
                rpm = 60000000 / pulse_time_us/4; // RPM calculation
            } else {
                rpm = 0;
            }

            if (rpm >= 300) {
              int motorPosition = map(rpm, 0, 5000, 0, STEPS); // Adjust the steps according to your motor's range
              motor1.stepTo(motorPosition);
             } else {
              motor1.stepTo(0);
              motor1.update();
             }

           // motor1.update();

            // Reset pulse_duration for next measurement
            pulse_duration = 0;
        }
    }
}

New problem now is the motor does not zero when there is no pulses.

Just an observation, these sorts of things are normally handled in setup().


  // Initialize Timer1, and Interrupts

  setup_timer1();
  setup_interrupt();