It is simple to perform a single task, but when you want to add in more tasks the difficulty and complexity increases. Without a structural way to handle multiple tasks, you will end up with a mess at the end. The code will be difficult to read and debug. Making changes might end up breaking something that is working previously.
In this post, I will introduce a method for you to handle multiple tasks in Arduino that will make it more manageable. It does require some boilerplate code, but once that is done you can focus on creating your ideas. I strongly recommend that you familiarize yourself with C/C++ pointers before continuing.
Process
The general process for the method to manage tasks effectively is broken down into four major steps:
- Create finite state machine (FSM) for each task
- Convert the FSMs into code
- Setup each task
- Run a scheduler to tick each task
Brief Intro to Finite State Machines
If you are familiar with finite state machines, you can skip this section.
A finite state machine is a way of logically breaking down a task into multiple states. A “state” is the condition of a thing at a specific time (in this case the task). The state of the machine gets evaluated periodically and is updated to a new state. When a transition to a new state occurs, the output(s) and action(s) corresponding to the new state will happen.
Note: It is possible for the new state to be the same as the current one.
Creating Finite State Machines
For every task, there should be an FSM for it. Before you can create a finite state machine you must define how a task will behave.
The idea behind FSM is abstract, so I will be using an example to give you a better idea. I will be referring to the example throughout this post in other sections.
Example FSMs
For this example, there are two tasks. One blinks a led every one second. The other task is to turn on a LED only when the user presses a button. Here are the FSMs for the tasks:
Convert the FSM into Code
One way to break an FSM down into code is to do the following:
- create an enumeration of all the states of the FSM
- create a function for each unique action in the FSM
- create a function that handles the transition and action between states of the FSM
Using the example FSMs, you will have a total of four states and two actions (turn led on and off). There are two functions to handle the transition of each FSM. Here is one way how the FSMs will look in code form:
static Task led_blink_task; unsigned long led_blink_task_period = 1000; // tick every second enum led_blink_states { OFF = 0, ON = 1 }; // task 1 FSM int blink_led_fsm(int current_state) { // transitions switch (current_state) { case OFF: current_state = ON; break; case ON: current_state = OFF; break; default: // do nothing break; }; // actions switch (current_state) { case OFF: digitalWrite(blink_led_pin, LOW); break; case ON: digitalWrite(blink_led_pin, HIGH); break; default: // do nothing break; }; return current_state; }
static Task user_led_task; unsigned long user_led_task_period = 100; // tick every 1/10 th of a second enum user_led_states { USER_LED_OFF = 0, USER_LED_ON = 1 }; int user_led_fsm(int current_state) { // transition switch (current_state) { case USER_LED_OFF: if (digitalRead(button_pin) == LOW) { // button is active low current_state = USER_LED_ON; } else { current_state = USER_LED_OFF; } break; case USER_LED_ON: if (digitalRead(button_pin) == LOW) { // button is active low current_state = USER_LED_ON; } else { current_state = USER_LED_OFF; } break; default: // do nothing break; } // action switch (current_state) { case OFF: digitalWrite(user_led_pin, LOW); break; case ON: digitalWrite(user_led_pin, HIGH); break; default: // do nothing break; } return current_state; }
Setup Tasks
You can represent a task as a class object with C++. Here is a way you can implement the task object (mainly boilerplate code):
#ifndef _TASK_H_ #define _TASK_H_ class Task { private: unsigned long period; unsigned long elapsed_time; signed char state; int (*TickFct)(int); public: Task(); void setState(const signed char &); signed char getState(); void setPeriod(const unsigned long &); unsigned long getPeriod(); void setElapsedTime(const unsigned long &); unsigned long getElapsedTime(); void increaseElapsedTime(const unsigned long &); void setTickFunction(int (*tick_function)(int)); signed char runTickFunction(const signed char &); ~Task(); }; #endif
#include "task.h" /** * Constructor * Set variables to default values here. */ Task::Task() { // do nothing } /** * function: setState * This method set the state of the task to the state * passed in. */ void Task::setState(const signed char & new_state) { state = new_state; } /** * function: getState * This method returns the current state of the task. */ signed char Task::getState() { return state; } /** * function: setPeriod * This method sets the period of the task. This influences * how often the finite state machine ticks. */ void Task::setPeriod(const unsigned long & new_period) { period = new_period; } /** * function: getPeriod * This method returns the period of the task. */ unsigned long Task::getPeriod() { return period; } /** * function: setElapsedTime * This method sets the time elapsed for the task. Use for initialization * and debugging. */ void Task::setElapsedTime(const unsigned long & time_elapsed) { elapsed_time = time_elapsed; } /** * function: getElapsedTime * This method returns the time elapsed since the last time the finite * state machine ticks. */ unsigned long Task::getElapsedTime() { return elapsed_time; } /** * function: increaseElapsedTime * This method adds a small amount of time to the time elapsed. The small * amount of time is the interval between the last check time and current. */ void Task::increaseElapsedTime(const unsigned long & delta_time) { elapsed_time += delta_time; } /** * function: setTickFunction * This method sets the function to invoke when it is time for task action to tick. * Ideally, the function is a FSM, but it can be any function that fits the parameter * and return type. */ void Task::setTickFunction(int (*tick_function)(int)) { TickFct = tick_function; } /** * function: runTickFunction * This method invokes the function (FSM) of this task. */ signed char Task::runTickFunction(const signed char & current_state) { return TickFct(current_state); } /** * Destructor * This gets invoke when the program closes to do clean up. Free or delete * any dynamically allocated variables here. */ Task::~Task() { // do nothing }
Note: I will be referring to this Task class object in any examples that involve tasks in this post
To set up a task, you need to provide the task with several missing information:
- the initial state (initial state in the FSM representing the task)
- the period (when does the task tick)
- the time elapsed (how long since the task last ran)
- the reference to the coded FSM
Let’s use the example FSMs and set up the tasks. We want one led to blink every one second, which means the period is 1000 ms. The other task is to respond to a user pressing a button by turning on a led. A good period for the task would be 100 ms because it is small enough to be responsive, but not tick too often. Now all four pieces of missing information are defined, we can set up the task in code. Here is one way to do it:
void setup() { // task 1 led_blink_task.setState(OFF); led_blink_task.setPeriod(led_blink_task_period); led_blink_task.setElapsedTime(led_blink_task_period); // tick immediately led_blink_task.setTickFunction(&blink_led_fsm); // task 2 user_led_task.setState(USER_LED_OFF); user_led_task.setPeriod(user_led_task_period); user_led_task.setElapsedTime(user_led_task_period); user_led_task.setTickFunction(&user_led_fsm); // set I/O direction for LEDs and button pinMode(blink_led_pin, OUTPUT); pinMode(user_led_pin, OUTPUT); pinMode(button_pin, INPUT); }
Scheduler for Tasks
With all the tasks set up and ready to go, the last thing to do is to create a scheduler to run each task when it needs to tick. The idea is as follow:
- check each task to see if they need to tick
- if so then evaluate the FSM, transition to a new state, and carry out any output(s)/action(s) at the new state
- update how much time has passed since the task last tick
Here is a way to implement the scheduler:
void loop() { current_time = millis(); for (int i = 0; i < num_tasks; i++) { if (tasks[i]->getElapsedTime() >= tasks[i]->getPeriod()) { // tick task int task_current_state = tasks[i]->getState(); int task_new_state = tasks[i]->runTickFunction(task_current_state); tasks[i]->setState(task_new_state); tasks[i]->setElapsedTime(0); } tasks[i]->increaseElapsedTime(current_time - previous_time); } previous_time = current_time; }
Full Code
#include "task.h" static const int blink_led_pin = 1; static const int user_led_pin = 2; static const int button_pin = 3; // variables for tasks static const unsigned char num_tasks = 2; unsigned long current_time = 0; unsigned long previous_time = 0; // task 1 specific static Task led_blink_task; unsigned long led_blink_task_period = 1000; // tick every second enum led_blink_states { OFF = 0, ON = 1 }; // task 1 FSM int blink_led_fsm(int current_state) { // transitions switch (current_state) { case OFF: current_state = ON; break; case ON: current_state = OFF; break; default: // do nothing break; }; // actions switch (current_state) { case OFF: digitalWrite(blink_led_pin, LOW); break; case ON: digitalWrite(blink_led_pin, HIGH); break; default: // do nothing break; }; return current_state; } // task 2 specific static Task user_led_task; unsigned long user_led_task_period = 100; // tick every 1/10 th of a second enum user_led_states { USER_LED_OFF = 0, USER_LED_ON = 1 }; int user_led_fsm(int current_state) { // transition switch (current_state) { case USER_LED_OFF: if (digitalRead(button_pin) == LOW) { // button is active low current_state = USER_LED_ON; } else { current_state = USER_LED_OFF; } break; case USER_LED_ON: if (digitalRead(button_pin) == LOW) { // button is active low current_state = USER_LED_ON; } else { current_state = USER_LED_OFF; } break; default: // do nothing break; } // action switch (current_state) { case OFF: digitalWrite(user_led_pin, LOW); break; case ON: digitalWrite(user_led_pin, HIGH); break; default: // do nothing break; } return current_state; } Task* tasks[] = {&led_blink_task, &user_led_task}; void setup() { // task 1 led_blink_task.setState(OFF); led_blink_task.setPeriod(led_blink_task_period); led_blink_task.setElapsedTime(led_blink_task_period); // tick immediately led_blink_task.setTickFunction(&blink_led_fsm); // task 2 user_led_task.setState(USER_LED_OFF); user_led_task.setPeriod(user_led_task_period); user_led_task.setElapsedTime(user_led_task_period); user_led_task.setTickFunction(&user_led_fsm); // set I/O direction for LEDs and button pinMode(blink_led_pin, OUTPUT); pinMode(user_led_pin, OUTPUT); pinMode(button_pin, INPUT); } void loop() { current_time = millis(); for (int i = 0; i < num_tasks; i++) { if (tasks[i]->getElapsedTime() >= tasks[i]->getPeriod()) { // tick task int task_current_state = tasks[i]->getState(); int task_new_state = tasks[i]->runTickFunction(task_current_state); tasks[i]->setState(task_new_state); tasks[i]->setElapsedTime(0); } tasks[i]->increaseElapsedTime(current_time - previous_time); } previous_time = current_time; }
I hope you found this post helpful. If you found this post helpful, share it with others so they can benefit too.
What are your biggest challenges when working with embedded systems?
To stay in touch, follow me on Twitter, leave a comment, or send me an email at steven@brightdevelopers.com.