Analog Signals, ADC, and Pulse-Width Modulation
Summary
Not everything in the physical world is simply on or off — sensors produce varying voltages, and lights can glow at any brightness. This chapter explains how the Pico's analog-to-digital converter (ADC) reads continuous voltages from potentiometers and light sensors, and how to scale those raw 16-bit readings into useful numbers. You will also learn pulse-width modulation (PWM), a clever technique that uses rapid on/off switching to simulate variable voltages — enabling LED fading, brightness control, and the motor and servo control skills used in later chapters.
Concepts Covered
This chapter covers the following 24 concepts from the learning graph:
- Analog Signal
- Digital Signal
- Analog-to-Digital Converter (ADC)
- ADC Resolution (bits)
- machine.ADC Class
- ADC.read_u16() Method
- ADC Voltage Reference
- Potentiometer
- Potentiometer as Voltage Divider
- Voltage Divider Circuit
- Reading Analog Values
- Scaling ADC Values
- Light Sensor (Photoresistor)
- LDR (Light-Dependent Resistor)
- Pulse-Width Modulation (PWM)
- PWM Frequency
- PWM Duty Cycle
- machine.PWM Class
- PWM.duty_u16() Method
- LED Fade with PWM
- Brightness Control
- PWM for Servo Control
- PWM for Motor Speed
- Soft PWM
Prerequisites
This chapter builds on concepts from:
Welcome to Chapter 7
The real world is not made of just ones and zeros — it has shades of brightness, degrees of warmth, and gradients of everything in between. In this chapter you will read that continuous world with the Pico's ADC, and control it smoothly with PWM. By the end you will be fading LEDs and setting up the concepts that drive motors and servos in later chapters!
Analog vs Digital Signals
A digital signal has exactly two states: HIGH (3.3 V) or LOW (0 V). That is what you worked with in Chapter 6.
An analog signal can take any value between 0 V and 3.3 V. A potentiometer turned halfway gives 1.65 V. A light sensor in a dim room might give 0.8 V; in sunlight, 3.0 V. Temperature sensors, microphones, and joysticks all produce analog signals.
To use an analog signal in a Python program, you must convert it to a number. That is the job of the analog-to-digital converter (ADC).
The ADC — Turning Voltage into Numbers
An ADC samples the voltage on a pin and converts it to a whole number. The Pico's ADC has 12-bit resolution internally, but MicroPython reports values as 16-bit unsigned integers (0–65535) to maintain compatibility across different hardware.
ADC resolution tells you how finely the converter can measure: - 16-bit: 65,536 possible values between 0 V and 3.3 V - Each step = 3.3 V ÷ 65,535 ≈ 0.00005 V (50 µV per step)
The ADC voltage reference is the maximum voltage the ADC can read. On the Pico it is 3.3 V — do not apply more than 3.3 V to an ADC pin.
The Pico has three ADC-capable pins: GP26 (ADC0), GP27 (ADC1), and GP28 (ADC2). There is also a built-in temperature sensor on ADC channel 4.
The machine.ADC Class
Import ADC from machine, then create an ADC object using the GPIO pin number:
1 2 3 | |
ADC.read_u16() Method
The ADC.read_u16() method reads the current voltage and returns an integer from 0 to 65535.
0means 0 V (GND)65535means 3.3 V (full scale)
1 2 | |
Potentiometers — Variable Resistors
A potentiometer (pot) is a three-terminal resistor with a sliding wiper. As you turn the knob, the wiper moves, changing how much resistance is between each terminal and the wiper.
When wired with one end to 3.3 V, the other end to GND, and the wiper to an ADC pin, the potentiometer acts as a voltage divider. A voltage divider circuit takes an input voltage and outputs a fraction of it, set by the ratio of two resistances.
As you turn the pot: - Fully counterclockwise: wiper at GND → ADC reads ~0 - Fully clockwise: wiper at 3.3 V → ADC reads ~65535 - Middle: wiper at 1.65 V → ADC reads ~32768
Scaling ADC Values
Raw ADC values (0–65535) are not usually the most useful form. Scaling maps them to a range that makes sense for your application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Light Sensors — Photoresistors and LDRs
A photoresistor (also called a light-dependent resistor or LDR) changes its resistance based on the amount of light hitting it. In bright light, resistance is low (a few hundred ohms). In darkness, resistance is high (a few megaohms).
Wire an LDR and a fixed resistor (10 kΩ) as a voltage divider between 3.3 V and GND, with the ADC pin in the middle. As light changes, the ADC reading changes.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Diagram: ADC and Potentiometer Explorer
ADC and Potentiometer Explorer MicroSim
Type: microsim
sim-id: adc-potentiometer-explorer
Library: p5.js
Status: Specified
Bloom Level: Apply (L3) Bloom Verb: calculate Learning Objective: Students can read a potentiometer value, interpret the raw ADC reading, and scale it to volts, percent, and a target range.
Canvas layout: - Left 40%: a rotary knob (drag to turn) representing the potentiometer - Center 30%: four labeled readouts — raw (0–65535), voltage (V), percent (%), and a user-defined target range slider - Right 30%: a bar graph showing the scaled value in the chosen range
Visual elements: - Rotary knob drawn with an arc indicating position; draggable - Waveform line below the knob showing the last 2 seconds of readings - Readouts update every 50 ms to simulate a real ADC sample
Interactive controls:
- Drag knob left/right to change value
- createSlider() for "Target range min" (0–1000) and "Target range max" (0–1000)
- Result formula shown: scaled = raw / 65535 × (max - min) + min
Instructional Rationale: Connecting the physical knob action to the live readout and scaling formula makes the abstract conversion concrete and interactive.
Implementation: p5.js. Knob as an arc; mouse drag changes value; three createSlider() for min, max, update rate; formula displayed dynamically.
Pulse-Width Modulation (PWM)
A GPIO pin can only be HIGH or LOW — it cannot output 1.65 V. So how do you control LED brightness smoothly, or set a motor to half speed?
The answer is pulse-width modulation (PWM). PWM rapidly switches the pin HIGH and LOW thousands of times per second. By changing the fraction of time it is HIGH, you control the average voltage the device receives.
Two parameters define a PWM signal:
- PWM frequency — how many complete on/off cycles happen per second, measured in hertz (Hz). For LEDs: 50–1,000 Hz is typical (faster prevents flicker). For servos: 50 Hz is required. For motors: 10–20 kHz is common.
- PWM duty cycle — the fraction of each cycle that the pin is HIGH. A 0% duty cycle is always off. A 100% duty cycle is always on. A 50% duty cycle means the pin is on for half of each cycle.
1 2 3 | |
The machine.PWM Class
1 2 3 4 5 6 7 8 9 10 | |
PWM.duty_u16() Method
PWM.duty_u16(value) sets the duty cycle as a number from 0 to 65535. The scale matches ADC.read_u16() output — this makes it convenient to connect a potentiometer directly to PWM brightness:
1 2 3 4 5 6 7 8 9 | |
LED Fade with PWM
The classic LED fade gradually increases the duty cycle from 0 to full, then decreases it back to 0, creating a breathing effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
PWM for Servo Control
Servo motors require a very specific PWM signal: 50 Hz frequency and a pulse width between 1 ms (0°) and 2 ms (180°). At 50 Hz, each cycle is 20 ms, so:
- 1 ms pulse = 1/20 = 5% duty cycle =
0.05 × 65535 ≈ 3277 - 2 ms pulse = 2/20 = 10% duty cycle =
0.10 × 65535 ≈ 6554
1 2 3 4 5 6 7 8 9 10 11 12 | |
PWM for Motor Speed
DC motors respond to the average voltage — higher duty cycle means faster rotation. You typically pair PWM with a motor driver chip (Chapter 12), but the PWM concept is the same:
1 2 3 | |
What Is Soft PWM?
Soft PWM (software PWM) generates a PWM-like signal using the CPU itself — switching a pin HIGH and LOW with utime.sleep_us() loops. It works on any GPIO pin but is imprecise because the CPU can be interrupted. Hardware PWM uses dedicated silicon that runs independently of your code. Always use hardware PWM when you can. Soft PWM is only for quick experiments on pins that do not have hardware PWM support.
Key Takeaways
- Analog signals have continuous voltage levels; digital signals are only HIGH or LOW.
- The Pico's ADC on GP26–GP28 reads 0–3.3 V and returns 0–65535 (16-bit).
ADC.read_u16()reads the current value; scale withvalue / 65535 × target_range.- A potentiometer wired as a voltage divider provides a variable 0–3.3 V to the ADC.
- An LDR changes resistance with light; use a voltage divider to make it readable by the ADC.
- PWM rapidly switches a pin to simulate a variable average voltage.
- PWM frequency sets how fast; duty cycle (0–65535) sets how much on-time.
- Servos need 50 Hz; LEDs work at 500–1,000 Hz; motors work well at 10–20 kHz.
Quick Check: What duty_u16 value gives 25% brightness? (Click to reveal)
16,383 (or 16384) — 25% of 65,535 is 16,383.75, so 65535 × 0.25 ≈ 16384.
Analog and PWM Mastered!
You can now read the analog world and control devices with smooth PWM signals. A potentiometer controls an LED's brightness, and the same concept drives servos and motors. In Chapter 8 you will learn the communication protocols — I2C, SPI, and UART — that let the Pico talk to sensors, displays, and other chips. The sensor library chapters are almost in reach!