Learning Goals 5 min
- Wire a soil-moisture probe (resistive or capacitive) and measure its analog output in three soil states: bone dry, finger-damp, and freshly watered.
- Re-use last lesson's 5-second calibration template — only the meaning of "dark" and "bright" changes (now "dry" and "wet").
- Understand the trade-off between cheap resistive probes (corrode in days) and pricier capacitive probes (last for years) — and pick the right one for the right job.
Warm-Up 10 min
If you spotted that the light meter, the thermistor and the LDR all share the same skeleton — read raw, smooth, convert, classify — you're ready for the soil probe. It's the same shape, just with a different sensor and a new vocabulary: dry instead of dark; wet instead of bright.
What the probe actually measures
The cheap resistive probe is literally two metal forks stuck in the soil. Water in the soil makes it more conductive — meaning current flows more easily between the forks — meaning the resistance between them drops. Just like the LDR with light. The capacitive probe is fancier: it measures how much the soil distorts an electric field around a ceramic blade. Both kinds output an analog voltage you read with analogRead().
Quick puzzle
On a typical resistive soil probe wired with the module's output going to A0, dry soil reads roughly 850–1023 and freshly watered soil reads 300–500. The water made the analog reading drop. Why?
Reveal
The probe forms one leg of a voltage divider on the module's built-in board. Wet soil = lower resistance between forks = the module's output voltage drops toward GND = lower analog reading. Dry soil = high resistance = output stays near +5V = high analog reading. Exactly the same physics as the LDR divider — but the convention here is "higher number = drier" rather than "higher number = brighter".
New Concept · Soil probes and how they age 20 min
Wiring (both probe types)
| Module pin | Connect to |
|---|---|
| VCC (or +) | +5 V |
| GND (or −) | GND |
| A0 (analog out) | UNO A0 |
| D0 (digital out, optional) | Leave unconnected |
Three wires. The D0 pin on the module is a digital threshold output set by the little blue trim-pot on the board — we ignore it; analog is more flexible.
Resistive vs capacitive — when to pick which
| Resistive (forks) | Capacitive (blade) | |
|---|---|---|
| Price | ~RM 3 | ~RM 15 |
| How it ages | Forks corrode in 1–3 weeks | Sealed in epoxy, lasts years |
| Power-on time | Always on → corrodes fast | Always on → no corrosion |
| Accuracy | Drifts as forks tarnish | Stable |
| Best for | Today's lesson, short demos | Real plant projects |
The pro trick for resistive probes: only power them while reading. Wire VCC to a digital pin (not 5 V), set that pin HIGH for 50 ms before the read, then LOW. That extends the probe's life from days to months. Code-wise:
pinMode(7, OUTPUT); // VCC to D7 instead of 5V
digitalWrite(7, HIGH); // power up
delay(50); // let it settle
int raw = analogRead(A0);
digitalWrite(7, LOW); // power downThe calibration template — same shape as L02-15
The 5-second cover-then-shine routine becomes a "dry then wet" routine: leave the probe in dry air for 2 s, then dip it into a cup of water for 2 s. Save dryRaw (highest seen) and wetRaw (lowest seen). Map every later reading into a 0–100 percent moisture scale where 0% = dry and 100% = wet.
int clipped = constrain(rawNow, wetRaw, dryRaw);
int pct = map(clipped, dryRaw, wetRaw, 0, 100); // inverted: dry → 0, wet → 100Notice how map() handles the inversion: by listing dryRaw first and wetRaw second, it knows to flip the direction. No need for clever subtractions.
Worked Example · Three-state moisture display 20 min
Step 1 — wiring
Module + 3 wires to the UNO as above. Get a small cup of water and a dry paper towel ready before you upload.
Step 2 — the sketch
Save as soil-moisture.ino:
// L02-16: Soil moisture probe with 5-second calibration
const int SOIL = A0;
int dryRaw = 0; // highest value seen during calibration
int wetRaw = 1023; // lowest value seen during calibration
void calibrate() {
Serial.println("Calibrating — hold probe in air, then dip in water (5 s)...");
unsigned long t0 = millis();
while (millis() - t0 < 5000) {
int r = analogRead(SOIL);
if (r > dryRaw) dryRaw = r;
if (r < wetRaw) wetRaw = r;
}
if (dryRaw - wetRaw < 50) {
dryRaw = 1023; wetRaw = 0;
Serial.println("(no clear range — using 0..1023 fallback)");
}
Serial.print("dry="); Serial.print(dryRaw);
Serial.print(" wet="); Serial.println(wetRaw);
}
const char* labelFor(int pct) {
if (pct < 20) return "bone dry";
if (pct < 50) return "dry";
if (pct < 80) return "moist";
return "soggy";
}
void setup() {
Serial.begin(9600);
delay(500);
calibrate();
}
void loop() {
int raw = analogRead(SOIL);
int clipped = constrain(raw, wetRaw, dryRaw);
int pct = map(clipped, dryRaw, wetRaw, 0, 100);
Serial.print("raw: "); Serial.print(raw);
Serial.print(" pct: "); Serial.print(pct);
Serial.print("% "); Serial.println(labelFor(pct));
delay(500);
}Step 3 — upload, calibrate
Open Serial Monitor. Quickly: hold the probe in mid-air (~2 s) → then dunk just the metal forks into a cup of water (~2 s). The sketch will print the dry/wet pair it learned:
Calibrating — hold probe in air, then dip in water (5 s)... dry=1018 wet=312 raw: 985 pct: 4% bone dry raw: 980 pct: 6% bone dry
Step 4 — three soil states
You'll need either a small flower pot of soil, or a plastic cup of soil with a teaspoon of water nearby. Try:
- Stick probe into dry soil → expect 5–20%, label "bone dry" or "dry".
- Add one teaspoon of water, wait 30 s, re-read → expect 30–60%, label "dry" or "moist".
- Add another two teaspoons, mix slightly, re-read → expect 60–95%, label "moist" or "soggy".
The label flow is the whole point — the project went from a vague "raw 612" to a meaningful "moist". That's the L2 polish.
Step 5 — pull the probe out and wipe it
If you're using the cheap resistive probe, wipe the forks dry with a paper towel after every session. Tarnish from prolonged contact with wet soil + electricity is a real thing and it'll make tomorrow's readings drift. Capacitive probes don't care.
Try It Yourself 20 min
Goal: Add an LED on D9. Light it whenever the label is "bone dry". The classic "your plant is thirsty" reminder.
Hint
digitalWrite(9, (pct < 20) ? HIGH : LOW);Same exact pattern as last lesson's night-light. The template is paying off.
Goal: Power the probe only while reading, to slow corrosion. Move VCC from +5 V to D7; power D7 HIGH for 50 ms before each analogRead, then LOW.
Hint
const int SOIL_PWR = 7;
int readSoil() {
digitalWrite(SOIL_PWR, HIGH);
delay(50);
int r = analogRead(SOIL);
digitalWrite(SOIL_PWR, LOW);
return r;
}In setup: pinMode(SOIL_PWR, OUTPUT); Replace every analogRead(SOIL) with readSoil() — including inside calibrate(). Probe life: weeks → months.
Goal: Track a soil-drying curve. Water a flower pot generously, plug in the probe, log percent every 5 minutes for an hour. Paste into a spreadsheet, chart it. You've made a soil-evaporation experiment.
Hint
Use a millis() gate instead of delay(300000):
unsigned long lastLog = 0;
const unsigned long LOG_INTERVAL = 300000; // 5 min
// in loop, after computing pct:
if (millis() - lastLog >= LOG_INTERVAL) {
Serial.print(millis() / 60000); Serial.print(",");
Serial.println(pct);
lastLog = millis();
}You'll get one line per 5 minutes. After an hour, ~12 lines. Plot "minutes since start" on X, "moisture %" on Y. Expect a smooth downward curve.
Mini-Challenge · Thirsty-plant alarm 15 min
Use the soil probe, a green LED (D9), a red LED (D10), and the buzzer (D8). Build the alarm a forgetful plant-owner would actually use:
Behaviour spec:
- Green ON while soil is "moist" or "soggy". ("Plant is fine.")
- Red ON while soil is "dry" or "bone dry".
- Buzzer chirps every 30 s while soil is "bone dry". ("PLEASE water me.")
- Five-second calibration on boot.
- Reads probe every 1 s, in a non-blocking loop.
It's done when:
- Dipping the probe in water → green within 1 s, no chirp.
- Pulling probe out into air → red within 5 s, chirp starts within 30 s.
- The chirp never fires more than once every 30 s.
- The Serial Monitor mirrors the LED + chirp state in plain English.
Reveal one valid sketch
const int SOIL = A0;
const int GREEN = 9;
const int RED = 10;
const int BUZZ = 8;
int dryRaw = 0, wetRaw = 1023;
unsigned long lastBeep = 0;
unsigned long lastRead = 0;
void calibrate() {
Serial.println("Calibrating (air then water, 5 s)...");
unsigned long t0 = millis();
while (millis() - t0 < 5000) {
int r = analogRead(SOIL);
if (r > dryRaw) dryRaw = r;
if (r < wetRaw) wetRaw = r;
}
if (dryRaw - wetRaw < 50) { dryRaw = 1023; wetRaw = 0; }
}
void setup() {
pinMode(GREEN, OUTPUT);
pinMode(RED, OUTPUT);
pinMode(BUZZ, OUTPUT);
Serial.begin(9600);
calibrate();
}
void loop() {
if (millis() - lastRead < 1000) return;
lastRead = millis();
int raw = analogRead(SOIL);
int clipped = constrain(raw, wetRaw, dryRaw);
int pct = map(clipped, dryRaw, wetRaw, 0, 100);
bool dry = (pct < 50);
bool boneDry = (pct < 20);
digitalWrite(GREEN, dry ? LOW : HIGH);
digitalWrite(RED, dry ? HIGH : LOW);
if (boneDry && millis() - lastBeep >= 30000) {
tone(BUZZ, 1200, 150);
lastBeep = millis();
}
Serial.print(pct); Serial.print("% ");
Serial.println(boneDry ? "BONE DRY" : dry ? "dry" : "moist");
}Two millis() gates: one for the read cadence, one for the chirp cadence. They don't interfere with each other — that's the whole point of non-blocking timing.
Recap 5 min
The soil moisture probe is the third sensor in three lessons that uses the same Level-2 template: read, smooth (optional), calibrate, map, classify, act. The new wrinkle here is the corrosion problem — resistive probes literally rot when left powered, so we either move to capacitive (more money, less hassle) or pulse the probe's VCC from a digital pin (free, takes 5 extra lines). The lesson also makes a quiet but important point: a soil moisture "%" isn't a precise number. It's a calibrated relative reading good enough for a plant-watering reminder; for laboratory soil science you'd need very different hardware.
- Resistive soil probe
- Two metal forks measuring the conductivity of the soil between them. Cheap, corrodes fast unless pulsed.
- Capacitive soil probe
- A sealed ceramic blade measuring the dielectric of the soil. Pricier, doesn't corrode, lasts years.
- Pulsed power
- Powering a sensor briefly from a digital pin (just for the read) instead of leaving it on full-time — extends the life of corroding probes.
- Dielectric
- How well a material can hold an electric field. Dry soil has a low dielectric, wet soil has a high one — the basis of capacitive moisture sensing.
- Volumetric water content
- The lab definition of soil wetness: volume of water ÷ volume of soil. Requires expensive calibrated probes; not what our reading represents.
- Inverted map
- Using
map(x, hi, lo, 0, 100)instead ofmap(x, lo, hi, 0, 100)to flip the direction without extra arithmetic. We did it because dry = high raw and wet = low raw.
Homework 5 min
Adopt a houseplant for a week. Pick one houseplant at home (or a small cup of soil with a sprouting bean). Each day:
- Read the soil moisture percent.
- Note whether you watered it that day, and how much.
- Note the weather (humid, dry, hot, cool).
Maintain a 7-day table. At the end, answer:
- Roughly how many percent does the moisture drop per day with no watering, in your conditions?
- What was the lowest percent the plant got to before it visibly drooped (if it did)?
- Based on the data, what % threshold would you use for an "auto-water" trigger if you built one? (Hint: pick a percent where the plant is dry but not stressed.)
Bring back next class:
- Your 7-day table.
- Your three written answers.
- Your
hw-l02-16.inosketch — tomorrow we add a rain sensor and start combining inputs.