June 10, 2017

8. Software Architecture and Multitasking

In the previous posts, you might notice that as the code of the robot was growing, we started grouping related pieces into separate files and tried to make these pieces as independent as possible.

Now it is time to give some explanations, why that was done, and how can we ensure timely reaction to the outside events, having that many things to do on such a simplistic board as Arduino UNO/MEGA.

The robot's thinking process includes many elements. Usually, this is a set of:
  • low-level routines (turn the motor on, blink the LED, beep the beeper)
  • middle-level scenarios (ride while no obstacle in front, stop if obstacle was detected, turn around, escape)
  • high-level intelligence (choose the behavior strategy, learn and remember the rooms configuration, recognize voices and faces).

All these "thoughts" in a form of the program code are running in the robot's "head" continuously and simultaneously. It will be disastrous, if the robot will focus on the complicated route calculation and, in the midst of this excitement, fall down from the stairs, not paying attention to the screaming edge detectors.

The ability of the robot to think many "thoughts" simultaneously is called "multitasking". If you are familiar with this concept and have practical programming experience - feel free to download the source code and experiment with it directly. This post just explains what led us to create such code and why it is done that way.

Arduino uses a single-core processor, thus any reference to multitasking is not strictly correct. At any given moment Arduino can deal only with one "thought" (thread). The effect similar to multitasking can be achieved when each task is split into some small portions, which are handled one by one, quickly switching between the steps from different tasks.

Excellent outline of the Arduino multitasking ideas and principles is provided by Adafruit. See Multi-tasking the Arduino part 1 and part 2. Lots of other articles are just copies and minor extensions to this one.

https://www.hackster.io/feilipu/using-freertos-multi-tasking-in-arduino-ebc3cc
There are good libraries which implement robust and powerful multitasking systems, comparable to those, which are used by big computers. For example, FreeRTOS provides a full-featured preemptive multitasking with the threads prioritization. But, this comes at a price. The smallest footprint of the FreeRTOS core is 5 kilobytes. This is really a lot, considering the size of the actual "tasks" we are going to run on the robot (and the memory size available on our boards).

We decided to create a simplified multitasking system which fits our needs and does not impose excessive overheads. The following principles were taken as a foundation:
  1. Cooperativeness. All tasks should behave in the spirit of cooperation. They should not occupy the CPU for a long time and must voluntarily and timely give up the control, so other tasks can also get a chance do something.
  2. No delay() in the code. Every use of the delay() in the code means the robot regularly lose consciousness and can do nothing while waiting for the delay to finish. If it is more than 10-20 milliseconds -  the robot can easily miss something important (don't stop in front of the wall, don't finish turning on time, get into other troubles). Also, even simple maneuvers will be shaky and harsh.
  3. The time to do some action is determined by calling millis() and comparing it to the pre-calculated dead-line.
  4. Exceptional and catastrophic events are handled by interrupts. This ensures timely reaction to any robot's life-threating events (like an abyss in front).

A simple and straightforward code built using these principles will look like the following:

 // this is the main Arduino controller loop
 loop() {
     <if it is time to turn the motor on> {
         <turn the motor on>
         <remember the time when the motor should be turned off>
     }
     <if it is time to check the distance to the obstacles> {
         <check the distance to the obstacle>
         <think what to do if we have such distance> {
             <maybe enqueue command to stop motors?>
             <maybe enqueue command to turn right or left?>
         }
     }
     <if it is time to blink the LED> {
         <blink the LED>
     }
     …
 }

It is, obviously, the most convenient way to organize your program code - at least it is convenient for your microcontroller. But as your program complexity grows, and more and more cases need to be addressed - it will get harder and harder to maintain such code. Programmers are weak creatures - their memory easily gets overloaded when the logical piece of code grows beyond two screens.

Just to save their time and overcome own brain weaknesses, programmers prefer to group related code in some smaller blocks and work with these blocks separately. Thus instead of dealing with one huge pile of code, they work with easy to remember, easy to understand and easy to change small pieces.

The most obvious idea is to wrap related code to a separate C/C++ functions which will be responsible for some autonomous part. So the main loop of the program will look like the following:

 // this is the main Arduino controller loop
 loop() {
     handleTheMotorsTasks();
     handleTheDistanceSensorsTasks();
     handleTheSoundGenerationTasks():
     handleTheAITasks():
 }

Each function mentioned here should take care only about own business and do not interfere with the others. To avoid any accidental interference between such functions programmers invented a unique concept. It is known as "Object Oriented Programming" (OOP). C++ language was created as an attempt to bring OOP approach to the C-language world.

According to this concept, the program is divided into the logical pieces - objects. Each object should contain all it needs to function properly (both the variables and the functions). Object can be accessed only through the pre-defined communication channels (public properties and methods). This self-sufficiency is referred as "encapsulation".

NB: while it might look that "class" and "object" can be used interchangeably - that is not correct. Class is a DEFINITION of how the object should look like if it happens to be created. Class definition itself does not add any code and does not increase memory consumption in the resulting program. Object - is an ACTUAL INSTANCE of some class, which has own memory and the associated code is built-in to the final program.

Another nice pair of features is inheritance and polymorphism. They allow objects to inherit features from their "parents". Also, child objects can adjust their behavior overriding the parent's code as needed.

For example, we can make all our robot's tasks to be objects which inherit  all common task features from a single ancestor:

  // the ancestor for all robot tasks
  class TaskInterface {
  public:
      virtual void processTask() = 0;
  };

This declaration means that all descendants of this class will have a publicly accessible method processTask(). Assigning zero to the method definition means that all descendants MUST implement their own version of the processTask() method.

Actually, this is all the main program should know about the task objects to be able to call them in the main loop. The main program can look as simple as that:
   
#include "Arduino.h"// Creating instances of the objects, responsible for different tasks.
// Classes for these objects are described in separate files.
// All these classes should be inherited from the TaskInterface
static RobotMotors motors(); // motors object
static RobotVoice robotVoice(); // sound effects object
static RobotAI robotAI(); // artificial intelligence object

// creating the tasks list which will be processed in the main program
// in this example we have 3 such objects
static TaskInterface* (robotTasks[3]);

void setup() {
    // store the references to the task objects in the list
    robotTasks[0] = &motors;
    robotTasks[1] = &robotVoice;
    robotTasks[2] = &robotAI;
}

void loop() {
    // activate each task's object calling their processTask() methods
    for(uint8_t i=0; i<3; i++) {
        robotTasks[i]->processTask();
    }
}

Technically, we could avoid using robotTasks[] array and for() cycle in the loop() function. We could kick all the task objects in the loop(), directly calling their processTask() method. But such code would not look sexy and professionally - never miss a chance to demonstrate everyone you know how to declare an array of pointers in C++ 💪

Having this done, we can switch to the next (smaller and simpler) step - definition of the actual tasks classes, which are stored in separate files.

The definition of the class content is usually done in the header file (that one which have the ".h" extension). The actual code is usually placed in the implementation file (which have the same name but the ".cpp" extension).

For example here is a definition of the class, which takes care about the sounds generation.
   
/*
 * File RobotVoice.h
 */
#ifndef ROBOTVOICE_H_
#define ROBOTVOICE_H_
#include "TaskInterface.h"

// enum is a convenient way to enumerate some limited number of things
// in this case we use it to represent types of the sounds with some human-readable
// identifiers, rather than "sound_code = 0", "sound_code = 1" etc.
enum VoiceSounds {sndNoSound, sndHello, sndOK, sndQuestion, sndScared};

// The RobotVoice class declaration
// It is inherited from TaskInterface,
// so it can be used as a task in the main program
class RobotVoice: public TaskInterface {
private:
    // external modules have no access to the private area of the class
  
    // In this variable we will store the code of the sound,
    // we plan to play next
    VoiceSounds nextSound;
  
    // two functions from the library we discussed in the last post, remember this?
    void playTone(unsigned int toneFrequency, byte beats);
    void playFreqGlissando(float freqFrom, float freqTo, float duty, float duration);

public:
    // this is a constructor which is called automatically when an object is created
    // it should initialize all the fields and do any other needed preparations
    RobotVoice();
  
    // destructor is called automatically, when an object is destroyed
    // it should at least turn off all the sounds
    ~RobotVoice();
  
    // this method is called from the main program loop
    // it checks if it is time to play some sound
    // and if it is - starts playing
    virtual void processTask();
  
    // this method allows queuing sound to be played next
    // this is the only way for other modules (for example AI)
    // to ask the sound module to do something
    // the requested sound will start playing next time the main program will 
    // call sound's module processTask()
    void queueSound(VoiceSounds soundCode);
};
#endif /* ROBOTVOICE_H_ */

The complete source code can be downloaded from the GitHub repository. This example was intentionally simplified to illustrate the whole idea. In the final code you will see the following complications:
  1. Objects, when created, receive the Arduino pin numbers to be used for their functions. Thus we can keep track of all Arduino pins usage in a single place (robot.ino).
  2. Objects also receive references to other objects when they need to use each other. For example, the AI module, as the main orchestrator, receives references to all robot's subsystems.
  3. TaskInterface definition includes two additional convenience methods. They make it easier for the descendants adding events to the queue and checking if the deadline for the event was reached. Thanks to the OOP techniques - there is no need to copypaste the same code to all classes. They just inherit it from the ancestor.

The program was created in the Sloeber environment, but it will also compile in the standard Arduino IDE. Just extract the source code archive in a single folder and open robot.ino in the IDE.

No comments:

Post a Comment