Différentes méthodes pour faire tourner le pas à pas

Sommaire:
      Quelle méthode choisir?
      Notes sur l'impulsion de Step
      Autre + Préparation + les pas
      delay()
      Avec dernière_action = micros()
      Avec dernière_action = micros() puis Step
      Avec dernière_action += T
      Non bloquant avec micros()
      Un moteur sous interruption
      Plusieurs moteurs avec plusieurs timers
      Plusieurs moteurs avec un seul timer, prochaine impulsion
      Plusieurs moteurs avec un timer, impulsion ou pas
      Table de déplacements

 

Quelle méthode choisir?

Pour faire tourner un moteur pas à pas avec une Arduino, il faut générer quelques signaux. Que l'on commande les bobines individuellement ou en utilisant un circuit spécialisé qui fait une grande partie du travail, il faut dans tous les cas avoir une ou plusieurs sorties qui changent d'état à des instants précis. Je m'intéresse principalement dans le cadre de la bibliothèque QuickStep à la création des deux signaux Step et Dir, mais les principes sont les mêmes si d'autres signaux étaient utiles.

Il n'y a pas une méthode miracle pour tout, chacune ayant ses avantages. Je vais passer en revue quelques idées possibles.

Quelle que soit la méthode utilisée, il faudra dans un ordre ou un autre:
- calculer comment faire la pas suivant (temps qu'il faudra, éventuellement quel moteur, les accélérations...), noté CALCUL par la suite
- faire passer Step à 1, noté Step par la suite
- faire passer Step à 0, noté Step par la suite
- éventuellement attendre un minimum entre deux changements de Step (A4988: 1µs / DRV8825: 1,9µs / TB6600: 2,2µs), noté 1µs par la suite
- éventuellement attendre un minimum entre deux fronts montants de Step (A4988: 2µs / DRV8825: 4µs / TB6600: 4,4µs), ignoré dans un premier temps
- attendre un temps T entre deux pas (c'est ce qui va donner la vitesse), noté T par la suite
- préparer le déplacement, noté PREPARATION par la suite
- faire d'autres traitements (programme principal: afficher, lire, dialoguer, traiter d'autrs données...), noté AUTRE par la suite

 

Notes sur l'impulsion de Step

Pour faire une impulsion sur Step, une réalisation courante est d'utiliser les deux lignes:

       digitalWrite(Step, HIGH);
       digitalWrite(Step, LOW);
Le temps d'une instruction digitalWrite étant de 2,4µs, ces deux lignes vont alors faire une impulsion dont l'état haut devrait durer 2,4µs ce qui est suffisant pour les circuits A4988, DRV8825 et TB6600. Si par contre on utilise les instructions directes de modifications de port (durée 2 cycles d'horloge à 16MHz pour une Uno, soit 0,125µs), il sera nécessaire de rajouter entre les deux des instructions un délai pour élargir l'impulsion, par exemple à 2µs. Cela peut donner:
       PINX = 0bxxxxxxxx; // Inversion de Step, donc passage à l'état haut
       delayMicroseconds(3); // 1µs pour un A4988, 2µs pour un DRV8825, 3µs pour un TB6600...
       PINX = 0bxxxxxxxx; // Inversion de Step, donc passage à l'état bas
Je ne parlerai plus de ce délai par la suite si ce n'est pas utile.

 

Autre + Préparation + les pas

Une façon simple de faire tourner un moteur pas à pas est de préparer le déplacement puis de faire les pas indépendamment les uns les autres.

      ...       AUTRE PREPARATION Pas N°1 Pas N°2 Pas N°3       ...       Dernier pas AUTRE PREPARATION Pas N°1 Pas N°2       ...      

Cette façon de travailler est un grand classique, il y a plusieurs façons de réaliser un pas, que je vais analyser.

Avantages:
    - Facile à écrire et à comprendre
    - Facile à utiliser
Inconvénients:
    - Le code est bloquant, on ne peut rien faire pendant que le moteur tourne
    - Entre deux déplacements il y a un temps d'arrêt (AUTRE + PREPARATION)
    - On ne peut faire tourner qu'un seul moteur car cela occupe tout le temps disponible
Conclusion:
Très bien si on n'a pas beaucoup d'exigences et de contraintes.

 

Avec delay()

Avec la méthode précédente, on peut avancer d'un pas en faisant:

      ...       CALCUL Step
-----
1µs à 3µs -----
Step
CALCUL delay(T)       ...      

On peut difficilement retirer à delay() le temps mis par CALCUL car ce dernier peut être variable (si présence de if par exemple). Du coup, comme le délai est constant, les pas ne le sont plus. Il faudrait mesurer le temps de calcul total et le temps de l'impulsion de Step pour le déduire de la valeur à passer à delay().

Avantages:
    - Très facile à écrire (pas de connaissances spéciales)
    - Facile à utiliser
Inconvénients:
    - Le code est bloquant, on ne peut rien faire pendant que le moteur tourne
    - Entre deux déplacements il y a un temps d'arrêt (la PREPARATION)
    - Si on tourne vite, le temps de calcul n'est plus négligeable devant T
    - On ne peut faire tourner qu'un seul moteur car cela occupe tout le temps disponible
Conclusion:
Très bien si on n'a pas beaucoup d'exigences et de contraintes.

Le programme Montant.ino et Descendant.ino utilisent cette méthode car il ma fallait un programme court, compréhensible, et il n'y avait aucune exigences sur la vitesse.

 

Avec dernière_action = micros()

Comme delay() est bloquant et que l'on ne peut rien faire d'autre, on ne peut pas commencer très tôt le comptage du temps. Classiquement ce problème se résout avec la fonction millis(). On peut par exemple mémoriser l'instant de l'impulsion dans une variable dernière_action , faire les calculs et attendre que millis() se soit suffisamment incrémenté pour faire le pas suivant. Le chronogramme pour un pas va ressembler à:

      ...       CALCUL N°1 Step
-----
-----
Step
dernière_action = micros() CALCUL N°2 while (micros() - dernière_action < T); CALCUL N°3       ...      

En début de la gestion du pas, on a un petit calcul (calcul N°1), en général, il s'agit devoir si il reste des pas à faire. Vient ensuite l'impulsion sur Step, la lecture du temps de cette action, éventuellement un traitement un peu plus important, par exemple de calculer les données pour le pas suivant si on a une accélération. Quand on a fini ce traitement une boucle d'attente permet de patienter jusqu'au début du pas suivant. Il reste toujours un petit quelque chose à faire ensuite, au moins le retour au début du traitement du pas.

Le temps entre la lecture de dernière_action et la fin de la boucle peut dans un premier temps être estimé comme constant et le temps de calcul n'interviendra plus. Sans correction le temps T est donc très légèrement inférieur au temps réel entre deux impulsions. C'est surtout sur ce point que l'on y gagne.

Avantages:
    - Assez facile à écrire, mais il faut savoir utiliser micros()
    - Facile à utiliser
Inconvénients:
    - Le code est bloquant, on ne peut rien faire pendant que le moteur tourne
    - Entre deux déplacements il y a un temps d'arrêt (la PREPARATION)
    - On ne peut faire tourner qu'un seul moteur car cela occupe tout le temps disponible
Conclusion:
Très bien si on n'a pas beaucoup d'exigences et de contraintes. Assez semblable à la méthode précédente, sauf que le temps est plus juste.

La bibliothèque BasicStepperDriver de StepperDriver-master utilise cette méthode.

 

Avec dernière_action = micros() puis Step

On peut encore améliorer la méthode précédente en prenant la mesure du temps le plus tôt possible. Ainsi le temps programmé sera plus près encore du temps réel. Le chronogramme pour un pas va ressembler à:

      ...       CALCUL N°1 dernière_action = micros() Step
-----
-----
Step
CALCUL N°2 while (micros() - dernière_action < T); CALCUL N°3       ...      

Avantages:
    - Assez facile à écrire, mais il faut savoir utiliser micros()
    - Facile à utiliser
Inconvénients:
    - Le code est bloquant, on ne peut rien faire pendant que le moteur tourne
    - Entre deux déplacements il y a un temps d'arrêt (la PREPARATION)
    - On ne peut faire tourner qu'un seul moteur car cela occupe tout le temps disponible
Conclusion:
Très bien si on n'a pas beaucoup d'exigences et de contraintes. Assez semblable à la méthode précédente, sauf que le temps est encore plus juste.

La bibliothèque Stepper utilise cette méthode.

 

Avec dernière_action += T

On peut une nouvelle fois améliorer la méthode précédente en ne mettant à jour dernière_action sans millis(). Si un pas a été fait à l'instant dernière_action, le suivant doit être fait à dernière_action + T. C'est à mon avis la seule bonne méthode pour utiliser millis() correctement pour des instants réguliers. Ainsi quelle que soit la durée du calcul, l'intervalle moyen sera correct. Si un pas intervenait trop tard pour une raison quelconque, le suivant serait à sa place. Le temps programmé est ainsi le temps réel. Le chronogramme pour un pas va ressembler à:

      ...       CALCUL N°1 Step
-----
-----
Step
dernière_action += T CALCUL N°2 while (micros() - dernière_action < T); CALCUL N°3       ...      

Avantages:
    - Assez facile à écrire, mais il faut savoir utiliser micros()
    - Le temps programmé est correct
    - Facile à utiliser
Inconvénients:
    - Le code est bloquant, on ne peut rien faire pendant que le moteur tourne
    - Entre deux déplacements il y a un temps d'arrêt (la PREPARATION)
    - On ne peut faire tourner qu'un seul moteur car cela occupe tout le temps disponible
Conclusion:
Très bien si on n'a pas beaucoup d'exigences et de contraintes. Assez semblable à la méthode précédente, sauf que le temps est juste.

 

Non bloquant avec micros()

Puisque on boucle sans rien faire en attendant le pas suivant, on pourrait pendant ce temps rendre la main à loop() quelque temps. On dira alors que ce code est non bloquant car pendant que le moteur tourne, on peut effectuer une autre tâche. Le programme va ressembler à:

void avance_d_un_pas(void)
{
...
}

void loop(void)
{
  if (micros() - dernière_action > T) avance_d_un_pas();
  <le code a exécuter en même temps>
}

Bien entendu, le code à exécuter devra se faire pendant l'intervalle de temps d'un pas. Si le code est très court, il y a plusieurs exécutions de <le code a exécuter en même temps>. Par contre si ce dernier code prend trop de temps, cela va retarder l'appel de avance_d_un_pas() et le moteur ne tournera plus correctement.

avance_d_un_pas() quand à lui va ressembler à:

CALCUL N°1 Step
-----
-----
Step
CALCUL N°2

Dans la pratique, on ne va pas faire qu'un seul mouvement, mais on va en enchaîner plusieurs. Supposons que l'on veuille boucler sur 2 ordres, la boucle loop() peut devenir:

void loop(void)
{
  <demande d'avance>
  while (<il reste des pas à faire>)
  {
    if (micros() - dernière_action > T) avance_d_un_pas();
    <le code a exécuter en même temps>
  }	
  <demande du retour>
  while (<il reste des pas à faire>)
  {
    if (micros() - dernière_action > T> avance_d_un_pas();
    <le code a exécuter en même temps>
  }	
}

Pour ne pas surcharger loop(), le test if (micros() - dernière_action > T> va être mis dans la fonction qui fait progresser le moteur. On aura alors:

void scan_moteur(void)
{
   if (micros() - dernière_action > T) avance_d_un_pas();
}

void loop(void)
{
  <demande d'avance>
  while (<il reste des pas à faire>)
  {
    scan_moteur();
    <le code a exécuter en même temps>
  }	
  <demande du retour>
  while (<il reste des pas à faire>)
  {
    scan_moteur();
    <le code a exécuter en même temps>
  }	
}
Cela simplifie un peu pour l'utilisateur.

Comme le code pour un moteur n'est pas bloquant, on peut faire tourner plusieurs moteurs en même temps. Il y aura par exemple plusieurs fonctions avance_d_un_pas(), ou une seule qui scanne les différents moteurs

Avantages:
    - On peut exécuter une tâche pendant que le moteur tourne (le code n'est pas bloquant)
Inconvénients:
    - Un peu plus compliqué pour écrire la bibliothèque
    - Beaucoup plus compliqué pour l'utilisateur (loop)
    - Il faut que l'utilisateur écrive un code très court
    - Le code utilisateur ne doit pas être bloquant.
Conclusion:
Cela se complique surtout pour l'utilisateur, mais il peut écrire du code qui s'exécute pendant la rotation

La bibliothèque NonBlocking de StepperDriver-master utilise cette méthode avec un seul moteur. Elle rajoute un test en plus, ne permettant à l'utilisateur d'exécuter son code que si il teste au moins 100µs de disponible. Ainsi l'avance d'un pas ne dépend plus du code utilisateur.

 

Un moteur sous interruption

Si on a un seul moteur, on peut utiliser une procédure d'interruption que l'on met en place pour démarrer le déplacement, qui va gérer l'avance des pas avec l'interruption et qui sera arrêtée quand les pas seront faits. Une fois la procédure d'interruption mise en place, le moteur va avancer et on peut pendant ce temps exécuter une tâche quelconque (même bloquante). L'utilisateur n'a pas besoin de se préoccuper du moteur une fois qu'il a donné l'ordre d'avancer. On peut avoir par exemple un programme principal (loop) qui donne le chronogramme:

Demande d'avancer de 10 pas Autre tâche, par exemple affichage, calcul.... Demande d'avancer de 100 pas Autre tâche, par exemple affichage, calcul....       ...      
Pendant ce temps sur la broche Step:
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ...

Le code est divisé en deux. D'un côté il y a le code pour l'interruption, de l'autre le code utilisateur. Ces codes sont complètement indépendant. Le code sous interruption qui gère l'avance du moteur est prioritaire et va provisoirement suspendre le code utilisateur à chaque appel. Ce dernier peut être bloquant, ce n'est pas important. L'écriture du code utilisateur est donc simple et si le moteur doit tourner pendant une seconde, c'est comme si on avait une seconde (moins le temps des appels d'interruption) pour l'utilisateur.

Bien entendu, si on lance des ordres de déplacements plus rapidement que le moteur n'est capable de le faire, une boucle d'attente (bloquante) dans la demande d'avancement doit être mise en place pour attendre.

Le gros avantage de cette méthode est que les impulsions sur Step sont relativement régulières car elle sont synchrones avec l'Horloge qui gère l'interruption. Il faut quand même finir l'instruction en cours, qui peut durer 1, 2 ou 3 cycles d'horloge, et il peut y avoir une erreur de 0,125µs sur la position de Step. Mais si une impulsion arrive un peu trop tard, la suivante va rattraper. Il en est de même pendant la mise à jour de l'horloge système (delay, micros...); une impulsion sera retardée de 8µs (temps que j'ai estimé), mais la suivante étant à sa place, elle aura rattrapé le retard.

Si on travaille en 8 micro-steps minimum (ce que je conseille pour d'autres raisons), on peut sans problème avoir une erreur sur une impulsion.

Avantages:
    - Le code utilisateur est très facile à écrire et à comprendre
    - Les impulsions sur Step sont quasi régulières et le temps programmé est le temps réel
    - Pendant que le moteur tourne, on peut faire autre chose
Inconvénients:
    - Pour écrire la fonction d'interruption, il faut savoir gérer les timers
    - On a besoin d'un timer
    - Le code de la bibliothèque est plus complexe à comprendre (mais l'utilisateur s'en moque)
    - Entre deux déplacements il y a un toujours temps d'arrêt (la demande d'avancer)
Conclusion:
C'est presque parfait.

 

Plusieurs moteurs avec plusieurs timers

La méthode précédente permettait avec un timer de faire tourner un moteur. Si on dispose de plusieurs timers, un par moteur, on peut faire tourner plusieurs moteurs. Bien entendu, il est impossible d'avoir en même temps deux impulsions Step car ce sont deux fonctions différentes qui ne peuvent s'exécuter en même temps. Cela peut décaler une impulsion, mais comme précédemment, les impulsions suivantes rattraperont le retard. En mode 8 micro-steps et plus, ce n'est pas problématique.

Avantages:
    - Le code utilisateur est très facile à écrire et à comprendre
    - Les impulsions sur Step sont quasi régulières et le temps programmé est le temps réel
    - Pendant que le moteur tourne, on peut faire autre chose
    - On peut faire tourner plusieurs moteurs
Inconvénients:
    - Il vaut mieux être en mode 8 micro-steps minimum
    - Il faut autant de timers disponibles que de moteurs (on ne peut faire tourner que 3 moteurs avec une Uno, 5 avec une Mega)
    - Pour écrire la fonction d'interruption, il faut savoir gérer les timers
    - Le code de la bibliothèque est plus complexe à comprendre (mais l'utilisateur s'en moque)
    - Entre deux déplacements il y a un toujours temps d'arrêt (la demande d'avancer)
Conclusion:
C'est presque parfait.

La bibliothèque QuickStep quand on a que deux vecteurs dans la table fonctionne comme cette méthode.

 

Plusieurs moteurs avec un seul timer, prochaine impulsion

Si on veut gérer plusieurs moteurs avec un seul timer, c'est possible. On peut faire alors comme on le faisait avec une attente par while: une fois une impulsion Step pour un des moteur faite, on calcule le temps auquel la prochaine impulsion Step doit se faire, et on programme le timer en conséquence. La gestion est un peu compliquée; il faut voir avec chaque moteur quand la prochaine impulsion de Step interviendra et à ce moment voir quel est ou quel sont les moteurs qui doivent voir une impulsion Step.

Pour que cela fonctionne, il faut que la période d'horloge soit plus grande que le temps de la fonction d'interruption, sinon certaines interruptions ne seront pas exécutées. Cela limite la résolution des pas. Si la fonction d'interruption dure 6µs, on aura une résolution maximale de 6µs.

Avantages:
    - On peut utiliser plusieurs moteurs avec un seul timer (la Uno n'a qu'un timer 16 bits libre)
    - Les impulsions sur Step sont quasi régulières et le temps programmé est le temps réel
    - Pendant que les moteurs tournent, on peut faire autre chose
Inconvénients:
    - La fonction d'interruption est complexe
    - La résolution de l'horloge est mauvaise
    - Plus on a de moteurs plus la résolution est mauvaise
    - Entre deux déplacements il y a un toujours temps d'arrêt (la demande d'avancer)
Conclusion:
Intéressant si on a beaucoup de moteurs.

 

Plusieurs moteurs avec un timer, impulsion ou pas

Dans un premier temps, je vais m'intéresser à un seul moteur.

Si on utilise le mode 8 micro-pas ou plus, on peut se permettre d'avoir une erreur d'un micro-pas (en fait +/- un demi micro-pas). On va donc envoyer au mieux des impulsions avec une horloge qui n'est pas à priori idéale. On travaille toujours par interruption et on va supposer que l'horloge est à 20µs. Ceci veut dire que l'on peut ou non avoir une impulsion sur Step toutes les 20µs. On désirerait avoir des pas espacées de 25µs.

La première impulsion aura lieu à t=0µs.
Idéalement la deuxième devrait avoir lieu à t=25µs, mais on ne peut la faire qu'à t=20µs ou t=40µs. On va donc la faire à t=20µs
Idéalement la troisième sera à 50µs, on la fera à t=40µs (on aurait pu la mettre à t=60µs)
Idéalement la quatrième impulsion devrait être à t=75µs, on choisira t=80µs qui est plus près
La cinquième impulsion sera à t=100µs, c'est parfait
Et ainsi de suite. Cela donne:

Pour que cela fonctionne, il faut que l'horloge ait une période plus petite que la période souhaitée.

Si je veux maintenant gérer plusieurs moteurs avec des périodes pour les signaux Step différentes, je peux alors choisir une horloge qui ait une période plus petite que toutes les périodes souhaitées des signaux Step souhaités. Plus la période de l'horloge est petite, plus je vais avoir d'appel à la fonction d'interruption et plus je vais passer de temps. L'idéal est donc de prendre pour l'horloge la plus petite des périodes des Step souhaités. Ainsi, le moteur qui tournera le plus vite aura des impulsion sur Step régulières, et il en manquera pour les autres.

Avantages:
    - On peut utiliser plusieurs moteurs avec un seul timer (la Uno n'a qu'un timer 16 bits libre)
    - Les impulsions sur Step sont en moyenne régulières et le temps programmé est le temps réel
    - Pendant que les moteurs tournent, on peut faire autre chose
Inconvénients:
    - Il y a une irrégularité dans les impulsions Step d'un demi micro-pas
    - La fonction d'interruption est encore plus complexe
    - Entre deux déplacements il y a un toujours temps d'arrêt (la demande d'avancer)
Conclusion:
Intéressant si on a beaucoup de moteurs ou peu de timers.

La bibliothèque QuickStep quand on utilise deux moteurs associés s'inspire fortement de cette méthode.

 

Table de déplacements

Jusqu'à présent quand un avait fini un déplacement et que l'on voulait un nouveau déplacement, il y avait un petit temps mort entre les deux ne serait ce par l'appel à la fonction qui les demande. Tant que les vitesses de rotations sont faibles, ce temps ne se voit pas trop. Mais si les vitesses deviennent intéressantes, et que l'on veut une rotation continue (nombre de pas très important, supérieur à ce qu'un seul ordre peut donner) on obtient une rotation saccadée. En sur-vitesse, ce temps mort entre deux déplacements provoque l'arrêt du moteur. Pour pouvoir enchaîner deux ordres de mouvement sans arrêt entre les mouvements, il est nécessaire de donner le deuxième ordre avant que le premier ne soit fini.

La solution consiste à mettre le déplacement dans une pile FIFO (first in first out). Au minimum la pile doit contenir deux déplacements: le déplacement en cours et le déplacement futur. A chacun sa réalisation, pour deux déplacements, on peut ne mémoriser pour le déplacement actuel que le nombre de pas restant à faire et le déplacement futur être dans un espace physique fixe... Avec une pile importante, on peut mémoriser plusieurs ordres et ainsi cumuler les temps pour faire une tâche assez longue.

Avantages:
    - Les mouvements sont enchaînés sans temps morts entre deux déplacements
    - Si on a une pile importante on peut avoir beaucoup de temps libre
Inconvénients:
    - Cela complique la bibliothèque
    - Cela complique éventuellement le programme utilisateur
    - La pile peut prendre beaucoup de place en mémoire
Conclusion:
C'est une bonne façon d'enchaîner des rotations successives.

La bibliothèque QuickStep utilise une pile contenant 1 à 255 valeurs.

 

 


dansetrad.fr Contactez-moi