We've got some sensing needs. Particularly of the distance variety.
We need you to build us a sensor that uses infrared light to detect the distance between your sensor and an object and then use a servo display that distance by turning the dial on a gauge. You also need to make sure the sensor data is adequately filtered, and we need a way to calibrate our sensor.
This page has lots of detail and goes into more depth on some things than the lecture did. We encourage you to read over it!
The circuit for this project has three important sections. Working clockwise around the diagram below, the three key parts are:
The two button circuits, featuring 10kΩ pull-down resistors and 100nF ceramic debouncing capacitors.
The the servo and its 220μF electrolytic reservoir capacitor.
The IR light sensor (an IR phototransistor and IR LED).
Let's take a closer look at how each section works.
The first consideration when feeding any kind of digital input into a microcontroller is ensuring the input pin is never left "floating." A pin is floating whenever it has no voltage connected to it (whenever nothing's plugged into it). We shouldn't ever leave input pins floating because, in short, they can act like tiny antennas and pick up random electrical noise from the environment, and in turn, our MCU might unpredictably interpret that noise on the pin as either a LOW or a HIGH input. Or, in very noisy environments, you can even build up enough static-electric charge on the pin to damage the MCU or other parts of the circuit when it discharges.
We need our circuits to be predictable, so we use what are called pull-up and pull-down resistors to make sure our pins are always "pulled" to some default value whenever our active signal isn't present. For example, if we're using a button to control a pin, we might want our active signal to be a 1 or a HIGH on the pin when the button is pressed. To get that, we need to connect the pin to 5V when we press the button, so we'd connect one side of the button to our 5V bus bar, and the other to the pin on the Arduino. But the problem is when our button isn't active (pressed), our pin isn't connected to anything, leaving it floating. Since our active signal is a high state, we need our default state to be LOW (so we'll read a LOW or 0 on the pin whenever the button isn't pressed). To do that, we'll connect a pull-down resistor to the Arduino pin and put the other end of that resistor on the ground bus bar. This is what the resistors connected between each button and GND are doing in the circuit schematic above.
And as you can imagine, if we wanted the active state to be LOW instead, we'd connect our switch between the pin and GND, and attach a pull-up resistor from the pin to 5V, making the pin default to HIGH when it's not pressed. And as a fun fact, the ATmega328P in our Arduino has built in pull-up resistors (but not pull-downs). You can save on a resistor in your circuit by enabling these internal pull-ups in code, simply set the pinMode to INPUT_PULLUP in your setup() code.
But why do we need a resistor to pull a pin up to 5V or down to GND?
Well, as you might remember from our very first intro to circuits, any time you connect a voltage source to the GND in a circuit, you're shorting that source all the way to ground. If we apply Ohm's law to that short, we get I = V/R = V/(a very very low number, essentially zero) = (as much current as our voltage source is capable of giving). In other words, when you short 5V to GND bad things happen. You could get sparks, wires glowing red, and flames from all the current running through the wire causing it to heat up (if your power supply is capable of providing that much current).
What is more likely to happen for us, however, is much less dramatic since the USB port on your computer probably can't supply a whole lot of current. Your Arduino will turn off because you've shorted the 5V rail down to ground potential, removing the voltage your Arduino needed to run on. And then your computer will likely disconnect the Arduino from the USB port in order to protect itself from the excessive current the shorted Arduino is trying to draw. In that case, you'd have to un-plug and re-plug the USB cable, or potentially even restart your computer, to re-enable the USB port and start using the Arduino again (of course, after you remove the short that's connecting 5V to GND!).
Placing that resistor in the circuit prevents us from directly shorting 5V to GND whenever the button is pressed.
Important: Make sure to use the 10kΩ resistors (color bands: black, brown, orange) in your kit any time you need a pull-up or pull-down for an Arduino input. If you're curious as to why that 10kΩ value matters, ask one of the OPS instructors in class! As a hint, it has to do with how the GPIO ports inside of the MCU function. This is a topic you can learn more about in ECE 306.
Like any sensor, it turns out buttons (and by extension, all switches) that use metal contacts to bridge a circuit have some unwanted signal properties that we have to deal with.
Like most metals do when you flex them, the contacts used in switches have some springiness. When you move a switch (or a button) from one position to another, the contacts have to flex to either to open the circuit or close it. As they flex, they can vibrate some due to that springiness in the metal, causing the circuit to open and close several times as the two pieces of metal bounce against each other. This phenomena of the contacts bouncing open and closed several times before they settle into a resting position is known as contact bounce.
But, one important thing to know about contact bounce, is that it doesn't happen in "human" time; in a decent quality button, the contacts will only bounce for tens of microseconds before they stabilize into the open or closed position (a low quality or worn out button might bounce for a couple hundred microseconds). Humans can't even perceive events that happen on the millisecond scale (10⁻³ seconds), much less anything on a microsecond scale (10⁻⁶ seconds). To us, when we press a button, it goes from being off to on. For example, when you built the push button circuit in the breadboarding workshop and pressed the button, you simply saw the LED turn on; you didn't see it flicker from off to on, because the contacts finished bouncing far faster than we can see.
Here's an oscilloscope screenshot showing what that switch bounce signal looks like for one of the buttons in your OPS kit:
As it turns out, this is actually really good performance as far as buttons go; it only bounced a few times and it settled in about 100 microseconds (0.1ms).
However, your MCU doesn't operate on "human" time. The ATmega328P in your Arduino operates at a clock speed of 16MHz—meaning the clock inside the chip that makes it execute our machine code instructions runs those instructions at 16MHz, or at a period of about 65.2 nanoseconds (10⁻⁹ seconds).
Now, a bit of nuance (that isn't super important to you) is that it usually takes several clock cycles to read a digital input (depending on the MCU). But still, in a fastest-case scenario, let's say our program is able to check a button pin every 1-2µs. If our button's contacts are bouncing for a a few dozen µs like we said earlier, that could mean our MCU reads 15-30 button presses every time we push the button once!
So clearly, we need to filter out that switch bounce!
With software debounce, we go ahead and detect those bounces in software but simply choose to ignore all of them with some time-based program logic. We'll be taking a closer look at this in project 4.
With hardware debounce, we need to add a storage element to our circuit that can absorb all of those bounces and smooth out our signal creating one single transition from low to high (or from high to low). We'll use a small 100nF (nano-Farad, referring to how much energy it can store) ceramic capacitor as our storage element and a 10kΩ resistor (separate from the pull-down discussed earlier) to control how fast the capacitor stores/loses its charge to do this, forming what is called a low-pass RC (resistor-capacitor) filter. It's a low-pass filter because it allows low frequencies (our button changing state) to pass through, but it cuts out (or attenuates) high frequencies (the contacts bouncing).
Don't worry if none of what you just read makes sense; you'll learn more about RC filters in ECE 200 (and you'll learn about capacitors in ECE 200 and Physics 2).
Here's what that hardware-debounced signal looks like (and the circuit that provides it):
(Note that the time scale here is different from before; it's zoomed out to show the full charge curve)
Note: Ceramic capacitors are not polarized, meaning it doesn't matter which pin is connected to GND in your circuit (hence the circuit symbol just having two flat bars instead of one flat and one curved).
Electrolytic caps like the one you'll use with the servo motor are polarized. They're discussed further down the page!
Phototransistors are a type of transistor that responds to light. They have no third pin for controlling the base current like normal transistors do; instead, the base junction inside the transistor is doped (additional elements fused onto the semiconductor) with material that makes it sensitive to a target wavelength of light. The phototransistors in our OPS kits respond to infrared light (more specifically, the 940nm band of IR).
Since the base of the transistor is controlled by the light coming into the transistor, that leaves us with just two pins: the collector and emitter. More infrared light shining into our phototransistor will cause more current to pass through from the collector to the emitter. We can use the phototransistor in series with a resistor to create a voltage (the voltage across the resistor) in our circuit that varies with light. In practice, this looks like a voltage divider, except our phototransistor goes on top instead of a second resistor.
Note: The pinout of your phototransistor is not the same as an LED. Reference the image below for how to identify the collector and emitter pins, and note which node they need to connect to in your circuit!
Another note, if you ever work with these in the future: not all phototransistors are created equal; unlike LEDs, their pinouts (what the short vs long pin connects to) can vary from manufacturer to manufacturer. We've provided you the correct pinout for the phototransistor in your kit below. But when in doubt, you can check them with a multimeter, or build a test circuit and shine a light (of the right wavelength) on them and observe their behavior in the circuit. You can Google how to do this if you need to.
Now, let's take a look at the phototransistor circuit below.
We know our phototransistor controls the amount of current flowing through it (more light = more current), but how can we use that varying current to create a varying voltage to measure with our analog input?
First, we need to know that microcontroller input pins are designed to have a very high input impedance (it's like resistance, but also taking into account some AC characteristics we don't need to worry about until ECE 211). Because that input resistance is really high, we can ignore the current flowing out of our phototransistor circuit and into the MCU's analog pin.
Since we're assuming current in that branch of the circuit is zero, we can say the current flowing through the phototransistor is equal to the current flowing through the 10kΩ resistor. As the phototransistor allows more or less current through the resistor, there the voltage dropped across the resistor changes proportionally (thanks to Ohm's Law). That voltage is what we're measuring.
Important: You have to place the phototransistor on the top (between the analog pin and 5V), and the resistor on the bottom of the circuit (between the analog pin and GND). If you switch the two components around, you'd be sampling the voltage across the phototransistor, which will decrease as the amount of IR light increases (reversing the values you get from analogRead())
Connecting the IR LED is much simpler; in fact, you connect it exactly the same as you have done for regular LEDs with a 470Ω current limiting resistor in series.
For simplicity, we can just connect the IR LED and its resistor in series from 5V to GND; we don't need to switch it on or off, so it doesn't need to be controlled by a digital pin like we have done with LEDs in previous projects.
Troubleshooting tip: Our eyes can't see infrared light, but our phone cameras can pick up a tiny bit of the infrared spectrum. To check if your IR LED is on, zoom in on it with your phone camera and look for a dim pinkish red glow. It'll look something like this:
The reservoir capacitor acts as a temporary storage device; it can feed a little bit of extra energy into the 5V rail to account for the large amount of power drawn when our servo motor first begins to turn (remember the startup spike discussion from lecture?). We need this extra bit of juice from the capacitor because the 5V source coming from our USB port isn't designed to provide large amounts of instantaneous current (i.e. the large current draw needed to kickstart the motor from standing still to turning). Instead, if we try to draw too much current, the 5V rail will start to "sag" or dip lower and lower, until it drops below the minimum voltage the Arduino needs to stay turned on. A motor startup spike can draw enough current to cause that voltage sag.
It's very important that you connect polarized capacitors with the correct orientation; feeding a reverse voltage into the capacitor can damage it!
The short pin, next to the gray stripe down the side of the capacitor, is the GND pin and should be connected directly to GND in your circuit. The other (longer) pin connects to 5V.
And if you were wondering: ceramic capacitors are never polarized; you don't need to worry about orientation when connecting your button debounce capacitors (that's why the symbols are different on the schematic!).
The polarized (electrolytic) capacitor in your kit is the black cylinder with 220μF written on the side (that's it's capacitance rating, which corresponds to how much energy it can store)
Servo motors allow us to rotate an arm (sometimes called the servo horn) to a specific angle between 0° and 180° using a PWM-like control signal.
Connecting the servo is simple; plug three jumper wires into the connector, and connect the other ends of the jumpers to 5V (labeled Vcc in the pinout diagram below), GND, and the PWM pin you want to use to control the servo.
Be sure to check the Arduino's pinout diagram to make sure the pin you're using for the control signal is PWM-capable.
And don't forget to make some kind of gauge background!
Get creative and draw out tick marks marks or something on some paper (or even print something out if you're fancy like that), then cut out a hole for your servo and tape it all together. We promise we won't laugh (too much) at your wicked artistic skills😊
You still have all the parts, code, and big brain energy you've used in past projects at your disposal.
It's not required, but we'd love to see what else you can add to your distance gauge, either in code or in additional components, to make it do cool stuff. Some ideas to get the gears turning:
Maybe you could make it act like a vehicle back-up alarm and beep your buzzer faster the closer an object is?
Add some kind of lighting display with LEDs that changes with the distance reading?
Give the user a way to manually adjust the calibration or sensitivity by turning a potentiometer knob?
And yes, we do know this project just used up all five of your 10k resistors. If you need to connect more buttons, there's a slightly less resistor-y way to do so: You can remove the 10kΩ pull-down resistors from your buttons, switch the other button connection from 5V to GND, and instead set their pinModes to INPUT_PULLUP in your setup code. This enables built-in pull-up resistors that are inside the GPIO ports on the MCU, ensuring the pin is always at 1 by default (and that means you'll need to check for when the pin goes to 0 in your code to know that the button's been pressed).
Why didn't we tell you about these earlier? Well that'd spoil all the fun, now wouldn't it! But also, do note that the MCU inside the Nano does not have built in pull-down resistors; that's something you don't see as often in microcontrollers (long story; it's an industry thing). But you will still need a 10kΩ and 100nF debounce circuit for each button.
Just like we saw with the tone function in project 2, the Servo library adds some restrictions to what we can do with the IO pins on our Arduino:
Using the Servo library will disable PWM functionality on pins 9 and 10 (meaning you can't use analogWrite() on those pins for this project).
Your servo's control pin needs to be connected to a PWM-capable pin (one other than pin 9 or 10).
Arduino Nanos can only control up to 10 servo motors at a time (you don't need to worry about that for OPS, but it's good to remember for any future projects you might embark upon)
Very Important: Don't try to force your servo motor to turn or put any heavy of load on the arm (e.g. don't try to lift weights with it). The plastic gears inside the servo are fragile and can shear off easily if you force them to turn. Additionally, the more torque your servo needs to generate, the more current it will draw, and the more likely you are to trigger over-current protection on your laptop's USB port (disabling the port and shutting down the Arduino).
If you need your servo arm to point in a different physical direction for the angle you've commanded it to using your code, take it the arm off and turn it where you want, then re-attach it. And you don't need to attach it with the included screw; it'll stay on well enough for our needs just from snapping it on.
The Servo.h library is included with the Arduino IDE and allows us to control small servo motors like the one in your kit with the PWM pins on your Arduino. If you go to the File -> Examples menu in the Arduino IDE, you can find some examples (Knob and Sweep) for the servo library under it's category.
Here are some useful bits of code to remember:
#include <Servo.h> tells the compiler to load the Servo library so we can use it's functions in our sketch.
Note the <Name.h> format used here instead of the "name.h" we've seen before. Angle brackets tell the compiler to include a built in library (stored in the IDE's installation files), while quotes tell the compiler to look for a user made file that we've saved in our project's folder (the same folder where your sketch .ino file is saved, usually in Documents/Arduino on Windows).
Servo myServo; goes somewhere at the top of your sketch above the setup() function (but below the #include statement!). This creates what's called a Servo object that we'll be able to issue commands to. If you take any C++ or Object Oriented Programming courses (like ECE 309), you'll learn more about what an object is and why they're useful to us.
myServo.attach(pin); goes in your setup() function and sends a command to your Servo object telling it to attach itself to the pin you've chosen for your servo. You don't need to use a pinMode() command to configure your servo pin as an output; the attach() command does this for you behind the scenes!
myServo.write(angle); is how we tell the servo to turn to a certain angle. This angle must be a number between 0 and 180.
Fun fact: If you're ever using a continuous rotation servo, the write() command will set the servo's rotation speed instead of its angle. 90 sets the speed to zero (stopped), 0 is full speed in one direction, and 180 is full speed in the other direction.
myServo.read(); returns an integer value of what the servo is currently set to (the last value that was sent to it with write()).
Example: int x = myServo.read();
map(value, inRangeMin, inRangeMax, outRangeMin, outRangeMax); Returns a value scaled from an initial range to a final range.
Make sure the value you give to map() is inside the initial range (between inRangeMin and inRangeMax), otherwise you may get strange junk data out of the map() function!
Example:
We read an analog value from 0-1023, but want to scale it to 0-255 so we can analogWrite( ) to an LED:
int y = map(analogRead(A0), 0, 1023, 0, 255); // takes an analog reading, which will be between 0 and 1023, and sacles it back to a number between 0 and 255
analogWrite(3, y); // outputs y PWM signal with to the LED's pin
Example:
We want to "reverse" a value. For example, our servo is backwards in our robot and we need to fix it in code.
int reversedAngle = map(originalAngle, 0, 180, 180, 0); // if originalAngle was 170, reversedAngle will be 10
myServo.write(reversedAngle);
Sometimes, we may end up with values that are outside of the bounds we want. To prevent this, we can constrain() our numbers.
int y = constrain(value, floor, ceiling); will limit a value to be equal to or greater than the floor, and less than or equal to the ceiling:
If value is between floor and ceiling, y = value
If value is below floor, y = floor
If value is above ceiling, y = ceiling
Example:
int input = 284; // too big for analogWrite()
int output = constrain(input, 0, 255); // clips input to 255
analogWrite(3, output); // pin 3 gets a 255 PWM signal
Arrays are a way to store several elements of the same data type in one place.
Declaration syntax: dataType name[size] = {element0,element1,element2,...};
Example: int myNumbers[4] = {4,8,16,32}; // myNumbers[] is an array containing 4 ints
How to access data in arrays:
Arrays are zero-indexed, meaning the first element in the array is number zero.
myArray[index] → “myArray select index” gives us whatever value is stored at that index in the array
Example: x = myNumbers[2]; // x is 16
How to store data in arrays:
We can modify the value at any index in an array by selecting that index and setting it equal to some value.
Example: myNumbers[2] = 3; // myNumbers is now {4,8,3,32}
Signal filtering is important with sensors because the real world is noisy. In the case of our light sensor, we have to worry about light-based noise sources like TV remotes, sunlight, and lights being switched on or off (or flickering at a frequency that resonates with our sensor, such as with fluorescent lighting) in the background. Fail to filter out all these noise sources and they can generate negative consequences in your output such as jitter, drift, and clipping.
Filtering out light-based noise is our primary concern with this project, and the primary indicator that your filtering code isn't working well is one of two things. Either your servo arm will jitter, indicating your averaging filter isn't strong enough, or your servo arm won't move across it's full range as you move an object farther and closer to the sensor, indicating your upper and lower bound calibration isn't working.
And if all this sounds fascinating to you, you might enjoy taking a controls class like ECE 308!
There are several ways to filter a sensor's signal in code. In fact, it's a whole field of programming!
For our purposes, we'll use a very simple filter called a moving average. The movingAverage(newValue) function, which we've included for you in the pseudocode file, simply keeps a buffer of the last n sensor samples and returns the average of those samples every time you call it.
It has one argument, which is the next value (int) to put into the averaging buffer (n-size an array). It returns an int, which is the average of the samples in the buffer (including the new value you just gave it).
If you have more questions about how this moving average works, or what other programmatic filtering options are out there, ask us!
Calibration is also very important when using sensors. In our case, we're using a light sensor. And depending on where we are (outside in the sun, in a dark room, under bright fluorescent classroom lights, etc.), the ambient light levels will vary. So naturally, we need some way of telling our program what the ambient light level is, and then the program needs to subtract that ambient light level from the reading it takes (so that our zero point is at the ambient level).
This can be pretty easy. You can create a global variable to store the ambient light level (like minIRValue). Next, connect a button and use that as a "calibrate ambient" button (or whatever you want to call it). Finally, whenever the button's pressed, take a reading from the light sensor and set that to the minIRValue. and use that variable as a lower bound in your map and constrain statements.
And because we might not read a full 5V on our analog pin when an object is as close to the light sensor as possible, we'll also need an upper-bound calibration variable (maxIRValue perhaps), to make sure we map our sensor's real distance resolution to the full 180 degrees of our servo. We'll attach a second calibration button to collect a sample and set the the maxIRValue. But now, before we press this button, we'll have to place some object right in front of our sensor, so that the reading we take represents the maximum mount of light that could get reflected back into the sensor.
Or, if you want your calibration to be more accurate, you could write code to take several samples when you press a button and store the average of those samples as the calibration value. But remember, for this averaging to be useful, you'll need a bit of delay between each sample (1-20ms should be plenty)! This way, if you happen to take a sample at the very moment some background noise hits the sensor (e.g., someone using a TV remote on the other side of the room, or a light being switched on somewhere), that noise should get eliminated once the other samples are averaged in.
Here's pseudocode to guide you toward basic distance gauge functionality. You can treat this as a fill-in-the-blank to get your basic code working, then add additional features onto it if you want.
As always, remember to work on just one thing at a time, then test your code and make sure that part works before moving on to the next part of your program. Printing messages and variables out to the serial console can help a lot with your testing, especially when it comes to signal filtering and making sure your filtering code / math is working the way you want it to.