Addresses in Detail
One question that’s come up is this: what do
PORTB and
PB5 actually mean? How does the compiler work its magic for these? In this article, we’’ll walk through writing our blinky light example without using
avr/io.h. The example code for this will be in the
04_addresses directory in the example code.
PORTB and DDRB are two of the hardware registers on the AVR (which you’ll find by reading the description of the GPIO pins), so the first step is to look up the register summary in the datasheet; you’ll find it on page 423 in section 30, “Register Summary.” There will be a column of addresses; if you find PORTB, you’ll see it given as
0x05 (0x25). Note 4 discusses this, but it comes down to the assembly instructions used. For now, understand that our C will be using the
LD and
ST instructions, so we’ll use the address in parenthesis.
Both
PORTB and
DDRB take up 1 byte (one row in the summary); 16-bit locations would have a high and low byte. This means
PORTB and
DDRB should be defined as unsigned 8-bit integers. For 16-bit addresses, there are special considerations as memory is operated on a byte at a time; this isn’t something we’ll worry about yet.
Let’s write our own
io.h. We’ll be able to mostly drop in the LED blinking example from the first article. The UART code won’t be in there (it’s doable, but a lot of extra work that doesn’t add any real benefit) and instead of using
_BV, our
io.h will have a
BV macro.
The first thing we’ll do is write a convenience macro for using a number as an 8-bit address:
/* * The argument should be the address of an 8-bit register. A description * and listing of memory locations is given in section 30 of the ATmega328P * data sheet. */ #define REGISTER8_ADDR(addr) (*((volatile uint8_t *)(addr)))
What does this actually do? Well,
volatile means that the contents of the variable can be changed outside of this program; it’s shared memory. The compiler won’t try to optimise out this variable. In the case of our registers, the values that will be in this address come directly from the AVR’s registers. It’s an 8-bit address, hence
uint8_t *. The cast to
volatile uint8_t * is how we tell the compiler that this is the address to a hardware register. In order to actually use the values in the register, it needs to be dereferenced; this is really what we’re going to do every time we use a register, so it makes sense to just include it in the macro.
That’s really the magic bit. The rest of our header file is filling in values:
/* * Page 426 gives the memory locations for PORTB and DDRB. */ #define PORTB_ADDR 0x25 #define DDRB_ADDR 0x24 /* * Now we apply the memory addresses. */ #define PORTB REGISTER8_ADDR(PORTB_ADDR) #define DDRB REGISTER8_ADDR(DDRB_ADDR) /* * We define the bits in PORTB. */ #define PB0 0 #define PB1 1 #define PB2 2 #define PB3 3 #define PB4 4 #define PB5 5 #define PB6 6 #define PB7 7
For completeness, we’ll add in a bit-value macro:
/* * This is a convenience macro for getting a bit value. */ #define BV(x) (1 << x)
Our
io.h will only work with port B to limit the tediousness of this example. We can drop in a program to use this for addressing instead of the
avr/io.h header—but without the UART and timer parts, and using the delay utility header.
#include "io.h" #include <util/delay.h> #define LED_DDR DDRB #define LED_PORT PORTB #define LED_PIN PB5 int main(void) { /* Set the LED pin as an output pin. */ LED_DDR |= BV(LED_PIN); while (1) { /* Toggle the LED. */ LED_PORT ^= BV(LED_PIN); /* Sleep for a half-second. */ _delay_ms(500); } /* * This should never be reached, but C requires that * main possibly returns *something*. */ return 0; }
In this
main, I changed the blinking speed to prove it’s running the new code.
Fortunately, the
avr/io.h header does all this for us, setting the right addresses for the right microcontroller and without us having to worry about changes between the various microcontrollers.