Arduino reading frequency correctly in the real world

Project: acquiring speed of a motor with SS49E hall sensor on shaft. 2 magnets on shaft, LM393 comparator to convert to square wave and sent to a 5V 16Mhz Promini.

Problem. 50% square wave from a signal generator is read just fine by the Promini, but not the one from the comparator. Some very strange results too. Changing the 3.65Vref on the comparator can significantly change the displayed reading, but not the comparator output viewed on the scope.
I'm using a sketch based on Nick Gammon's most helpful timers/counters page. (Thanks so much Nick for all your information, you've been such a great help over the years.)
I've tried other sketches and hardware like schmidt trigger inverter, but no success.
The attached pic shows the scope on the pin5 arduino input (1k pullup) showing a 26hz signal, while the Promini reports 1022hz. A laser tach shows 780 rpm, or 13hz, x2 magnets is 26hz verifying the scope reading.
Fairly sure this is some kind of electronics issue, impedence or stray currents but it remains even with all different hardware and circuits.

Anyone have any ideas?
Thanks very much.


#include <SSD1306_text.h>
//for rpi, had to add 'const' in front of the 'static' progmem statement in sshfont.h
//also changed the oled address to 0x3c in ssd1306_text.h

#include <Wire.h>

#define OLED_RESET 4
SSD1306_text display(OLED_RESET);


volatile unsigned long timerCounts;
volatile boolean counterReady;

// internal to counting routine
unsigned long overflowCount; //max 4,294,967,295 
unsigned int timerTicks; //max 65535
unsigned int timerPeriod; //

void startCounting (unsigned int ms) 
  {
  counterReady = false;         // time not up yet
  timerPeriod = ms;             // how many 1 ms counts to do
  timerTicks = 0;               // reset interrupt counter
  overflowCount = 0;            // no overflows yet

  // reset Timer 1 and Timer 2
  TCCR1A = 0;             
  TCCR1B = 0;              
  TCCR2A = 0;
  TCCR2B = 0;

  // Timer 1 - counts events on pin D5
  TIMSK1 = bit (TOIE1);   // interrupt on Timer 1 overflow

  // Timer 2 - gives us our 1 ms counting interval
  // 16 MHz clock (62.5 ns per tick) - prescaled by 128
  //  counter increments every 8 µs. 
  // So we count 125 of them, giving exactly 1000 µs (1 ms)
  TCCR2A = bit (WGM21) ;   // CTC mode
  OCR2A  = 124;            // count up to 125  (zero relative!!!!)

  // Timer 2 - interrupt on match (ie. every 1 ms)
  TIMSK2 = bit (OCIE2A);   // enable Timer2 Interrupt

  TCNT1 = 0;      // Both counters to zero
  TCNT2 = 0;     

  // Reset prescalers
  GTCCR = bit (PSRASY);        // reset prescaler now
  // start Timer 2
  TCCR2B =  bit (CS20) | bit (CS22) ;  // prescaler of 128
  // start Timer 1
  // External clock source on T1 pin (D5). Clock on rising edge.
  TCCR1B =  bit (CS10) | bit (CS11) | bit (CS12);
  }  // end of startCounting

ISR (TIMER1_OVF_vect)
  {
  ++overflowCount;               // count number of Counter1 overflows  
  }  // end of TIMER1_OVF_vect


//******************************************************************
//  Timer2 Interrupt Service is invoked by hardware Timer 2 every 1 ms = 1000 Hz
//  16Mhz / 128 / 125 = 1000 Hz

ISR (TIMER2_COMPA_vect) 
  {
  // grab counter value before it changes any more
  unsigned int timer1CounterValue;
  timer1CounterValue = TCNT1;  // see datasheet, page 117 (accessing 16-bit registers)
  unsigned long overflowCopy = overflowCount;

  // see if we have reached timing period
  if (++timerTicks < timerPeriod) 
    return;  // not yet

  // if just missed an overflow
  if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256)
    overflowCopy++;

  // end of gate time, measurement ready

  TCCR1A = 0;    // stop timer 1
  TCCR1B = 0;    

  TCCR2A = 0;    // stop timer 2
  TCCR2B = 0;    

  TIMSK1 = 0;    // disable Timer1 Interrupt
  TIMSK2 = 0;    // disable Timer2 Interrupt

  // calculate total count
  timerCounts = (overflowCopy << 16) + timer1CounterValue;  // each overflow is 65536 more
  counterReady = true;              // set global flag for end count period
  }  // end of TIMER2_COMPA_vect

void setup () 
  {    
    display.init();
    display.clear();                 // clear screen
    display.setCursor(0,0);
    display.setTextSize(1,1); 
    display.write("Hello world!");
    //float floatVal = 23.792;
    //display.print(floatVal,3);
    //display.writeInt(int i);
  Serial.begin(115200);       
  Serial.println("Frequency Counter");
  } // end of setup

void loop () 
  {
  // stop Timer 0 interrupts from throwing the count out
  byte oldTCCR0A = TCCR0A;
  byte oldTCCR0B = TCCR0B;
  TCCR0A = 0;    // stop timer 0
  TCCR0B = 0;    

  startCounting (500);  // how many ms to count for

  while (!counterReady) 
     { }  // loop until count over

  // adjust counts by counting interval to give frequency in Hz
  float frq = (timerCounts *  1000.0) / timerPeriod;

  Serial.print ("Frequency: ");
  Serial.print ((unsigned long) frq);
  Serial.println (" Hz.");
    display.clear();
    display.setTextSize(2,1);
    display.setCursor(5,0);
    display.print((unsigned long) frq);
    display.print(" Hz");
  
  // restart timer 0
  TCCR0A = oldTCCR0A;
  TCCR0B = oldTCCR0B;

  // let serial stuff finish
  delay(200);
  }   // end of loop

Since there is hardware that could cause this problem please post an annotated schematic, not a frizzy. Be sure to show all connections, power, ground, power sources and note any wire over 10"/25cm.

Hi, @ddllpp
Welcome to the forum.

Did you Google ;

arduino tacho

Tom.... :smiley: :+1: :coffee: :australia:

if you have an ESP32 available you could use the Pulse Counter (PCNT)

Simplify your circuit & code as much as possible until you can get the frequency to read correctly.

Remove (comment out) all the OLED code. Connect the USB-serial adapter to laptop and use serial monitor.

What is the max rpm to be measured?

Yes, quite alot of googling actually. I'm a retired electronics tech too, so it's got me questioning my sanity. I believe the problem is with the comparator, since I've had no trouble with speed sensing with other types of sensors via an opto.
At least I'm not alone. No real solution here:

Trouble is the hall sensor I'm using isn't TTL compliant, so I need the comparator to bring the lows down. I've tried all sorts of circuit mods with hysteresis as well. They may change the error, but it's still significant.
Yes I can throw out the whole circuit and go with some other sensor, but having to deal with comparators is pretty common, so I stubbornly persist.

The simplest circuit, pic & schematic:


Since my little STM32-based scope reads the signal just fine, I thought isolation, trying different supplies for the hall/comparator vs the promini. Same thing, even when on different breadboards. Next tried a CNY17F opto for both signal and ground isolation and that works, at least reasonably, giving me 26-32hz while the scope is more steady, giving 10ths to 100ths of a hz variations.
But not orders of magnitude values as before.
So a solution for sure, but still a mystery as to why total isolation is necessary for an Atmega328p to accurately acquire signal frequency from a comparator.

Updated schematic:

Wouldn't have been easier to use a digital output Hall Effect, rather than a linear device.

That way the sensor output would be low or high.

Tom.... :smiley: :+1: :coffee: :australia:

1 Like

I guess the Promini frequency meter is seeing spikes that your pocket oscilloscope cannot. Adding the (slow) optocoupler, which you've said seems to improve things, may be filtering some of this out.

I'd also be tempted to experiment with a small capacitor across the output of the hall sensor. I'd also be curious about its analog waveform.

The 300 ohm pull up resistor on the comparator output is quite strong and the comparator has no decoupling capacitor. Try using a 10k pullup resistor and putting a 100nF capacitor directly across the power rails of the comparator.

You could also add some hysteresis to the comparator:

2 Likes

In post #7, your schematic does not match your breadboard picture.
Also you have no hysteresis and it is absolutely needed.

Hi, @ddllpp

You have two comparators on that IC, the other one at the moment has both inputs open circuit, short both of the second comparators input pins, 5 and 6, to gnd.

This will ensure that the second comparator is not oscillating uncontrollably.

Also try a 0.1uF and a 10uF electrolytic capacitor across the Vcc and gnd pins of the 393, for bypassing.

Tom.... :smiley: :+1: :coffee: :australia:

1 Like

Consider that digital pin read of the Hall sensor would make the signal LOW or HIGH as long as the sensor varied between < 1.1V and > ~3V. Masking and turning a PINx register read to 0 or 1 takes few cycles at all.

The shaft has 2 magnets on opposite sides? 2 HIGH's per rev?

What is the max revs per second?
How fast might that change?

Consider that a 16 MHz AVR can poll the sensor at 10's of KHz fast, if the revs are not super high it might poll HIGH and LOW several times in a row each using direct PINx register reads at 10 or 20 microsecs timed reads (X cycles between reads) leaving many cycles to process the data. How many timed reads per half-rev HIGH and LOW can get you speed to knowable accuracy and still have cycles to process the data and output results.
No div or mult math is needed, just adds. If the output is in hex a binary to decimal conversion wouldn't be needed to print text, just binary to text.

Interrupts have overhead. At high speed with 2 interrupts per rev, the overhead will add up and clog ( sampling + processing + output ). At low speed, not a problem.

So, what speed range in Hz?

1 Like

ddllpp,

I decided to do some tests on your code.

I do not have your hardware, so I used an Arduino Uno R3 instead.
As I don't have your display, I removed the parts of the code that used the display.

Initially, I used a Function Generator to provide an input signal to pin 5.

I found out that using a 26 Hz input frequency, and with a 1V amplitude rectangular wave (with a 2.5 V offset), that your frequency counter worked correctly for all duty cycles in the range of 0.001% to 99.999%.
This was very encouraging.

In order to make the counter 'go wrong', I used a second Arduino programmed to generate a 26 Hz square wave.

For the code used, click here.

unsigned long previousMillis = 0;
int interval = 0;
int frequency = 26;
int outputPin = 12;

void setup() { 
  pinMode(outputPin, OUTPUT);
  Serial.begin(9600);
  interval = 1000 / frequency;
  Serial.println();
  Serial.print(" Frequency: ");
  Serial.print(frequency);
  Serial.println(" Hz");
  Serial.print(" Period:    ");
  Serial.print(interval);
  Serial.println(" ms");
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    previousMillis = previousMillis + interval;

    PORTB = B00010000;    // pin 12 HIGH
    PORTB = B00000000;    // pin 12 LOW
    PORTB = B00010000;    // pin 12 HIGH
    
    delay(interval / 2);
    
    PORTB = B00000000;    // pin 12 LOW
    PORTB = B00010000;    // pin 12 HIGH
    PORTB = B00000000;    // pin 12 LOW
  }
}

Using direct port manipulation I added a 'glitch' to the waveform.
Instead of the rising edge of the signal just going from LOW to HIGH, I made it go from LOW to HIGH, back to LOW, and then to HIGH.
I made it so that I could do a similar thing on the falling edge of the 26Hz signal as well.

Here are the results:
Standard 26Hz square wave:


The Arduino frequency counter correctly displays 26Hz.

Here is a 26Hz signal with a glitch on the rising edge:
I've zoomed in on the rising edge of the signal (50ns/div).


The Arduino frequency counter shows double the correct frequency.

Here is a 26Hz signal with glitches on the rising and falling edges:
Zoomed in on the trailing edge.


The Arduino frequency counter now displays thrice the correct frequency.

This shows that your code will detect a pulse as narrow as 1 clock cycle (62.5ns).

I believe that your comparator generates a burst of pulses on its output as the input signal gets close to the reference voltage.
Perhaps you can speed up the time base on your oscilloscope in order to zoom in on the waveform to see if this is the case.

Try connecting a 1MΩ resistor between the comparator output and the + input to give positive feedback and provide hysteresis to the circuit.

3 Likes

Thanks everyone for all your great input. I have tried many of your solutions like capacitors and hysterisis, and will try the ones I haven't. I included only the simple circuit, since I was asked to do so and since I've tried so many.
Yesterday I ran across a post (coincidentally from Nick Gammon again) about the internal comparator in the Atmega328 which I didn't know it had. And, thanks again Nick, that works just fine and makes life simple. I'll attach the code and schematic.

--edit-- Oops, on second look it's reading about double what it should, but it's promising.

https://www.gammon.com.au/forum/?id=11916

//internal comparator
  //AIN1 D7 Vref
  //AIN0 D6 Pulse 

#include <SSD1306_text.h>
//for rpi, had to add 'const' in front of the 'static' progmem statement in sshfont.h
//also changed the oled address to 0x3c in ssd1306_text.h

#include <Wire.h>

#define OLED_RESET 4
SSD1306_text display(OLED_RESET);


volatile unsigned long timerCounts;
volatile boolean counterReady;

// internal to counting routine
//unsigned long overflowCount; //max 4,294,967,295
 unsigned long ComparatorCount;
unsigned int timerTicks; //max 65535
unsigned int timerPeriod; //

void startCounting (unsigned int ms) 
  {
  counterReady = false;         // time not up yet
  timerPeriod = ms;             // how many 1 ms counts to do
  timerTicks = 0;               // reset interrupt counter
  //overflowCount = 0;            // no overflows yet
  ComparatorCount=0;
  
  // reset Timer 1 and Timer 2
//  TCCR1A = 0;             
//  TCCR1B = 0;              
  TCCR2A = 0;
  TCCR2B = 0;

  // Timer 1 - counts events on pin D5
  //TIMSK1 = bit (TOIE1);   // interrupt on Timer 1 overflow

  // Timer 2 - gives us our 1 ms counting interval
  // 16 MHz clock (62.5 ns per tick) - prescaled by 128
  //  counter increments every 8 µs. 
  // So we count 125 of them, giving exactly 1000 µs (1 ms)
  TCCR2A = bit (WGM21) ;   // CTC mode
  OCR2A  = 124;            // count up to 125  (zero relative!!!!)

  // Timer 2 - interrupt on match (ie. every 1 ms)
  TIMSK2 = bit (OCIE2A);   // enable Timer2 Interrupt

  //TCNT1 = 0;      // Both counters to zero
  TCNT2 = 0;     

  // Reset prescalers
  GTCCR = bit (PSRASY);        // reset prescaler now
  // start Timer 2
  TCCR2B =  bit (CS20) | bit (CS22) ;  // prescaler of 128
  // start Timer 1
  // External clock source on T1 pin (D5). Clock on rising edge.
  //TCCR1B =  bit (CS10) | bit (CS11) | bit (CS12);
  }  // end of startCounting
//
//ISR (TIMER1_OVF_vect)
//  {
//  ++overflowCount;               // count number of Counter1 overflows  
//  }  // end of TIMER1_OVF_vect

//internal comparator ISR
ISR (ANALOG_COMP_vect)
  {
  ++ComparatorCount;
  }

//******************************************************************
//  Timer2 Interrupt Service is invoked by hardware Timer 2 every 1 ms = 1000 Hz
//  16Mhz / 128 / 125 = 1000 Hz

ISR (TIMER2_COMPA_vect) 
  {
  // grab counter value before it changes any more
  //unsigned int timer1CounterValue;
  //timer1CounterValue = TCNT1;  // see datasheet, page 117 (accessing 16-bit registers)
  //unsigned long overflowCopy = overflowCount;

  // see if we have reached timing period
  if (++timerTicks < timerPeriod) 
    return;  // not yet

  // if just missed an overflow
  //if ((TIFR1 & bit (TOV1)) && timer1CounterValue < 256)
    //overflowCopy++;

  // end of gate time, measurement ready

//  TCCR1A = 0;    // stop timer 1
//  TCCR1B = 0;    

  TCCR2A = 0;    // stop timer 2
  TCCR2B = 0;    

//  TIMSK1 = 0;    // disable Timer1 Interrupt
  TIMSK2 = 0;    // disable Timer2 Interrupt

  // calculate total count
  //timerCounts = (overflowCopy << 16) + timer1CounterValue;  // each overflow is 65536 more
  counterReady = true;              // set global flag for end count period
  
  }  // end of TIMER2_COMPA_vect

void setup () 
  {    
    display.init();
    display.clear();                 // clear screen
    display.setCursor(0,0);
    display.setTextSize(1,1); 
    display.write("Hello world!");
    //float floatVal = 23.792;
    //display.print(floatVal,3);
    //display.writeInt(int i);
  Serial.begin(115200);       
  Serial.println("Frequency Counter");
//internal comparator
  //AIN1 D7 Vref
  //AIN0 D6 Pulse 
  ADCSRB = 0;           // (Disable) ACME: Analog Comparator Multiplexer Enable
  ACSR =  bit (ACI)     // (Clear) Analog Comparator Interrupt Flag
        | bit (ACIE)    // Analog Comparator Interrupt Enable
        | bit (ACIS1);  // ACIS1, ACIS0: Analog Comparator Interrupt Mode Select (trigger on falling edge)
  
  } // end of setup

void loop () 
  {
  // stop Timer 0 interrupts from throwing the count out
  byte oldTCCR0A = TCCR0A;
  byte oldTCCR0B = TCCR0B;
  TCCR0A = 0;    // stop timer 0
  TCCR0B = 0; 
     

  startCounting (1000);  // how many ms to count for

  while (!counterReady) 
     { }  // loop until count over
  
  // adjust counts by counting interval to give frequency in Hz
  float frq = ComparatorCount;
  
  
  Serial.print ("Frequency: ");
  Serial.print ((unsigned long) frq);
  Serial.println (" Hz.");
    display.clear();
    display.setTextSize(2,1);
    display.setCursor(5,0);
    display.print((unsigned long) frq);
    display.print(" Hz");
  
  // restart timer 0
  TCCR0A = oldTCCR0A;
  TCCR0B = oldTCCR0B;

  // let serial stuff finish
  delay(200);
  }   // end of loop

If you look at the following project, the spiking issues associated with that sensor (SS49E hall sensor) will become clear and that some form of filtering is needed on its output before it is fed into a frequency meter:

Here is a waveform as a magnet is moved in the vicinity of the sensor:

Edit

I suggest that you experiment with a low pass filter at the output of the sensor (say 10k and 100nF to start with) then examine the waveform either with that oscilloscope (if it has sufficient resolution) or, as in the quoted article, feed it to an Arduino analog pin and use the serial plotter in the IDE to display it.

It would have more meaning to me if the axes were labeled.
Is the domain micro or milli seconds?
Is the range volts or millivolts?

With that I could tell if a digital pin would serve as a filter and how well it would do with frequent reads. Using the comparator and a digital pin might make a better picture for timing shaft motion.

Consider that an analog read takes appx 109 microsecs per read while a digital pin takes a small fraction of 1 microsecs per read. The processing of the analog read will take fewer microsecs than the analog read while the processing of the digital read will take more time than the digital read, together they may take a whole 1 microsec.
In 1 millisec it's possible to make and process 8 analog reads or perhaps 60 to 80 digital reads.

Knowing the timescale of that graph could make determining the suitability of sampling methods possible.

In the code that supposedly produced that graph (see the link in post #17), there is a 50ms delay between analogue reads, so the 500 points take 25 seconds to be recorded.

Hi, @ddllpp

Can I suggest you cut back your code to just the sensor and serial monitor output.
Remove the display and its code.
You are manipulating timers, do you know if the display library uses them too?

See what RPM you get with raw unadulterated code.

How are you powering your code?
What model Promini, 5V or 3V3?

Tom.... :smiley: :+1: :coffee: :australia: