Skip to main content

Introduction to I/O Registers

This tutorial will teach you how to use the I/O ports on an AVR microcontroller. I will be using an Atmega8 but the general principles apply to any AVR microcontroller.

Introduction

The Atmega8 has 23 I/O ports which are organised into 3 groups:

  • Port B (PB0 to PB7)
  • Port C (PC0 to PC6)
  • Port D (PD0 to PD7)

These are shown on the pinout diagram below.

Pinout Diagram

All of the I/O pins have secondary functions. These are shown in parenthesis on the pinout diagram.

PC6 is almost always used as a reset pin and is not normally available for I/O. PB6 and PB7 are often used for external crystal oscillators but not in this tutorial.

Port Registers

The following Registers are used for reading and writing to the I/O ports.

Register Type Description Notes
DDRB Read/Write Port B Data Direction Register 1=output, 0=input
PORTB Read/Write Port B Data Register
PINB Read only Port B Input Register
DDRC Read/Write Port C Data Direction Register 1=output, 0=input
PORTC Read/Write Port C Data Register
PINC Read only Port C Input Register
DDRD Read/Write Port D Data Direction Register 1=output, 0=input
PORTD Read/Write Port D Data Register
PIND Read only Port D Input Register

Each of these registers are 8 bits wide, with each bit (with the exception bit 7 of the Port C registers) corresponding to a single pin.

For the code examples I will be using binary literals. Consider the following code block.

1
2
3
PORTD = 0b11110001;
PORTD = 0xF1;
PORTD = 241;

Each line is doing the same thing. In each case a literal value is being assigned to PORTD. In the first case the value is being expressed in binary, in the second it is being expressed as Hexadecimal and in the last case it is being expressed in decimal.

The rightmost bit (least significant bit) represents pin 0 of port D (PD0) whilst the leftmost bit (most significant bit) represents pin 7.

Output

Let’s start by building a circuit with 8 LEDs connected to the Port D pins.

Circuit Diagram
Circuit on Breadboard

To use these pins as outputs, we need to set the Data Direction Register pin values to 1. A value of 1 is used for output whilst 0 is used for input.

DDRD  = 0b11111111;

Next we set the value of PORTD. Each bit is either a 1 or a 0 depending on whether we want the output to be on or off. i.e. 0 is off and 1 is on. so

PORTD = 0b00110001;

Will produce the following pattern in our LEDs

LEDs

_BV and bitwise operators

When programming AVR devices, you often need to address single pins. The _BV macro and bitwise operators come in handy here.

_BV is a macro that takes a value between 0 and 7 and returns an 8 bit value with a “1” in the position nominate by the input parameter. So

  • _BV(0) gives you 0b00000001
  • _BV(3) gives you 0b00001000

_BV(i) is equivalent to 1 << i

The C programming language includes a set of bitwise operators which apply logic operations to individual bits. These can be combined with _BV to control individual pins. Consider the following code example.

PORTD = PORTD | _BV(3);   // Turn PD3 on
PORTD |= _BV(3);          // Same as previous statement, but using compound assignment operator instead
PORTD &= !_BV(3)          // Turn PD3 off

To learn more about bitwise operators, please refer to http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Bitwise_operators.

3 different ways to create a sweep pattern

Using what we have learned so far, consider 3 different implementations of a sweep pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void sweep1()
{
    PORTD = 0b10000000;
    _delay_ms(100);
    PORTD = 0b01000000;
    _delay_ms(100);
    PORTD = 0b00100000;
    _delay_ms(100);
    PORTD = 0b00010000;
    _delay_ms(100);
    PORTD = 0b00001000;
    _delay_ms(100);
    PORTD = 0b00000100;
    _delay_ms(100);
    PORTD = 0b00000010;
    _delay_ms(100);
    PORTD = 0b00000001;
    _delay_ms(100);
    PORTD = 0b00000000;
}
 
void sweep2()
{
    for (int i=7;i>=0;i--)
    {
        PORTD = _BV(i);
        _delay_ms(100);
    }
    PORTD = 0b00000000;
}
 
void sweep3()
{
    PORTD = 0b10000000;
    for (int i=0;i<8;i++)
    {
        _delay_ms(100);
        PORTD >>= 1;
    }
}

Input

For the next section we will add 2 buttons to PC0 and PC1 as shown in the schematic and photo below.

Circuit Diagram
Circuit on Breadboard

To use these button you need to set the Data Direction Register values to 0 and the Data Register values to 1.

1
2
DDRC  = 0b11111100;   // set PC0 & PC1 to input
PORTC = 0b00000011;   // set PC0 & PC1 to high

By setting the PORTC values to 1, we are telling the microcontroller that this are held high. You will notice on the schematic above that when the buttons are pressed these pins go to ground. An alternative approach would have been to hold the lines low by setting the PORTC values to 0 and connecting the buttons to VCC.

To read the input values we look at the Input Register. In this example we look at PINC. A value of 1 for a given bit means the switch is open (not pressed) and a value of 0 means that it is being press.

We are now ready for some code.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <avr/io.h>
 
int main (void)
{
    DDRD  = 0b11111111;   // All outputs
    DDRC  = 0b11111100;   // set PC0 & PC1 to input
    PORTC = 0b00000011;   // set PC0 & PC1 to high
 
    while(1)
    {
        PORTD = PINC;   
    }
}

In this example the LEDs will show you the state of the PINC input register. The 2 rightmost LEDs should be on, and will go off when each button is pressed.

Addressing Individual Pins

Using bitwise operators we can test if an individual button is pressed

if ((PINC & _BV(0))==0) //if button on PC0 is pressed
{
    blink_3_times();
}
if ((PINC & _BV(1))==0) //if button on PC1 is pressed
{
    sweep();
}

This is a bit convoluted. An easier and more readable way is to use the bit_is_set or bit_is_clear macros. Refer to http://www.nongnu.org/avr-libc/user-manual/group__avr__sfr.html for documentation for these macros.

1
2
3
4
5
6
7
8
if (bit_is_clear(PINC,0)) //if button on PC0 is pressed
{
    blink_3_times();
}
if (bit_is_clear(PINC,1)) //if button on PC1 is pressed
{
    sweep();
}

The final program

Using what we have learned up to this point, we can now write the following program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <avr/io.h>
#include <util/delay.h>
void blink_3_times(void)
{
    for (int i=0;i<3;i++)
    {   
        PORTD = 0b11111111;
        _delay_ms(250);
        PORTD = 0b00000000;
        _delay_ms(250);
    }
}
 
void sweep()
{
    PORTD = 0b10000000;
    for (int i=0;i<8;i++)
    {
        _delay_ms(100);
        PORTD >>= 1;
    }
}
 
int main (void)
{
    DDRD  = 0b11111111;   // All outputs
    DDRC  = 0b11111100;   // set PC0 & PC1 to input
    PORTC = 0b00000011;   // set PC0 & PC1 to high
 
    while(1)
    {
        if (bit_is_clear(PINC,0)) //if button on PC0 is pressed
        {
            blink_3_times();
        }
 
        if (bit_is_clear(PINC,1)) //if button on PC1 is pressed
        {
            sweep();
        }
    }
}

Related News

Reading and writing Atmega168 EEPROM

EEPROM (Electrically Erasable Programmable Read Only Memory) Is non-volatile memory, meaning it persists after power...

Breadboards – 101

Breadboards are invaluable for experimenting with electronic circuits. They allow you to create temporary circuits...

Severed hand in a jar Halloween display

For this post I thought I'd try something a little different. I've created 3 videos...

2 Comments

  1. Zach Alterman

    for the first code example you have – in the 3rd sweep in line 38 – did you mean PORTD >> i , as opposed to PORTD >> 1
    ?

  2. protostack

    Firstly the operator being used is >>= This is a combination of assignment and right shift. so

    PORTD >>= 1 is equivalent to
    PORTD = PORTD >> 1

    each time me iterate through the loop, we want to move the 1, 1 position to the right. So we start with:

    10000000

    then have

    01000000
    00100000
    00010000
    00001000
    00000100
    00000010
    00000001

    If we right shifted by i we would have

    10000000 initial value
    10000000 i=0
    01000000 i=1
    00010000 i=2
    00000010 i=3

    and so on…

Leave a reply

Shopping Cart