Tải bản đầy đủ (.pdf) (10 trang)

Practical Arduino Cool Projects for Open Source Hardware- P21 pdf

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (206.03 KB, 10 trang )

CHAPTER 10  WATER FLOW GUAGE
unsigned long oldTime;
The setup routine is fairly straightforward except for the definition of the interrupt handler, which
we'll get to in a moment.
First it makes some calls to methods in the lcd object. The begin call specifies the number of
columns and rows in the attached LCD, then the cursor is set to character 0 on row 0 (the top row) before
16 spaces are printed. The cursor is then set to character 0 on row 1 (the second row) and another 16
spaces printed.
This ensures that the LCD starts off blank with no leftover characters visible on the display.
Normally it's not a problem, but this just ensures that anything left from a previous program you may
have run, such as an LCD test program, is erased.
void setup()
{
lcd.begin(16, 2);
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print(" ");
A serial connection is then opened to the host to report values back.
Serial.begin(38400);
The pin to control the status LED is switched to an output and then set HIGH, which turns off the
LED since we connected it via a dropper resistor to +5V.
pinMode(statusLed, OUTPUT);
digitalWrite(statusLed, HIGH);
The I/O lines connected to the counter reset buttons are set as inputs so we can read the button
state, but then the program writes to them as if they're outputs. This has the effect of activating the
ATMega CPU's internal 20K pull-up resistors on those inputs, biasing them high unless they are pulled
low via the button and the 1K resistors.

pinMode(resetButtonA, INPUT);
digitalWrite(resetButtonA, HIGH);


pinMode(resetButtonB, INPUT);
digitalWrite(resetButtonB, HIGH);
The pin for connecting the Hall-effect sensor is then treated in the same way. We set it to be an
input then write a HIGH value to it, activating the internal pull-up resistor so that it will be high unless
the open-collector output of the sensor pulls it low.
pinMode(sensorPin, INPUT);
digitalWrite(sensorPin, HIGH);
A number of variables are initialised to starting values.
pulseCount = 0;
flowMilliLitres = 0;
totalMilliLitresA = 0;
totalMilliLitresB = 0;
oldTime = 0;
The attachInterrupt() function takes three arguments: the ID of the interrupt to configure, the name
of the function to call when the interrupt is triggered, and the transition that will trigger the interrupt. In
this case we're setting up interrupt0 so the first argument is simply 0. Our ISR function is going to be
called pulse_counters passed in as the second argument. We only want to detect transitions from a high
to a low state on the input, so the third argument is set to FALLING. Other possible values for the
transition argument are RISING, to trigger on a transition from low to high; LOW, to trigger whenever the
input is in a low state; and CHANGE, to trigger on both rising and falling transitions.

179
CHAPTER 10  WATER FLOW GUAGE
attachInterrupt(sensorInterrupt, pulseCounter, FALLING);
}

The main program loop is where all the action is. The loop repeats very fast because there are no
delays or other things happening to hold it up: it runs through each cycle quite quickly and goes straight
back to the start. This simplifies a few things for us, particularly the way we manage the counter reset
buttons. If the loop was slower we'd probably need to connect them to interrupts in a similar way to the

flow gauge so that we wouldn't miss fast button presses. De-bouncing the buttons could also become an
issue. Button de-bouncing is discussed in the Vehicle Telemetry Platform project in Chapter 15, but in
this case we don't care about it because we're not toggling between states or counting button presses.
We're simply checking whether either button is pressed on every pass through the loop, and if it is, we
reset the associated counter. If the button is held down the associated counter will be reset on every pass
through the loop but that really doesn't matter.Remember that the counter reset buttons are biased
HIGH by the CPU and pulled LOW when the button is pressed, so we're checking for a LOW state to
indicate that the counter needs to be reset.
When a counter is reset we don't just clear the associated variable. We also need to overprint that
portion of the LCD with a zero value. Because of the way a character-based LCD works, any characters
that are written to it are displayed continuously until they are replaced with something else. If counter A
had incremented to, say, 123 liters, the first four characters on the bottom row of the display would read
"123L." Resetting the counter without clearing the display would subsequently cause the value 0L to be
written to the first two characters of the display, but the third and fourth characters wouldn't be altered.
The result is that the display would end up reading "0L3L," which wouldn't make much sense.
Overwriting those positions in the display with 0L followed by six spaces prevents this from happening.
The same thing is done for counter B, but of course we first set the cursor to position 8 (actually the
ninth character on that row since it starts from 0) before writing it out.
void loop()
{
if(digitalRead(resetButtonA) == LOW)
{
totalMilliLitresA = 0;
lcd.setCursor(0, 1);
lcd.print("0L ");
}
if(digitalRead(resetButtonB) == LOW)
{
totalMilliLitresB = 0;
lcd.setCursor(8, 1);

lcd.print("0L ");
}
The status LED is illuminated if either of the counter reset buttons is pressed, so we then check if
either button is pressed and set the status LED to LOW (on) or HIGH (off) appropriately.
if( (digitalRead(resetButtonA) == LOW) || (digitalRead(resetButtonB) == LOW) )
{
digitalWrite(statusLed, LOW);
} else {
digitalWrite(statusLed, HIGH);
}
The main loop spins through very fast and we don't want to do all the input processing every time
through because we need to average the number of pulses across one second, so the rest of the main
loop code is wrapped in a check to see if at least one second has passed since the last time it was
180
CHAPTER 10  WATER FLOW GUAGE
executed. Only if the difference between the current time and the previous time is greater than 1,000
milliseconds is the rest of the code executed.
if((millis() - oldTime) > 1000)
{
We need to disable interrupts while executing this section of the loop, so the very first thing to do is
call detachInterrupt() to disable the interrupt we set up previously. Otherwise, comms may fail if an
interrupt arrives while the program is in the middle of sending data to the host. Note that this doesn't
actually remove the configuration for the interrupt, and the CPU will still set the interrupt flag if it's
triggered while in the main program loop as explained previously.
detachInterrupt(sensorInterrupt);
The first step is to calculate the amount of flow that has occurred since last time. This is done by
taking the pulse count and multiplying it by 1,000 to convert liters to milliliters, then dividing it by the
product of the calibration factor and 60 to convert it from seconds to minutes.
flowMilliLitres = pulseCount * (1000/(calibrationFactor*60));
All the calculations of flow rate (as opposed to volume) are based on time, so we could assume that

this part of the loop executes once per second but that wouldn't necessarily be accurate. It will typically
be slightly longer than one second and this error will be cumulative if we just assume that we reach this
point every 1,000 milliseconds precisely. Instead, we calculate the actual number of milliseconds that
have passed since the last execution and use that to scale the output. That leaves us with this somewhat
complicated looking line that takes into consideration the amount of time that has passed and the flow
volume in that period.
flowRate = (flowMilliLitres * (60000 / (millis() - oldTime))) / 1000;
This flow volume is then added to the pair of cumulative counters tracking the number of milliliters
measured since the counters were reset.
totalMilliLitresA += flowMilliLitres;
totalMilliLitresB += flowMilliLitres;
During testing it can be useful to output the literal pulse count value so you can compare that and
the calculated flow rate against the datasheets for the flow sensor. The next two lines display the raw
pulse count value followed by a separator. You must uncomment them during testing if you want to
make sure that the calculated values you're seeing actually make sense or you need to check the sensor
calibration against a known flow rate.
//Serial.print(pulseCount, DEC);
//Serial.print(" ");

Now the program can write the calculated value to the serial port. Because we want to output a
floating-point value and print() can't handle floats we have to do some trickery to output the whole
number part, then a decimal point, then the fractional part. First, we define a variable that will be used
to hold the part after the decimal point, i.e., the fractional part of the floating-point number.
unsigned int frac;
To print the previously calculated flow rate for this sample period in liters/minute we cast the
flowRate variable to an integer value. This discards everything after the decimal point and sends only the
whole number part to the host via the USB connection. Then we send a period to represent the decimal
point.
Serial.print(int(flowRate));
Serial.print(".");

Now comes the trickery to determine the fractional part of the value. By subtracting the rounded
(integer) value of the variable from the variable itself we're left with just the part after the decimal point.
Then, multiplying this by 10 returns the number with the values after the decimal point shifted one digit
to the left. A starting value of 13.5423 would, therefore, become 0.5423 after subtraction of the integer
value. It is then shifted left to become 5.423, and then, because the result is being stored as an integer, it
becomes 5. If you want more decimal places to be displayed you can change the multiplier to 100 for two
181
CHAPTER 10  WATER FLOW GUAGE
decimal places, 1000 for three, 10000 for four, and so on. The resulting value is then simply sent to the
serial port just like any other integer and appears to the host immediately after the decimal point sent
previously.
frac = (flowRate - int(flowRate)) * 10;
Serial.print(frac, DEC);
Because the next three values to be displayed are simple integers we don't need to do any tricks.
They're sent straight to the serial port as they are, with space separators in between. Note that the last
output line uses Serial.println() instead of Serial.print() so that the display in the IDE will wrap to the
next line ready for the next sample.
Serial.print(" ");
Serial.print(flowMilliLitres);
Serial.print(" ");
Serial.print(totalMilliLitresA);
Serial.print(" ");
Serial.println(totalMilliLitresB);

Having output the values to the host, we then need to update the LCD. First we clear the entire first
row, then output the "Flow: " text that will appear in front of the flow-rate value.
lcd.setCursor(0, 0);
lcd.print(" ");
lcd.setCursor(0, 0);
lcd.print("Flow: ");

The sensor we used can output a flow rate from 0 up to about 20L/min, so sometimes the value to
display will be a single digit and sometimes it will be two digits. Because the position is set from the left it
can look a bit stupid if the decimal point jumps around, so we check whether the value is going to be less
than 10 (i.e., a single digit), and pad it with a space if it is. That way the number will appear with the
decimal place in the same location on the LCD no matter what value it displays.
if(int(flowRate) < 10)
{
lcd.print(" ");
}
Just as before we then need to display the integer portion of the value, then a decimal point, and
then the fraction. Then we output a space followed by the units.
lcd.print((int)flowRate);
lcd.print('.');
lcd.print(frac, DEC);
lcd.print(" L/min");
The two counters are displayed on the second line, with the first starting at position 0 and the
second starting at position 8. Because the counters actually accumulate milliliters and we want to
display liters we divide them by 1000 and convert the result to an integer before it is sent to the LCD.
lcd.setCursor(0, 1);
lcd.print(int(totalMilliLitresA / 1000));
lcd.print("L");
lcd.setCursor(8, 1);
lcd.print(int(totalMilliLitresB / 1000));
lcd.print("L");
Before finishing up the loop the pulse counter needs to be reset so that next time the ISR is called it
will begin counting up from 0 again.
pulseCount = 0;
We're almost at the end now, so we need to update the oldTime variable to the current time so the
main loop won't execute this chunk of code again for at least another 1,000 milliseconds. There's a little
182

CHAPTER 10  WATER FLOW GUAGE
catch here, though, that doesn't cause us any problems in this project but is something to be very careful
of in your own programs: technically, the millis() function is lying to us and returning an incorrect value.
This is because millis() is updated behind the scenes by a time-triggered interrupt (as opposed to the
input-triggered interrupt we used for the sensor) that fires approximately every millisecond and causes
the time counter to increment. But while interrupts are disabled the millis() function won't actually be
incrementing, and will simply return the value it was set to just before interrupts went away rather than
what the current value should really be.
For us it doesn't matter so we just set the oldTime variable to the value returned by millis().
oldTime = millis();
At this point, though, the interrupt is still disabled, so the ISR will never be called. Now that we're
done with the main program loop we enable the interrupt again.
attachInterrupt(sensorInterrupt, pulseCounter, FALLING);
}
}
The last part of the sketch is the ISR itself. This function is never called by the main program, but is
instead invoked by interrupt0 once per rotation of the Hall-effect sensor. The interrupt handler is kept
extremely small so it returns quickly each time it executes. This is the ideal way to structure an ISR: a
single instruction to increment a global variable and then immediately bail out. An ISR like this can
execute in just a few microseconds.
void pulseCounter()
{
pulseCount++;
}
Once you've uploaded the sketch to your Arduino and ensured your flow sensor is plugged in, click
the "serial monitor" button near the top right of the Arduino IDE and make sure the port speed setting is
set to 38400. You should see a series of values being displayed (hopefully 0.0) with one reading taken per
second. The LCD will also display a flow rate of 0.0 and counter values of 0L.
Because the pickup on the flow-rate sensor spins quite easily, you can test that it is functioning by
simply blowing through the sensor and watching the values displayed in your IDE or on the LCD. Note,

however, that the sensor is directional. Look on the side of the body for an arrow that indicates the
required liquid flow direction and blow through it in the direction of the arrow.
Try blowing gently through the sensor and watching the flow-rate value. After a few seconds the
counters should click over to 1L and then continue incrementing as you keep blowing. Pressing either of
the counter reset buttons should set that counter back to 0, leaving the other counter unaffected.
Install Flow Sensor
The flow sensor itself is manufactured from a very strong material consisting of a glass-fiber
reinforced plastic resin and has a standard 1/2-inch BSP thread on each end so it can be screwed into
standard plumbing fittings.
Find the lead-in pipe for the water source you want to measure and have a plumber fit the sensor
into the pipe. In many jurisdictions it is illegal to perform plumbing work yourself without the necessary
qualifications, but even if you are allowed to perform the work yourself it's best to seek expert assistance
if you're at all unsure about working with pipes. However, if you have someone such as a plumber
perform the installation for you, make sure you show them the direction marker on the sensor body so
they know it needs to be installed so that the water flows through it in that direction.
183
CHAPTER 10  WATER FLOW GUAGE
184
Variations
Online Logging
By adding an Ethernet shield your flow gauge could connect to the Internet and upload readings to an
online resource logging service such as Pachube (www.pachube.com) or Watch My Thing
(www.watchmything.com).
However, something to be careful of in this particular project is possible contention for the pins
used fo
r interrupts. Interrupts are often used by shields that perform time-critical communications
functions, including Ethernet shields, so if you want to combine this project with an Ethernet
connection you need to be careful not to use any pins needed by the shield. Ethernet shields based on
the official design published on the Arduino web site generally use interrupt1 on pin 3, while Ethernet
shields based on the nuElectronics design, as well as the WiShield wifi shield, generally use interrupt0 on

pin 2.
As discussed previously the input pin used for this project is determined by the sensorInterrupt
va
riable: if the value is set to 0, it will use interrupt0 on pin 2; setting it to 1 causes it to use interrupt1 on
pin 3. Check which interrupt your Ethernet shield uses and then configure the sensor to use the other
one.
Multiple Sensors
We've only connected a single sensor in this example, but a standard Arduino has two interrupt pins, so
you could connect one sensor to pin 2 and one to pin 3. You'd then need to modify the software to have
two ISR functions and two sets of relevant variables such as pulseCount.
If you want to measure flow through ev
en more sensors you could use an Arduino Mega to connect
up to six flow gauges at once. Alternatively you may be able to use the port-level interrupt technique
discussed in the Vehicle Telemetry Platform project in Chapter 15 to connect even more.
Resources
If you want to learn more about Hall-effect sensors there is an introductory article on Wikipedia
explaining how they work:
en.wikipedia.org/wiki/Hall_effect_sensor
Wikipedia also has a basic article on
the HD44780 display controller:
en.wikipedia.org/wiki/HD44780_Character_LCD
The datasheets for the ZD1200 and ZD1202 flow gauges also contain
lots of helpful information:
www.jaycar.com.au/images_uploaded/ZD1200.pdf www.jaycar.com.au/images_uploaded/ZD1202.pdf
C H A P T E R 11

  

Oscilloscope/Logic Analyzer
One of the frustrating things about developing and debugging electronic circuits is that you can't look

inside the circuit to see what is happening. Even with a circuit laid out before you on a workbench and
powered up, it may seem like you're in the dark, unable to figure out why an input change or alteration
in one part of the circuit isn't having the effect you expected. Sometimes it can feel like you're working
with a blindfold on.
A multimeter lets you measure a constant or slowly changing voltage, such as checking whether ICs
or other devices are being powered correctly, with 3.3V or 5V as appropriate. But they're no good at
helping you visualize signals that change rapidly, such as a stream of data flowing through a serial port
or an audio waveform coming out of an oscillator. The best you can hope for with a multimeter is a
reading that represents the average voltage: a 5V bitstream with a random mix of 1s and 0s will read
about 2.5V, since it spends half its time high and half low. If the multimeter tried to keep up with the
changes, the display would flicker too fast for you to read it.
The solution is two different test instruments that started as totally separate devices, but over the
years have progressively become more and more similar. Nowadays, their features often overlap so
much that it can be hard classifying these two devices as strictly one or the other.
The first is an oscilloscope, an instrument most frequently used when working with analog circuits
but also very handy for digital circuit analysis. An oscilloscope (or just "scope") has a screen to display a
signal trace that is offset in the X and Y axis by measurements taken from two different inputs. The most
common usage is with the Y (vertical) axis controlled by a probe connected to the system under test, and
the X (horizontal) axis controlled by an internal timebase that can run at different frequencies. If the X
input is left unconnected and the Y input is attached to a signal that oscillates rapidly between low and
high values, the trace on the screen will flicker up and down rapidly and simply draw a vertical line. By
applying the timebase on the X axis, the varying input signal is spread out horizontally so that it's
possible to see how it varies with time.
The X input can alternatively be attached to another input probe, providing independent X and Y
input to the oscilloscope and allowing two signals to be plotted against each other. The classic classroom
example is to attach the X and Y probes to two sine waves of equal frequency and amplitude that are 90
degrees out of phase, with the result being that the oscilloscope plots a perfect circle on the screen (see
Figure 11-1).
185
CHAPTER 11  OSCILLOSCOPE/LOGIC ANALYZER


Figure 11-1. Oscilloscope displaying the combination of two equal sine waves 90 degrees out of phase
Different amplitudes will cause the circle to be compressed around one of the axes, causing it to
form an oval around either the horizontal or vertical axis. A 0 degree phase offset will cause a diagonal
line to be plotted from bottom left to top right, a 180 degree offset will cause a diagonal line to be plotted
from top left to bottom right, and other offsets will cause the shape to form an oval on a diagonal axis.
Differences in frequency also alter the shape of the display, with a 1:2 frequency ratio creating a display
that looks like a figure eight (8) and a 2:1 ratio looking like the infinity symbol, ∞
An experienced technician can look at a scope display and very quickly deduce a lot of information
from it, such as the relative amplitudes, frequencies, and phase offset of two different waveforms.
Early oscilloscopes were called a "cathode-ray oscilloscope," or simply CRO, and many people still
call them that. An old-style CRO is literally just a cathode-ray tube like the one found in an older
television set but with no tuning circuit and two inputs connected to signal amplifiers that drive the X
and Y deflection plates directly. It's even possible to convert an old TV into a basic CRO with some fairly
simple alterations to the circuit, although you have to be careful of the high voltages required to run the
electron gun and deflection circuitry.
Modern oscilloscopes no longer use cathode-ray tubes, and instead use high-speed analog-to-
digital converters (ADCs) to sample an analog reading and process it digitally before displaying the result
on an LCD. As a result, they can perform tricks such as recording a sequence of readings for future
analysis rather than simply displaying it on-screen like older CROs, and they can also have multiple
independent inputs so you can display different waveforms on-screen at the same time. Two, four, or
eight individual input channels, each with their own timebase, are not uncommon.
The second instrument we need to discuss is a logic analyzer. Logic analyzers are a more recent
development and came about from the need to track the digital status (high or low) of many connections
in parallel, as well as a sequence of changes in series. With many digital devices using 8-, 16-, or 32-bit
parallel buses internally, it can be handy to have a device that reads the logic level of each bus line
186
CHAPTER 11  OSCILLOSCOPE/LOGIC ANALYZER
independently and displays them all simultaneously, showing you what binary value was being
transmitted at that point in time. Likewise, the transmission of a sequence of bits on a single serial data

line can be captured and analyzed with a logic analyzer.
Advanced logic analyzers can deduce a tremendous amount of information about data that is
passing through the circuit under test, and many also allow the operator to assign meaningful names to
each input and to group inputs together. Some even apply heuristics to the data that has been acquired
and process it to convert it into a meaningful form; for example, reading the raw electrical signals in an
Ethernet connection and converting them into a bitstream before progressively decoding the layers of
the network stack right up to, say, the HTTP packet level to display the message payload passing across
the interface.
Once you start regularly using an oscilloscope or a logic analyzer, it's like your eyes have been
opened to a vast amount of information and understanding that was previously locked away out of sight.
Unfortunately, though, professional-quality oscilloscopes and logic analyzers don't come cheap and are
out of the price range of most hobbyists.
But that doesn't mean you can't make do with a bit of ingenuity, and an Arduino makes a
surprisingly good starting point for building your own basic test equipment. It has a decent number of
digital inputs that can be used to sample the status of digital pins in a circuit under test, and even has
analog inputs that can be used to build a crude oscilloscope so you can visualize analog waveforms
using your PC monitor as the display.
In this project, we use an Arduino to capture multiple input values and pass them via the USB
connection to a host computer running a program that deciphers the values and displays them on-
screen. Because the Arduino itself is not providing any particular intelligence and simply passes on any
values it reads, this project is very flexible and the behavior of the system can be changed simply by
altering the software that runs on your computer. This opens up a wide range of possibilities for using
the same basic hardware to process and visualize analog data, parallel digital data, and serial digital
data.
The visualization program demonstrated in this project is written in Processing, a sister project to
Arduino that is designed to allow rapid development of visual programs in the same way that Arduino
allows rapid development of physical programs. Processing runs on Windows, Linux, and Mac OS X.
However, this simple approach has some major limitations in terms of both sample rate and
resolution, so don't expect an Arduino-based system to rival a professional-grade oscilloscope or logic
analyzer. The analog inputs on an Arduino operate by default at 10-bit resolution, which provides a scale

of 0 to 1023. More advanced ADCs provide 12-bit resolution or higher. The Arduino analog inputs also
take around 100 microseconds to take a reading in their default configuration, limiting the number of
samples it can take per second and restricting it to much lower frequencies than a more advanced ADC.
The result is a system that will operate quite well at 10-bit resolution at up to around 5KHz,
depending on how many channels you are monitoring. Not great specs, but certainly better than
nothing if you can't afford a professional oscilloscope or logic analyzer. The required parts are shown in
Figure 11-2, and the schematic is shown in Figure 11-3.
Parts Required
1 Arduino Duemilanove, Arduino Pro, or equivalent
1 Prototyping shield
1 Panel-mount LED
1 470R resistor
1 Black test clip or probe
187
CHAPTER 11  OSCILLOSCOPE/LOGIC ANALYZER
8 Yellow test clips or probes
1 Black panel-mount socket
8 Yellow panel-mount sockets
1 Black line plug
8 Yellow line plugs
5m shielded single-core cable
Metal project case
3 10mm plastic spacers
3 20mm M3 bolts with matching nuts
3 Plastic insulating washers
4 self-adhesive rubber feet
Source code available from www.practicalarduino.com/projects/scope-logic-analyzer.


Figure 11-2. Parts required for Arduino Oscilloscope / Logic Analyzer

188

×