Doing several things at the same time, RTOS style

This is a supplement to the helpful and popular thread, demonstration code for several things at the same time. That thread is definitely helpful for people who want to be super close to the metal, but at the same time, what if there's an easier way that usually gives us better performance?

I wanted to share how the exact same effects can be achieved using FreeRTOS, with less code and more clarity. In FreeRTOS, we create tasks, which are individual threads of execution with their own stacks. Tasks can communicate with primitives like queues and streams, although that is not needed in this demonstration because none of the tasks use any shared resources or need to convey information to one another.

Using FreeRTOS in a core that supports it is stupid-easy. Just download the library, #include its headers, and you're off to the races.

I have preserved the original code from this demonstration alongside the FreeRTOS version to serve as a basis for comparison.

// SeveralThingsAtTheSameTimeRevRTOS.ino

#include <FreeRTOS.h>
#include <Servo.h>
#include <semphr.h>
#include <task.h>

// ----CONSTANTS (won't change)

const int onBoardLedPin = LED_BUILTIN;  // the pin numbers for the LEDs
const int led_A_Pin = 13;
const int led_B_Pin = 14;
const int buttonLed_Pin = 12;

const int buttonPin = 7;  // the pin number for the button

const int servoPin = 15;  // the pin number for the servo signal

const int onBoardLedInterval = 500;  // number of millisecs between blinks
const int led_A_Interval = 2500;
const int led_B_Interval = 4500;

const int blinkDuration =
    500;  // number of millisecs that Led's are on - all three leds use this

const int buttonInterval = 300;  // number of millisecs between button readings

const int servoMinDegrees = 20;  // the limits to servo movement
const int servoMaxDegrees = 150;

//------- VARIABLES (will change)

byte onBoardLedState = LOW;  // used to record whether the LEDs are on or off
byte led_A_State = LOW;      //   LOW = off
byte led_B_State = LOW;
byte buttonLed_State = LOW;

Servo myservo;  // create servo object to control a servo

int servoPosition = 90;      // the current angle of the servo - starting at 90.
int servoSlowInterval = 80;  // millisecs between servo moves
int servoFastInterval = 10;
int servoInterval = servoSlowInterval;  // initial millisecs between servo moves
int servoDegrees = 2;                   // amount servo moves at each step
                       //    will be changed to negative value for movement in
                       //    the other direction

unsigned long currentMillis =
    0;  // stores the value of millis() in each iteration of loop()
unsigned long previousOnBoardLedMillis =
    0;  // will store last time the LED was updated
unsigned long previousLed_A_Millis = 0;
unsigned long previousLed_B_Millis = 0;

unsigned long previousButtonMillis = 0;  // time when button press last checked

unsigned long previousServoMillis =
    0;  // the time when the servo was last moved

//========
void OnboardLedTask(void* unused);
void LedATask(void* unused);
void LedBTask(void* unused);
void ButtonTask(void* unused);
void ServoTask(void*);

//========

void setup() {
  Serial1.begin(9600);
  Serial1.println("Starting SeveralThingsAtTheSameTimeRevRtos.ino");

  pinMode(onBoardLedPin, OUTPUT);
  pinMode(led_A_Pin, OUTPUT);
  pinMode(led_B_Pin, OUTPUT);
  pinMode(buttonLed_Pin, OUTPUT);

  pinMode(buttonPin, INPUT_PULLUP);

  myservo.write(servoPosition);  // sets the initial position
  myservo.attach(servoPin);

  // Start RTOS tasks.
  auto simple_task = [](auto function, auto name) {
    xTaskCreate(function, name, 256, NULL, 1, NULL);
  };
  simple_task(OnboardLedTask, "OnboardLedTask");
  simple_task(LedATask, "LedATask");
  simple_task(LedBTask, "LedBTask");
  simple_task(ButtonTask, "ButtonTask");
  simple_task(ServoTask, "ServoTask");
}

//=======

void loop() {}

//========

void BlinkLedForDurationPerInterval(int pin, int32_t off_duration,
                                    int32_t on_duration) {
  while (true) {
    delay(off_duration);
    digitalWrite(pin, HIGH);
    delay(on_duration);
    digitalWrite(pin, LOW);
  }
}

void OnboardLedTask(void* unused) {
  BlinkLedForDurationPerInterval(onBoardLedPin, onBoardLedInterval,
                                 blinkDuration);
}

void LedATask(void* unused) {
  BlinkLedForDurationPerInterval(led_A_Pin, led_A_Interval, blinkDuration);
}

void LedBTask(void* unused) {
  BlinkLedForDurationPerInterval(led_B_Pin, led_B_Interval, blinkDuration);
}

void ButtonTask(void* unused) {
  while (true) {
    if (digitalRead(buttonPin) == LOW) {
      // Invert the LED state if the button is pressed when we check.
      buttonLed_State = !buttonLed_State;
      digitalWrite(buttonLed_Pin, buttonLed_State);
    }
    delay(buttonInterval);
  }
}

void ServoTask(void*) {
  while (true) {
    servoPosition += servoDegrees;
    if (servoPosition <= servoMinDegrees) {
      if (servoInterval == servoSlowInterval) {
        servoInterval = servoFastInterval;
      } else {
        servoInterval = servoSlowInterval;
      }
    }
    if (servoPosition >= servoMaxDegrees || servoPosition <= servoMinDegrees) {
      servoDegrees = -servoDegrees;
      servoPosition = servoPosition + servoDegrees;
    }
    myservo.write(servoPosition);
    delay(servoInterval);
  }
}

#if 0
// Non-RTOS code
void loop() {
  // Notice that none of the action happens in loop() apart from reading millis()
  //   it just calls the functions that have the action code

  currentMillis = millis();   // capture the latest value of millis()
                              //   this is equivalent to noting the time from a clock
                              //   use the same time for all LED flashes to keep them synchronized
  
  readButton();               // call the functions that do the work
  updateOnBoardLedState();
  updateLed_A_State();
  updateLed_B_State();
  switchLeds();
  servoSweep();
}

void updateOnBoardLedState() {
  if (onBoardLedState == LOW) {
    if (currentMillis - previousOnBoardLedMillis >= onBoardLedInterval) {
      onBoardLedState = HIGH;
      previousOnBoardLedMillis += onBoardLedInterval;
    }
  } else {
    if (currentMillis - previousOnBoardLedMillis >= blinkDuration) {
      onBoardLedState = LOW;
      previousOnBoardLedMillis += blinkDuration;
    }
  }
}

//=======

void updateLed_A_State() {
  if (led_A_State == LOW) {
    if (currentMillis - previousLed_A_Millis >= led_A_Interval) {
      led_A_State = HIGH;
      previousLed_A_Millis += led_A_Interval;
    }
  } else {
    if (currentMillis - previousLed_A_Millis >= blinkDuration) {
      led_A_State = LOW;
      previousLed_A_Millis += blinkDuration;
    }
  }
}

//=======

void updateLed_B_State() {
  if (led_B_State == LOW) {
    if (currentMillis - previousLed_B_Millis >= led_B_Interval) {
      led_B_State = HIGH;
      previousLed_B_Millis += led_B_Interval;
    }
  } else {
    if (currentMillis - previousLed_B_Millis >= blinkDuration) {
      led_B_State = LOW;
      previousLed_B_Millis += blinkDuration;
    }
  }
}

//========

void switchLeds() {
  digitalWrite(onBoardLedPin, onBoardLedState);
  digitalWrite(led_A_Pin, led_A_State);
  digitalWrite(led_B_Pin, led_B_State);
  digitalWrite(buttonLed_Pin, buttonLed_State);
}

//=======

void readButton() {
  if (millis() - previousButtonMillis >= buttonInterval) {
    if (digitalRead(buttonPin) == LOW) {
      buttonLed_State =
          !buttonLed_State;  
      previousButtonMillis += buttonInterval;
    }
  }
}

//========

void servoSweep() {
  if (currentMillis - previousServoMillis >= servoInterval) {
    previousServoMillis += servoInterval;
    servoPosition =
        servoPosition + servoDegrees;  

    if (servoPosition <= servoMinDegrees) {
      if (servoInterval == servoSlowInterval) {
        servoInterval = servoFastInterval;
      } else {
        servoInterval = servoSlowInterval;
      }
    }
    if ((servoPosition >= servoMaxDegrees) ||
        (servoPosition <= servoMinDegrees)) {
      servoDegrees = -servoDegrees;
      servoPosition = servoPosition + servoDegrees;
    }
    myservo.write(servoPosition);
  }
}

#endif

In the interest of demonstrating that this code does the same thing as the original, here's a video:

About 75 lines of RTOS code have replaced about 150 lines of Arduino-loop-style code. The RTOS code will likely be faster, and, to my eye, is vastly clearer. Consider using an RTOS for your next project that involves concurrency!

What's a "super-loop"? Not a term I've run across in many years of coding.

I've seen it referred to on this forum. I believe it refers to a loop() function calling other loop() functions (e.g. loop() calling mqtt.loop(), wifi.loop(), and myclass.loop()). If there is a more preferred term for "the standard arduino way of doing concurrency" I'd love to substitute it. Please do let me know. :slight_smile:

(I edited my post to remove the reference to this term, in case it distracted anyone else.)

I doubt that. In general, an rtos task will require a full context switch between tasks, while a “super-loop” only requires saving the (smaller) context requires by the function ABI.

1 Like

Would you typically use this construct in a real program?
You've set the stack size to be the same for all tasks and there's no task handle so how do you inspect stack usage, send notifications etc. How did you arrive at 256 bytes as a suitable number for all tasks? In addition, on the Arduino platform pinning to core 1 is less likely to give WiFi issues.

Personally I give each new task 8k of stack and then periodically check to see how much is actually used and downsize as needed. None of the tasks I've ever created have ended up with the exact same stack size.

In the Arduino ESP32 BSP, WiFi, BlueTooth, etc runs on Core 0.

1 Like

Agree, using the lambda there adds no value.

Agree on inter-task notifications. But you can check stack usage from within the task (without its handle) using uxTaskGetStackHighWaterMark(NULL);.

That's not enough stack space for anything but a trivial demo function.

@jaguilar84
So what is your programming question?
Do you want to know if using an RTOS make sense or not?

I agree, there's no reason to believe that using an RTOS will be faster in general than a well-designed loop. IMO the value of using an RTOS is being able to do sophisticated inter-task sequence control and data sharing in a more structured way.

1 Like

Hrm, I guess it depends? If you're talking about the lambda, I think there's not much benefit to it, but also not any harm. It's down to taste.

If you're talking about a task with a similar size and without storing the handle, sure! If I don't need to manipulate the handle, storing it is just wasted space. This is not particularly uncommon in a microcontroller program, since a lot of times we'll be starting tasks that run forever. If you're talking about the size (256 words, or 1kB) I think it's adequate for many simple tasks. In a real program I would measure the top of the stack and leave a little room for growth. In fact, that's what I did when composing this demo! :slight_smile: I think your strategy of giving new tasks 8k is also great. I also turn on the gcc stack protector during burn-in and testing.

It depends on how big your context is, and also what RTOS features you have turned on. FreeRTOS has been measured to have context switch times of around 80 cycles in some configurations. You're going to burn at a minimum a significant fraction of that checking for work to do in a loop of any complexity.

For me I would say the main values are:

  • Simplicity of doing concurrent programming.
  • Priority schemes, which are more difficult to implement in loops.

And, as you mentioned, the structured sharing of data between tasks.

So you have no questions?

The question perhaps implied is "why aren't you all using FreeRTOS?".

a7

I would never presume to judge people whose use cases I'm not familiar with. I'm also somewhat new to the the microcontroller game. I just noticed that a very common question on the forums is how to manage doing more than one thing at once. My favored approach seems to be covered less often so I thought I'd write up this solution as an exercise. :slight_smile:

1 Like

So you have no programming questions?
You are posted in the Programming Questions catagory so I assumed you have questions.

I would love to see a category where different programming concepts can be discussed.

@frameworklabs
Project Discussion and Showcase?

FWIW, I checked with the moderators, and they feel the current category is fine. I appreciate you're trying to keep things on topic, so I just wanted to assure you that I'm not ignoring your concerns.

I have no concerns just wanted to know if you had a programming question

The Super Loop v. RTOS argument has also raged on Reddit

https://www.reddit.com/r/embedded/comments/10qw1vl/what_criteria_do_you_use_to_choose_super_loop/?rdt=56756

It somehow reminds me of the String v. char[] discussions which flare up here from time to time.

Here incidently is a definition for those, like me, who have only recently seen the Superloop construct being discussed: Embedded Systems Programming: A Foreground-Background ("Superloop") Architecture - InTechHouse