In this lesson, we will build on the blink and basic LED tutorials. We will connect three LEDs to the Oak and control them independently using analogWrite()
. The use of a for()
loop will also be introduced to show you how to cycle through a range of variables, in this case the brightness and blinking speed of the LEDs.
Part | Quantity | Identification |
---|---|---|
Oak with soldered headers | 1 | |
Breadboard | 1 | |
Jumper wires | 7 | |
220 ohm resistor | 3 | Red-Red-Brown |
LEDs, 5mm | 3 | Any color |
Notes: resistors ranging from 100-1,000 ohms are all in a good range. Lower resistance will be brighter, while 1k ohms will produce a slightly dimmer LED.
Any size LED is acceptable; 5mm is simply a recommendation.
A for()
loop is a core function commonly used in microcontrollers, as well as programming in general. In layman's terms, the for()
loop creates a temporary variable (often an integer, i
, is used) with a starting value, a condition in order to know whether or not to keep running, and a way to modify that variable with each pass. Lastly, code is contained within the for()
loop, typically based on the temporary variable's current value.
That might seem abstract, so here is a concrete example:
for(int i = 0; i < 5; i = i + 1) { Serial.println(i); }
The format of the for()
line is always the same, with it having three parts:
int i = 0
: the first part defines a new temporary integer variable called i
and sets it's starting value to 0.i < 5
: the next part tells the for()
loop how to know if it should continue running. This is usually a condition that needs to be TRUE
; in our case we're using “less than” (<
) 5.i = i + 1
: the last part tells the loop how to change i
with each pass. In this case, we are defining the new value of i
as the old value + 1. This is commonly written as i++
, which does the same thing. You could also use i = i - 1
, i = i + 10
, i = i * 5
or any other equation you choose.
What does this loop do? It prints the value of i
each time through the loop until the condition i < 5
is no longer met. Here is a table showing what will occur:
Time through loop | Value of i | Value printed |
---|---|---|
1 | 0 | 0 |
2 | 1 | 1 |
3 | 2 | 2 |
4 | 3 | 3 |
5 | 4 | 4 |
The last time through, i
contains the value 4, the code is run (printing out 4
via serial), and then 1 is added to i
, making it 5. When the loop goes back to the top i < 5
will be false and the loop will exit. Keep in mind that a for()
loop is often contained within the loop()
section, so once the for()
loop exits, it will go back to the top of loop()
and start over. This will begin the for()
loop all over again with i
reset to 0. Using the above in a loop()
section would would print 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0 …
again and again.
loop() { for(int i = 0; i < 5; i++) { Serial.print(i); } }
The map()
function takes arguments for a variable, the variable's range, and the output range:
map(var, var_min, var_max, out_min, out_max)
The function creates a linear mapping, just like converting from, say, Fahrenheit to Celsius. We would use map() to do so like this:
map(temp, 32, 212, 0, 100)
Some temperature in F is stored in temp
, and we feed it into map()
which understands the linear relationship between F and C based on the two pairs of min/max values we gave it. One other way to think of map is with two number lines:
var | |var_min|-----------|-----------------|var_max| |out_min|-----------|-----------------|out_max|
We know how the two endpoints of the input and output scales match up since we've told map()
the min/max of each. Thus, for some value on the variable's scale, map()
can translate it to the output scale.
What the map()
function will not do is constrain the value. In fact, if you look at the code for map()
(shown at the end of the function definition), you may realize that map()
uses these min/max pairs to create the formula for a line! With the temperature example above, we could actually feed in 0 F (even though it's less than the min value, 32, we provided) and receive the output in degrees C.
This is where constrain()
comes in. It's a pretty simple function: constrain(x, min, max)
. If x
is less than min
, the function returns min
. If x
is bigger than max
, max
is returned. We will use this below to set up a constrained output from 0-1023, the min/max used by analogWrite()
.
We will connect this circuit, which is three copies of the basic LED circuit.
Steps:
Here is an example of this setup in real life:
We will begin by controlling 1 led, changing it's on and off time using a for()
loop:
// create two integers to hold the on and off delay times int on_time; int off_time; void setup() { // set pin 6 to an OUTPUT pinMode(6, OUTPUT); } void loop() { // the for() loop will initialize a new temporary variable, i // it runs until i >= 500, adding 50 each time for(int i = 50; i < 500; i = i+50) { // the on time will equal i (50, 100, 150 ...) on_time = i; // the off time will equal 500 - i (450, 400, 350 ...) off_time = 500 - i; digitalWrite(6, HIGH); delay(on_time); digitalWrite(6, LOW); delay(off_time); } delay(1000); }
Here is what happens each time the loop runs:
time through loop | i | on_time | off_time |
---|---|---|---|
1 | 50 | 50 | 450 |
2 | 100 | 100 | 400 |
3 | 150 | 150 | 350 |
. | . | . | . |
. | . | . | . |
10 | 450 | 450 | 50 |
When you upload this code, you will see this (other LEDs shown removed for simplicity; we'll use them next!):
This next example will use all three LEDs, pulsing them from 0 to bright, and then back down… to accomplish this, we will use a for()
loop, map()
, and constrain()
. Here is the code:
// these values will store the brightness for each of our LEDs int led_1_bright = 0; int led_2_bright = 0; int led_3_bright = 0; void setup() { // set all LED pins to OUTPUT pinMode(6, OUTPUT); pinMode(7, OUTPUT); pinMode(8, OUTPUT); } void loop() { // this first loop will run from i = 0 until i = 2000 for(int i = 0; i <= 2000; i++) { // these lines map the value of i to the desired brightness range, 0-1023 // because we've "offset" the min/max of i, we need to constrain // the output to make sure it's between 0-1023 // Can you figure out why the input min/max's are not the same? led_1_bright = constrain(map(i, 0, 1000, 0, 1023), 0, 1023); led_2_bright = constrain(map(i, 500, 1500, 0, 1023), 0, 1023); led_3_bright = constrain(map(i, 1000, 2000, 0, 1023), 0, 1023); // each time through the loop, we write each brightness // value to it's corresponding LED analogWrite(6, led_1_bright); analogWrite(7, led_2_bright); analogWrite(8, led_3_bright); // we delay for 1 millisecond, otherwise this runs at // lightning speed! delay(1); } // now we repeat the same cycle of i from 0-2000, but with a twist! for(int i = 0; i < 2000; i++) { // this is the same code except that our output range in map() // is reversed! When i is 0, led_1_bright will be 1023; // when i is 1000, it will be 0. This let's us dim the LEDs led_1_bright = constrain(map(i, 0, 1000, 1023, 0), 0, 1023); led_2_bright = constrain(map(i, 500, 1500, 1023, 0), 0, 1023); led_3_bright = constrain(map(i, 1000, 2000, 1023, 0), 0, 1023); analogWrite(6, led_1_bright); analogWrite(7, led_2_bright); analogWrite(8, led_3_bright); delay(1); } }
They key portion of this code are the two loops and the constrain(map(…))
lines. Did you figure out why each LED has a different input range for i
? It allows us to offset the behavior of each LED.
i range | LED 1 | LED 2 | LED 3 |
---|---|---|---|
0-500 | ramping to half | off | off |
500-1000 | ramping to full | ramping to half | off |
1000-1500 | full on | ramping to full | ramping to half |
1500-2000 | full on | full on | ramping to full |
The loop then re-runs, but reverses the mappings so that each LED gradually turns off slightly staggered from one another. Upload the code, and see this!
Congratulations for finishing another tutorial! You've completed a more complex circuit and learned some great tools for future use: for()
, map()
, and constrain()
. These are extremely useful functions, and often used to match an input range to an output. We used a loop variable, i
, however this would enable you to do something like this: