1. Trang chủ
  2. » Công Nghệ Thông Tin

Practical Arduino Cool Projects for Open Source Hardware- P20 doc

10 376 0
Tài liệu đã được kiểm tra trùng lặp

Đang tải... (xem toàn văn)

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Tiêu đề Water Flow Gauge
Trường học Open Source Hardware University
Chuyên ngành Electrical Engineering
Thể loại Thesis
Năm xuất bản 2023
Thành phố San Francisco
Định dạng
Số trang 10
Dung lượng 238,89 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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 da

Trang 1

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

Trang 2

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

ground connection

connection

adjustment voltage

Select

Data input (HIGH) / Control input (LOW)

Trang 3

GND 5 R/W Read / Write Read (HIGH) /

Write (LOW)

byte/nibble transfer

power

Backlight +5V connection

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

Trang 4

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

Trang 5

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

Trang 6

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

Trang 7

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

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

Trang 8

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

Trang 9

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

Trang 10

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

Ngày đăng: 03/07/2014, 20:20

TỪ KHÓA LIÊN QUAN