Learning Goals 5 min
- Wire a piezo disc backwards — as an analog input rather than an output — so it senses vibration instead of making sound.
- Detect a single knock cleanly: read the peak voltage on the analog pin, threshold it, and use a short cooldown so one knock doesn't register as five.
- Build a knock counter and a tap-tempo (count knocks per second) — your first real signal-processing project.
Warm-Up 10 min
You've been using the piezo buzzer as an output since L01-14 — apply a voltage at 880 Hz and it sings A5. But the piezo crystal is a two-way device: when you bend it, it generates a tiny voltage. That's the piezoelectric effect — discovered by the Curie brothers in 1880, and the working principle behind quartz watches, ultrasonic transducers, BBQ lighters, and the strange little disc sitting on your bench.
Try it before you wire it
Even before today's sketch: take a piezo disc, connect a multimeter on its 200 mV range to the two leads. Tap the disc gently with your fingernail. The needle (or digits) jump briefly — you're reading the voltage you generated by deforming the crystal. That's the entire principle of today's sensor.
New Concept · The piezo as a knock sensor 20 min
Wiring — almost the same as the buzzer
| Piezo lead | Connect to |
|---|---|
| Red (+) | A0 (analog input) |
| Black (−) | GND |
| 1 MΩ resistor | Between A0 and GND, in parallel with the piezo |
Two new ideas to unpack:
- The piezo is now an INPUT. No
pinModeneeded (A0 defaults to input). The piezo's tiny voltage shows up at A0 and we read it withanalogRead. - The 1 MΩ "bleed" resistor. A piezo is essentially a tiny capacitor — once you give it a charge it holds it for a while. Without the resistor, every knock leaves A0 stuck at a high reading for seconds. The 1 MΩ resistor slowly drains the charge so each knock is a sharp, short spike. Don't skip it.
What the analog reading looks like
Sitting still: raw hovers around 0–10. Tap the disc with a fingernail: raw briefly spikes to 200–800 for one or two readings, then collapses back to ~0. Hit it harder: spike goes to 900+. The reading is roughly proportional to how hard the disc was struck.
Detecting a knock — the cooldown idea
A naive sketch like:
if (analogRead(A0) > 100) Serial.println("KNOCK");will print KNOCK five times in a row for one tap — because one physical knock produces a spike that lasts a few milliseconds, which crosses your loop() a handful of times. The fix is a cooldown: after detecting a knock, ignore the sensor for the next 100–200 ms.
const int THRESHOLD = 100;
const unsigned long COOLDOWN_MS = 150;
unsigned long lastKnock = 0;
void loop() {
int raw = analogRead(KNOCK);
if (raw > THRESHOLD && millis() - lastKnock > COOLDOWN_MS) {
Serial.print("KNOCK strength: "); Serial.println(raw);
lastKnock = millis();
}
}Exactly the same pattern you used for the non-blocking beep in L02-12 and the rain-reminder in L02-17. Pattern: a millis() gate on top of a threshold.
Worked Example · Knock counter 20 min
Step 1 — wiring
Piezo + on A0, piezo − on GND, 1 MΩ across A0 and GND. Tape the piezo to the breadboard or the desk with masking tape so it's mechanically coupled.
Step 2 — the sketch
Save as knock-counter.ino:
// L02-18: Piezo as a knock sensor with cooldown
const int KNOCK = A0;
const int THRESHOLD = 100;
const unsigned long COOLDOWN_MS = 150;
unsigned long lastKnock = 0;
int knockCount = 0;
void setup() {
Serial.begin(9600);
Serial.println("Tap the piezo to count knocks.");
}
void loop() {
int raw = analogRead(KNOCK);
if (raw > THRESHOLD && millis() - lastKnock > COOLDOWN_MS) {
knockCount++;
lastKnock = millis();
Serial.print("KNOCK #");
Serial.print(knockCount);
Serial.print(" strength: ");
Serial.println(raw);
}
}Step 3 — upload and tap
Open Serial Monitor. Tap the piezo with one fingernail. You should see one line per tap, with a strength number that varies with how hard you tap:
Tap the piezo to count knocks. KNOCK #1 strength: 180 KNOCK #2 strength: 240 KNOCK #3 strength: 540 KNOCK #4 strength: 95 ← too soft, won't appear KNOCK #4 strength: 380
Soft taps below the threshold are silently ignored — that's working as designed. If the count is missing taps you wanted, lower the threshold. If it's catching footsteps you didn't want, raise it.
Step 4 — verify the cooldown
Tap the piezo really hard once. Without the cooldown, the spike would print KNOCK 3–5 times. With it, you should see exactly one KNOCK per tap, even hard ones. If you see doubles, raise COOLDOWN_MS to 200 or 250.
Step 5 — quick tempo check
Tap a steady rhythm — once per second for 10 seconds. Count the knock messages. Should be 9 or 10, not 50. The cooldown is keeping you honest.
Try It Yourself 20 min
Goal: Light an LED on D9 for 200 ms every time a knock is detected. Visual feedback so you don't need to watch the Serial Monitor.
Hint
Inside the knock-detection if-block, flash the LED:
digitalWrite(LED, HIGH);
delay(50); // short blocking flash — OK for visual feedback
digitalWrite(LED, LOW);For a non-blocking version (cleaner): store a flashUntil = millis() + 200 and have a separate line outside the if-block that does digitalWrite(LED, millis() < flashUntil).
Goal: Classify each knock by strength: soft (100–250), medium (250–600), hard (> 600). Print the label alongside the strength.
Hint
const char* strengthFor(int s) {
if (s < 250) return "soft";
if (s < 600) return "medium";
return "hard";
}Then in the knock block: Serial.print(" "); Serial.println(strengthFor(raw));. Same labelling pattern you've used since L02-12.
Goal: Compute and display the tap tempo — beats per minute (BPM) — based on the gap between the last two knocks. So a tap once per second = 60 BPM, twice per second = 120 BPM.
Hint
Save the previous knock's millis() stamp. On every new knock, compute gap_ms = millis() - prevKnock and bpm = 60000 / gap_ms. Print it:
unsigned long prevKnock = 0;
// inside the knock-detection block:
if (prevKnock > 0) {
unsigned long gap = millis() - prevKnock;
int bpm = 60000 / gap;
Serial.print(" gap: "); Serial.print(gap);
Serial.print(" ms "); Serial.print(bpm); Serial.println(" BPM");
}
prevKnock = millis();Useful tip: a steady 60 BPM (one tap per second) is what a metronome calls Largo. 120 BPM is Allegro. Try to tap a steady 90 BPM by feel and check the readout — it's harder than it sounds.
Mini-Challenge · Secret-knock unlock 15 min
Build the classic "shave-and-a-haircut" secret-knock detector. The sketch must:
- Wait for a knock pattern matching 5 knocks with specific gaps (the famous shave-and-a-haircut rhythm: short-short-long-short-short, with the last two knocks slightly faster than the others).
- If the pattern matches within ±30% tolerance on each gap → flash an LED on D9 for 2 seconds (the "door unlocked" signal).
- If the pattern is wrong, do nothing — wait for the next attempt.
- After matching or after 5 seconds without a knock, reset the counter and try again.
Suggested target rhythm (millisecond gaps between knocks): 250, 250, 500, 250. So tap a steady rhythm with one slightly longer pause in the middle.
It's done when:
- The LED unlocks reliably when you tap the correct rhythm.
- Tapping wrong (e.g. all even, or only 4 knocks, or 6) does not unlock.
- After 5 seconds of silence the buffer clears — no stale partial patterns.
- You can see the recorded gaps in the Serial Monitor for debugging.
Reveal one valid sketch
const int KNOCK = A0;
const int LED = 9;
const int THRESHOLD = 100;
const unsigned long COOLDOWN_MS = 150;
const unsigned long PATTERN_TIMEOUT = 5000;
// Target gaps (ms): 250, 250, 500, 250 → 5 knocks total
const int TARGET_GAPS[] = { 250, 250, 500, 250 };
const int TARGET_COUNT = 4;
const float TOLERANCE = 0.30;
unsigned long lastKnock = 0;
unsigned long firstKnock = 0;
unsigned long gaps[10];
int gapIdx = 0;
void resetPattern() {
gapIdx = 0;
firstKnock = 0;
Serial.println("(buffer reset)");
}
bool patternMatches() {
if (gapIdx != TARGET_COUNT) return false;
for (int i = 0; i < TARGET_COUNT; i++) {
int target = TARGET_GAPS[i];
int low = target * (1 - TOLERANCE);
int high = target * (1 + TOLERANCE);
if (gaps[i] < low || gaps[i] > high) return false;
}
return true;
}
void setup() {
pinMode(LED, OUTPUT);
Serial.begin(9600);
Serial.println("Knock shave-and-a-haircut to unlock.");
}
void loop() {
int raw = analogRead(KNOCK);
// timeout
if (gapIdx > 0 && millis() - lastKnock > PATTERN_TIMEOUT) {
resetPattern();
}
if (raw > THRESHOLD && millis() - lastKnock > COOLDOWN_MS) {
unsigned long now = millis();
if (gapIdx == 0) {
firstKnock = now;
gapIdx = 0; // ready to record gaps
} else if (gapIdx < 10) {
gaps[gapIdx - 1] = now - lastKnock;
Serial.print("gap "); Serial.print(gapIdx);
Serial.print(": "); Serial.println(gaps[gapIdx - 1]);
}
gapIdx++;
lastKnock = now;
if (gapIdx == TARGET_COUNT + 1) {
if (patternMatches()) {
Serial.println(">> UNLOCKED");
digitalWrite(LED, HIGH);
delay(2000);
digitalWrite(LED, LOW);
} else {
Serial.println(">> wrong pattern");
}
resetPattern();
}
}
}Two new ideas in there: storing several knock-gaps in an array (we'll formalise arrays in L03-39), and checking each gap against a tolerance window. Try tweaking TARGET_GAPS to your own secret rhythm — make it long and unique so a random visitor can't guess it.
Recap 5 min
A piezo is two-way: drive it with a voltage and it sings; bend it with mechanical force and it generates a voltage. Today you used it backwards — as a knock sensor on an analog pin, with a 1 MΩ bleed resistor across it to drain the charge between knocks. The detection pattern was the now-familiar threshold-plus-cooldown: register a knock when the analog reading exceeds a level, then ignore the pin for 100–200 ms so a single physical tap doesn't produce a flood of detections. With that pattern + an array of gap timings you can recognise rhythms — the secret-knock unlock is the classic "hello, embedded signal processing" demo.
- Piezoelectric effect
- The two-way property of certain crystals (quartz, ceramic discs): apply a voltage → the crystal deforms (buzzer); deform the crystal → it produces a voltage (sensor).
- Knock sensor
- Any sensor that detects a sharp mechanical impulse — typically a piezo, but accelerometers count too. Differs from a microphone (steady audio) in that it's tuned for spikes.
- Bleed resistor
- A high-value resistor across a capacitor (or in this case the piezo) to drain stored charge slowly so each impulse is sharp and short rather than long and smeared.
- Cooldown
- A short period after a detection during which we ignore the sensor — prevents one physical event from producing many software events.
- Threshold + cooldown pattern
- The general recipe for "detect a discrete event from a continuous sensor": trigger when the reading exceeds a level AND enough time has passed since the last trigger.
- Tap tempo
- The rate of repeated knocks, usually expressed as beats per minute. Calculated from the gap between consecutive knock timestamps.
Homework 5 min
Tap analysis. Use your knock counter to investigate three surfaces. For each, tap the piezo against the surface (or tape the piezo to the surface and tap nearby) with a consistent fingernail-strength tap, ten times:
- Wooden desk.
- Hard plastic (like a phone case or a textbook's cover).
- Something soft (sofa armrest, a stack of papers, anything that doesn't vibrate well).
Record the strength number for each tap. Compute the average for each surface. Then answer:
- Which surface gave the loudest readings? Why might that be?
- Which surface gave the most consistent readings (smallest range between tap 1 and tap 10)?
- If you were building a "door knock detector", would you mount the piezo on the door, the door frame, or the floor near the door? Justify briefly.
Bring back next class:
- Your three-surface comparison table (30 readings + 3 averages).
- Your three written answers.
- Your
hw-l02-18.inosketch — tomorrow we combine sensors with proper boolean logic.