A GPIO is a general-purpose pin that we can configure as an input or output to interact with the outside world. Behind something that looks like a simple pin, there are actually transistors, multiplexers, and configuration registers.
Instead of sticking to Arduino’s simple digitalWrite, let’s look at what really happens inside the silicon. This approach works whether you program an ATmega in C, an STM32, or an ESP32.
In this article, we will momentarily ignore Arduino-specific functions (digitalWrite) to understand the universal architecture of input and output ports, applicable to AVR, PIC, STM32, or ESP32.
The digital GPIO: what’s inside?
A digital pin is not a simple direct wire to the CPU. It is a configurable peripheral circuit that acts as an intermediary.
Output mode
When we configure a pin as an output, the microcontroller connects the physical pin to the internal power rails (Vcc or GND) using transistors (MOSFETs).
There are two fundamental output topologies you should know:
Push-pull (totem pole)
This is the standard configuration. We use two transistors: one connected to Vcc (PMOS) and another to GND (NMOS).
- To output a 1 (HIGH): The top transistor (Push) is activated. The pin sources current from Vcc.
- To output a 0 (LOW): The bottom transistor (Pull) is activated. The pin sinks current to GND.
- Advantage: It is fast and can drive small loads, like an LED with its resistor, within the pin’s current limits.
Open-drain
Here we only use the bottom transistor (the one connected to GND). The top one does not exist (or is disabled).
- To output a 0: The transistor connects the pin to GND.
- To output a 1: The transistor turns off. The pin is left “floating” (disconnected).
What is this for? It is the basis for buses like I2C and allows setting the high level in the appropriate voltage domain. We need a pull-up resistor so that when the transistor turns off, the line rises to that level.
Input mode and High Impedance (Hi-Z)
When we configure a pin as an input, we disconnect the output transistors. The pin enters a High Impedance (Hi-Z) state.
Electrically, it is as if the pin has an enormous resistance to the external circuit (in the order of megaohms or more). It “listens” to the voltage without drawing almost any current and without altering the circuit.
The ADC: digitizing reality
The real world is analog. For the CPU to understand a temperature or a position, we need the ADC (Analog-to-Digital Converter).
Many microcontrollers use a SAR (Successive Approximation Register) ADC. It works like a balance scale trying different weights to estimate the value.
Resolution (bits)
The ADC divides the input voltage into discrete steps.
- 10 bits (Arduino Uno): 2¹⁰ levels (0 to 1023).
- 12 bits (ESP32 / STM32): 2¹² levels (0 to 4095).
The reference voltage ( )
This is the critical concept. The ADC does not measure absolute volts; it measures a proportion relative to its reference.
If your
- Reading 0 → 0V.
- Reading 2048 → 1.65V.
- Reading 4095 → 3.3V.
If you power your microcontroller from a noisy source (a poor-quality USB) and use that same supply as
PWM: the poor man’s DAC
Most microcontrollers do NOT have a real DAC (Digital-to-Analog Converter). They cannot output a pure 2.5V from a pin. They can only output 0V or Vcc.
To simulate analog outputs (dimming an LED, controlling a motor), we use PWM (Pulse Width Modulation).
It consists of generating a square wave at a fixed frequency, varying the Duty Cycle:
- 0%: Always off (0V average).
- 50%: Half on, half off (Vcc/2 average).
- 100%: Always on (Vcc average).
PWM frequency and resolution
Here we also configure hardware registers (Timers):
-
Frequency: How many times per second does it switch?
- Low (50Hz): Ideal for Servomotors.
- Medium (1kHz - 20kHz): DC motors and LEDs.
- High (>20kHz): Inaudible to humans (to avoid motor whine).
-
Resolution: How many width steps we can define (8 bits = 255 steps).
If you connect a PWM signal to an RC filter (Resistor + Capacitor), you can obtain a real analog voltage. The capacitor averages the pulses.