Question regarding using Adafruit flowmeter with Feather M0

Hello All,

Not sure if this is the best forum to post this but any help will be appreciated

I am attempting to use the LIQUID FLOW METER - PLASTIC 1/2" NPS THREADED (product # 828) with an Adafruit Feather M0 with RFM95 LoRa Radio - 900MHz - RadioFruit (product # 3178).

My understanding is that the sensor functions via the principle of the hall effect, as water flows it turns a flywheel with a magnet, as the magnet turns it induces a voltage which is measured using the digital read on the arduino. According to the example code shown below, I can expect 7.5 pulses for every 1L that passes through the sensor.

I began this process by using the provided example code for the sensor, linked here:GitHub - adafruit/Adafruit-Flow-Meter: Example code for the Adafruit liquid flow meters. This code works fine with an Arduino Uno or Mega, however, the interrupt code is not designed to work on the M0 with its ATSAMD21 controller.

To circumvent this I have tried to combine the flow meter example code with a timer approach designed for the ATSAMD21 based on this example code: SAMD21 Arduino Timer Example · GitHub

My code is shown below.
The key portions are the TC5_Handler ifunction and the loop.

The objective with the TC5_Handler function is that each time the interrupt is called (every 1 ms) a digital read of the Flow sensor pin is occurs. If the state of the pin has not changed then a counter increases which tracks the time between the last state change "return" is used to exit the function

If the state has changed then the number of pulses is increased. The sensor frequency in hertz is calculated by dividing 1000 by the elapsed time since the last pulse.

In the loop function the total volume in liters is calculated by dividing the total number of pulses by 7.5 pulses per second * 60 second per liter. I have set a delay of 1000 ideally the frequency and volume should be printed once a second

Calculation
// if a plastic sensor use the following calculation
// Sensor Frequency (Hz) = 7.5 * Q (Liters/min)
// Liters = Q * time elapsed (seconds) / 60 (seconds/minute)
// Liters = (Frequency (Pulses/second) / 7.5) * time elapsed (seconds) / 60
// Liters = Pulses / (7.5 * 60)

uint32_t sampleRate = 1; //sample rate in milliseconds, determines how often TC5_Handler is called


// Define necessary pins here
#define FLOWSENSORPIN 12 //just for an example



// count how many pulses!
volatile uint16_t pulses = 0;
// track the state of the pulse pin
volatile uint8_t lastflowpinstate;
// you can try to keep time of how long it is between pulses
volatile uint32_t lastflowratetimer = 0;
// and use that to calculate a flow rate
volatile float flowrate;
// save volume in liters
float liters;
void setup() {
  Serial.begin(9600);
  while (!Serial) ;
  Serial.println("Flow sensor test!");
  tcConfigure(sampleRate); //configure the timer to run at <sampleRate>Hertz
  tcStartCounter(); //starts the timer
  pinMode(FLOWSENSORPIN, INPUT);
  digitalWrite(FLOWSENSORPIN, HIGH);
  lastflowpinstate = digitalRead(FLOWSENSORPIN);
}

void loop() {
  //tcDisable(); //This function can be used anywhere if you need to stop/pause the timer
  //tcReset(); //This function should be called everytime you stop the timer

  Serial.print("Freq: "); Serial.println(flowrate);
  Serial.print("Pulses: "); Serial.println(pulses, DEC);

  float liters = pulses;
  liters /= 7.5;
  liters /= 60.0;

  Serial.print(liters); Serial.println(" Liters");

  delay(1000);
}

//this function gets called by the interrupt at <sampleRate>Hertz
void TC5_Handler (void) {
  //YOUR CODE HERE
  uint8_t x = digitalRead(FLOWSENSORPIN);
  if (x == lastflowpinstate) {
    lastflowratetimer++;
    return; // nothing changed!


  }

  if (x == HIGH) {
    //low to high transition!
    pulses++;

    lastflowpinstate = x;
    flowrate = 1000.0;
    flowrate /= lastflowratetimer;  // in hertz
    lastflowratetimer = 0;
  }

  lastflowpinstate = x;
  // END OF YOUR CODE
  TC5->COUNT16.INTFLAG.bit.MC0 = 1; //Writing a 1 to INTFLAG.bit.MC0 clears the interrupt so that it will run again
}

/*
    TIMER SPECIFIC FUNCTIONS FOLLOW
    you shouldn't change these unless you know what you're doing
*/

//Configures the TC to generate output events at the sample frequency.
//Configures the TC in Frequency Generation mode, with an event output once
//each time the audio sample frequency period expires.
void tcConfigure(int sampleRate)
{
  // Enable GCLK for TCC2 and TC5 (timer counter input clock)
  GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID(GCM_TC4_TC5)) ;
  while (GCLK->STATUS.bit.SYNCBUSY);

  tcReset(); //reset TC5

  // Set Timer counter Mode to 16 bits
  TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;
  // Set TC5 mode as match frequency
  TC5->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;
  //set prescaler and enable TC5
  TC5->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIV1024 | TC_CTRLA_ENABLE; //you can use different prescaler divisons here like TC_CTRLA_PRESCALER_DIV1 to get different ranges of frequencies
  //set TC5 timer counter based off of the system clock and the user defined sample rate or waveform
  TC5->COUNT16.CC[0].reg = (uint16_t) (SystemCoreClock / sampleRate - 1);
  while (tcIsSyncing());

  // Configure interrupt request
  NVIC_DisableIRQ(TC5_IRQn);
  NVIC_ClearPendingIRQ(TC5_IRQn);
  NVIC_SetPriority(TC5_IRQn, 0);
  NVIC_EnableIRQ(TC5_IRQn);

  // Enable the TC5 interrupt request
  TC5->COUNT16.INTENSET.bit.MC0 = 1;
  while (tcIsSyncing()); //wait until TC5 is done syncing
}

//Function that is used to check if TC5 is done syncing
//returns true when it is done syncing
bool tcIsSyncing()
{
  return TC5->COUNT16.STATUS.reg & TC_STATUS_SYNCBUSY;
}

//This function enables TC5 and waits for it to be ready
void tcStartCounter()
{
  TC5->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE; //set the CTRLA register
  while (tcIsSyncing()); //wait until snyc'd
}

//Reset TC5
void tcReset()
{
  TC5->COUNT16.CTRLA.reg = TC_CTRLA_SWRST;
  while (tcIsSyncing());
  while (TC5->COUNT16.CTRLA.bit.SWRST);
}

//disable TC5
void tcDisable()
{
  TC5->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE;
  while (tcIsSyncing());
}

Problems:

This code is severely under estimating the total flow, it is increasing at a rate of 1 pulse per second when the water is flowing and often the frequency is listed as inf which leads me to believe for some reasons the freq is being calculated when the time between state changes is zero (ie dividing by zero). Additionally, the freq and pulses are only printed when the pump is running. When the pump is off the code seems to simply loop through the TC_5 handler with the "lastflowratetimer" increasing.

Sample serial output:

08:53:21.288 -> Flow sensor test!
08:53:21.288 -> Freq: 0.00
08:53:21.288 -> Pulses: 0
08:53:21.288 -> 0.00 Liters
08:53:41.053 -> Freq: 0.00
08:53:41.053 -> Pulses: 1
08:53:41.053 -> 0.00 Liters
08:53:42.034 -> Freq: 2.15
08:53:42.034 -> Pulses: 2
08:53:42.034 -> 0.00 Liters
08:53:43.047 -> Freq: 0.22
08:53:43.047 -> Pulses: 3
08:53:43.047 -> 0.01 Liters
08:53:44.067 -> Freq: inf
08:53:44.067 -> Pulses: 4
08:53:44.067 -> 0.01 Liters
08:53:45.051 -> Freq: 0.40
08:53:45.051 -> Pulses: 5
08:53:45.051 -> 0.01 Liters
08:53:46.043 -> Freq: 7.69
08:53:46.043 -> Pulses: 6
08:53:46.043 -> 0.01 Liters
08:53:47.063 -> Freq: 7.69
08:53:47.063 -> Pulses: 6
08:53:47.063 -> 0.01 Liters
08:53:48.051 -> Freq: inf
08:53:48.051 -> Pulses: 7
08:53:48.051 -> 0.02 Liters
08:53:49.073 -> Freq: 0.69
08:53:49.073 -> Pulses: 8
08:53:49.073 -> 0.02 Liters

Any help would be appreciate. I apologize in advance if my question is not structured clearly or does not follow forum etiquette in some way, I am happy to add additional information for clarity.

Pulses per liter = 7.5 pulses per second * 60 seconds per minute = 450, liters = pulses / 450.

Yes, I agree. I am not entirely understanding your point. Could you elaborate? The calculation you mentioned is built into the code.

proctork42:
This code works fine with an Arduino Uno or Mega, however, the interrupt code is not designed to work on the M0 with its ATSAMD21 controller.

So, are you telling us that the Feather has no external interrupt facility? I didn't know that. If that is the case, I imagine the best bet is to use something that has, like a Pro Mini.

As I understand it, the Feather M0 does have an external interrupt facility, it is just different than the one used in the example code. Additionally, I need to use these boards because they have LoRa allowing me to transmit the flow data to the other side of the field.

proctork42:
As I understand it, the Feather M0 does have an external interrupt facility, it is just different than the one used in the example code.

I find that a bit hard to believe. External interrupt is what it is. If it is that different, I imagine it would be common knowledge, and there would be software examples aplenty. As things are, I think you are just bashing your head against the wall, and using a Pro Mini is starting to look like pretty good idea.

In my opinion I would move from this timer interrupt code to an external interrupt code. That is a far more common way of reading these flow rate sensors.

Use a rising or falling edge, to count the pulses and determine the time between them. Or determine the time for a number of pulses.

I think that the timer interrupt method which uses an accumulated count at one state to determine the frequency may not properly account for the number of degrees of the rotation the magnet is in front of the sensor.

@Cattledog, thank you for this insight. Using this approach I was able to get the code working as expected.
For those that come across this thread with similar questions, there is great documentation on how to use interrupts on this page: attachInterrupt() - Arduino Reference

I have included the working code below. I do have one more question regarding the timing, this may simply be due to my misunderstanding of how Hall sensors work.

In the original code the time between pulses is measured. The flow rate, which is set to 1000, is then divided by the time between pulses to determine the frequency in hz. Is this flow rate simply the actual flow rate of my pump? Meaning the code must be altered when used with different pumps and will not be effective if measuring flow with a variable flow rate?

// Define necessary pins here
#define FLOWSENSORPIN 12 //just for an example
// count how many pulses!
volatile uint16_t pulses = 0;
// track the state of the pulse pin
volatile uint8_t lastflowpinstate;
// you can try to keep time of how long it is between pulses
volatile uint32_t lastflowratetimer = 0;
// and use that to calculate a flow rate
volatile float flowrate;
// save volume in liters
float liters;
// save time in minutes

void setup() {
  Serial.begin(9600);
  while (!Serial) ;
  Serial.println("Flow sensor test!");
  pinMode(FLOWSENSORPIN, INPUT);
  digitalWrite(FLOWSENSORPIN, HIGH);
  attachInterrupt(digitalPinToInterrupt(FLOWSENSORPIN), Hall, RISING);
}

void loop() {
  float liters= pulses/ (7.5 *60); // 7.5 pulses per liter, 60 seconds per minute
 
  
  Serial.print("Freq   ");
  Serial.println(flowrate);
  Serial.print("Number of Pulses   ");
  Serial.println(pulses);
  Serial.print("Number of Liters   ");
  Serial.println(liters);
  delay(2000); 
}

void Hall(){
  pulses++;
  lastflowratetimer=millis()-lastflowratetimer;
  flowrate=1000;
  flowrate/= lastflowratetimer;
}

Typically, flowmeter codes are all the same, They use an Interrupt service routine to count the pulses over one second, and then do the maths. The only item you need to change is the pulses/litre factor, which depends on the sensor you are using. It is also the only thing you might need to adjust for greater accuracy, but probably not by much. If you are interested in rate, you might prefer to either average readings before display, or use a wider time window and adjust the maths accordingly.

Good job changing course.

On issue that I see with the code is that values are read from the ISR by the main code and they can be in the process of changing while they are being read. This was an issue with the Timer interrupt code as well. It's best to briefly suspend interrupts and make protected copies of the values to be used.

Read Nick Gammon's excellent tutorial on interrupts which discusses this point.

[gammon.com.au interrupts](http://gammon.com.au interrupts)