Keeping the average speed on display for a Tachometer

Hi All,

I am new to the Arduino community, I am building a Tachometer / Treadmill for my RC car hobby, My intention is to build with a Hall sensor and make the project handheld possible with Li-po battery power.

I got examples from youtube and already have a prototype that's basically working; (Credits to youtube
InterlinkKnight and cbm80amiga)

I am able to get the below information fairly accurately.

  • RMP
  • Speed (KM/h) through realtime calculation
  • Timer (counter) starts whenever there is a new rotation.

However I am facing some issues;

  1. While the code already handles all the difficult parts of getting RPM (Average and Smoothing) all these values are calculated in real time. I would like to keep the average since the last rotation start on screen, but I don't know how to post such value out of the Loop() where the the Speed is being calculated all the time.

So my first question is "how I may show the average Speed within a certain period (e.g. Last 10 second) and keep it on screen until next rotation start?

  1. Although I am able to show the Timer Counter on the bottom, there is an issue with this counter that sometimes if the rotation is pickup in high speed, the timer may
  • Not restart
  • Jumping a few seconds (sometimes jumping to 12/13)

What may I improve to avoid the above?

I am new to the forum and Coding, if my questions or how I ask is not up to forum standard, I apologise.

void setup()
{
  Serial.begin(9600);
  attachInterrupt(digitalPinToInterrupt(2), Pulse_Event, RISING);


  delay(1000);  

  // OLED 0.96" Display:
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setRotation(0);
  display.setTextWrap(false);
  display.dim(0);

}

void loop()
{
  LastTimeCycleMeasure = LastTimeWeMeasured;  // Store the LastTimeWeMeasured in a variable.
  CurrentMicros = micros();  // Store the micros() in a variable.
  curTime = millis();

  if(CurrentMicros < LastTimeCycleMeasure)
  {
    LastTimeCycleMeasure = CurrentMicros;
  }

  // Calculate the frequency:
  FrequencyRaw = 10000000000 / PeriodAverage;  // Calculate the frequency using the period between pulses.
  
  // Detect if pulses stopped or frequency is too low, so we can show 0 Frequency:
  if(PeriodBetweenPulses > ZeroTimeout - ZeroDebouncingExtra || CurrentMicros - LastTimeCycleMeasure > ZeroTimeout - ZeroDebouncingExtra)
  {  // If the pulses are too far apart that we reached the timeout for zero:
    FrequencyRaw = 0;  // Set frequency as 0.
    ZeroDebouncingExtra = 2000;  // Change the threshold a little so it doesn't bounce.
  }
  else
  {
    ZeroDebouncingExtra = 0;  // Reset the threshold to the normal value so it doesn't bounce.
  }

  FrequencyReal = FrequencyRaw / 10000;  // Get frequency without decimals.
                                          // This is not used to calculate RPM but we remove the decimals just in case
                                          // you want to print it.


  // Calculate the RPM:
  RPM = FrequencyRaw / PulsesPerRevolution * 60;  // Frequency divided by amount of pulses per revolution multiply by
                                                  // 60 seconds to get minutes.
  RPM = RPM / 10000;  // Remove the decimals.

  // Calculate Speed KM/h with my rotation device standard 19mm Diameter
  RealTimeSpeed = ((FrequencyReal / PulsesPerRevolution * 60)* 5.97 *60)/100000 ;


  if(curTime-cntTime>resetTime) { // reset when less than 30RPM (dt>2s)
    cnt = measureCnt = 0;
    rpm = 0;
  }
  if(cnt==1) startTime = cntTime;
  if(cnt-measureCnt>=minRotNum) {
    rpm = 60000L*(cnt-measureCnt)/(cntTime-measureTime);
    //Serial.println(String("Cnt=")+cnt+" dcnt="+(cnt-measureCnt)+" dtime="+(cntTime-measureTime));
    measureCnt = cnt;
    measureTime = cntTime;
  }
  rotTime = (cntTime-startTime)/1000; // time in seconds
  if(cnt>1 || !dispRotTime) {  // keep previous time on the OLED until new rotation starts
    dispRotTime = rotTime;
    dispCnt = cnt;
  }
  


  // Smoothing RPM:
  total = total - readings[readIndex];  // Advance to the next position in the array.
  readings[readIndex] = RPM;  // Takes the value that we are going to smooth.
  total = total + readings[readIndex];  // Add the reading to the total.
  readIndex = readIndex + 1;  // Advance to the next position in the array.

  if (readIndex >= numReadings)  // If we're at the end of the array:
  {
    readIndex = 0;  // Reset array index.
  }
  
  // Calculate the average:
  average = total / numReadings;  // The average value it's the smoothed result.

  // OLED 0.96" Display:
  // Convert variable into a string, so we can change the text alignment to the right:
  // It can be also used to add or remove decimal numbers.
  char string[10];  // Create a character array of 10 characters
  // Convert float to a string:
  dtostrf(average, 6, 0, string);  // (<variable>,<amount of digits we are going to use>,<amount of decimal digits>,<string name>)

  display.clearDisplay();  // Clear the display so we can refresh.
  display.setTextSize(2);  // Set text size. We are using a custom font so you should always use the text size of 0.
  display.setCursor(0, 0);  // (x,y).
  display.println("RPM:");  // Text or value to print.

  // Print variable with right alignment:
  display.setCursor(40, 0);  // (x,y).
  display.println(string);  // Text or value to print.

  // Print Max RPM variable with right alignment:
  display.setCursor(0, 16);  // (x,y).
  display.println("Avg:");  // Text or value to print.
  display.setCursor(50, 16);  // (x,y).
  display.println(dispSpeed);  // Text or value to print.

  // Print KM/h variable with right alignment:
  display.setCursor(78, 32);  // (x,y).
  display.println("KM/h");  // Text or value to print.
  display.setCursor(50, 32);  // (x,y).
  display.println(RealTimeSpeed);  // Text or value to print.

  // Print Time variable with right alignment:
  display.setCursor(0, 48);  // (x,y).
  display.println("Time:");  // Text or value to print.
  display.setCursor(115, 48);  // (x,y).
  display.println("s");  // Text or value to print.
  display.setCursor(80, 48);  // (x,y).
  display.println(dispRotTime);  // Text or value to print.

  display.display();  // Print everything we set previously.

}
void Pulse_Event()  // The interrupt runs this to calculate the period between pulses:
{

  PeriodBetweenPulses = micros() - LastTimeWeMeasured;  // Current "micros" minus the old "micros" when the last pulse happens.
                                                        // This will result with the period (microseconds) between both pulses.
                                                        // The way is made, the overflow of the "micros" is not going to cause any issue.

  LastTimeWeMeasured = micros();  // Stores the current micros so the next time we have a pulse we would have something to compare with.

  cnt++;
  cntTime = millis();

  if(PulseCounter >= AmountOfReadings)  // If counter for amount of readings reach the set limit:
  {
    PeriodAverage = PeriodSum / AmountOfReadings;  // Calculate the final period dividing the sum of all readings by the
                                                   // amount of readings to get the average.
    PulseCounter = 1;  // Reset the counter to start over. The reset value is 1 because its the minimum setting allowed (1 reading).
    PeriodSum = PeriodBetweenPulses;  // Reset PeriodSum to start a new averaging operation.


    // Change the amount of readings depending on the period between pulses.
    // To be very responsive, ideally we should read every pulse. The problem is that at higher speeds the period gets
    // too low decreasing the accuracy. To get more accurate readings at higher speeds we should get multiple pulses and
    // average the period, but if we do that at lower speeds then we would have readings too far apart (laggy or sluggish).
    // To have both advantages at different speeds, we will change the amount of readings depending on the period between pulses.
    // Remap period to the amount of readings:
    int RemapedAmountOfReadings = map(PeriodBetweenPulses, 40000, 5000, 1, 10);  // Remap the period range to the reading range.
    // 1st value is what are we going to remap. In this case is the PeriodBetweenPulses.
    // 2nd value is the period value when we are going to have only 1 reading. The higher it is, the lower RPM has to be to reach 1 reading.
    // 3rd value is the period value when we are going to have 10 readings. The higher it is, the lower RPM has to be to reach 10 readings.
    // 4th and 5th values are the amount of readings range.
    RemapedAmountOfReadings = constrain(RemapedAmountOfReadings, 1, 10);  // Constrain the value so it doesn't go below or above the limits.
    AmountOfReadings = RemapedAmountOfReadings;  // Set amount of readings as the remaped value.
  }
  else
  {
    PulseCounter++;  // Increase the counter for amount of readings by 1.
    PeriodSum = PeriodSum + PeriodBetweenPulses;  // Add the periods so later we can average.
  }

}

Please post your entire sketch including the global variable declarations and includes.

One problem you have is that you are sharing multibyte variables between the interrupt and your loop processing. Because this is an 8-bit processor it is possible that an interrupt can occur while the variable is updating. The solution is to disable interrupts while reading or manipulating these shared variables. Generally, I disable the interrupts, make a copy of the shared variable I'm interested in, then enable interrupts. Then I can use the copy in as many operations as I want without have to disable interrupts multiple times.

ToddL1962:
Please post your entire sketch including the global variable declarations and includes.

const byte PulsesPerRevolution = 1;
const unsigned long ZeroTimeout = 100000;
const byte numReadings = 2;  

volatile unsigned long LastTimeWeMeasured; 
volatile unsigned long PeriodBetweenPulses = ZeroTimeout+1000;
volatile unsigned long PeriodAverage = ZeroTimeout+1000; 
unsigned long FrequencyRaw;
unsigned long FrequencyReal; 
unsigned long RPM; 

unsigned long RealTimeSpeed;
unsigned long Speed;

unsigned int PulseCounter = 1;
unsigned long PeriodSum; 
unsigned long LastTimeCycleMeasure = LastTimeWeMeasured;
unsigned long CurrentMicros = micros(); 

unsigned int AmountOfReadings = 1;
unsigned int ZeroDebouncingExtra;

unsigned long readings[numReadings];
unsigned long readIndex;
unsigned long total; 
unsigned long average;


volatile unsigned long cntTime=0;
volatile unsigned long cnt=0;
unsigned long curTime;
unsigned long startTime=0;
const int resetTime = 2000;
const int minRotNum = 1;
int measureCnt=0;
volatile unsigned long rpm;
volatile unsigned long maxrpm=0;
unsigned long measureTime=0;
int dispRotTime=0;
int rotTime=0;
int dispCnt=0;
int dispSpeed=0;


#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64);

void setup()
{
  Serial.begin(9600);
  attachInterrupt(digitalPinToInterrupt(2), Pulse_Event, RISING);


  delay(1000);  

  // OLED 0.96" Display:
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setRotation(0);
  display.setTextWrap(false);
  display.dim(0);

}

void loop()
{
  LastTimeCycleMeasure = LastTimeWeMeasured;  // Store the LastTimeWeMeasured in a variable.
  CurrentMicros = micros();  // Store the micros() in a variable.
  curTime = millis();

  if(CurrentMicros < LastTimeCycleMeasure)
  {
    LastTimeCycleMeasure = CurrentMicros;
  }

  // Calculate the frequency:
  FrequencyRaw = 10000000000 / PeriodAverage;
  
  // Detect if pulses stopped or frequency is too low, so we can show 0 Frequency:
  if(PeriodBetweenPulses > ZeroTimeout - ZeroDebouncingExtra || CurrentMicros - LastTimeCycleMeasure > ZeroTimeout - ZeroDebouncingExtra)
  {  // If the pulses are too far apart that we reached the timeout for zero:
    FrequencyRaw = 0;  // Set frequency as 0.
    ZeroDebouncingExtra = 2000;  // Change the threshold a little so it doesn't bounce.
  }
  else
  {
    ZeroDebouncingExtra = 0;  // Reset the threshold to the normal value so it doesn't bounce.
  }

  FrequencyReal = FrequencyRaw / 10000;  // Get frequency without decimals.
                                          


  // Calculate the RPM:
  RPM = FrequencyRaw / PulsesPerRevolution * 60;  // Frequency divided by amount of pulses per revolution multiply by 60 seconds to get minutes.
  RPM = RPM / 10000;  // Remove the decimals.

  // Calculate Speed KM/h with my rotation device standard 19mm Diameter
  RealTimeSpeed = ((FrequencyReal / PulsesPerRevolution * 60)* 5.97 *60)/100000 ;


  if(curTime-cntTime>resetTime) { // reset when less than 30RPM (dt>2s)
    cnt = measureCnt = 0;
    rpm = 0;
  }
  if(cnt==1) startTime = cntTime;
  if(cnt-measureCnt>=minRotNum) {
    rpm = 60000L*(cnt-measureCnt)/(cntTime-measureTime);
    //Serial.println(String("Cnt=")+cnt+" dcnt="+(cnt-measureCnt)+" dtime="+(cntTime-measureTime));
    measureCnt = cnt;
    measureTime = cntTime;
  }
  rotTime = (cntTime-startTime)/1000; // time in seconds
  if(cnt>1 || !dispRotTime) {  // keep previous time on the OLED until new rotation starts
    dispRotTime = rotTime;
    dispCnt = cnt;
  }
  


  // Smoothing RPM:
  total = total - readings[readIndex];  // Advance to the next position in the array.
  readings[readIndex] = RPM;  // Takes the value that we are going to smooth.
  total = total + readings[readIndex];  // Add the reading to the total.
  readIndex = readIndex + 1;  // Advance to the next position in the array.

  if (readIndex >= numReadings)  // If we're at the end of the array:
  {
    readIndex = 0;  // Reset array index.
  }
  
  // Calculate the average:
  average = total / numReadings;  // The average value it's the smoothed result.

  char string[10];  // Create a character array of 10 characters
  // Convert float to a string:
  dtostrf(average, 6, 0, string);  // (<variable>,<amount of digits we are going to use>,<amount of decimal digits>,<string name>)

  display.clearDisplay();  // Clear the display so we can refresh.
  display.setTextSize(2);  // Set text size. We are using a custom font so you should always use the text size of 0.
  display.setCursor(0, 0);  // (x,y).
  display.println("RPM:");  // Text or value to print.

  // Print variable with right alignment:
  display.setCursor(40, 0);  // (x,y).
  display.println(string);  // Text or value to print.

  // Print Max RPM variable with right alignment:
  display.setCursor(0, 16);  // (x,y).
  display.println("Avg:");  // Text or value to print.
  display.setCursor(50, 16);  // (x,y).
  display.println(dispSpeed);  // Text or value to print.

  // Print KM/h variable with right alignment:
  display.setCursor(78, 32);  // (x,y).
  display.println("KM/h");  // Text or value to print.
  display.setCursor(50, 32);  // (x,y).
  display.println(RealTimeSpeed);  // Text or value to print.

  // Print Time variable with right alignment:
  display.setCursor(0, 48);  // (x,y).
  display.println("Time:");  // Text or value to print.
  display.setCursor(115, 48);  // (x,y).
  display.println("s");  // Text or value to print.
  display.setCursor(80, 48);  // (x,y).
  display.println(dispRotTime);  // Text or value to print.

  display.display();  // Print everything we set previously.

}

void Pulse_Event()  // The interrupt runs this to calculate the period between pulses:
{

  PeriodBetweenPulses = micros() - LastTimeWeMeasured;  

  LastTimeWeMeasured = micros();  

  cnt++;
  cntTime = millis();

  if(PulseCounter >= AmountOfReadings)  // If counter for amount of readings reach the set limit:
  {
    PeriodAverage = PeriodSum / AmountOfReadings;  // Calculate the final period dividing the sum of all readings by the
                                                   // amount of readings to get the average.
    PulseCounter = 1;  // Reset the counter to start over. The reset value is 1 because its the minimum setting allowed (1 reading).
    PeriodSum = PeriodBetweenPulses;  // Reset PeriodSum to start a new averaging operation.



    int RemapedAmountOfReadings = map(PeriodBetweenPulses, 40000, 5000, 1, 10);  // Remap the period range to the reading range.
    // 1st value is what are we going to remap. In this case is the PeriodBetweenPulses.
    // 2nd value is the period value when we are going to have only 1 reading. The higher it is, the lower RPM has to be to reach 1 reading.
    // 3rd value is the period value when we are going to have 10 readings. The higher it is, the lower RPM has to be to reach 10 readings.
    // 4th and 5th values are the amount of readings range.
    RemapedAmountOfReadings = constrain(RemapedAmountOfReadings, 1, 10);  // Constrain the value so it doesn't go below or above the limits.
    AmountOfReadings = RemapedAmountOfReadings;  // Set amount of readings as the remaped value.
  }
  else
  {
    PulseCounter++;  // Increase the counter for amount of readings by 1.
    PeriodSum = PeriodSum + PeriodBetweenPulses;  // Add the periods so later we can average.
  }

}

Again, every one of your volatile variables are multi byte and shared between your main (loop) processing and the interrupt. You MUST disable interrupts when manipulating these variables or you are sure to get corruption.