Buttons, Photoresistors, and Analog Sensor Input¶
Summary¶
This chapter extends breadboard skills into analog and digital sensing: push buttons and momentary switches, photoresistors and voltage dividers for light sensing, and a survey of additional sensors -- tilt switches, reed switches, temperature and humidity sensors, sound sensors, and buzzers. It covers the software techniques needed to turn raw sensor readings into reliable signals: threshold values, calibration, button debounce code, sampling rate, and moving-average noise filtering. Students finishing this chapter will be able to read an analog sensor, calibrate a threshold, and write a debounced sensor-reading loop.
Concepts Covered¶
This chapter covers the following 28 concepts from the learning graph:
- Push Button
- Momentary Switch
- Photoresistor
- Light Dependent Resistor
- Voltage Divider
- Analog Read
- Digital Read
- Threshold Value
- Calibration
- Potentiometer
- Tilt Switch
- Reed Switch
- Magnetic Sensor
- Temperature Sensor
- DHT11 Sensor
- Humidity Sensor
- Sound Sensor
- Microphone Module
- Piezo Buzzer
- Passive Buzzer
- Active Buzzer
- Tone Generation
- Interrupt Handler
- Button Debounce Code
- Sensor Reading Loop
- Sampling Rate
- Noise Filtering
- Moving Average Filter
Prerequisites¶
This chapter builds on concepts from:
- Chapter 1: Computational Thinking and Debugging for Physical Computing
- Chapter 3: Breadboard Wiring and Electrical Fundamentals
- Chapter 4: Digital I/O, PWM, and the MicroPython Workflow
Time to Listen to the World
So far your Pico has mostly been talking -- blinking an LED, driving a NeoPixel strip. This chapter flips that around. You're about to give your circuits ears, eyes, and fingertips: buttons that notice a press, photoresistors that notice a shadow, and a handful of other sensors that notice temperature, sound, and magnets. Let's build something that listens!
Reading a Button: Your First Digital Sensor¶
A push button is the simplest input device on a breadboard: a small mechanical switch that closes an electrical circuit only while a person is physically pressing it. Most push buttons used in kits are a specific style called a momentary switch -- a switch that returns to its resting (open) position the instant you release it, unlike a light switch on a wall that stays wherever you leave it. That "momentary" behavior is exactly what makes a button useful for a microcontroller: the circuit is only closed for as long as a finger is actually on it.
Your Pico reads a button's state with digital read -- checking a GPIO pin and getting back exactly one of two values, HIGH or LOW, corresponding to the pin's voltage being close to 3.3 volts or close to 0 volts. There's no "half-pressed" reading; a digital pin only ever reports one of two states, which is why a button pairs naturally with digital read rather than the analog techniques you'll meet later in this chapter.
from machine import Pin
button = Pin(15, Pin.IN, Pin.PULL_UP)
while True:
if button.value() == 0:
print("Button is pressed")
Here, Pin(15, Pin.IN, Pin.PULL_UP) configures GPIO pin 15 as a digital input and turns on the Pico's internal pull-up resistor, which quietly holds the pin at HIGH (reading as 1) whenever nothing else is pulling it low. Wiring the button so it connects that pin to ground when pressed means the reading flips to 0 (LOW) exactly while a finger is on the button -- which is why the code checks == 0 rather than == 1 to detect a press.
The Trouble With a Bouncing Switch¶
Here's something the ideal on/off diagram of a button hides: the metal contacts inside a real switch don't snap cleanly from open to closed. They physically bounce, making and breaking contact several times in the space of a few milliseconds before settling. To your Pico, reading thousands of times per second, one human button press doesn't look like one clean transition -- it looks like a rapid flicker of several presses and releases.
Berry's Key Insight
A single, deliberate press of a button can register as five or six presses in code if you don't account for bounce. It's not your code being buggy in the usual sense -- it's the physical switch telling the truth about what its metal contacts are actually doing, just faster than a human finger can follow.
Button debounce code is software written specifically to filter out that mechanical noise so a single physical press is reported as exactly one logical event. The most common approach is a short, deliberate pause: after detecting a change in the pin's value, the code waits a few milliseconds -- long enough for the bounce to settle but short enough that a human can't notice the delay -- before trusting the reading.
from machine import Pin
from time import sleep_ms
button = Pin(15, Pin.IN, Pin.PULL_UP)
last_state = 1
while True:
current_state = button.value()
if current_state != last_state:
sleep_ms(20) # debounce delay
current_state = button.value() # re-check after the bounce settles
if current_state == 0:
print("Button pressed")
last_state = current_state
The sleep_ms(20) call is the debounce delay itself: twenty milliseconds is enough time for nearly any mechanical switch's bounce to finish, but far too short for a person to perceive as lag. Reading the pin a second time, after the pause, is what actually confirms the press was real rather than mid-bounce.
There's a second technique worth knowing about here, one you first met conceptually back in Chapter 1: instead of a program repeatedly polling a pin in a loop, an interrupt handler is a function you register in advance that the Pico runs automatically the instant a pin's voltage changes, without your main program having to keep checking. Interrupt handlers are especially useful for buttons in projects where the main loop is busy doing something else -- like animating a display -- and can't afford to poll a pin every few milliseconds. Debounce logic still matters with interrupts, though; a bouncing switch will trigger the handler multiple times just as fast as it would trigger a polling loop, so most interrupt-based button code adds its own short timing check inside the handler.
Before the diagram below, notice the shape of the problem: a bouncing signal looks "noisy" for a few milliseconds, and debounce code is really just a tiny, well-timed filter applied right at the source.
Diagram: Button Debounce Visualizer¶
Run the Button Debounce Visualizer MicroSim fullscreen
Button Debounce Visualizer (MicroSim)
Type: microsim
sim-id: button-debounce-visualizer
Library: p5.js
Status: Specified
Learning objective: Students will analyze (Bloom L4: Analyze) how mechanical switch bounce produces a noisy raw signal and how a debounce delay converts it into a single clean logical event.
Canvas: 700x460px, responsive -- recompute the waveform plot width from windowWidth inside windowResized() so the trace rescales rather than clipping on narrow screens.
Layout: two stacked oscilloscope-style waveform plots sharing a common time axis. The top plot, labeled "Raw Pin Signal," shows a simulated bouncy transition: a HIGH-to-LOW step that includes 4-6 rapid up-down bounces within roughly the first 15 milliseconds before settling LOW. The bottom plot, labeled "Debounced Signal," shows the same time window but as a single clean HIGH-to-LOW step delayed by the debounce window. A vertical shaded band overlays both plots marking the active debounce delay window (default 20 ms), and a small readout above the plots displays the current delay value in milliseconds.
Controls: a createButton() labeled "Simulate Press" that re-triggers the bounce-and-settle animation from the start; a createSlider() labeled "Debounce Delay (ms)" (range 0-50, default 20, step 1) that widens or narrows the shaded delay band and, at the extremes, visibly demonstrates the tradeoff -- setting the slider to 0 leaves visible false extra transitions in the bottom plot, while a very large value (over 40 ms) adds a noticeable, labeled "lag" arrow showing delayed response.
Interaction: hovering over either waveform plot shows a tooltip with the exact simulated voltage state (HIGH/LOW) and timestamp at that x-position. Clicking directly on one of the bounce spikes in the top plot opens a small infobox explaining that this spike represents a mechanical contact briefly reconnecting, reusing the plain-language explanation from the surrounding chapter text.
Implementation: p5.js, waveform data stored as an array of {t, value} step points generated procedurally at setup (randomized bounce timing within fixed bounds, reseeded on "Simulate Press"). Draw plots with line() segments between consecutive step points. Use millis()-based animation to sweep a playhead across both plots in sync. Recompute all pixel positions from data coordinates in windowResized() rather than caching absolute pixel values.
Sensing Light: Photoresistors and Voltage Dividers¶
Buttons are digital: pressed or not pressed, nothing in between. Light, by contrast, is a continuous quantity -- a room can be pitch dark, dim, bright, or anywhere in between -- and sensing it well means moving past digital read into the world of analog input.
A photoresistor, also called a light dependent resistor (LDR), is a component whose electrical resistance changes with the amount of light hitting its surface: more light lowers its resistance, less light raises it. On its own, though, a photoresistor doesn't produce a voltage your Pico can read directly -- resistance isn't voltage. To turn a changing resistance into a changing voltage, photoresistor circuits use a voltage divider: two resistors (here, the photoresistor and a fixed resistor) wired in series across a power source, with the midpoint between them tapped as the output. Because the two resistors share the total voltage in proportion to their resistances, as the photoresistor's resistance changes with light level, the midpoint voltage changes right along with it.
That midpoint voltage is what your Pico actually measures, using analog read -- a function that, unlike digital read's simple HIGH/LOW, reports a value across a whole range representing the actual voltage present on the pin. On the Raspberry Pi Pico, analog input comes through dedicated ADC (analog-to-digital converter) pins, and a typical reading is a 16-bit number from 0 to 65535 representing the voltage from 0 to 3.3 volts.
from machine import ADC
light_sensor = ADC(26) # ADC0, connected to the voltage divider midpoint
while True:
raw = light_sensor.read_u16() # 0 (dark) to 65535 (bright), roughly
print(raw)
ADC(26) configures GPIO pin 26 (the Pico's first ADC-capable pin) as an analog input. The read_u16() method is the analog-read call itself -- it returns a 16-bit unsigned number, so darker readings from a typical photoresistor voltage divider land toward the low end of that range and brighter readings toward the high end, though the exact numbers depend on which resistor value you paired with the photoresistor.
Turning Numbers into Decisions: Threshold and Calibration¶
A raw analog number like 41,200 doesn't mean anything on its own -- a program needs to turn it into a decision. A threshold value is a chosen cutoff point that separates one category of reading from another: "if the light reading is below 20,000, treat the room as dark; otherwise treat it as light." Choosing a good threshold isn't a guess, though -- it's the product of calibration, the process of testing a sensor under the real conditions it will actually operate in and adjusting the code's threshold (or other reference values) to match what you measured, rather than a number copied from a datasheet or another student's project.
Berry's Gentle Warning
A threshold that works perfectly in one room can fail completely in another. Overhead fluorescent lights, a sunny window, and a closet all produce wildly different "bright" and "dark" readings from the exact same photoresistor circuit. Always calibrate on-site, in the room your project will actually run in -- not at your kitchen table the night before.
Sometimes you want a threshold a person can adjust by hand, on the fly, without reflashing code. A potentiometer is a variable resistor with a rotating knob or sliding lever that lets a person mechanically change its resistance -- and, wired into its own voltage divider, it produces a variable voltage your Pico can read with the exact same ADC.read_u16() call used for the photoresistor. A common calibration trick is wiring a potentiometer purely as a "threshold knob": its analog reading isn't a sensor measurement at all, it's a live, physically adjustable threshold value that a student (or a teacher demonstrating the project) can tune in real time while watching the photoresistor's readings.
Now that voltage dividers, analog read, threshold values, and potentiometers have all been defined, the diagram below lets you explore how they connect as one working circuit.
Diagram: Photoresistor Voltage Divider and Threshold Explorer¶
Run the Photoresistor Voltage Divider and Threshold Explorer MicroSim fullscreen
Photoresistor Voltage Divider and Threshold Explorer (MicroSim)
Type: microsim
sim-id: photoresistor-voltage-divider-threshold-explorer
Library: p5.js
Template: https://github.com/dmccreary/moving-rainbow/tree/main/docs/sims/photoresistor-component
Status: Specified
Learning objective: Students will apply (Bloom L3: Apply) a calibrated threshold value to a photoresistor's voltage-divider output to classify a simulated room as light or dark.
Canvas: 700x480px, responsive -- recompute the circuit-diagram and graph panel widths as fractions of windowWidth in windowResized(), stacking the two panels vertically below 600px wide.
Layout: left panel shows a simple schematic voltage-divider circuit drawn with p5.js primitives (power rail, photoresistor symbol as a zig-zag resistor inside a circle, fixed resistor symbol, ground rail, with the midpoint tap highlighted). Right panel shows a live horizontal bar gauge of the current ADC reading (0-65535) with a draggable threshold marker line overlaid on the same scale, and a text readout below stating "Reading: [value] -- Room is [LIGHT / DARK]" that updates live.
Controls: a createSlider() labeled "Simulated Light Level" (range 0-100, default 50) representing ambient brightness, which drives the simulated ADC reading; a second createSlider() labeled "Threshold Value" (range 0-65535, default 32768) representing the calibrated cutoff, whose handle also appears as the draggable marker line on the gauge; a createButton() labeled "Auto-Calibrate" that sets the threshold slider to the midpoint between the last recorded darkest and brightest readings (tracked as running min/max as the light-level slider is moved).
Interaction: hovering the circuit schematic's photoresistor symbol shows a tooltip reading "Resistance falls as light increases," and hovering the fixed resistor shows "Fixed resistance -- sets the divider ratio." Dragging the threshold marker directly on the gauge updates the Threshold Value slider (and vice versa), keeping both controls synchronized. The text readout's LIGHT/DARK classification and its background color (warm gold above threshold, deep indigo below) update immediately as either slider moves, so students can directly see calibration change the classification boundary without changing the sensor reading itself.
Implementation: p5.js. Simulated ADC value computed as a simple monotonic function of the light-level slider plus a small amount of random jitter (±500) added each frame to mimic real sensor noise. Circuit schematic drawn once and cached; only the gauge, marker, and readout redraw each frame. Use map() to convert between the 0-65535 ADC range and pixel positions on the gauge, recalculated in windowResized().
A Wider Sensor Toolkit: Tilt, Reed, and Magnetic Switches¶
Push buttons and photoresistors cover two of the most common sensing needs, but plenty of projects call for sensors that notice something more specific. Several of these are, at heart, still simple digital switches -- they just close or open their circuit in response to something other than a finger.
A tilt switch is a small sealed component containing a loose conductive ball (or blob of mercury in older designs) that rolls to complete or break a circuit depending on the switch's orientation -- tip it past a certain angle and the connection made or lost. It reads with the same Pin.IN digital-read pattern as a push button; the only difference is what causes the pin to change state. A reed switch is a switch whose two thin metal contacts snap together only when a magnet is held near it, and is a common example of a magnetic sensor -- any sensor that responds to a nearby magnetic field rather than to direct physical contact. Reed switches show up constantly in security systems, where a magnet mounted on a door frame and a reed switch mounted on the door itself let a program detect "door open" the instant the magnet moves away.
Berry's Key Insight
A tilt switch and a reed switch both plug into a digital pin exactly like a push button does -- same Pin.IN, same debounce concerns if the trigger is mechanical. Once you've learned to read one digital switch, you've basically learned to read all of them; only the physical thing that closes the circuit changes.
Sensing Temperature and Humidity¶
Not every quantity worth measuring reduces to a single voltage the way light level does. A temperature sensor measures the ambient heat of its surroundings and reports it as a value a program can use, and a humidity sensor does the same for the amount of water vapor in the air. Rather than wiring these as raw analog voltage dividers, most STEM kits use an integrated sensor module that measures both at once and hands your code an already-processed reading. The DHT11 sensor is exactly that: a low-cost combined temperature-and-humidity sensor module that communicates over a single digital data pin using its own small timing-based protocol, returning both a temperature value (in Celsius) and a relative humidity percentage from one read.
import dht
from machine import Pin
sensor = dht.DHT11(Pin(16))
sensor.measure()
print("Temperature:", sensor.temperature(), "C")
print("Humidity:", sensor.humidity(), "%")
The sensor.measure() call triggers the DHT11's internal read cycle -- it must be called before either value method, since the sensor doesn't push data on its own. temperature() and humidity() then return the two most recently measured values as separate numbers, already converted from the sensor's internal timing signal into the units shown.
The DHT11 is only one option in a small family of temperature sensors, each trading off accuracy, wiring, and cost differently -- the poster below compares it against three others you may encounter in other kits.
Diagram: Temperature Sensors Compared¶
Run the Temperature Sensors poster fullscreen
Temperature Sensors Compared (reused poster)
Type: interactive-infographic-poster
sim-id: temperature-sensors
Status: Reused
Source: https://dmccreary.github.io/learning-micropython/posters/temperature-sensors/
Source Repo: https://github.com/dmccreary/learning-micropython/tree/main/docs/posters/temperature-sensors
Reused from the companion book Learning MicroPython's poster catalog. Compares this section's DHT11 sensor against the DHT22, BME280, and DS18B20, showing where each fits on accuracy, interface, and cost.
Sensing Sound¶
A sound sensor is a module that detects the presence or loudness of sound in its environment, typically built around a small microphone module -- a component that converts air pressure variations (sound waves) into a small electrical signal. Many breadboard-friendly sound sensor modules include their own onboard amplifier and comparator circuit, so they can offer either a simple digital output ("sound detected" versus "quiet") read with digital read, or an analog output proportional to loudness read with analog read -- the same two techniques you've already used for buttons and photoresistors, just applied to a new physical quantity.
Now that tilt switches, reed switches, magnetic sensing, temperature and humidity sensing, and sound sensing have all been introduced, the infographic below organizes them side by side so you can compare what each one actually measures.
Diagram: Sensor Survey Interactive Infographic¶
Run the Sensor Survey Interactive Infographic MicroSim fullscreen
Sensor Survey Interactive Infographic
Type: interactive-infographic
sim-id: sensor-survey-infographic
Library: p5.js
Status: Specified
Learning objective: Students will classify (Bloom L2: Understand) a set of physical computing sensors by the physical quantity each one measures and whether it is typically read as digital or analog.
Canvas: 700x480px, responsive -- recompute a cardWidth from windowWidth in windowResized() so a 4x2 card grid above 640px collapses to a 2x4 grid between 400-640px and a 1x8 stacked list below 400px.
Layout: eight rounded-rectangle cards, one each for Tilt Switch, Reed Switch, Magnetic Sensor, Temperature Sensor, DHT11 Sensor, Humidity Sensor, Sound Sensor, and Microphone Module. Each card shows the component name, a small icon drawn with basic p5.js shapes representing its physical form factor, and a colored dot indicating Digital (circuit green #2E7D32) or Analog (raspberry #C2185B) typical read mode.
Interaction: clicking a card flips it to reveal a one-sentence description of what physical quantity it measures and one real-world example use case, drawn from the surrounding chapter prose (e.g., clicking "Reed Switch" reveals "Detects a nearby magnet -- used in door and window security sensors"). Hovering a card (desktop) or single-tapping it (touch) highlights its border before a second interaction flips it, so the widget supports both input styles. A createButton() labeled "Flip All" toggles every card between description-visible and description-hidden simultaneously, useful for quick review.
Implementation: p5.js. Card data stored as an array of eight objects {name, quantity, example, mode, isFlipped}. Draw cards with rect() using rounded corners; render flip state as an instantaneous swap of displayed text rather than a true 3D animation, to keep the implementation simple and fast on low-powered classroom devices. Hit-testing via rectangle bounds-checking in mousePressed(). Recompute the grid layout from windowWidth inside windowResized().
Making Noise: Buzzers and Tone Generation¶
Sensors give a project ears; buzzers give it a voice. A piezo buzzer is a small output component that produces sound by rapidly vibrating a piezoelectric disc in response to an electrical signal -- the same basic physics behind a piezo lighter, run in reverse to move air instead of spark a flame. Piezo buzzers come in two distinct varieties that are easy to confuse but behave very differently in code.
A passive buzzer has no built-in oscillator circuit; it simply vibrates at whatever frequency the signal driving it changes, which means your code controls the exact pitch produced. An active buzzer has a tiny built-in oscillator that produces one fixed tone the moment it receives power -- send it a simple HIGH signal and it beeps at its one built-in pitch, with no pitch control available in code at all. The table below summarizes that difference now that both terms have been explained.
| Passive Buzzer | Active Buzzer | |
|---|---|---|
| Built-in oscillator | No -- needs a changing signal | Yes -- built in |
| Pitch control from code | Full control over frequency | None; one fixed pitch |
| Typical driving code | PWM signal at a chosen frequency | Simple digital HIGH/LOW |
| Good fit for | Melodies, alarms with distinct tones | Simple "beep" notifications |
Driving a passive buzzer to produce a specific musical note is called tone generation -- using PWM (which you met in an earlier chapter) to output a square wave at the exact frequency of the note you want to hear, since a note's pitch is entirely determined by how many times per second the wave repeats.
from machine import Pin, PWM
buzzer = PWM(Pin(18))
buzzer.freq(440) # 440 Hz is the musical note A4
buzzer.duty_u16(32768) # 50% duty cycle -- audible tone
buzzer.freq(440) sets the PWM signal's frequency in hertz, and since the buzzer is passive, that frequency is exactly the pitch produced -- 440 Hz happens to be the standard tuning pitch for the musical note A above middle C. buzzer.duty_u16(32768) sets the PWM duty cycle to 50% (half of the maximum 65535), which is the setting that produces the loudest, clearest tone for most passive piezo buzzers; setting the duty cycle to 0 silences the buzzer entirely without changing its frequency setting.
Let's hear (and see) that relationship between frequency and pitch before moving on to how a program stays reliable while reading many sensors and driving buzzers at once.
Diagram: Buzzer Tone Generator Simulator¶
Run the Buzzer Tone Generator Simulator MicroSim fullscreen
Buzzer Tone Generator Simulator (MicroSim)
Type: microsim
sim-id: buzzer-tone-generator-simulator
Library: p5.js
Template: https://github.com/dmccreary/signal-processing/tree/main/docs/sims/tone-gen
Status: Specified
Learning objective: Students will compare (Bloom L4: Analyze) how active and passive buzzers respond to code, and apply (Bloom L3: Apply) a frequency value to generate a target musical pitch on a simulated passive buzzer.
Canvas: 700x420px, responsive -- recompute waveform plot width from windowWidth in windowResized().
Layout: a mode toggle at the top switching between "Passive Buzzer" and "Active Buzzer" views. In Passive mode, a live square-wave plot shows the PWM signal at the current frequency, with a note-name readout (e.g., "440 Hz -- A4") beneath it. In Active mode, the plot is replaced with a static fixed-frequency wave and a text note reading "Active buzzers ignore frequency code -- always the same pitch," and the frequency slider becomes visibly disabled.
Controls: a createSelect() dropdown for buzzer mode (Passive / Active, default Passive); a createSlider() labeled "Frequency (Hz)" (range 220-880, default 440, step 1, disabled in Active mode); a createSlider() labeled "Duty Cycle (%)" (range 0-100, default 50) that visibly compresses or widens each pulse in the waveform plot and, at 0%, flattens the wave to silence; a createButton() labeled "Play Tone" that uses the Web Audio API (via a simple osc = new OscillatorNode(...) or p5.sound p5.Oscillator) to actually sound the selected frequency for 500 ms, so students hear the pitch that matches the plotted wave.
Interaction: moving the Frequency slider live-updates both the waveform plot and the note-name readout (computed from the standard equal-tempered frequency table); clicking "Play Tone" plays the audible pitch and briefly flashes the waveform plot's border to sync sound with visual. Switching to Active mode and clicking "Play Tone" always plays the same fixed reference pitch regardless of slider position, reinforcing that active buzzers ignore frequency input.
Implementation: p5.js with p5.sound (or raw Web Audio API) for actual tone playback; waveform rendered with beginShape()/vertex() tracing a square wave computed from the frequency and duty-cycle values. Guard audio playback behind the "Play Tone" button click (required by browser autoplay policy) rather than playing automatically.
Writing a Reliable Sensor Reading Loop¶
Every sensor in this chapter eventually needs to be read repeatedly, not just once, so a program can react as conditions change. A sensor reading loop is the recurring block of code -- almost always the body of a while True: loop -- that reads one or more sensors, processes their values, and acts on the result, over and over, for as long as the program runs. How often that loop actually samples a sensor is its sampling rate: the number of readings taken per second, usually controlled by how long the loop pauses (or how much other work it does) between iterations.
Sampling rate matters because real sensor readings are rarely perfectly smooth. Noise filtering is the general practice of processing a sequence of raw sensor readings to reduce random fluctuation without losing the real underlying trend -- separating the signal you actually care about from small, meaningless jitter caused by electrical interference, tiny light flickers, or sensor imperfections. One of the simplest and most widely used noise-filtering techniques is the moving average filter: instead of trusting each raw reading on its own, the filter keeps the last several readings in a small buffer and reports their average, so a single noisy spike gets diluted by the readings around it rather than triggering a false decision on its own.
readings = []
WINDOW_SIZE = 5
def moving_average(new_value):
readings.append(new_value)
if len(readings) > WINDOW_SIZE:
readings.pop(0) # drop the oldest reading
return sum(readings) / len(readings)
WINDOW_SIZE sets how many recent readings the filter averages together -- a small window (like 5) reacts quickly to real changes but filters out less noise, while a larger window smooths more aggressively but reacts more slowly to a genuine change in the sensor's environment. readings.pop(0) is what keeps the buffer from growing forever: once it's full, the oldest reading is discarded to make room for the newest one, which is exactly what makes it a moving average rather than a running total of everything ever read.
Berry's Tip
A moving average filter and a debounce delay solve a similar-sounding problem in very different ways -- debounce ignores readings for a short time after a change, while a moving average blends several readings together continuously. Use debounce for a mechanical switch's brief bounce; use a moving average for a continuously noisy analog sensor like a photoresistor or a sound sensor.
Before the closing diagram, it's worth connecting sampling rate and window size directly: a moving average filter's window size is measured in number of readings, but how much real time that window covers depends entirely on the sampling rate -- five readings taken at 100 samples per second cover only 50 milliseconds of history, while the same five readings taken at 2 samples per second cover 2.5 full seconds.
Diagram: Moving Average Filter Simulator¶
Run the Moving Average Filter Simulator MicroSim fullscreen
Moving Average Filter Simulator (MicroSim)
Type: microsim
sim-id: moving-average-filter-simulator
Library: p5.js
Status: Specified
Learning objective: Students will evaluate (Bloom L5: Evaluate) how window size and sampling rate together affect a moving average filter's tradeoff between responsiveness and noise reduction.
Canvas: 700x480px, responsive -- recompute plot width from windowWidth in windowResized().
Layout: a single scrolling strip-chart plot showing two overlaid traces against a shared time axis -- a thin, jittery gray line for "Raw Sensor Reading" and a thicker raspberry-red #C2185B line for "Filtered (Moving Average)." The raw trace is generated from a smooth underlying signal (a slow sine wave representing a real change, such as a hand slowly passing over a photoresistor) plus random noise added each sample. A live readout beneath the plot displays the current window size in both "readings" and the equivalent time span in milliseconds, computed from the sampling rate control.
Controls: a createSlider() labeled "Window Size (readings)" (range 1-30, default 5); a createSlider() labeled "Sampling Rate (samples/sec)" (range 1-50, default 10); a createSlider() labeled "Noise Level" (range 0-100, default 40) controlling the amplitude of random jitter added to the raw signal; a createButton() labeled "Reset Trace" that clears the scrolling history and restarts the underlying sine wave from zero.
Interaction: as Window Size increases, the filtered trace visibly smooths and its peak lags further behind the raw trace's peak, with a small arrow annotation appearing once lag exceeds roughly 300 ms labeled "Filter lag." As Noise Level increases with a small window size, the filtered trace visibly still jitters, demonstrating that a too-small window under-filters; setting Window Size to 1 makes the filtered trace exactly equal the raw trace, visually proving the boundary case. Hovering anywhere on the plot shows a tooltip with the raw and filtered values at that time.
Implementation: p5.js, new samples pushed onto a fixed-length history array at intervals controlled by the Sampling Rate slider (via millis() timing rather than frameRate(), so it stays correct regardless of the browser's actual frame rate). Moving average recomputed each new sample from the last windowSize raw values. Scrolling implemented by shifting all plotted x-positions left each frame and dropping points that scroll off the left edge.
Bringing It Together¶
Every sensor you wired up in this chapter -- button, photoresistor, tilt switch, reed switch, DHT11, sound sensor -- ultimately funnels into the same two software patterns: read the pin (digital or analog), and then clean up what you read (debounce, threshold, or filter) before trusting it enough to act on. That's the same sense-think-act cycle from Chapter 1, just with a much bigger toolbox of "sense" now available to you. The next chapter puts this whole toolbox to work on a moving target: a STEM robot that uses exactly these techniques -- debounced buttons, calibrated distance thresholds, and reliable sensor reading loops -- to decide, on its own, when to stop, turn, or keep going.
You Unlocked a Superpower!
That's a berry impressive sensor toolkit you just built! Buttons, light, temperature, sound, magnets -- your Pico can notice all of it now, and filter out the noise while it's at it. Let's build something that moves with all this new awareness. See you in Chapter 7!