« take me back please!

AVRs for friendly people {in progress}

Preface

Once upon a time, two Norwegian dudes decided to design their own microcontroller architecture. First it was available as an IP block (something companies could buy to stick in their larger chips) but today it is available in a number of chips manufactured by the Atmel Corporation. Yes, we are talking about the Atmel AVR.

AVRs are the microcontroller of choice for EE projects at my university, but I learned how to use them long before I was a student there. At that time there were none of these fancy Arduinos and no super easy IDEs. Fortunately, I had a friend I could annoy pretty much continuously with my questions (he probably regrets giving me his email) and who I learned a great deal from. I have recently realised that perhaps not everyone has the same opportunity that I had, and I also remember how painful and slow it was to learn how to program AVRs in C with little to no beginner resources. This guide is me giving back to the community that gave me so much: this is my guide to AVRs (for friendly people).

Who this guide is for (and some other important information)

Having read a few programming books in my time, I understand that two things are absolutely critical in a guide: a “who this is for” section and a “hello world” program. We will get onto the latter shortly but here is the former:

This guide is for anyone who wants to start using C and microcontrollers to do stuff. No, this won’t make you an expert and no, you won’t find answers to your homework here (I hope); it is designed for the pragmatic hobbyist rather than the guru algorithm builder. Alternately, while I have a great deal of respect for what the Arduino has done for electronics in general, I feel like using them is cheating (get off my lawn, etc etc). In this guide I will be minimising all this sugar-coating that has become so popular, mainly because I prefer to know exactly what is going on down at the lower levels. Even if you still love your Arduino to bits, you may find this useful in understanding what goes on behind the scenes.

As an aside, every time an EE student builds their engineering thesis with an Arduino I want to cry. Don’t do it.

I won’t be teaching much C in this guide, but don’t be afraid if you don’t know a lot of it yet. A lot of other languages are based on C syntax (Java, Javascript, Wiring, PHP and the list goes on) so you might be able to work out what is going on without too much difficulty. A knowledge of binary and hexadecimal numbering systems would also be a huge advantage. You should also know how to wire up a basic circuit on a breadboard/protoboard if you would like to try any of the examples out.

Since there are so many different AVRs with different features, I will be using the popular and cheap ATMEGA88 microcontroller as a reference throughout this document. You can find the datasheet for this chip on the Atmel website here. It may prove useful to have this open in another tab to look at while you read through this guide.

Given that most people will be using Windows and the official Atmel development software runs on Windows, I will only be using Windows in this guide. I know this sucks (I’m writing this sshed into a Linux server from a Macbook right now) but I need to nail down some specifics so I can show what I am doing. All of the code will still compile on the other platforms.

Hopefully I will get some time after I finish this to rewrite the relevant parts of it to include OSX and Linux support.

I only have one request of you, the reader: please do not copy this guide to another website. I put a lot of hard work into this and am sharing it for nothing, so help a brother out. You are absolutely welcome to link back here though! I suppose I can forgive you if you want to throw this on your Kindle (or similar).

If you find any mistakes or have some suggestions, feel free to drop me a line at jeremy dot 006 at gmail dot com. Please mention “AVR Guide” in the subject so I don’t lose your mail.

&

Hi! In case we haven’t met before, I’m Jeremy. I’ve written a few programs in my time and I’d like to share some of them with you.

Project 1: Gerald (aka the stare-master aka “hello world”)

Gerald is where we begin. It’s super easy to understand the code, so why don’t we just jump straight in?

gerald.c:

#include <avr/io.h>

int main(void)
{
    DDRB = 0xFF;
    PORTB = 0xFF;
}

If you’ve ever dabbled in C, most of Gerald should be familiar; the first line includes a bunch of scary code that allows you to access the peripherals of your chosen AVR microcontroller, while lines 3,4 and 7 are just standard C syntax. Lines 5-6 are the ones we are interested in, but we need to go over the anatomy of an AVR first.

Ultimately, the reason you would use an AVR in your design is to have a chip that can flick its pins between a “high” voltage (usually 5/3.3V) and a “low” voltage (usually 0V). All your program does is tell it when and under what conditions to do this changing. A pin that can be set to either high or low is known as an IO pin (in output mode) and these pins constitute the majority of all pins on a microcontroller. Pins can also be configured so that your code can determine if an external circuit has applied a high or low voltage to them (input mode).

For reasons that may not be immediately obvious, the AVR architecture breaks IO pins into groups of up to 8. We call these groups ports and it is through these that we can control individual pins. In the case of the ATMEGA88, the IO pins are divided into three ports: PORTB, PORTC and PORTD. If you take a look at page 2 on the datasheet (remember, you can find it here), the diagram in the top right shows the pinout of the chip we are interested in. The PBx, PCx and PDx pins are IO pins, where the second letter in its designator is the port that it belongs to.

Ok, let’s jump back to Gerald. Now that we know a little bit more about the hardware, we can take a closer look at lines 5 and 6. Before we use any pins in our code, we need to instruct the AVR as to whether we want them to be input or output (they are IO pins after all). To do this, we put a special number (more on this later) in the Data Direction Register (DDR) that corresponds to the port the pin belongs to. In the case of Gerald, by assigning DDRB to be 0xFF, we set all of the pins in PORTB to be output. You might be able to work out what line 6 does now: it sets all of the pins in PORTB to be high.

Sidenote: for the sake of my sugar-minimisation policy, here is the contents of one of the io.h files for the ATMEGA88 IC:

iom88.h:

/* Copyright (c) 2004, Theodore A. Roth
   All rights reserved.

   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are met:

   * Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.

   * Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in
     the documentation and/or other materials provided with the
     distribution.

   * Neither the name of the copyright holders nor the names of
     contributors may be used to endorse or promote products derived
     from this software without specific prior written permission.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  POSSIBILITY OF SUCH DAMAGE. */

/* $Id: iom88.h,v 1.4.2.6 2009/02/11 18:05:30 arcanum Exp $ */

#ifndef _AVR_IOM88_H_
#define _AVR_IOM88_H_ 1

#include <avr/iomx8.h>

/* Constants */
#define SPM_PAGESIZE 64
#define RAMEND       0x4FF
#define XRAMEND      RAMEND
#define E2END        0x1FF
#define E2PAGESIZE   4
#define FLASHEND     0x1FFF


/* Fuses */
#define FUSE_MEMORY_SIZE 3

/* Low Fuse Byte */
#define FUSE_CKSEL0 (unsigned char)~_BV(0)  /* Select Clock Source */
#define FUSE_CKSEL1 (unsigned char)~_BV(1)  /* Select Clock Source */
#define FUSE_CKSEL2 (unsigned char)~_BV(2)  /* Select Clock Source */
#define FUSE_CKSEL3 (unsigned char)~_BV(3)  /* Select Clock Source */
#define FUSE_SUT0   (unsigned char)~_BV(4)  /* Select start-up time */
#define FUSE_SUT1   (unsigned char)~_BV(5)  /* Select start-up time */
#define FUSE_CKOUT  (unsigned char)~_BV(6)  /* Clock output */
#define FUSE_CKDIV8 (unsigned char)~_BV(7) /* Divide clock by 8 */
#define LFUSE_DEFAULT (FUSE_CKSEL0 & FUSE_CKSEL2 & FUSE_CKSEL3 & FUSE_SUT0 & FUSE_CKDIV8)

/* High Fuse Byte */
#define FUSE_BODLEVEL0 (unsigned char)~_BV(0)  /* Brown-out Detector trigger level */
#define FUSE_BODLEVEL1 (unsigned char)~_BV(1)  /* Brown-out Detector trigger level */
#define FUSE_BODLEVEL2 (unsigned char)~_BV(2)  /* Brown-out Detector trigger level */
#define FUSE_EESAVE    (unsigned char)~_BV(3)  /* EEPROM memory is preserved through chip erase */
#define FUSE_WDTON     (unsigned char)~_BV(4)  /* Watchdog Timer Always On */
#define FUSE_SPIEN     (unsigned char)~_BV(5)  /* Enable Serial programming and Data Downloading */
#define FUSE_DWEN      (unsigned char)~_BV(6)  /* debugWIRE Enable */
#define FUSE_RSTDISBL  (unsigned char)~_BV(7)  /* External reset disable */
#define HFUSE_DEFAULT (FUSE_SPIEN)

/* Extended Fuse Byte */
#define FUSE_BOOTRST (unsigned char)~_BV(0)
#define FUSE_BOOTSZ0 (unsigned char)~_BV(1)
#define FUSE_BOOTSZ1 (unsigned char)~_BV(2)
#define EFUSE_DEFAULT (FUSE_BOOTSZ0 & FUSE_BOOTSZ1)


/* Lock Bits */
#define __LOCK_BITS_EXIST
#define __BOOT_LOCK_BITS_0_EXIST
#define __BOOT_LOCK_BITS_1_EXIST 


/* Signature */
#define SIGNATURE_0 0x1E
#define SIGNATURE_1 0x93
#define SIGNATURE_2 0x0A


#endif /* _AVR_IOM88_H_ */

See, I told you it was nasty! Don’t worry if you have no idea what is going here, it will all make sense in time.

Setting up AVR Studio

Getting up and running is fairly straightforward thanks to Atmel’s free IDE and the fantastic team behind WinAVR. To compile and play around with Gerald, you will need to download and install both AVR Studio and the WinAVR compiler toolchain. I’m going to assume that if you have read this far you are smart enough to work out how to install it.

Once you have installed it, run AVR Studio and you should be greeted with the Project Wizard as shown below:

wizard Click on “New Project” and select AVR GCC as the project type. Call your project “gerald”, save it somewhere convenient and make sure the “Create initial file” box is ticked.

wizard2 In the next window, we need to select the device we are compiling our code for. Choose “AVR Simulator” in the left box and scroll down to “ATmega88” in the box on the right. Be careful to select only the “ATmega88” and not any of the other varieties; you may find the code does not work as you expect otherwise.

wizard3 Once you click finish, you should find yourself with a blank window. This is our canvas! Grab the code for Gerald from above and paste it in, then click “Build and Run” in the Build menu. Simple, isn’t it?

Simulating code in AVR Studio

simulating_gerald After you run your code, you should see the AVR Studio window change up a bit. To the far left, you should see the processor toolbox; although it may not seem too useful at the moment, the numbers in the table let you see right into the bare silicon of the microcontroller so you can work out what is going on. We’ll come back to it later. You might also notice that just to the left of line 4 there is a yellow arrow; this is the line we are currently frozen on.

On the right of the window is the IO View; this toolbox will allow us to see what the code is actually doing. For now, we are only interested in PORTB, so click on the little plus next to PORTB so that your toolbox looks like this:

simulating_gerald2 Let’s run our code now: the F10 key will step over a single line of code, so hit it once. In the IO View, you should see the value of DDRB change over to 0xFF and all of this little white boxes should go black. This is indicating that all of the PORTB pins have been set to output mode. Hit F10 again to run line 6 and you should see PORTB change to 0xFF.

simulating_gerald3 AVR Studio lets you change the magic numbers (don’t worry, I’ll get onto these in a minute) by clicking on the black boxes. So if for some reason we wanted to change the simulation so that only PB0, PB2 and PB7 were high, you can just click on the boxes to change it like so:

simulating_gerald4 Congratulations, you just ran your first piece of AVR microcontroller firmware!

Understanding the “magic numbers”: a brief introduction to binary and hexadecimal numbers

When I was back in school, my teacher told us a story about how the French tried to introduce decimal time in the late 1700s. There were 10 hours in a day, each hour containing 100 minutes and each minute 100 seconds. On the surface, this seems like a pretty good idea right? Measuring time with a decimal scale would appear to solve a ton of date arithmetic problems overnight.

However, complications began to arise when they tried to get people to use it. Everything they used was already designed for 24 hour days, so converting back and forth between the two time measurements was very inefficient and cumbersome. Eventually, they returned to standard time because the tools used in the country were simply not designed for decimal time.

You might now be wondering what this has to do with programming. Well, it turns out that on the lowest level computers don’t use decimals either. Clearly they can still do mathematics and they definitely can give you the result in decimal, so what the hell is going on?

Computers actually use binary numbering. Information is passed between logic blocks by either putting a voltage on a wire (1), or not putting a voltage on a wire (0). 1 and 0 are the only two states that can ever exist (sort of, we won’t be going into it though) and we call these bits. Binary is known as a base 2 numbering system; that is, only the digits 0 and 1 are ever used. In contrast, the numbering system we normally use is base 10; we only ever use 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 and after that we start putting these digits together (10, 11, etc). Binary works the same way:

b0      = decimal 0  
b1      = decimal 1  
b10     = decimal 2 (see how we added another number to the left?)  
b11     = decimal 3  
b100    = decimal 4 (again, we ran out of digits so we added a number to the left)  
b101    = decimal 5  

One pattern you might have noticed is that each 1 represents 2^n (where n is the column number from the right) in base 10. If you didn’t, that’s perfectly fine also. It took me a very long time to understand it too!

Hexadecimal notation

Now writing out a bunch of 1s and 0s is pretty lame when what you want to do is code. So we often use a shorthand for binary called hexadecimal notation (hex = 6, decimal = 10, hexadecimal is base 16). Essentially, the hexadecimal numbering system counts from 0 through 9, but then moves on to the letters A through F. So to count to decimal 12 in hexadecimal, we use 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C. The super important thing you need to take away from this however, is that because of some funky math, each hexadecimal digit represents four binary bits. Have a look at this table [with hex denoted as 0x…]:

b0      = 0x0  
b1      = 0x1  
b10     = 0x2  
...  
b1001   = 0x9  
**b1010 = 0xA**  
b1011   = 0xB  
b1100   = 0xC  
b1101   = 0xD  
b1110   = 0xE  
**b1111 = 0xF**  
b10000  = 0x10  

Hopefully you can see what is going on here. It might be easier to not think of hex as a numbering system, rather a notation for quickly describing binary numbers.

Now, if you do a lot of programming you will eventually be able to work these out in your head. For now, if you open up your favourite calculator application and set it to programmers’ mode, you can click on bits and it will spit out the hexadecimal. Piece of cake.

A quick backtrack

Now that we know how hexadecimal and binary numbers work, we can revisit a few things. Remember how in Gerald we assigned some special registers to 0xFF? That was because the registers contained 8 bits and how many bits do we have in a 2 digit hexadecimal number? 4*2 = 8. In fact, this is likely why the IO pins are broken into blocks of 8 pins; so we can use an 8 bit number like 0xFF to set the highs and lows of 8 pins all at once. Now, let’s use this new knowledge in a new project!

(For all you Assembly programmers out there: yes, it’s actually because of opcode size limits. Leave me alone.)

Project 2: Hubert (aka blinky)

Using hexadecimal notation, we can now start to do some tricky things in our code. Let’s go:

hubert.c:

#define F_CPU	8000000

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
	DDRB = 0xFF;	// set PORTB to all outputs
	PORTB = 0xAA;	// PORTB = 1010 1010
	
	while (1) {
		_delay_ms(500);	// delay 500 ms
	
		if (PORTB == 0xAA) {
			PORTB = 0x55;	// PORTB = 0101 0101
		} else {
			PORTB = 0xAA;	// PORTB = 1010 1010
		}
	}
}

Since are using a delay function in this code, we need to define how fast our processor runs. Some compilers do this automatically, but I’m just setting it here on line 1 for reference purposes. 8000000 = 8MHz, the speed that the AVR will be running.

On lines 3 and 4 we include our header files. Nothing special happening here.

The code on lines 8 and 9 should seem fairly familiar, except this time we are only turning on 4 pins in PORTB instead of 8 like in Gerald. Our main loop (starting at line 11) simply delays for 500ms and then swaps 0xAA and 0x55 in and out of PORTB. Seem straight forward? Try simulating it yourself this time, I’m going to run it on some real hardware.

Running Hubert on the STK500

The STK500 is an AVR development board produced by Atmel. At AU$150 a pop it isn’t the cheapest development solution around, but it has almost all the features you could ever need. For this guide, I will be using my STK500 board to load code onto the AVR, you will have to look at the manual for your programming device to work out how to use it. If you don’t have an STK500 and don’t want to burn a lot of money, Sparkfun sells a bunch of cheaper boards or you can try their Australian distributor Little Bird Electronics.

Given that all we will be using in this project is LEDs, I am going to run the code on the STK500 itself. One thing to keep in mind is that on the STK500, the LEDs are active low; that is, when you send them a 0 they switch on and a 1 turns them off. I’m going to be pretending that this isn’t the case though.

We ain’t done yet, I’ll get back to this! (need to borrow a decent camera)

Logical operations: the basis of computers (mathematics warning!)

We’ve all heard it somewhere before: computers are dumb, computers don’t understand anything. This is true to some extent; a computer is ultimately a glorified lookup table. Remember when you learned your times tables? I bet you used a long lookup table:

1 * 1   = 1
1 * 2   = 2
...
9 * 9   = 81

A computer doesn’t understand any relationship between the numbers, but it still knows that 6 * 7 = 42. However, a computer is supposed to be a general tool and not just a multiplication tool so we can’t just put a hard coded table in the silicon. How do we make a “general” lookup table (ie a computer) you ask? We use these strange mathsy things called logical operations, and they are very important in, among other things, embedded development. Let’s take a look at them in a bit more detail.

What are logical operations?

Logical operators work in a similar way to the mathematical operators you are probably more accustomed to. For example, the addition operator, + (also known as “plus”) indicates that you should combine the closest number on either side of the operator using the process of addition.

A + B means "use addition to combine A and B"
2 + 3 [= 5] where A=2 and B=3

Now you may not realise it, but many, many mathematical operators exist outside those you learned in school (unless you attended a school for mathematical geniuses). Some of them are quite simple (like the logical operators we are about to go through) and some are exceedingly complex (like the vector operators curl and divergence) and do strange things like only operate on a single number.

While A and B can be of any base in our normal world (remember bases?), in the world of logical operators they have to be in base 2, which is binary. If you want to perform a logical operation in your head, you need to convert both numbers to binary first. No exceptions. Doing it in your code is easier; the compiler automatically handles it for you.

There is one last trick we need to cover: what happens if you want to add three numbers, A, B and C? Hopefully you already know; you can split the operation into two parts:

A + B + C is the same as
(A + B) + C

So in this case, if we define D=(A + B), then we can use our two number operator again to give the same result:

(A + B) + C is the same as
D + C

Recapping using actual numbers (A=5, B=2, C=3)

5 + 2 + 3       [= 10]
(5 + 2) + 3     [= 10]
7 + 3           [= 10] using D=A+B=7

It is very important to remember that [for any operator used in this guide] no matter how many operators and in what order, it is always possible to break down an operation into paired numbers.

Take a breather now if you need it, we’ve got some operators to learn.

Logical AND

The first operation we will look at is the AND operation (notice the capitals? that is the convention for writing about logical operations). The symbol we use for it in programming is usually &, like so:

A & B

AND is a very simple operator. To start with, it is probably easiest to describe how it works in words: when A and B are 1 digit binary numbers; if A and B are both 1, the result is 1. Otherwise the result is 0.

But us engineers and computer nerds don’t do words, we use a thing called a truth table that shows all the possible outcomes, like so:

A & B = X

A   B  |  X
-----------
0   0  |  0
0   1  |  0
1   0  |  0
1   1  |  1

If you try and read through the rows of this table in words, the operation should start to make a little more sense. You can actually apply the AND operator to a binary number of any size, you just have to treat each bit column as its own AND operation. An example would probably be easier to understand:

  1010 1110 (I like to split numbers every 4 bits to make them easier to line up with their hex representation)
& 1000 1010
-----------
  1000 1010

Logical OR

Now that we have the semantics out of the way, it should be much easier to go through these other operators. The OR operation (using the symbol |, the one above your enter key) can be described as follows: when A and B are 1 digit binary numbers; if A or B are 1, the result is 1. Otherwise, the result is 0.

Let’s take a look at the truth table for the OR operation:

A | B = X

A   B  |  X
-----------
0   0  |  0
0   1  |  1
1   0  |  1
1   1  |  1

And the same goes for applying it to multi-bit numbers, just pair off the columns into their own operations:

  1010 1110
| 1000 1010
-----------
  1010 1110

It’s starting to make sense, isn’t it!

Logical XOR

Logical XOR (symbol ^ for programming) is a strange one. While AND and OR are words in the english language already, XOR stands for eXclusive OR. To describe it in words: when A and B are 1 digit binary numbers; if A or B are 1, but not both, the result is 1. Otherwise the result is zero.

The XOR truth table will probably be easier to understand:

A ^ B = X

A   B  |  X
-----------
0   0  |  0
0   1  |  1
1   0  |  1
1   1  |  0

The “exclusive” part refers to the fact that the two numbers cannot be the same (see mutual exclusivity). Again, performing a XOR operation on larger numbers follows the same pattern as the previous two operations: group and go!

  1010 1110
^ 1000 1010
-----------
  0010 0100

Logical NOT (a bit of an exception)

Remember when I said that operators need two numbers? I lied. Sorry. The NOT operator (denoted by ~ in the C programming language) simply flips all of the bits in a number to their opposite; that is, all 0s become 1s and all 1s become 0s:

~(1010 1110) turns into
  0101 0001

As in natural language, double negatives work cancel out (NOT, NOT xyz):

~(~(1010 1110)) = 1010 1110

NOR and NAND

NOR and NAND are examples of other logical operators that we won’t really be using. In simple terms, they are the the same as their logical operator namesake with a NOT around the outside. While they are very important in logic design (they can be implemented easily with transistors), they aren’t used in computer programs as single operations, only as chains of two or more operations. Despite this, I’m going to include their truth tables here for reference purposes. And for funsies.

NOR (Not OR):

~(A | B) = X

A   B  |  X
-----------
0   0  |  1
0   1  |  0
1   0  |  0
1   1  |  0

NAND (Not AND):

~(A & B) = X

A   B  |  X
-----------
0   0  |  1
0   1  |  1
1   0  |  1
1   1  |  0

A more efficient Hubert using the XOR operation

Often, programs can be written much more efficiently using binary logic tricks. The swapping code in Hubert can be rewritten in a single line using the XOR operation, and I thought I would put it here as a demonstration of how there are heaps tricks you can use to make your code more efficient.

hubert_sneaky.c:

#define F_CPU	8000000

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
	DDRB = 0xFF;	// set PORTB to all outputs
	PORTB = 0xAA;	// PORTB = 1010 1010
	
	while (1) {
		_delay_ms(500);	// delay 500 ms
	
        PORTB ^= 0xFF;	// XOR bit flip
	}
}

Let’s look at how it works: say PORTB = 0xAA and we perform the operation PORTB ^ 0xFF (the ^= just means PORTB = PORTB ^ 0xFF),

  1010 1010
^ 1111 1111
-----------
  0101 0101 [= 0x55]

It just flipped all of the bits! Now when the loop runs again, PORTB will be 0x55 and that will be XORed with 0xFF:

  0101 0101
^ 1111 1111
-----------
  1010 1010 [= 0xAA]

Now, I definitely don’t expect you to be able to pull out crazy tricks like this yet. Just try to remember this one, it’ll help you squeeze out those extra clock cycles when you really need them!

Project 3: Audrey (aka the repeater)

Whew. While that may have seemed like a whole lot theory with no obvious practical applications, I promise you it was very important. In fact, a great deal of Audrey is based on logical operators. Keep in mind this will be our first big project; it uses a whole lot of stuff we have learned up until now and a fair bit of new stuff too. To top it off, I will be building the entire circuit on a breadboard so you can see how it’s done. Put on your thinking cap and let’s go:

audrey.c:

#include <avr/io.h>

int main(void)
{
	DDRB = 0x0F;	// higher pins to input and low pins to output
	
	char in;		// a temporary variable
	while (1) {
		in = PINB;	// read the state of the pins on PORTB
					// the PINx register is specially made for input
		
		in &= 0xF0; // mask out the lower bits
					// the same as "in = in & 0xF0"
		
		in >>= 4;	// bit shift right 4 places
					// same as "in = (in >> 4)"
		
		PORTB = in; // set the pins we read as output
	}	
}

Remember how each IO port had 8 pins? The purpose of Audrey is to read the higher 4 pins (in this case, PB4-PB7) and output their state to the lower 4 pins (PB0-PB3) of PORTB. Line 5 of Audrey sets this relationship up: by setting the 4 upper bits of DDRB to 0 and the 4 lower bits to 1, we tell the AVR to set up the internal circuitry to use the upper 4 as inputs and the lower 4 as outputs. Line 7 defines a temporary variable for us to use in our program. We are using a char-type variable because it holds exactly 8 bits.

Line 9 is where the new stuff comes in. In the same way that PORTB can be used to output across several pins, PINB is used to read in the state of the pins in the port. In the case of line 9, the states of all of the pins on PORTB are stored in the variable “in”.

You should see something familiar on line 10: the AND operator! As to why we are using it: in this program, we don’t really care about the state of the lower 4 bits of PORTB. After all, they are set up as outputs in the data direction register, so their input state must be undefined! We use the AND operator to set the lower 4 bits to 0 so they don’t mess with our program. How does it work? Take a look:

Let PORTB = 0110 1010 [= 0x6A]

  0110 1010
& 1111 0000
-----------
  0110 0000

Or how about:

Let PORTB = 0101 1011 [= 0x5B]

  0101 1011
& 1111 0000
-----------
  0101 0000

Hopefully you can see that because of the way AND works, it doesn’t matter what the lower 4 bits are set to; they will be switched over to 0s after the AND operation. Programmers call this operation bitmasking, where 0xF0 is the bitmask or sometimes just mask.

Line 15 introduces an operator we haven’t come across yet. The >> and << operators are known as the “bitwise shift” or “bit shift” operators. They take a binary number and shift its bits through the columns in the direction of the arrow. So as an example,

(0xAA >> 1) goes from
1010 1010 to
0101 0101 after the right bitwise shift

Alternatively, we can use the left bitwise shift to go the other way:

(0xAA << 1) goes from
1010 1010 to
0101 0100 after the left bitwise shift

Notice that in C, the bitwise shift operators always shift in 0s and never 1s. This is an important distinction between C and Assembly because in assembly certain conditions on specific instructions can cause 1s to be shifted in. But let’s not worry about that.

Back to line 15, why are we shifting? We need to move the four bits right so that when we output the temporary variable, the bits we read in will be in the correct columns.

Finally, line 18 should be fairly clear; it sets PORTB to the value of our temporary variable “in”.

Optimising Audrey

Once you get a bit more experience with embedded development, you’ll probably start coding like this:

audrey-all-in-one.c:

#include <avr/io.h>

int main(void)
{
	DDRB = 0x0F;	// higher pins to input and low pins to output
	
	while (1) {
		// they call me captain tricky
		PORTB = ((PINB & 0xF0) >> 4);
	}	
}

Now while this code probably makes sense and will work just fine, keep in mind that using too many bit/logical operators in one line can get pretty confusing, especially when you are debugging at 3AM in the morning. You’ve been warned!

And just for fun, we can use another trick to simplify Audrey even further. In C, the bit shift operator always shifts in 0s so we can leave out the bitmasking altogether:

audrey-all-in-one-better.c:

#include <avr/io.h>

int main(void)
{
	DDRB = 0x0F;
	
	while (1) {
		// no, they seriously do
		PORTB = (PINB >> 4);
	}	
}

I guess now it’s time to dig out the breadboard.

Building Audrey on a breadboard

never fear, this part is coming soon!

Project 4: Cuthbert (aka the interruptor)

All of the code we have written so far has been pretty straightforward. If you sat down with a piece of paper you could probably follow the programs on paper and accurately determine their outcomes. The inherent linearity in these programs presents a problem for real world systems though: it’s slow and non-responsive. Imagine if we wanted to do some complex maths in the main loop of our code, but when a button was pressed we wanted to switch an LED on. You might use some code like this:

cuthbert-no-interrupts.c:

#include <avr/io.h>
#include <util/delay.h>

void do_complex_maths()
{
	_delay_ms(5000); // pretend this is doing something super complicated	
}

int main(void)
{
	DDRD = 0x01;	// make only PD0 output
	
	while (1) {
		if (PIND & 0xFB) { // if PD2 is not 0
			PORTD = 0x01; // set PD0 to 1
		} else {
			PORTD = 0x00; // switch off PD0
		}
		
		do_complex_maths(); // this takes ages!
	}
}

If you run this code on a real hardware, you will notice the problem quickly. The LED that we want to switch on and off will only be updated once every 5 seconds because of the delay caused by the calculations. Given you can press the button a bunch of times in 5 seconds, this ruins the responsiveness of our device.

To solve this problem, most microcontrollers (and microprocessors) have special mechanisms that instruct the CPU to drop everything and run a certain block of code immediately. These are known as interrupts. AVRs have a ton of useful interrupts (see page 55 in the datasheet) but the one we will be using in Cuthbert is the External Interrupt Request 0. This interrupt (described on page 65 of the datasheet) triggers when the INT0 pin (actually PD2) changes to a condition you specify. It might be easier to just go through the code, so here is an interrupt based Cuthbert:

cuthbert.c:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>

void do_complex_maths()
{
	_delay_ms(5000); // pretend this is doing something super complicated	
}

int main(void)
{
	DDRD = 0x01;	// make only PD0 output
	
	EICRA = (1 << ISC11) | (1 << ISC10); // interrupt on rising edge
	EIMSK = (1 << INT0); // enable INT0
	sei(); 			// enable the interrupt system
	
	while (1) {
		do_complex_maths(); // this takes ages!
	}
}

ISR(INT0_vect) // interrupt routine
{
	if (PIND & 0xFB) { // if PD2 is not 0
		PORTD = 0x01; // set PD0 to 1
	} else {
		PORTD = 0x00; // switch off PD0
	}
	// could also be written as
	// PORTD = ((PIND & 0xFB) >> 2);
}

A note of thanks

Wow, you actually read this until the end? I’m impressed! You should be well on your way to being a microcontroller megastar! In all seriousness though, there are a few people that deserve thanks after all this. First I would like to thank plex who, if you remember my opening spiel, answered pretty much every arcane question I could come up with about microcontrollers. He introduced me to real engineering as a discipline and I am forever grateful for that. I’d also like to thank my [much smarter] EE mates at uni who keep pushing my knowledge to its limits; keep the crazy ideas coming guys!

And it would be silly to miss this ultimate cliché opportunity: I would like to thank you, the reader, for coming this far. I hope you had as much fun reading this as I had writing it.