Table of Contents
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.
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.
Now, every register in every microcontroller is nothing but a location such as 0x04. 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.
Dealing with Registers
For understanding how to deal with registers, we will use the DDR register. I mentioned that DDR register is used to set a particular pin as input or output. What we need to understand here is that this register DDRx is specific only to the AVR family. Other families may have different names for similar registers. However, for this post, we are not concerned with that. We are only using DDR as an example to understand how to write and clear the registers.
This knowledge will be useful to deal with any register in any microcontroller. There are a few terms here that we need to understand:
- Setting a bit – It means writing 1 to that particular bit location
- Clearing a bit – Writing a zero to that particular bit location
- Resetting a bit – Clearing the bit
Operations on Registers
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).
For Example: Consider the following diagram represents the DDRB register. Writing a value “1” to any of the 8 bits will make that particular pin as output. Conversely, writing a value “0” to any of the 8 bits will make that particular pin as input.
Thus, if want to set pin 5 of Port B as output, we will write a value “1” to the 5th bit of DDRB. The code for this will be
DDRB = 0b00100000; and the register looks like this:
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;
the pins 7,5,3,1 are configured as output while the remaining pins are configured as inputs. And now the register looks as follows:
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.
Set a bit with Bitwise OR
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.
Clear a bit with Bitwise AND
What about going the other way – setting a bit to 0 to make the pin an input? Or maybe for any other register, we just want to clear the bit, i.e., set it to zero.
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.
In case you are not comfortable with or do not understand the bitwise operations, The following videos will help you a lot:
Functions and Macros to set and clear bits in registers:
Maybe all this business with shifting bits around looks clumsy to you. Personally, I think it’s usefully explicit – I see ‘1 << PINB4’ and meaning “move a 1 into the PINB4 position”, so the code is explicit and clear. And when you start getting into other registers, such as those controlling the SPI and I2C (two wire) interfaces, the use of macro labels with these operations helps clarify what the program is doing.
However, some people like to abstract away some of this stuff inside macros and functions. In the Atmel libraries, the avr/sfr_defs.h file (which is automatically included from avr/io.h which in turn you tend to include in pretty much all your programs) includes some handy macros for setting ‘special function registers’ (hence the ‘sfr’ abbreviation in the following examples). These include:
bit_is_set(sfr, bit) bit_is_clear(sfr, bit) loop_until_bit_is_set(sfr, bit) loop_until_bit_is_clear(sfr, bit)
So to use them you just pass the register and the bit for the relevant pin. The first two return a ‘non-zero’ result if the condition you’re testing for is true and 0 if false. The other two are fairly self-explanatory.
You may also want to create your own functions. However, there is a catch. The macro labels for the registers are often not the addresses of the registers themselves but pointers to those addresses. For example, you might be tempted to write a function something like this:
void setBit (uint8_t register, uint8_t pin) { register |= (1 << pin); // *** THIS DOESN'T WORK } setBit(PORTB, PB3);
That’s not going to work. Instead you need to use a pointer in the function and pass the register by reference. You also need to use ‘volatile’ to prevent a compiler error.
void setBit (volatile uint8_t * register, uint8_t pin) { *register |= (1 << pin); } setBit(&PORTB, PB3);
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.
Hi, I’m Vivek, a Senior Embedded Innovation Specialist. I have been working on Embedded Systems and IoT for the past 11 years. I love to share my knowledge and train those who are interested. Nerdyelectronics.com was started out of this interest. You can read my full profile in this link.