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