Peripherals
foundational
Weight: 4/10

GPIO

GPIO (General Purpose Input/Output) fundamentals including pin modes, push-pull vs open-drain, pull-up/pull-down resistors, interrupts, debouncing, and atomic port operations for embedded systems.

peripherals
gpio
interrupts
polling
debouncing

Quick Cap

GPIO pins are the most fundamental MCU peripheral -- configurable digital pins that let the processor read external signals (buttons, sensors) and drive outputs (LEDs, relays, enable lines). Every other peripheral (UART, SPI, I2C, PWM) ultimately uses GPIO pins in alternate function mode, which is why a solid understanding of GPIO electrical behavior underpins all embedded hardware interfacing.

Interviewers test understanding of pin modes (input/output/analog/alternate function), push-pull vs open-drain output types, pull-up/pull-down resistors, interrupt vs polling trade-offs, and debouncing strategies for mechanical switches.

Key Facts:

  • Pin modes: Input, output (push-pull / open-drain), alternate function, and analog
  • Pull resistors: Internal pull-up or pull-down prevents floating inputs; external resistors for noisy environments
  • Output types: Push-pull drives both HIGH and LOW; open-drain drives only LOW (needs external pull-up)
  • Interrupts: Edge-triggered (rising, falling, both) via the EXTI controller for event-driven input handling
  • Atomic operations: BSRR register enables glitch-free pin set/reset without read-modify-write races
  • Speed settings: Control output slew rate -- higher speed means faster edges but more EMI

Deep Dive

At a Glance

FeatureDetail
Pin modesInput, output (push-pull / open-drain), alternate function, analog
Pull resistorsInternal pull-up, pull-down, or floating (none)
Output typesPush-pull (drives both HIGH and LOW) or open-drain (drives LOW only, needs external pull-up)
Speed settingsLow, medium, high, very high (affects slew rate and EMI)
InterruptsEdge-triggered (rising, falling, both) via EXTI
Atomic operationsBSRR register for glitch-free set/reset without read-modify-write

Pin Modes

Every GPIO pin on a modern MCU can be configured into one of several modes. Understanding what each mode does electrically is essential for correct hardware interfacing.

Input mode -- The pin is configured as high-impedance. The MCU reads the external voltage level through the IDR (Input Data Register). The digital input buffer includes a Schmitt trigger that converts the analog voltage into a clean digital 0 or 1. A floating input (no pull-up or pull-down, no external connection) reads random values because the pin acts as an antenna picking up stray electromagnetic fields. Worse, the Schmitt trigger oscillates between HIGH and LOW thresholds, wasting power. Always configure a pull resistor on every input pin.

Output push-pull -- The pin actively drives both HIGH (connecting to VDD through a P-channel MOSFET) and LOW (connecting to GND through an N-channel MOSFET). This is the most common output mode -- it can source and sink current, producing fast, clean voltage transitions. Use push-pull for LEDs, chip selects, SPI signals, and any general-purpose output where the MCU is the sole driver.

Output open-drain -- The pin can only actively pull LOW (N-channel MOSFET to GND). When the output is "HIGH," the MOSFET turns off and the pin floats -- it does not drive any voltage. An external pull-up resistor is required to pull the line HIGH when no device is driving it LOW. This creates a wire-AND configuration: if any device on the line pulls LOW, the line reads LOW for everyone. Open-drain is essential for I2C (where multiple devices share SDA/SCL), level shifting between different voltage domains, and any shared bus where multiple devices may drive the same line.

Alternate function -- The pin is controlled by an on-chip peripheral (UART TX, SPI SCK, I2C SDA, PWM output) rather than the GPIO output data register. The GPIO configuration registers still control the electrical characteristics (push-pull vs open-drain, speed, pull resistors), but the peripheral controls the actual data on the pin. Each pin has a fixed set of alternate function mappings (AF0-AF15 on STM32) defined in the datasheet.

Analog mode -- The digital input buffer (Schmitt trigger) is completely disabled, and the pin connects directly to the analog circuitry (ADC input, DAC output, or comparator). This mode has the lowest power consumption of any configuration because the Schmitt trigger draws no current. Best practice: configure all unused pins as analog mode to minimize power.

Push-Pull vs Open-Drain

This distinction is one of the most commonly tested GPIO concepts in interviews. The table below captures the key differences:

FeaturePush-PullOpen-Drain
Drives HIGHYes (to VDD)No (floats)
Drives LOWYes (to GND)Yes (to GND)
External pull-up neededNoYes
Can wire-ANDNo (bus contention!)Yes
Level shiftingSame voltage onlyCan interface different voltages
Typical usesLEDs, chip select, SPI, general outputsI2C, shared buses, level shifting

The critical insight: if two push-pull outputs are connected to the same line and one drives HIGH while the other drives LOW, you get a short circuit through the two MOSFETs. With open-drain, two devices driving simultaneously is safe -- the line simply stays LOW until both release it. This is why I2C mandates open-drain and why bus protocols that allow multiple drivers always use open-drain or open-collector outputs.

Pull-Up and Pull-Down Resistors

Floating inputs are one of the most common GPIO mistakes. When a pin has no pull resistor and no external driver, the input voltage sits in the undefined zone between the Schmitt trigger's HIGH and LOW thresholds. The result: random reads, oscillating input values, and increased power consumption as the input buffer rapidly switches states.

Internal pull-ups and pull-downs are convenient but typically weak -- 20 to 50 kOhm on most STM32 devices. For noisy environments, long PCB traces, or high-speed switching, external resistors (1-10 kOhm) provide stronger pull and better noise immunity.

Common button wiring patterns:

  • Button to GND with pull-up -- Pin reads HIGH normally, LOW when pressed (active-low). This is the most common pattern because internal pull-ups are universally available.
  • Button to VDD with pull-down -- Pin reads LOW normally, HIGH when pressed (active-high). Less common because some MCUs lack internal pull-downs.
⚠️Common Trap: Floating Inputs

An unconnected GPIO input without a pull-up or pull-down reads random values and wastes power. Always configure a pull resistor on every input pin -- even unused ones. Best practice for unused pins: configure as analog mode (lowest power) or output LOW.

Polling vs Interrupt-Based Input

There are two fundamental approaches to reading GPIO inputs: continuously checking the pin state in a loop (polling) or configuring hardware to notify the CPU when a pin changes (interrupts). Each has clear trade-offs:

AspectPollingInterrupt (EXTI)
CPU usageHigh (continuous loop)Low (CPU sleeps between events)
Response timeDepends on polling rateNear-immediate (interrupt latency only)
PowerHigh (CPU always running)Low (CPU can enter sleep mode)
ComplexitySimpleRequires ISR, NVIC, EXTI setup
DebouncingEasy (sample in timer tick)Harder (must disable interrupt during bounce)
Best forFew inputs, simple systemsBattery devices, many inputs, real-time response

How EXTI works: The External Interrupt controller monitors GPIO pin edges. When a configured edge (rising, falling, or both) occurs, it generates an interrupt request to the NVIC (Nested Vectored Interrupt Controller), which dispatches the appropriate ISR. A key hardware detail: on many STM32 MCUs, EXTI lines are shared across ports. For example, PA0, PB0, PC0, and PD0 all share EXTI line 0. This means only ONE pin per EXTI line number can be used as an interrupt source at a time. If you need interrupts on both PA0 and PB0, you must use a different pin number for one of them (e.g., move one signal to PB1 and use EXTI1).

Polling is not inherently bad. In an RTOS environment, polling a button every 10 ms inside a timer callback is simple, deterministic, and naturally handles debouncing. Interrupts shine when the CPU needs to sleep between events (battery-powered devices) or when response latency must be minimized (emergency stop buttons, encoder signals).

Debouncing

When a mechanical switch closes, the metal contacts physically bounce against each other for 1-20 ms, producing multiple rapid transitions between HIGH and LOW before settling into a stable state. Without debouncing, a single button press can register as 5-50 presses.

px-2 py-1 rounded text-sm font-mono border
Ideal switch: ────────────┐
└──────────────────
Real switch: ────────┐ ┌┐ ┌─┐
│ ││ │ │
└─┘└─┘ └──────────────
| |
|← bounce →|
| (1-20 ms)|

The bounce region contains rapid, unpredictable transitions. Any edge-triggered interrupt or fast polling loop during this window will see multiple false transitions.

MethodHardware CostCPU CostEffectivenessBest For
Wait-and-resampleNoneVery lowGoodSimple projects
Timer-based (check every N ms)NoneLowVery goodRTOS / timer tick systems
RC filter + Schmitt triggerResistor + capNoneExcellentNoisy environments
Software state machineNoneMediumExcellentComplex multi-button UIs

The simplest and most reliable software approach: in a periodic timer ISR (e.g., every 10 ms), read the button state. Only register a state change when the new state has been consistent for N consecutive reads (e.g., 3 reads = 30 ms stable). This costs almost no CPU, is immune to bounce, and works with both polling and interrupt architectures. The timer tick acts as a natural low-pass filter.

For interrupt-based systems, debouncing is trickier. One common pattern: when the EXTI interrupt fires, disable the EXTI interrupt for that pin, start a one-shot timer (e.g., 20 ms), and in the timer callback, read the pin state (now stable) and re-enable the EXTI interrupt. This avoids processing the bounce transitions as separate events.

💡Interview Tip: Debouncing

When asked about debouncing, don't just describe the algorithm -- explain WHY bounce happens (physical contact mechanics), typical bounce duration (1-20 ms), and the trade-off between response time and false-trigger rejection. Then describe your preferred approach and why you chose it over alternatives.

Atomic Port Operations

Setting or clearing a single GPIO pin seems straightforward, but the naive approach -- read the output data register (ODR), modify the target bit, write it back -- is a read-modify-write (RMW) operation. If an interrupt fires between the read and the write, and the ISR modifies a different pin on the same port, one of the two changes will be lost. This is a classic race condition.

Consider: the main loop reads ODR (pins 0-15 all LOW), sets bit 3, and writes back. But between the read and write, an ISR fires and sets bit 7 via the same RMW pattern. When the main loop's write completes, it overwrites ODR with a value that has bit 3 set but bit 7 cleared -- the ISR's change is silently lost.

The solution on STM32 (and most modern MCUs): use the BSRR (Bit Set/Reset Register). Writing a 1 to bits [0:15] sets the corresponding pin. Writing a 1 to bits [16:31] clears the corresponding pin. This is a single write operation -- no read step, no modify step, no race condition, no need to disable interrupts. The hardware guarantees atomicity.

If both a set bit and a reset bit are written for the same pin in a single BSRR write, the set takes priority (per STM32 reference manual). In practice, you never do this intentionally -- you either set or clear in each write.

GPIO Speed and Power

GPIO speed settings control the output slew rate -- how fast the voltage transitions from LOW to HIGH or HIGH to LOW. Higher speed means faster rising and falling edges. Faster edges are necessary for high-frequency signals (SPI clock at 10+ MHz, for example) but produce more electromagnetic interference (EMI) and consume more dynamic power due to the higher peak currents during transitions.

The rule of thumb: use the lowest speed setting that meets your signal's timing requirements. An LED that toggles at 1 Hz does not need "very high" speed. An SPI clock running at 20 MHz does. Incorrectly setting all pins to maximum speed is a common source of EMC failures during certification testing.

Power considerations beyond speed:

  • Unused pins configured as analog mode consume the least power (Schmitt trigger disabled).
  • Floating inputs waste power because the Schmitt trigger oscillates.
  • Internal pull resistors draw a small but constant current (VDD / R_pull) whenever the pin is in the opposite state. On battery devices, this adds up across many pins.
  • Output pins driving external loads consume current proportional to the load. An LED with a 330 ohm resistor at 3.3V draws 10 mA -- multiply by 20 LEDs and that is 200 mA.

Pin Multiplexing (Alternate Functions)

Each GPIO pin can serve multiple roles through alternate function mapping. On STM32, each pin has up to 16 alternate functions (AF0 through AF15), and the mapping is fixed in hardware. You must consult the datasheet's "Alternate Function Mapping" table to determine which AF number routes which peripheral signal to which pin.

A common mistake during board bring-up: configuring the wrong AF number. For example, PA9 might be UART1_TX on AF7 and TIM1_CH2 on AF1. If you select AF1 when you intended UART, you get PWM output instead of serial data. The peripheral initializes without error, the code compiles and runs, but no serial data appears on the wire. This is a pure configuration error that no amount of software debugging will reveal -- you must verify the AF mapping against the datasheet.

When designing a PCB, pin multiplexing also constrains which peripherals can coexist. If your design needs UART1_TX on PA9 and TIM1_CH2 on PA9, you have a conflict -- you must move one of the signals to an alternative pin that offers the same peripheral on a different AF.

Debugging Story

A board bring-up engineer spent a full day debugging an I2C bus that refused to communicate. The SDA and SCL lines were stuck HIGH on the oscilloscope. The GPIO pins were confirmed as alternate function mode (I2C peripheral), and the I2C peripheral registers were correctly initialized. Slave devices were powered and functional (verified on a known-good board). The engineer checked pull-up resistors, swapped cables, and even replaced the MCU -- nothing worked.

The root cause: the GPIO output type was configured as push-pull instead of open-drain. Because I2C requires open-drain outputs with external pull-ups, the push-pull configuration meant the MCU was actively driving the lines HIGH through its internal P-channel MOSFET. When a slave device tried to pull SDA LOW for an ACK, the slave's open-drain output fought against the master's push-pull HIGH drive -- the slave could not overcome it, so ACK was never detected. Changing the output type to open-drain and verifying the 4.7k external pull-up resistors fixed the issue immediately.

The lesson: always verify that the output type (push-pull vs open-drain) matches the protocol's electrical requirements. I2C = open-drain. SPI = push-pull. Getting this wrong does not produce a compiler error or a configuration warning -- it produces a "doesn't work on the bus" mystery that can waste hours.

What interviewers want to hear is that you understand GPIO at the electrical level, not just the register level. You should be able to explain why open-drain exists (wire-AND, level shifting), why floating inputs are dangerous (random reads, wasted power), why BSRR exists (atomic operations, race condition prevention), and why debouncing matters (physical contact bounce). Interviewers want to see that you think about the hardware behavior behind the software configuration -- that you can predict what happens on the wire when you write a value to a register, and that you can reason about failure modes (bus contention, race conditions, EMI) rather than just writing code that works on the bench.

Interview Focus

Classic GPIO Interview Questions

Q1: "What is the difference between push-pull and open-drain GPIO outputs?"

Model Answer Starter: "Push-pull uses two MOSFETs to actively drive both HIGH and LOW. Open-drain uses only the N-channel MOSFET, so it can pull LOW but floats when 'HIGH' -- you need an external pull-up resistor. The key advantage of open-drain is wire-AND behavior: multiple devices can safely share a line because no device ever drives HIGH. This is why I2C uses open-drain. Open-drain also enables level shifting -- the pull-up can be connected to a different voltage rail than the MCU's VDD. Push-pull is simpler and faster for point-to-point signals like SPI or LED control where only one device drives the line."

Q2: "How do you handle debouncing for a mechanical button?"

Model Answer Starter: "Mechanical contacts bounce for 1-20 ms when closing, producing multiple rapid transitions. My preferred approach is timer-based sampling: I read the button state every 10 ms in a periodic timer callback and only register a state change after 3 consecutive consistent reads (30 ms stable). This is simple, deterministic, and works well in RTOS environments. For interrupt-driven systems, I disable the EXTI interrupt on the first edge, start a one-shot timer, and re-read the pin in the timer callback after the bounce window has passed. Hardware debouncing with an RC filter and Schmitt trigger is best when CPU budget is zero or the environment is electrically noisy."

Q3: "When would you use polling vs interrupts for reading GPIO inputs?"

Model Answer Starter: "I use polling when the system is already running a main loop with spare cycles, the input rate is predictable, and simplicity matters -- for example, reading a few buttons every 10 ms in a timer tick. I switch to interrupts when the CPU needs to sleep between events (battery-powered devices), when response latency must be minimal (emergency stop, rotary encoder), or when there are many inputs that would waste cycles to poll continuously. The trade-off is complexity: interrupts require EXTI configuration, NVIC priority management, and careful debouncing to avoid processing bounce edges."

Q4: "What happens if you leave a GPIO input floating, and how do you prevent it?"

Model Answer Starter: "A floating input has no defined voltage -- it picks up stray electromagnetic fields and the Schmitt trigger oscillates between HIGH and LOW thresholds, producing random reads and wasting power. I prevent it by always enabling an internal pull-up or pull-down on every input pin. For unused pins, I configure them as analog mode, which disables the Schmitt trigger entirely and draws the least power. In safety-critical systems, I also verify pull resistor configuration during a startup self-test by reading each pin and confirming the expected idle state."

Q5: "Why should you use BSRR instead of directly writing to ODR?"

Model Answer Starter: "Writing to ODR requires a read-modify-write sequence: read the current port value, change one bit, write it back. If an interrupt fires between the read and write and the ISR modifies a different pin on the same port via the same RMW pattern, one change is lost -- a classic race condition. BSRR is a write-only register where each bit position corresponds to a set or reset action. A single write atomically sets or clears the target pin without reading or affecting other pins. No critical section needed, no interrupt disable needed, no race condition possible."

Trap Alerts

  • Don't say: "GPIO is just setting pins HIGH or LOW" -- interviewers want to hear about electrical behavior (push-pull vs open-drain, pull resistors, drive strength, slew rate)
  • Don't forget: That floating inputs are a real problem -- always explain the consequence (random reads, power waste) and the fix (pull-up, pull-down, or analog mode)
  • Don't ignore: The race condition in read-modify-write port access -- mentioning BSRR and explaining WHY it exists demonstrates systems-level thinking

Follow-up Questions

  • "How would you design a GPIO driver that supports both polling and interrupt modes with runtime switching?"
  • "What considerations apply when using GPIO pins for level shifting between 3.3V and 5V domains?"
  • "How do you handle GPIO pin configuration in a low-power sleep mode, and what happens to pin states when the MCU wakes up?"
  • "If two peripherals need the same GPIO pin, how do you resolve the conflict at the hardware and software level?"
💡Practice GPIO Interview Questions

Ready to test yourself? Head over to the GPIO Interview Questions page for a full set of Q&A with collapsible answers -- perfect for self-study and mock interview practice.

Practice

What is the main advantage of open-drain output over push-pull?

Why should unused GPIO pins be configured as analog mode?

What problem does the BSRR register solve that ODR does not?

What is the typical bounce duration for a mechanical switch?

On STM32, EXTI line 0 is shared by PA0, PB0, PC0, and PD0. What constraint does this create?

Real-World Tie-In

Battery-Powered IoT Sensor Node -- A wearable health monitor used GPIO interrupts to wake the MCU from deep sleep when the user pressed a button. All unused pins were configured as analog mode, and the single active button input used an internal pull-up with timer-based debouncing in the wakeup handler. This configuration reduced idle current from 1.2 mA (all pins floating in input mode) to 8 uA, extending battery life from 2 weeks to over 6 months.

Automotive Dashboard Controller -- A dashboard module controlled 32 LED indicators and read 16 mechanical buttons across two GPIO ports. The design used BSRR for all LED updates to avoid race conditions between the main display loop and the CAN-receive ISR (which could update warning LEDs at any time). Buttons were sampled every 10 ms in a timer tick with 3-sample debouncing, providing reliable operation despite engine-generated EMI that caused false edges on direct EXTI interrupts.

Industrial Motor Controller with Mixed Voltage Domains -- A motor drive board needed to interface a 3.3V MCU with 5V logic sensors and a 24V relay driver. Open-drain GPIO outputs with external pull-ups to 5V provided level-shifted enable signals to the sensor board. The relay driver used a push-pull output through an optocoupler for galvanic isolation. Choosing the correct output type for each interface voltage domain prevented the level-mismatch damage that had destroyed two prototype boards during initial bring-up.