Embedded Rust - Capacitive Soil Moisture Sensor and the STM32F401

In this chapter, we'll build a simple embedded project using the STM32F401 microcontroller and a capacitive soil moisture sensor. We'll include an LED to indicate the moisture level of the soil. The LED will be blinking when the soil is too dry.

Getting prepared

To build this project, you need the following:

  • Capacitive Soil Moisture Sensor, I've got mine from Az-Delivery,
  • Orange LED,
  • 150Ω resistor,

Wiring

Connect the sensor to the STM board like this:

Capacitive Soil Moisture Sensor schematic

As you can see, the sensor needs 5V to power up. You can use an external power supply or connect the 5V pin of the ST-Link to the sensor, which is what I did.

The sensor has an analog output that we'll connect to the PA7 pin of the STM32F401. The PA7 pin is connected to the ADC1 channel 7.

The LED is connected to the PA0 pin of the STM32F401.


Make main.rs look like this:

#![no_std]
#![no_main]

use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::{
    adc::Adc,
    gpio::{Level, Output, Speed},
    peripherals::{ADC1, PA7},
};
use embassy_sync::{
    blocking_mutex::raw::ThreadModeRawMutex,
    channel::{Channel, Sender},
};
use embassy_time::{Delay, Timer};

use {defmt_rtt as _, panic_probe as _};

const DRYNESS: u16 = 3000;

static CHANNEL: Channel<ThreadModeRawMutex, u16, 64> = Channel::new();

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());

    // orange LED
    let mut led = Output::new(p.PA0, Level::High, Speed::Low);

    info!("Setting up ADC");
    let mut delay = Delay;
    let adc = Adc::new(p.ADC1, &mut delay);
    let pin = p.PA7;

    spawner
        .spawn(read_moisture(adc, pin, CHANNEL.sender()))
        .unwrap();

    let rx_moisture = CHANNEL.receiver();

    loop {
        let alarm_on = rx_moisture.receive().await > DRYNESS;

        // Blink the LED if the moisture is too low
        if alarm_on {
            led.set_high();
            Timer::after_millis(500).await;
        }

        led.set_low();
    }
}

#[embassy_executor::task(pool_size = 2)]
async fn read_moisture(
    mut adc: Adc<'static, ADC1>,
    mut pin: PA7,
    tx_moisture: Sender<'static, ThreadModeRawMutex, u16, 64>,
) {
    loop {
        let v = adc.read(&mut pin);
        info!("ADC value: {}", v);
        tx_moisture.send(v).await;

        Timer::after_millis(1000).await;
    }
}

You need these dependencies in your Cargo.toml:

[dependencies]
embassy-stm32 = { version = "0.1.0", features = ["defmt", "stm32f401cc", "unstable-pac", "memory-x", "time-driver-any", "exti", "chrono"] }
embassy-sync = { version = "0.5.0", features = ["defmt"] }
embassy-executor = { version = "0.5.0", features = ["task-arena-size-32768", "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt", "integrated-timers"] }
embassy-time = { version = "0.3.0", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] }
embassy-usb = { version = "0.1.0", features = ["defmt"] }
embassy-net = { version = "0.4.0", features = ["defmt", "tcp", "dhcpv4", "medium-ethernet", ] }

defmt = "0.3"
defmt-rtt = "0.4"

cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = "0.7.0"
embedded-hal = "0.2.6"
embedded-io = { version = "0.6.0" }
embedded-io-async = { version = "0.6.1" }
panic-probe = { version = "0.3", features = ["print-defmt"] }

Run the project with:

$ cargo run --release

You should see the moisture level printed on the console. The LED should be blinking when the soil is too dry. It could be that you need to adjust the DRYNESS value to match your sensor.