Le PID, c'est comme le piano. Ce n'est pas parce que l'on sait comment cela fonctionne que l'on sait en jouer.
J'ai découvert une vidéo de Ian Carey qui propose un banc de test extrêmement facile à réaliser qui permet de s'entrainer à modifier les variables pour bien les maitriser.
L'idée de Ian Carey, avec un Arduino Nano, est de générer une tension de référence (target) sur une sortie PWM au travers d'un filtre Passe Bas. Puis cette tension est réinjectée dans une entrée analogique. C'est ça, le truc génial !
Dans cette vidéo, j'ai également découvert l'influence très importante de dt (delta time) dans le calcul du PID.
J'ai modifié son filtre en reprenant un filtre calculé par Sando, avec R=470 et C=100µF. Ce filtre fonctionne parfaitement et génère très peu d'oscillations.
J'ai réécrit le programme de Ian Carey à ma manière. Avec 25 lignes de code, j'ai un PID qui fonctionne très bien.
Il y a quand même des choses que je ne m'explique pas dans le programme Ian Carey.
Dans la fonction setup(), il initialise l'abscisse à 0 et target , dans une boucle. J'ai voulu supprimer cette boucle, mais le "dessin" caractéristique du début du feedback, disparaissait. Il s'agit de cette oscillation à peu près symétrique sur la ligne de target. En fait, sa boucle camouflait ce problème.
J'ai alors eu l'idée de remplacer cette boucle par une temporisation de 5000ms, init_time .
J'aimerais bien savoir pourquoi, il est nécessaire de patienter pendant 5 secondes pour avoir ce début d'oscillations.
C'est important, car dans la vraie vie, on peut se demander si on lance le processus immédiatement ou si on doit attendre un petit moment avant que le feedback atteigne la bonne position.
Je génère donc l'axe des abscisses et le target dans la fonction loop(). De plus, je génère une ligne symétrique aux abscisses par rapport à la ligne du target. Cela pour que le Plotter Arduino ne zoom pas en fonction des valeurs du feedback.
Je paramètre également un délai à la fin de la fonction loop(), loop_time . Ce paramètre est très important car, il détermine le dt , et donc le calcul du PID.
Le Plotter Arduino est assez sensible, mais je pense avoir trouver le bon code.
Après avoir lancer le programme, il faut attendre les 5 secondes d'initialisation avant de voir apparaitre le feedback. Il est préférable de cocher "Interpolate" pour avoir une oscillation plus "fluide".
Je pense que l'utilisation du Plotter a une grande influence dans le dt à cause du temps d'exécution des Serial.print() , mais ce délai n'impacte pas négativement le calcul du PID, car si je diminue la variable loop_time , le résultat est impacté négativement.
Il me semble donc nécessaire que le PID soit déterminer dans un lapse de temps minimum, incompressible.
Le schéma du banc de test sur une breadbord.
Schema01_bb.jpg 151,71 Ko
1 téléchargement(s)
Le résultat avec mon code.
PID01.jpg 107,5 Ko
1 téléchargement(s)
Mon code :
// Program inspired from the code of Ian Carey https://www.youtube.com/@careyian - 03/11/2023 const int INPUT_PIN = A0; const int OUTPUT_PIN = DD5; float dt, last_time = 0, init_time = 5000, loop_time = 200, target = 60.00, kp = 0.76, ki = 0.191, kd = 0.0009; void setup(){ Serial.begin(115200); analogWrite(OUTPUT_PIN, 0); delay(init_time); } void loop(){ float now = millis(); dt = (now - last_time) /1000.00; last_time = now; float feedback = map(analogRead(INPUT_PIN), 0, 1024, 0, 255); float error = target - feedback; float output = pid(error); analogWrite(OUTPUT_PIN, (int)output); Serial.print(target); Serial.print(","); Serial.print(target*2); Serial.print(","); Serial.print(feedback); Serial.print(","); Serial.println(0); delay(loop_time); } float pid(float error){ static float integral, previous = error; float P = kp * error; float I = ki * (integral = integral + error * dt); float D = kd * (error - previous) / dt; return P + I + D; }
// code de Ian Carey https://www.youtube.com/@careyian // https://youtu.be/RZW1PsfgVEI?si=yrka40X2lhyUlicM // https://gist.github.com/careyi3/02a57dfd3a62a96d46171489c83488bd#file-pid-ino const int INPUT_PIN = A0; const int OUTPUT_PIN = DD3; double dt, last_time; double integral, previous, output = 0; double kp, ki, kd; double setpoint = 75.00; void setup() { kp = 0.8; ki = 0.20; kd = 0.001; last_time = 0; Serial.begin(9600); analogWrite(OUTPUT_PIN, 0); for(int i = 0; i < 50; i++) { Serial.print(setpoint); Serial.print(","); Serial.println(0); delay(100); } delay(100); } void loop() { double now = millis(); dt = (now - last_time)/1000.00; last_time = now; double actual = map(analogRead(INPUT_PIN), 0, 1024, 0, 255); double error = setpoint - actual; output = pid(error); analogWrite(OUTPUT_PIN, output); // Setpoint VS Actual Serial.print(setpoint); Serial.print(","); Serial.println(actual); // Error //Serial.println(error); delay(300); } double pid(double error) { double proportional = error; integral += error * dt; double derivative = (error - previous) / dt; previous = error; double output = (kp * proportional) + (ki * integral) + (kd * derivative); return output; }