Issue with tachometer

Hey guys,

We made a sensor package at work here and we're having issues with the tach at lower speeds. Below 100 RPM the code doesn't want to show less than that. Anywhere between 120-300 RPM there's no problem and it's bang on to a photo tach. We started out with counting teeth over a fixed point in time, changed to measuring the interval. Tried a 20 tooth wheel, and a 40 tooth wheel. We've been able to improve things, but not getting it to where we need. Not too sure where to go with it. Is it too much for the board to handle. We are using a Mega board. Thanks!

//Version notes 6.5
//Updated Tachometer to measure RPM based on time taken for 5 counts, rather than counting teeth over 1/4 second
//Updated RPMtimeout to 500 miliseconds for better slow speed tachometer readings.
//Changed Tooth count to a 40 tooth even spaced tone wheel to get better low speed measurements.
//Changed tachometer to rising instead of falling

#include <DFRobot_HX711_I2C.h>
#include <DFRobot_LIS2DW12.h>
#include <LiquidCrystal_I2C.h>
#include <RunningAverage.h>
#include "LSM6DS3.h"
#include <Wire.h>

// Sampling rate
const int frequency = 5;
const unsigned long measurementInterval = 1000 / frequency;

// ───────── Load Cells ─────────
DFRobot_HX711_I2C Scale1(&Wire, 0x66);
DFRobot_HX711_I2C Scale2(&Wire, 0x64);
float tension1 = 0.0, tension2 = 0.0;
float scale1cal = 999.26;
float scale2cal = 685.34;

// ───────── Accelerometers ─────────
DFRobot_LIS2DW12_I2C acce2(&Wire, 0x19); // LIS2DW12
LSM6DS3 myIMU(I2C_MODE, 0x6A);           // LSM6DS3
float x2, y2, z2, pitch2, roll2;
RunningAverage RApitch2(5);
RunningAverage RAroll2(5);
float ax, ay, az, pitch, roll;

// ───────── Tachometer ─────────
const byte irSensorPin = 3;
const byte teethPerRevolution = 40;
volatile unsigned long lastToothMicros = 0;
volatile unsigned long toothIntervalSum = 0;
volatile byte toothCount = 0;
volatile bool newToothBatch = false;
float rpm = 0.0;
RunningAverage RArpm(5);

// ───────── Pressure Sensor ─────────
const int PressureSensor = A0;
const int numReadings = 4;
int readings[numReadings] = {0}, readIndex = 0, total = 0, average = 0;
float Press_cal = 9.18;

// ───────── LCD ─────────
LiquidCrystal_I2C lcd(0x27, 20, 4);

// ───────── Flow Meter ─────────
const byte sensorPin = 2;
const byte sensorInterrupt = 0;
const byte statusLed = 13;
float calibrationFactor = 24.98;
volatile byte pulseCountflow = 0;
float flowRate = 0.0;
unsigned int flowMilliLitres = 0;
float totalMilliLitres = 0.0;


// ───────── Setup ─────────
void setup() {
  Serial.begin(9600);
  Wire.begin();

   // LCD setup
  lcd.init();
  lcd.backlight();
  lcd.clear();


  // Accelerometer 1 (LIS2DW12)
  while (!acce2.begin()) {
    Serial.println("3 Axis Offline");
    lcd.setCursor(0, 0); lcd.print("3 Axis Offline");
    delay(1000);
  }
  acce2.softReset();
  acce2.continRefresh(true);
  acce2.setDataRate(DFRobot_LIS2DW12::eRate_50hz);
  acce2.setRange(DFRobot_LIS2DW12::e2_g);
  acce2.setFilterPath(DFRobot_LIS2DW12::eLPF);
  acce2.setFilterBandwidth(DFRobot_LIS2DW12::eRateDiv_4);
  acce2.setPowerMode(DFRobot_LIS2DW12::eContLowPwrLowNoise2_14bit);
  RApitch2.clear(); RAroll2.clear();
  Serial.println("3 Axis Online");
  lcd.setCursor(0, 0); lcd.print("3 Axis Online");
  delay(1000);

  // Accelerometer 2 (LSM6DS3)
  if (myIMU.begin() != 0) {
    Serial.println("6 Axis Offline");
    lcd.setCursor(0, 1);
    lcd.print("6 Axis Offline");
  } else {
    Serial.println("6 Axis Online");
    lcd.setCursor(0, 1);
    lcd.print("6 Axis Online");
    delay(1000);
  }

  // Load Cells
  while (!Scale1.begin()) {
    Serial.println("Load cell 1 Offline");
    lcd.setCursor(0, 2);
    lcd.print("LC 1 Offline");
    delay(1000);
  }
  Scale1.setCalibration(scale1cal);
  Serial.println("Load cell 1 Online");
  lcd.setCursor(0, 2);
  lcd.print("LC 1 Online");
  delay(1000);

  while (!Scale2.begin()) {
    Serial.println("Load cell 2 Offline");
    lcd.setCursor(0, 3);
    lcd.print("LC 2 Offline");
    delay(1000);
  }
  Scale2.setCalibration(scale2cal);
  Serial.print("Load cell 2 Online");
  lcd.setCursor(0, 3);
  lcd.print("LC 2 Online");
  delay(1000);
  delay(3000);
  //Set up LCD Display
  lcd.clear();
  lcd.setCursor(2, 0); lcd.print("Boat #1  Boat #2");
  lcd.setCursor(0, 1); lcd.print("P:");
  lcd.setCursor(0, 2); lcd.print("T:");
  lcd.setCursor(6, 3); lcd.print("RPM:");

  // Tachometer
  pinMode(irSensorPin, INPUT_PULLUP);
  pinMode(11, OUTPUT); digitalWrite(11, HIGH);
  attachInterrupt(digitalPinToInterrupt(irSensorPin), countTooth, FALLING);

  // Pressure Sensor
  for (int i = 0; i < numReadings; i++) readings[i] = 0;

  // Flow Meter
  pinMode(statusLed, OUTPUT); digitalWrite(statusLed, HIGH);
  pinMode(sensorPin, INPUT); digitalWrite(sensorPin, HIGH);
  attachInterrupt(sensorInterrupt, pulseCounter, FALLING);
}

// ───────── Main Loop ─────────
void loop() {
  static unsigned long lastLoopTime = 0;
  unsigned long currentMillis = millis();

  if (currentMillis - lastLoopTime >= measurementInterval) {
    lastLoopTime = currentMillis;

    readAccelerometers();
    readTachometer(currentMillis);
    readPressureSensor();
    readFlowMeter(currentMillis);
    readLoadCells();
    updateLCD();
    outputSerialData(currentMillis);
  }
}

// ───────── Modular Functions ─────────
void readAccelerometers() {
  //Read 6 axis
  ay = myIMU.readFloatAccelX();
  ax = myIMU.readFloatAccelY();
  az = myIMU.readFloatAccelZ();
  roll = atan2(ay, az) * 57.3;
  pitch = -(atan2(-ax, sqrt(ay * ay + az * az)) * 57.3);

  //Read 3 axis
  x2 = acce2.readAccX();
  y2 = acce2.readAccY();
  z2 = acce2.readAccZ();
  roll2 = atan2(y2, z2) * 57.3;
  pitch2 = atan2(-x2, sqrt(y2 * y2 + z2 * z2)) * 57.3;
  RAroll2.addValue(roll2);
  RApitch2.addValue(pitch2);
}

void readTachometer(unsigned long now) {
  static unsigned long lastRPMUpdate = 0;
  static unsigned long rpmTimeout = 500;

  noInterrupts();
  bool batchReady = newToothBatch;
  unsigned long intervalSum = toothIntervalSum;
  byte count = toothCount;
  newToothBatch = false;
  toothIntervalSum = 0;
  toothCount = 0;
  

  if (batchReady && count >= 1 && intervalSum > 0) {
    float avgToothInterval = intervalSum / float(count);
    float toothFrequency = 1e6 / avgToothInterval;
    rpm = (toothFrequency * 60.0) / teethPerRevolution;
    lastRPMUpdate = now;
  } else if (now - lastRPMUpdate > rpmTimeout) {
    rpm = 0;
  }
  RArpm.addValue(rpm);
  interrupts();
}

void readPressureSensor() {
  total -= readings[readIndex];
  readings[readIndex] = ((analogRead(PressureSensor) - 86) / Press_cal);
  total += readings[readIndex];
  readIndex = (readIndex + 1) % numReadings;
  average = total / numReadings;
}

void readFlowMeter(unsigned long now) {
  static unsigned long lastFlowTime = 0;
  if ((now - lastFlowTime) > measurementInterval) {
    detachInterrupt(sensorInterrupt);
    flowRate = ((1000.0 / (now - lastFlowTime)) * pulseCountflow) / calibrationFactor;
    lastFlowTime = now;
    flowMilliLitres = (flowRate / 60) * 1000;
    totalMilliLitres += flowMilliLitres;
    pulseCountflow = 0;
    attachInterrupt(sensorInterrupt, pulseCounter, FALLING);
  }
}

void readLoadCells() {
  tension1 = Scale1.readWeight(1);
  tension2 = Scale2.readWeight(1);
}

void updateLCD() {
  lcd.setCursor(2,1); lcd.print("                  ");
  lcd.setCursor(2,2); lcd.print("                   ");
  lcd.setCursor(10,3); lcd.print("          ");

  lcd.setCursor(2, 1); lcd.print(pitch,2);
  lcd.setCursor(11, 1); lcd.print(RApitch2.getAverage(),2);
  lcd.setCursor(2, 2); lcd.print(tension1,1);
  lcd.setCursor(11, 2); lcd.print(tension2,1);
  lcd.setCursor(10, 3); lcd.print(RArpm.getAverage(),0);
}

void outputSerialData(unsigned long now) {
  Serial.print(now / 1000.0, 2); Serial.print(",");
  Serial.print(average); Serial.print(",");
  Serial.print(flowRate); Serial.print(",");
  Serial.print(RArpm.getAverage()); Serial.print(",");
  Serial.print(tension1); Serial.print(",");
  Serial.print(tension2); Serial.print(",");
  Serial.print(pitch); Serial.print(",");
  Serial.print(roll); Serial.print(",");
  Serial.print(RApitch2.getAverage()); Serial.print(",");
  Serial.print(RAroll2.getAverage()); Serial.println();
}

// ───────── Interrupts ─────────
void countTooth() {
  static unsigned long prevToothMicros = 0;
  unsigned long now = micros();
  unsigned long interval = now - prevToothMicros;

  if (interval > 1000 ) {
    toothIntervalSum += interval;
    prevToothMicros = now;
    toothCount++;
    if (toothCount >= 10) {
      lastToothMicros = now;
      newToothBatch = true;
    }
  }
}

void pulseCounter() {
  pulseCountflow++;
}

Please read and use this link: How to get the best out of this forum - Development Tools / IDE 1.x - Arduino Forum

Note that schematics are mandatory.

Just from a quick look you have a lot of work going on in the ISR void countTooth, why not just increment the count there, and move all the calculations (if even required) to void readTachometer where some already are?

1 Like

Over what RPM range should this tachometer function ?

300 RPM with a 40 tooth cog means pulses at about 200Hz or 5ms intervals which is well within the capabilities of a Mega and, since you are using an interrupt, it should not be vulnerable to blocking code in the loop.

Your tachometer code looks rather complicated for the simple task of counting pulses in unit time and maybe some averaging. I'd have a separate sketch with the just the tachometer code and get that working correctly first, then integrate it with the rest of the code.

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