ESP32 FreeRTOS single core tasks seemingly running at the same time

So, I've been trying my hand at an ESP32 FreeRTOS automatized irrigation project, and so far so good, it works allright. Thing is, when I take the tasks start and end ticks, it says 2 of the tasks start and end at the same time.

I use xTaskGetTickCount() both at the start (right after the while) and end (right before the vTaskDelay) of each task's function, which I believe to be the correct way of doing it. I also store this data in a2 column array, the first column being the start tick, and the second the end tick.

I've tried everything I could - I'm fairly new to all this - and no sign of success so far. The code is as follows:

// #include <LiquidCrystal_I2C.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/timers.h"
#include <driver/adc.h>
#include <driver/i2c.h>
#include <esp_log.h>
#include <string>
#include "sdkconfig.h"
#include "HD44780.h"

// Single core config -> ESP32's second core (app_cpu)
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

// Queue config
static const int queue_len = 5;     // Size
static QueueHandle_t queue_handle;  // Handle

// Mutex config
static SemaphoreHandle_t mutex_handle;

// Timer config
static const TickType_t timer_delay = 200 / portTICK_PERIOD_MS;
static TimerHandle_t software_timer_handle = NULL;

// LCD config

#define rs    19
#define en    23
#define d4    18
#define d5    17
#define d6    16
#define d7    15

// Resistive moisture sensor config
#define umiAnalog ADC1_CHANNEL_0

volatile int umiR; // stores the moisture

// Solenoid valve
#define solenoid_pin GPIO_NUM_26

// ESP32 In-built led -> Testing
static const int led_pin = 2;

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////


const int l_size = 100; // Max tick data stored for each task
// First column of each array is for start ticks, second for end ticks
unsigned long l_umi[l_size][2]; // Moisture
int c_umi = 0; // index

unsigned long l_sol[l_size][2]; // Solenoid valve
int c_sol = 0;

unsigned long l_timer[l_size][2]; // Display timer
int c_timer = 0;

int aux_1 = 0; // Auxiliary variable for limiting array write

long converte(long x, long in_min, long in_max, long out_min, long out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

void tempo(uint32_t ms)
{
    // Convert milliseconds to ticks
    TickType_t delay_ticks = pdMS_TO_TICKS(ms);
    // Get the current tick count
    TickType_t current_ticks = xTaskGetTickCount();
    // Delay the task until the current tick count plus the delay
    vTaskDelayUntil(&current_ticks, delay_ticks);
}

void mostra_tempo(void *parameter){ // A function for printing one array on the serial monitor when it reaches l_size items (max size)
  while (true){
    if (aux_1 == 0 and c_umi == l_size){
      printf("\nSensor, %d\n", c_umi);
      for(int i = 0; i < l_size; i++){
		printf("%lu %lu\n", l_umi[i][0], l_umi[i][1]);
      }
      aux_1 = 1;
    }
    if (aux_1 == 1 and c_sol == l_size){
      printf("\nSolenoid, %d\n", c_sol);
      for(int i = 0; i < l_size; i++){
		printf("%lu %lu\n", l_sol[i][0], l_sol[i][1]);
      }
      aux_1 = 2;
    }
    if (aux_1 == 2 and c_timer == l_size){
      printf("\nTimer display, %d\n", c_timer);
      for(int i = 0; i < l_size; i++){
		printf("%lu %lu\n", l_timer[i][0], l_timer[i][1]);
      }
      aux_1 = 3;
    }
    vTaskDelay(60000 / portTICK_PERIOD_MS); // Delay for preventing this task from overwriting the others ever so often
  }
}


///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////


// Moisture sensor task
void leitura_sensorResistivo(void *parameter) {
  while(1) {
    ///////////////////////////////////////////////////////////////////////////
    if (c_umi < l_size){ // Stores the starting tick
      l_umi[c_umi][0] = xTaskGetTickCount();
    }
    ///////////////////////////////////////////////////////////////////////////
    if (xSemaphoreTake(mutex_handle, portMAX_DELAY) == pdTRUE) {
      umiR = adc1_get_raw(umiAnalog); // Reads moisture 
      if (xQueueSend(queue_handle, (void *)&umiR, 10) != pdTRUE) {
        printf("ERROR: Sending of data to Queue incomplete\n");
      }
      xSemaphoreGive(mutex_handle);
    } else {
      printf("ERROR: MUTEX acquisition incomplete at the sensor\n");
    }
    tempo(50); // So that the task takes more than a few ticks
    ///////////////////////////////////////////////////////////////////////////
    if (c_umi < l_size){ // Stores the ending tick
      l_umi[c_umi][1] = xTaskGetTickCount();
      c_umi++;
    }
    ///////////////////////////////////////////////////////////////////////////
    // Sensor delay
    vTaskDelay(150 / portTICK_PERIOD_MS);
  }
}

// Função para controle da válvula solenóide
void valv_control(void *parameter) {
  while(1) {
    ///////////////////////////////////////////////////////////////////////////
    if (c_sol < l_size){ // Stores the starting tick
      l_sol[c_sol][0] = xTaskGetTickCount();
    }
    ///////////////////////////////////////////////////////////////////////////
    int rcv_data;
    // Limite de umidade
    const int limite_umidade_superior = 50; // "Moist" (Upper) moisture limit
    const int limite_umidade_inferior = 35; // "Dry" (Lower) moisture limit    // Caso tenha algo novo na fila considere para fazer o controle da válvula
    if (xQueueReceive(queue_handle, (void *)&rcv_data, 10) == pdTRUE) {
      int umidade_medida = converte(umiR, 4095, 2100, 0, 100);
      if (umidade_medida < limite_umidade_inferior && gpio_get_level(solenoid_pin) == 0) {
        printf("Starting solenoid\n");
        gpio_set_level(solenoid_pin, 1);
      } else if (umidade_medida >= limite_umidade_superior && gpio_get_level(solenoid_pin) == 1) {
        printf("Shutting solenoid off\n");
        gpio_set_level(solenoid_pin, 0);
      }
    }
    tempo(50);
    ///////////////////////////////////////////////////////////////////////////
    if (c_sol < l_size){ // Stores the ending tick
      l_sol[c_sol][1] = xTaskGetTickCount();
      c_sol++;
    }
    ///////////////////////////////////////////////////////////////////////////
    // Task delay
    vTaskDelay(150 / portTICK_PERIOD_MS);
  }
}

// Timer callback
void softwareTimer_callback(TimerHandle_t xTimer) {
  ///////////////////////////////////////////////////////////////////////////
  if (c_timer < l_size){ // Stores the starting tick
    l_timer[c_timer][0] = xTaskGetTickCount();
  }
  ///////////////////////////////////////////////////////////////////////////
  if (xSemaphoreTake(mutex_handle, portMAX_DELAY) == pdTRUE) {
    // Moisture value at the display
    int umidade = converte(umiR, 4095, 2000, 0, 100); // Converts the digital value to moisture percentage
    //LCD_setCursor(0,1);
    //LCD_clearScreen();
    //LCD_writeStr("Moisture: ");
    char str_umi = static_cast<char>(umidade);
    //LCD_writeStr(str_umi);
    xSemaphoreGive(mutex_handle);
  } else {
    printf("ERROR: MUTEX acquisition incomplete at the Display\n");
  }
  tempo(50);
  ///////////////////////////////////////////////////////////////////////////
  if (c_timer < l_size){ // Stores the ending tick
    l_timer[c_timer][1] = xTaskGetTickCount();
    c_timer++;
  }
  ///////////////////////////////////////////////////////////////////////////
}

extern "C" void app_main() {

  // Delay
  vTaskDelay(500 / portTICK_PERIOD_MS);
  
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_0);

  printf("-- Starting irrigation system --\n");

  // LCD configs
  //LCD_init(0x27, 19, 18, 16, 2);

  // Solenoid valve pin configuration
  // pinMode(solenoid_pin, OUTPUT);
  gpio_set_direction(solenoid_pin, GPIO_MODE_OUTPUT);
  gpio_set_level(solenoid_pin, 0); // Valve starts closed

  // Creating the queue
  queue_handle = xQueueCreate(queue_len, sizeof(int));
  if (queue_handle == NULL) {
    printf("ERROR: Failed queue creation\n");
    while (1);
  }

  // Creating the mutex
  mutex_handle = xSemaphoreCreateMutex();
  if (mutex_handle == NULL) {
    printf("ERROR: Failed mutex creation\n");
    while (1);
  }

  // Creating the software timer
  software_timer_handle = xTimerCreate("software_timer_handle", timer_delay, pdTRUE, (void *)0, softwareTimer_callback);  // Parâmetros: Name of timer, Period of timer (in ticks), Auto-reload, Timer ID, Callback function
  if (software_timer_handle == NULL) {
    printf("ERROR: Failed software timer creation\n");
    while (1);
  }

  // Starting the timer
  xTimerStart(software_timer_handle, portMAX_DELAY); // Blocks the task until a command is successfully given to the timer queue

  // Task creation
  xTaskCreatePinnedToCore(
              leitura_sensorResistivo,     // Function
              "leitura_sensorResistivo",   // Name
              1024,                        // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,                        // Parameters
              1,                           // Task priority (0 to configMAX_PRIORITIES - 1)
              NULL,                        // Handle
              app_cpu);                    // Single core

  xTaskCreatePinnedToCore(
              valv_control,            // // Function
              "valv_control",          // // Name
              1024,                    // // Stack size (bytes in ESP32, words in FreeRTOS
              NULL,                    // // Parameters
              1,                       // // Task priority (0 to configMAX_PRIORITIES - 1)
              NULL,                    // // Handle
              app_cpu);                // // Single core

  xTaskCreatePinnedToCore(
              mostra_tempo,
              "mostra_tempo",
              4096,
              NULL,
              2,
              NULL,
              app_cpu
  );

  // Delete "setup and loop" task
  vTaskDelete(NULL);
}

I appreciate any and all help. Thanks in advance!

For better resolution use esp_timer_get_time().

Thanks for the tip! But it stil looks like they are running at the same time. Take the first instance of leitura_sensorResistivo and valv_control for example (delay() increased to 100):
leitura_sensorResistivo: [652706, 752871]
valv_control: [653021 752881]
Even though I can see they no longer exactly start and end at the same time, it still seems that they are running concurrently:
image

Quite frankly, I'm not willing to spend the time analyzing such a large code containing functions unrelated to FreeRTOS task switching (adc, LCD, etc). Please post an MRE. This is the smallest possible code that compiles and displays the issue in question. Leave out all extraneous cluttter.

Also:

Out of curiosity, why did you choose to implement an app_main() function rather than sticking with the standard setup(), loop() paradigm? And why does it need to be compiled as 'C' and not 'C++'

Even when I'm coding for FreeRTOS and don't need loopTask(), I do it this way:

void setup() {
  // Configure all the tasks, queues, semaphores, etc here in setup
}

void loop() {
  // delete loopTask
  vtaskDelete(NULL);
}

Sorry mate, posted the espressif IDE code. The arduino one uses setup and loop allright. Give me a few minutes and I'll provide the correct code in MRE

Do you want xQueueReceive to block until there is something in the queue?
If not, describe in words or pictures how you want the the code to behave.

Do you want xQueueReceive to block until there is something in the queue?

Exactly

The code in MRE format (I left only FreeRTOS related content and the tick array printing function (mostra_tempo)):

#include <Arduino.h>

// Single core config -> ESP32's second core (app_cpu)
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

// Queue config
static const int queue_len = 5;     // Size
static QueueHandle_t queue_handle;  // Handle

// Mutex config
static SemaphoreHandle_t mutex_handle;

// Timer config
static const TickType_t timer_delay = 200 / portTICK_PERIOD_MS;
static TimerHandle_t software_timer_handle = NULL;

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

const int l_size = 10; // Max tick data stored for each task
// First column of each array is for start ticks, second for end ticks
unsigned long l_umi[l_size][2]; // Moisture
int c_umi = 0; // index

unsigned long l_sol[l_size][2]; // Solenoid valve
int c_sol = 0;

unsigned long l_timer[l_size][2]; // Display timer
int c_timer = 0;

int aux_1 = 0; // Auxiliary variable for limiting array write

void mostra_tempo(void *parameter){ // A function for printing one array on the serial monitor when it reaches l_size items (max size)
  while (true){
    if (aux_1 == 0 and c_umi == l_size){
      Serial.println("");
      Serial.print("Sensor, ");
      Serial.println(c_umi);
      for(int i = 0; i < l_size; i++){
        Serial.print(l_umi[i][0]);
        Serial.print(" ");
        Serial.println(l_umi[i][1]);
      }
      aux_1 = 1;
    }
    if (aux_1 == 1 and c_sol == l_size){
      Serial.println("");
      Serial.print("Solenoid, ");
      Serial.println(c_sol);
      for(int i = 0; i < l_size; i++){
        Serial.print(l_sol[i][0]);
        Serial.print(" ");
        Serial.println(l_sol[i][1]);
      }
      aux_1 = 2;
    }
    if (aux_1 == 2 and c_timer == l_size){
      Serial.println("");
      Serial.print("Timer Display, ");
      Serial.println(c_timer);
      for(int i = 0; i < l_size; i++){
        Serial.print(l_timer[i][0]);
        Serial.print(" ");
        Serial.println(l_timer[i][1]);
      }
      aux_1 = 3;
    }
    vTaskDelay(10000 / portTICK_PERIOD_MS); // Delay for preventing this task from overwriting the others ever so often
  }
}

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

// Moisture sensor task
void leitura_sensorResistivo(void *parameter) {
  while(1) {
    ///////////////////////////////////////////////////////////////////////////
    if (c_umi < l_size){ // Stores the starting tick
      l_umi[c_umi][0] = esp_timer_get_time();
    }
    ///////////////////////////////////////////////////////////////////////////
    if (xSemaphoreTake(mutex_handle, portMAX_DELAY) == pdTRUE) {
      delay(100); // So that the task takes more than a few ticks
    }
    ///////////////////////////////////////////////////////////////////////////
    if (c_umi < l_size){ // Stores the ending tick
      l_umi[c_umi][1] = esp_timer_get_time();
      c_umi++;
    }
    ///////////////////////////////////////////////////////////////////////////
    // Sensor delay
    vTaskDelay(150 / portTICK_PERIOD_MS);
  }
}

// Solenoid Valve
void valv_control(void *parameter) {
  while(1) {
    ///////////////////////////////////////////////////////////////////////////
    if (c_sol < l_size){ // Stores the starting tick
      l_sol[c_sol][0] = esp_timer_get_time();
    }
    int rcv_data;
    ///////////////////////////////////////////////////////////////////////////
    if (xQueueReceive(queue_handle, (void *)&rcv_data, 10) == pdTRUE) {
      delay(100);
    }
    ///////////////////////////////////////////////////////////////////////////
    if (c_sol < l_size){ // Stores the ending tick
      l_sol[c_sol][1] = esp_timer_get_time();
      c_sol++;
    }
    ///////////////////////////////////////////////////////////////////////////
    // Task delay
    vTaskDelay(150 / portTICK_PERIOD_MS);
  }
}

// Timer callback
void softwareTimer_callback(TimerHandle_t xTimer) {
  ///////////////////////////////////////////////////////////////////////////
  if (c_timer < l_size){ // Stores the starting tick
    l_timer[c_timer][0] = esp_timer_get_time();
  }
  ///////////////////////////////////////////////////////////////////////////
  if (xSemaphoreTake(mutex_handle, portMAX_DELAY) == pdTRUE) {
    delay(100);
  }
  ///////////////////////////////////////////////////////////////////////////
  if (c_timer < l_size){ // Stores the ending tick
    l_timer[c_timer][1] = esp_timer_get_time();
    c_timer++;
  }
  ///////////////////////////////////////////////////////////////////////////
}

void setup() {
  // Starts serial comms
  Serial.begin(115200);

  // Delay
  vTaskDelay(500 / portTICK_PERIOD_MS);

  // Creating the queue
  queue_handle = xQueueCreate(queue_len, sizeof(int));
  if (queue_handle == NULL) {
    Serial.println("ERROR: Failed queue creation");
    while (1);
  }

  // Creating the mutex
  mutex_handle = xSemaphoreCreateMutex();
  if (mutex_handle == NULL) {
    Serial.println("ERROR: Failed mutex creation");
    while (1);
  }

  // Creating the software timer
  software_timer_handle = xTimerCreate("software_timer_handle", timer_delay, pdTRUE, (void *)0, softwareTimer_callback);  // Parameters: Name of timer, Period of timer (in ticks), Auto-reload, Timer ID, Callback function
  if (software_timer_handle == NULL) {
    Serial.println("ERROR: Failed software timer creation");
    while (1);
  }

  // Starting the timer
  xTimerStart(software_timer_handle, portMAX_DELAY); // Blocks the task until a command is successfully given to the timer queue

  // Task creation
  xTaskCreatePinnedToCore(
              leitura_sensorResistivo,     // Function
              "leitura_sensorResistivo",   // Name
              1024,                        // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,                        // Parameters
              1,                           // Task priority (0 to configMAX_PRIORITIES - 1)
              NULL,                        // Handle
              app_cpu);                    // Single core

  xTaskCreatePinnedToCore(
              valv_control,            // // Function
              "valv_control",          // // Name
              1024,                    // // Stack size (bytes in ESP32, words in FreeRTOS
              NULL,                    // // Parameters
              1,                       // // Task priority (0 to configMAX_PRIORITIES - 1)
              NULL,                    // // Handle
              app_cpu);                // // Single core

  xTaskCreatePinnedToCore(
              mostra_tempo,
              "mostra_tempo",
              4096,
              NULL,
              2,
              NULL,
              app_cpu
  );

  // Delete "setup and loop" task
  vTaskDelete(NULL);
}

void loop() {
  // Nothing
}

Your code has a 10 ticks timeout period.
Use portMAX_DELAY.
See https://www.freertos.org/a00118.html

Thanks mate, I'm testing it right away

An even more barebone version of the code btw:

#include <Arduino.h>

// Single core config -> ESP32's second core (app_cpu)
#if CONFIG_FREERTOS_UNICORE
static const BaseType_t app_cpu = 0;
#else
static const BaseType_t app_cpu = 1;
#endif

// Queue config
static const int queue_len = 5;     // Size
static QueueHandle_t queue_handle;  // Handle

// Mutex config
static SemaphoreHandle_t mutex_handle;

// Timer config
static const TickType_t timer_delay = 200 / portTICK_PERIOD_MS;
static TimerHandle_t software_timer_handle = NULL;

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

// Moisture sensor task
void leitura_sensorResistivo(void *parameter) {
  while(1) {
    if (xSemaphoreTake(mutex_handle, portMAX_DELAY) == pdTRUE) {
      delay(100); // So that the task takes more than a few ticks
    }
    vTaskDelay(150 / portTICK_PERIOD_MS);
  }
}

// Solenoid Valve
void valv_control(void *parameter) {
  while(1) {
    int rcv_data;
    ///////////////////////////////////////////////////////////////////////////
    if (xQueueReceive(queue_handle, (void *)&rcv_data, portMAX_DELAY) == pdTRUE) {
      delay(100);
    }
    vTaskDelay(150 / portTICK_PERIOD_MS);
  }
}

// Timer callback
void softwareTimer_callback(TimerHandle_t xTimer) {
  if (xSemaphoreTake(mutex_handle, portMAX_DELAY) == pdTRUE) {
    delay(100);
  }
}

void setup() {
  // Starts serial comms
  Serial.begin(115200);

  // Creating the queue
  queue_handle = xQueueCreate(queue_len, sizeof(int));

  // Creating the mutex
  mutex_handle = xSemaphoreCreateMutex();

  // Creating the software timer
  software_timer_handle = xTimerCreate("software_timer_handle", timer_delay, pdTRUE, (void *)0, softwareTimer_callback);  // Parameters: Name of timer, Period of timer (in ticks), Auto-reload, Timer ID, Callback function

  // Starting the timer
  xTimerStart(software_timer_handle, portMAX_DELAY); // Blocks the task until a command is successfully given to the timer queue

  // Task creation
  xTaskCreatePinnedToCore(
              leitura_sensorResistivo,     // Function
              "leitura_sensorResistivo",   // Name
              1024,                        // Stack size (bytes in ESP32, words in FreeRTOS)
              NULL,                        // Parameters
              1,                           // Task priority (0 to configMAX_PRIORITIES - 1)
              NULL,                        // Handle
              app_cpu);                    // Single core

  xTaskCreatePinnedToCore(
              valv_control,            // // Function
              "valv_control",          // // Name
              1024,                    // // Stack size (bytes in ESP32, words in FreeRTOS
              NULL,                    // // Parameters
              1,                       // // Task priority (0 to configMAX_PRIORITIES - 1)
              NULL,                    // // Handle
              app_cpu);                // // Single core

  xTaskCreatePinnedToCore(
              mostra_tempo,
              "mostra_tempo",
              4096,
              NULL,
              2,
              NULL,
              app_cpu
  );

  // Delete "setup and loop" task
  vTaskDelete(NULL);
}

void loop() {
  // Nothing
}

Use portMAX_DELAY.

I've also taken out the delay() (in case it wasn't really prolonging the task) and it seems to have worked actually! The following time marks were collected (skip ahead if you'd rather look at graphs):

Sensor, 10
652706 652972
802875 802922
952875 952922
1102875 1102922
1252876 1257048
1406875 1406922
1556875 1556922
1706875 1706922
1856875 1857046
2006875 2006922

Solenoid, 10
653027 653040
802933 802935
952933 952935
1102933 1102936
1252891 1257061
1406933 1406935
1556933 1556935
1706933 1706935
1856891 1857059
2006933 2006936

Timer Display, 10
851750 857044
1051747 1056984
1251747 1256989
1451747 1456984
1651747 1656984
1851747 1856987
2051747 2056984
2251747 2256984
2451747 2456987
2651747 2656984

Unless the timer comes around (in which case it will preempt both tasks), the GANTT looks something like this:

And whenever the timer comes around, zoomed in at the start:

And at the end:

So does the delay() function, while inside tasks, prolong them or not at all? Because from what I remember it was supposed to... Also, how come both tasks start during the timer?

Yes, the Arduino delay() function simply calls vTaskDelay() which puts the task in the Blocked state for the specified time period. After that, it enters the Ready to Run state.

After creating the mutex, you must give it in order for it to be available to take:

  // Creating the mutex
  mutex_handle = xSemaphoreCreateMutex();
  xSemaphoreGive(mutex_handle);

Also, once taken, it can never be taken again until it's given back (unless you create a recursive mutex). Your code can never take the mutex because it's not available (you didn't first give it). And even if the first attempt to take it successful, it can never take it again because you never give it back. So, your delay statements never run.

It's an absolute requirement of mutexes that once taken, it must be returned by the same task that took it.

Well, that explains most of it, thanks. Anyway I could artificially increase a task's runtime? Maybe doing a for loop for a huge value and print something random?

delay() will work just fine for that ... if you code actually runs the delay() function instead of skipping over it like it does now.

Your code can never take the mutex because it's not available

So basically I only created the mutex and never made it available? And with the xSemaphoreGive() I make it so, then a task takes it, then returns it once done?

I see. Will try it right away. Thanks mate!

Rather than continue to struggle, take the time to read “Mastering the FreeRTOS Real Time Kernel - A Hands On Tutorial Guide” form here: https://www.freertos.org/Documentation/RTOS_book.html
You can skip the first two chapters.

1 Like

The delay doesn't seem to actually extend thetask (as in a 10s delay will stop the other task for 10s):

But that was for better viewing anyway; without the delay the tasks run separately. The only matter (I suppose) that remains is the fact that the tasks start mid-Timer:

Again, they run their normal execution time as soon as the timer ends, but should they even start running while the timer is doing it's thing?