Learn
Reading Soil Moisture with a Soil Moisture Sensor on a Raspberry Pi 4B
Wire a Soil Moisture Sensor to a Raspberry Pi 4B, read a dry/wet alarm from its digital output (DO), and read the raw moisture level from its analog output (AO) through an MCP3008 ADC. In the simulator, you set how wet the soil is in the settings panel.
Note: the Soil Moisture Sensor is a VIP-tier component in the simulator — you’ll need a VIP plan to use it there.
Try this directly in the free Raspberry Pi 4B simulator — no hardware or signup required. New to the GPIO header? Start with the interactive pinout guide.
What you’ll need
- Raspberry Pi 4B
- Soil Moisture Sensor
- Resistor (1kΩ)
- Resistor (2kΩ)
- MCP3008 ADC (for the analog part)
Step by step
- Drag Raspberry Pi 4B and the Soil Moisture Sensor onto the Canvas.
- Power the sensor:
| Soil Moisture Sensor pin | Pi pin |
|---|---|
| VCC | 5V |
| GND | GND |
The sensor has two outputs. DO is a simple dry/wet alarm: it is HIGH while the soil is dry and goes LOW once the soil is wet enough to reach the sensor's threshold. AO is the raw moisture reading as a voltage, and it works backwards from what you might expect: the drier the soil, the higher the voltage. In the simulator there is no real soil, so you set the moisture level yourself in the settings panel.
- Part 1, the dry/wet alarm (DO). DO swings up to 5V, but the Pi's GPIO pins only tolerate 3.3V, so drop it through a voltage divider:
| From | To |
|---|---|
| Sensor DO | one end of the 1kΩ Resistor |
| other end of the 1kΩ Resistor | Pi physical pin 37 and one end of the 2kΩ Resistor (the divider's middle) |
| other end of the 2kΩ Resistor | Pi GND pin |
1kΩ over 2kΩ turns 5V into about 3.3V at the middle point, which is safe for the Pi. If you wire DO straight to a Pi pin instead, the Console prints a warning about 5V on a 3.3V pin when the script sets the pin up.
- Go to the Code tab and write a script that reports dry or wet:
import RPi.GPIO as GPIOimport asyncio GPIO.setmode(GPIO.BOARD)GPIO.setup(37, GPIO.IN) last = None while True: now = GPIO.input(37) if now != last: # only print when it changes print("Soil is DRY - water it!" if now == 1 else "Soil is WET") last = now await asyncio.sleep(0.1)- Select the sensor and look at its settings panel. Soil moisture is how wet the soil is (0% is bone dry, 100% is saturated), and Wet threshold is the moisture level at which the sensor decides the soil is wet. The Dry, Moist and Wet buttons are quick presets. Click Start to run the script, then Stop, pick another preset, and Start again.
With the default 30% moisture and 50% threshold the soil counts as dry, so DO is HIGH and the script prints "Soil is DRY". Choose the Wet preset (85%) and run again: the moisture is now above the threshold, DO goes LOW, and the sensor's onboard LED lights. The moisture level can only be changed while the simulation is stopped.
- Part 2, the raw moisture level (AO). Drag the MCP3008 ADC onto the Canvas and wire it to the Pi's fixed SPI pins (physical/BOARD numbering):
| MCP3008 pin | Pi pin |
|---|---|
| VDD | 3.3V |
| VREF | 3.3V |
| AGND | GND |
| DGND | GND |
| CLK | Physical pin 23 (SCLK) |
| DOUT | Physical pin 21 (MISO) |
| DIN | Physical pin 19 (MOSI) |
| CS/SHDN | Physical pin 24 (CE0) |
- Wire the sensor's analog output to the ADC:
| From | To |
|---|---|
| Sensor AO | MCP3008 CH0 |
- Replace the script with one that reads the analog level:
import spidevimport asyncio spi = spidev.SpiDev()spi.open(0, 0)spi.max_speed_hz = 1350000 def read_channel(channel): adc = spi.xfer2([1, (8 + channel) << 4, 0]) return ((adc[1] & 3) << 8) + adc[2] # returns 0-1023 while True: value = read_channel(0) print(f"Moisture reading: {value} / 1023 (lower = wetter)") await asyncio.sleep(0.5)AO is inverted: 5V when the soil is completely dry, falling to 0V when it is saturated. The MCP3008 here measures against 3.3V, so a very dry sensor (below about 34% moisture) is above the ADC's range and the reading pins at 1023, with a Console warning that the channel is over range. That warning is real: on hardware you would put a divider on AO too. Set the Moist preset (50%) and you should see about 775; the Wet preset (85%) gives about 232.
What “working correctly” looks like
- Part 1: with the Dry setting the Console prints "Soil is DRY - water it!"; with the Wet preset it prints "Soil is WET" and the sensor's onboard LED lights.
- Lowering the Wet threshold makes the sensor call the soil wet sooner; raising it above the moisture level keeps it reporting dry.
- Part 2: the reading gets smaller as the moisture level goes up: about 775 at 50%, about 232 at 85%, and 1023 (over range) when very dry.
If something’s wrong
- The Console warns about 5V on a 3.3V pin → DO is wired straight to the Pi pin; put the 1kΩ/2kΩ divider between DO and the pin as shown.
- DO always reads the same value → check the Soil moisture against the Wet threshold in the settings panel: DO only goes LOW when the moisture is at or above the threshold. Also make sure VCC goes to 5V and GND to GND.
- The reading goes down when the soil gets wetter → that is correct: like the real sensor, AO is highest when the soil is dry.
- The analog reading is always 0 → check the MCP3008's VDD, VREF, AGND and DGND, that CS goes to physical pin 24 (CE0), and that AO goes to CH0.
- The analog reading pins at 1023 with an over-range warning → the sensor's AO is above the ADC's 3.3V reference at that dryness; raise the moisture level, or add a divider on AO as you would on real hardware.
- The reading doesn't change when you change the moisture → change it while stopped, then Start again.