GPIO and Digital IO

General-Purpose Input/Output — the most basic peripheral on any MCU. Each GPIO pin can be configured as a digital input or output, and most pins have alternate functions (UART TX, SPI clock, PWM output, etc.) selected through a multiplexer.

Why It Matters

GPIO is how the MCU touches the physical world. LEDs, buttons, relays, chip-select lines, bit-banged protocols — all GPIO. Understanding output modes, pull resistors, and alternate functions is prerequisite to using every other peripheral.

How It Works

Pin Multiplexing

Each physical pin connects to multiple internal peripherals through a mux. Only one function is active at a time.

                   ┌─────────┐
  Physical Pin <---┤   MUX   │<-- GPIO output
                   │         │<-- UART TX (AF7)
                   │         │<-- SPI MOSI (AF5)
                   │         │<-- TIM PWM (AF1)
                   └─────────┘
                     ^
               AFR register
              selects which AF

On STM32, the GPIOx->AFR[0] (pins 0-7) and AFR[1] (pins 8-15) registers select the alternate function. Each pin gets 4 bits (AF0-AF15).

Output Modes

Push-Pull:                    Open-Drain:
  VDD                           VDD
   |                             |
  [P-FET] -- ON for HIGH       [pull-up R]
   |                             |
   +---- Pin                     +---- Pin
   |                             |
  [N-FET] -- ON for LOW        [N-FET] -- ON for LOW
   |                             |
  GND                           GND

  Can drive HIGH and LOW        Can only pull LOW
  Most common mode              HIGH = floating (needs pull-up)
                                Used for I2C, level shifting,
                                wired-OR buses

Push-pull: actively drives both HIGH and LOW. Default for most outputs (LEDs, SPI, UART TX).

Open-drain: only pulls LOW; HIGH state is floating. Requires external or internal pull-up resistor. Essential for I2C (SDA/SCL are open-drain by spec) and any shared bus where multiple devices must drive the same line.

Input Modes

ModePUPDR bitsBehavior
Floating00No pull resistor. Pin voltage undefined when disconnected. Use when external driver always provides a signal.
Pull-up01Internal ~40k resistor to VDD. Default HIGH, reads LOW when grounded.
Pull-down10Internal ~40k resistor to GND. Default LOW, reads HIGH when driven.

For buttons, always use a pull-up or pull-down. Floating inputs pick up noise and read randomly.

GPIO Speed Setting

The OSPEEDR register controls output slew rate (how fast the pin transitions):

SettingTypical max freqUse case
Low2 MHzLEDs, relays
Medium25 MHzGeneral IO
High50 MHzSPI, UART
Very High100 MHzSDIO, high-speed SPI

Higher speed = faster edges = more EMI and power draw. Use the lowest speed that meets your timing requirement. Relates to Signal Integrity.

STM32 GPIO Registers

RegisterNamePurpose
MODERModeInput/Output/AF/Analog (2 bits per pin)
OTYPEROutput typePush-pull / Open-drain (1 bit per pin)
OSPEEDRSpeedSlew rate (2 bits per pin)
PUPDRPull-up/downNone/Up/Down (2 bits per pin)
IDRInput dataRead pin state (read-only)
ODROutput dataSet pin output level
BSRRBit set/resetAtomic set/clear (no read-modify-write needed)
AFR[2]Alternate functionSelect AF0-AF15 (4 bits per pin)

Code Example

LED on PA5 (output) + button on PA0 (input with pull-up, active low):

// Enable GPIOA clock
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
 
// PA5: output, push-pull, low speed
GPIOA->MODER  &= ~(3 << (5 * 2));
GPIOA->MODER  |=  (1 << (5 * 2));   // 01 = output
GPIOA->OTYPER &= ~(1 << 5);         // 0  = push-pull
GPIOA->OSPEEDR &= ~(3 << (5 * 2));  // 00 = low speed
 
// PA0: input with pull-up
GPIOA->MODER  &= ~(3 << (0 * 2));   // 00 = input
GPIOA->PUPDR  &= ~(3 << (0 * 2));
GPIOA->PUPDR  |=  (1 << (0 * 2));   // 01 = pull-up
 
// Toggle LED when button pressed
while (1) {
    if (!(GPIOA->IDR & (1 << 0))) {        // button grounds PA0
        GPIOA->BSRR = (1 << 5);            // set PA5 (LED on)
    } else {
        GPIOA->BSRR = (1 << (5 + 16));     // reset PA5 (LED off)
    }
}

Note: BSRR is preferred over ODR |= because it is atomic — bits 0-15 set, bits 16-31 clear. No read-modify-write race condition with ISRs.

Software Debounce

Mechanical buttons bounce for 1-10ms, producing multiple edges on a single press:

uint8_t read_button(void) {
    static uint32_t last_time = 0;
    uint32_t now = HAL_GetTick();
    if (now - last_time < 50) return 0;     // 50ms debounce window
    if (!(GPIOA->IDR & (1 << 0))) {         // active low
        last_time = now;
        return 1;
    }
    return 0;
}