Lecture slides, class recording, and project specs for this module are available below.
You can find additional resources for this module (and others) in the Course Wiki.
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.
The circuit for this project has two important sections. The first part is the IR light sensor (a phototransistor and IR LED), and the second is the servo and its reservoir capacitor. Let's take a closer look at how each section works.
Phototransistors are a type of transistor that responds to light. They have no third terminal for controlling the base current; 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 pin is controlled by the light coming into the transistor, that leaves us with two pins: the collector and emitter. More infrared light shining into our photodiode 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 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 orientation they need to have in your circuit!
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 pin?
First, 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). Because that input resistance is really high, we can ignore the current flowing out of our phototransistor circuit into the Arduino. 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.
From Ohm's law, we know that voltage = current × resistance. If the resistance is constant (our resistor's value isn't changing), and the current through the resistor is varying (depending on the phototransistor's state), then we can visualize how the phototransistor's control over current is able to change the voltage across the 10k resistor (and that's what we're measuring on our analog input pin).
Important: You have to place the phototransistor on the top of the input node (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 your analogRead() values!)
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 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 faint 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 voltage regulator on our Arduinos 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); that can burn the regulator out.
It's very important that you connect polarized capacitors with the correct orientation; feeding a reversed 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.
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 set 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 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 Nano's pinout diagram from Module 1 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?
Just like we saw with the tone fuction 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 Nano's 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 projecs you might ebark 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 break 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 damage the voltage regulator on your 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 screws; 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, 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}
Todo (for now, you can reference the comments for movingAverage() in the pseudocode below)
Todo (for now, you can reference the lecture slides)
Here's pseudocode for 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.
For simplicity, you can copy-paste this into the Arduino IDE and use it as your starting point.
// Include the Servo library and define pins
#include <Servo.h>
#define servoPin #
#define photoPin A#
#define notifLED #
// Set some default values for our signal filter parameters
int minIRValue = 60; // Minimum IR value to consider valid
int maxIRValue = 600; // Maximum IR value to consider valid
// Variables used by movingAverage()
#define samples 5 // How many previous samples should we average together?
int buffer[samples] = {0}; // array of size (samples), all values initialized to 0
int index = 0; // To keep track of which sample we're updating in the buffer
int sum = 0; // To keep track of the sum of all the samples currently in the buffer
// Create the servo object (we're just naming it servo)
Servo servo;
// Declare our averaging function
int movingAverage(int);
void setup() {
// Configure pin modes
// Attach the servo object to the pin we're using for the servo's PWM signal
servo.attach(servoPin);
/* Move the servo to zero?
Sweep it back and forth like a car dashboard when it starts?
Blink an LED?
Up to you!
*/
// Start serial communication (highly recommended for debugging)
// Calibrate for the ambient IR in the room
int cal;
/* We will learn more about for loops in project 4!
What for() is doing here: run the following code once for
however many samples movingAverage() uses.
It uses the i variable to keep count of which iteration the
loop is currently on, i++ means add 1 to i after each
iteration, and it stops when i < samples is no longer true.
*/
for (int i = 0; i < samples; i++) {
// Take a reading from the phototransistor
int distanceReading = analogRead(photoPin);
// Compute this reading into a moving average.
cal = movingAverage(distanceReading);
delay(100); // Wait a bit before we take the next reading
}
minIRValue = cal + 10; // Add a safe margin to the average we just calculated
/* Should we add a calibration process for the maximum possible IR value too?
(i.e. when an object is directly in front of sensor).
Maybe notify the user somehow (Serial message? Bink an LED? Rotate the servo?)
to place an object in front of the sensor, then compute another average like we
just did for the ambient IR value (minIRValue)?
Or dont. Up to you!
*/
}
void loop() {
// Take a raw reading from the sensor
// Compute this reading into our moving average.
int distanceReading = analogRead(photoPin);
// Constrain the averaged value to make sure it's between
// our minimum and maximum IR values
// Map the value to whatever range of angles we want on the servo
// Write the angle we just calculated to the servo
// Delay a bit so we don't sample the transistor too quickly?
// Bonus question: why might we want to delay between taking samples?
}
// Maintains a moving average of values stored in a buffer
int movingAverage(int newValue) {
// Subtract the old value from the sum
sum -= buffer[index];
// Store the new value in the buffer where the old one was
buffer[index] = newValue;
// Add the value we just got to the sum
sum += newValue;
// Move to the next index in the buffer (% is the modulo operator)
index = (index + 1) % samples;
// Return the average of what's in the buffer now
return sum / samples;
}