AVR GPIO in C: Using DDRx, PORTx and PINx for LEDs and Push Buttons
General purpose input/output (GPIO) is where every microcontroller course begins. On an AVR, each I/O port is controlled by exactly three registers, and once you understand how they interact you can drive LEDs, relays and transistors, and read switches and digital sensors without any library.
In this tutorial you will build a small circuit with four LEDs and one push button on an ATmega328P, and write a program that changes the LED pattern each time the button is pressed. Along the way you will learn about internal pull-up resistors, the toggle feature of the PINx register, and simple software debouncing.
The Three Port Registers
Each port (B, C and D on the ATmega328P; A, B, C and D on the ATmega16/32) has three 8-bit registers, where bit n controls pin n of that port.
| Register | Name | What it does |
|---|---|---|
| DDRx | Data Direction Register | 1 makes the pin an output, 0 makes it an input |
| PORTx | Data Register | For outputs: 1 drives high, 0 drives low. For inputs: 1 enables the internal pull-up |
| PINx | Input Pins Address | Reading gives the actual logic level on each pin. On the ATmega328P, writing 1 to a bit toggles the PORTx bit |
Combining DDRx and PORTx gives four possible pin states. The input-with-pull-up state is the one you will use most for switches, because it removes the need for an external resistor.
| DDxn | PORTxn | Pin state |
|---|---|---|
| 0 | 0 | Input, high impedance (floating) |
| 0 | 1 | Input with internal pull-up enabled |
| 1 | 0 | Output, driven low |
| 1 | 1 | Output, driven high |
Circuit
Connect four LEDs to PB0 to PB3 (DIP pins 14 to 17), each through a 330 Ω resistor to ground, so a high output turns the LED on. Connect a push button between PD2 (pin 4) and ground. No external pull-up is needed because the internal one will be enabled. On an Arduino Uno these pins are D8 to D11 and D2.
Each AVR pin can source or sink a few tens of milliamps, and the datasheet specifies 40 mA as an absolute maximum per pin, but total port current is also limited. With 330 Ω at 5 V a red LED draws roughly 9 mA, which is comfortable. For relays or motors, always use a transistor driver.
Program: Button Steps Through LED Patterns
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>
#include <stdint.h>
#define LED_MASK 0x0F /* PB0..PB3 */
#define BTN_PIN PIND2
/* LED patterns shown one after another */
static const uint8_t patterns[] = {
0x01, 0x02, 0x04, 0x08, /* single LED walking */
0x03, 0x0C, 0x0F, 0x00 /* pairs, all on, all off */
};
/* Returns 1 once per press, after a simple debounce */
static uint8_t button_pressed(void)
{
if (!(PIND & (1 << BTN_PIN))) { /* pin low = pressed */
_delay_ms(20); /* let contacts settle */
if (!(PIND & (1 << BTN_PIN))) {
while (!(PIND & (1 << BTN_PIN))) /* wait for release */
;
_delay_ms(20);
return 1;
}
}
return 0;
}
int main(void)
{
uint8_t index = 0;
DDRB |= LED_MASK; /* PB0..PB3 outputs */
PORTB &= ~LED_MASK; /* all LEDs off */
DDRD &= ~(1 << DDD2); /* PD2 input */
PORTD |= (1 << PORTD2); /* enable pull-up on PD2 */
while (1) {
if (button_pressed()) {
index++;
if (index >= sizeof(patterns))
index = 0;
/* write only the low four bits, keep PB4..PB7 unchanged */
PORTB = (PORTB & ~LED_MASK) | patterns[index];
}
}
}
How the Code Works
The line DDRB |= LED_MASK; sets bits 0 to 3 of DDRB while leaving bits 4 to 7 alone. This read-modify-write style matters because PB3, PB4 and PB5 are also the SPI programming pins, and other code may be using the upper bits of the port.
For the button, DDRD bit 2 is cleared to make PD2 an input and PORTD bit 2 is set to switch on the pull-up. The pin therefore reads 1 when the button is open and 0 when it is pressed, which is why the test is !(PIND & (1 << BTN_PIN)). Always read inputs from PINx, never from PORTx; PORTx only returns what you last wrote.
Mechanical switches bounce, producing several quick transitions over a few milliseconds. The function confirms the press after 20 ms, waits for release, then waits again so that release bounce is not counted as a new press. This blocking method is fine for a demo; later tutorials replace it with a timer-based approach that does not stall the program.
Fast toggling with PINx
On the ATmega328P, writing a 1 to a PINx bit toggles the matching PORTx bit in a single instruction, so PINB = (1 << PINB5); flips the LED on PB5. This feature does not exist on the ATmega16 and ATmega32; there you must use PORTB ^= (1 << PB5);, which compiles to a read, an XOR and a write.
Common Mistakes
- Writing
PORTB = 0x01;when you only meant to change one pin. This clears every other bit of the port, turning off other outputs and pull-ups. - Reading PORTD instead of PIND to check a switch. The result never changes when you press the button.
- Leaving a button input without a pull-up, so it floats and triggers randomly when you touch the wires.
- Using PC2 to PC5 on an ATmega16/32 while the JTAG interface is still enabled by fuse. Those pins will not respond to DDRC and PORTC.
- Using PD0 and PD1 for LEDs on an Arduino Uno board. These are connected to the USB-serial chip and are also the USART pins.
Summary
AVR GPIO uses three registers per port: DDRx sets direction, PORTx sets output level or enables the pull-up, and PINx reads the real pin level. Use bitwise OR, AND and XOR with shifted masks so that only the intended pins change, read inputs from PINx, and debounce mechanical switches. The next tutorial replaces polling of the button with an external interrupt on INT0.