Interrupt delay causing inaccurate target value

Hi all,

I am having trouble using interrupts to turn off a solenoid at an exact target value. I have a flowmeter that requires me to use the interrupt function. It is recommended that the interrupt be on for as long as possible to count pulses but for my use, I need the flowmeter to instantaneously turn off a solenoid once a certain mL value is reached. With the 1 second intervals that the interrupts are enabled and not updating the calculated mL values, it is easy to skip or miss the target value I'm looking to accurately obtain using the flowmeter.

Any advice on how to get constant feedback using the interrupt function?

Here is an example code of the flowmeter only updating values every second instead of keeping track of live values constantly:

/*
Liquid flow rate sensor -DIYhacking.com Arvind Sanjeev

Measure the liquid/water flow rate using this code. 
Connect Vcc and Gnd of sensor to arduino, and the 
signal line to arduino digital pin 2.
 
 */

byte statusLed    = 13;

byte sensorInterrupt = 0;  // 0 = digital pin 2
byte sensorPin       = 2;

// The hall-effect flow sensor outputs approximately 4.5 pulses per second per
// litre/minute of flow.
float calibrationFactor = 4.5;

volatile byte pulseCount;  

float flowRate;
unsigned int flowMilliLitres;
unsigned long totalMilliLitres;

unsigned long oldTime_Flowmeter;

void setup()
{
  
  Serial.begin(38400);

  pinMode(statusLed, OUTPUT);
  digitalWrite(statusLed, HIGH);  // We have an active-low LED attached
  
  pinMode(sensorPin, INPUT);
  digitalWrite(sensorPin, HIGH);

  pulseCount        = 0;
  flowRate          = 0.0;
  flowMilliLitres   = 0;
  totalMilliLitres  = 0;
  oldTime_Flowmeter           = 0;

  // The Hall-effect sensor is connected to pin 2 which uses interrupt 0.
  // Configured to trigger on a FALLING state change (transition from HIGH
  // state to LOW state)
  attachInterrupt(sensorInterrupt, pulseCounter, FALLING);
}


void loop()
{
   
   if((millis() - oldTime_Flowmeter) > 1000)    // Only process counters once per second
  { 
    // Disable the interrupt while calculating flow rate and sending the value to
    // the host
    detachInterrupt(sensorInterrupt);
        
    // Because this loop may not complete in exactly 1 second intervals we calculate
    // the number of milliseconds that have passed since the last execution and use
    // that to scale the output. We also apply the calibrationFactor to scale the output
    // based on the number of pulses per second per units of measure (litres/minute in
    // this case) coming from the sensor.
    flowRate = ((1000.0 / (millis() - oldTime_Flowmeter)) * pulseCount) / calibrationFactor;
    
    // Note the time this processing pass was executed. Note that because we've
    // disabled interrupts the millis() function won't actually be incrementing right
    // at this point, but it will still return the value it was set to just before
    // interrupts went away.
    oldTime_Flowmeter = millis();
    
    // Divide the flow rate in litres/minute by 60 to determine how many litres have
    // passed through the sensor in this 1 second interval, then multiply by 1000 to
    // convert to millilitres.
    flowMilliLitres = (flowRate / 60) * 1000;
    
    // Add the millilitres passed in this second to the cumulative total
    totalMilliLitres += flowMilliLitres;
      
    unsigned int frac;
    
    // Print the flow rate for this second in litres / minute
    Serial.print("Flow rate: ");
    Serial.print(int(flowRate));  // Print the integer part of the variable
    Serial.print(".");             // Print the decimal point
    // Determine the fractional part. The 10 multiplier gives us 1 decimal place.
    frac = (flowRate - int(flowRate)) * 10;
    Serial.print(frac, DEC) ;      // Print the fractional part of the variable
    Serial.print("L/min");
    // Print the number of litres flowed in this second
    Serial.print("  Current Liquid Flowing: ");             // Output separator
    Serial.print(flowMilliLitres);
    Serial.print("mL/Sec");

    // Print the cumulative total of litres flowed since starting
    Serial.print("  Output Liquid Quantity: ");             // Output separator
    Serial.print(totalMilliLitres);
    Serial.println("mL"); 

    // Reset the pulse counter so we can start incrementing again
    pulseCount = 0;
    
    // Enable the interrupt again now that we've finished sending output
    attachInterrupt(sensorInterrupt, pulseCounter, FALLING);
  }
}

/*
Inerrupt Service Routine
 */
void pulseCounter()
{
  // Increment the pulse counter
  pulseCount++;
}

So why do you disable the interrupts that long? And why do you only update the flow every 1second if that's to long...

I've heard many times that we need the interrupt to be enabled as long as possible to consistently count every pulse that has occurred. Otherwise you might be missing pulses that occurred while disabling and calculating things too often. Does this sound right?

retry with 500ms update interval

Yeah, but if disabling it for a long time is a problem, why do you do so?

And why do you only calculate the flow every 1 second? That makes you can also only act every 1 second.

I need my accuracy to be within a few milliliters of the real value. Decreasing the enabled interrupt time will mean I miss pulses meaning the readings become less accurate. But of course it isn't any better leaving the interrupt enabled too long as my target value wont be read on time. I hope that I can minimize both the missed pulses and enabled interrupt time enough to get an almost perfect reading as accuracy is very important for my current project. Thanks for giving me the confidence to decrease the 1 second interval much lower though!

Aero2019:
I need my accuracy to be within a few milliliters of the real value. Decreasing the enabled interrupt time will mean I miss pulses meaning the readings become less accurate. But of course it isn't any better leaving the interrupt enabled too long as my target value wont be read on time. I hope that I can minimize both the missed pulses and enabled interrupt time enough to get an almost perfect reading as accuracy is very important for my current project. Thanks for giving me the confidence to decrease the 1 second interval much lower though!

then you need something fast, like esp 8266, that will barely miss pulses when executing counting

Aero2019:
Decreasing the enabled interrupt time will mean I miss pulses meaning the readings become less accurate.

Yes, you say one thing, but in code you do the complete opposite. That's what I'm talking about.

Aero2019:
But of course it isn't any better leaving the interrupt enabled too long as my target value wont be read on time.

What? Nonsense, that's fully up to you just calculating the flow every second. If you want faster, just calculate it more often.

@KASSIMSAMJI, nonsense. If doing calculations in the order of every half a second will get you there there is no need for faster. Don't even expect a signal above 1MHz so every Arduino will do.

Since my flowrate is so fast I might need to update the calculation atleast every 100 milliseconds but I can see how a faster baud rate would help. Thank you both for the advice.

Why are you disabling interrupts while doing the calculations and sending the data through the serial line? Most code only disables interrupts while accessing the variable used by the interrupt routine, so that an interrupt does not occur during the access and change the value of the variable, but in your case the variable is a byte, so you can get away with never disabling interrupts if you handle it correctly.

Also, the usual method is to disable and re-enable interrupts, not detach and reattach the interrupt.

There is no need to ever reset pulseCount to 0, each time you do the calculations, save the value of pulseCount, and the next time you do the calculation the number of pulses will be equal to (current pulseCount - previous pulseCount)

The general structure of your loop function should be something like this:

if (time interval has elapsed){
pulseCountCurrent = pulseCount //no need to disable interrupts, single byte operations cannot be interrupted
//do all calculations here
// number of pulses used in calculations is pulseCountCurrent - pulseCountPrevious
//print data to serial port
pulseCountPrevious = pulseCountCurrent;
}

Then update it every it every 100ms, no problem. But keep the time you need to disable the interrupt to a minimum.

I made some changes to the code:

/*
Liquid flow rate sensor -DIYhacking.com Arvind Sanjeev

Measure the liquid/water flow rate using this code.
Connect Vcc and Gnd of sensor to arduino, and the
signal line to arduino digital pin 2.
 
 */
const byte StatusLedPin    = 13;
const byte SensorPin       = 2;

// The hall-effect flow sensor outputs approximately 4.5 pulses per second per
// litre/minute of flow.
const unsigned int PulsesPerLiter = 270;
const unsigned long FlowUpdateInterval = 100;

volatile byte pulseCount; 

unsigned long flowRate; //in ml * 10 (10^-4) per minute
unsigned long totalVolume; //in ml * 10 (10^-4)

void setup()
{
  Serial.begin(115200);
  
  digitalWrite(StatusLedPin, HIGH);  // We have an active-low LED attached
  pinMode(StatusLedPin, OUTPUT);
 
  pinMode(SensorPin, INPUT_PULLUP);

  // The Hall-effect sensor is connected to pin 2 which uses interrupt 0.
  // Configured to trigger on a FALLING state change (transition from HIGH
  // state to LOW state)
  attachInterrupt(digitalPinToInterrupt(SensorPin), pulseCounter, FALLING);
}


void loop()
{
  updateFlow();
}

void updateFlow()
{
  static unsigned long previousFlowMillis;
  const unsigned long MillisNow = millis();
  
  if((MillisNow - previousFlowMillis) >= FlowUpdateInterval)
  {
    //only a short break!
    noInterrupts();
    const byte LocalPulseCount = pulseCount;
    pulseCount = 0;
    interrupts();
    
    //calculate the volume passes since last calculation
    const unsigned int DeltaVolume = LocalPulseCount * 10000UL / PulsesPerLiter; //10000 = 1L in mL * 10 (10^-4)
    
    //Add to total valume
    totalVolume += DeltaVolume;
    
    //calculate flowrate in ml * 10 (10^-4) per minute
    //use previous millis to compenste if it took a little longer
    flowRate = DeltaVolume * 1000UL * 60 / (MillisNow - previousFlowMillis);
  }
}

/*
Inerrupt Service Routine
 */
void pulseCounter()
{
  // Increment the pulse counter
  pulseCount++;
}

I did change the unit of the variables to mL * 10 (10-4L (aka 12mL is represented as 120) and flow as 10-4L/min. This way everything can be done in integer math. Keeps things fast and accurate.

Didn't add the printing part back. But that's not that hard.