Project 1 Basic Blinky
Wiring it up

So going back to the onboard LED, we examine the schematic drawing and note that the LED is not actually connected to 5V, its connected to a microcontroller pin. Specifically, pin 11, which is also called PD6. PD6 is just an acronym/shortcut for "Port D pin #6". Ports are lettered to group them, a port tends to have 8 pins per group, but sometimes less. For example, this microcontroller has 3 ports, PORTA, PORTB, and PORTD. Port C is AWOL, there's probably a good reason they didn't include it but I have no idea why. On this micro, PORTA has only 3 pins, PORTB has all 8 and PORTD has only 7. Some micros have as many as 6 ports, each one with 8 pins. Obviously, they're much larger and more expensive, but they are available.

The essential capability of the microcontroller is in its ports & pins, its input/output (I/O) system. For example, if the microcontroller sets the voltage at PD6 to be 5V then the LED would turn on because current would from from the pin, through the LED and resistor and to ground. On the other hand, if the voltage at PD6 is 0V, then the LED would turn off because no current flows between 0V and ground (because, of course, ground is defined to be 0V). If, however, the LED and resistor were connected to 5V not ground, and the LED turned around, then it would light when the pin was set to 0V and turn off when the pin is at 5V. I.e. the microcontroller can control the LED by controlling the setting of pin PD6.

Blink

Blinking an LED is the 'hello world' of electronics. Lets look at the code for blink.c

00001 // this is the header file that tells the compiler what pins and ports, etc.
00002 // are available on this chip.
00003 #include <avr/io.h>
00004 
00005 // define what pins the LEDs are connected to.
00006 // in reality, PD6 is really just '6'
00007 #define LED PD6
00008 
00009 // Some macros that make the code more readable
00010 #define output_low(port,pin) port &= ~(1<<pin)
00011 #define output_high(port,pin) port |= (1<<pin)
00012 #define set_input(portdir,pin) portdir &= ~(1<<pin)
00013 #define set_output(portdir,pin) portdir |= (1<<pin)
00014 
00015 // this is just a program that 'kills time' in a calibrated method
00016 void delay_ms(uint8_t ms) {
00017   uint16_t delay_count = F_CPU / 17500;
00018   volatile uint16_t i;
00019 
00020   while (ms != 0) {
00021     for (i=0; i != delay_count; i++);
00022     ms--;
00023   }
00024 }
00025 
00026 int main(void) {
00027   // initialize the direction of PORTD #6 to be an output
00028   set_output(DDRD, LED);  
00029 
00030   while (1) {
00031     // turn on the LED for 200ms
00032     output_high(PORTD, LED);
00033     delay_ms(200);
00034     // now turn off the LED for another 200ms
00035     output_low(PORTD, LED);
00036     delay_ms(200);
00037     // now start over
00038   }
00039 }

(The lovely code formatting is generated by doxygen)

Now lets do a simple walkthrough, since this is the first piece of avr code you've seen. Start with the first 3 lines, 2 of which are comment.

00001 // this is the header file that tells the compiler what pins and ports, etc.
00002 // are available on this chip.
00003 #include <avr/io.h>

Only one header is needed for this code, "io.h" which contains definitions and macros for dealing with the I/O system. You can look at these headers, they're stored in "/usr/local/avr/include/avr/" (or something like that) in linux and "C:\WinAVR\avr\include\avr" in windows. In reality, io.h is just a 'stand in' for the real header file, which for this chip is "avr/iotn2313.h" which loosely translates into "I/O tiny2313." How does the header file know that we're compiling for this chip? Well, we tell it via the Makefile (MCU = attiny2313, in the first line) which gets passed to the compiler when we compile. Basically, we just always use <avr/io.h> and define the actual chip in the Makefile so that its easy to change what chip you're compiling for.

00005 // define what pin the LED is connected to.
00006 // in reality, PD6 is really just '6'
00007 #define LED PD6

Here we make a little macro just to make our code simpler: we call the pin that the LED is connected to "LED" instead of what it really is, which is PD6. Just a naming shortcut. One thing to note is that even though "PD6" sounds like it completely defines the pin (PORTD #6) it actually doesn't. In reality, PD6 is just 6. When we want to actually turn the pin on and off, we'll need to specify the port also. This is one of the great bummers of avr-gcc...some other compilers make it easier to define both the port and pin in one go. Oh well.

00009 // Some macros that make the code more readable
00010 #define output_low(port,pin) port &= ~(1<<pin)
00011 #define output_high(port,pin) port |= (1<<pin)

This is actually the most important part of the code. Here is where we do the 'heavy lifting' of the code: turning the pin on and off. To understand the real beauty here, one must realize that with microcontrollers, the way you get the brains to do something is to twiddle with some of its "I/O memory."

What is I/O memory?
Well, you know that a computer has memory in it. Memory is where things are stored and retreived. Memory is also addressed. The first address is 0 and the last address is however much memory you have. Lets say you have 256 bytes of memory, then the last address is 255 (because we start counting with 0). In reality, desktop computers have massive amounts of memory, but the microcontroller's memory is small and really is on the order of 256 bytes.

Microcontrollers have quite a few different types of memory. One is "RAM" memory, which is actually the same as the sticks of DDR RAM you put into your computer. Except that the micro has about 128 bytes, not 128 megabytes. And there's flash memory, which is where the microcontroller stores the program (such as blink_led.c) and there's also EEPROM memory which is sort of like a permanent RAM. And theres also that I/O memory.

I/O memory is addressed just like other memory, on this chip there are 64 (0x0 through 0x3F) locations. Each location is called a "register" mostly because its faster than saying "I/O memory location." And each register has a particular job. Register #18 (0x12), for example, is called the "PORTD" register and is, not surprisingly, in charge of the I/O pins on PORTD. (You can see what the other registers are for in the Attiny2313 datasheet under "Register summary.")

And every bit in this register (registers are 8 bits wide) corresponds to an actual, physical pin on the microcontroller. (Except for PORTD bit #7 which corresponds to nothing because there is no PD7 on this chip.) If you set only PORTD bit #0 to be 1 (e.g. PORTD = 0x01) then PD0 will have 5V on it and the rest of the pins in PORTD will have 0V. If you set only PORTD bit #0 to be 0 (e.g. PORTD = 0x7E) then PD0 will have 0V on it and the rest of the pins will have 5V.

So the macro output_high(port, pin) does exactly what it says, turning on the one bit in the register that corresponds to that port. For example, output_high(PORTD, 0) sets PD0 to high/5V and vice versa, output_low() turns off the bit.

00012 #define set_input(portdir,pin) portdir &= ~(1<<pin)
00013 #define set_output(portdir,pin) portdir |= (1<<pin)

These macros are identical, but named differently just to make the code look neater. In this case, we are going to modify the I/O registers associated with the direction of the pin. That is to say, each pin on the microcontroller can be an input (i.e. reads the voltage on the pin) or an output (i.e. sets the voltage on the pin). If you're trying to tell if a button has been pressed, for example, you want the pin to be an input. If you're tyring to turn on an LED you want the pin to be an output. These registers are called DDRx where DDR stands for "Data Direction Register" and x is the port designation, say like DDRD for port D.

00015 // this is just a program that 'kills time' in a calibrated method
00016 void delay_ms(uint8_t ms) {
00017   uint16_t delay_count = F_CPU / 17500;
00018   volatile uint16_t i;
00019 
00020   while (ms != 0) {
00021     for (i=0; i != delay_count; i++);
00022     ms--;
00023   }
00024 }

This function is pretty simple, but not worth going into detail here. Regardless, it can be used to make the microcontroller "wait around" for a specified number of milliseconds. Note that only an 8 bit variable is passed so you can't wait more than 255 ms at a time. If you want to wait longer, just call it more than once.

00026 int main(void) {

Just like other types of programs, the actual execution starts at main(). Of course, no arguments get passed to main(). Oddly enough, though, there is a return value. I'm not quite sure why the compiler requires it since theres 'nowhere' to return to. Note that when main() finishes executing, it just starts over again, you cant 'quit' main() because theres nothing else on the chip to run!

00027   // initialize the direction of PORTD #6 to be an output
00028   set_output(DDRD, LED);  

The first thing we do is set the direction, using the macro we've already defined. This is pretty straightforward. By default, all pins are set to inputs to start. This is because its safer to be an input pin than an output pin: nothing bad happens if two chips are connected with the inputs together, but if two outputs are together and they're set to different values, it can damage the pin.

00030   while (1) {
00031     // turn on the LED for 200ms
00032     output_high(PORTD, LED);
00033     delay_ms(200);
00034     // now turn off the LED for another 200ms
00035     output_low(PORTD, LED);
00036     delay_ms(200);
00037     // now start over
00038   }

Now we have a loop and all we do is turn on and off the LED, waiting 200ms inbetween. There isn't much going on here either.

Compile

Now we're ready to compile the code, turning it from a text file (ledblink.c) into an executable file for the microcontroller (ledblink.hex)

There's a whole bunch of stuff thats happening here, but most of it is uninteresting. Near the end, the makefile spits out the 'size' of the file, which tells you how much flash memory is taken up by this code: 188 bytes. Thats not too bad, remember that the microcontroller itself can hold up to 2Kbytes of flash code and the bootloader takes up a quarter of that. So whenever you right code for the Atmex, just make sure that the size is less than 1.5K.

Burn

Now that the .hex file is generated, its time to burn it into the microcontroller's permanent flash memory. Type "make program_ledblink" into the command prompt and hit return just after pressing the reset button on the Atmex.

Note that the 188 byte size shows up again. Which makes sense.

Now press the reset button to reset the Atmex, a few seconds afterwards, the bootloader should timeout and you'll see the LED blink.

Exercises

Now that you've gone through the code and process. Its time to make modifications to the code. Try out these exercises to verify what you've learned:

  1. Make the LED blink twice as fast
  2. Make the LED blink twice as slow
  3. Change the line that sets the pin direction so that its an input, what happens? (Turn off the light and observe. This effect will be explained later...)
  4. Make the LED blink REALLY fast (like, only 5ms delay) What happens? Turn off the light and wave the board around.
  5. Make the LED blink out morse code for SOS (3 short blinks, a pause, 3 long blinks, a pause, 3 short blinks, a long pause...)
  6. Make the LED blink out morse code for "Hello World"
  7. Make the LED blink out morse code for Shakespeare's Sonnet #73. Lovely, isn't it?
May 17, 2011 20:07