Ways of counting pulses for analog speedometer project

Hello everyone!

I am starting with arduino programming and I’m looking for some guidance/code review help.

I have found some code online that gets me a good starting point, but this doesn’t seem to update fast enough for what I’d like to do. I am wondering if someone might have a better idea on how to implement this.

Here is the code I’ve been using. Works totally fine and outputs to display, just slower than I’d hoped, which seems to be from the 1 second measurement of the counter timing. Not sure if there is a better way to accomplish this with less code, or perhaps faster code.

Ultimately I want to output this to one of the Switch X27 stepper motors to make a custom analog speedometer for a gauge cluster project. Real car, not a driving sim.

This reads from a Toyota Vehicle Speed Sensor (VSS) that outputs 4 pulses per revolution. It is a 3 wire sensor (gnd, vcc, signal)

Thanks for any help!

#include <Adafruit_SSD1306.h>
#include <splash.h>
#include <SwitecX25.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>




#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

#define OLED_DC     6
#define OLED_CS     7
#define OLED_RESET  8
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &SPI, OLED_DC, OLED_RESET, OLED_CS);

const int hardwareCounterPin = 5;
const int samplePeriod = 1000; //in milliseconds
const float pulsesPerMile = 4000; // this is pulses per mile for Toyota. Other cars are different.
const float convertMph = pulsesPerMile / 3600;
unsigned int count;
float mph;
unsigned int imph;
int roundedMph;
int previousMph;
int prevCount;


void setup() {

  // put your setup code here, to run once:
  Serial.begin(115200);

  TCCR1A = 0; //Configure hardware counter
  TCNT1 = 0;  // Reset hardware counter to zero

  if (!display.begin(SSD1306_SWITCHCAPVCC)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Don't proceed, loop forever
  }
}
void loop() {
  // put your main code here, to run repeatedly:


  /////////////////////////////////////////////////////////////
  // This uses the hardware pulse counter on the Arduino.
  // Currently it collects samples for one second.
  //
  bitSet(TCCR1B, CS12); // start counting pulses
  bitSet(TCCR1B, CS11); // Clock on rising edge
  delay(samplePeriod); // Allow pulse counter to collect for samplePeriod
  TCCR1B = 0; // stop counting
  count = TCNT1; // Store the hardware counter in a variable
  TCNT1 = 0;     // Reset hardware counter to zero
  mph = (count / convertMph) * 10; // Convert pulse count into mph.
  imph = (unsigned int) mph; // Cast to integer. 10x allows retaining 10th of mph resolution.

  int x = imph / 10;
  int y = imph % 10;

  // Round to whole mile per hour
  if (y >= 5) {
    roundedMph = x + 1;
  } else {
    roundedMph = x;
  }

  //If mph is less than 1mph just show 0mph.
  //Readings of 0.9mph or lower are some what erratic and can
  //occasionally be triggered by electrical noise.
  if (x == 0) {
    roundedMph = 0;
  }

  // Don't display mph readings that are more than 50 mph higher than the
  // previous reading because it is probably a spurious reading.
  // Accelerating 50mph in one second is rocketship fast so it is probably
  // not real.
  if ((roundedMph - previousMph) > 50) {
    Serial.print(previousMph);
    Serial.println("mph");
  } else {
    Serial.print(roundedMph);
    Serial.println("mph");
  }

  previousMph = roundedMph; // Set previousMph for use in next loop.

  display.clearDisplay();
  display.setCursor(1, 15);

  display.setTextSize(5);             // Draw 2X-scale text
  display.setTextColor(WHITE);
  display.print(roundedMph);

  display.setTextSize(2);
  display.print("mph");
  display.display();


}

How can it update faster than every second when you delay(samplePeriod) [which is 1000 msec]. How about you lower this value?

Also, after every measurement period, you clear the entire display and redraw everything. That takes time.

I would suggest not resetting TCNT1 to zero. Just capture the counter value, then calculate...

uint16_t count_now = TCNT1;
uint16_t elapsed_count = count_now - count_previous;
count_previous = count_now;

You can then just let the counter free run and use millis() for timing instead of delay().
See blink without delay example sketches for how to achieve this.
Now the hardware can be counting pulses for the next period while your code is off updating the display.

Some good suggestions already as to how to improve update speed.
Of course reducing sample time also reduces accuracy (less counts over shorter period of time).
If that is a problem implement a rolling average such that you display the result every 100ms but count the total pulses over the last 1000ms.

blh64:
How can it update faster than every second when you delay(samplePeriod) [which is 1000 msec]. How about you lower this value?

Also, after every measurement period, you clear the entire display and redraw everything. That takes time.

Well, I kind of figured the 1 second sample period was driving the slow update. I was not sure if there was perhaps a better method to accomplish the same goal. I'm sure there are multiple ways to getting the same outcome.

Since this is a speedometer project, the counting delay seems more detrimental for city driving as the speed can fluctuate greatly within a short time with low gears, etc. When on the highway, sudden changes aren't as drastic or as feasible due to power and gearing.

If there is a better way to update the OLED display, I'm all ears. I only patched together what I could to see it work and to understand the code. Thank you for the input, I appreciate it.

pcbbc:
I would suggest not resetting TCNT1 to zero. Just capture the counter value, then calculate...

uint16_t count_now = TCNT1;

uint16_t elapsed_count = count_now - count_previous;
count_previous = count_now;




You can then just let the counter free run and use millis() for timing instead of delay().
See blink without delay example sketches for how to achieve this.
Now the hardware can be counting pulses for the next period while your code is off updating the display.

Some good suggestions already as to how to improve update speed.
Of course reducing sample time also reduces accuracy (less counts over shorter period of time).
If that is a problem implement a rolling average such that you display the result every 100ms but count the total pulses over the last 1000ms.

This seems like a good approach. I'll try patching up the code in the morning and give it a try. I am using an Uno board, and one thing I couldn't directly find, was if using the other timers on the board might be better. Thank you for the help, I appreciate it.

you could use an interrupt that makes snapshots of micros() and use the timedifference of two snapshots for calculating the speed. The interrupt-service-routine (in short ISR) should only store the snapshots and maybe calculate the difference of the two time-snapshots. This should give the maximum time-precision.
Everything else about the calculation of the speed should be done outside the ISR.
Then you have to know the Circumference of the wheel very precise to calculate the speed
especially floating-point-caclulations need a lot of processorcycles.

As each two pulses mean the surface of the wheel has made a certain distance.

I haven't coded much with interupts. I guess in this case it should be considered / tested if it is better to disable interrupts through the rest of the speed-calculation. I'm not sure.

best regards Stefan

StefanL38:
I guess in this case it should be considered / tested if it is better to disable interrupts through the rest of the speed-calculation. I'm not sure.

Yes, the simple approach of just storing the time at each interrupt, introduces a risk that the ISR might be called multiple times before the main program has a chance to read the time. You can use a flag in the ISR to communicate the main program status. The ISR will not update a time value unless it's clear. Upon updating the time, it should immediately set the flag. In this way, the main program can always be sure that the time value represents the interval between the first pulse after it enables the timer, and the following pulse. The same flag also tells the main program when a time value is "ready".

So in the main code,

timeReady = false;
while (timeReady == false) {}
[process time value from ISR]

(the while statement must be converted to an 'if', with state variables, if it is to be multitasking...)

in the ISR,

if (timeReady == false)
{
  time = micros();
  timeReady = true;
}