Arduino: Modules et multitaches

Les cartes arduino sont des microcontrôleurs abordables et simples à programmer. Mais les programmes faits pour arduino sont souvent des usines a gaz très peu modulaires. Comme la carte n'est capable de faire tourner qu'un seul programme a la fois, il est difficile de combiner plusieurs fonctionnalités simplement.

Par exemple pour un robot, l'arduino doit pouvoir gérer en simultané la connectivité wifi, les capteurs à ultrasons et les servos et les moteurs.

Nous allons voir comment programmer ces modules afin qu'ils se séparent et partagent le temps de calcul de l'arduino.

Le code de cet article est relativement long et difficle. Jespére ne perdre personne en route! Le code sera réalisé en C++ mais les principes resterons basiques.

Les modules

Notre programme sera donc constitué de modules indépendants, représenté par des classes. Pour rappel, une classe est un ensemble d'attributs (variables) et de méthodes. On peux avoir plusieurs versions (instances) d'une même classe en simultané sans conflit entre elles.

Voici donc l'interface (la classe de base) de nos modules:

/**
* Defines a Brain module
*/
class AbstractBrain {
public:
  /* The name of the module */
  virtual char* getName() = 0;
  /* Initialize the module */
  virtual void initialize() = 0;
  /* Run the module code */
  virtual int tick() = 0;
};

Pour chaque module, on devrai définir son nom, une fonction d'initialisation et une fonction tick. Le principe reste proche du setup/loop d'arduino, mis a part que cette fois la fonction tick sera appellée avec un délai variable.

Mes modules s'apellent Brain. Comme je travaille sur un robot, c'est logique qu'il aie un cerveau! Libre à vous de renommer les classes pour quelque chose qui à plus de sens pour vous.

Comme les modules doivent se partager le temps de calcul de l'arduino, la méthode tick retourne un int. Cet int représente le temps (en millisecondes) à attendre avant de rapeller la fonction tick pour ce module.

Un module de base

Commençons par défini un module de base. Quelque chose qui va valider que tout fonctionne. Pour ça, le plus simple est de faire clignotter la led de la carte (celle qui est reliée au pin 13).

On défini donc notre module (nommé StatusLed) qui étend AbstractModule via un fichier StatusLed.h:

#include "BrainModules.h"

/**
 * Define a module that just blink a led
 */
class StatusLed : public AbstractBrain {
private:
	bool isOn;
	int blinkFreq;
public:
	char* getName();
	void initialize();
	int tick();
};

Et enfin le code .c associé:

#include "BrainModule.h"
#include "StatusLed.h"


char * StatusLed::getName()
{
	return "Status Led";
}

void StatusLed::initialize()
{
	blinkFreq = 1000;
	pinMode(13, OUTPUT);
}

int StatusLed::tick()
{
	isOn = !isOn;

	if (isOn) {
		digitalWrite(13, HIGH);
	}
	else {
		digitalWrite(13, LOW);
	}

	return blinkFreq;
}

Nous avons donc maintenant un module de base. Passons maintenant à l'ordonnanceur, c'est a dire le coeur qui va orchestrer tout ça!

L'ordonanceur

Pour répartir le temps entre les différents modules, nous avons besoin d'un ordonnanceur. C'est lui qui s'occupera de donner la main à chaque module tour à tour.

Basiquement, le but de l'ordonanceur est de rajouter une couche entre les fonctions boot et loop d'arduino et le code qui sera executé.

Voici l'interface de cette classe:

#define BRAIN_MAX_MODULES 4

#define BRAIN_OK 0
#define BRAIN_ERROR_NO_MODULES -1

/**
 *  Define a structure that old information for a given Brain module
 */
typedef struct BrainConfig {
	boolean enabled;
	AbstractBrain* module;

	unsigned long lastRunTime;
	int timeInterval;

	inline bool isDue() {
		return (unsigned long)(millis() - lastRunTime) >= timeInterval;
	}
};

/**
 * The main Brain class
 */
class Brain {
private:
	int nextModule;
	int moduleCount;
	BrainConfig* actualModule;
	BrainConfig configs[BRAIN_MAX_MODULES];
public:
	int addModule(AbstractBrain* module);
	int removeModule(AbstractBrain* module);
	void initialize();
	void tick();
	void printModule(AbstractBrain* module, Print* output);
	void printModules(Print* output);

protected:
	BrainConfig* getModuleConfig(AbstractBrain* module);
	BrainConfig* getFreeModuleConfig();
	void executeModule(BrainConfig * config);
};

La première structure qui contiendra les informations spécifiques a chaque Module. Cette structure contiendra un pointeur vers la classe du module, ainsi que la date de derniére execution et l'intervalle avant la prochaine. La méthode inline isDue sert a définir si le module doit être éxécuté ou non.

La seconde classe représente l'ordonnanceur en lui-même. On retrouve les méthodes initialize et tick qui iront respectivement dans le boot et le loop d'arduino. Les méthodes addModule et removeModule permettent d'ajouter/supprimmer des modules à executer. Enfin, les méthodes printModules et printModule (sans S cette fois!) servent au débuggage.

Enfin, passons au code de l'ordonnanceur:

#include "Origo.h"
#include "Brain.h"

int Brain::addModule(AbstractBrain * module)
{
	DEBUG_PRINT("Brain: Adding module ");
	DEBUG_PRINT(module->getName());
	DEBUG_PRINT("\n");

	BrainConfig* freeConfig = getFreeModuleConfig();
	if (freeConfig == nullptr) {
		DEBUG_PRINT("Brain: No more modules slots\n");
		return BRAIN_ERROR_NO_MODULES;
	}

	freeConfig->enabled = true;
	freeConfig->module = module;

	return BRAIN_OK;
}

int Brain::removeModule(AbstractBrain * module)
{
	DEBUG_PRINT("Brain: Removing module ");
	DEBUG_PRINT(module->getName());
	DEBUG_PRINT("");

	BrainConfig* config = getModuleConfig(module);
	config->enabled = false;
	config->module = nullptr;

	return BRAIN_OK;
}

void Brain::initialize()
{
	DEBUG_PRINT("Brain: Initializing modules \n");
	for (int i = 0; i < BRAIN_MAX_MODULES; i++) {
		if (configs[i].enabled) {
			DEBUG_PRINT("\tBrain: Initializing module ");
			DEBUG_PRINT(configs[i].module->getName());
			DEBUG_PRINT("\n");

			configs[i].module->initialize();
		}
	}
}

void Brain::tick()
{
	//Run module code
	if (configs[nextModule].enabled && configs[nextModule].isDue()) {
		executeModule(&configs[nextModule]);
	}

	//Jump to the next module available
	for (int i = 1; i < BRAIN_MAX_MODULES; i++) {
		int index = (nextModule + i) % BRAIN_MAX_MODULES;
		if (configs[index].enabled && configs[index].isDue()) {
			nextModule = index;
			break;
		}
	}
	
}

void Brain::printModule(AbstractBrain * module, Print * output)
{
	BrainConfig* moduleConfig = getModuleConfig(module);

	if (moduleConfig == nullptr) {
		return;
	}

	output->print("Brain: ");
	output->print("\t\t");
	output->print(moduleConfig->module->getName());
	output->print("\t\t");
}

void Brain::printModules(Print * output)
{
	output->print("Brain: ");
	output->print("\t\t");
	output->print("Name");
	output->print("\t\t\t");
	output->print("Run time");
	output->print("\t");
	output->print("Late Time");
	output->print("\n");

	for (int i = 0; i < BRAIN_MAX_MODULES; i++) {
		if (configs[i].enabled) {
			printModule(configs[i].module, output);
		}
	}

	output->print("\n");
}

BrainConfig * Brain::getModuleConfig(AbstractBrain * module)
{
	for (int i = 0; i < BRAIN_MAX_MODULES; i++) {
		if (configs[i].enabled && configs[i].module == module) {
			return &configs[i];
		}
	}

	return nullptr;
}

BrainConfig * Brain::getFreeModuleConfig()
{
	for (int i = 0; i < BRAIN_MAX_MODULES; i++) {
		if (!configs[i].enabled) {
			return &configs[i];
		}
	}

	return nullptr;
}

void Brain::executeModule(BrainConfig * config)
{
	actualModule = config;

	int retVal = config->module->tick();
	config->lastRunTime = millis();
	config->timeInterval = retVal;

	actualModule = nullptr;
}

Premier lancement

Nous avons donc défini nos modules, l'ordonnanceur qui va répartir le temps de calcul entre chaque module, et un module de base. Il ne nous reste plus qu'a écrire un sketch arduino (.ino) qui va nous permettre de connecter et de lancer tout ça sur la carte.

#include "Brain.h"
#include "StatusLed.h"

Brain brain;          //Create Brain instance
StatusLed status;     //Create instance of StatusLed BrainModule

void setup() {
	Serial.begin(19200); while (!Serial); //Init serial and wait for it (for leonardo)

	Serial.println("Setup: Configuring brain modules");
	brain.addModule(&status);             //Register the module to the Brain
	
	Serial.println("Setup: Initializing brain modules");
	brain.initialize();                   //Initialize all registered modules

	Serial.println("Setup: starting done");
}

void loop() {
	brain.tick();     //Take care of executing all modules
}

Ce code est trés simple, on déclare d'abord le Brain, puis chaque module. On ajoute chaque module a la liste interne du brain, puis on lance les fonctions initialize et tick.

Conclusion

Normalement, la led de votre arduino clignotte et indique fiérement la fin de cet article! Vous avez un arduino multitaches qui n'attends plus que d'autres modules plus utiles de votre conception !