Skip to content

Lab 01 – FreeRTOS Task Creation and Scheduling

Activity Goals

  • Understand FreeRTOS task creation and scheduling
  • Implement GPIO control for LED blinking on ESP32
  • Manage time delays using vTaskDelay
  • Configure task priorities and stack sizes correctly

Materials

No materials required

Code Example

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"

#define LED_GPIO GPIO_NUM_2

static const char *TAG = "LAB1";

static void blink_task(void *pvParameters)
{
    gpio_reset_pin(LED_GPIO);
    gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);
    while (1) {
        gpio_set_level(LED_GPIO, 1);
        vTaskDelay(pdMS_TO_TICKS(300));
        gpio_set_level(LED_GPIO, 0);
        vTaskDelay(pdMS_TO_TICKS(300));
    }
}

static void hello_task(void *pvParameters)
{
    int n = 0;
    while (1) {
        ESP_LOGI(TAG, "hello_task says hi, n=%d", n++);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Starting Lab 1 (two tasks)");
    xTaskCreate(blink_task, "blink_task", 2048, NULL, 5, NULL);
    xTaskCreate(hello_task, "hello_task", 2048, NULL, 2, NULL);
}

Exercises

Priority Experiment: Change hello_task priority from 5 to 2. Does behavior change? Why? - Yes. Lower priority numbers execute less frequently, allowing the higher-priority blink task to run more often.

Starvation Demo: Remove vTaskDelay(...) from hello_task. What happens? - The LED stops blinking because hello_task monopolizes CPU time, starving other tasks. - Fix: Blocking tasks with vTaskDelay allows the scheduler to handle other tasks fairly.

Lab 02

Code Of exercise LAB02

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"

static const char *TAG = "LAB2";
static QueueHandle_t q_numbers;

static void producer_task(void *pvParameters)
{
    int value = 0;
    while (1) {
        value++;
        if (xQueueSend(q_numbers, &value, pdMS_TO_TICKS(50)) == pdPASS) {
            ESP_LOGI(TAG, "Produced %d", value);
        } else {
            ESP_LOGW(TAG, "Queue full, dropped %d", value);
        }
        vTaskDelay(pdMS_TO_TICKS(20));
    }
}

static void consumer_task(void *pvParameters)
{
    int rx = 0;
    while (1) {
        if (xQueueReceive(q_numbers, &rx, pdMS_TO_TICKS(1000)) == pdPASS) {
            ESP_LOGI(TAG, "Consumed %d", rx);
            vTaskDelay(pdMS_TO_TICKS(300));
        } else {
            ESP_LOGW(TAG, "No data in 1s");
        }
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Starting Lab 2 (queue)");
    q_numbers = xQueueCreate(20, sizeof(int));
    if (q_numbers == NULL) {
        ESP_LOGE(TAG, "Queue create failed");
        return;
    }
    xTaskCreate(producer_task, "producer_task", 2048, NULL, 5, NULL);
    xTaskCreate(consumer_task, "consumer_task", 2048, NULL, 5, NULL);
}

Exercises

1. Make the producer faster

  • Change: Producer delay 200ms → 20ms.
  • Effect:
    With this change, the producer generates data every 20 ms, but the consumer does not keep up.
  • Observation: You will see “Queue full” when the producer tries to send data faster than the consumer can process.
  • Reason: The consumer is slower than the producer, so the queue fills up quickly.

2. Increase the queue length

  • Change: Queue length 5 → 20.
  • Effect:
  • The consumer now has more buffer space to store data.
  • After filling those 20 slots, the consumer will start losing data again if it cannot process fast enough.
  • Conclusion: Increasing queue length delays data loss but does not solve the imbalance between producer and consumer speeds.

3. Make the consumer “slow”

  • Change: After a successful receive, add:
    ```c vTaskDelay(pdMS_TO_TICKS(300)); /

## What pattern is happening now (buffering / backlog)?

With this code we can see that the system in the first seconds the producer will load the 20 spaces of the queue.
The consumer is slower, so it will be processing data item 1 while the producer has already sent items 2, 3, 4, ...

Therefore, our queue will always be full, because the consumer takes 300 ms to process a single piece of data, and in that time the producer tried to send 15 new pieces of data (300 / 20 = 15).

Lab 03

Code of exercise LAB03A

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

static const char *TAG = "LAB3A";

static volatile int shared_counter = 0;

static void increment_task(void *pvParameters)
{
    const char *name = (const char *)pvParameters;

    while (1) {
        // NOT safe: read-modify-write without protection
        int local = shared_counter;
        local++;
        shared_counter = local;

        if ((shared_counter % 1000) == 0) {
            ESP_LOGI(TAG, "%s sees counter=%d", name, shared_counter);
        }

        vTaskDelay(pdMS_TO_TICKS(1));
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Starting Lab 3A (race demo)");

    xTaskCreate(increment_task, "incA", 2048, "TaskA", 5, NULL);
    xTaskCreate(increment_task, "incB", 2048, "TaskB", 5, NULL);
}

Part A: Why can the counter be wrong?

The counter can be wrong because Task A copies the counter value into its local memory, but before finishing the update the system pauses Task A and gives control to Task B, which reads the same value (100), increments it to 101, and stores it; when Task A resumes it still believes the counter is 100, increments it to 101, and overwrites Task B’s update, so although two increments were made the counter only increased by 1 and information was lost due to a race condition.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define LED_GPIO GPIO_NUM_2   
#define BUTTON5_GPIO  GPIO_NUM_3
int Read_button5 = 0; // Aquí guardamos la lectura

void read_button_task5(void *pvParameters)
{
    gpio_set_direction(BUTTON5_GPIO, GPIO_MODE_INPUT);
    gpio_set_pull_mode(BUTTON5_GPIO, GPIO_PULLUP_ONLY);
  print(my name is isaac_rec.name)

    while (1) {
        Read_button5 = gpio_get_level(BUTTON5_GPIO);
        vTaskDelay(pdMS_TO_TICKS(20)); // cada 20ms
    }
}
    while (1) {
        Read_button5 = gpio_get_level(BUTTON5_GPIO);
        vTaskDelay(pdMS_TO_TICKS(20)); // cada 20ms
    }
}
// TAG para los mensajes del log (ESP_LOGI)
static const char *TAG = "LAB3A";
struct patient{
    int counter=0;
    char[10] name="";
};

}
static void HEARTBEAT(void *pvParameters)
{
    gpio_reset_pin(LED_GPIO);
    gpio_set_direction(LED_GPIO, GPIO_MODE_OUTPUT);

    while (1) {
        gpio_set_level(LED_GPIO, 1);
        vTaskDelay(pdMS_TO_TICKS(2000));
        gpio_set_level(LED_GPIO, 0);
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

static void Alive(void *pvParameters)
{
    int n = 0;
    while (1) {
        ESP_LOGI(TAG, "Alive, n=%d", n++);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

static QueueHandle_t q_numbers;

static void producer_task(void *pvParameters)
{
    struct patient isaac_sends;
    isaac_sends.name = "isaac"

    while (1) {
        isaac_sends.counter=isaac.counter+1;


        // Send to queue; wait up to 50ms if full
        if (xQueueSend(q_numbers, &isaac, pdMS_TO_TICKS(50)) == pdPASS) {
            ESP_LOGI(TAG, "Produced %d", value);
        } else {
            ESP_LOGW(TAG, "Queue full, dropped %d", value);
        }

        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

static void consumer_task(void *pvParameters)
{
    struct patient isaac_rec;


    while (1) {
        // Wait up to 1000ms for data
        if (xQueueReceive(q_numbers, &isaac_rec, pdMS_TO_TICKS(1000)) == pdPASS) {
            ESP_LOGI(TAG, "Consumed %d", rx);
            print(my name is isaac_rec.name and im counting to isaac_rec.counter)
        } else {
            ESP_LOGW(TAG, "No data in 1s");
        }
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Starting Lab 2 (queue)");

    q_numbers = xQueueCreate(7, sizeof(counter)); // length 5
    if (q_numbers == NULL) {
        ESP_LOGE(TAG, "Queue create failed");
        return;//crash m rogram
    }

void app_main(void)
{
    ESP_LOGI(TAG, "Starting Lab 3A (race demo)");

    // Crea varias tareas que compiten por incrementar shared_counter.
    // 1 tarea "TaskA"
    xTaskCreate(increment_task, "incA", 2048, "TaskA", 5, NULL);

    // 4 tareas "TaskB" (todas compiten también por el mismo contador)
    // OJO: todas tienen el mismo nombre de task ("incB") y el mismo parámetro "TaskB".
    // Esto dificulta distinguirlas en depuración.
    xTaskCreate(HEARTBEAT, "blink_task", 2048, "TaskA", 1, NULL);
    xTaskCreate(Alive, "Alive", 2048, "TaskB", 2, NULL);
    xTaskCreate(producer_task, "TaskC", 2048, NULL, 3, NULL);
    xTaskCreate(consumer_task, "TaskD", 2048, NULL, 4, NULL);
    xTaskCreate(read_button_task5, "incE", 2048, "Task5", 5, NULL);
    xTaskCreate( read_button_task6, "incF", 2048, "Task6", 6, NULL);

}

Exercises

Remove the mutex again. Do you ever see weird behavior?

Yes, the counter will grow slower than expected or show inconsistent values.


Change priorities: TaskA priority 6, TaskB priority 4. What do you expect and why?

Task A will interrupt Task B almost every time it finishes its waiting period, so Task A dominates execution.


In one sentence: what does a mutex “guarantee”?

A mutex guarantees that only one task can access a shared resource at any given time.

Extra mini-challenges

Heartbeat + work task

Add a third task that prints “alive” every 2 seconds.


Queue with struct

Send a struct: {int id; int value;}


Mutex around a shared peripheral

Make two tasks write to the same log message format (simulate “shared UART resource”) and guard it with a mutex.

Results

--

Labs codes running

Enlace directo Enlace directo