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

Practical Arduino Cool Projects for Open Source Hardware- P20 doc

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 (238.89 KB, 10 trang )

CHAPTER 10  WATER FLOW GUAGE
the R/W pin to ground on the module we're down to a total of 12 connections to the LCD module, 10 of
which are using up the limited number of I/O lines available on the Arduino: eight data bits, Enable, RS,
Ground, and 5V. It’s still a bit of a rat’s nest, and uses up almost all of the limited number of I/O lines
available on the Arduino.
The RS connection is the "Register Select" line, and it's used to switch between command and data
modes in the LCD. We can't tie it permanently HIGH or LOW like the Enable connection because the
LCD drivers use it to initialize the module and then send data to it.
Luckily the HD44780 can also operate in 4-bit mode, a strange mode that's something like a cross
between a parallel and a serial interface. In 8-bit mode an entire byte is presented at once to the data
lines. In 4-bit mode half a byte (called a "nibble") is presented to four of the data lines and read into the
LCD controller, then the other half of the byte is presented and read in the same way. The LCD controller
then reassembles the two nibbles into a complete byte internally, just as if it had all been transmitted at
once. Using 4-bit mode saves us another four connections to the controller, bringing it down to a total of
eight wires including power and ground. That's six data lines on the Arduino taken up just driving the
LCD, which isn't ideal, but does leave enough I/O lines available for us to connect the buttons and Hall-
effect flow sensor.
If you're really running short of I/O lines in a project and need to reduce the number of connections
to an LCD module even further, you can use a device called a "shift register" such as a 74HC4094. A shift
register acts as a serial-to-parallel converter, allowing you to use just three data lines to send a sequence
of bits in series that are then exposed in parallel on the shift register outputs. Using a 74HC4094 to
connect an HD44780 to an Arduino is more complicated than connecting it up directly, but it drops the
I/O line requirement to just three—saving you even more lines. It's not necessary in this project because
we're not that short of I/O lines, but if you want to give it a go there is a good explanation on the Arduino
web site: www.arduino.cc/playground/Code/LCD3wires.
Since we're going to use 4-bit mode we need a total of eight connections from the LCD module to
the Arduino, so cut off a short length of ribbon cable and strip it down to eight wires. Strip back both
ends of each wire and "tin" it with solder, then connect one end to the LCD module using the
connections shown in the schematic in Figure 10-2. The result is shown in Figure 10-5.It's also necessary
to make several connections between pads on the LCD module itself since we won't be controlling them
from the Arduino. Use short lengths of hookup wire to jumper pins 1 (ground), 3 (contrast), 5 (R/W), and


16 (backlight ground) together.
In most HD44780 displays you can simply tie pin 3 (contrast) to ground and the module will supply
maximum contrast, with the text very crisp and easily visible. Some displays, though, can require a bit
more fiddling with the contrast to make them visible. If shorting the contrast pin to ground doesn't
produce visible text on your display it may be necessary to use a 10K variable resistor or trimpot to
provide it with a voltage somewhere between 0V and 5V. If you connect the center (wiper) pin of a
trimpot to pin 3, one side of the trimpot to ground, and the other side of the trimpot to 5V, you can then
use it to adjust the contrast setting. The three-wire LCD page on the Arduino web site includes a contrast
adjustment trimpot in the schematic in case you find it's necessary for your particular LCD.
Also use the 10R resistor to connect pin 2 (+5V) to pin 15 (backlight power) if you want to illuminate
the backlight. In most cases 10R is a reasonable value to try as a starting point and should work fine on a
typical 16x2 display with LED backlighting. However, the current required by displays of different sizes
can vary and some displays even use different backlight technology entirely, so it's a good idea to check
the datasheet for your specific display if you're not sure what it requires.
169
CHAPTER 10  WATER FLOW GUAGE

Figure 10-5. LCD module connected to shield using ribbon cable
The other end of the ribbon cable needs to be connected to the prototyping shield. Working from
left to right on the LCD module, the ground and 5V lines obviously need to connect to GND and 5V on
the shield. RS and Enable then connect to digital I/O lines 9 and 8, respectively. Data bits 4 through 7
connect to I/O lines 7 through 4, respectively. See Table 10-1.
Table 10-1. Connections between Arduino and LCD module
Arduino Pin LCD Pin Label Name Description
GND 1 GND Ground Display
ground
connection
+5V 2 VCC Power Display +5V
connection
GND 3 Vo Contrast Contrast

adjustment
voltage
Digital OUT 9 4 RS Register
Select
Data input
(HIGH) /
Control input
(LOW)
170
CHAPTER 10  WATER FLOW GUAGE
GND 5 R/W Read / Write Read (HIGH) /
Write (LOW)
Digital OUT 8 6 E Enable Enable
byte/nibble
transfer
7 D0 Data0 Data bit 0
8 D1 Data1 Data bit 1
9 D2 Data2 Data bit 2
10 D3 Data3 Data bit 3
Digital OUT 7 11 D4 Data4 Data bit 4
Digital OUT 6 12 D5 Data5 Data bit 5
Digital OUT 5 13 D6 Data6 Data bit 6
Digital OUT 4 14 D7 Data7 Data bit 7
+5V via 10R 15 VB1 Backlight
power
Backlight +5V
connection
GND 16 VB0 Backlight
ground
Backlight

ground
connection

Yes, the data bits are reversed between the Arduino and the LCD, but that really doesn't matter
because we have to explicitly configure them in the program anyway and wiring them in this order
makes the cabling neat and easy.
Fit LCD to Case
If you're measuring water flow you will probably have to place your Arduino in a location that is subject
to dust and moisture. To keep it operating reliably over a long period of time it's a good idea to mount it
inside a plastic case, preferably one designed for outdoor use that has a rubber gasket around the edge of
the lid to ensure a watertight seal. We used a weatherproof PVC box with a transparent lid. It was perfect
for mounting the LCD because you can see it right through the case, allowing the display to be kept safe
and weatherproof.
The mounting holes in the corners of our particular LCD module were just a bit too small to fit
standard M3 bolts through, but luckily there were no PCB tracks close to the holes so it was an easy job
to enlarge them with a 3mm drill bit. We then drilled matching holes in the box lid and also drilled holes
where the pair of reset buttons will be mounted, then bolted the LCD in place using 10mm plastic
171
CHAPTER 10  WATER FLOW GUAGE
spacers and 20mm M3 bolts. Metal washers were used on the outside and plastic washers on the inside
to ensure the nuts didn't short anything out on the LCD module's PCB.
The result is a very neatly mounted LCD with the face suspended just behind the transparent lid of
the box (see Figure 10-6).


Figure 10-6. LCD and pushbuttons mounted in case lid
You can use just about any momentary-action pushbuttons, but we chose a couple of low-profile
splash-proof buttons that came fitted with rubber seals to provide extra protection against wet hands.
Wiring up the buttons is easy. Connect one terminal of each button together as the common-
ground connection, then link it to ground on the shield. The other terminals of each button then

connect to the two 1K resistors fitted to the shield earlier and link to digital I/O lines 11 and 12. It doesn't
even matter much which button you connect to which input. If you find that you got it wrong, it's trivial
to swap the pin assignments in the software. We connected the left button (on the right when looking at
the back of the case lid, remember!) to input 11 to reset counter A, and the right button to input 12 to
reset counter B.
172
CHAPTER 10  WATER FLOW GUAGE
Fit Arduino in Case
The Arduino itself also needs to be mounted in the case. For convenience we cut a rectangular hole in
the side of the box to allow the USB connector to protrude through. However, this prevents the box from
being weathertight, so you may choose to mount it in a different way. Just like with the LCD module, we
then used 20mm M3 bolts through the bottom of the case with plastic spacers (6mm this time) for the
Arduino to sit on. Plastic washers on top of the Arduino PCB then protect it from the M3 nuts.
Once the Arduino is mounted in the bottom of the case you can test-fit the prototyping shield into
it, joining the LCD and the front panel pushbuttons to the Arduino (see Figure 10-7). Even without the
sensor fitted you can run tests on the hardware at this point; for example, by loading an example sketch
from the LiquidCrystal library and altering to suit the pin assignments as explained in the following
section “Configure, Compile, and Test Sketch.”


Figure 10-7. Arduino, shield, LCD, and buttons all mounted inside weatherproof case
The only hardware assembly left to do now is to connect the Hall-effect flow sensor. As shown on
the circuit diagram in Figure 10-2, the sensor needs to be connected to ground, +5V, and to the end of
the 1K resistor fitted previously to digital I/O line 2. You can either fit a line plug to the cable and mount
a socket in the case, or just pass the cable through a hole in the box, tie a knot in it to prevent it from
pulling back out, and solder it directly to the prototyping shield as shown in Figure 10-8.
173
CHAPTER 10  WATER FLOW GUAGE

Figure 10-8. Assembled unit with sensor connected

With the sensor connections in place make sure the prototyping shield is firmly mounted, fit the lid,
and move on to the software.
Determine Scaling Factor
Like almost all Hall-effect devices, water flow-rate sensors output a series of pulses at a rate that varies
proportionally with the parameter being measured. All devices that output pulses need a scaling factor
to convert the frequency into a meaningful value. For example, a car wheel rotation sensor might output
one, two, four, or five pulses per rotation, but that information is useless on its own: you also need to
know the circumference of the wheel so you can multiply the pulse count by the circumference to
determine the distance traveled.
The sensor we used outputs approximately 4.5 pulses per second per liter of flow per minute. That
sounds odd because we're using values measured in pulses per second to represent liters per minute.
Consider the following examples:
• At 1 liter per minute, the sensor will output 4.5 pulses per second.
• At 5 liters per minute, the sensor will output 22.5 pulses per second.
• At 10 liters per minute, the sensor will output 45 pulses per second.
• At 20 liters per minute, the sensor will output 90 pulses per second.
This means our scaling factor to convert pulses per second into liters per minute is 1/4.5, or
approximately 0.22. By measuring the pulse frequency and dividing by 4.5 (or multiplying by 0.22) we
can determine the current flow rate in liters per minute.
The program for this project, therefore, acts as a simple frequency counter to determine how many
pulses are being generated per second, and then applies that scaling factor to convert the measured
frequency into a flow-rate value in liters per minute. It also outputs the value as the number of liters
passed in that second, and as a cumulative total of the number of liters passed since the program began.
174
CHAPTER 10  WATER FLOW GUAGE
There is a slight complication though: most flow-rate sensors do not have a consistent scaling factor
across their entire operational range.
At low flow rates the sensor might be impeded more by friction in the bearings, so its output
frequency could actually be lower per liter than at higher flow rates. That variation could be corrected in
software by applying a different scaling factor depending on the measured pulse rate. However, because

the accuracy of inexpensive flow sensors is typically only +/– 10% anyway it doesn't really matter much
in practice that the scaling factor deviates slightly at low flow rates.
Configure, Compile, and Test Sketch
The example sketch contains two things that are likely to be a bit puzzling if you haven't seen them
before: hardware interrupts and volatile variables.
Hardware Interrupts
The first trick is the use of an interrupt to process pulses coming from the sensor.
An "interrupt" is a special signal sent to the CPU that does pretty much what it sounds like: it
interrupts the current program flow and makes it jump off in a different direction temporarily, before
returning to whatever it was doing previously. As far as the main program code is concerned, it doesn't
even need to know that an interrupt has taken place. It will simply lose some time in the middle of
whatever it was doing; other than that, everything will continue as if nothing happened.
Of course this can cause big problems if your main program code is doing something time-critical,
and it's important to keep interrupts as short as possible.
Interrupts can come from a variety of sources, but in this case we're using a hardware interrupt that
is triggered by a state change on one of the digital pins. Most Arduino designs have two hardware
interrupts (referred to as "interrupt0" and "interrupt1") hard-wired to digital I/O pins 2 and 3,
respectively. The Arduino Mega has a total of six hardware interrupts, with the additional interrupts
("interrupt2" through "interrupt5") on pins 21, 20, 19, and 18, respectively as shown in Table 10-2.
Table 10-2. Hardware interrupt pin assignments
Interrupt Pin Model
0 2 most Arduinos
1 3 most Arduinos
2 21 Arduino Mega
3 20 Arduino Mega
4 19 Arduino Mega
5 18 Arduino Mega

By defining a special function called an "Interrupt Service Routine" (usually simply called an "ISR")
that you want executed whenever the interrupt is triggered, and then specifying the conditions under

175
CHAPTER 10  WATER FLOW GUAGE
which that can happen (rising edge, falling edge, or both), it's possible to have that function executed
automatically each time an event happens on an input pin. That way you don't have to keep checking
the pin to see if it has changed state since the last time you checked it because your program can get on
with doing something else and just be interrupted when necessary. It's like having a doorbell on your
house: you don't have to keep checking if someone is at the front door because you know that if
someone arrives they will ring the bell. Attaching an interrupt to a program is just like installing a
doorbell and then getting on with doing other things until visitors arrive.
A common beginner's mistake is to put too much code into the ISR. It's important to remember that
when an interrupt occurs and your ISR is being executed, your main program code is frozen and all other
interrupts are automatically disabled so that one interrupt can't disrupt another while it is being
processed. Disabling interrupts is, therefore, something that should be done for the briefest possible
time so that no other events are missed, so always make your ISR code as short and fast as possible and
then get straight back out again. A common approach, which is the technique we use here, is to have the
ISR update a global variable and then immediately exit. That way the entire ISR can execute in only a few
clock cycles and the interrupts are disabled for the shortest time possible. Then the main program loop
just has to periodically check the global variable that was updated by the ISR and process it as
appropriate in its own time.
Volatile Variables
The second trick is the use of the «volatile» keyword when declaring the pulseCount variable. The
volatile keyword isn't technically part of the program itself: it's a flag that tells the compiler to treat that
particular variable in a special way when it converts the source code you wrote into machine code for
the Arduino's ATMega CPU to execute.
A "volatile" variable is one whose value may change at any time without any action taken by the
code near it. Compilers are designed to optimize code to be as small and fast as possible, so they use
techniques such as finding variables that are not modified within the code and then replacing all
instances of that variable with a literal value. Normally that's exactly what you want, but sometimes the
compiler optimizations trip up on situations that aren't quite what it's expecting. The volatile keyword is
therefore a warning to the compiler that it shouldn't try to optimize that variable away, even when it

thinks it's safe to do so.
In practice there are three general situations in which a variable can change its value without nearby
code taking any action.
1. Memory-mapped peripheral registers. Some peripheral interfaces, including
some digital I/O lines, map those lines directly into the CPU's memory space.
A classic example is the parallel port on a PC: the pins on a parallel port map
directly to three bytes of system memory starting at address 0x378. If the
values in the memory locations for the output pins are changed by the CPU,
the electrical state of the pins changes to match. If the electrical state of the
input pins changes, the corresponding memory location values change and
can be accessed by the CPU. In memory-mapped peripheral registers the
interface is effectively a real-time physical representation of the current state
of a chunk of system memory, and vice versa. In the case of memory-mapped
input lines the value of that location in memory might never be changed by the
running program and so the compiler could think it's valid to optimize it away
with a static value, but the program will then never see changes caused by
changing input levels from the connection to the peripheral.
176
CHAPTER 10  WATER FLOW GUAGE
2. Global variables within a multithreaded application. This doesn't really apply
to Arduino programs, but it's worth remembering if you're working on larger
systems. Threads are self-contained chunks of code that run in parallel to each
other within the same memory space. From the point of view of each thread, a
global variable is actually very similar to a memory-mapped peripheral
register: it can change at any time due to the action of another thread. Threads,
therefore, can't assume that a global variable has a static value, even if that
particular thread never changes it explicitly.
3. Global variables modified by an ISR. Because an ISR appears to the compiler to
be a separate chunk of code that is never called by the main program, it could
decide that variables referenced by the main program can never change after

initialization and so optimize them away by replacing them with literal values.
This is obviously bad if the ISR changes the value because the main program
will never see the change. The water flow sensor sketch uses an ISR to update
the pulseCount variable, and because the main program loop accesses that
variable but never modifies it the compiler could incorrectly decide that it can
safely be optimized away and replaced with a literal value. In the example code
we therefore need the volatile keyword to allow the main program loop to see
changes to the pulseCount value caused by execution of the ISR.
Note that the example program disables interrupts while sending data to the host. This is important
because otherwise it may end up in a situation where the next pulse arrives before the transmission has
finished, and the interrupt could cause problems for the serial connection. While interrupts are disabled
the CPU will still see additional interrupts at the hardware level and set an internal flag that says an
interrupt has occurred, but it won't be allowed to disrupt the flow of the program because ISRs cannot
be executed in a stacked or nested fashion. Only one can ever be in operation at a given time. Then,
when the ISR finishes executing, the CPU could immediately trigger another ISR call if the interrupt flag
has been set in the background. It's fairly common for interrupt-heavy systems to spend time processing
an ISR, return, and be immediately shunted into another ISR without the main program code getting a
chance to do any processing at all.
One thing to remember, however, is that the interrupt flag in the CPU is only a 1-bit flag. If you
spend time with interrupts disabled and in that time an event occurs to set the flag, you have no way of
knowing if it was only one event or one thousand. The CPU doesn't have an internal counter to keep
track of how many times the interrupt was tripped. There is therefore the very real possibility of
undercounting events such as input pulses if you spend too long with interrupts disabled.
In this project it's not a problem because the pulse rate is never particularly high. Handling 90
interrupts per second at the maximum rated flow for this sensor is trivial even for a relatively slow CPU
such as those found in an Arduino. In fact, one thing that's slightly odd about the example program is
that it spends most of its time with interrupts disabled: it disables them at the start of the main program
loop, then enables them again at the end before it loops back to the start. Interrupts are therefore only
enabled for a very brief period on each cycle, but because the CPU sets the interrupt flag even when
interrupts are disabled it all works out nicely. Most pulses from the sensor will arrive while the program

is in the main loop and interrupts are disabled, and will then be processed as soon as the main loop
ends. Because the main loop executes in about 5ms and even at 90 pulses per second the interval
between pulses is about 11ms we're fairly safe from missing any pulses.
177
CHAPTER 10  WATER FLOW GUAGE
Flow Gauge Sketch
First the sketch includes the LiquidCrystal library to take care of communicating with the LCD module
for us. Then we create a LiquidCrystal object called "lcd" and configure it with the pins used for RS,
Enable, and D4 through D7. Because of the way we wired the ribbon cable to the shield this corresponds
directly to pins 9 through 4.

#include <LiquidCrystal.h>
LiquidCrystal lcd(9, 8, 7, 6, 5, 4);
We also need to specify the pins connected to the pair of counter reset buttons and the status LED.
The LED is illuminated (pulled LOW) whenever a reset button is pressed.
byte resetButtonA = 11;
byte resetButtonB = 12;
byte statusLed = 13;
The connection for the Hall-effect sensor also needs to be configured, and we need to specify two
values: the interrupt number and the pin number. It would be nice if this could be done in a single
command to avoid confusion but unfortunately there's no way to do that in an Arduino, because the
interrupts are numbered from 0 up and they can correspond to different pins depending on what
Arduino model you are using.
For our prototype we connected the sensor to pin 2, which corresponds to interrupt 0. Alternatively
you could connect to pin 3 and use interrupt 1. A Mega gives you even more options.

byte sensorInterrupt = 0;
byte sensorPin = 2;

We also need to set a scaling factor for the sensor in use as discussed previously. The Hall-effect flow

sensor used in the prototype outputs approximately 4.5 pulses per second per liter/minute of flow.
float calibrationFactor = 4.5;
We also need a variable that will be incremented by the ISR every time a pulse is detected on the
input pin, and as discussed previously this needs to be marked as volatile so the compiler won't optimize
it away.
volatile byte pulseCount;
The measured values also need variables to store them in, and in this program we use three different
types of numeric variables. The float type used for flowRate handles floating-point (decimal) numbers,
since the flow rate at any one time will be something like 9.3 liters/minute. The unsigned int
flowMilliLitres can store positive integer values up to 65,535, which is plenty for the number of milliliters
that can pass through the sensor in a one-second interval. With a high-flow sensor that can measure
more than 65 liters/second, it would be necessary to switch this to type unsigned long instead. The
unsigned long variables, totalMilliLitresA and totalMilliLitresB, can store positive integer values up to
4,294,967,295, which is plenty for the cumulative counter of total milliliters that have passed through the
sensor since the counter was reset. Eventually the counters will wrap around and start again at 0 after a
bit more than 4 megaliters, but that should take quite a while in a typical domestic application!
float flowRate;
unsigned int flowMilliLitres;
unsigned long totalMilliLitresA;
unsigned long totalMilliLitresB;
The loop needs to know how long it has been since it was last executed, so we'll use a global variable
to store the number of milliseconds since program execution began and update it each time the main
loop runs. It needs to be of type long so that it can hold a large enough value for the program to run for a
reasonable amount of time without the value exceeding the storage capacity of the variable and
wrapping back around to 0.
178

×