Cette semaine, on se propose de vous montrer en détail comment coder, en Delphi, une application Windows qui utilise des modules Yoctopuce. Pour rendre le sujet un peu intéressant, on va appliquer ça à de la régulation de température en utilisant juste un corps chauffant et un relais, exemple pratique à l'appui.
On suppose que vous avez déjà quelques connaissances de base en Delphi et que vous avez déjà lu notre article sur la structure logique des modules Yoctopuce.
Mise en place
La première chose à faire consiste télécharger l'API Yoctopuce pour Delphi. Décompressez le fichier où bon vous semble, le répertoire indispensable est "sources". Pour que le compilateur sache où trouver l'API, lancez Delphi, et dans le menu "tools/options/delphi/options/library" ajoutez le chemin vers le répertoire "Sources" de l'API. La capture d'écran ci-dessous montre l'interface de Delphi 10.2, mais le principe reste le même pour toutes les versions de Delphi.
Configuration de Delphi 10.2
Notez que si vous ne voulez pas modifier la configuration de votre environnement Delphi, vous pouvez aussi directement inclure les fichiers de l'API dans votre projet. Dans notre cas, vous aurez besoin de yjson.pas, yocto_api.pas, yocto_relay.pas et yocto_temperature.pas.
Enfin, remarquez que le répertoire sources contient un sous-répertoire "dll", qui contient lui-même le fichier yapi.dll, vous en aurez besoin plus tard.
Le projet
L'idée est de montrer comment réguler précisément la température d'un système à l'aide d'un corps chauffant piloté par un simple relais, c'est-à-dire en tout-ou-rien. Pour illustrer la chose, on a fabriqué un petit banc d'essai qui se présente sous la forme d'une petite boîte métallique contenant deux ampoules halogène 12V 50W. Les ampoules sont pilotées par un Yocto-LatchedRelay et sont alimentées par une grosse alim 12V: les ampoules consomment à elles seules 8A. La température de l'air dans la boîte est mesurée par un Yocto-Thermocouple.
Notre petit banc d'essai
Pour rendre la programmation plus facile, on a donné un nom logique aux fonctions relay et temperature1 des deux modules. A l'aide du VirtualHub, on a appelé le relais "Box-Lamp" et la sonde de température "Box-Temp".
On donne un nom logique aux fonctions qu'on compte utiliser
Un simple test démontre que ce banc d'essai est particulièrement efficace. En moins de 75 secondes, la température dans la boîte passe de 30 à 120°C et la courbe laisse penser qu'on pourrait monter bien plus haut. En revanche, le refroidissement de la boîte étant purement passif, la température met bien plus de temps à redescendre.
Performances "naturelles" de la boîte.
La question est donc de savoir si on arrive à maintenir une température arbitraire constante dans cette boîte. Voyons comment coder une application Delphi qui fait ça.
L'application
Initialisation de l'application
On commence par créer une nouvelle application VCL forms, a.k.a. une application Windows tout ce qu'il y a de plus standard. La première chose à faire consiste à initialiser l'API Yoctopuce le plus tôt possible dans la vie de l'application. Pour cela, dans le "Project manager" de Delphi, faites un "clic droit/View Source" (ou Ctlr-V) directement sur le nœud principal de votre projet. Vous allez obtenir le code source du point d'entrée de l'application. Il suffit alors de l'éditer pour rajouter un appel à YRegisterHub.
uses
Vcl.Forms,dialogs,yocto_api,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
var
errmsg:string;
begin
if (YRegisterHub('127.0.0.1',errmsg)<>YAPI_SUCCESS) then
messagedlg(errmsg,mtwarning,[mbok],0);
else
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end;
end.
Notez qu'on a mis "127.0.0.1" pour le paramètre de YRegisterHub. En effet, on compte faire marcher l'application a travers le VirtualHub, ce qui nous permettra de faire tourner même temps l'application Yocto-Visualization et ainsi d'obtenir facilement des courbes de température.
Avant de lancer l'application pour la première fois, pensez à copier le fichier yapi.dll dans le même répertoire que l'exécutable, sinon ça ne marchera pas du tout.
Creation du form
Dans le form de l'application, on place:
- Un champ TEdit en "read only" pour afficher la température courante
- Un deuxième champ TEdit en "read only" pour afficher la température de consigne
- Deux boutons TButton "+" et "-" pour ajuster la température de consigne
- Deux boutons TButton "start" et "stop" pour lancer et arrêter l'expérience
- Un TTimer, réglé à 1000ms
Création de l'interface
Initialisation du form
A l'initialisation du form, on récupère les objets correspondant à notre relais et notre sonde de température à l'aide des fonctions YFindTemperature et YFindrelay. On vérifie qu'ils sont en état de marche grâce à isOnline() et on les stocke dans des variables du form: Sensor et Relay.
begin
Sensor := YFindTemperature('Box-Temp');
Relay := YFindrelay('Box-Lamp');
if not(Sensor.isOnline()) then
begin
MessageDlg('No temperature sensor named Box-Temp',mtwarning,[mbok],0);
RunButton.Enabled:=false;
end;
if not(Relay.isOnline()) then
begin
MessageDlg('No relay named Box-Lamp',mtwarning,[mbok],0);
RunButton.Enabled:=false;
end;
end;
Code l'interface
Le code de gestion l'interface n'a pas beaucoup d'intérêt, il consiste simplement à pouvoir changer la valeur de consigne et lancer/arrêter l'expérience.
begin
TargetValue.Text := inttoStr(strtoint(TargetValue.Text)+1);
end;
procedure TForm1.DecButtonClick(Sender: TObject);
begin
TargetValue.Text := inttoStr(strtoint(TargetValue.Text)-1);
end;
procedure TForm1.RunButtonClick(Sender: TObject);
begin
RunButton.Enabled :=false;
StopButton.Enabled:=true;
running:=true;
end;
procedure TForm1.StopButtonClick(Sender: TObject);
begin
RunButton.Enabled:=true;
StopButton.Enabled:=false;
running:=false;
end;
La boucle de contrôle
On aborde maintenant la partie vraiment intéressante du problème: comment maintenir une température constante dans la boîte? On a un TTimer qui appelle un callback une fois par seconde. C'est dans ce callback qu'on va placer notre code de contrôle, mais il y a plusieurs manières de s'y prendre.
Bête et méchant
La méthode la plus simple consiste à allumer les lampes quand la température est trop basse et l'éteindre quand elle est trop haute.
procedure TForm1.Timer1Timer(Sender: TObject);
var
temp : double ;
target : double ;
delay:integer;
begin
if Sensor.isOnline() then
begin
target := strtofloat(TargetValue.Text) ;
temp := Sensor.get_currentValue();
CurrentValue.Text := format('%.2f', [temp] )+ Sensor.get_unit();
if (running and relay.isOnline()) then
begin
if temp<target then relay.pulse(timer1.Interval+100)
else relay.set_state(0);
end;
end;
end;
Notez qu'à la place de simplement allumer le relais avec set_state(1), on a utilisé la méthode "Pulse()", ainsi le relais reviendra automatiquement à sa position de repos une fois le délai donné en paramètre écoulé, et ce même si l'application de contrôle s'arrête. Cela afin d'éviter que la boîte ne parte en surchauffe incontrôlée. Le résultat de cet algorithme est correct, sans plus: on voit pas mal d'oscillations sur la courbe de température.
Bête contrôle allumé/éteind, ça oscille pas mal
On pourrait probablement diminuer l'amplitude de ces oscillations en diminuant la période du timer, mais faire basculer un relais électro-mécanique plus d'une fois par seconde n'est vraiment pas très raisonnable.
Utiliser un PID
En fait, il existe une méthode beaucoup plus efficace pour contrôler des systèmes qui ont de l'inertie: le PID. C'est un algorithme qu'on a déjà utilisé dans un article précédent. En gros, cela consiste à calculer l'erreur entre la valeur de température actuelle et la valeur désirée. On calcule la commande du relais sous la forme d'une combinaison de trois facteurs, un premier Proportionnel à l'erreur, un second proportionnel à l'Intégrale de l'erreur et un troisième, proportionnel à la Dérivée de l'erreur. Chacun de ces facteurs est pondéré par une constante, respectivement A,B et C. La difficulté consiste à trouver les valeurs correcte pour ces constantes. Google vous aidera à trouver tout plein de théories à ce sujet. Nous, on s'est juste contenté de les ajuster jusqu'à obtenir quelque chose de raisonnable. Voici le code du PID en question.
PID_Data : array[0..10] of double;
PID_Ptr : integer;
function PID(target,current:double):double;
var err,P,I,D : double;
n:integer;
const
PID_A = 150.0;
PID_B = 100.0;
PID_C = -50.0;
begin
err := target-current;
if PID_Ptr<length(PID_Data)-1 then
begin
PID_Data[PID_Ptr]:=err;
inc(PID_Ptr);
end
else
begin
move(PID_Data[1],PID_Data[0],(length(PID_Data)-1)*sizeof(double));
PID_Data[length(PID_Data)-1] := err;
end;
if PID_Ptr>2 then
begin
P := PID_Data[PID_Ptr-1];
I:=0;
for n:=0 to PID_Ptr-1 do i:=i+ PID_Data[n];
I := I / PID_Ptr;
D := ( PID_Data[PID_Ptr-1] - PID_Data[0] ) / PID_Ptr;
PID := PID_A * P + PID_B * I + PID_C * D;
end else PID:=0;
end;
Ce PID renvoie directement valeur que l'on doit utiliser pour piloter le relais. On ne peut évidement pas fermer le relais "à moitié", par contre on peut jouer sur le temps pendant lequel il va rester fermé, c'est à dire le paramètre de la fonction pulse().
procedure TForm1.Timer1Timer(Sender: TObject);
var
temp : double ;
target : double ;
delay:integer;
begin
if Sensor.isOnline() then
begin
target := strtofloat(TargetValue.Text) ;
temp := Sensor.get_currentValue();
CurrentValue.Text := format('%.2f', [temp] )+ Sensor.get_unit();
if (running and relay.isOnline()) then
begin
delay:= round(PID(target,temp));
if (delay<0) then delay:=0;
if (delay>timer1.Interval+100) then delay:=timer1.Interval+100;
Relay.pulse(delay );
end;
end;
end;
On obtient alors une bien meilleure régulation. Parce contre, on a un overshoot assez flagrant au départ. Cela vient du fait qu'à pleine puissance, le système est capable de faire monter la température plus vite qu'il ne peut la contrôler, une embardée thermique en quelque sorte.
Le PID donne un meilleur résultat, mais il y a un overshoot au début
La solution consiste à empêcher la consigne de varier trop vite. Plutôt que d'utiliser directement la consigne pour nourrir le PID, on utilise une variable "virtualTarget" qui converge tranquillement vers la consigne à la vitesse maximale de 1°C par seconde.
procedure TForm1.Timer1Timer(Sender: TObject);
var
temp : double ;
target : double ;
delay:integer;
begin
if Sensor.isOnline() then
begin
target := strtofloat(TargetValue.Text);
if abs(target-virtualTarget)>1 then
begin
if virtualTarget< target
then virtualTarget:=virtualTarget+1
else virtualTarget:=virtualTarget-1;
end else virtualTarget:=target;
temp := Sensor.get_currentValue();
CurrentValue.Text := format('%.2f', [temp] )+ Sensor.get_unit();
if (running and relay.isOnline()) then
begin
delay := round( PID(virtualTarget,temp) );
if (delay<0) then delay:=0;
if (delay>timer1.Interval+100) then delay:=timer1.Interval+100;
Relay.pulse(delay );
end;
end;
end;
On obtient alors un résultat tout à fait acceptable avec une température tenue à quelques dixièmes de degré près. Ce qui est quand même remarquable quand on pense qu'on a affaire à un système contrôlé en tout-ou-rien avec un bête relais électro-magnétique.
Le contrôle de la montée en température évite l'overshoot
Si vous avez envie d'essayer par vous-même, vous trouverez le code source complet de l'application dans ce fichier zip.