AVR basics: ports and direction registers

Pretty much the first thing anyone does with a microcontroller, whether it’s a naked AVR chip or something fancy like an Arduino, is flash an LED.

This series is part of my learning process and I hope it will help others who, like me, are embarking on projects such as programming AVR chips. The way I learn things is to write about them as I go. Feel free to correct my mistakes. FYI, I program for AVR ATMEGAs using AtmelStudio 7 on Windows 10. I use the Atmel-ICE programmer to get the code on to the chip. If your toolchain is different you may need to take that into account. The macros mentioned here come from the standard Atmel libraries.

Switching a GPIO pin high or low is a fundamental skill in microcontroller projects and it’s made trivially easy in the Arduino environment thanks to much of the work being carried out by built-in software libraries. When you’re working directly with AVR chips you have to put in a little more effort – but not much. And the additional steps make you think a little more about what’s going on and help you to understand the architecture a little better.

Registers are a critical feature of any computer processor and are the secret to doing anything with a microcontroller. Put crudely, you make things happen by writing a value to a register. You find out what’s happening (eg, get input) by reading the values in registers. And a register is essentially just a location within the microcontroller’s address space.

To make life easier, we give names to these registers, as we’ve seen before.

For example, the DDRB register on the ATMEGA 328, which controls whether certain GPIO pins are inputs or outputs, is a single byte at location 0x04. To set the pins as inputs or outputs, you simply write a value between 0 and 255 into that location. It would be a pain to have to remember (or constantly look up) that number, so the standard Atmel libraries define the macro ‘DDRB’ that we can use wherever we want to make a reference to that register. (To be strict, some of the macros contain pointers to memory addresses – we’ll see in a future post why that matters, but here it doesn’t.) So whenever you see things like ‘PINB7’ or ‘PORTD’ just be aware that there is nothing magical about these labels – they are just handy labels for memory locations. With that in mind, let’s get setting those pins.

Ports and pins

Each AVR chip has a number of ‘ports’. The Atmega 328 has three – ‘B’, ‘C’ and ‘D’. (Want to know what happened to ‘A’? Don’t ask – it’s never talked about…). Again, there’s nothing very mysterious about ports – they are just a way of grouping together GPIO pins. Remember that we’re dealing with an eight-bit architecture here. And in a moment we’ll see that setting or reading pins requires writing to or reading from a register. As a register is one byte long, then the maximum number of pins it can handle is eight. So the pins are split up into groups of (at most) eight.

For each port there are three important registers:

  • The Data Direction Register (DDRx) determines whether the pins operate as inputs or outputs.
  • The port output register (PORTx) determines the actual value set on each pin when it’s being used as an output.
  • The port input register (PINx) is used for reading input values.

The ‘x’ in the abbreviations above is just my placeholder and varies according to which port we’re discussing. So let’s talk about Port B to show how this works. The Data Direction Register for Port B is labelled DDRB.

Data direction – In or Out

It’s best to start thinking in binary when it comes to setting or reading values for registers because each bit within the byte at address DDRB represents a separate pin. To set a pin as an input you write a 0 to the relevant bit. To set it as an output you write a 1. So if you want all eight pins on Port B to be inputs you’d simply write 0 to DDRB (in binary, 0b00000000). If you want them all to be outputs, you’d write 255 (in binary, 0b11111111).

Mixing inputs and outputs involves using intermediate values and you can quickly see why it’s better to work in binary. For example, let’s say you want the pins to be alternatively inputs and outputs, starting with pin 0 being an input, pin 1 an output and so on. The value you need to write to DDRB would be 170. Not obvious, is it? It’s no clearer in hex: 0xAA. But in binary you can easily see what’s happening: 0b10101010.

So to set the direction of the pins in C code you can simply write:

DDRB = 0b10101010;

That’s fine if you want to set all the input/output configurations for all eight pins in one go. But that’s not always the case. In fact, it’s arguably more common to want to do one pin at a time.

Let’s say you want to set pin 2 as an output. One way to do this is:

DDRB = 0b00000100;

That’s fairly clear, but it has a downside. At the same time as setting pin 2 as an output it also sets all the other pins as inputs – which may not be the effect you’re after! Generally, it’s good practice to focus purely on the pin in question and not risk side effects.

Luckily, there’s a good way to do this. We use a logical OR. We take the existing state of DDRB and OR it with the new value. You could write this as:

DDRB = DDRB | 0b00000100;

Those zeroes mean that the other pins will be unaffected and will stay in whatever state they’re currently in. Pin 2 will be sure to be set as an output (regardless of its current state). A slightly more concise way of writing this is:

DDRB |= 0b00000100;

But there’s actually an even better way. As it stands, it’s fairly clear what’s going on here. But you still need to count the zeroes from the right-hand side to see which pin is being affected. And as it’s in the third position, can you remember whether that’s pin 2 or pin 3? (They actually count from 0.)

The AVR libraries define some handy macros for us. Instead of using numbers directly, we can use the label DDB2 to set pin 2 on DDRB. If you look at the definitions, you find that DDB2 is actually just the value 2 and using that in the line of code above wouldn’t work. This is where things start shifting. The way you’d actually use this is:

DDRB |= (1 << DDB2);

Now this probably looks more complex, not less. (Ignore the parentheses for a moment.) In fact, once you get used to it you’ll find it’s actually very natural and flexible. What we’re saying here is take the value 1 (0b00000001) and shift it left ‘DDB2’ times. As DDB2 is just 2, then we shift left two times, resulting in the 0b00000100 we need.

Why do it this way? Well the ‘1 <<‘ part is always the same and you just get used to seeing it. You can understand it as meaning something like, “move a 1 into this position”. In this case, you’re moving a 1 into the position required to set DDB2 (ie, pin 2). Seen in that light it’s clear and practically self-documenting.

And it’s very flexible. Let’s say we want to set pins 2 and 5 as outputs. All you need do is OR them together with the DDRB:

DDRB |= (1 << DDB2 | 1 << DDB5);

Now you see why we put the parentheses in the first example – it makes it clearer what’s going on.

What about going the other way – setting a bit to 0 to make the pin an input? We can use a similar approach. If we want to set pin 2 to an input, without affecting any of the other pins, we use:

DDRB &= ~(1 << DDB2);

First we do the same as before, shifting a 1 into the position required to affect pin 2. This results in the familiar value of 0b00000100.

Then we use the bitwise NOT operator, represented by the tilde (~). This ‘flips’ all the bits, so we get: 0b11111011.

Finally we AND this with the Data Direction Register (rather than ORing). As the bit representing the pin we’re trying to set is now a 0, this will guarantee a 0 following the AND operation, regardless of the original value. As all the other bits are 1s, then any bit that was already a 1 in the register will remain a 1. Any bit that was a 0 will remain a 0.

So this shows how you set up a port for input and output operations. In the next post we’ll look at how you actually set or read the GPIO pins.

Leave a Reply

Your email address will not be published. Required fields are marked *