Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Working with GPIOs on AVR Atmega-328: A Hands-On Guide

February 11, 2024

Working with GPIOs on AVR Atmega-328: A Hands-On Guide

The AVR Atmega-328 microcontroller is widely used in many embedded systems and Arduino boards. One crucial aspect of working with microcontrollers is controlling the General Purpose Input/Output (GPIO) pins. These pins provide the means to interact with the external world by either reading input signals or driving output signals. In this hands-on guide, we will dive into the basics of working with GPIOs on the AVR Atmega-328, complete with extensive examples and explanations.

Prerequisites

To follow along with this guide, you will need an AVR Atmega-328 microcontroller-based development board such as an Arduino Uno, a solid understanding of C or C++ programming, and familiarity with embedded systems.

Introduction to GPIOs

GPIO pins are the most fundamental way to communicate with a microcontroller. The AVR Atmega-328 microcontroller, like many others, has several GPIO pins that can be configured as either inputs or outputs.

GPIO as Input

When configured as inputs, the GPIO pins can receive signals from external sources, such as sensors or switches. The microcontroller can then read the state of these pins to react accordingly.

GPIO as Output

When configured as outputs, the GPIO pins can drive signals to control other devices like LEDs, motors, or relays. By manipulating the output voltage, we can control the state of these external components.

Getting Started with AVR Atmega-328 GPIOs

To begin working with GPIOs on the AVR Atmega-328, we need to understand a few important concepts, including pin numbering, I/O port registers, and pin control.

Pin Numbering

The AVR Atmega-328 has a total of 23 GPIO pins, numbered from 0 to 22. Pin 0 to Pin 7 are located on Port D, Pin 8 to Pin 13 are on Port B, and Pin 14 to Pin 19 are on Port C.

I/O Port Registers

To interact with GPIO pins, we access the I/O port registers, which are memory-mapped registers. These registers allow us to set the direction of the pins (input or output), read their current state, and modify their values.

For example, the DDRB register corresponds to Port B and controls the direction of the respective pins. Setting a bit to 1 configures the corresponding pin as an output, and setting it to 0 configures it as an input.

Similarly, the PORTB register is used to write output values to the pins. Setting a bit to 1 in this register drives the corresponding pin high, while setting it to 0 drives the pin low.

The PINB register allows us to read the current state of the pins. A logic high or low value indicates the respective pin’s input state.

Pin Control

To encapsulate the configuration and manipulation of individual pins, we can define helper functions or macros. These abstractions improve code readability and ease of reuse.

Let’s explore some practical examples to solidify our understanding.

Example 1: Blinking an LED

Let’s begin with a simple example of blinking an LED connected to Pin 13 (PB5) on an Arduino Uno board.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <avr/io.h>
#include <util/delay.h>

#define LED_PIN PB5

int main(void) {
    DDRB |= (1 << LED_PIN);

    while (1) {
        PORTB ^= (1 << LED_PIN);
        _delay_ms(500);
    }

    return 0;
}

In this example, we first set the direction of Pin 13 (PB5) to output by setting the corresponding bit in the DDRB register. The |= (1 << LED_PIN) operation sets bit 5 to 1.

Inside the infinite while (1) loop, we toggle the state of Pin 13 by manipulating the bit in the PORTB register. The ^= (1 << LED_PIN) XOR operation with the desired pin masks inverts the current state. A delay of 500 milliseconds is added using the _delay_ms() function from the util/delay.h library for a visible blinking effect.

Example 2: Reading a Switch Input

Now, let’s explore reading the state of a switch connected to Pin 2 (PD2). We will turn on an LED connected to Pin 13 (PB5) when the switch is pressed and turn it off otherwise.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <avr/io.h>

#define SWITCH_PIN PD2
#define LED_PIN PB5

int main(void) {
    DDRB |= (1 << LED_PIN);
    DDRD &= ~(1 << SWITCH_PIN);  // Set as input

    while (1) {
        if (PIND & (1 << SWITCH_PIN))
            PORTB |= (1 << LED_PIN);    // Button pressed, LED on
        else
            PORTB &= ~(1 << LED_PIN);   // Button released, LED off
    }

    return 0;
}

In this example, we configure Pin 2 (PD2) as an input by clearing the corresponding bit in the DDRD register using &= ~(1 << SWITCH_PIN). We also set Pin 13 (PB5) as an output.

Inside the infinite while (1) loop, we continuously check the state of Pin 2 using the PIND register. If the switch is pressed, the corresponding bit will be high, and we set Pin 13 high using PORTB |= (1 << LED_PIN) to turn on the LED. When the switch is released, we clear the bit using PORTB &= ~(1 << LED_PIN) to turn off the LED.

Conclusion

In this hands-on guide, we explored the basics of working with GPIOs on the AVR Atmega-328. We covered pin numbering, I/O port registers, and demonstrated practical examples to blink an LED and read switch input. This knowledge provides a solid foundation for building more complex projects and interacting with the external world.

Remember, experimenting with GPIOs is the gateway to unlock the full potential of microcontrollers. So, keep exploring, tinkering, and pushing the boundaries to unleash your creativity. Happy hacking!


➡️ A Flask primer


⬅️ Subparsers in Python `argparse`


Go back to Posts.