UPDATE: had to fix some html entities in my posted code. Hopefully it's okay. I've since posted this code on github, so check it out at https://github.com/torriem/ppmreader/.
I've received a number of comments on the Youtube video about the flow meter I was working on. Here's the video:
In the end the project was a failure. My electric valve hack job was just too hard to work with. Also I never could dial in the PID look quite right. In the future I will look at this again, using a valve more designed for this purpose. PID tuning is really hard! Especially when the system responds in non-linear ways. For example the plastic hose expands slightly under pressure, so when you first crack the valve you get a spurt of high-speed fluid which then drops off quickly. Putting the flow meter before the valve in the high pressure seems to be the most logical thing to do.
A number of Youtubers are saying they want to see my code. Unfortunately I don't see how my code in its entirety is going to help most of them. However,I would like to post the code that I use to actually read the flow meter.
Ostensibly it's a very easy thing to do. Most flow meters use a hall effect sensor, so every time the wheel turns around you get so many edges in a high/low signal. So all you have to do is measure the elapsed time between these pulses and you've got it!
Except it's not quite that simple. There's one problem especially that is vexing. Suppose you are spinning at a particular rate (measured in pulses per minute). Suddenly the wheel stops turning. If you are just measuring elapsed time then you'll have to wait an indefinite amount of time to see if either the wheel is stopped, or maybe it's just turning really slowly. There are probably a number of ways to solve this problem, but I find the simplest way is to just define a time cutoff. If a certain amount of time has passed without a pulse, we just set our speed to zero. That works for most real-world applications.
Other improvements and complicates include using a moving window to smooth out the average PPM that you're getting.
Anyway I've created a simple class library that makes working with measure average pulse per minute fairly simple and easy. It works with anything that generates a PPM signal. So hall-effect flow sensors, wheel speed sensors, etc. Here's the code and then I'll share a few things about using it:
ppmreader.h
/* Licensed under the MIT license
* Copyright 2014 Michael Torrie
* torriem@gmail.com
*/
#ifndef __PPMREADER_H__
#define __PPMREADER_H__
#define TICKS_PER_CALC 10
class PPMReader
{
protected:
uint32_t last_interrupt;
uint32_t last_times[TICKS_PER_CALC];
uint8_t last_time;
uint16_t ave_ppm;
public:
uint8_t record;
uint32_t totalpulses;
uint16_t zero_time; //ms since last interrupt to zero ave ppm (stopped)
uint16_t ppm_cutoff; //ppm at which we may as well just go to zero.
//probably not needed check() has other better ways
PPMReader();
uint16_t get_ppm(void);
void reset(void);
void on_trigger(void); //must be called from a static isr wrapper func
};
#endif
ppmreader.cpp
/* Licensed under the MIT license
* Copyright 2014 Michael Torrie
* torriem@gmail.com
*/
#include "ppmreader.h"
#include <limits.h>
//TODO should this be configurable by the caller?
//This sets up a moving average so that what was seen
//before is less important.
static const double coeff = 0.10;
void PPMReader::on_trigger(void)
{
uint32_t now;
uint32_t delta;
uint16_t ppm;
if (record)
totalpulses ++;
/* after the first 10 ticks, this will calculate an average
* speed over the last 10 ticks each tick. It's a bit slower
* but it's going to be a lot smoother. Hopefully eliminates
* spikes
*/
now = micros();
/* Not sure if this is really necessary but wait for the ring
* buffer of last times to be completely full (at least
* TICKS_PER_CALC number of ticks) before starting calculations.
*/
if (0 == last_times[last_time]) {
last_times[last_time] = now;
last_time = (last_time + 1 ) % TICKS_PER_CALC;
return;
}
ppm = (uint16_t)( TICKS_PER_CALC *
/* convert microseconds to minutes */
1000000 * 60 /
/* divided by microseconds passed */
(now - last_times[last_time]) );
last_time = (last_time + 1 ) % TICKS_PER_CALC;
/* smooth ppm using a moving average. */
ave_ppm = ppm * coeff + ave_ppm * (1 - coeff);
last_interrupt = millis();
}
uint16_t PPMReader::get_ppm(void) {
uint32_t now;
uint16_t max_ppm;
//TODO possibly turn off interrupts here
//I'm tempted to leave them on though, as if the ISR runs
//durring this routine, it's not a big deal, but it could
//introduce some noise.
if (ave_ppm < ppm_cutoff) {
ave_ppm = 0;
return;
}
/* if too much time has elapsed since our last interrupt,
* let's zero out the average pulses/minute
*/
now = millis() - last_interrupt;
if (now > zero_time) {
/* if a certain amount of time has passed without an
* interrupt, assume we're stopped, since we're going
* so slow as to just as well be stopped.
*/
ave_ppm = 0;
} else {
/* calculate the maximum ppm, given time since last
* interrupt
*/
max_ppm = 60000 /*ms in a minute*/
/ now; //leaves us with per minute
/* if an interrupt by now would result in a ppm that
* is already lower than the current running average,
* at least we know we're slowing down, so go ahead and
* bring the average down
*/
if (ave_ppm > max_ppm) {
ave_ppm = max_ppm * coeff +
ave_ppm *
(1 - coeff);
}
}
}
PPMReader::PPMReader ()
{
totalpulses = 0;
//default values that can be overridden
ppm_cutoff = 50;
zero_time = 1000;
record = false;
reset();
}
uint16_t PPMReader::get_ppm(void) {
uint32_t now;
uint16_t max_ppm;
//TODO possibly turn off interrupts here
//I'm tempted to leave them on though, as if the ISR runs
//durring this routine, it's not a big deal, but it could
//introduce some noise.
if (ave_ppm < ppm_cutoff) {
ave_ppm = 0;
return;
}
/* if too much time has elapsed since our last interrupt,
* let's zero out the average pulses/minute
*/
now = millis() - last_interrupt;
if (now > zero_time) {
/* if a certain amount of time has passed without an
* interrupt, assume we're stopped, since we're going
* so slow as to just as well be stopped.
*/
ave_ppm = 0;
} else {
/* calculate the maximum ppm, given time since last
* interrupt
*/
max_ppm = 60000 /*ms in a minute*/
/ now; //leaves us with per minute
/* if an interrupt by now would result in a ppm that
* is already lower than the current running average,
* at least we know we're slowing down, so go ahead and
* bring the average down
*/
if (ave_ppm > max_ppm) {
ave_ppm = max_ppm * coeff +
ave_ppm *
(1 - coeff);
}
}
}
PPMReader::PPMReader ()
{
totalpulses = 0;
//default values that can be overridden
ppm_cutoff = 50;
zero_time = 1000;
record = false;
reset();
}
How to use it
Using the code is straight foward. Instantiate a PPMReader (it's a class because in my code I used two of them). Create a static ISR trigger callback function in your code that call's the PPMReader object's on_trigger() method. Once the ISR is installed, the PPMReader will automatically count. At any time you look at it's public totalpulses variable to read the total since the last reset. This is how you can do things like measure a liquid.
To get a reading of the current PPM, call the get_ppm() function. Though the interrupt service routine is doing some average calculations on the fly, if the flow meter stops, then the ISR isn't being triggered, so get_ppm() will do cutoff detection and clean up the average accordingly.
I hope this helps the Youtube commenters. And please Google, stop creating Google+ profiles when people comment. You said this was not going to happen anymore but it still is!