Microcontroller Peripherals
Learning Objectives
- Explain what a peripheral is and why MCUs offload work to dedicated hardware instead of the CPU
- Configure and use GPIO pins in input and output modes, including pull-up resistors
- Explain how an ADC converts an analog voltage to a digital value and calculate real voltage from a raw reading
- Generate a PWM signal using a hardware timer and explain duty cycle
- Compare UART, I2C, and SPI communication interfaces by wiring, speed, and addressing
- Read a timer/PWM register configuration and predict the resulting signal frequency
Quick Answer
Peripherals are dedicated hardware blocks built into a microcontroller that handle specific jobs — reading analog voltages, generating precise timing, or talking to other chips — without tying up the CPU. Instead of writing software loops to measure voltage or generate exact pulse timing (which would be slow and imprecise), the CPU just configures the peripheral's registers once and lets dedicated hardware do the real work in parallel, often notifying the CPU only when it's done via an interrupt. This matters because virtually everything a microcontroller does in the real world — reading a sensor, driving a motor, talking over Wi-Fi, displaying on a screen — goes through one of five peripheral types: GPIO, ADC, DAC, timers/PWM, and serial communication interfaces (UART, I2C, SPI).
Why Peripherals Exist
Imagine trying to generate a perfectly timed 50 kHz square wave using only software loops that toggle a pin — you'd need to count CPU cycles exactly, and any interrupt or instruction-timing variation would throw off your timing. Now imagine trying to measure a voltage using pure digital logic — you'd need a whole comparator-based algorithm running in software.
Peripherals solve both problems by putting specialized, purpose-built circuitry directly on the chip, running independently of (and often faster/more precisely than) code the CPU executes. The CPU's job becomes: configure the peripheral via its control registers, then either poll a status flag or wait for an interrupt.
1. General Purpose Input/Output (GPIO)
GPIO pins are the simplest peripheral: each can be configured in software as a digital input (read a voltage as HIGH/LOW) or digital output (drive a voltage HIGH/LOW).
void setup() {
pinMode(7, OUTPUT); // pin 7 drives a relay
pinMode(2, INPUT_PULLUP); // pin 2 reads a button, internal pull-up enabled
}
void loop() {
if (digitalRead(2) == LOW) { // button pulls the pin to ground when pressed
digitalWrite(7, HIGH); // energize the relay
} else {
digitalWrite(7, LOW);
}
}
Why INPUT_PULLUP matters: a floating (unconnected) digital input pin picks up electrical noise and reads randomly HIGH/LOW. Enabling the internal pull-up resistor ties the pin to a known HIGH voltage through a large resistor, so a button pressing it to ground gives a clean, defined LOW.
Real-World Example
A washing machine's door-lock sensor is a single GPIO input: the microcontroller reads HIGH when the door is closed and latched, LOW otherwise, and refuses to start the spin cycle unless that pin reads HIGH — a genuine safety interlock built from the simplest peripheral available.
2. Analog-to-Digital Converter (ADC)
Most real-world signals (temperature, light, sound, potentiometer position) are analog voltages, but a CPU only understands digital numbers. The ADC bridges that gap by sampling a voltage and outputting a digital number proportional to it.
void setup() {
Serial.begin(9600);
}
void loop() {
int raw = analogRead(A0); // 10-bit ADC on most Arduino AVR boards: 0-1023
float voltage = raw * (5.0 / 1023.0); // scale to the 0-5V reference range
Serial.print("Raw: "); Serial.print(raw);
Serial.print(" Voltage: "); Serial.println(voltage);
delay(200);
}
Resolution determines precision: a 10-bit ADC (Arduino Uno) has 1024 possible output levels across the reference voltage range; a 12-bit ADC (many STM32/ESP32 boards) has 4096 levels, resolving smaller voltage changes. Sampling rate determines how fast you can take readings — critical for anything time-varying like audio.
Common Misunderstanding
Students often think analogRead() returns "the voltage." It returns a raw integer (0-1023 typically) that must be multiplied by (reference voltage / max ADC value) to get an actual voltage — skipping this conversion is a very common source of wrong readings in lab reports.
3. Digital-to-Analog Converter (DAC)
A DAC does the reverse of an ADC: it converts a digital number into an actual analog voltage, letting the MCU output a smooth, continuously variable signal rather than just HIGH/LOW.
// Example on a board with a true DAC (e.g., ESP32, Arduino Due)
void setup() {
// no pinMode needed for DAC on ESP32 - dacWrite() handles it
}
void loop() {
dacWrite(25, 128); // GPIO25 on ESP32; 128 out of 0-255 -> roughly 1.65V of 3.3V range
delay(1000);
}
Boards without a true DAC (like the classic Arduino Uno) fake an analog output using PWM plus a low-pass filter — which is why analogWrite() on an Uno is really generating a fast digital pulse train, not a true continuous voltage.
4. Timers and Counters, and PWM
A hardware timer is a register that increments automatically on every clock tick (or every external pulse, in counter mode), completely independent of the main program. Timers underlie delays, precise event scheduling, and — most commonly — PWM (Pulse Width Modulation).
PWM rapidly switches a pin between HIGH and LOW; the duty cycle (percentage of time HIGH) determines the effective average voltage seen by whatever's connected — dimming an LED, controlling a DC motor's speed, or setting a servo's position.
const int motorPin = 9; // must be a PWM-capable pin (has a ~ next to it on Arduino)
void setup() {
pinMode(motorPin, OUTPUT);
}
void loop() {
analogWrite(motorPin, 64); // ~25% duty cycle -> motor runs slow
delay(2000);
analogWrite(motorPin, 191); // ~75% duty cycle -> motor runs fast
delay(2000);
}
Register-level equivalent, configuring Timer1 on an ATmega328P for Fast PWM on pin 9:
#include <avr/io.h>
void setupPWM() {
DDRB |= (1 << PB1); // pin 9 (OC1A) as output
// Fast PWM, 8-bit, non-inverting mode, prescaler = 64
TCCR1A |= (1 << COM1A1) | (1 << WGM10);
TCCR1B |= (1 << WGM12) | (1 << CS11) | (1 << CS10);
}
void setDutyCycle(uint8_t duty) {
OCR1A = duty; // compare register: sets duty cycle out of 255
}
OCR1A (Output Compare Register) is what the timer hardware compares against its counting value every tick, automatically toggling the pin — no CPU intervention needed once configured. This is exactly why PWM output remains stable and precise even while your main loop is busy doing other work.
Why It Matters
If you tried to fake PWM in pure software by toggling a pin with delay() calls, any interrupt or code branch elsewhere in the program would jitter the timing — visible as motor stutter or LED flicker. Hardware timers guarantee the signal stays precise regardless of what the CPU is doing elsewhere.
5. Communication Interfaces: UART, I2C, SPI
Microcontrollers rarely work alone — they need to talk to sensors, displays, memory chips, or other MCUs. Three protocols dominate embedded systems:
UART (point-to-point, 2 wires: TX/RX) — simple, asynchronous, no shared clock line:
void setup() {
Serial.begin(9600); // configure UART for 9600 baud
}
void loop() {
Serial.println("Hello from UART");
delay(1000);
}
I2C (shared bus, 2 wires: SDA/SCL, address-based) — lets many devices share the same two wires:
#include <Wire.h>
void setup() {
Wire.begin(); // join I2C bus as controller
Wire.beginTransmission(0x48); // address of a typical temperature sensor (e.g., LM75)
Wire.write(0x00); // point to the temperature register
Wire.endTransmission();
}
void loop() {
Wire.requestFrom(0x48, 2); // request 2 bytes
if (Wire.available() == 2) {
int raw = (Wire.read() << 8) | Wire.read();
float tempC = raw / 256.0;
Serial.println(tempC);
}
delay(500);
}
SPI (4 wires: MOSI, MISO, SCK, CS, no addressing) — full-duplex and fast, used for displays and SD cards:
#include <SPI.h>
void setup() {
pinMode(10, OUTPUT); // chip-select pin
SPI.begin();
}
void loop() {
digitalWrite(10, LOW); // select the device
byte response = SPI.transfer(0x9F); // send a command, get response simultaneously
digitalWrite(10, HIGH); // deselect
delay(1000);
}
Real-World Example
A weather station board typically uses I2C to read a BME280 temperature/humidity/pressure sensor (because I2C lets you add multiple sensors on just two shared wires), SPI to drive a fast SD card logger (because SPI's higher speed suits bulk data), and UART to send readings to a PC over USB for debugging — all three protocols coexisting on the same board, each chosen for what it does best.
Key Terms
| Term | Definition |
|---|---|
| Peripheral | Dedicated on-chip hardware that performs a specific function (I/O, conversion, timing, communication) independently of the CPU core |
| GPIO | General Purpose Input/Output — a configurable digital pin |
| Pull-up resistor | A resistor tying a pin to a known HIGH voltage so it doesn't float when not actively driven |
| ADC | Analog-to-Digital Converter — converts a voltage into a digital number |
| DAC | Digital-to-Analog Converter — converts a digital number into a voltage |
| Resolution (ADC/DAC) | The number of bits used to represent the converted value; more bits = finer precision |
| Duty cycle | The percentage of time a PWM signal spends HIGH within one period |
| Timer/Counter | A hardware register that increments on clock ticks or external pulses, used for timing and PWM generation |
| UART | Universal Asynchronous Receiver/Transmitter — simple 2-wire point-to-point serial protocol |
| I2C | Inter-Integrated Circuit — 2-wire, address-based bus protocol supporting multiple devices |
| SPI | Serial Peripheral Interface — 4-wire, fast, full-duplex protocol without addressing |
Common Mistakes
-
Misconception: "
analogRead()gives me the actual voltage." Why it's wrong: Students skip the scaling step because the number "looks like" a measurement. Correct explanation:analogRead()returns a raw integer proportional to voltage (e.g., 0-1023 for a 10-bit ADC). You must scale it by (reference voltage ÷ max ADC value) to get the actual voltage. -
Misconception: "I2C and SPI are basically the same thing, just with different pin names." Why it's wrong: Both are "serial buses used to talk to chips," so they seem interchangeable at a glance. Correct explanation: I2C uses 2 wires and device addresses to support multiple devices on a shared bus at moderate speed; SPI uses 4 wires (no addressing, using a separate chip-select line per device) for higher speed, full-duplex communication. They are not drop-in replacements for each other.
-
Misconception: "PWM produces a true analog voltage." Why it's wrong: The waveform looks smooth on an LED or motor's response because of physical filtering (motor inertia, LED persistence, or an actual RC filter), so students assume the signal itself is analog. Correct explanation: PWM is a digital signal rapidly switching between HIGH and LOW; only the average voltage over time behaves like an analog value once filtered by the load or an external circuit. A true DAC produces a continuously variable voltage directly.
Comparison and Connections
| Interface | Wires | Speed | Addressing | Typical use |
|---|---|---|---|---|
| UART | 2 (TX, RX) | Slow-moderate (9600-115200 baud typical) | None (point-to-point) | Debug console, GPS modules, Bluetooth modules |
| I2C | 2 (SDA, SCL) | Moderate (100 kHz-1 MHz) | Yes (7/10-bit address) | Multiple sensors sharing a bus |
| SPI | 4 (MOSI, MISO, SCK, CS) | Fast (up to tens of MHz) | No (dedicated CS line per device) | Displays, SD cards, high-speed sensors |
| Peripheral | Converts... | Common use |
|---|---|---|
| ADC | Analog voltage → digital number | Reading sensors |
| DAC | Digital number → analog voltage | Audio output, analog control signals |
| PWM (via timer) | Digital number → average voltage (indirectly) | Motor speed, LED brightness, servo position |
Practice Questions
Recall
- Name the five common categories of microcontroller peripherals covered in this chapter. Answer guidance: GPIO, ADC, DAC, Timers/PWM, and communication interfaces (UART, I2C, SPI).
- What do the acronyms UART, I2C, and SPI stand for? Answer guidance: Universal Asynchronous Receiver/Transmitter; Inter-Integrated Circuit; Serial Peripheral Interface.
Understanding
3. Explain why hardware PWM (via a timer peripheral) produces a more stable signal than toggling a pin with delay() in software.
Answer guidance: The timer hardware runs independently of the CPU's main program, comparing its count against a fixed register value every clock cycle with no jitter; software toggling is affected by interrupts, branch timing, and other code running in the loop, causing inconsistent pulse widths.
4. Explain why I2C needs device addresses but SPI does not.
Answer guidance: I2C shares two wires among multiple devices, so each device needs a unique address to know when data is meant for it; SPI instead uses a separate chip-select (CS) line per device, so the controller selects the intended device electrically rather than by address.
Application 5. You're building a project that reads a potentiometer's position and uses it to set an LED's brightness. Which two peripherals are involved, and in what order does data flow through them? Answer guidance: ADC first (reads the potentiometer's analog voltage and converts it to a digital number), then PWM/timer (uses that number to set the duty cycle driving the LED). 6. A weather station board needs to read three different sensors and also log data to an SD card. Which communication interfaces would you choose for the sensors versus the SD card, and why? Answer guidance: I2C for the sensors (multiple devices sharing two wires with different addresses, moderate speed is fine); SPI for the SD card (higher throughput needed for writing data blocks quickly).
Analysis 7. A 10-bit ADC and a 12-bit ADC both sample the same 0-3.3V analog sensor signal. Analyze how their outputs differ for the same input voltage and why one is "better" for precision applications. Answer guidance: The 10-bit ADC has 1024 levels across 3.3V (~3.2 mV per step); the 12-bit ADC has 4096 levels (~0.8 mV per step). The 12-bit ADC resolves smaller voltage changes, making it better for applications needing fine precision, such as measuring small sensor variations. 8. Evaluate the tradeoff of using PWM plus a low-pass filter to fake an analog output versus using a true onboard DAC. Answer guidance: PWM+filter is cheaper (no dedicated DAC hardware needed) and works on almost any MCU, but introduces ripple/settling time and requires extra external components (resistor + capacitor) for a clean signal; a true DAC gives an instantly accurate, ripple-free analog voltage directly from a register write, at the cost of requiring DAC-capable hardware.
FAQ
Q1: Why does the Arduino Uno only have PWM on some pins (marked with a ~)? Because PWM output requires a hardware timer connected to that specific pin; only pins wired internally to a timer's output compare unit can produce PWM. Other pins can only do plain digital I/O.
Q2: Can I connect an I2C sensor and an SPI sensor to the same microcontroller at the same time? Yes — most MCUs have separate hardware blocks for each interface, and the pins don't conflict, since I2C and SPI use different dedicated pins.
Q3: What happens if two I2C devices on the same bus have the same address? They'll both respond simultaneously, causing bus contention and garbled or ambiguous data. This is a real and common bug — always check sensor datasheets for the default address and whether it's configurable via an address pin.
Q4: Why is my ADC reading noisy/jumping around even with a steady input voltage? Common causes include electrical noise on the analog reference, a floating or high-impedance sensor output, or lack of decoupling capacitors near the ADC's supply pins. Averaging multiple readings in software often helps.
Q5: Do all microcontrollers have a built-in DAC? No — many popular 8-bit MCUs (including the classic Arduino Uno's ATmega328P) have no true DAC and rely on PWM plus filtering instead. More capable MCUs like the ESP32 and Arduino Due include genuine onboard DACs.
Quick Revision
- Peripherals let the CPU offload specialized work (I/O, conversion, timing, communication) to dedicated hardware
- GPIO pins are configured as input or output; use
INPUT_PULLUPto avoid floating inputs - ADC converts analog voltage to a digital number; must scale the raw value by (Vref / max ADC value) to get real voltage
- DAC converts digital numbers to analog voltage directly; many cheap MCUs fake this with PWM + filtering instead
- Timers run independently of the CPU and are the hardware behind precise delays and PWM generation
- Duty cycle (% time HIGH) determines the average effective voltage of a PWM signal
- UART: 2 wires, point-to-point, no addressing — good for simple serial links
- I2C: 2 wires, shared bus, address-based — good for multiple sensors on one bus
- SPI: 4 wires, no addressing (uses chip-select), fastest of the three — good for displays and storage
- Register-level PWM setup (e.g.,
TCCR1A,OCR1Aon AVR) is what Arduino'sanalogWrite()wraps automatically
Related Topics
Prerequisites: Introduction to Microcontrollers, Programming Microcontrollers
Related Topics: Embedded Systems Programming, Interfacing Microcontrollers
Next Topics: Interfacing Microcontrollers, Applications of Microcontrollers