Skip to content

GPIO Registers and Digital I/O

GPIO Registers and Digital I/O hero image
Modified:
Published:

Every pin on the ATmega328P is controlled by three registers: DDRx sets direction, PORTx drives output or enables pull-ups, and PINx reads the physical voltage level. Understanding these registers is the foundation for all digital interfacing. In this lesson you will wire up four LEDs and a push button, then write firmware for an electronic dice that rolls a random number on each button press. No digitalRead(), no digitalWrite(), just clean bit manipulation on bare hardware. #GPIO #BitManipulation #AVR

What We Are Building

Electronic Dice

Press a button and four LEDs light up in a pattern representing a dice face (1 through 6). The random number comes from a 16-bit linear feedback shift register (LFSR) that runs continuously in the main loop. Button debouncing is handled in software with a simple delay-based check. The LED patterns map dice pips to specific pin combinations.

Project specifications:

ParameterValue
MCUATmega328P (on Arduino Nano or Uno)
LEDs4 (on PD2, PD3, PD4, PD5)
Button1 (on PD6, active low with pull-up)
PRNG16-bit Galois LFSR
Debounce50 ms delay-based
Current per LED~10 mA (220 ohm resistor)
Dice range1 to 6

Parts for This Lesson

RefComponentQuantityNotes
1Arduino Nano or Uno (ATmega328P)1From Lesson 1
2Breadboard1From Lesson 1
3LED (any color, 3mm/5mm)43 new plus 1 from Lesson 1
4220 ohm resistor43 new plus 1 from Lesson 1
5Push button (tactile switch)16mm through-hole
610K ohm resistor1External pull-up (or use internal)
7Jumper wires~10Male-to-male

GPIO Register Overview



The ATmega328P groups its 23 I/O pins into three ports: Port B (PB0 through PB7), Port C (PC0 through PC6), and Port D (PD0 through PD7). Each port has three registers. DDRx (Data Direction Register) sets each pin as input (0) or output (1). PORTx (Port Data Register) drives high/low on outputs or enables the internal pull-up on inputs. PINx (Port Input Pins Register) reads the current logic level regardless of direction.

RegisterRead/WritePurpose
DDRxR/WSet pin direction: 1 = output, 0 = input
PORTxR/WOutput: drive high/low. Input: enable pull-up
PINxRRead pin logic level

For a single pin, the DDRx and PORTx bits combine to select one of four configurations:

DDRx PORTx Pin Configuration
---- ----- ----------------------------
0 0 Input, floating (Hi-Z)
0 1 Input, internal pull-up
1 0 Output, drive LOW
1 1 Output, drive HIGH
Output push-pull: Input with pull-up:
VCC VCC
| |
+--- (DDRx=1) [R] internal
| | ~20K-50K
+--+--+ +--+--+
| Pin |---> to load | Pin |<--- from switch
+--+--+ +--+--+
| |
GND (switch to GND)

Bit Manipulation Patterns

These operations rely on binary and hexadecimal representations of register values. For a refresher on number systems and bitwise logic, see Digital Electronics: Binary, Hex, and Number Systems.

/* Set bit (make pin output or drive high) */
DDRB |= (1 << PB5);
/* Clear bit (make pin input or drive low) */
PORTB &= ~(1 << PB5);
/* Toggle bit */
PORTB ^= (1 << PB5);
/* Read a single bit */
if (PIND & (1 << PD6)) {
/* PD6 is high */
}
/* Set multiple bits at once */
DDRD |= (1 << PD2) | (1 << PD3) | (1 << PD4) | (1 << PD5);

Circuit Connections



Wire the four LEDs with their anodes to PD2 through PD5 (through 220 ohm resistors to ground). Connect the push button between PD6 and ground, with a 10K pull-up resistor to VCC.

ATmega328P
+-----------+
| |
VCC---+---[10K]---+--PD6 (Button)
| | |
| PD2 ----+--[220R]--[LED1]--GND
| PD3 ----+--[220R]--[LED2]--GND
| PD4 ----+--[220R]--[LED3]--GND
| PD5 ----+--[220R]--[LED4]--GND
| |
+-----------+
|
GND
Button: normally open to GND
Press pulls PD6 low

Alternatively, you can skip the external pull-up and enable the internal pull-up in software by setting the PORTx bit while the pin is configured as input.

SignalArduino Nano PinATmega328P PinConnection
LED 1D2PD2LED anode, 220R to GND
LED 2D3PD3LED anode, 220R to GND
LED 3D4PD4LED anode, 220R to GND
LED 4D5PD5LED anode, 220R to GND
ButtonD6PD6Button to GND, 10K to VCC

Arduino Nano with 4 LEDs and push button on breadboard

The LFSR Random Number Generator



A linear feedback shift register produces pseudo-random numbers using only XOR and shift operations. It needs no multiplication or division, making it ideal for small microcontrollers. The Galois form is particularly efficient: each step shifts the register right by one, and if the outgoing bit is 1, the register is XORed with a polynomial tap mask. For a 16-bit LFSR with taps at bits 16, 14, 13, and 11, the sequence repeats after 65535 values.

static uint16_t lfsr = 0xACE1; /* Non-zero seed */
static uint8_t roll_dice(void)
{
/* Galois LFSR step */
uint8_t lsb = lfsr & 1;
lfsr >>= 1;
if (lsb) lfsr ^= 0xB400; /* Taps at 16,14,13,11 */
return (lfsr % 6) + 1; /* Result: 1 to 6 */
}

Dice LED Patterns



A real dice has a specific pip layout for each face. With four LEDs arranged in a square, you can represent all six faces by choosing which LEDs to light. The mapping below uses LED1 (top-left), LED2 (top-right), LED3 (bottom-left), and LED4 (bottom-right).

Dice ValueLED1LED2LED3LED4Pattern
1OFFONOFFOFFCenter-ish
2ONOFFOFFONDiagonal
3ONOFFONONTriangle
4ONONONONAll corners
5ONONONONAll (plus flash)
6ONONONONAll (double flash)

Complete Firmware



#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>
/* Variable-length delay: _delay_ms() requires a compile-time
constant on many avr-gcc versions, so we wrap it in a loop. */
static void delay_ms(uint16_t ms)
{
while (ms--) _delay_ms(1);
}
#define LED_MASK ((1<<PD2)|(1<<PD3)|(1<<PD4)|(1<<PD5))
#define BTN_PIN PD6
static uint16_t lfsr = 0xACE1;
static uint8_t roll_dice(void)
{
uint8_t lsb = lfsr & 1;
lfsr >>= 1;
if (lsb) lfsr ^= 0xB400;
return (lfsr % 6) + 1;
}
static void show_dice(uint8_t value)
{
/* Clear all LEDs */
PORTD &= ~LED_MASK;
uint8_t pattern = 0;
switch (value) {
case 1: pattern = (1<<PD3); break;
case 2: pattern = (1<<PD2)|(1<<PD5); break;
case 3: pattern = (1<<PD2)|(1<<PD4)|(1<<PD5); break;
case 4: pattern = (1<<PD2)|(1<<PD3)|(1<<PD4)|(1<<PD5); break;
case 5: pattern = (1<<PD2)|(1<<PD3)|(1<<PD4)|(1<<PD5); break;
case 6: pattern = (1<<PD2)|(1<<PD3)|(1<<PD4)|(1<<PD5); break;
}
PORTD |= pattern;
}
static uint8_t button_pressed(void)
{
if (!(PIND & (1 << BTN_PIN))) {
_delay_ms(50); /* Debounce */
if (!(PIND & (1 << BTN_PIN))) {
return 1;
}
}
return 0;
}
int main(void)
{
/* LEDs as outputs */
DDRD |= LED_MASK;
/* Button as input with internal pull-up */
DDRD &= ~(1 << BTN_PIN);
PORTD |= (1 << BTN_PIN);
uint8_t last_value = 1;
show_dice(last_value);
while (1) {
/* Keep the LFSR running for randomness */
(void)roll_dice();
if (button_pressed()) {
/* Rolling animation */
for (uint8_t i = 0; i < 10; i++) {
show_dice((i % 6) + 1);
delay_ms(50 + i * 20);
}
last_value = roll_dice();
show_dice(last_value);
/* Wait for button release */
while (!(PIND & (1 << BTN_PIN)));
_delay_ms(50);
}
}
}

Compiling and Flashing



Use the same Makefile structure from Lesson 1. Update main.c with the dice code, then:

Terminal window
make flash

Press the button and watch the LEDs cycle through a rolling animation before settling on a random dice value.

Understanding Pull-Up Resistors



The following diagram compares a floating input (unpredictable readings) with a pulled-up input (stable HIGH until grounded by a switch):

Floating input: Pull-up input:
??? VCC
| |
| (noise couples in) [R] 10K (ext) or
| | 20-50K (int)
+--+--+ +--+--+
| PINx | reads random | PINx | reads HIGH
+--+--+ +--+--+
| |
(nothing) [Switch]--GND
(press = LOW)

When a pin is configured as input, it is high-impedance and will float to unpredictable values if left unconnected. A pull-up resistor ties the pin to VCC so it reads high by default. Pressing the button connects the pin to ground, pulling it low. The ATmega328P has built-in pull-up resistors (20K to 50K typical) that you enable by writing a 1 to the PORTx bit while DDRx is 0. External 10K pull-ups give a stronger, more predictable pull-up current.

Exercises



  1. Modify the dice to support values 1 through 8 by adding unique blink patterns for 7 and 8 (for example, alternating flashes).
  2. Replace the delay-based debounce with a state machine that samples the button every 5 ms and requires three consecutive identical readings before accepting a press.
  3. Add a “cheat mode”: if you hold the button for more than 2 seconds, always roll a 6.
  4. Measure the current drawn by each LED with a multimeter. Verify it matches the calculation: (5V minus LED forward voltage) divided by 220 ohms.

Summary



You now understand the three GPIO registers (DDRx, PORTx, PINx) and how to use bit manipulation to control individual pins. You have implemented software debouncing, used a Galois LFSR for pseudo-random numbers, and driven multiple LEDs with distinct patterns. These register-level techniques apply to every peripheral on the ATmega328P, not just GPIO.

Comments

Loading comments...


© 2021-2026 SiliconWit®. All rights reserved.