LABORATORY 02

The Digital Input-Output Port

Duration: 2 hours Material: Chapter 2 Registers: DDRx · PORTx · PINx PDF handout RO versiunea română

Digital ports are the microcontroller's interface with the outside world. Each port is controlled by exactly three registers, and understanding their role explains everything that happens at the pin level: from lighting an LED to reading a button with no external components.

1Lab objectives

  • Understanding the role of the DDRx, PORTx and PINx registers
  • Configuring pins through direct register access, with bitwise operations
  • Using the internal pull-up resistors to read buttons
  • Implementing contact-bounce removal
  • Comparing the speed of digitalWrite() with a direct register write

2Materials needed

  • 1 Arduino Uno
  • 1 Breadboard
  • 4 LEDs
  • 4 220 Ω resistors
  • 2 Buttons
  • 10 Jumper wires

3The three registers of a port

The ATmega328P has three ports: B (digital pins 8-13), C (analog pins A0-A5) and D (digital pins 0-7). Each is controlled by three 8-bit registers:

RegisterFull nameRoleAccess
DDRxData Direction Registersets the direction: 0 = input, 1 = outputread / write
PORTxData Registeron output: the logic level
on input: enables the internal pull-up
read / write
PINxPort Input Pinsreflects the real state of the pinsread only
ATmega328P pin assignment across ports B, C and D
Fig. 1 - Physical assignment of the ATmega328P microcontroller's pins across the three ports. Notice the alternate functions in parentheses: PCINT, ADC, OC (timer outputs), INT. Fig. 2.1 in the handout
The PORTD, DDRD and PIND registers
Fig. 2 - The three registers of port D, with their addresses, bit names and access rights. Fig. 2.4 in the handout

The complete configuration table

DDRxnPORTxnConfigurationEquivalent resistanceUse
00input, floatingvery high (high Z)to avoid - random readings
01input with pull-up20-50 kΩ to 5 Vreading buttons, with no external components
10output LOW~25 Ω to GNDturn off the LED / pull the load to ground
11output HIGH~25 Ω to 5 Vturn on the LED
The floating pinA pin configured as input, with no pull-up and nothing connected, behaves like an antenna: it picks up ambient electromagnetic noise and reads values that change at random. Always use a pull-up, internal or external.

4Internal structure of a pin

Vcc = 5 V GND DDRxn direction PORTxn level / pull-up PINxn driver pull-up 20k PIN outside read into PINxn The pull-up switch closes only when DDRxn = 0 and PORTxn = 1
Fig. 3 - Simplified diagram of an I/O pin. The same pin can be a driven output, a floating input, or an input with pull-up, depending on the combination of the two control bits.

5Register simulator

Click on bits to change the registers. The state of the pins at the bottom updates exactly per the configuration table - just like on the real microcontroller.

Port B - direction, level, and resulting state
Try thisBit 5 of DDRB and PORTB is already set - it corresponds to pin D13, the onboard LED, lit. Now set DDRB bit 0 to 0 and PORTB bit 0 to 1: pin D8 becomes an input with pull-up. Watch how the label at the bottom changes.

6Mask lab

You never modify the whole register if you want to change a single pin - that would affect the other seven. Bit masks are used, built with the shift operator:

OperationSyntaxEffect
Set bit (1)PORTB |= (1 << PB5);only bit 5 becomes 1
Clear bit (0)PORTB &= ~(1 << PB5);only bit 5 becomes 0
TogglePORTB ^= (1 << PB5);bit 5 flips its state
Test bitif (PINB & (1 << PB0))true if bit 0 is 1
Set a groupDDRB |= 0b00001111;pins 8-11 become outputs

Check below, bit by bit, what each operation does. Choose the & operator with operand B set to 11011111 to see how bit 5 gets cleared exactly.

Bitwise operators - building masks
Why ~(1 << n) works 1 << 5 produces 00100000. Negation gives 11011111 - every bit at 1, except the one at position 5. AND with this mask leaves every other bit untouched and forces only the desired bit to 0.

7Wiring diagrams

Arduino Uno D2 (INPUT_PULLUP) SW GND 20k Vcc - INTERNAL pull-up idle → the pin reads 1 pressed → the pin reads 0
Fig. 4 - Button connected between a pin and ground. With the internal pull-up enabled, the logic is the reverse of intuition: pressing produces 0, not 1.
Practical setup for lighting an LED
Fig. 5 - The practical setup for lighting an LED, as you will build it on the workbench. Fig. 2.5 in the handout
Practical setup for reading a button's state
Fig. 6 - The setup for reading a button's state, powered from batteries. Fig. 2.6 in the handout
The PORTD register with bit 0 set to enable the pull-up
Fig. 7 - Enabling the pull-up resistor for pin 0 of port D: PORTD = 0b00000001, with DDRD having bit 0 at 0 (input). Fig. 2.7 in the handout

8Contact bounce

A mechanical button does not switch cleanly. The metal contacts bounce for a few milliseconds on touch, generating dozens of false transitions. Without filtering, a single physical press is seen by the program as several presses.

real signal at the pin - with bounce press release after software filtering (debounce 40 ms)
Fig. 8 - Top: the false transitions generated by contact bounce. Bottom: the clean signal obtained through software filtering.
The filtering principleThe moment of the last observed change is recorded. A new state is accepted only if it has stayed unchanged for longer than the debounce interval (typically 20-50 ms). Any transition that occurs earlier restarts the timer.

9Debounce simulator

Follow, step by step, how the algorithm rejects false transitions and only accepts the stable state. Notice that the bounces at t = 102 and t = 107 produce no increment at all.

Filtering bounce - step-by-step execution

10Source code

Arduino functions vs. direct register access

ports_comparison.ino
void setup() {
  // --- Arduino variant: readable, but slow ---
  pinMode(13, OUTPUT);
  pinMode(2, INPUT_PULLUP);

  // --- equivalent register variant: fast ---
  DDRB  |= (1 << PB5);    // pin 13 = output
  DDRD  &= ~(1 << PD2);   // pin 2  = input
  PORTD |= (1 << PD2);    // enable pull-up on pin 2
}

void loop() {
  // reading the button: 0 = pressed, because of the pull-up
  if (!(PIND & (1 << PD2))) {
    PORTB |= (1 << PB5);    // turn on
  } else {
    PORTB &= ~(1 << PB5);   // turn off
  }
}
A measurable performance differencedigitalWrite() needs about 50 clock cycles: it checks the pin is valid, looks up the port in a Flash table, and disables PWM if it was active. PORTB |= (1 << PB5) compiles into a single instruction. At 16 MHz, the difference is between about 3 µs and 62 ns - roughly a factor of 50.

Measuring the speed difference

We do not have the serial monitor yet (lab 6), so we do not print the duration: we make it visible. Each method toggles the pin in a tight loop, and the pin drives a separate LED. The faster the method, the sooner the loop finishes and the shorter the "busy" LED stays lit. The difference is visible to the naked eye, and it can also be measured on an oscilloscope.

benchmark.ino
const uint16_t N = 20000;

void setup() {
  DDRB |= (1 << PB0);          // pin 8: the pin being toggled
  DDRB |= (1 << PB4);          // pin 12: marker "running digitalWrite"
  DDRB |= (1 << PB3);          // pin 11: marker "running the register variant"
}

void loop() {
  // ---- function-call variant: the marker on pin 12 stays lit as long as it runs
  PORTB |= (1 << PB4);
  for (uint16_t i = 0; i < N; i++) {
    digitalWrite(8, HIGH);
    digitalWrite(8, LOW);
  }
  PORTB &= ~(1 << PB4);

  delay(400);

  // ---- register variant: the marker on pin 11
  PORTB |= (1 << PB3);
  for (uint16_t i = 0; i < N; i++) {
    PORTB |=  (1 << PB0);
    PORTB &= ~(1 << PB0);
  }
  PORTB &= ~(1 << PB3);

  delay(400);
}
What you should seeThe marker for the digitalWrite variant stays visibly lit - tens of milliseconds. The marker for the register variant blinks so briefly it is barely noticeable. The ratio between the two durations is exactly the factor of about 50 from the box above.

Complete debounce

debounce.ino
const uint8_t BUTTON_PIN = 2;
const uint8_t LED_PIN   = 13;
const uint16_t DEBOUNCE_INTERVAL = 40;   // ms

bool ledState       = false;
bool lastReading    = HIGH;
bool stableState    = HIGH;
unsigned long lastChangeTime = 0;

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  bool reading = digitalRead(BUTTON_PIN);

  if (reading != lastReading) {          // any change restarts the timer
    lastChangeTime = millis();
    lastReading  = reading;
  }

  if (millis() - lastChangeTime > DEBOUNCE_INTERVAL) {
    if (reading != stableState) {        // the state has stabilized
      stableState = reading;

      if (stableState == LOW) {          // falling edge = a real press
        ledState = !ledState;
        digitalWrite(LED_PIN, ledState);   // a single toggle per press
      }
    }
  }
}

A light effect on four LEDs

knight_rider.ino
void setup() {
  DDRB |= 0b00001111;    // pins 8, 9, 10, 11 as outputs, in a single operation
}

void loop() {
  // shift left
  for (uint8_t i = 0; i < 4; i++) {
    PORTB = (PORTB & 0xF0) | (1 << i);   // keep bits 4-7, set only one of bits 0-3
    delay(120);
  }
  // shift right, without repeating the ends
  for (int8_t i = 2; i > 0; i--) {
    PORTB = (PORTB & 0xF0) | (1 << i);
    delay(120);
  }
}
Why (PORTB & 0xF0)The mask leaves bits 4-7 untouched (including D13, the onboard LED) and lets only bits 0-3 be rewritten. Without it, a direct assignment would turn off everything on the port's other pins.

11Interactive circuit

An input pin is read from PINx, not from PORTx - and when you use the internal pull-up resistor, the logic is inverted: a pressed button means 0. The circuits below let you press the button with your mouse and watch the bit in PIND change in real time.

Button with internal pull-up - why pressed means zero
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRD  &= ~(1 << PD2);      // pin 2 is an INPUT
    PORTD |=  (1 << PD2);      // internal pull-up resistor, enabled
    DDRB  |=  (1 << PB5);      // pin 13 is an OUTPUT
    DDRD  |=  (1 << PD7);      // pin 7 is an OUTPUT

    while (1) {
        if (!(PIND & (1 << PD2))) {     // the bit is 0 -> button PRESSED
            PORTB |=  (1 << PB5);       // the "pressed" LED lights up
        } else {
            PORTB &= ~(1 << PB5);
        }

        /* The second LED copies the raw bit, with no negation.
           Watch them both: they are always in opposition. */
        if (PIND & (1 << PD2)) PORTD |=  (1 << PD7);
        else                   PORTD &= ~(1 << PD7);

        _delay_ms(150);
    }
}
Try thisWatch the PIND2 bit in the register panel: at idle it is 1, because the internal resistor pulls it to 5 V. Pressing the button connects the pin to ground and the bit becomes 0. The two LEDs are always in opposition - that is the entire meaning of the inverted logic. Try deleting the line PORTD |= (1 << PD2); - without the pull-up resistor, the input is left "floating" and the reading becomes unpredictable.
Exercise - a memory switch with bounce removal
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRD  &= ~(1 << PD2);      // pin 2 = INPUT (the button)
    PORTD |=  (1 << PD2);      // internal pull-up resistor
    DDRB  |=  (1 << PB5);      // pin 13 = OUTPUT (the LED)

    /* At idle the button gives 1. Pressed, it gives 0.
       If we only wrote "if it is 0, toggle the LED", the LED would toggle
       hundreds of times for as long as we hold the button. We want it to
       toggle ONCE, right at the moment of pressing - that is, exactly when
       the reading GOES from 1 to 0. To catch that transition, we remember
       what we read on the previous pass. */

    unsigned char previousState = 1;   // at idle the button gives 1

    while (1) {
        unsigned char currentState = (PIND & (1 << PD2)) ? 1 : 0;

        /* --- STEP 1 -------------------------------------------------
           Replace the 0 below with the condition for going from 1 to 0:
           "it was 1 before AND it is 0 now", written with == and &&.
           As long as you leave 0, the condition is always false and the
           LED does NOT light up - exactly what you see if you run the
           program now.                                                */

        if (0) {

            /* --- STEP 2 ---------------------------------------------
               Toggle bit PB5 of PORTB. The TOGGLE idiom is the one with
               ^= , from the previous lab. Write the line here: */



            /* --- STEP 3 ---------------------------------------------
               The button's contacts bounce for a few milliseconds. Put
               a 20 ms pause here to let them settle: */


        }

        /* --- STEP 4 ---------------------------------------------------
           The current reading becomes "the previous one" for the next
           pass. Without this line, the comparison has nothing to work
           with and the LED never lights up. Write it here:            */


        _delay_ms(5);
    }
}
Try thisIf you run it now, the LED does not light up - and that is expected: the condition in step 1 is written as if (0), that is always false, so its body never runs. There are four lines to complete, marked in the code. The point of the whole exercise: we do not care whether the button is pressed, but the moment it was pressed. This is called a falling edge and it is caught by comparing the current reading with the one from the previous pass of the loop - which is why we need the previousState variable and step 4, which keeps it up to date. Without the 20 ms from step 3, a single click can be seen as dozens of presses, because the contacts bounce for a few milliseconds. Check it on the oscilloscope: the LED must toggle exactly once for every press. If you get stuck, the 💡 view solution button loads a correct variant into the editor.

12Work tasks

  • Wire up 4 LEDs on pins 8-11 and light them sequentially, using only register operations.
  • Connect a button on pin 2 with the internal pull-up; print its idle and pressed value over serial.
  • Check what is read on an input pin left floating, with no pull-up.
  • Implement software debounce and count presses; check that each physical press produces exactly one increment.
  • Modify the program so each press reverses the direction of the light effect.
  • Run the benchmark and note the ratio between digitalWrite() and direct access.
  • Build a 4-bit binary counter on the LEDs, incremented on each button press.
  • Try PORTB = 0b00000001; with no mask and observe what happens to the LED on D13.

13Extended application

ExtensionImplement an "electronic die": when the button is pressed, the LEDs blink randomly for one second, then display in binary a value between 1 and 6. So the sequence differs on every power-up, use as seed the time measured between the board's power-up and the first button press: randomSeed(millis());

Further extension: detect a long press (over 1 second) and use it to reset a roll counter, also shown in binary on the LEDs.

14Review questions

15Resources