Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Pull-Up, Pull-Down
& Floating Inputs
Why floating inputs are dangerous, the PUPDR register bit by bit, open-drain vs push-pull, and software button debouncing with real examples.
The Floating Input Problem
When a GPIO pin is configured as an input but nothing is connected to it (or a switch is open), the pin is said to be floating (also called high-impedance or tri-state). A floating pin is extremely dangerous in embedded systems.
Why Does This Happen?
A GPIO input buffer has extremely high input impedance (millions of ohms). With no definite voltage source connected, even picoamps of leakage current through nearby PCB traces create enough voltage to tip the comparator inside the buffer to either state. The behavior is completely unpredictable.
Pull-Up vs Pull-Down — The Solution
The fix is to connect a weak resistor (typically 10–100kΩ) from the pin to either VDD (pull-up) or GND (pull-down). This provides a defined “default” state when nothing drives the pin.
- No defined state when switch is open
- Reads random 0s and 1s
- Triggers false interrupts
- Higher current consumption
- Never do this intentionally
- Resistor from VDD to pin
- Default state = HIGH (1) when open
- Button connects pin to GND → reads LOW
- Active-LOW button logic
- Most common for buttons/switches
- Resistor from GND to pin
- Default state = LOW (0) when open
- Button connects pin to VDD → reads HIGH
- Active-HIGH button logic
- Used on PA0 (USER button on Discovery)
PUPDR Register — Bit-by-Bit
On STM32F407, both pull-up and pull-down resistors are built into the chip. You enable them through the GPIOx_PUPDR register without needing external components (though external resistors are better for noise immunity and power efficiency).
| PUPDRy[1:0] | Configuration | Typical Resistor Value | When to Use |
|---|---|---|---|
| 00 | No pull-up, no pull-down | N/A | Active-driven signals (UART RX from another MCU), analog inputs (always use this for ADC) |
| 01 | Pull-up enabled | ~40kΩ (internal) | Active-low buttons, I2C lines (though external 4.7kΩ preferred), unused pins |
| 10 | Pull-down enabled | ~40kΩ (internal) | Active-high signals, CAN bus recessive level, ensure LOW when not driven |
| 11 | Reserved | — | Never use — reserved by STMicroelectronics |
/* ---- Configure PA1 with internal pull-up (active-low button) ---- */
/* Step 1: Enable GPIOA clock */
RCC_AHB1ENR |= (1U << 0);
/* Step 2: Set PA1 as input (MODER bits [3:2] = 00) */
GPIOA->MODER &= ~(0x3U << 2); /* bits [3:2] = 00 */
/* Step 3: Enable pull-up on PA1 (PUPDR bits [3:2] = 01) */
GPIOA->PUPDR &= ~(0x3U << 2); /* clear bits [3:2] */
GPIOA->PUPDR |= (0x1U << 2); /* set bits [3:2] = 01 (pull-up) */
/* Reading: button NOT pressed = 1, button pressed = 0 */
uint8_t btn = (1 - (GPIOA->IDR >> 1) & 0x1U);
/* ---- Configure PA2 with pull-down ---- */
GPIOA->MODER &= ~(0x3U << 4); /* input */
GPIOA->PUPDR &= ~(0x3U << 4); /* clear */
GPIOA->PUPDR |= (0x2U << 4); /* bits [5:4] = 10 (pull-down) */
/* ---- Configure PC0 as analog (ADC) — MUST disable pull resistors ---- */
GPIOC->MODER |= (0x3U << 0); /* analog mode */
GPIOC->PUPDR &= ~(0x3U << 0); /* NO pull resistors on analog pin! */
Internal vs External Resistors
When should you use the STM32’s internal ~40kΩ pull-up/pull-down vs adding an external resistor?
| Aspect | Internal (~40kΩ) | External (1k–10kΩ) |
|---|---|---|
| Setup | Just set PUPDR register — zero components | Requires physical resistor on PCB |
| Rise/Fall time | Slow (RC with pin capacitance) — bad for fast signals | Faster with lower resistance |
| Noise immunity | Poor — high resistance means less current, more noise susceptibility | Better — stronger pullup means more stable |
| Power consumption | Very low (~80µA per pin at 3.3V) | Depends on value — 10kΩ = 330µA at 3.3V |
| Use for I2C | Only for very short buses at slow speeds | Required — 4.7kΩ pull-ups mandatory |
| Use for buttons | Fine — 40kΩ is adequate for most buttons | Better noise rejection in industrial environments |
| Best for | Prototyping, development boards, unused pins | Production PCBs, I2C, long cable runs |
Open-Drain Output with Pull-Up
Open-drain outputs (OTYPER bit = 1) are used when multiple devices share a signal line — like I2C SDA and SCL. Each device can only pull the line LOW; the pull-up resistor brings it HIGH when nobody is driving it. This is how I2C achieves wire-AND logic.
/* Configure PB6 (I2C1_SCL) and PB7 (I2C1_SDA) as open-drain outputs */
/* with internal pull-up (use external 4.7kΩ in production!) */
RCC_AHB1ENR |= (1U << 1); /* enable GPIOB clock */
/* Set as alternate function (I2C1 = AF4) */
GPIOB->MODER &= ~((0x3U<<12) | (0x3U<<14));
GPIOB->MODER |= ((0x2U<<12) | (0x2U<<14)); /* AF mode */
/* Open-drain (mandatory for I2C) */
GPIOB->OTYPER |= (1U<<6) | (1U<<7);
/* High speed for I2C fast mode (400 kHz) */
GPIOB->OSPEEDR &= ~((0x3U<<12)|(0x3U<<14));
GPIOB->OSPEEDR |= ((0x2U<<12)|(0x2U<<14)); /* Fast speed */
/* Internal pull-up (replace with external 4.7kΩ for production) */
GPIOB->PUPDR &= ~((0x3U<<12)|(0x3U<<14));
GPIOB->PUPDR |= ((0x1U<<12)|(0x1U<<14)); /* Pull-up */
/* Set alternate function to AF4 (I2C1) in AFRL */
GPIOB->AFR[0] &= ~((0xFU<<24)|(0xFU<<28)); /* clear AF for PB6,PB7 */
GPIOB->AFR[0] |= ((0x4U<<24)|(0x4U<<28)); /* AF4 = I2C1 */
Button Debouncing
A mechanical button doesn’t make clean, instant contact. When you press it, the metal contacts bounce several times in the first few milliseconds, generating rapid pulses that look like dozens of presses. You must debounce to get a single clean press event.
PRESS 0
Software Debounce — Delay Method
/* Simple delay-based debounce (polling approach) */
#define DEBOUNCE_DELAY 20 /* 20 ms debounce window */
uint8_t button_get_state(void) {
uint8_t current = (GPIOA->IDR >> 0) & 1U; /* PA0, active HIGH */
if (current == 1) { /* Button appears pressed */
delay_ms(DEBOUNCE_DELAY); /* Wait through bounce */
current = (GPIOA->IDR >> 0) & 1U;
return (current == 1); /* Confirm it's still pressed */
}
return 0;
}
Better Debounce — State Machine (Non-Blocking)
/* Non-blocking debounce using SysTick tick count */
typedef enum {
BTN_IDLE = 0,
BTN_PRESSED,
BTN_RELEASED
} BtnState;
static BtnState btn_state = BTN_IDLE;
static uint32_t btn_timestamp = 0;
/* Call this from SysTick_Handler or polling loop every 1ms */
BtnState button_update(uint32_t tick_ms) {
uint8_t raw = (GPIOA->IDR >> 0) & 1U; /* PA0 raw reading */
switch(btn_state) {
case BTN_IDLE:
if (raw == 1) { /* Potential press detected */
btn_timestamp = tick_ms;
btn_state = BTN_PRESSED;
}
break;
case BTN_PRESSED:
if (raw == 0) { /* Released during bounce window */
btn_state = BTN_IDLE; /* False press — reset */
} else if ((tick_ms - btn_timestamp) > 20) {
btn_state = BTN_RELEASED; /* Stable for 20ms — real press! */
return BTN_PRESSED;
}
break;
case BTN_RELEASED:
if (raw == 0) {
btn_state = BTN_IDLE;
return BTN_RELEASED;
}
break;
}
return BTN_IDLE;
}
Practical Configuration Guide
| Scenario | MODER | OTYPER | PUPDR | Notes |
|---|---|---|---|---|
| Button with external pull-up | 00 (Input) | N/A | 00 (None) | External resistor does the job |
| Button without resistor (pull-up in SW) | 00 (Input) | N/A | 01 (Pull-up) | Active LOW — reads 0 when pressed |
| PA0 USER button on Discovery | 00 (Input) | N/A | 00 (None) | Board has 10kΩ pull-down hardware |
| LED output (push-pull) | 01 (Output) | 0 (PP) | 00 (None) | Never pull-up/down on driven outputs |
| I2C SDA/SCL | 10 (AF) | 1 (OD) | 01 (PU) | Use external 4.7kΩ in production |
| UART TX | 10 (AF) | 0 (PP) | 00 or 01 | Some use pull-up to keep line IDLE HIGH |
| ADC input | 11 (Analog) | N/A | 00 (None) | NEVER enable pull resistors on analog! |
| Unused GPIO pin | 01 (Output) | 0 (PP) | 00 (None) | Drive to known state (LOW) to prevent floating |
Complete Code — All Button Configurations
#include <stdint.h>
typedef struct {
volatile uint32_t MODER,OTYPER,OSPEEDR,PUPDR,IDR,ODR,BSRR,LCKR;
volatile uint32_t AFR[2];
} GPIO_TypeDef;
#define GPIOA ((GPIO_TypeDef*)0x40020000UL)
#define GPIOD ((GPIO_TypeDef*)0x40020C00UL)
#define RCC_AHB1ENR (*(volatile uint32_t*)0x40023830UL)
void gpio_configure_button_pulldown(void) {
/* PA0: Input, no pull (board has hardware pull-down) */
RCC_AHB1ENR |= (1U<<0);
GPIOA->MODER &= ~(0x3U<<0); /* input */
GPIOA->PUPDR &= ~(0x3U<<0); /* no pull (00) */
}
void gpio_configure_button_sw_pullup(void) {
/* PB0: Input, software pull-up, active LOW */
RCC_AHB1ENR |= (1U<<1);
GPIOB->MODER &= ~(0x3U<<0); /* input */
GPIOB->PUPDR &= ~(0x3U<<0);
GPIOB->PUPDR |= (0x1U<<0); /* pull-up (01) */
}
int main(void) {
gpio_configure_button_pulldown(); /* PA0 with active HIGH */
/* Configure PD12 as output LED */
RCC_AHB1ENR |= (1U<<3);
GPIOD->MODER &= ~(0x3U<<24);
GPIOD->MODER |= (0x1U<<24);
GPIOD->PUPDR &= ~(0x3U<<24); /* no pull on output */
while(1) {
if ((GPIOA->IDR & (1U<<0)) != 0) {
GPIOD->BSRR = (1U<<12); /* LED ON when button held */
} else {
GPIOD->BSRR = (1U<<28); /* LED OFF when released */
}
}
}
- Floating inputs read random values — always define a pull-up or pull-down
- PUPDR register: 00=None, 01=Pull-up (~40kΩ), 10=Pull-down, 11=Reserved
- Discovery board PA0 has external hardware pull-down — set PUPDR=00
- Internal ~40kΩ is fine for buttons; use external 4.7kΩ for I2C
- Open-drain output MUST have pull-up (internal or external) to reach HIGH
- Always debounce mechanical buttons — 20ms delay or state machine
- Never enable PUPDR on analog pins (ADC/DAC) — it biases the measurement
STM32F407 Discovery Tutorial Series — Tutorial 04: Pull-Up, Pull-Down & Floating Inputs
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments