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 . 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. 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. 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