Microcontroller - A Beginners Guide - Revising the First Program to Make the LED
Blink and an Introduction to Bitwise Operations
You've written the
first program, which turned on an LED. Yeah, that was spectacular! Well,
not really, but let's introduce a little bit of craziness to the LED. We'll give
it a "bi-polar" personality by making it blink. Then we'll step it up a notch and
make it blink really fast. The program is surprisingly concise and easy to implement.
See for yourself:
#include <avr/io.h>
#include <util/delay.h>
int main(void)
{
DDRB |= 1 << PINB0;
while (1)
{
PORTB ^= 1 << PINB0;
_delay_ms(100);
}
}
HEY!! What are those crazy symbols!! Crazy seems to be a common theme here. Well,
I'll explain. But, I promise, it's pretty easy. You just need to memorize a few
things. I will get into a little bit of detail, but if you don't get it, don't worry,
just memorize what the entire statement accomplishes and you will be fine. I will
give you a hint along the way. Here comes the detail of bitwise operations:
There are logical operators called AND, OR, NOT and XOR that we are concerned with
at this point. They really do mean what they say. This is all for the sake of comparing
two bits, "1" and "0". See? How can just comparing two numbers be difficult! "1"
AND "1" is True or "1". And yes, you guessed it, "0" AND "0" is False or "0". But
what is "0" AND "1" and vice versa? Well, False or "0", obviously. Those guys are
different, so, truely, they are false.
Example AND binary number operation (the "&" is used in C programming for AND):
01001011 &
10001101
equals
00001001
Easy, right? Well, OR is even easier! "1" OR "1" is "1", obviously. "0" OR "0" is
also "0". It's starting to look very similar to AND, but here is the difference.
If the two numbers are different, it will result in "1". So, "1" OR "0" is "1".
Let's use the same binary number for the example (that funny character "|" is the
OR, sometimes located above the "\" onthe keyboard):
01001011 |
10001101
equals
11001111
Hmm, that turned on all of the places where there were missing ones! This is where
the magic happens. Yes, it kept the ones in the first number and where there are
ones in the second binary number, but not the first, it changed those to ones. Very
simple. NOT simply takes all of the bits and flips them. For example, for this binary
number: 01001101 will turn into 10110010. It turned the 0's into 1's and 1's into
0's, but don't get this confused with the XOR.
XOR is similar to OR but the "1" XOR "1" is actually "0". I'll just show the example
and you can figure out what happenes.
01001011 ^
10001101
equals
11000110
Yep, you guessed it, the "^" that is above the "6" is the XOR symbol. Hmmm... The
"|" and the "^" are in the program. Cool. Well, since we are thinking program, let's
disect the stuff we don't know yet!
#include <util/delay.h>
You already know what the <avr/io.h> does, so I won't waste your time with
that one, but there is a new #include statement. The delay.h will provide us a couple
of convenient methods for us to use. Just like the name implies, the delay.h will
provide us with a way to create delays in our program.
Skipping over the "main" since we know that already, we see the DDRB changed. Don't
be scared! Here is the process from where we were to where we are:
This is what we had before. This is not a very good way of minipulating the pins
because we just changed all of the pins from 1 to 7 as input, but "what if" we had
a larger program that used those pins for other things, like for instance, pin 2
allpies brake pressure control for the anti-lock braking system. You wouldn't want
to just arbitrarily set that as an input. That would render your brakes useless
(which would be REALLY bad).
DDRB = 0b00000001;
We need a way to ONLY affect one bit, the pin 0 bit. Well, if you look above at
the "OR", we can do this with a mask (not the carnival mask you are thinking, but
a binary mask.
DDRB = DDRB | 0b00000001;
This will takes it's former self and "OR" it to a mask. The mask is: 0b00000001.
Yes this looks like the actual binary number, but if the previous DDRB was, say,
0b01001010, then doing an OR to that with our mask would be: 0b01001010 | 0b00000001
= 0b01001011. What is different in the result. That's right, only the pin 0 bit
is changed!
This statememt can be further compressed in C++:
DDRB |= 0b00000001;
But that is not what is in the program. Even though this is perfectly valid, why
don't we take advantage of some of the definitions in the io.h header file. I mean,
it's there for our convenience, isn't it? Notice my use of contractions! This is
the same in C++: "it's" is really "it is", just like "DDRB |= 0b00000001" is the
same as "DDRB = DDRB | 0b00000001". I sink in my chair with that bad analogy. Whataver,
helps!
So why "DDRB |= 1 << PINB0"?
1 << PINB0 is the act of creating the mask. The "1" represents
what will be inserted into the mask, the << is a left shift operator. It does EXACTLY
what it says, and PINB0 is a number of positions that the "1" will shift left. In
essence, PINB0 is just equal to 0. So, you start with a 0b00000000, and you add
a "1" to make 0b00000001 and then you shift it left 0 positions. So you are left
with 0b00000001, the same number from above. So, what if it was PINB4? The statement
would be: 1 << PINB4. The "1" would be left shifted 4 times resulting in: 0b00010000.
Remember, we are using a zero index, so there is four 0s after the 1.
Let's move on to the While loop. You're right, we didn't have anything in the "infinite
loop" before. Well, now we need the microcontroller to show some action. This is
only possible within the loop. The loop is where the action is repeated over and
over. If the action was located before the loop, then the action would only happen
once, like setting the direction of the pin, which is appropriate for this program.
But to create forever blinking, we need to turn the PINB0 on and off within the
loop. Here is also where the delays come in. If we didn't have delays, we would
not see the LED blinking at all, it would look like it is just on, since the blinking
would occur faster than the eye could perceive, so we need to slow it down.
We know how to set a specific bit in the binary number, but we don't know how to
make a specific bit "0" if it is a "1" yet. The following code does just this, but
you will notice that it is not what the program shows. The first couple of lines
turns the bit to "1" (5 volts, light), and pauses for 100 miliseconds (by the way,
you can use microseconds by changing the "ms" to "us"). The second two lines turns
the PINB0 bit to "0" (0 volts, no light). No, the AND comparison cannot just make
a "0" from the bit, but if you NOT "~" the binary number mask, it will turn all
of the 0s to 1s and all of the 1s to 0s. This will permit you to only affect the
PINB0 bit and turn it into a "0". I added the parenthesis just to contain the masking
operation so the NOT could NOT the whole maks and not just the "1" before the left
shift "<<".
PORTB |= 1 << PINB0;
_delay_ms(100);
PORTB &= ~(1 << PINB0);
_delay_ms(100);
If the delay is going to be the same for on and off, we could shorten the previous
four lines to only two and take advantage of the XOR operation. Remember, the XOR
will turn our specific pin to a 1 if it is 0 and vice versa. This instruction will
only affect the PINB0. Every time that the instruction is executed, it will flip
the bit to the opposite.
PORTB ^= 1 << PINB0;
_delay_ms(100);
That's it. See that was not painfull at all.