5.5.2 · D3Embedded Systems & Real-Time Software

Worked examples — GPIO — input - output, pull-up - pull-down, interrupt on pin change

3,416 words16 min readBack to topic

Before anything else, three plain-word reminders so no symbol is used unexplained:


The scenario matrix

Every GPIO exercise falls into one of these cells. The examples below are labelled with the cell they cover, so together they hit all of them.

Cell Axis The case Example
A Logic level Pull-up + open switch → reads HIGH Ex 1
B Logic level Pull-up + pressed switch → reads LOW (active-LOW) Ex 1
C Logic level Pull-down + pressed-to- → reads HIGH (active-HIGH) Ex 2
D Degenerate No pull at all → floating → undefined read Ex 3
E Sizing Choose to cap wasted current (normal range) Ex 4
F Limiting small (few ) → huge current, wasted power Ex 4
G Limiting large (M) → noise wins → flaky Ex 4
H Fight/divider Strong driver LOW vs pull-up → who wins? (voltage divider) Ex 5
I Timing Bounce → count spurious interrupts; debounce window Ex 6
J Word problem Real device: current budget over many pins Ex 7
K Exam twist Pin set as OUTPUT then read → returns written value, not button Ex 8

Example 1 — Cells A & B: pull-up + active-LOW button

The figure below draws the same circuit twice — open on the left, pressed on the right. Trace the wire from (lavender rail at top) down through the butter-yellow resistor to the black "pin" dot, then to the button. Watch the coloured badge at the bottom flip from mint HIGH to coral LOW.

Figure — GPIO — input - output, pull-up - pull-down, interrupt on pin change
  1. Open button, trace the current. In the left circuit the only path from the pin is up through to ; the button's lever is lifted (open), so nothing connects to . The sense-buffer (defined above) draws essentially zero current. Why this step? Ohm's law says the voltage dropped across is . If , then , so no voltage is lost across the resistor.
  2. Conclude the level. With no drop across , the pin sits at HIGH = 1. The left badge is mint. (Cell A)
  3. Pressed button, trace the current. In the right circuit the button (coral bar) shorts the pin straight to (a near-zero-ohm path). The pin is pinned to . Why this step? A hard ground connection dominates the weak pull-up completely — see the divider argument in Ex 5.
  4. Conclude the level. Pin at LOW = 0. The right badge is coral. (Cell B)

So pressed reads 0 — this is what "active-LOW" means. See Switch Debouncing for what happens during the press.

Recall Check yourself

With pull-up + button-to-GND, the OPEN state reads what? ::: HIGH (1) With the same wiring, PRESSED reads what? ::: LOW (0), i.e. active-LOW

Verify: open → (HIGH). Pressed → (LOW). ✓


Example 2 — Cell C: pull-down + active-HIGH button

  1. Open button. Only path is down through to . Sense-buffer draws ~0, so no drop across → pin sits at LOW = 0. Why this step? Same "no current ⇒ no voltage lost" logic as Ex 1, but the resistor now ties us to ground instead of supply.
  2. Pressed button. Button shorts the pin to → pin at HIGH = 1. (Cell C) Why this step? A direct low-resistance connection to overwhelms the weak pull-down — the same "hard connection wins over a weak pull" idea as Ex 1's pressed case, just pointing up instead of down. We must show this half explicitly so the reader sees pressed is genuinely HIGH, not merely assumed by symmetry.

This wiring is active-HIGH (pressed = 1). It reads more naturally but is used less because most MCUs bundle pull-ups and grounding is electrically cleaner (Ex 1's reason).

Verify: open → (LOW); pressed → (HIGH). Levels are exactly swapped versus Ex 1. ✓


Example 3 — Cell D: the floating (degenerate) input

Before the example, one new idea we need:

The figure plots the floating pin's voltage over time (lavender wiggle). The coral dashed line is the sense-buffer's decision threshold at ; every coral dot marks where the wandering voltage crosses it and the read flips between "reads 1" (mint zone, top) and "reads 0" (bottom).

Figure — GPIO — input - output, pull-up - pull-down, interrupt on pin change
  1. Model the pin. An undriven input is that tiny capacitor (a few pF, defined just above) attached to a super-high-impedance sense-buffer. No resistor ties it anywhere. Why this step? Without a defined path, Ohm's law has no fixed to give us — and with node capacitance holding charge, whatever voltage lands there simply sticks. The node is electrically "orphaned".
  2. What sets the voltage then? Stray coupling: nearby switching wires, your finger's capacitance, 50/60 Hz mains hum. These push tiny charges onto , and by the voltage wanders anywhere between and (the lavender curve).
  3. Read result. Whenever the wandering voltage crosses the buffer's threshold (≈ , the coral line), the logic value flips. So the read is undefined / random — sometimes 0, sometimes 1. (Cell D)

This is the #1 beginner bug ("button reads pressed when I don't touch it"). The cure is any pull resistor, internal or external.

Recall Degenerate case

A floating input reads ::: neither a stable 0 nor 1 — it is undefined and picks up noise.

Verify (conceptual): no pull means no deterministic ; the outcome is not a single value, so any code branching on it is a bug. (Nothing numeric to check — the point is that no number exists.)


Example 4 — Cells E, F, G: sizing (normal + both limits)

  1. Set up the current (Cell E). When pressed, current flows . By Ohm's law . Why this step? This is the only current path in the pressed state; capping it caps wasted power.
  2. Solve for . We want , so Choose 33 k–47 k. (Cell E) Why this step? We know and the current budget , but the unknown is . Ohm's law has on the bottom, so we algebraically flip it — multiply both sides by , divide by — to get , isolating the quantity we actually get to choose. And because a bigger gives a smaller , the "" on current becomes "" on .
  3. Limit F — (too small). That is 150× your budget — the button held down dumps 15 mA to ground forever. Bad for battery. (Cell F)
  4. Limit G — (too big). Barely any waste — but so weak that pico-amp noise currents shift the pin: the "override" is feeble, so the read goes flaky. (Cell G)

Verify: (a) ✓. (b) ✓. (c) ✓.


Example 5 — Cell H: the fight (voltage divider)

The figure stacks the two resistors: butter-yellow on top from , mint on the bottom to , with the coral "pin (tap)" dot squeezed between them. The coral current arrow shows the single current running top-to-bottom.

Figure — GPIO — input - output, pull-up - pull-down, interrupt on pin change
  1. See it as a voltage divider. Current runs (pin) . Two resistors in series across ; the pin is the tap between them. Why this step? Both paths conduct simultaneously, so the pin can't be exactly or — it's the divider point. The divider formula tells us where.
  2. Apply the divider. The tap voltage equals supply times the bottom resistor over the total: Why this form? The voltage splits in proportion to resistance; the bottom leg's share is the pin's voltage above ground.
  3. Crunch it. Why this step? We need the actual number, not just the formula, because the whole question is "is this near or near ?" Only by evaluating the fraction (tiny bottom resistor over huge total ⇒ tiny fraction) do we see the pin lands microscopically above ground.
  4. Conclude. → the pin reads LOW. The strong (5 Ω) driver wins overwhelmingly against the weak (33 kΩ) pull-up. (Cell H) Why this step? A logic input decides "LOW" or "HIGH" by comparing to a threshold (for 3.3 V parts, anything below ≈ is a guaranteed LOW). Since is ~2000× below that threshold, there is zero ambiguity — this is why weak pulls are safe: a real driver always crushes them.

Verify: , safely below any LOW threshold (~ for 3.3 V logic). ✓


Example 6 — Cell I: bounce and the debounce window

The figure shows the bouncing pin voltage (lavender) dropping to LOW; each coral down-triangle marks a raw falling edge, the butter-outlined triangle at is the one accepted press, and the mint band is the 20 ms lockout that swallows all the later edges.

Figure — GPIO — input - output, pull-up - pull-down, interrupt on pin change
  1. Count raw edges. Five listed edges → 5 ISR calls with no debounce. Each spurious call re-runs pressed=true — the app thinks the button was hit 5 times. (Cell I) Why this step? Hardware fires on every electrical transition; it can't tell "real press" from "contact chatter".
  2. Apply the 20 ms lockout. Accept edge at ; start a 20 ms window . Edges at 1.5, 3, 4.5, 7 all fall inside → ignored. Next accept would need ; there is none. Why this step? All bounce happens within a few ms; a 20 ms window comfortably swallows the whole burst while still feeling instant to a human.
  3. Conclude. With debounce: 1 accepted press. Correct! ✓

See Switch Debouncing for RC-filter and state-machine variants, and Interrupts & ISR Design for keeping the ISR short so it never misses the first edge.

Verify: raw count ; edges inside ms (4 suppressed), accepted . ✓


Example 7 — Cell J: real-world current budget (word problem)

  1. Per-button current. Pressed = pin grounded through button, so . Why this step? Each pull-up is its own independent path when its button is down.
  2. Total. Eight identical paths in parallel add up: . (Cell J) Why this step? Parallel current sources sum; they don't share a resistor.
  3. Battery life from this leak. Capacity current: days. Why this step? mAh mA cancels to hours — a direct units check that we set it up right.

So even a "wasteful" all-pressed case is ~2.6 mA — usually fine, but on a coin cell you'd bump to 47 k to cut it ~5×.

Verify: ✓; ✓; life ✓.


Example 8 — Cell K: the exam twist (read an output pin)

  1. Recall push-pull output. As OUTPUT-HIGH, the top transistor is ON, hard-connecting the pin to through only a few ohms. See Push-Pull vs Open-Drain Outputs. Why this step? An output pin is actively driving, not listening — so unlike an input, it is not a passive sense-buffer waiting to be told a level.
  2. Button pressed = fight, but the driver wins. The button's path to GND has some resistance (say ) versus the driver's . This is a voltage divider exactly like Ex 5, but now the strong driver sits on top (to ) and the button's larger resistance is on the bottom: Why this step? is far above the HIGH threshold (~ for 3.3 V logic), so even if you could sense the wire, it still reads 1, not the 0 you hoped a press would give. The strong driver refuses to be pulled down by a mere button.
  3. The deeper read-path point. On many MCUs, reading a push-pull output returns the written value straight from the output register, not the physical sense at all — so read(pin2) returns 1 regardless of the button. Either mechanism gives the same wrong answer: the press is invisible. (Cell K)
  4. Fix. Set the pin to INPUT_PULLUP before sensing. Then idle = HIGH, pressed = LOW (Ex 1). Bonus reason: as configured, pressing also shorts a strong driver straight to GND through the button — a fault current that can damage the pin.

Verify: divider (HIGH, above ~ threshold) ✓ — the press is never seen as 0.


Recall Which cells did we cover?

A–K, all eleven — levels (A,B,C), degenerate float (D), sizing + both limits (E,F,G), the divider fight (H), timing/bounce (I), word-problem current budget (J), and the output-read exam trap (K). ::: ✓ every cell has at least one worked example.

Related deep dives: Polling vs Interrupt-Driven I/O · volatile & Memory-Mapped Registers · Interrupts & ISR Design.