Asservissement numérique

Bonjour,

Je souhaiterais mettre en place un asservissement numérique avec mon Arduino Uno.
Pour mettre en place un asservissement numérique, il faut que les calculs des commandes se fassent à intervalle de temps régulier.

Question : puis-je simplement appeler ma fonction d'asservissement au sein de ma fonction loop() ou dois-je impérativement mettre en place un système de timer (avec la librairie du même nom par ex) ?

J'ai tenté une implémentation à base de loop(), voici mon algo :
loop ()
{
1 - Lecture des consignes sur le port Serie
2 - Lecture des capteurs
3 - Calcul des erreurs
4 - Calcul des commandes (fonction d'asservissement)
5- Application des commandes
}

Merci et bonne journée :slight_smile:

hello
si tu veux un "asservissement" régulier, passes par une fonction appelée par un timer
la fonction durant au maxi ( par exemple 800ms), le timer l'appelera toutes les secondes.

si tu passes avec la loop, l'asservissement dépendra du temps qu'elle mettra à "boucler" et ce temps dépendra de la longueur des calculs qui peuvent etre différents à chaque passage.

Bonjour

Pas tout à fait d'accord.

Un déclenchement à fréquence fixe peut très bien être géré simplement avec millis() dans la fonction loop(), même si celle-ci n'a pas une durée d'exécution constante.

Pour reprendre l'exemple donné :

void loop()
  static unsigned long ref_millis = 0; //valeur initiale à positionner selon souhait de première exécution

  if (millis() - ref_millis >= 1000)
  {
    ref_millis = ref_millis + 1000; //et non ref_millis = millis();
    ...
    (reste du traitement dont la durée peut varier)
    ...
  }
}

oui, d'accord avec toi
on ne va pas jouer sur les mots
il faut un "timer " ou système de gestion du temps.
je voulais dire qu'une boucle loop qui tourne seule ne sera pas régulière

mais que tu passes par un timer ou milli, si les calculs sont + longs que le temps timer ou milli
le résultat ne sera pas régulier.

Je vois, avec la fonction loop, on ne contrôle pas la fréquence d'exécution de la fonction d'asservissement.

Du coup, un algo comme celui-ci serait-il plus convenable ? :

loop()
{
1 - Lecture des consignes : mise à jour de variables globales
2 - Lectures de capteurs : mise à jour de variables globales
3 - Calcul des erreurs : mise à jour de variables globales
}

Toutes x secondes : asservissement();

non : on peut contrôler à peu près tout dans la fonction loop(), faut juste savoir ce qu'on veut faire

  1. Lecture des consignes
    J'imagine que cela consiste à écouter en permanence le port série, et si de nouvelles consignes arrivent, les lire, vérifier leur validité et les mémoriser.
    NB : si tu veux que tes consignes survivent à un arrêt / relance, tu peux les sauvegarder dans l'eeprom.

  2. Lecture des capteurs
    Est-ce que tu as une contrainte de fréquence de lecture à ne pas dépasser ?
    Si non, tu peux les lire aussi souvent que possible

  3. Calcul de l'écart consigne / capteurs, et de la commande rectificatrice associée
    Ca c'est du logiciel pur, en général très rapide même sur aduino, sauf cas d'algo de calculs hyper complexes.
    A exécuter aussi souvent que possible, ou a minima après chaque étape 1) et après chaque étape 2)

  4. Asservissement = envoi de commande
    Est-ce que tu as une contrainte de fréquence de commande à ne pas dépasser ?

Si ton système de capteur/commande n'a aucune contrainte de fréquence, quelle serait la raison de vouloir espacer les asservissements de x secondes ?

De toute manière l'arduino tourne en permanence.
Pourquoi lui faire passer l'essentiel de son temps à attendre l'expiration d'un timer, quand on peut avoir une meilleure réactivité de l'asservissement ?

@bricoleau C'est aussi ce que je me disais, pourquoi s'emmerder avec un timer, autant calculer le plus de fois possible et donc tout placer la fonction loop(), ça permet de gagner en réactivité. Le truc c'est que je me pose la question de la régularité de l'exécution. La durée d'un tour de ma fonction loop() ne va pas toujours être la même suivant les conditions dans lesquelles le code est passé, suivant qu'il y avait des données à lire dans la FIFO du capteur...etc Cette irrégularité peut-elle entraîner un dysfonctionnement de mon asservissement ou altérer son temps de réponse ?

Bonjour

A mon avis tu théorises trop, ce qui t'amène à te poser des questions qui n'ont peut-être pas lieu d'être.

Au final, tu auras peut-être une boucle loop() qui s'exécute plusieurs milliers de fois par seconde (ça pédale un nono !).
Si au milieu il y a une irrégularité "énorme", avec une fonction loop() qui dure 10 millisecondes, ton asservissement ne la verra pas passer car il ne fonctionne pas à la même échelle de temps.

Mon conseil : expérimente et mesure le temps
Cela te permettra de savoir de quoi il retourne exactement

Par exemple, voici le genre de code qui te permettra d'appréhender la durée d'exécution de ton loop()

void loop()
{
  traceLoop(1);

  ...
  
  traceLoop(0);
}

void traceLoop(byte debut)
{
  const  unsigned long intervalle_stat = 10000; // toutes les 10 secondes
  static unsigned long nb_loop = 0;
  static unsigned long max_loop;
  static unsigned long millis_stats;
  static unsigned long millis_debut;
  unsigned long maintenant, duree_loop, duree_stat;
  
  maintenant = millis();
  if (debut)
  {
    millis_debut = maintenant;
    if (nb_loop == 0)
    {
      max_loop = 0;
      millis_stat = maintenant;
    }
  }
  else
  {
    nb_loop++;
    duree_loop = maintenant - millis_debut;
    if (duree_loop > max_loop)
    {
      max_loop = duree_loop;
    }
    duree_stat = maintenant - millis_stat;
    if (duree_stat >= intervalle_stat)
    {
      Serial.print("stat loop sur ");
      Serial.print(duree_stat);
      Serial.println(" ms");
      Serial.print("nb exec=");
      Serial.println(nb_loop);
      Serial.print("duree moy=");
      Serial.print(duree_stat / nb_loop);
      Serial.println(" ms");
      Serial.print("duree max=");
      Serial.print(max_loop);
      Serial.println(" ms");
      nb_loop = 0;
    }
  }
}

lobodol:
Je souhaiterais mettre en place un asservissement numérique avec mon Arduino Uno.
Pour mettre en place un asservissement numérique, il faut que les calculs des commandes se fassent à intervalle de temps régulier.
.........
........
autant calculer le plus de fois possible et donc tout placer la fonction loop(), ça permet de gagner en réactivité

bricoleau:
A mon avis tu théorises trop, ce qui t'amène à te poser des questions qui n'ont peut-être pas lieu d'être.

Je pense plutôt que le projet n'a pas été assez théorisé.
On se fiche de chercher à tout prix la réactivité la plus rapide. Tout dépend de la réactivité qu'il faut pour le projet en cours et ça c'est le plus dur à définir. Le mauvais réflexe quand on ne sait pas est de vouloir la réactivité max en se disant que cela devrait aller.

Chercher le max de réactivité à tout prix n'est pas une assurance de réussite : si tu veux contrôler un objet qui fonctionne à des GigaHertz tu aura beau faire la boucle la plus rapide un atmega sera à la ramasse. Par contre c'est le meilleur moyen pour faire une usine à gaz inutile.

Il faut se poser les bonnes questions.
Préalable : si ce n'est pas déjà fait consulter un peu de doc théorique sur les asservissement : erreur statique, erreur de traînage, temps de réaction, "over-shoot", "undershoot", régulation PID, etc....
Première chose à définir : la nature des choses à asservir. Je ne suis pas un spécialiste en asservissement mais il me parait évident qu'asservir de l'électronique rapide ou asservir un montage mécanique ne conduira pas aux mêmes solutions.

Donc ..............

lol

En fait nos propos ne me semblent pas aussi contradictoires qu'il n'y paraît

OUI il est indispensable de commencer par poser les contraintes du système, dont on ne sait d'ailleurs rien jusqu'à présent :

  • contraintes liées aux capteurs (= grosso modo durée d'acquisition + fréquence max de sollicitation + marge d'erreur)
  • contraintes liées au système asservi (=grosso modo fréquence max de commande + courbe d'écart à ne pas dépasser entre deux commandes successives)

NON il n'est pas nécessaire de commencer par se poser 150 questions sur comment ralentir l'arduino, sans même savoir à quelle vitesse il tourne, ni même si cette vitesse est trop faible / trop grande par rapport aux contraintes listées ci-dessus.

Je maintiens qu'il vaut mieux que l'arduino passe son temps à faire (et refaire) des mesures et calculs, plutôt qu'à attendre l'expiration d'un timer.
Cela ne veut pas dire que derrière il doit envoyer des commandes qui ne respectent pas les contraintes propres au système asservi (tant en fréquence qu'en variation).

Et si tu nous en disait plus sur ce que tu veux asservir ?

bricoleau:
En fait nos propos ne me semblent pas aussi contradictoires qu'il n'y paraît

Le deuxième degrés en plus ils sont très proches.

bricoleau:
Et si tu nous en disait plus sur ce que tu veux asservir ?

C'était bien le sens de :

68tjs:
Donc ..............

J'ajoute que la liste des renseignements à fournir dans l'intérêt du demandeur pour avoir des réponses rapides et efficaces est dans le message épinglé en tête de forum :
"Bienvenue sur le forum de la communauté francophone"
qui n'a apparemment pas été lu.

Bonjour,

Merci pour vos réponses.
Effectivement je vous en ai dit très peu sur le système que je souhaite asservir. Sans plus de mystère, je souhaite asservir un quadricoptère de type X.

Situation actuelle
Actuellement, j'ai réalisé deux asservissements de type P et PI (le PID sera pour quand les précédents seront OK).

Le capteur que j'utilise est un MPU6050 (gyro 3 axes + accéléro 3axes). La lecture du capteur se fait via le port série à un débit de 57600 bauds et utilise une interruption pour signaler que de nouvelles données sont présentes dans sa FIFO.

Dans un premier temps je me suis fixé comme contrainte de stabiliser mon quadri en assiette (pour le moment, peu importe s'il "glisse").
En d'autres thermes, ma consigne est angulaire. Dans un soucis de simplicité de mise en oeuvre, ma consigne de base est 0° sur chaque axe : le drone à donc une assiette "plate".

Pour tester mon implémentation, j'équipe mon quadri de 2 hélices du même axe et je fixe l'autre axe de manière à ce qu'il puisse basculer sur un seul axe : ce point vous parait clair ou un schéma vous aiderait ?

Constat
Dans le cas de mon asservissement de type proportionnel, lorsque je valorise mes gains P à 0, je constate que mon drone se stabilise seul. Si je le penche à 90° d'un côté et que je lâche, il revient tranquillement à sa position d'équilibre, en 2-3sec. Ce temps de réponse ne me convient pas car trop élevé. Je souhaiterais obtenir un temps de réponse de l'ordre de 0.5s : cela vous semble-t-il réalisable ? Surréaliste ?
Lorsque je valorise les gains P avec une valeur > 0 (0.5 par expl), le temps de réponse est nettement meilleur mais le drone part en instabilité : le dépassement augmente à chaque oscillation et finirait par tourner sur lui-même si je ne l'arrête pas.

J'ai donc tenté de diminuer petit à petit mes gains P mais je pers en réactivité par conséquent.

J'en viens donc à me m'interroger sur mon implémentation qui ne comporte pas de timer. Voici un extrait de mon code source :

Fonction principale

loop()
{
    // 1 - Lecture des consignes
    cmd = getCommands();

    // 2 - Mesure du capteur
    mesures = getMesures();

    // 3 - Calcul des erreurs
    errors = calcErrors(cmd, mesures);

    // 4 - Calcul des commandes : asservissement P
    cmdMot = asservissementP(errors, cmd_h);

    // 5 - Application des commandes calculées
    motA.write(cmdMot[0]);
    motB.write(cmdMot[1]);
    motC.write(cmdMot[2]);
    motD.write(cmdMot[3]);
}

Fonction d'asservissement

/**
 * Calcul les commandes de chacun des moteurs en fonctions des erreurs par rapport à la consigne
 * pour un quadricoptère de type X : 
 *
 * (A) (B)     x
 *   \ /     z ↑
 *    X       \|
 *   / \       +----→ y
 * (C) (D)
 * 
 * Les moteurs A et D tournent dans le sens horaire
 * Les moteurs B et C tournent dans le sens anti-horaire
 *
 * Asservissement de type Proportionnel
 *
 * @param float[3] errors : tableau des erreurs Yaw, Pitch, Roll
 * @param float cmd_h     : commande des gaz
 */
int* asservissementP(float errors[3], int cmd_h)
{
  float Kp[3] = {1.5, 1.5, 1}; // Coefficient P dans l'ordre : Yaw, Pitch, Roll
  static int commandes[4] = {0,0,0,0};

  if (cmd_h == 0) {
      return commandes;
  }

  // Initialisation des commandes moteur
  int cmd_motA = cmd_h;
  int cmd_motB = cmd_h;
  int cmd_motC = cmd_h;
  int cmd_motD = cmd_h;

  // Yaw - Lacet (Z)
  cmd_motA -= errors[0] * Kp[0];
  cmd_motD -= errors[0] * Kp[0];
  cmd_motC += errors[0] * Kp[0];
  cmd_motB += errors[0] * Kp[0];

  // Pitch - Tangage (Y)
  cmd_motA -= errors[1] * Kp[1];
  cmd_motB -= errors[1] * Kp[1];
  cmd_motC += errors[1] * Kp[1];
  cmd_motD += errors[1] * Kp[1];

  // Roll - Roulis (X)
  cmd_motA -= errors[2] * Kp[2];
  cmd_motC -= errors[2] * Kp[2];
  cmd_motB += errors[2] * Kp[2];
  cmd_motD += errors[2] * Kp[2];

  // Cas limites [0, 180]
  commandes[0] = normaliser(cmd_motA);
  commandes[1] = normaliser(cmd_motB);
  commandes[2] = normaliser(cmd_motC);
  commandes[3] = normaliser(cmd_motD);

  return commandes;
}

Que pensez-vous de mon implémentation à première vue ?

(le PID sera pour quand les précédents seront OK)

Je re-précise que je ne suis pas un spécialiste en asservissement mais je crois bien que c'est justement une PID bien réglée qu'il te faut.
Sans parler des cas complètement instables : "auto-oscillation" où l'asservissement se transforme en oscillateur ou "pompage" où l'asservissement part en butée, on distingue en première approche deux régimes d'amortisement : avec et sans sur-oscillations.
Le régime sans sur-oscillation est absolument stable mais lent, ce que tu constate.
Pour gagner en rapidité il faut accepter de faibles sur-oscillations maîtrisées et c'est là que la PID entre en jeu car elle mixte avec des coefficients ajustables des action stables, mais lentes avec des actions rapides mais "moins" stables.

Images tirées du site wikipédia

Source Wikipédia -> Tableau récapitulant l'influence d'un PID série sur le système qu'il corrige si l'on augmente séparément l'action proportionnelle (P), intégrale (I) ou dérivée (D).

Merci pour le tableau récapitulatif, c'est toujours bien de l'avoir sous la main pour ne pas oublier l'influence de chaque paramètre :slight_smile:

Je suis d'accord avec toi sur le fait que c'est le PID complet qui permettra d'avoir la meilleure stabilité.
Je me rend compte que je n'ai pas suffisamment expliqué mon protocole expérimental ni son but.

Ce que je cherche à faire, c'est déterminer les coefficients P, I et D me permettant d'avoir un vol à la fois réactif et stable.
Pour se faire, je procède étape par étape.

Etape 1
Déterminer expérimentalement le coefficient P permettant d'avoir un temps de montée convenable (entre 0.5 et 1s) sans provoquer "d'auto-oscillation".
A ce stade, j'ai donc un vol "réactif" mais peu stable (légères oscillations).

Etape 2
Une fois P calibré, déterminer expérimentalement le coefficient I permettant d'améliorer la précision sans provoquer d'auto-oscillation. A ce stade il s'agit d'un asservissement PI.

Etape 3
Une fois P et I calibrés, déterminer expérimentalement le coefficient D permettant d'améliorer la stabilité et rapidité sans "trop" dégrader la précision.

Tout d'abord, ce protocole expérimental a-t-il des chances d'aboutir ?

Actuellement je suis bloqué à la deuxième étape, je n'arrive pas trouver une valeur pour I sans que le drone parte en looping. Peut-être devrais-je calibrer D avant I ?

Joker .

Comme je te l'ai dit je comprend (ou crois comprendre) le fonctionnement d'un asservissement mais je n'ai jamais pratiqué ou alors d'une manière tellement basique qu'il vaut mieux ne pas en parler.

lobodol:
Tout d'abord, ce protocole expérimental a-t-il des chances d'aboutir ?

Mais c'est tout le but d'une expérimentation !

Pour infos mon drone n'a quasiment jamais volé a cause de l'ajustage PID (mais je suspecte des négligences de ma part au niveau du montage) bref pour te dire que c'est bien casse ****..

Lit cet article jusqu'au bout, il est passionnant http://www.ferdinandpiette.com/blog/2012/04/asservissement-en-vitesse-dun-moteur-avec-arduino/

Salut,

J'ai discuté avec un pote ingénieur en électronique ce weekend et il me confirme que pour qu'un asservissement numérique ait toujours le même comportement, il faut une régularité dans l'asservissement. Un système déterministe en fin de compte. Du coup je commence à me demander s'il ne faudrait pas faire du temps-réel...
Un autre solution qu'il m'a suggérée c'est utiliser un quartz externe pour avoir une clock et tous les 5 fronts (5 au hasard) balancer les commandes calculées pendant les 4 clock précédentes. Ça permettrait d'avoir un asservissement régulier.

Je pense donc que je vais prendre le temps d'essayer la solution Timer.h qui, même si ça revient à faire attendre l'Arduino, me permettra d'avoir un asservissement régulier, ce qui est au final prépondérant pour avoir un comportement "déterministe" (je met entre guillemets car je raconte peut-être des conneries)

Jambe:
Lit cet article jusqu'au bout, il est passionnant http://www.ferdinandpiette.com/blog/2012/04/asservissement-en-vitesse-dun-moteur-avec-arduino/

Oui je l'ai déjà lu, il est effectivement très intéressant et accessible. Je me suis servi de cet article comme base pour l'écriture de mon code et je retourne le lire régulièrement pour être sûr de n'avoir rien oublié :slight_smile: