JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Talking to the World: GPIO, Digital I/O and PWM

In rung 1 you made a pin blink. But a blinking pin is a brain trapped in a box — it can only twitch one finger. This time we wire that finger up to the real world: read a button, light an LED, slam a relay, then learn the magician's trick that lets a chip with only two voltages — on and off — dim a lamp or spin a motor at exactly half speed.

One pin, two personalities

Look at a microcontroller under a magnifying glass and you'll see it bristling with metal legs — a 28-pin AVR, the 40-pin header of a Raspberry Pi, the dozens of pads ringing an ESP32. Most of those legs are GPIO pins: *general-purpose input/output*. The word general-purpose is the whole point. Each pin is a tiny chameleon you configure in firmware, and it has two personalities. As an output, the pin becomes a switch the CPU can flip — drive it HIGH to push out the supply voltage (say 3.3 V), or LOW to pull it to 0 V. As an input, the same pin becomes an ear: it stops driving and instead *listens*, reporting back whether the outside world is holding it near 3.3 V (a logic 1) or near 0 V (a logic 0).

Inside the silicon, that personality switch is real hardware. A *data-direction register* — one bit per pin — decides whether an output driver (a pair of transistors that can yank the pin to the rail) is connected, or whether instead a high-impedance input buffer is reading the pin's voltage. Setting that one bit is the difference between the pin *shouting* and the pin *listening*. Get the direction wrong and your button reads gibberish, or worse, your LED never lights.

Output: lighting an LED, slamming a relay

The classic first output is an LED — which is just a diode that glows when current flows the right way through it. But you can't wire an LED straight from pin to ground. An LED has almost no internal resistance; once it 'turns on' near ~2 V it will gulp as much current as the pin can deliver, frying both the LED and the pin's output driver. The fix is a humble series resistor, and you size it with Ohm's law.

  3.3 V ──[ pin ]──►├── LED (Vf ≈ 2.0 V) ──[ R ]── GND
                     (anode)              (resistor)

  Voltage left for the resistor:
    V_R = V_supply − V_f = 3.3 V − 2.0 V = 1.3 V

  Pick a safe LED current, say I = 10 mA = 0.010 A:
    R = V_R / I = 1.3 V / 0.010 A = 130 Ω

  Nearest standard value: 150 Ω  →  I ≈ 1.3 / 150 ≈ 8.7 mA  (safe)
Sizing an LED's series resistor with Ohm's law. The resistor 'eats' the leftover voltage and sets the current; without it, the LED draws whatever it can and dies.

Now scale up. A relay coil, a motor, or a string of LEDs wants far more current than a GPIO pin can safely source — a pin is typically rated for only 20–40 mA, while a small relay coil might want 70 mA and a motor hundreds. Ask the pin for that and you'll cook it. So the pin doesn't *do* the heavy lifting; it *commands* it. The GPIO pin drives the gate of a MOSFET or the base of a transistor, and that transistor — a power-handling muscle — switches the real load. The pin is the trigger finger; the transistor is the trigger.

Input: reading a button without reading noise

Reading a switch sounds trivial: wire a button between the pin and ground, press it, the pin goes LOW. But what is the pin reading when the button is *not* pressed? Nothing is connected to it. A high-impedance input pin left dangling is called floating, and it's a genuine antenna — it picks up stray fields from the mains, from your hand, from a nearby wire, and flickers randomly between 0 and 1. Your code sees ghost button presses. This is one of the most common beginner bugs in all of embedded systems.

The cure is a pull resistor — a resistor that gently ties the pin to a known voltage when nothing else is driving it. A *pull-up* (to 3.3 V) parks the idle pin at logic 1; pressing a button to ground then yanks it firmly to 0. A *pull-down* (to GND) parks it at 0; a button to 3.3 V drives it to 1. The resistor is large — typically 10 kΩ — so when the button *is* pressed, only a trickle of current (3.3 V / 10 kΩ ≈ 0.33 mA) wastes through it, but it's small enough to overpower the picofarads of stray pickup. So convenient is this trick that nearly every modern MCU has *internal* pull-ups you enable with a single register bit — no external part needed.

  Pull-up configuration (button reads LOW when pressed):

     3.3 V
       │
      ┌┴┐  R_pull = 10 kΩ
      │ │
      └┬┘
       ├────────────►  to GPIO input pin  (reads 1 when idle)
       │
      _|_  push-button
       o
       │
      GND                (pressing pulls the pin to 0)
A pull-up resistor keeps the input at a definite logic 1 until the button shorts it to ground. Flip the supply and ground for a pull-down.

PWM: faking analog from a digital pin

Here's the puzzle that PWM solves. A GPIO output pin can only do two things: full on (3.3 V) or full off (0 V). There is no '1.65 V, please.' So how do you dim an LED to half brightness, or run a motor at 30% speed? You'd think you need a digital-to-analog converter to synthesize an in-between voltage. But there's a cheaper, almost cheeky trick: don't make a half voltage — make a full voltage half the time. Switch the pin on and off so fast that the LED, the motor, or your eye can't keep up, and what they *feel* is the average.

That's pulse-width modulation. The pin pumps out a steady train of square pulses at a fixed frequency, and the one knob you turn is the [[ee-duty-cycle|duty cycle]] — the fraction of each cycle the pin spends HIGH. 0% duty is always-off; 100% is always-on; 50% is on exactly half the time. Crucially, the *frequency* stays fixed (often a few hundred Hz to tens of kHz) — you're only sliding the width of the 'on' pulse wider or narrower. Hence *pulse-width modulation*.

  Three duty cycles, same frequency. ─ = HIGH (3.3 V), _ = LOW (0 V)

  25%   ──________──________──________   avg ≈ 0.25 × 3.3 V = 0.83 V

  50%   ────____────____────____         avg ≈ 0.50 × 3.3 V = 1.65 V

  75%   ──────__──────__──────__         avg ≈ 0.75 × 3.3 V = 2.48 V

        |←  one period T  →|
           (e.g. T = 1 ms  →  frequency = 1 kHz)
Same period, different pulse widths. The dotted average is what a slow load — your eye, an LED, a motor's inertia — actually perceives.

Why does the *average* matter? Because every real load is a low-pass filter in disguise. Your eye's persistence of vision blurs flicker faster than ~60–90 Hz into a steady glow, so an LED winking at 1 kHz with 25% duty simply looks dimmer. A motor's mechanical inertia can't react to a 20 kHz switch; it responds to the average torque, so it spins slower. Add an actual capacitor and inductor (an LC filter) and the square wave smooths into a genuine DC voltage — which, incidentally, is exactly how a buck converter turns PWM into an efficient adjustable power supply.

Working the numbers: dimming and servos

Let's make the average-voltage idea concrete. The duty cycle D is just the ratio of on-time to the full period, and for a load that responds to the average, the effective voltage is simply D times the supply. The arithmetic is refreshingly simple — no calculus, just a fraction.

  Duty cycle:    D = t_on / T          (T = t_on + t_off)
  Average volts: V_avg = D × V_supply

  Example — dim a 3.3 V LED to a quarter brightness:
    Let T = 1 ms (frequency f = 1/T = 1 kHz)
    Want D = 25%  →  t_on = 0.25 × 1 ms = 0.25 ms,  t_off = 0.75 ms
    V_avg = 0.25 × 3.3 V = 0.825 V   (the LED 'sees' a quarter)

  Example — set the speed of a hobby servo (different convention!):
    Servos read the ABSOLUTE pulse WIDTH, not the duty ratio.
    Frame period T = 20 ms (f = 50 Hz), and:
        1.0 ms pulse  →  full counter-clockwise (0°)
        1.5 ms pulse  →  centre (90°)
        2.0 ms pulse  →  full clockwise (180°)
    Here a 1.5 ms / 20 ms = 7.5% duty commands the centre position.
Two faces of PWM. For an LED or motor, the duty ratio sets the average. For an RC servo, the absolute high-time encodes an angle — same waveform, different meaning.

Notice the lovely twist in that second example. A hobby servo doesn't care about the *ratio* at all — it measures the literal width of each HIGH pulse and turns its shaft to a matching angle. Feed it 1.5 ms and it points dead centre; 1.0 ms and it swings hard one way. The 50 Hz frame rate is just a regular heartbeat that says 'here comes another command.' Same PWM hardware, same square wave — but now the *width itself* is the message, not the average voltage. This is exactly how the steering and throttle of nearly every RC car, drone, and robot arm are controlled.

  1. Pick a PWM frequency high enough that the load can't see individual pulses: a few hundred Hz hides LED flicker from the eye; 20+ kHz pushes motor whine above human hearing.
  2. Choose the duty cycle for the average you want: V_avg = D × V_supply for brightness/speed, or an absolute pulse width for a servo angle.
  3. Write the period and duty into the timer/counter registers and let the peripheral generate the pulse train autonomously.
  4. Drive heavy loads through a transistor, never the pin directly — and add a flyback diode for any coil or motor.

From blink to a sensing, acting machine

Step back and see how far one pin has carried us. In rung 1 the blink was a monologue — the chip talking to itself. Now the same pins listen (a debounced button through a pull-up), speak (an LED through a current-limiting resistor), command muscle (a relay or motor through a transistor), and even *modulate* — conjuring a smooth continuum of brightness, speed, and angle out of nothing but on and off, switched faster than the world can notice.

That is the whole soul of embedded systems in miniature: a digital brain reaching out through humble pins to feel and shape an analog world. Everything ahead — reading sensors over I²C and SPI, reacting in microseconds via interrupts, juggling many jobs under an RTOS — is built on exactly this foundation: knowing, with confidence, what each pin is doing and why.