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
| Mode | PUPDR bits | Behavior |
|---|---|---|
| Floating | 00 | No pull resistor. Pin voltage undefined when disconnected. Use when external driver always provides a signal. |
| Pull-up | 01 | Internal ~40k resistor to VDD. Default HIGH, reads LOW when grounded. |
| Pull-down | 10 | Internal ~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):
| Setting | Typical max freq | Use case |
|---|---|---|
| Low | 2 MHz | LEDs, relays |
| Medium | 25 MHz | General IO |
| High | 50 MHz | SPI, UART |
| Very High | 100 MHz | SDIO, 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
| Register | Name | Purpose |
|---|---|---|
| MODER | Mode | Input/Output/AF/Analog (2 bits per pin) |
| OTYPER | Output type | Push-pull / Open-drain (1 bit per pin) |
| OSPEEDR | Speed | Slew rate (2 bits per pin) |
| PUPDR | Pull-up/down | None/Up/Down (2 bits per pin) |
| IDR | Input data | Read pin state (read-only) |
| ODR | Output data | Set pin output level |
| BSRR | Bit set/reset | Atomic set/clear (no read-modify-write needed) |
| AFR[2] | Alternate function | Select 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;
}Related
- Microcontroller Architecture — GPIO sits on the AHB bus
- Memory-Mapped IO — register access patterns for GPIO
- Interrupts and Timers — EXTI for GPIO edge/level interrupts
- UART SPI I2C — alternate function peripherals that share GPIO pins
- Transistors as Switches — what push-pull and open-drain actually mean at transistor level
- Signal Integrity — why GPIO speed settings matter