Mouvoir lentement un organe mécanique revient fréquemment en modélisme ferroviaire. On peut citer les aiguillages, les signaux mécaniques, les passages à niveau ou bien encore les portes de remises. Parmi les actionneurs possibles, on trouve les moteurs à courant continu, les moteurs pas à pas et les servomoteurs. Ces derniers sont les plus simple à commander avec un Arduino car ils ne nécessitent aucune électronique supplémentaire. Les servomoteurs ont fait l’objet de plusieurs articles sur Locoduino, principalement « Les différents types de mouvements d’un servomoteur » et « La bibliothèque Servo ». Nous vous présentons maintenant une nouvelle bibliothèque réalisée par l’auteur et permettant de déplacer lentement plusieurs servomoteurs selon des trajectoires précises.
La bibliothèque SlowMotionServo
. Par :
. URL : https://www.locoduino.org/spip.php?article159Après la réalisation du logiciel de pilotage de servomoteurs destiné à la commande d’actionneurs d’aiguillages, voir « Manœuvre des aiguilles avec des servomoteurs » j’ai voulu m’attaquer à la manœuvre de portes de remises et de barrières de passage à niveau. Les mouvements à accomplir sont plus complexes mais aussi plus visuels. La technique doit donc être différente.
Le mouvement du servomoteur pour actionner un aiguillage est linéaire, c’est à dire que l’évolution de l’angle du palonnier au cours du temps est une droite. Cela convient tout à fait dans ce cas mais pour animer le reseau, d’autres mouvements sont nécessaires. Par exemple, l’ouverture d’une porte de remise aura une phase d’accélération, la porte est poussée, une phase de décélération, la porte continue sur son erre et les frottements la ralentisse puis une phase de rebond où la porte revient en arrière avec un mouvement amorti. Il est donc nécessaire de définir le mouvement du servomoteur comme une trajectoire. De cette manière tout est permis.
La bibliothèque SlowMotionServo est construite pour cela. Elle permet de piloter plusieurs servomoteurs simultanément et, pour chacun de ces servomoteurs, de définir une trajectoire, en fait deux trajectoires, une pour le mouvement du palonnier dans le sens trigonométrique et une pour le mouvement dans le sens horaire. La vitesse de parcours de ces deux trajectoires est également réglable.
La position du servomoteur est normalisée sous forme d’un nombre à virgule entre 0.0 et 1.0, 0.0 correspond à la largeur d’impulsion de commande minimum et 1.0 à la largeur maximum.
Installer la bibliothèque
SlowMotionServo est disponible via le gestionnaire de bibliothèque de l’IDE. Dans le menu Croquis, sélectionnez Inclure une bibliothèque puis Gérer les bibliothèques. Dans la case de recherche en haut à droite de la fenêtre qui apparaît, tapez SlowMotionServo.
Mise en œuvre rapide
Commençons par une utilisation simple. Nous n’avons qu’un seul servomoteur à piloter. Commençons par un simple mouvement linéaire. Tout d’abord, comme pour tout autre bibliothèque, il faut inclure SlowMotionServo et Servo, bibliothèque sur laquelle repose SlowMotionServo. Sélectionner dans le menu Croquis puis Inclure une bibliothèque puis SlowMotionServo ajoutera ces deux inclusions dans votre sketch.
#include <Servo.h>
#include <SlowMotionServo.h>
Ensuite, nous allons créer un objet de la classe SMSLinear
.
SMSLinear monServo;
Dans setup
, nous spécifions la broche par laquelle le servo est commandé, ici la broche 2.
void setup()
{
monServo.setPin(2);
}
Il nous faut également une variable qui va contenir alternativement 0.0 et 1.0 pour fixer la position cible du servomoteur. Nous initialisons cette variable à 0.0.
float positionCible = 0.0;
Dans loop
, nous devons appeler la fonction qui assure l’actualisation de la position du servo et, quand le servo est à l’arrêt, c’est à dire que sa position cible est atteinte, nous changeons cette position cible.
void loop()
{
SlowMotionServo::update(); /* actualisation de la position */
if (monServo.isStopped()) {
positionCible = 1.0 - positionCible;
monServo.goTo(positionCible);
}
}
Trois trajectoires sont livrées, une trajectoire linéaire, SMSLinear
, qui convient pour les aiguillages :
une trajectoire avec une phase d’accélération et de décélération, SMSSmooth
:
et une trajectoire avec dans une direction une phase d’accélération et de décélération suivi d’un amorti, la courbe rouge, et dans l’autre direction une phase d’accélération et de décélération seule, la courbe bleue sur le graphe qui suit, SMSSmoothBounce
:
Cette dernière trajectoire convient pour les portes de remise. Il est bien évidemment possible de définir ses propres trajectoires.
Changer les positions minimum et maximum
Nous avons vu que 0.0 correspond à la position minimum et que 1.0 correspond à la position maximum. Par défaut, ces positions correspondent respectivement à des largeurs d’impulsion de 1000µs et 2000µs, c’est à dire les valeurs minimum et maximum recommandées par la bibliothèque Servo. Il est bien évidemment possible de les changer mais faites attention de rester dans les limites permises par vos servomoteurs.
setMin (min)
Définit l’angle minimum du servo. L’angle est exprimé en son équivalent en microsecondes. La valeur peut varier de 544 à 2400. Une valeur inférieure à 544 sera ramenée à 544 et une valeur supérieure à 2400 sera ramenée à 2400. Si la valeur est inférieure à l’angle minimum, elle est ramenée à l’angle minimum.
setMax (max)
Définit l’angle maximal du servo. L’angle est exprimé en son équivalent en microsecondes. La valeur peut aller de 544 à 2400. Une valeur inférieure à 544 sera ramenée à 544 et une valeur supérieure à 2400 sera ramenée à 2400. Si la valeur est supérieure à l’angle maximum, elle est ramenée à l’angle maximum.
setMinMax (min, max)
Définit les angles minimum et maximum du servo. L’angle est exprimé en son équivalent en microsecondes. La valeur peut varier de 544 à 2400. Une valeur inférieure à 544 sera ramenée à 544 et une valeur supérieure à 2400 sera ramenée à 2400. Si l’angle minimum est supérieur à l’angle maximal, les deux angles sont définis sur la valeur moyenne . Par exemple, si vous définissez l’angle minimum sur 2000 et l’angle maximal sur 1000, les deux angles seront définis sur 1500, ce qui se traduira par une impossibilité de mouvement.
Changer la vitesse du mouvement et le sens du mouvement
Par défaut, quelques soient les angles minimum et maximum, les trajets de l’angle minimum à l’angle maximum et inversement sont effectués en 10s. Les vitesses peuvent être réglées via les fonctions suivantes.
setMinToMaxSpeed(vitesse)
Règle la vitesse du servo lorsqu’il se déplace de l’angle minimum à l’angle maximum. la vitesse est un nombre à virgule. Une vitesse de 1.0 correspond à une durée de 10s.
setMaxToMinSpeed(vitesse)
Règle la vitesse du servo lorsqu’il se déplace de l’angle maximum à l’angle minimum. la vitesse est un nombre à virgule. Une vitesse de 1.0 correspond à une durée de 10s.
setSpeed(vitesse)
Règle la vitesse du servo lorsqu’il se déplace de l’angle minimum à l’angle maximum et de l’angle maximum à l’angle minimum. la vitesse est un nombre à virgule flottante. Une vitesse de 1.0 correspond à un 10s en déplacement.
setReverted(sens)
Par défaut, sens
, un booléen, est false
et les trajectoires correspondent aux graphiques ci-dessus. Si sens
est mis à true
, le mouvement est inversé selon un axe de symétrie situé à t=0.5. Par exemple, la trajectoire SMSSmoothBounce
devient :
Déclenchement du mouvement
Les fonctions suivantes permettent de mouvoir le servomoteur.
setInitialPosition(position)
Définit la position initiale du servo. La position est un nombre à virgule flottante allant de 0.0 à 1.0. Si la valeur est supérieure à 1.0, elle est ramenée à 1.0 et si elle est inférieure à 0.0, elle est ramenée à 0.0
goTo(position)
Va à la position spécifiée en suivant la trajectoire. La position est un nombre à virgule flottante allant de 0.0 à 1.0. Si la valeur est supérieure à 1.0, elle est ramenée à 1.0 et si elle est inférieure à 0.0, il est ramenée à 0.0
goToMin()
Équivalent à goTo(0.0)
goToMax()
Équivalent à goTo(1.0)
setDetachAtMin(detache)
detache
est un booléen. Si true
, le servo est détaché lorsque la position minimum est atteinte. Le servo n’est plus piloté. Ceci est utile lorsque le servo doit pousser contre une force de rappel élastique. Si false
, le servo continue à être piloté.
setDetachAtMax(detache)
detache
est un booléen. Si true
, le servo est détaché lorsque la position maximum est atteinte. Le servo n’est plus piloté. Ceci est utile lorsque le servo doit pousser contre une force de rappel élastique. Si false
, le servo continue à être piloté.
setDetach(detache)
detache
est un booléen. Si true
, le servo est détaché lorsque la position minimum ou maximum est atteinte. Le servo n’est plus piloté. Ceci est utile lorsque le servo doit pousser contre une force de rappel élastique. Si false
, le servo continue à être piloté.
isStopped()
retourne true
si le servo est arrêté, false
si il est en mouvement.
Les autres fonctions
setPin(pin)
Spécifie la broche à laquelle le servo est branché.
SlowMotionServo::setDelayUntilStop(delai)
Cette fonction de classe définit le délai entre le moment où les servos atteignent leur position minimum ou maximum et le moment où ils sont détachés. Comme la mécanique est toujours en retard par rapport au programme, détacher immédiatement les servos les empêcherait d’atteindre leur position mécanique. Ceci est défini une fois pour tous les servos et utilisé uniquement pour les servos et les positions pour lesquelles setDetach(true)
est spécifié.
SlowMotionServo::update()
Met à jour les positions de tous les servos. Cette fonction de classe doit être appelée dans loop()
. Il ne faut pas que le temps d’exécution de loop()
soit trop long sinon le mouvement du ou des servomoteurs se fera par à-coups [1]. Il faut notamment et absolument bannir delay()
.
Comment définir ses propres trajectoires
Pour ce faire, vous devez hériter de la classe SlowMotionServo
et redéfinir les fonctions membres slopeUp
et slopeDown
. Prenons la classe SMSSmooth
comme exemple :
class SMSSmooth : public SlowMotionServo
{
public:
virtual float slopeUp(float time);
virtual float slopeDown(float time);
float slope(float time) { return (1.0 - cos(time * PI))/2.0; }
};
Comme les trajectoires sont les mêmes de min à max et max à min, nous définissons une nouvelle fonction membre, slope
, qui définit la trajectoire. Cette fonction est appelée par slopeUp
et slopeDown
:
float SMSSmooth::slopeUp(float time)
{
return slope(time);
}
float SMSSmooth::slopeDown(float time)
{
return slope(time);
}
Un exemple concret
Pour aller plus loin que le petit exemple déjà présenté, je vous propose une petite application permettant de gérer les portes d’une remise à locomotives au moyen d’un bouton poussoir. Le comportement est simple : si les portes sont toutes les deux fermées, une pression sur le bouton les ouvre. Si les portes sont toutes les deux ouvertes, une pression sur le bouton les ferme. Si au moins l’une est en mouvement une pression sur le bouton est sans effet.
Afin de permettre une réutilisation facile et une réplication des double-portes, nous allons définir une class Remise
va regrouper les deux servos et le bouton ainsi que la fonction les initialisant et la fonction faisant fonctionner le système. Il s’agit en fait de l’encapsulation du code de l’exemple PushButton2Servos livré avec la bibliothèque. Voici la classe Remise :
class Remise {
private: SMSSmoothBounce mPorteDroite;
private: SMSSmoothBounce mPorteGauche;
private: Bounce mBoutonOverture;
private: bool mPosition; /* false = fermé, true = ouvert */
public: void demarre(const byte pinBouton, const byte pinServoPorteDroite, const byte pinServoPorteGauche);
public: void gereRemise();
};
On y trouve nos deux servos de type SMSSmoothBounce
et le bouton [2]. On a également une variable booléenne, mPosition
permettant de mémoriser si les portes sont en position ouverte ou fermée. Enfin, la fonction demarre
permet d’initialiser tout ce petit monde et la fonction gereRemise
gère le bouton et lance le mouvement si il est enfoncé et que les portes sont stoppées.
La fonction demarre
Cette fonction procède donc aux intialisations. La broche du bouton est programmée en entrée avec résistance de tirage à 5V, l’objet mBouton
est accroché à sa broche et son intervalle de rafraichissement est fixé. Les valeurs minimum et maximum des deux portes sont fixées avec une ouverture légèrement supérieure à 90°. Les valeurs sont à affiner selon vos servos. La vitesse est fixée à 1.7, cette valeur est à régler selon vos goûts. Le mouvement de la porte gauche est inversé par rapport au mouvement de la porte de droite. Les positions initiales sont fixées à 0.1 afin qu’il y ait un mouvement au démarrage pour fermer les portes (les deux goTo(0.0)
à la fin de demarre
). En effet, il faut que la position initiale soit différente du premier mouvement demandé, sinon aucun mouvement n’aura lieu. Enfin, les deux servos sont accrochés à leur broche respectives.
void Remise::demarre(const byte pinBouton, const byte pinServoPorteDroite, const byte pinServoPorteGauche)
{
pinMode(pinBouton, INPUT_PULLUP);
mBoutonOverture.attach(pinBouton);
mBoutonOverture.interval(5); /* rafraichissement toutes les 5ms */
mPorteDroite.setMin(700);
mPorteDroite.setMax(1800);
mPorteDroite.setSpeed(1.7);
mPorteGauche.setMin(1100);
mPorteGauche.setMax(2200);
mPorteGauche.setSpeed(1.7);
mPorteGauche.setReverted(true); /* La porte gauche a un mouvement inverse */
mPorteDroite.setInitialPosition(0.1);
mPorteGauche.setInitialPosition(0.1);
mPorteDroite.setPin(pinServoPorteDroite);
mPorteGauche.setPin(pinServoPorteGauche);
mPosition = false;
/* ferme les porte au démarrage */
mPorteDroite.goTo(0.0);
mPorteGauche.goTo(0.0);
}
La fonction gereRemise
Cette fonction lit le bouton et, si aucune des portes n’est en mouvement et si le bouton a été enfoncé, fixe une position cible en fonction de la position courante. Voici cette fonction :
void Remise::gereRemise()
{
mBoutonOverture.update(); /* met à jour l'état du bouton */
if (mPorteDroite.isStopped() && mPorteGauche.isStopped())
{
/* si un mouvement était en cours, il est terminé */
if (mBoutonOverture.fell()) {
/* appui sur le bouton, on démarre le mouvement */
if (mPosition) {
/* la remise est ouverte, la cible pour la fermer est 0.0 */
mPorteDroite.goTo(0.0);
mPorteGauche.goTo(0.0);
mPosition = false;
}
else {
/* la remise est fermée, la cible pour l'ouvrir est 1.0 */
mPorteDroite.goTo(1.0);
mPorteGauche.goTo(1.0);
mPosition = true;
}
}
}
}
Le sketch
Tout d’abord, nous créons un objet de type Remise
:
Remise remise1;
Ensuite nous déclarons des constantes correspondant aux broches où sont connectés le bouton et les servos :
const byte pinServoDroitRemise1 = 4;
const byte pinServoGaucheRemise1 = 3;
const byte pinBoutonRemise1 = 5;
Dans setup
, nous initialisons la remise :
void setup()
{
remise1.demarre(pinBoutonRemise1, pinServoDroitRemise1, pinServoGaucheRemise1);
}
Enfin dans loop, nous appelons SlowMotionServo::update()
pour actualiser la position des servos et nous appelons gereRemise()
pour saisir les ordres de l’utilisateur et lancer le mouvement des portes :
void loop()
{
SlowMotionServo::update();
remise1.gereRemise();
}
Voici le sketch à télécharger :
Et une vidéo de démonstration de ce sketch :
Et si j’ai plusieurs double-portes ?
C’est assez simple :
- Déclarez toutes les constantes des numéros de broches pour tous les servos et les boutons ;
- Instanciez autant d’objets
Remise
que vous avez de double-portes. Par exemple,remise1
,remise2
,remise3
, etc. - Appelez
demarre
danssetup
pour chacun des objetsRemise
. - Appelez
gereRemise
dans loop pour chacun des objetsRemise
. Notez que l’appel deSlowMotionServo::update()
reste en un seul exemplaire.
[1] Par exemple, pour un movement linéaire, si le servomoteur parcoure 90° en 5s, ceci correspond à une variation de durée d’impulsion de 1000µs en 5s environ. La résolution de la durée d’impulsion imposé par la bibliothèque Servo étant de 4µs, update
doit être appelée au moins toutes les 20ms.
[2] Comme pour l’exemple PushButton2Servos, on utilise la bibliothèque Bounce2.