How to make this calculation more precise?

For a school project I'm currently working on a metronome with LEDs; the beats per minute (the tempo) being controlled by a potentiometer. I'm making the circuit in Tinkercad before I'm physically making everything, because I want to make sure it's gonna work properly.

This is the code I wrote:

#include <LiquidCrystal.h>
#include <math.h>

LiquidCrystal lcd(7, 6, 5, 4, 3, 2); // Setting up the LCD which displays the BPM.

int sensorPin = A0; // Potentiometer
int led1 = 13;
int led2 = 12;
int led3 = 11;
int led4 = 10; // Yes I'm using 4 LEDs instead of one, I just like the looks of having 4 LEDs.
int sensorValue;
int outputValue;
float bpm_ms;
int counter = 0;
String bpm_str; // To display the BPM onto the lcd display.

void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  pinMode(led4, OUTPUT);
  
  lcd.begin(16,2);
  lcd.setCursor(0,0);
  lcd.print("BPM");
  
  Serial.begin(9600);
}

void loop() {

// Reading the potentiometer
  sensorValue = analogRead(sensorPin);

// Mapping to be able to choose a BPM between 60 and 170.
  outputValue = map(sensorValue, 0, 1023, 60, 170);

// To display the BPM with LEDs there has to be a certain amount of time between ticks, thus 60s (60000ms) / BPM.
  bpm_ms = float(60000.0/outputValue);

// Displaying the BPM on the lcd is basically converting the mapped sensorValue to a string.
  bpm_str = String(outputValue);  
  
  lcd.setCursor(6,0);
  lcd.print(bpm_str);
  
// This metronome is only used for 4/4 beats; when the counter hits 1 I want the 1st LED to blink and so on.

  counter += 1; 
  if(counter==1) {
    digitalWrite(led1,HIGH);
    delay(bpm_ms);
    digitalWrite(led1,LOW);
  }
  
  if(counter==2) {
    digitalWrite(led2,HIGH);
    delay(bpm_ms);
    digitalWrite(led2,LOW);
  }
  
  if(counter==3) {
    digitalWrite(led3,HIGH);
    delay(bpm_ms);
    digitalWrite(led3,LOW);
  }
  
  if(counter==4) {
    digitalWrite(led4,HIGH);
    delay(bpm_ms);
    digitalWrite(led4,LOW);
    counter = 0;
  }
}

I know it looks amateurish, please just keep in mind this is my first Arduino project :sweat_smile:

The issue is that delay() only takes integers as parameters, but because that ignores all the decimals my metronome isn't 100% accurate (I tested it with online BPM tap tools). Using something more precise like delayMicroseconds() doesn't work with my code: delayMicroseconds() doesn't take large numbers as parameters:

For delays longer than a few thousand microseconds, you should use delay() instead.

bpm_ms = float(60000.0/outputValue) calculates the amount of milliseconds between ticks, so

bpm_ms = float(60000000.0/outputValue) would calculate the amount of microseconds.

This would result in bpm_ms to be 631579 (rounded) for a BPM of 95, a large number delayMicroseconds() doesn't work with.

This is why I was wondering if anyone knows a way to make the calculations I use in my code more precise. If you happen to know a way where my code didn't need to be completely altered, it would be amazing but I'm also okay with changing my code if it's to a point that I still understand what's happening :grinning:

Use the blink without delay technique, but with micros()

ayynoway:
This would result in bpm_ms to be 631579 (rounded) for a BPM of 95, a large number delayMicroseconds() doesn't work with.

You can delayMicroseconds for billions and billions of microseconds. Just make sure you don't accidentally use an int because an int can't hold a number larger than 32767 and the compiler doesn't treat this as an error.

But using any kind of delay is the wrong way to get accurate timing. Use the BlinkWithoutDelay method.

MorganS:
You can delayMicroseconds for billions and billions of microseconds.

...but even the reference advises against it.

You can and should use microseconds. You should adjust the value to get the desired results... Your current sketch calculates a delay assuming that no time at all is spent in the rest of your sketch. That is clearly not true. Put in an adjustment factor for that and adjust it until you get a BPM that matches the value displayed on the LCD.

Using an array for the four LED pins simplifies the code that rotates through the LEDs.

#include <LiquidCrystal.h>


LiquidCrystal lcd(7, 6, 5, 4, 3, 2); // Setting up the LCD which displays the BPM.


const byte Pot_AI_Pin = A0; // Potentiometer AnalogInput Pin
const byte LED_Count = 4;
const byte LED_DO_Pins[LED_Count] = {13, 12, 11, 10};
int CurrentBPM = 60;


unsigned long OnTimeMicroseconds;
byte BeatCounter = 0;


void setup()
{
  Serial.begin(115200);
  while (!Serial);


  for (byte i = 0; i < LED_Count; i++)
    pinMode(LED_DO_Pins[i], OUTPUT);


  lcd.begin(16, 2);
  lcd.setCursor(0, 0);
  lcd.print("BPM");
}


void loop()
{
  // Read the potentiometer
  int potValue = analogRead(Pot_AI_Pin);


  // Mapping to be able to choose a BPM between 60 and 170.
  int desired_BPM = map(potValue, 0, 1023, 60, 170);


  if (desired_BPM != CurrentBPM)
  {
    CurrentBPM = desired_BPM;


    // To display the BPM with LEDs there has to be a certain amount of time between ticks, thus 60s 
    // (60000000 microseconds) / BPM.
    OnTimeMicroseconds = (60000000UL / CurrentBPM) - 12;  // Subtract the time the rest of the code takes


    // Display CurrentBPM on the lcd
    lcd.setCursor(6, 0);
    lcd.print(CurrentBPM);
  }


  // This metronome is only used for 4/4 beats; when the counter hits 1 I want the 1st LED to blink and so on.


  digitalWrite(LED_DO_Pins[BeatCounter], HIGH);
  delayMicroseconds(OnTimeMicroseconds);
  digitalWrite(LED_DO_Pins[BeatCounter], LOW);


  BeatCounter = (BeatCounter + 1) % LED_Count;  // Next LED
}

Or don't use delay..() at all:

#include <LiquidCrystal.h>

LiquidCrystal lcd(7, 6, 5, 4, 3, 2); // Setting up the LCD which displays the BPM.

#define NUM_LEDS        4           //number of LEDs
#define POT_UPDATE      50ul        //mS per POT read
#define MICROS_PER_MIN  60000000ul  //uS per minute
const byte 
    sensorPin = A0,     //potentiometer
    led1 = 13,
    led2 = 12,
    led3 = 11,
    led4 = 10,
    grpinLEDs[NUM_LEDS] =
    {
        led1,
        led2,
        led3,
        led4
        
    };

unsigned long 
    bpm_ms;
char
    szBPM[12];
    
void setup() 
{
    for( byte i=0; i<NUM_LEDS; i++ )
        pinMode( grpinLEDs[i], OUTPUT);
 
    lcd.begin(16,2);
    lcd.setCursor(0,0);
    lcd.print("BPM");
 
    Serial.begin(9600);
    
}//setup

void Metronome( void )
{
    static byte
        lastidxLED = 3,
        idxLED = 0;
    unsigned long
        timeNow;
    static unsigned long
        timeBeat = 0;

    timeNow = micros();
    //has the BPM time expired yet?
    if( (timeNow - timeBeat) >= bpm_ms )
    {
        //yes; turn off the currently-on LED and turn on the next
        digitalWrite( grpinLEDs[lastidxLED], LOW );
        digitalWrite( grpinLEDs[idxLED], HIGH );
        //remember the currently-on LED...
        lastidxLED = idxLED;
        //and prep for the next, keeping in mind there are only NUM_LEDS
        idxLED++;
        if( idxLED == NUM_LEDS )
            idxLED = 0;

        //setup for timing this LED on time
        timeBeat = timeNow;

    }//if
    
}//Metronome

void SetBPM( void )
{
    static unsigned long
        timePOT = 0;
    unsigned long
        timeNow;
    static int
        lastoutputValue = -1;
    int
        outputValue;

    //update the pot reading every POT_UPDATE mS
    timeNow = millis();
    if( timeNow - timePOT < POT_UPDATE )
        return;

    //setup to time next POT update
    timePOT = timeNow;
    
    // Mapping to be able to choose a BPM between 60 and 170
    outputValue = map( analogRead( sensorPin ), 0, 1023, 60, 170 );
    sprintf( szBPM, "%d", outputValue );
    
    // To display the BPM with LEDs there has to be a certain amount 
    // of time between ticks, thus 60s (60E+06uS) / BPM
    bpm_ms = MICROS_PER_MIN/(unsigned long)outputValue;

    //only update the LCD if the BPM has changed
    if( outputValue != lastoutputValue )
    {
        lastoutputValue = outputValue;
        
        lcd.setCursor( 6,0 );
        lcd.print( szBPM );
        //debug
        Serial.print( "BPM: " ); Serial.println( szBPM );
        
    }//if
    
}//SetBPM

void loop() 
{
    //update pot and do the metronome (LEDs)
    SetBPM();
    Metronome();

}//loop

johnwasser:
You can and should use microseconds. You should adjust the value to get the desired results... Your current sketch calculates a delay assuming that no time at all is spent in the rest of your sketch. That is clearly not true. Put in an adjustment factor for that and adjust it until you get a BPM that matches the value displayed on the LCD.

Using an array for the four LED pins simplifies the code that rotates through the LEDs.

#include <LiquidCrystal.h>

LiquidCrystal lcd(7, 6, 5, 4, 3, 2); // Setting up the LCD which displays the BPM.

const byte Pot_AI_Pin = A0; // Potentiometer AnalogInput Pin
const byte LED_Count = 4;
const byte LED_DO_Pins[LED_Count] = {13, 12, 11, 10};
int CurrentBPM = 60;

unsigned long OnTimeMicroseconds;
byte BeatCounter = 0;

void setup()
{
 Serial.begin(115200);
 while (!Serial);

for (byte i = 0; i < LED_Count; i++)
   pinMode(LED_DO_Pins[i], OUTPUT);

lcd.begin(16, 2);
 lcd.setCursor(0, 0);
 lcd.print("BPM");
}

void loop()
{
 // Read the potentiometer
 int potValue = analogRead(Pot_AI_Pin);

// Mapping to be able to choose a BPM between 60 and 170.
 int desired_BPM = map(potValue, 0, 1023, 60, 170);

if (desired_BPM != CurrentBPM)
 {
   CurrentBPM = desired_BPM;

// To display the BPM with LEDs there has to be a certain amount of time between ticks, thus 60s
   // (60000000 microseconds) / BPM.
   OnTimeMicroseconds = (60000000UL / CurrentBPM) - 12;  // Subtract the time the rest of the code takes

// Display CurrentBPM on the lcd
   lcd.setCursor(6, 0);
   lcd.print(CurrentBPM);
 }

// This metronome is only used for 4/4 beats; when the counter hits 1 I want the 1st LED to blink and so on.

digitalWrite(LED_DO_Pins[BeatCounter], HIGH);
 delayMicroseconds(OnTimeMicroseconds);
 digitalWrite(LED_DO_Pins[BeatCounter], LOW);

BeatCounter = (BeatCounter + 1) % LED_Count;  // Next LED
}

I would use a 16 bit timer in CTC mode and use the ISR to blink the leds. Accurate and very fine grained timing.