[ARDUINO] Asservissement grâce au PID

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je suis actuellement en terminale SI et dans le cadre de cette matière, je dois réaliser un stabilisateur de drone "simplifié" c’est-à-dire ça : https://www.youtube.com/watch?v=w2hZoZQyRw8&t=28s

L’une des problématiques de ce projet est donc l’asservissement grâce au PID.

Notre cahier des charges nous impose de considérer que le drone n’est plus stabilisé lorsque ses oscillations dépassent de plus ou moins 5degrés de 90degrés.

Nous avons réaliser un programme comprenant la mesure de l’angle (qui fonctionne) et l’asservissement par PID mais nous rencontrons 2 problèmes avec celui-ci :

  • La consigne renvoyer est parfois négative
  • Nous pouvons avoir une consigne de 200 pour un angle de 40degrés mais de 100 pour un angle de 30…

Voici le programme en question :

////////////////////////////////////
// Les bibliothèques nécessaires //
////////////////////////////////////

#include <Wire.h>
#include <ADXL345.h>

double pitch = 0.00;

ADXL345 adxl; //variable adxl en relation avec la bibliothèque ADXL345 

const int ecart = 5;
float erreur = 0;//consigne - mesure

float consigne = 90;

float Kp = 10;
float Ki = 0.4;
float Kd = 20;

float erreur_precedente = 0;
float somme_erreurs = 0;
float variation_erreur = 0;

float mesure_ecart = 0;

float mesure = 0;
float commande = 0;

void setup(){
  Serial.begin(9600);
  adxl.powerOn();
}

void loop(){
  
    int x,y,z;  
    adxl.readXYZ(&x, &y, &z); //Lecture des valeurs issues de  l'accéléromètre et stockage dans les variables x,y,z
      // Valeurs de sorties x,y,z 
  double x_Buff = float(x);
  double y_Buff = float(y);
  double z_Buff = float(z);
  pitch = atan2((- x_Buff) , sqrt(y_Buff * y_Buff + z_Buff * z_Buff)) * 63;
  mesure = pitch;
  
  if((mesure < (consigne - (consigne * ecart / 100))) || (mesure > (consigne + (consigne * ecart / 100)))){
  
    erreur = consigne - abs(mesure);
    somme_erreurs += erreur;
    variation_erreur = erreur - erreur_precedente;
    commande = (Kp * erreur) + (Ki * somme_erreurs) + (Kd * variation_erreur);
    erreur_precedente = erreur;
  
    delay(500);

  } else {

    //Rénitialisation des variables
    
    erreur_precedente = 0 ;
    somme_erreurs = 0 ;
    variation_erreur = 0 ;

    commande = (Kp * erreur) + (Ki * somme_erreurs) + (Kd * variation_erreur); //Remettre à 0 la commande

  }

  erreur = 0 ;

}

Est-ce une méthode correcte ? Si oui, comment faire pour résoudre ces problèmes ?

Cordialement et Merci d’avance !

PS :

  • Pour l’instant nous n’avons pas de moteurs, nous réalisons donc nos tests "à la main" avec une équerre ^^
  • Les valeurs du PID sont rentrés au hasard.

Salut,

Alors il y a beaucoup à dire, alors je vais te répondre un peu en vrac.

Pour l’instant nous n’avons pas de moteurs, nous réalisons donc nos tests "à la main" avec une équerre

C’est bien pour tester le code, mais tu auras du mal à tester le PID en fonctionnement normal, c’est-à-dire avec le système piloté et la boucle de rétroaction de ta régulation.

Les valeurs du PID sont rentrés au hasard.

Ça ne pose pas de problème pour tester le code, mais ça deviendra vite gênant lors d’un test grandeur nature. Le réglage d’un PID n’est pas quelque chose de trivial, et se fera sûrement par essais-erreurs pour obtenir quelque chose de satisfaisant (comme dans la vidéo).

Notre cahier des charges nous impose de considérer que le drone n’est plus stabilisé lorsque ses oscillations dépassent de plus ou moins 5degrés de 90degrés.

Comment est défini 90° ? C’est l’horizontale ? Et 0° est la verticale ? Et comment est orienté le système ? Tout ça à une importance pour éviter que ton contrôleur n’essaie de réguler dans la direction opposée et déstabilise ton système.

En regard du code, je pense que tu devrais enlever les conditions. Il n’y a pas de raison d’arrêter de réguler quand on est proche de la cible. On continue de le faire, pour éviter de dériver. Il n’y pas tant que ça de systèmes réels où on débranche la régulation une fois arrivé à la consigne.

Nous avons réaliser un programme comprenant la mesure de l’angle (qui fonctionne) et l’asservissement par PID mais nous rencontrons 2 problèmes avec celui-ci :

  • La consigne renvoyer est parfois négative

C’est un cas normal, lié à l’orientation du système. Si tu penches vers la gauche, la consigne sera positive (en fonction de la définition de l’orientation du système) et si tu penchez vers la droite, elle sera négative.

  • Nous pouvons avoir une consigne de 200 pour un angle de 40degrés mais de 100 pour un angle de 30…

Sans plus d’infos, ce n’est pas forcément absurde. Quelle est l’unité de ton 200 et de ton 100 ?

if((mesure < (consigne - (consigne * ecart / 100))) || (mesure > (consigne + (consigne * ecart / 100)))){

Comme dit tout à l’heure, je ne comprends pas pourquoi il y a ce système de condition. À mon avis il ne sert à rien. Le contrôleur se « réinitialise » tout seul quand il régule autour de la consigne et que l’erreur devient nulle.

  pitch = atan2((- x_Buff) , sqrt(y_Buff * y_Buff + z_Buff * z_Buff)) * 63;

Je ne me prononce par sur la formule, mais je suis curieux de la multiplication par 63. Ça correspond à quoi ?

    erreur = consigne - abs(mesure);

Ça c’est douteux. Pourquoi une valeur absolue ?

    somme_erreurs += erreur;
    variation_erreur = erreur - erreur_precedente;
    commande = (Kp * erreur) + (Ki * somme_erreurs) + (Kd * variation_erreur);
    erreur_precedente = erreur;

À première vue, ça ne paraît pas déconnant.

  delay(500);

Jupiter41

500 ms, c’est très long pour ce type de systèmes, ça risque de poser problème avec le vrai système, même si pour tester c’est pas gênant.

Si tu veux vraiment tester l’implémentation de ton PID (et pas son réglage), l’idéal est de pouvoir enregistrer toutes les variables à chaque tour de boucle et de faire bouger ton système, afin d’observer la réaction en fonction du temps. Avec ça, tu peux analyser le comportement et voir s’il est normal.

Aabu t’a très bien répondu.

    somme_erreurs += erreur;
    variation_erreur = erreur - erreur_precedente;
    commande = (Kp * erreur) + (Ki * somme_erreurs) + (Kd * variation_erreur);
    erreur_precedente = erreur;

À première vue, ça ne paraît pas déconnant.

Aabu

Il faudrait juste prendre en compte le temps entre chaque mesure pour l’intégration des erreurs.


Pour la partie réglage de ton PID, tu peux faire un peu mieux que juste au hasard.

Tu fais un full proportionnel au début (tu vas avoir des oscillations plus ou moins grandes), et tu essaies d’avoir une correction suffisamment rapide et constante (que tes oscillations aient toujours la même amplitude).

Tu prends ton Kp ainsi obtenu, et tu le divises par deux. Maintenant tu essaies d’obtenir un temps de correction suffisamment correct en jouant avec Ki (mais une valeur "assez faible" qui répond à ça).

Ensuite tu règles ton Kd pour que ton système puisse ce stabiliser en une seule oscillation (ou que les suivantes soient vraiment minimes) et ne pas trop dépasser la consigne.

A ce stade, ta correction sera, je suppose, bien trop lente. Essaies de monter Ki (et légèrement Kp au besoin) pour ré-obtenir une correction suffisamment rapide et d’adapter Kd à chaque fois pour contrebalancer les oscillation du système.


Sinon, un peu plus compliqué (et une fois que ton prototype sera fonctionnel), c’est que tu laisse ton système apprendre/modifier ses coefficients. Et si tu veux aller plus loin, tu peux regarder du côté des filtres de Kalman et des centrales inertielles.

+2 -0

Il faudrait juste prendre en compte le temps entre chaque mesure pour l’intégration des erreurs.

C’est vrai. Il y a au moins deux manières de procéder : soit intégrer le temps une fois pour toute dans la valeur des coefficients (il faut alors que la boucle soit régulière), soit avoir des coefficients indépendants du temps et prendre en compte le temps dans les formules.

Ce qu’il faut retenir, c’est que le réglage du PID dépend de la durée depuis la dernière mise à jour. C’est assez intuitif en fait : si on attend plus longtemps, alors on risque d’avoir plus dérivé, et donc il faudra être plus vigoureux sur la réaction pour réguler correctement.

D’accord merci beaucoup pour vos réponses!

Je vais commencer par le plus simple ^^ ;

  • La valeur absolue a était retiré dans notre programme lors de nos derniers tests car on s’est rendu compte qu’elle ne servait à rien ^^

  • Le 63 dans le calcul à une origine inconnue :p : la bibliothèque impose de multiplier les valeurs du capteurs par un coefficient (et nous avons trouvé 63 pour avoir des valeurs correctes (qui sont assez précises)

  • 90 degrés correspond à la valeur de l’angle lorsque le système est à l’horizontale. Nous avons décidé de prendre cette valeur pour éviter d’avoir à traiter des valeurs négatives (on fera sûrement une projection de vecteur pour modifier l’angle (et le mettre à 90 degrés))

  • 0 degré correspond donc bien à la position si le système est à la verticale (mais il ne sera normalement jamais atteint ^^)

  • D’accord pour la détermination des valeurs du pid, je regarderai :)

  • Nous ne savons pas l’unité de la consigne… nous utilisons des angles pour la calculer mais nous ne savons pas si la consigne est toujours un angle… (Et on ne pense pas que cela soit un angle étant donné les valeurs renvoyées)

  • D’accord pour le 500 ms on le modifiera !

  • Comment intégrer le temps dans nos calculs ? (Quelle opération faire ?)

  • Pour la condition, apparement même avec le pid il y aura toujours un écart par rapport à la valeur désirée donc on a mis la condition pour ça :) (après c’est ce qu’on nous a dit :p)

J’espère que j’ai répondu à toute vos questions ! :)

+0 -0
  • Nous ne savons pas l’unité de la consigne… nous utilisons des angles pour la calculer mais nous ne savons pas si la consigne est toujours un angle… (Et on ne pense pas que cela soit un angle étant donné les valeurs renvoyées)

Votre consigne c’est 90°, c’est votre système à l’horizontal. Votre commande doit être une différence de poussée entre les 2 moteurs. le plus simple pour un système réglé empiriquement, c’est de travailler directement avec des valeurs de rapport cyclique de PWM (le signal envoyé aux ESC des moteurs)

  • D’accord pour le 500 ms on le modifiera !

Quelque chose comme 10 ou 20ms doit être pas mal pour ce genre de système.

  • Comment intégrer le temps dans nos calculs ? (Quelle opération faire ?)
commande = kp * erreur + ki * somme_erreur * periode_echantillonage + kd * variation_erreur / periode_echantillonage

Mais honnêtement, une fois la fréquence d’échantillonnage fixée, je la cacherait directement dans Ki et Kd, ça économise des calculs.

  • Pour la condition, apparement même avec le pid il y aura toujours un écart par rapport à la valeur désirée donc on a mis la condition pour ça :) (après c’est ce qu’on nous a dit :p)

C’est pas un problème, continue à faire tourner le PID même quand l’erreur est faible.

+0 -0

Merci pour ta réponse ! :) : encore une erreur de ma part je voulais bien dire commande.

Et merci pour la formule avec le temps ! Mais est-ce que vous pourriez m’indiquer a quoi correspond (à part avec le nom ^^) periode_echantillonage s’il vous plaît ? :)

Et merci aussi pour le reste, on modifiera ça :)

+0 -0

la période d’échantillonnage, c’est la période à laquelle on vient lire le capteur, calculer l’erreur, l’intégrer/la dériver, et mettre à jour la commande. donc ici c’est les 500ms entre chaque boucle, qu’on a proposé de descendre à une dizaine de millisecondes.

l’idée est la suivante: si on a une erreur (constante durant l’expérience) de 1. On la mesure à 10 Hz, au bout d’une seconde, l’erreur accumulée vaut 10. Même expérience, mais en mesurant à 100Hz, le cumul vaut 100. Si on multiplie par la période d’échantillonnage, 0.1s et 0.01s, dans les deux cas, l’intégrale vaut 1, et le résultat ne dépend que de l’expérience, pas de la période d’échantillonnage (en réalité, l’échantillonnage a d’autres effets, d’un point de vue mathématique, mais il ne me semble pas pertinent de les aborder ici).

D’accord merci ! La période d’échantillonnage seras du coup toujours la même si j’ai bien compris ? (C’est le temps dans le délai à la fin de loop ()). Il faut l’exprimer en seconde si j’ai bien compris aussi ? :)

Et j’aurais une dernière question du coup : étant donné que ma condition n’est utile, la rénitialisation des variables que je fait dans le elle n’est pas utile aussi ? (Si oui (elle n’est pas utile) comment les valeurs (de somme erreur et variation erreur notamment) peuvent-elles ne pas exiger une certaine commande à envoyer alors que le drone est stabilisé ?)

+0 -0

En général, on travaille avec une période d’échantillonnage fixe. Ensuite, j’ai utilisé la seconde parce que c’est l’unité du système international. Ca marcherait aussi en comptant en années martiennes.

Ce qu’il faut comprendre, c’est que ton Kp a une unité: le [grandeur de sortie].[grandeur d’entrée]^-1

Avec ma nouvelle expression, Ki et Kd ont la même unité de Kp. Vu que variation_erreur est en [grandeur d’entrée].[grandeur de temps], en le divisant par le temps d’échantillonnage, on a bien quelque chose d’homogène à une grandeur d’entrée. Physiquement c’est élégant.

Dans la pratique, supprimer la période d’échantillonnage, et exprimer Kd directement en [grandeur de sortie].[grandeur d’entrée]-1.[grandeur de temps]-1, ça fonctionne (la période étant constante), et ça reste une valeur choisie au jugé (quand on le fait pour une fusée, on a une meilleure méthode que de lancer des fusées jusqu’à avoir trouvé des valeurs de Kp/Ki/Kd correctes).


Quand le drone est stabilisé, l’erreur est nulle, la variation de l’erreur est nulle, le seul terme non nul est l’intégration de l’erreur. Ce dernier terme est important: en général, un système n’est pas parfaitement équilibré. Pour le maintenir parfaitement à sa place, il faut appliquer un petit effort. C’est à ça que sert le terme intégral.

En revanche, ce terme intégral est embêtant à régler (vous l’expérimenterez), et il m’est arrivé bien souvent de le régler à 0, et de stabiliser juste avec Kp et Kd. Comme je le disais, le système n’est jamais parfaitement équilibré, donc, à erreur nulle, système immobile, j’ai une commande nulle, et mon déséquilibre va faire s’éloigner mon système de sa position de consigne. Il va se stabiliser proche de celle ci, à une distance qui fait que Kp*erreur compense exactement l’effet du déséquilibre. L’erreur à cette position est ce qu’on appelle l’erreur statique (l’erreur qui reste quand le système est statique).

Pas de problème avec plaisir :) Si avec les modifications que vous m’avez proposé les commandes retournées par le PID sont plus cohérentes, il ne me resteras plus qu’à trouver comment les utiliser pour trouver de quelle manière faire tourner les moteurs et ma partie pour le projet seras fini :p (je m’occupe de l’asservissement, un ami de la portance et un autre sur le calcul des angles ^^)

Après il faudra juste fabriquer le bras à stabiliser et voilà :)

+0 -0

Par contre je viens d’y penser :p mais comme vous me l’avez dit le paramètre intégrale n’est jamais egal à 0 mais étant donné qu’il correspond à la somme des erreurs même lorsque l’erreur seras égale à 0 (drone stable) la somme des erreurs seras toujours élevé car il y aura toujours la somme des erreurs précédentes non ? (Donc la commande seras élevé alors que le drone est stable)

+0 -0

la somme des erreurs seras toujours élevé car il y aura toujours la somme des erreurs précédentes non ? (Donc la commande seras élevé alors que le drone est stable)

Jupiter41

Attention ! Quand tu es autour du point stabilisé, l’erreur est tantôt positive, tantôt négative, parce que tu oscilles lentement autour de la consigne. Quand on intègre (i.e. quand on somme) l’erreur pour une consigne constante, on finit par obtenir une commande constante, parce que les erreurs négatives et les erreurs positives vont se compenser. Dans certains cas, la commande obtenue sera nulle, mais le plus souvent, on aura une petite valeur résiduelle, qui sert à compenser les imperfections.

Par exemple, pour un drone, le cas idéal correspond à un équilibre parfait, où chaque moteur tourne identiquement pour rester horizontal (action intégrale nulle). Dans un cas plus réaliste, le drone aura peut être un bras légèrement plus lourd que l’autre, et le contrôleur devra tirer d’un côté en permanence pour maintenir l’horizontal (action intégrale non nulle).

Ah oui d’accord :) Sans le savoir c’est vrai qu’il est difficile de voir cette oscillation (et donc la compensation des erreurs) lorsque le drone est "stable" sans moteurs car on ne pense pas as faire oscillé le capteur lorsque l’on fait les tests à la main ^^ (donc le paramètre intégrale continue d’augmenter etc..).

Du coup je vais pouvoir modifier notre code avec vos indications pour voir si les commandes envoyés sont plus cohérentes :) Merci !

Re :),

Malheureusement nous n’avons pas pu continuer les projets cette semaine du coup j’ai dû attendre ce week-end pour pouvoir modifier le programme sur mon temps personnel. Nous ne pouvons donc pas le tester tout de suite. En attendant pouvez vous me dire si il correspond bien aux conseils que vous m’avez donné et si il devrait renvoyé des valeurs plus correctes ? :) :

////////////////////////////////////
// Les bibliothèques nécessaires //
////////////////////////////////////

#include <Wire.h>
#include <ADXL345.h>

double pitch = 0.00;

ADXL345 adxl; //variable adxl en relation avec la bibliothèque ADXL345 

float erreur = 0; //consigne - mesure

float consigne = 90;

const float Kp = 10 ;
const float Ki = 0.4 ;
const float Kd = 20 ;

float erreur_precedente = 0 ;
float somme_erreurs = 0 ;
float variation_erreur = 0 ;

float mesure = 0;
float commande = 0;

int periode = 1 ;

void setup() {

  Serial.begin(9600);
  adxl.powerOn();

}

void loop() {
  
    int x,y,z;  
    adxl.readXYZ(&x, &y, &z); //Lecture des valeurs issues de  l'accéléromètre et stockage dans les variables x,y,z

      // Valeurs de sorties x,y,z 
  double x_Buff = float(x);
  double y_Buff = float(y);
  double z_Buff = float(z);

  pitch = atan2((- x_Buff) , sqrt(y_Buff * y_Buff + z_Buff * z_Buff)) * 63;
  mesure = pitch;
  
    erreur = consigne - mesure ;
    somme_erreurs += erreur ;
    variation_erreur = erreur - erreur_precedente ;

    commande = (Kp * erreur) + (Ki * somme_erreurs * periode) + ((Kd * variation_erreur) / periode) ;

    erreur_precedente = erreur ;

    periode = 15 ;
  
    delay(15) ;

}

Cordialement et Merci d’avance !

PS : Définir la période après la première déstabilisation est fait exprès pour que le temps n’influe pas sur la commande suite à celle-ci.

+0 -0

Ton programme me paraît plutôt correct. Il faudra cependant tester pour en être sûr.

Une seule petite chose m’intrigue :

PS : Définir la période après la première déstabilisation est fait exprès pour que le temps n’influe pas sur la commande suite à celle-ci.

Q’est-ce que tu veux dire par là ? Pourquoi tu ne dis pas directement periode = 15 une bonne fois pour toute ?

Fais aussi attention aux unités, en fonction de comment tu définis les coefficients du contrôleur, tu auras peut-être envie de convertir la période en secondes pour le calcul de la commande.

Je me suis juste dit que le temps ne devait pas influer sur la commande la première fois ^^ (donc c’est faux ?)

Je ne sais pas où tu as eu cette idée, mais c’est étrange.

La période d’échantillonnage intervient dans la mise à jour la commande, mais la commande est toujours mise à jour de la même manière, que ce soit la première ou la millionième fois. La seule différence, c’est que la première fois, certaines valeurs seront égales aux valeurs initiales.

Après, il faut garder en tête que l’initialisation n’arrive qu’une fois, ce qui fait qu’elle est en général peu importante, sauf si on fait un cas tordu exprès.

Et dans quel(s) cas devrais je convenir la période en seconde ?

Jupiter41

C’est à toi de choisir. Physiquement, c’est la même chose, c’est juste qu’il ne faut pas te mélanger les pinceaux. Dans ton programme, ta période semble être en millisecondes. Cela se répercute dans l’unité de Ki et Kd. Comme tu n’as précisé aucune unité nulle part, je pense que c’est utile que tu y réfléchisses brièvement.

Ce sujet est verrouillé.