bibliothèque ArpStepper pour moteur pas à pas à tester

Bonjour à tous,
Je suis en train d'écrire une bibliothèque pour asservir un ou plusieurs moteurs pas à pas, en position et en vitesse.
Si vous souhaitez la tester vos retours, questions, conseils, idées seront les bienvenus.

arpschuino_objet_stepper014exemple.zip (24.7 KB)

Bonjour Jacques
Je vous remercie de votre réponse et je vais essayer de comprendre le fonctionnement de votre programme et peut être l'adapter pour mon projets qui est simple en programmation mais pas pour moi :slight_smile:
car en fin de compte c'est exactement cela qu'il me faut

Je vous souhaite, bonne continuation et bien sur je vous donnerais des nouvelles. Patrick

Ton code m'intéresse surtout pour le calcul de la vitesse. Il a le gros avantage d'être lisible.

Je suis aussi en train de faire la même chose, avec des objectifs légèrement différents. Je vais passer par les interruptions pour pouvoir utiliser au mieux le temps qu'il me reste. Je me pose aussi la question de l’accélération.

Quelques remarques note bien que ce qui est positif, ne fera pas l'objet de remarques et que si je fais des remarques c'est parce que je suis allé plus loin qu'un simple regard.


Je n'ai pas compris le paramètre resolution


calcul_nbr_steps() retourne un float alors que c'est un uint_16 qui contient la valeur à retourner. Autant mettre void, et supprimer le return, de toute façon le float retourné n'est pas utilisé.


  bool sens;
  if (m_nbr_pas>0)                  sens=LOW + m_reversed;
  else                              sens=HIGH - m_reversed;
  //bool sens =(!m_nbr_pas)+ m_reversed;???????

Pour la faire en une fois, il faut utiliser le ou exclusif:
bool sens =(m_nbr_pas>0) ^ m_reversed;
MAIS! le calcul prend alors 13 cycles d'horloge u lieu de 11 (avec l'optimisation par défaut)

  m_change_sens=(sens != m_previous_sens); //equivalent plus rapide a :
//  if (sens != m_previous_sens)      m_change_sens = 1;
//  else                              m_change_sens = 0;

Celui ci il est plus drôle: J'ai mesuré
10 cycles d'horloge pour m_change_sens=(sens != m_previous_sens);
9 cycles d'horloges pour if (sens != m_previous_sens) m_change_sens = 1; else m_change_sens = 0;
Pour moins cher c'est plus simple à comprendre...


Ca se complique vraiment avec les calculs de vitesses... Je ne suis pas sûr qu'une erreur de 1 au maximum soit problématique.


m_nbr_pas(0),//int16_t suffisant si commande en 16 bits ?

Chacun son truc, mon papier avance de 4cm par tour de moteur (200 pas) et je préfère peut être le 1/16 pas, je pense que cela devrait donner moins de vibrations. Il faudra que je fasse l'essai. Avec un uint_16 je peux avancer de 65535/800= 80cm. C'est suffisant pour beaucoup, mais mon papier fait 17 m de long. C'est un cas particulier, pour moi la solution est de travailler en relatif uniquement, ce que propose certaines bibliothèques. Passer par des long complique et ralenti (surtrout avec l'équivalent de map)


Je n'ai pas bien compris l'utilité de init. Pourquoi ne pas l'avoir intégré au constructeur?

Bonjour à tous,

oui donne nous des nouvelles Patrick et n’hésite pas à poser des questions.

Merci Vileroi pour tes remarques constructives.

La résolution : Il s’agit de la résolution de la commande, dans l’exemple avec le potentiomètre c’est 1023 ou 10 bits, pour une commande en midi ce serait 7 bits, en DMX 8 bits, 16 bit avec 2 canaux DMX, voir 1 bit avec un bouton poussoir . La résolution peut être n’importe quelle valeur comprise entre 1 et 1023. On peut aussi écrire _7bits,
_8bits, _10bits, _12bits, _16bits.

calcul_nbr_steps() retourne un float . Bien vu, c’est une erreur ! Je corrige en laissant pour l’instant le return, au cas ou.
Penses tu que la vonction void s’exécute plus rapidement ?

Si je te suis bien, pour la rapidité, il vaux mieux utiliser :

 bool sens;
  if (m_nbr_pas>0)                  sens=LOW + m_reversed;
  else                              sens=HIGH - m_reversed;

et :

 if (sens != m_previous_sens)      m_change_sens = 1;
  else                              m_change_sens = 0;

Mais dis moi, comment fais tu pour mesurer ça, ça m’intéresse !

Le map est terriblement long (oui tout est relatif!) : 60us. Une partie des calculs à exécuter peut se faire dans le setup, pourquoi les refaire à chaque foi ? On gagne environ 30us.
Dans le calcul restant, je n’ai pas mesuré de différence, en terme de vitesse, entre le calcul en floats et le calcul en integer. Dans ce cas autant prendre le plus précis, même si en effet une erreur de 1 n’est pas problématique.

Je répond aux deux dernières questions tout à l’heure, dans un prochain message (obligation familliale!)

vileroi:
Je n'ai pas bien compris l'utilité de init. Pourquoi ne pas l'avoir intégré au constructeur?

En fait il faudrait modifier les 2 :slight_smile:

Le code actuel d'init() et ses paramètres sont éventuellement utilisables au moment de l'appel du constructeur et vous pourriez les déplacer.

Par contre le code du constructeur

 {
    pinMode (m_pull, OUTPUT);
    pinMode (m_dir, OUTPUT);      
}

devrait aller dans init() car au moment de l'appel du constructeur vous n'êtes pas garanti par le compilateur que l'environnement Arduino ne viendra pas mettre le bazar ensuite sur vos pins et les repasser en INPUT.

=> Le conseil est donc d'avoir une fonction init() ou begin() qui est appelée à partir de setup(), après la création de l'objet. A ce moment là vous êtes sûr que le matériel est dans un état nominal connu.

Petit commentaire sur la forme (pas stratégique mais ça a son importance), vous mélangez le français et l'anglais dans le noms de variables (eg m_previous_pas, sens/reversed, calcul_step_duration, ...), vous devriez utiliser l'anglais partout (y compris les commentaires) si vous voulez toucher un public plus large, sinon mettez tout en français

Aussi sur la cohérence de type, quand vous avez un booléen, retournez true ou false, pas 0 ou 1. je sais que ça finit pareil au bout du compte, mais d'un point de vue conceptuel bool est un vrai type de vérité logique, pas un nombre.

De retour !

Dans le constructeur :

ArpStepper stepper0(2,3,200);

on déclare un moteur à 200 pas.
Tu as besoin de faire 425 tours (1700cm/4). Donc dans le setup on fait :

stepper0.init(5, 200, 425);
stepper0.position_resolution(_16bits);

Avec :

stepper0.perform(65535, 40000);

les 17m devraient bien être parcouru. Avec une précision de 0,26mm (17 000/65 535), je ne sais pas si c’est suffisant.
Sinon, stepper0.continuous renvoie 1 à chaque pas, on peut donc compter les pas effectués.

Pourquoi init ?
Dans le constructeur c’est la configuration hardware, le nombre de pas du moteur, les sorties auxquelle il est relié.
Dans init on définit ce qu’on va lui demander, vitesse, nombre de tour.
Dans l’exemple joins, il est appellé 2 fois : entre les deux il y a une procédure d’auto calibration du nombre de tour, avec 2 fin de course . Ca se passe dans le setup. Dans cet exemple le moteur est commandé en DMX mais peu importe.
Je viens de voir la répons de JML, j'y répond après avoir réfléchis !

testFDC003.ino (2.79 KB)

une autre suggestion pendant que vous réfléchissez :slight_smile:

dans do_step() commencez par tester si m_nbr_pas est nul ça ira plus vite que d'enchaîner les 2 tests positif et négatif et pas besoin des else puisque la condition fait un return. Si on n'a aucun pas à faire, je suppose qu'on n'a pas non plus à tester le changement de sens, donc on peut le mettre après

int8_t ArpStepper::do_step(uint16_t val_speed)
{

   if(m_nbr_pas == 0) return 0; 
   set_direction();

   if(m_nbr_pas>0) {
        m_previous_pas += step_plus(val_speed);
        return 1;              
   }
      
  // on sait qu'on est négatif
  m_previous_pas -= step_plus(val_speed);
  return -1;
}

cela dit comme set_direction() va à nouveau tester si m_nbr_pas est positif ou pas pour éviter un appel de fonction et la duplication de test, j'intègrerais "à la main" la gestion de la direction à la fonction do_step().

il y a plusieurs fonctions qui ne sont utilisées qu'une seule fois (calcul_nbr_steps()) ça vaudrait peut-être le coup de les déclarer au moins inline et éviter de retourner des valeurs qui ne sont pas utilisées.

OK je vais faire des corrections et vous envoie le résultat.

Je suis en train de raisonner dans un autre sens pour faire avancer le moteur pas à pas. Si je travaille en 16ème de pas, l'ai une résolution de 80 pas par mm. Et c'est à partir de là que je fais le reste. Le nombre de tours du moteur est un intermédiaire qui ne m'intéresse pas. J'ai d'un côté des impulsions à mettre, de l'autre des mm. Mon résultat c'est plutôt les mm.
Pour moi, le nombre de tours est intéressant quand on met le moteur sur la table, nu, avec un bout de scotch pour indiquer le repère, et que l'on fait des essais pour voir si il décroche ou pas.
Du coup, si je parle en mm, je n'ai plus de problèmes de résolution, je suis toujours au maximum de résolution. Et quand j'ai parcouru 17m les déplacements relatifs à la fin sont toujours avec la même précision.


Pour la mesure du temps et du nombre de cycles je fais comme tout le monde, on lit micros, on fait des boucles, on relit micros et on fait la différence divisé par le nombre de boucles. Je me suis fait un petit programme qui me permet facilement une fois pour toutes de mettre tout e place.

Dans ce que j'utilise, il y a deux boucles, la première est la tare et ne comporte qu'un NOP. la deuxième comporte un NOP et le code à mesurer. Ainsi en faisant la différence je n'ai que le temps de ce que je cherche à mesurer.

La résolution de micros est de 4µs avec une Uno, il faut au moins faire 4000 boucles pour avoir une résolution de 1ns (un nop dure 62,5 ns). Je pourrais faire 8000 boucles et afficher le nombre juste, mais cela n'a pas une grossi importance. Cela compliquerait un peu l'affichage.

Le chronométrage est interrompu par le timer 0 qui s'occupe de micros. En faisant plus de boucles la division va éliminer le petit temps dû au rafraîchissement de micros et millis. J'ai donc choisi de faire 100000 boucles. Pour mesurer un NOP, j'utilise ce nombre et j'ai le temps affiché à une ns près. Si c'est pour une fonction qui dure plus longtemps, je divise par 1000 le nombre de boucles et pense que les ns sont en réalité des ms... Si c'est très long, je ne fait qu'une boucle et j'ai le résultat en 1/10s. Ça marche comme cela avec un peu de calcul mental, je ne me suis pas fatigué plus.

Dans les tests,on utilise des 8, des 16 bits. j'en déclare quelques uns au début, une fois pour toutes.
Pour chaque variable, je lui affecte une valeur inconnue du compilateur pour qu'il ne transforme pas ma variable en constante, et je l'affiche en fin de programme pour qu'il ne me la supprime pas. le test est toujours faux, mais le compilateur ne le sait pas (encore).

Voici mon code:

byte b, b2, b3;
word w, w2;
float f;

void setup()
{
  randomSeed(analogRead(0));
  b = random(2);
  b2 = random(2);
  b3 = random(2);
  w = random(0x10000);
  w2 = random(0x10000);
  f = random(0x10000);



  Serial.begin(115200);
  unsigned long t1, t2, t3; // Chronomètres
  t1 = micros();
  for (unsigned long x = 100000ul; x > 0; x--) _NOP(); // Tarer le temps d'une boucle
  t2 = micros();
  for (unsigned long x = 100000ul; x > 0; x--) // Boucle de mesure
  {
    _NOP();


    // Mettre ici le code à tester





  }
  t3 = micros();
  t1 = t3 + t1 - 2 * t2;
  Serial.println();
  Serial.println(String(t1 / 100ul) + "ns");
  Serial.println(String(t1 / 6250ul) + "cycle(s)\n");
  if (t1 > 1000000000l)
  {
    Serial.print(b);
    Serial.print(b2);
    Serial.print(b3);
    Serial.print(w);
    Serial.print(w2);
    Serial.print(f);
  }
}

void loop()
{
}

Voici les fichiers avec les corrections suivantes :

  • int16_t calcul_nbr_steps a la place de float calcul_nbr_steps. un clean final la rendra surement void, je laisse un return pour l'instant
  • end_of_pulse() supprimé
  • calcul de m_change_sens : retour à la méthode classique
  • ajout de la fonction set_wait() (sur les conseils d'un ami), m_wait en millisecondes avec 20ms par defaut
  • initialisation des pins en sortie déplacé dans init()
  • m_nbr_pas remplace par m_nbr_steps
  • m_previous_pas remplace par m_previous_steps
  • m_previous_sens remplace par m_previous_direction
  • m_change_sens remplace par m_change_direction
  • true et false pour les booléens
  • commentaires en anglais
    Une petite question, je débute avec la programmation orienté objet : faut-il un destructeur ?

ArpStepper.cpp (7.81 KB)

ArpStepper.h (3.09 KB)

Une petite question, je débute avec la programmation orienté objet : faut-il un destructeur ?

le destructeur ne sert que si vous avez fait de l'allocation dynamique de mémoire, pris le contrôle d'une resource matérielle, si vous avez des variables de classe qui dépendent du nombre d'instances, etc.. --> dans ce cas vous libérez ces resources dans le destructeur.

  • manque une petite adaptation linguistique sur calcul_nbr_steps

  • continuous, step_plus ne retournent pas un vrai bool (true ou false)

  • do_step n'a pas changé

vous pouvez peut être éviter la passage en calcul flottant pour step_duration dans calcul_step_duration en faisant juste

return 600000000UL / (RPM * step_per_revolution)

Je vais tenir compte de tout ce que vous me dites et y répondre.
Mais je bloque pour l'instant sur une chose que j'aimerais résoudre avant :
Dans la fonction step_plus :
la condition : if(m_change_direction==true) est toujours vrais.
Pour le mettre en évidence j'ai fait : m_wait*1000;
c'est sans doute un truc tout bête mais là je m'arrache les cheveux !
Je joins la derniere version (016)

ArpStepper.cpp (9.41 KB)

ArpStepper.h (3.12 KB)

Au fait, 17m ce ne serait pas la longueur d'un carton d'orgue de barbarie par hasard ? :slight_smile:

Au fait, 17m ce ne serait pas la longueur d'un carton d'orgue de barbarie par hasard ?

Damned je suis fait. Effectivement, le papier d'origine (rouleau de caisse enregistreuse) fait 50 m.

Je pense aussi à la prise d'origine: faire tourner le moteur jusqu'à un capteur de fin de course par exemple.

Autre point d'intérêt: mesurer la vitesse maximale des moteurs ce qui permet un rembobinage.

Regarde un peu plus haut, j'ai posté un exemple avec des fins de courses (testFDC003.ino).
Mesurer la vitesse max, ça serait bien mais comment savoir quand le moteur décroche ?
Sinon je bloque toujours...

Mesurer la vitesse max, ça serait bien mais comment savoir quand le moteur décroche ?

Je crois que je viens de trouver:

  • on tourne jusqu'au fin de course "position 0".
  • vitesse initiale V faible
  • on répète
  • avancer de 1000 pas à la vitesse V
  • reculer à la vitesse V/2 jusqu'au fin de course
  • si on a reculé de 100 pas augmenter la vitesse V de 50%
    sinon sortir de la boucle et garder la vitesse V d'avant ou par sécurité la vitesse V/2

bouault:
Mais je bloque pour l'instant sur une chose que j'aimerais résoudre avant :
Dans la fonction step_plus :
la condition : if(m_change_direction==true) est toujours vrais.

Notez qu'on n'écrit pas le == true quand la variable est un booléen, c'est déjà une valeur de vérité en elle même.

si vous regardez d'où vient votre direction, vous avez

    bool Direction;
    if (val < (m_resolution / 2) + 1)
      Direction = HIGH + m_reversed; //ou val>(m_resolution/2)
    else
      Direction = LOW - m_reversed;
    m_change_direction = (Direction != m_previous_direction);

Direction est déclaré comme une valeur de vérité mais vous faites des opérations mathématiques avec du contenu booléen, ce n'est pas très propre.

Par exemple:

m_reversed va être vrai ou faux, c'est un booléen représenté en mémoire par 0 ou 1.
HIGH est un #define qui vaut 1.

Donc quand vous faites Direction = HIGH + m_reversed vous dites donc Direction = 1+1 ou Direction = 1+0 --> 2 ou 1 vont être ensuite transformés par le compilateur en true puisque la variable destination est booléenne => donc Direction est toujours vrai (ce qui ne porte aucun sens conceptuellement une direction n'est pas vraie ou fausse).

si vous êtes dans le else vous allez évaluer Direction = LOW - m_reversed soit (0 - 0) ou (0 - 1). Dans le premier cas si m_reversed était faux vous restez sur faux, si m_reversed était vrai vous obtenez -1 qui est non nul et donc le compilateur transforme ça en vrai. donc Direction prend toujours la valeur de m_reversed.

votre code revient donc à dire

    bool Direction;
    if (val < (m_resolution / 2) + 1) Direction = true;
    else Direction = m_reversed;
    m_change_direction = (Direction != m_previous_direction);

qui se simplifie en Direction = m_reversed | (val < (m_resolution / 2) + 1);

tout cela me gène bien conceptuellement car en plus vous utilisez un booléen pour une valeur de tension (concept HIGH ou LOW) dansdigitalWriteFast(m_dir,Direction); et je ne suis pas sûr de comprendre la logique derrière ce calcul de changement de direction.

Pouvez vous exprimer en français l'idée qui déciderait si on change de direction?

Si le nombre de pas qu'il reste à faire est positif : on va en avant
sinon si le nombre de pas qu'il reste à faire est négatif : on va en arrière
sinon on reste dans la même direction

si la direction prise est la même que la précédente : il n'y a pas de changement de direction
sinon : il y a changement de direction

(en cas de changement de direction, on fera une mini pause avant de repartir dans l'autre sens)

Je me concentre pour l'instant sur la fonction set_direction()

Essayons de faire de la logique...

  bool Direction;
  if (m_nbr_steps>0)                        Direction=LOW ^ m_reversed;
  else if  (m_nbr_steps<0)                  Direction=HIGH ^ m_reversed;
  else                                      Direction= m_previous_direction;
  m_change_direction = Direction ^ m_previous_direction;
  m_previous_direction = Direction;

Sinon pour Vileroi, ci joint un code avec l’approche en mm

arpschuino_objet_stepper_relatif.ino (742 Bytes)

Vous pilotez la direction par une PIN    uint8_t m_dir;//the output to be connected to the DIR of the driverCette PIN est HIGH pour aller dans un sens, LOW pour aller dans l'autre.

Quand vous dites

Si le nombre de pas qu'il reste à faire est positif : on va en avant
sinon si le nombre de pas qu'il reste à faire est négatif : on va en arrière
sinon on reste dans la même direction

c'est direct - donc pour moi

  if (m_nbr_steps>0) Direction=HIGH;
  else if  (m_nbr_steps<0) Direction=LOW;
  else Direction= m_previous_direction;

ensuite on change de direction si m_change_direction = (Direction != m_previous_direction);

Sinon pour moi Direction et m_previous_direction ne sont pas bien nommées ni typées. ce devrait être des uint8_t et ils devraient stocké la valeur HIGH ou LOW affectée à la pin m_dir

j'utilserais:

uint8_t m_dir_state; 
uint8_t m_dir_new_state;