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.