Attiny (tinyAVR) series-0 and how to use their GPIO

The easiest way to start programming MCU is to use their General Purpose Input/Output pins to read a button and/or turn ON/OFF an LED. This post is going to be exactly that – an introduction into Attiny Series-0 GPIOs with easy to understand examples which should help you get started with the MCU.

What is a GPIO?

So, a GPIO stands for General Purpose Input/Output. Simply speaking GPIO is a Pin on an IC. The pin can act as an Input or Output. Input reads voltage (High or Low), while output pin creates voltage level (High or Low).

The pins can be individually configured – each one can be set to Input or Output independently.

The input can have internal Pullup resistor enabled – which is useful in some cases.


The GPIO pins are set up and controlled via port peripherals registers, which are usually named as PORTx (where x is a letter A, B, C …). In case of an Attiny404, it has two control register instances: PORTA and PORTB. If you have ever programmed older devices, like ATmega16, you will know that PORTx is an actual register with changeable bit values. While with tinyAVR microcontrollers, logic is a bit different: PORTx is an instance and this instance has registers like DIR, DIRSET, DIRCLR… which has actual changeable values.

Block diagram

Attiny tinyAVR GPIO diagram Schematic


To set up the system clock before using this peripheral is mandatory. The good thing is that by default, the CPU will be running at some specific speed (3.33 or 2.67 MHz), so there are not much to do, unless you want to change the default clock speed.

Setting up interrupt and/or event controllers is optional. You will have to set it up if your are going to use either peripheral interrupts, events or both.

Initialization and operation

By default, after a start-up, all GPIOs are set into tri-state (no pull-up) inputs.

Each GPIO pin can be controlled by a register(s) in PORTx instances. For example, PORTx.DIR register is used to set the pin directions. Writing a value to PORTx.DIR register will change all GPIOs directions. Writing to DIR registers should be well thought through, as you might accidently change some pin values that you wouldn’t want to change.

So, to make thing a little bit easier, there are registers like PORTx.DIRSET, PORTx.DIRCLR and PORTx.DIRTGL registers. They naming and register functions are as follows: DIRSET – sets (sets value to 1) those DIR register values that have ‘1’ in DIRSET register, DIRCLR – clears (sets to 0) those register DIR bits which have ‘1’ in DIRCLR register and DIRTGL – toggles those DIR bits which are set to 1 in DIRTGL register. So, using PORTx.DIRSET, PORTx.DIRCLR and PORTx.DIRTGL registers you can change (set, clear toggle) only particular bits without changing value of other bits.

An example: let’s say PORTA.DIR register value is 0b00000001. When you write a value 0b10000000 directly to PORTA.DIR register, its value changes to 0b10000000, which means that 7th bit is set to 1, and 0th bit is cleared. But, if you have PORTA.DIR register value 0b00000001 and write 0b10000000 to PORTA.DIRSET register, the PORTA.DIR becomes 0b10000001, as only the 7th bit is set and the 0th bit is left untouched.


PORTx instance in total has four registers: DIR (already mentioned), OUT, IN and INTFLAG. When the GPIO is configured as output (writing 1 into DIR register corresponding bit) the PORTx.OUT register changes output value (either 0 or 1). OUT register also has its additional registers which help you out like with the DIR register: OUTCLR, OUTSET, OUTTGL.

The input value can be read from bit n in register PORTx.IN as long as the ISC bit is not set to INPUT_DISABLE in Pin configuration register (see below GPIO pin configuration).

INTFLAGS register hold interrupt values. If interrupt happens – the corresponding bit changes to 1. Writing to this register clear the bit.

GPIO pin configuration

If you want to configure inverted I/O, pullups and how pin input is sensed, you will have to use PORTx.PINnCTRL (Pin n configuration) register. Each pin has its own configuration register.

So, the Pin Configuration register has several bits that might be interesting to you. INVEN bit sets whether pin is inverted or not.

PULLUPEN bit enables (1) or disables (0) pullup resistor on the input.

When a pin is used for interrupt generation, the interrupt conditions are defined by the ISC bits in the Pin configuration register. The possible settings are: Bothedges – interrupt is triggered on rising and/or falling signal edge. Rising – interrupt only on the rising signal edge and Falling – only on falling edge. Finally, a setting Level – sets that an interrupt is generated when the signal level is Low.

Lastly, it is worth mentioning that not all GPIOs are asynchronous pins, which means that synchronous pin won’t always wake the system from sleep. For more information see the datasheet.

Interrupts and events (in short)

Each port pin n can be configured as an interrupt source. Each interrupt can be individually enabled or disabled by writing to ISC in PORT.PINCTRL.

When an interrupt happens, the interrupt flag is set in the Interrupt Flag register the GPIO register (PORT.INTFLAGS).

An interrupt request is generated when the corresponding interrupt is enabled and the interrupt flag is set. The interrupt request remains active until the interrupt flag is cleared. Writing a ‘1’ to a flag’s bit location will clear the flag.

Pins are also connected to the event system. It allows generated signals (in a form of a generated event) from one peripheral transfer to another without CPU intervention. There are as many event generators as there are PORT pins in the device.

It should be noted, that all PORT pins can act as event generators, but not the event inputs.

PORT registers (overview)

DIRData Direction register. 8 bit value’s each bit represents each PIN in the PORT. 1 – pin set to output, 0 – input.
DIRSETData Direction Set register. Writing 1 to DIRSET[n] to this register will set n Pin to output.
DIRCLRData Direction Clear register. Writing a 1 to DIRCLR[n] will clear the n-th bit in PORT.DIR.
DIRTGLData Direction Toggle register. Writing a 1 to DIRTGL[n] will toggle the n-th bit in PORT.DIR.
OUTOutput value register. When pin configured as output, writing 1 sets output to High, 0 – Low.
OUTSETOutput value Set register. Writing a 1 to OUTSET[n] will set n-th bit to 1 in PORT.OUT register.
OUTCLEAROutput value Clear register. Writing a 1 to OUTCLR[n] will clear n-th bit to 0 in PORT.OUT register.
OUTTGLOutput value Toggle register. Writing a 1 to OUTTGL[n] will toggle n-th bit in PORT.OUT register.
INInput value register. This register shows the value present on the pins.
INTFLAGSInterrupt pit Flags register.
PINxCTRLThe INT Flag is set when a pin change/state matches the pin’s input sense configuration. Writing 1 to flag vit will clear the flag.

Portmux and alternative pins


In this part I would like just to notice that only some of the Pin functions can be muxed to different physical pins (text in grey color shows alternative function pins). So, when designing a device with such MCU you will need to keep in mind and firstly select which MCU functions you are going to use, which pins are used by those selected functions/peripherals and only then other pins can be used as GPIO.

The table was taken from the Attiny404 datasheet.

Real example (Hello World)

So let’s do a “Hello World” example – read input from a button and turn on or off an LED.

This example will be shown using PlatformIO. It should also work with other IDEs.

Firstly, the connection schematic looks like this:

Attiny404 button led connection schematic for GPIO tutorial

Parts list

And in reality everything was wired on a breadboard:

Attiny GPIO tutorial connection on a breadboard prototype

You can notice, that Attiny MCU is soldered on a DIY adapter board. As series Tyny MCUs comes only in SMD packages you will have to use some kind of SMD-TH adapter board. Many of them can be sourced from electronics shops.

Also, as I had only an SMD button, I have soldered additional “pins” made of resistor legs, so it could be inserted into the breadboard.

SMD button modification to be fitten into prototyping bread board

First example

Firstly, open PlatformIO (Visual Studio Code) and create a new project. It will ask you to define project name and select a board. Search for Attiny404 (or other tiny MCU – depend what you are using). By default only available framework is Arduino – leave it as is.

Platformio creating Attiny404 project

When you have created a project, now open platformio.ini file and delete the arduino framework line, so only these lines are left:

platform = atmelmegaavr
board = ATtiny404

That means that we won’t be using arduino framework, but only Atmel libraries.

Next, open main.cpp file and change it to:

#include <avr/io.h>

int main(void) {
  PORTB_DIRSET = (1<<2); //set PORTB PIN2 to output
  PORTB_OUTSET = (1<<2); // set output HIGH;
  PORTA_PIN5CTRL |= (1<<3); //pullup enable on PORTA PIN5 via PIN control register;

  while (1){
    if(PORTA_IN & (1<<5)){ // if input is HIGH
      PORTB_OUTSET = (1<<2); // set HIGH (LED ON);
    else{ //if input is low
      PORTB_OUTCLR = (1<<2); // set LOW (LED OFF);


The code should be quite self-explanatory. The first include line points to avr library which has all register name definitions. So, you won’t need to define them by yourself.

When you upload the code to Attiny, the LED will lit up. When you press the button the LED will turn off and when you release button, the LED will lit up again.

And here is a demonstration:

Second example

Although the first example showns the basics on how to use GPIO, it is not very interesting as in reality, you could the same action directly connecting button with an LED without using an MCU and you would get the same behaviour 😀 So, next example code shows how to toggle LED with each button press:

#include <avr/io.h>

#define F_CPU 3333333UL
#include <util/delay.h>

int main(void) {
  PORTB_DIRSET = (1<<2); //set PORTB PIN2 to output
  PORTB_OUTSET = (1<<2); // set output HIGH;

  PORTA_PIN5CTRL |= (1<<3); //pullup enable on PORTA PIN5 via PIN control register;

  while (1){
    if(!(PORTA_IN & (1<<5))){ // if input is LOW (button pressed)
      PORTB_OUTTGL = (1<<2); // toggle LED pin;
      _delay_ms(200); //delay 300 ms -> works as debounce;


If you have read the GPIO theory written in this post’s beginning the code should be again self-explanatory, except some parts which are out of scope of this tutorial.

So, the lines:

#define F_CPU 3333333UL
#include <util/delay.h>

The first line defines CPU frequency. It needs to be set up to the actual CPU frequency that it operates. In my case, by default Attiny integrated oscillator works at 20 MHz frequency and it has default devider set to 6. So, actual clock frequency becomes 3.3333 MHz.

The socond line points to AVR library called delay. It has everything if you need to delay your code for some time. The library uses F_CPU definition mentioned above.

And lastly, the line I would like to talk about:

_delay_ms(200); //delay 300 ms -> works as debounce;

This line, as the comment states, works as a button debounce. In short, when you press a button the contacts inside it bouces and creates lots of button presses, but we need only one. So, waiting for some time will “filter out” that unwanted button bouncing.

In the end, when you upload the code to the MCU you should notice that now with each button press the LED changes it state: if it was ON – becomes OFF and if it was ON, now it will be OFF.

Here is a demonstrational video:


So, this was intro into Attyny Series-0 MCUs with their GPIO abilities and simple, easy to understand examples which should help you getting started with these MCUs.

Subscribe to a newsletter!


Was this page helpful?