FreeRTOS Task Scheduling with ESP32: A Smart Irrigation Example

In our last discussion, we explored the basics of FreeRTOS and how it enables multitasking on microcontrollers like the ESP32. Today, we're going further under the hood—examining how the FreeRTOS scheduler works and how tasks are prioritized, delayed, and preempted in real time.

We’ll demonstrate this using a fun, realistic example: a smart irrigation controller running on the ESP32.

🌱 Why FreeRTOS Scheduling Matters

If you’ve ever wondered how multiple things can happen “at the same time” in embedded systems—like reading sensors, blinking LEDs, or responding to inputs—FreeRTOS is the answer. It allows your program to switch between tasks so efficiently that it feels like everything is running in parallel.

⏱️ The FreeRTOS Tick and Scheduler

Here’s what’s really going on under the hood:

  • A special hardware timer (called the tick timer) triggers an interrupt every few milliseconds.

  • This interrupt activates the scheduler, which checks for any task that’s ready to run.

  • The highest-priority task gets to run first.

  • Tasks with the same priority take turns using a method called time slicing.

All of this happens at lightning speed—typically every 1 millisecond!

📋 How FreeRTOS Chooses Tasks

Each task in FreeRTOS is given a priority number. A higher number means higher priority. The scheduler chooses the highest-priority READY task to run.

Tasks can transition between several states:

  • READY: Can run when scheduled

  • RUNNING: Currently executing

  • BLOCKED: Waiting (e.g., due to delay)

  • SUSPENDED: Manually paused

  • DELETED: Terminated by the program

If a high-priority task becomes ready while a lower one is running, preemption occurs—FreeRTOS instantly stops the lower one and gives CPU time to the higher.

🌦️ Example: Smart Irrigation with ESP32

Let’s simulate a smart irrigation controller:

  • Task 1: Watering task — simulates turning on a water pump for 100ms at intervals.

  • Task 2: Soil monitoring task — prints a mock soil moisture level every 50ms.

  • Task 3: Controller task — toggles the watering system on/off every few seconds.

We’ll assign higher priority to the monitoring task, so it can interrupt watering if needed.

The circuit diagram is shown below.

esp32  Smart Irrigation Circuit Diagram

✅ Arduino Code for ESP32

#include <Arduino.h>

// Define GPIO pins for real hardware
#define MOISTURE_SENSOR_PIN 34   // Analog-capable pin (A0 equivalent on ESP32)
#define RELAY_PIN 26             // Digital pin to control relay (pump)

// Task handles
TaskHandle_t ReadTaskHandle;
TaskHandle_t WaterPumpTaskHandle;

// Shared moisture level variable
volatile int moistureLevel = 0;

// Moisture threshold (adjust based on sensor output)
const int dryThreshold = 2000;  // Value below which soil is considered dry

void TaskReadSensor(void *pvParameters) {
  while (1) {
    moistureLevel = analogRead(MOISTURE_SENSOR_PIN);
    Serial.print("Soil Moisture Reading: ");
    Serial.println(moistureLevel);
    vTaskDelay(1000 / portTICK_PERIOD_MS); // 1 second delay
  }
}

void TaskControlPump(void *pvParameters) {
  while (1) {
    if (moistureLevel < dryThreshold) {
      Serial.println("Soil is dry. Activating water pump...");
      digitalWrite(RELAY_PIN, HIGH); // Turn on relay
      vTaskDelay(2000 / portTICK_PERIOD_MS); // Keep pump on for 2 seconds
      digitalWrite(RELAY_PIN, LOW);  // Turn off relay
      Serial.println("Pump deactivated.");
    }
    vTaskDelay(5000 / portTICK_PERIOD_MS); // Check every 5 seconds
  }
}

void TaskLogger(void *pvParameters) {
  while (1) {
    Serial.println("System running...");
    vTaskDelay(3000 / portTICK_PERIOD_MS); // Every 3 seconds
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("ESP32 FreeRTOS Moisture Control");

  pinMode(MOISTURE_SENSOR_PIN, INPUT);
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, LOW); // Make sure pump is OFF initially

  // Create tasks
  xTaskCreate(TaskReadSensor, "ReadMoisture", 2048, NULL, 1, &ReadTaskHandle);
  xTaskCreate(TaskControlPump, "WaterPump", 2048, NULL, 2, &WaterPumpTaskHandle);
  xTaskCreate(TaskLogger, "Logger", 1024, NULL, 1, NULL);
}

void loop() {
  // loop() is not used in FreeRTOS, but it still runs as a task
}

Pins Used:

ComponentESP32 GPIODescription
Soil Moisture SensorGPIO 34ADC input (analogRead)
Relay Module (Pump)GPIO 26Digital output (HIGH to turn ON)

🔍 Serial Output: What You’ll Observe

Watch closely in the Serial Monitor:

  • Moisture readings print every 50ms.

  • Watering messages appear but are interrupted whenever monitoring runs.

  • The controller suspends/resumes watering periodically.

  • Eventually, watering is stopped entirely by deleting the task.

📌 Key Takeaways

  • Tasks with higher priority can interrupt lower-priority tasks in real time.

  • Use vTaskDelay() to pause tasks, allowing others to run.

  • Use vTaskSuspend() and vTaskResume() to dynamically control task execution.

  • Task deletion is permanent—make sure to clean up handles after calling vTaskDelete().

🚀 What's Next?

In the next part, we’ll break down:

Until then—keep experimenting and automating the world around you! 🌎💡

 

Post a Comment

Previous Post Next Post