Rotary Potentiometer

🎮 The Knob of a Thousand Values
Module Goal: leave the world of 1s and 0s behind forever. From today your robots understand in-between values: a knob at 37%, light at 61%, heat at 84%. The potentiometer is your first ANALOG sensor, and it opens every other analog module in the course (light, soil, rain, distance...).
Goal:
watch a THOUSAND values flow through your screen as you turn the knob.
Concepts:
Analog Input · ADC · The 0–65535 Scale
📦 CONCEPT BOX What is an ANALOG signal?
You know DIGITAL: a light switch. Two states. 1 or 0. Nothing between.
ANALOG is a dimmer knob: it has many values every position between the extremes is valid.
- On a digital pin, voltage is only ever 0 V or 3.3 V.
- On an analog pin, voltage can be anywhere in between: 0 V, 1.2 V, 2.7 V, 3.1 V... continuously.
The real world is mostly analog: temperature, light level, loudness, knob position none of them are just ON/OFF.
But computers only understand numbers. So the ESP32 carries a special translator inside called the ADC Analog-to-Digital Converter. It measures the voltage on the pin and converts it into a number.
The course scale (memorize this): 0 to 65535 16 bits, always.
Pin at 0 V → the ADC returns 0
Pin at 3.3 V → the ADC returns 65535
Pin exactly in the middle → ~32768
The reading command is new: adc2.read_u16() it returns that 0–65535 number.
Your three families are now complete:
- DIGITAL 2 values (button, crash, touch, tilt, sound, radar, guardian)
- ANALOG many values (today: the knob; soon: light, soil, rain, distance)
- PWM the flicker trick that fakes in-between power (lamp, fan, car)
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc4 as an analog input for the potentiometer
# adc4 corresponds to A1 on the Sensor Shield V5.0
adc4 = machine.ADC(machine.Pin(4))
adc4.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
while True: # Repeat forever
# 16-bit is the default; just use adc4.read_u16()
value = adc4.read_u16() # Ask the ADC: what voltage is on the pin? (0 to 65535)
print("Knob value:", value) # Send the raw value to the PC
time.sleep_ms(300) # Pause so the screen stays readableSerial Monitor (turn the knob slowly from one end to the other):
Knob value: 312
Knob value: 8450
Knob value: 18902
Knob value: 32768 ← middle of the turn
Knob value: 47550
Knob value: 59810
Knob value: 65400
💡 Note: compare with everything you've read before. The button could only ever say 1 or 0 this knob says 65,536 different things. That number is not random: 2 to the power of 16 = 65536. The u16 in read_u16() means "unsigned 16 bits" sixteen little switches inside the ADC, combining into one big number. Turn the knob to what LOOKS like the middle and check the screen: ~32768 means your eyes and the ADC agree.
🎮 Percentages and Zones
Goal:
convert the raw value into a friendly percentage, then classify it into zones.
Concepts:
Scaling · Integer Division Order · Zone Classification
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc4 as an analog input for the potentiometer
# adc4 corresponds to A1 on the Sensor Shield V5.0
adc4 = machine.ADC(machine.Pin(4))
adc4.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
while True: # Repeat forever
# 16-bit is the default; just use adc4.read_u16()
value = adc4.read_u16() # Raw reading: 0 to 65535
percent = (value * 100) // 65535 # Scale to 0-100 percent
print("Knob at", percent, "%") # Report the percentage
if percent < 33: # Bottom third?
print("Zone: LOW") # Low zone
elif percent < 66: # Middle third?
print("Zone: MEDIUM") # Medium zone
else: # Top third
print("Zone: HIGH") # High zone
time.sleep_ms(300) # Pause between readingsSerial Monitor:
Knob at 18 %
Zone: LOW
Knob at 51 %
Zone: MEDIUM
Knob at 87 %
Zone: HIGH
💡 Note: the percentage line hides a critical secret about order of operations: (value * 100) // 65535 multiply FIRST, divide LAST. If you divide first (value // 65535), you get 0 almost always (raw values are smaller than 65535!), and multiplying 0 by 100 gives... 0. Forever. This bug is called integer truncation // throws away decimals, so you must not throw them away too early. Multiplying before dividing protects the small numbers. Every programmer has hit this bug once; now you're immune.
🎮 Which Way Did It Turn?
Goal:
detect the knob's DIRECTION did it turn clockwise or counter-clockwise?
Concepts:
Comparing Consecutive Readings · Dead Zone · Direction Detection
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc4 as an analog input for the potentiometer
# adc4 corresponds to A1 on the Sensor Shield V5.0
adc4 = machine.ADC(machine.Pin(4))
adc4.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
prev = 0 # Remembers the previous reading
while True: # Repeat forever
# 16-bit is the default; just use adc4.read_u16()
now = adc4.read_u16() # Fresh reading
if now > prev + 800: # Clearly BIGGER than before (with a safety margin)
print("Turning RIGHT (clockwise)") # Direction report
elif now < prev - 800: # Clearly SMALLER than before
print("Turning LEFT (counter-clockwise)") # Direction report
prev = now # Save this reading for the next comparison
time.sleep_ms(100) # Quick checkingSerial Monitor (turn gently right, then left):
Turning RIGHT (clockwise)
Turning RIGHT (clockwise)
Turning LEFT (counter-clockwise)
Turning LEFT (counter-clockwise)
💡 Note: why the mysterious +800 / −800? Because ADC readings are never perfectly still they jitter by tiny amounts even with your hand off the knob. The margin (a dead zone) means "ignore jitters smaller than 800" and only react to REAL turns. Try removing it (compare with now > prev) and watch the screen panic with false turns! Dead zones are how engineers ignore noise you will reuse this trick with every analog sensor to come.
🎮 Moving or Resting?
Goal:
detect movement bursts announce when the knob starts moving and when it settles.
Concepts:
Dead Zone · State Memory · Event Detection on Analog
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc4 as an analog input for the potentiometer
# adc4 corresponds to A1 on the Sensor Shield V5.0
adc4 = machine.ADC(machine.Pin(4))
adc4.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
prev = 0 # Remembers the previous reading
moves = 0 # Counts movement bursts
moving = False # Remembers if we were moving last check
while True: # Watch forever
# 16-bit is the default; just use adc4.read_u16()
now = adc4.read_u16() # Fresh reading
if now > prev + 800 or now < prev - 800: # Real movement detected
if moving == False: # We were resting a moment ago...
moves += 1 # ...so a NEW burst starts!
print("Movement burst", moves, "started!") # Announce the burst
moving = True # Remember: we are moving now
else: # No real change
if moving == True: # We WERE moving a moment ago...
print("Movement stopped. The knob rests.") # ...so the burst just ended
moving = False # Remember: we are resting now
prev = now # Save this reading
time.sleep_ms(100) # Checking paceSerial Monitor (turn, pause, turn again):
Movement burst 1 started!
Movement stopped. The knob rests.
Movement burst 2 started!
Movement stopped. The knob rests.
💡 Note: you just did something impressive you applied event detection to an analog signal. Digital sensors gave you events for free (pressed/not pressed); analog values change gradually, so YOU built the boundary: "a change bigger than 800 counts as movement." The moving variable remembers the state between checks the same memory trick as the touch lamp's mode. Analog + state memory = motion detection, gesture detection, shake detection. This pattern is about to become very valuable.
🎮 The Magic Bar
Goal:
build a live bar of # symbols in the Serial Monitor that GROWS and SHRINKS as you turn the knob like a video game volume bar. And you will build it with a loop that constructs the bar piece by piece.
Concepts:
Scaling · String Building · Loop Construction · Live Visualization
import machine # Library that controls the board's pins
import time # Library for pauses
# Sets up adc4 as an analog input for the potentiometer
# adc4 corresponds to A1 on the Sensor Shield V5.0
adc4 = machine.ADC(machine.Pin(4))
adc4.atten(machine.ADC.ATTN_11DB) # Sets the analog input range
print("=== MAGIC BAR ===") # Mission title
print("Turn the knob and watch the bar!") # Instructions
while True: # Run forever
# 16-bit is the default; just use adc4.read_u16()
value = adc4.read_u16() # Raw reading: 0 to 65535
percent = (value * 100) // 65535 # Scale to 0-100 percent
bars = percent // 10 # How many # blocks? (0 to 10)
bar = "" # Start with an EMPTY bar
for i in range(1, bars + 1, 1): # Repeat once per block
bar += "#" # Add one more block to the bar
print(bar, "|", percent, "%") # Show the bar and the number
time.sleep_ms(200) # Refresh 5 times per secondSerial Monitor (turn the knob slowly from zero to max):
| 0 %
# | 9 %
### | 34 %
##### | 52 %
######## | 83 %
########## | 100 %
💡 Note: the most elegant trick of the module hides in three lines. ① bar = "" creates an empty box. ② The loop adds one # per repetition turn to 52% and bars becomes 5, so the loop runs 5 times and the bar grows to #####. ③ If the knob is at 0%, bars is 0, the loop never runs, and the bar stays empty the loop simply skips. You are now building data structures inside a variable, one piece at a time this exact idea (accumulate in a loop) will later build lists of sensor readings, messages, and game maps. Demo challenge: without looking at the percent number, can a classmate guess the knob position just by the bar's length? Your screen has become an instrument.
