How to use GPIO pins on new generation of AVR Microcontrollers

How to use GPIO pins on new generation of AVR Microcontrollers

This article will guide you on using GPIO pins for input and output on the latest AVR microcontrollers. Although the examples provided are for the ATtiny 202 microcontroller, you can easily adapt them for other models with minor modifications. We’ll begin with a straightforward example of how to make an LED blink. Next, we’ll learn how to read a signal from the input pin, and finally, how to make use of interrupts.

As a student, I was introduced to Atmel AVR 8-bit microcontrollers, and this was before Arduino became popular. I particularly worked with ATmega8 and ATMega16 in small projects. After completing my studies, I worked as a software engineer and stopped working on hardware projects for a while. A few years ago, I decided to start using these microcontrollers again for fun. I found out that Atmel was bought by Microchip in 2016, and they released a new generation of AVR microcontrollers. While the older generation is still available, the new generation caught my attention. Although it had been more than ten years since I had last worked with microcontrollers, I noticed that I still had the skills to read datasheets.

One inconvenience of the new microcontrollers is that they do not come in DIP packages like the previous generation. As a result, if I want to use them on a breadboard, I have to solder them onto a SOIC/VQFN to DIP breakout board.

The latest generation of AVR microcontrollers offers several advantages, such as being more cost-effective, consuming less power, and having more peripherals. While the C headers for the new generation differ in how we reference chip registers, the fundamental working principle remains the same. You can enable/disable peripherals by setting or clearing specific bits in the appropriate register.

The new AVR microcontrollers are programmed and debugged using the Unified Program and Debug Interface (UPDI). For development, I prefer to use Microchip Studio IDE (previously Atmel Studio) and the ATMEL-ICE programmer. I know, the programmer is a bit costly, and there are alternative options available, perhaps that’s a topic for another discussion.

Setting/clearing bits

When we say “set a bit,” we mean that we set the bit to “1.” Conversely, when we say “clear a bit,” we mean that we set the bit to “0.” The <avr/io.h> header file offers eight PINx_bm bitmasks that allow for individual bit manipulation, with x ranging from 0 to 7. The value of x signifies the position of the bit that has a value of “1”, while the remaining bits are 0.

To set a bit in a register, an OR operation is performed with the bitmask:

REGISTER = REGISTER | PINx_bm;
// alternatively, it can be written as:
REGISTER |= PINx_bm;

To clear a bit in a registry, an AND operation is performed with the inverted bitmask:

REGISTER = REGISTER & ~PINx_bm;
// alternatively, it can be written as:
REGISTER &= ~PINx_bm;

To modify a specific register, replace the word REGISTER with the actual register name and replace x with the bit number. Microchip documentation recommends modifying individual bits to avoid changing other bits by mistake.

Blinking LED

To make a pin work as input or output, we change the Data Direction Register - PORTn.DIR. The letter “n” in PORTn.DIR refers to the port letter, such as A, B, C, etc. For example, to set pin PA1 on port A as output (physical pin 4), we need to set bit 1 in the PORTA.DIR register (Figure 1a). All pins are set as input by default since the default value is 0.

To control the value of an output pin, we set or clear the bits in the Output Value Register - PORTn.OUT. When we set bit 1 in the PORTA.OUT register (Figure 1b), pin PA1 will output the source voltage value (pulled up to VDD). If we clear the bit, the pin will output 0V (pulled down to GND).

Data Direction and Output Value Registers

Figure 1. Data Direction and Output Value Registers.

ATtiny202 microcontroller and LED connected to pin PA1

Figure 2. ATtiny202 microcontroller and LED connected to pin PA1.

The following program blinks an LED connected to pin PA1. Figure 2 shows the circuit schematic.

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

int main(void)
{
	PORTA.DIR |= PIN1_bm; 
	
	while(1)
	{
		PORTA.OUT |= PIN1_bm;
		_delay_ms(1000);
		PORTA.OUT &= ~PIN1_bm;
		_delay_ms(1000);
	}
}
  • Line 1: Defines the F_CPU macro which is required when using the util/delay.h file.
  • Line 2 and 3: Import the necessary header files.
  • Line 7: Sets the bit 1 in the PORTA.DIR register, which sets the PA1 pin as an output.
  • Line 9: Runs an infinite loop as long as the microcontroller is powered up.
  • Line 11: Sets the PA1 pin to HIGH level state.
  • Line 12: Pauses the program for 1 second.
  • Line 13: Sets the PA1 pin to LOW level state.
  • Line 14: Pauses the program for another second, and after the cycle repeats again on line 11.

Toggle LED with a button

Let’s modify the schematic and program to toggle the LED manually using a button. Connect a button between the PA2 pin (physical pin 5) and GND, so that when the button is pressed, the pin will be pulled down to GND. Figure 4 shows the updated schematic. To avoid random fluctuation on the pin, we’ll enable the internal pull-up resistor. This means there’s no need to connect an external pull-up resistor. To enable the pull-up resistor, we set the PULLUPEN bit in the Pin 2 Configuration Register - PORTA.PIN2CTRL (Figure 3b). Since all pins are set to input by default, there’s no need to clear the bit in the PORT.DIR register. To read the value on the pin, we read the value of bit 1 from the PORT.IN register (Figure 3a).

Input Value and Pin 2 Control Registers

Figure 3. Input Value and Pin 2 Control Registers.

Updated schematic with a button connected to PA2 pin

Figure 4. Updated schematic with a button connected to PA2 pin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#define F_CPU 3333333
#include <avr/io.h>
#include<util/delay.h>

int main(void)
{
	PORTA.DIR |= PIN1_bm;
	PORTA.PIN2CTRL |= PORT_PULLUPEN_bm;
	uint8_t prevState = PORTA.IN & PIN2_bm;
	
	while(1)
	{
		uint8_t currState = PORTA.IN & PIN2_bm;
		if(currState != prevState && !currState) {
			PORTA.OUT ^= PIN1_bm;
		}
		prevState = currState;
		_delay_ms(10);
	}
}
  • Line 8: Enables the pull-up resistor on pin PA2.
  • Line 9: Defines a variable and stores the initial state of the PA2 pin.
  • Line 11: Enter the infinite loop.
  • Line 13: Gets the current state of the PA2 pin.
  • Line 14: Checks if the current state is different from the previous state and the current state is LOW (falling edge).
  • Line 15: Toggles the value on pin PA1.
  • Line 17: Stores the current state in the previous state variable.
  • Line 18: Adds a delay of 10ms to filter all the signal fluctuation during the button press. Alternatively, you can avoid using the delay by connecting a 1uF capacitor between the pin and ground to filter signal fluctuations during button presses - see Figure 4.

Using interrupts

Let’s enhance the previous example by implementing interrupts instead of delay functions. We don’t need the F_CPU macro or the delay functions from the <utils/delay.h> header file. However, we need to import the <avr/interrupt.h> header file. The schematic remains the same as shown in Figure 4. To enable an interrupt on pin PA2, we must set the Input/Sense Configuration (ICS) bits, which are the first three bits in the PORTA.PIN2CTRL register. In this example, we will configure interrupts to occur on a falling edge by writing 0x3 to the ICS bits using the PORT_ISC_FALLING_gc configuration group mask. For additional options, please refer to the datasheet.

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

ISR(PORTA_PORT_vect)
{
	if(PORTA.INTFLAGS & PIN2_bm) {
		PORTA.OUT ^= PIN1_bm;
		PORTA.INTFLAGS &= PIN2_bm;
	}
}

int main(void)
{
	PORTA.DIR |= PIN1_bm;
	PORTA.PIN2CTRL |= PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;
	sei();
	
	while(1)
	{
		// Additional logic here
	}
}
  • Line 4: Defines the interrupt handler function for PORTA changes
  • Line 6: Checks if the interrupt was triggered for the PA2 pin
  • Line 7: Toggles the value on PA1 pin
  • Line 8: Clears the interrupt flag to prevent further triggering
  • Line 14: Sets PA1 as an output pin
  • Line 15: Enables the internal pull-up resistor and sets the interrupt to be triggered on a falling edge on pin PA2
  • Line 16: Enables interrupts, which are disabled by default. Note that in this program, the LED will be toggled only when an interrupt is triggered by a button press. Therefore, the main loop does not contain any logic related to toggling the LED.

Now you know the basics of using GPIO pins on AVR microcontrollers, including how to use interrupts and detect changes on the pin. With some modifications, you can reuse the code presented in this article for more advanced models of the ATTiny 0, 1-series, and ATMega 0-series microcontrollers. Good luck in exploring and experimenting with your microcontroller to learn more advanced concepts and functionalities.