Dimanche, septembre 14 2014

Régulation PID, comment la régler 1/2

Étant donné que je n’ai pas encore tout le matériel nécessaire pour avancer mes autres projets, j’en profite pour approfondir un peu mes connaissances théoriques et vous en fait donc profiter. Mon projet ici est simple : Réguler une température à l’aide d’un microcontrolleur, mais surtout bien comprendre les tenants et aboutissements des différents réglages et algorithmes. L’objectif ici n’est pas tant d’expliquer ce qu’est un PID, ni comment il fonctionne dans le detail, mais plutôt de donner une méthode permettant d’approximer les coefficients du régulateur.

Je profiterais de cette expérience pour faire 2 articles :
– celui-ci concernant le PID a proprement parler
– Un second article concernant ma méthode pour obtenir des courbes « lives » a partir d’un arduino et d’un PC.

Qu’es-ce qu’un PID

Pour simplifier, on parlera dans la suite de régulation de température, vu que ce sera notre application ici. Mais bien entendu, une régulation peut se faire sur tout type de process (vitesse, débit, etc…)

Lorsqu’on veut réguler la température (d’une pièce par exemple), l’idée la plus simple qui nous vient à l’esprit est de faire une régulation tout-ou-rien : on est en dessous d’un premier seuil, on allume la chauffe, on passe au dessus d’un second seuil on coupe la chauffe. Cette façon de procéder, bien que très simple à implémenter n’est pas exempte de défauts. Par exemple, la température ne sera jamais vraiment constante, même si elle l’est « en moyenne ». Selon le process, on peut arriver à fortement dépasser les consignes (dans un sens ou dans l’autre), en fonction de l’inertie du système, ce qui peut s’avérer nuisible selon ce qu’on veut faire. Enfin, la puissance du chauffage oscillera entre 0 et 100%, ce qui engendre une surconsommation non négligeable.

La régulation PID permet de coller « au plus juste » à la consigne, en évitant les dépassement (en fonction du réglage comme on le verra plus loin), et en n’utilisant que la puissance nécessaire à aller à la consigne et à la maintenir. Concrètement, dans PID, on a P pour Proportionnelle, D pour Dérivée, et I pour Intégrale. Si comme moi vous avez tendance à être un peu allergique aux maths, vous commencez déjà à vous sauver, mais en fait c’est moins compliqué que ça n’en a l’air.

Proportionnelle : c’est ni plus ni moins que la différence entre votre consigne et la valeur actuelle, multiplié par un coefficient de proportionnalité (Que vous définissez, cf la suite). Dans la suite, nous l’écrirons Kc.e(t)

Intégrale : A chaque mesure, vous allez avoir une différence avec la consigne (en plus ou en moins). Prenez cette erreur à chaque pas de temps, multipliez la par un (autre) coefficient de proportionnalité, et additionnez le résultat de tous les pas de temps depuis le début, et vous avez la fameuse composante Intégrale. Dans la suite, l’intégrale sera notée de la façon suivante :

I

 

Dérivée : Son rôle est de « freiner » l’augmentation de puissance calculée en fonction de P et de I, de manière à ne pas dépasser la consigne. C’est une façon de prendre en compte la vitesse d’évolution de notre process. Pour le calculer, vous faites erreur actuelle – erreur précédente, multiplié par un troisième coefficient de proportionnalité

La formule globale de notre régulateur PID peut donc s’écrire de la manière suivante (plus d’explications plus bas):

Formule PID, source ControlGuru

Formule PID, source ControlGuru

 

Description du materiel

Le montage utilisé pour ces essais est très simple : un arduino, un bouton 2 positions, 1 capteur de température (DS18B20), un mosfet (IRF540), une résistance 10Ω 5W, et une alimentation 12V. Le mosfet est piloté par une sortie PWM de l’arduino, et gère le courant (puissance) transitant dans la résistance. Cette dernière chauffe donc plus ou moins en fonction du ratio PWM (la tension change) envoyé par l’arduino. Le bouton 2 positions sert à passer d’une première consigne (valeur PWM 5 sur 255) à une seconde (valeur PWM 30 sur 255) Un petit programme python enregistre les valeurs dans un fichier texte sur le pc (via connection série), et j’utilise GnuPlot pour avoir une jolie courbe et pouvoir lire les valeurs des graphiques plus facilement que directement sur l’image. Par la suite, j’ai ameliore mon script python, de maniere a pouvoir saisir une consigne via le port serie, tout en continuant de logguer.

Montage régulation de température

Montage régulation de température. Attention, utiliser une résistance 5W mini !

Méthode de réglage

Je vous présente une méthode « graphique » directement tirée du site Control Guru, appliquée à mon montage de test. Ce site étant en anglais et très dense en informations, j’ai essayé de résumer les points essentiels ici. J’invite ceux qui voudraient approfondir le sujet à consulter ce site qui est une véritable mine d’information sur la régulation.

Définitions

  • PV : Process Variable, cela correspond à la grandeur mesurée que l’on veut influencer (la température dans cet exemple)
  • CO : Controller Output , la sortie de notre contrôleur en %. (Correspond à l’ouverture de la vanne par exemple)
  • Kp : Gain du process
  • Kc : Gain du contrôleur

Introduction

Une fois modélisé notre boucle de contrôle, nous allons pouvoir en déterminer les trois paramètres principaux par expérimentation, mesure et calcul (et oui…)

Les paramètres à déterminer sont :

  • Kp : la Gain du process, ou, dit autrement, le sens et la grandeur de la variation de la sortie
  • Tp : Constante de temps du process, ou la vitesse de variation quand le process a commencé à répondre
  • θp : (Prononcer Thêta P) Temps mort du process, ou le délai écoulé avant que le process commence à réagir
Boucle de contrôle

Boucle de contrôle

Afin de pouvoir déterminer nos trois paramètres, il va nous falloir mesurer la façon dont réagis notre système. La première étape consiste donc à fixer une première valeur de sortie à notre contrôleur (CO) et attendre que la variable de sortie (PV, ici la température) soit bien stabilisée.

Une fois cette dernière stable, nous allons modifier la valeur de sortie du contrôleur (CO), et mesurer le changement obtenu sur la variable de sortie (PV). On logue bien sûr tout ça dans un fichier, avec les pas de temps, de manière à pouvoir également déterminer le temps mort du process (θp) et la vitesse de variation (Tp).

On se sert ensuite de ces mesures pour tracer un joli graphique, qui nous servira à déterminer nos différentes valeurs.

La formule permettant de déterminer notre premier paramètre Kc, qui est à la base du calcul de nos 3 paramètres (cf. formule) est la suivante :

source : controlguru

source : controlguru

pour déterminer Kc, nous devons donc commencer par déterminer différentes variables intermédiaires.

Calcul de Kp

Kp décrit le sens et de combien va évoluer PV, en fonction d’un changement de consigne (CO) Le cacul de Kp est assez simple : Kp=ΔPV/ΔCO ou en d’autre termes Kp = (différence en PV initial et PV final)/(différence entre consigne initiale et finale)

Sur mon prototype, j’ai modifié la consigne initiale (1,96%) pour une nouvelle consigne à 11.76%, voici la courbe obtenue:

Graphique test du système.

Graphique test du système.

En appliquant la formule ci-dessus, on a donc Kp = (79-40)/(11,76-1,96) = 39/9.8 = 3.98°C/%

Détermination de la constante de temps Tp

Tp représente le « en combien de temps » le système va réagir. En partant du même graphique, on va déterminer quand l’état de sortie vaut 63% de δPV, à partir du moment où le système à déjà commencé à réagir (et pas à partir du moment où on a modifié la consigne. Pour ceux qui se demandent d’où sort les 63%, vous trouverez toutes les explications (attention, maths !) dans ce pdf Fichier:Derive63rule.pdf On regarde donc le nombre de secondes écoulées lorsqu’on arrive à 63% de δPV (ici 39*0.63 + 40 = 64.6°C), et le nombre de secondes écoulées au moment où le système commence à réagir (très rapide ici)

Détermination de la constante de temps TP

Détermination de la constante de temps Tp

On obtiens donc Tp = 172 secondes.

Le cas de θp

θp représente le temps mort avant réaction. Dans une boucle de contrôle, il n’est jamais très bon d’avoir du temps mort, et il sera nécessaire d’avoir cette valeur la plus faible possible. Du fait de mon montage de test, le temps mort est ici très réduit. Ceci s’explique facilement par la position du capteur (sur la résistance), et par le faible volume à chauffer (uniquement la résistance. De manière assez intuitive, on imagine très bien que plus le volume à chauffer sera important, plus le temps mort sera long, pour une même puissance de chauffe.

Si on reprend le graphique ci-dessus, on vois que la consigne a été modifié à T1=195s, et j’ai décider de compter le début de la réaction à T2=198s. On a donc θp = T2 – T1 = 198 – 195 = 3s.

Il est intéressant de noter ici que mon échantillonnage étant d’une seconde, la valeur de θp peut être sensiblement erronée, du fait de sa faible durée dans ce cas.

Application à un modèle PID

Formule PID, source ControlGuru

Formule PID, source ControlGuru

Avec:

  • CO = Sortie du contrôleur (Controller Output)
  • CObias = biais du contrôleur, ou valeur nulle.
  • e(T) = Erreur actuelle, définie par SP – PV
  • SP = consigne (Set Point)
  • PV = valeur actuelle du process (Process Value)
  • Kc = Gain du controlleur, un paramètre de réglage
  • Ti = reset time, un paramètre de réglage
  • Td = derivative time, un paramètre de réglage

Calcul du gain et du reset time

Il faut tout d’abord se demander si notre process accepte un dépassement de consigne ou pas, ainsi que la vitesse à laquelle ce process doit réagir.

  • Si on accepte un dépassement, on peut choisir un réglage agressif, et donc un faible temps de réponse (petit Tc). Dans ce cas, Tc est le plus grand de 0.1Tp ou 0.8θp
  • Modéré, le contrôleur produira peu ou pas de dépassement. Dans ce cas, Tc est le plus grand de 1.Tp ou 8.θP
  • Conservateur, le contrôleur ne produira pas de dépassement, mais ira doucement, Tc sera le plus grand de 10.Tp ou 80.θP

Une fois décidé du comportement de notre contrôleur, et calculé Tc, on peut calculer le gain du contrôleur et le reset time grâce à la formule suivante :

source : controlguru

source : controlguru

Pour mon application, je vais choisir un comportement modéré. Tc sera donc la plus grande valeur de 1.Tp ou 8.θP (1×172 ou 8*3=24). On retiendra donc Tc = 172.

J’obtiens donc Kc = (1/3.98)*(172/(3+172)) = 0.25%/°C et Ti = 172

Je peux maintenant calculer mes deux principaux paramètres P et I :
P = Kc = 0.25
I=Kc/Ti. = 0.25/172 = 0.00145
Reste à déterminer D. la formule permettant de déterminer D est Kc.Td, avec Td = (Tp.θP)/(2Tp+θP) = (172*3)/(344+3) = 1.487. D vaut donc 0.25*1.487=0.372
D = 0.372

Mise en application

Reste maintenant à confronter les calculs à la réalité. Voici le code Arduino que j’ai utilisé pour effectuer mes tests :

#include <OneWire.h>
#include <DallasTemperature.h>

// Data wire is plugged into port 2 on the Arduino
#define ONE_WIRE_BUS 2
// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&oneWire);
DeviceAddress insideThermometer;

unsigned long lastTime;
double Input, Output, Setpoint;
double ITerm, lastInput;
double kp, ki, kd;
int sampleTime = 1000; //1 sec
double outMin, outMax;
String cmd = String("");
double tfirst = 0;

float printTemperature(DeviceAddress deviceAddress)
{
  sensors.getAddress(insideThermometer, 0);
  float tempC = sensors.getTempC(deviceAddress);
  Serial.print(tempC);
  Serial.print(" ");
  Serial.print(Output);
  Serial.print(" ");
  Serial.println(Setpoint);
  return(tempC);
}

void compute()
{
  unsigned long now = millis();
  int timeChange = (now-lastTime);
  if(timeChange>=sampleTime)
    {
      //get the new input value
      Input = getInput();
      //compute all working error variables
      double error = Setpoint - Input;
      ITerm+=(ki * error);
      if(ITerm>outMax) ITerm = outMax;
      else if(ITerm < outMin) ITerm = outMin;
      double dInput = (Input - lastInput);
      
      //compute PID output
      Output = kp * error + ITerm - kd * dInput;
      if(Output > outMax) Output = outMax;
      else if(Output < outMin) Output = outMin;
      
      //remember some variables for next round
      lastInput = Input;
      lastTime = now;
    }
}

void setTunings(double Kp, double Ki, double Kd)
{
  double sampleTimeInSec = ((double)sampleTime)/1000;
  kp = Kp;
  ki = Ki * sampleTimeInSec;
  kd = Kd / sampleTimeInSec;
}

void setSampleTime(int NewSampleTime)
{
  if (NewSampleTime>0)
  {
    double ratio = (double)NewSampleTime / (double)sampleTime;
    
    ki *= ratio;
    kd /= ratio;
    sampleTime = (unsigned long)NewSampleTime;
  }
}

void setOutputLimits(double Min, double Max)
{
  if(Min > Max) return;
  outMin = Min;
  outMax = Max;
  
  if(Output > outMax) Output = outMax;
  else if (Output < outMin) Output = outMin;
  
  if(ITerm>outMax) ITerm = outMax;
  else if(ITerm<outMin) ITerm = outMin;
}

double getInput(void)
{
  sensors.setResolution(insideThermometer, 9);
  sensors.requestTemperatures(); // Send the command to get temperatures
  return printTemperature(insideThermometer);
}

void setup()
{
  Serial.begin(9600);
  pinMode(3, OUTPUT);
  Setpoint = 0; //on fixe 0 de consigne
  double P, I, D;
  P = 0.25;
  I = 0.00145;
  D = 0.372;
  int STime = 1000;
  setTunings(P,I,D);
  setSampleTime(STime);
  setOutputLimits(0, 255);
}

void loop(){
compute();
analogWrite(3,Output);
if (Serial.available() > 0) {
      char SerialInByte;
      SerialInByte = Serial.read();
      
      if(SerialInByte==13){
        Setpoint = cmd.toInt();
        cmd = String("");
      }
      else
      {
        cmd += String(SerialInByte);
      }
  }
}

et voici le résultat :

Test des paramètres PID calculés

Test des paramètres PID calculés

Conclusion :

J’ai utilisé des paramètres « conservateurs » afin d’éviter de dépasser la consigne. On peut voir qu’effectivement la consigne n’est jamais dépassée, au prix d’une montée en charge un peu lente. En partant sur cette bonne base, il est maintenant facile d’affiner si l’on souhaite quelque chose d’un peu plus réactif, il reste de la marge de manœuvre.

Sources :

La régulation étant un vaste domaine, j’ai trouvé beaucoup d’informations pertinentes sur les deux sites suivants. Si vous n’êtes pas anglophobe, et désirez approfondir le sujet, je vous les recommandes chaudement !
ControlGuru
brettbeauregard.com, auteur d’osPID

Dimanche, avril 20 2014

Thermomètre Nixie Steampunk

Fan de ce style depuis pas mal de temps déjà, c’est ma première réalisation concrète. L’idée était de réaliser un (joli) thermomètre d’ambiance, histoire de savoir quelle température il fait dans la pièce. J’avais déjà réalisé un thermomètre à tubes Nixies, mais ce dernier avait deux défauts : les tubes n’étaient pas centrés sur le pcb, et il consommait un peu trop pour avoir envie de le laisser allumé en permanence (et en plus, il n’était pas « habillé »…).

Du coups, la première étape de cette réalisation a été de refaire un pcb complet. Pas simplement déplacer les tubes, j’en ai profité pour alléger le tout, histoire de supprimer les composants qui n’étaient pas nécessaires. En effet, sur la version précédente, j’utilisais un NE555 pour générer les impulsions nécessaires à la haute tension. Désormais cette tâche est réalisée par le microcontrolleur lui-même.

Schéma thermomètre Nixie

Schéma thermomètre Nixie

Quitte a devoir reprogrammer le microcontrolleur pour rajouter la génération de la haute tension, j’en ai profité pour tout ré-écrire en avrc. Ca me permet d’avoir un timing très précis, autant sur la génération du signal HT que sur le multiplexage des tubes. (La fréquence est importante pour la génération de la haute tension car elle joue pour beaucoup dans le rendement). Mon code aurais pu être grandement optimisé si j’avais un peu mieux réfléchi à mes branchements, mais sur ma version, je m’étais trompé sur certaines liaisons (ce qui expliquera les fils visibles sur les photos). Le schéma proposé corrige ces erreurs.

tarthermomètre nixie (AVRC)

 

Thermomètre à tube Nixie Steampunk

Thermomètre à tube Nixie Steampunk

Pour l’habillage, je me suis fait un peu plaisir. Les deux « chapeaux » sont en laiton, que j’ai tourné, moitié façon meca, c’est à dire en utilisant le tour de manière traditionnelle, moitié à main levée à l’aide d’une lime (pour les arrondis notamment). Le reste de l’accastillage est composé de différents tubes de laitons, diamètre 5 et 3mm, que l’on trouve facilement en magasin de modélisme.
Le tube est quand a lui un tube de plexyglass acheté pour l’occasion.

Socle vu de dessous

Socle vu de dessous

Le socle a été tourné dans un beau morceau de chêne, par un ami car je ne disposais pas de tour à bois, et le tour à metal n’est vraiment pas adapté à ce genre d’opérations. Le plot du milieu est assez profond pour que le tube laiton soit bien maintenu, mais ne va pas jusqu’en bas pour pouvoir laisser passer les fils. J’ai repris ensuite le socle tourné pour le fraiser afin de fixer le connecteur d’alimentation, et fait les 4 perçages nécessaires (2 pour les tubes verticaux, un pour le capteur de température, et un pour le connecteur d’alim).

Détail du capteur de température

Détail du capteur de température

Le capteur de température utilisé est un LM35. Pas particulièrement esthétique donc. Pour le masquer, je l’ai donc glissé à l’intérieur d’une douille de 22lr qu’un ami tireur m’a gentiment fourni. Le capteur est fixé à l’intérieur à la colle à chaud.

En fonctionnement

En fonctionnement

Dimanche, janvier 19 2014

DIY – shield PWM

Bon, petit montage pas bien compliqué à la demande d’un client : pouvoir utiliser tous les canaux PWM d’un arduino Uno, avec de la puissance.
Vous l’aurez deviné, un petit mosfet piloté par le PWM et le tour est (presque) joué. On y ajoute une diode de flyback (si on veut pouvoir y connecter par la suite des charges inductives), et des connecteurs, le tour est joué.

Shield PWM Arduino

Shield PWM Arduino

Bien sûr, il ne faut pas oublier l’alimentation de l’arduino (on utilisera un 7805 tout bête pour ça, accompagné d’un condensateur pour le lissage), ainsi qu’une petite diode signalant que le montage est en fonctionnement.
La charge que vous pourrez connecter à ce montage dépendra directement des mosfets choisis : 100v jusqu’à 9A dans mon cas, avec des IRF520. (Bon, en vrai les pistes du PCB ne devraient tenir que jusqu’à 4 ampères environ)
L’intérêt de ce montage, outre le fait de pouvoir piloter des moteurs CC, est de piloter les guirlandes de led RGB. En effet, en utilisant 3 cannaux, vous pilotez chaque composante de votre ruban de led, et la puissance disponible permet d’alimenter des rubans de grande longueur.

 

Shield PWM avec led RGB

Shield PWM avec led RGB

Vendredi, mai 17 2013

DIY – Thermomètre à tube Nixie

Ce projet, qui m’aura occupé quelques temps, est parti de trois points :

- Je n’avais pas de thermomètre chez moi, et ma femme et moi n’avons pas tout à fait la même sensibilité à la température, cela permet de donner une valeur objective et d’ajuster en conséquence (soit on met le chauffage, soit l’autre enfile un pull ;) )
- J’avais besoin de tester un circuit de commutation pour tubes Nixie (spoiler : dans le but de réaliser une horloge), mais sur un nombre limité de tube, car en cas d’erreur, c’est très pénible de dessouder le tube et de le ressouder
- J’avais envie de réaliser un montage CMS le plus compact possible, et de tester au passage la mise en oeuvre de CMS taille 0402.

Si si, il y a un composant sur C6. La LED est une 5mm, placée là pour donner l'echelle.

Si si, il y a un composant sur C6. La LED est une 5mm, placée là pour donner l’échelle.

Le montage se divise donc en 4 parties : la mesure de la température, effectuée par un vénérable LM35 (mais le montage permet aussi l’utilisation d’un LM73 plus précis) ; l’élévation de tension pour alimenter les tubes, le contrôle des tubes, et le pilotage de tout ça, réalisé par un Atmega328, version cms évidemment.

Le schéma d'ensemble

Le schéma d’ensemble

La partie mesure de température ne nécessite pas d’explications particulières. A noter simplement que le LM73 fonctionne en I2C, et que dans ce cas, il faut impérativement mettre les résistances de pullup R6 et R7. Dans le cas du lm35, elles ne sont plus nécessaires car ce dernier fonctionne en analogique, la sortie de celui-ci étant à connecter à la broche 4 de l’emplacement du lm73 (A5/SCL sur l’atmega).

La partie élévateur de tension est désormais classique sur mon site, il s’agit de la même que pour mes compteurs geiger, à savoir NE555 + Mosfet + bobine. Un petit condensateur 400v sert à lisser la tension obtenue.

La mise en oeuvre de l’AtMega328 n’a rien de spécifique. Il faut en revanche noter deux connecteurs, un connecteur ISP, et un connecteur permettant de brancher un adaptateur série. Le premier devant servir à charger le bootloader Arduino sur l’Atmega, le second à charger le programme/débugger comme s’il s’agissait d’un simple Arduino. Pour une raison que j’ignore, bien que le bootloader soit correctement chargé, il n’a fonctionné que sur une seule de trois cartes que j’ai assemblé. Après tests, la communication série s’effectue correctement et dans les deux sens, mais impossible de flasher l’atmega par ce biais (si quelqu’un a une idée…). Du coups, la programmation se fait via ISP, et le debug par la connexion série.

La partie la plus intéressante de ce montage est la partie pilotage des tubes nixie. Un des objectifs était de réaliser le montage le plus compact possible, exit donc les drivers type 7441, tout sera fait ici à base de transistors.
Afin de ne pas trop consommer, l’affichage des 2 digits ne se fera pas simultanément, mais l’un après l’autre, de manière très rapide, la persistance rétinienne se chargeant de donner l’impression d’un affichage fixe.
Coté cathode, les transistors sont dans une configuration peu courante : la base est commune à tous les transistors, en permanence à +5v, ce qui permet de n’avoir qu’une seule résistance (mais qui impose de n’utiliser qu’un seul digit à la fois). La commutation se fait en ramenant l’émetteur du transistor voulu à  0v. Dans cette configuration, il faut autant d’entrées/sorties sur le microcontrolleur que de digits, mais en l’occurrence, l’Atmega nous en propose nettement plus que nécessaire dans notre cas.

Le driver coté anode

Le driver coté anode

Coté anode, il aurais été possible également de mettre un simple transistor NPN avec une résistance pour faire le travail. Cependant, la consommation « à vide » aurais été supérieure à la consommation lors de l’affichage sur un tube, ce qui n’est clairement pas le but recherché.
Le montage ci-dessus « coupe » le courant, en limitant les pertes à des valeurs infimes. La résistance R12 et le transistor NPN forment un driver de courant constant, réglé de manière à laisser passer juste le courant nécessaire au déblocage du transistor PNP.

Thermomètre Nixie

Le thermomètre Nixie assemblé

Le circuit complet tiens sur un PCB de 5x5cm double face. J’aurais probablement pu faire encore plus petit, mais ça me semblais déjà un bon début !

Le circuit vu du dessus

Le circuit vu du dessus

Concernant l’assemblage du PCB, rien de spécial à mentionner, celui-ci étant étonnamment plus facile à assembler que ce qu’il pourrais sembler au premier abord, et ce, malgré le fait que j’ai soudé des résistances 0805 sur des emplacements 0603 (donc un peu plus petits que les résistances). Ayant fait plusieurs essais, j’ai testé différentes techniques de soudure, je vous ferais un petit topo là-dessus dans un prochain article. Globalement, si on omet les 2 composants 0402 (taille qui n’était pas impérative du tout, mais pour faire des tests), ce n’est pas vraiment plus compliqué qu’avec du traversant, au contraire même.  Le circuit intégré demande un petit coups de main, mais ça se fais très bien, et très rapidement. Les 0402, pour le coups, sont assez délicat à placer, leur petite taille faisant qu’ils se collent à la pane du fer à souder par capillarité, et leur taille nécessite de bons yeux en plus d’une bonne loupe (idéalement, une bino)

Enfin, pour finir, le code source, qui n’a rien de très spécifique, il se contente de récupérer la valeur du lm35, et décomposer le résultat obtenu en deux digits, les unité et les dizaines.

thermometre

Lundi, avril 1 2013

DIY – Interrupteur télécommandé pour modélisme

Bon, si je me suis embêté à vous pondre une série d’articles sur la programmation AVR, c’est que j’avais une petite idée derrière la tête. En l’occurrence, j’avais besoin de pouvoir couper une alimentation embarquée dans un avion RC, bien sûr à distance.
L’idée était donc de réaliser un petit périphérique qui se connecte comme un servo-moteur standard de modélisme, mais qui active un relais en fonction de la valeur.

Déjà, il est important de savoir à quoi ressemble les signaux transmis par le récepteur RC aux servos :

Credit : http://nononux.free.fr

Credit : http://nononux.free.fr

Bon, il s’agit d’une sorte de PWM, mais avec un champs très limité car allant de 5 à 10% de la valeur. Dans mon cas, je ne suis intéressé que par 2 cas : On ou Off. Je décide donc de couper à 50%, de manière à pouvoir utiliser le manche des gaz en guise d’interrupteur : de 0 à 50% je suis Off, de 50% à 100% je suis On.
A partir de là, il y a plusieurs façons de voir les choses : il est possible de régler ça en analogique pur, mais j’avais peur que ce soit trop sensible aux éventuelles perturbations. (Et dans mon cas, je préférerais que ce ne soit pas sensible ;) ). Je suis donc partis sur la solution numérique pour traiter les impulsions.
Comme c’est pour embarquer dans un avion RC, il ne faut pas que ce soit lourd, donc j’ai choisi le micro-controlleur le plus petit possible, à savoir l’AtTiny85, que vous devez bien connaître désormais (voir ici, ici et ici), et qui plus est dans sa version CMS.

Le schéma

Le sch

Comme vous pouvez le constater sur le schéma ci-dessus, la partie électronique est réduite au strict minimum : Un régulateur de tension, pour fournir le 5v nécessaire à tout l’appareillage (servos, récepteur, etc…) dans le cas où l’on ne dispose pas de BEC (par exemple s’il n’y a pas de moteur). L’AtTiny85 dans sa configuration la plus simple (oscillateur interne), un mosfet pour piloter le relais, et le relais en lui même, petit relais 5v, capable de couper 175W quand même.
Le circuit a été réalisé sur un PCB de 0.8mm d’épaisseur, de manière, là encore à gagner du poids.

Vous l’aurez donc compris, dans ce montage, c’est le code qui fait tout le travail :

#include <avr/io.h>
#include <avr/interrupt.h>

//	   ___
//  PB5  *|+  |*  VCC
//  PB3  *|   |*  PB2
//  PB4  *|   |*  PB1   --> declenchement relais
//  GND  *|___|*  PB0   --> entrée PWM
//

volatile int count = 0; //le rapport cyclique
volatile int toff = 0;  //durée du signal à 0
volatile int ton = 0;	//durée du signal haut

int main(void)
{
	//configuration de la pin de sortie
	DDRB |= (1 << PORTB1); 	//on configure PB1 en tant que sortie
				//DDRB = Port B Data Direction Register

	//configuration du timer1 (Ton)
	TCCR0B |= (1 << CS00) | (1<<CS02); 	//Set up timer1 with prescaler 1/1024


	//configuration des interruptions
	GIMSK |= (1 << PCIE); 	//Enable pin change interrupt for PORTB 
				//GIMSK = General Interrupt Mask Register
				//PCIE = Pin Change Interrupt Enable

	PCMSK = (1 << PB0);  	//Enable pin change interrupt for PB0 (pcint0)
				//PCMSK = Pin Change Mask Register

	sei(); //mise en place des interrupts (set global interrupts)

	for(;;)
	{
		
		
		if(count>=7.5) //Si le rapport cyclique est > à 7 (Ton ~1.5ms)
		{
			PORTB |= (1 << PB1);
		}
		else
		{
			PORTB &= ~(1<<PB1);
		}
	}
}


ISR (PCINT0_vect) { //vecteur d'interruption
	if (PINB & (1<<PB0)) // detection de front montant
	{  
		
		toff = TCNT0; 	//Enregistrement de la valeur du timer Toff
		TCNT0 = 0;	//Réinitialisation du timer
	}
	else //detection de front descendant
	{
		ton = TCNT0;	//Enregistrement de la valeur du timer Ton
		TCNT0 = 0;	//Réinitialisation du timer
		if(toff)	//si on a déjà une valeur pour Toff
		{
			count = (ton*100L)/(ton+toff); //Le rapport cyclique = ton/(Ton+toff)
		}		
	}
} 

Voilà, il ne reste donc plus qu’à assembler tout ça, et à tester :

Le montage avec une led pour tester.

Le montage avec une led pour tester.

La chaîne complète, avec le récepteur

La chaîne complète, avec le récepteur

Et histoire de vérifier que tout fonctionne, une petite vidéo :


media

Voilà, un nouvel article prochainement pour vous faire voir l’utilisation réelle du bidule ;)
Ps : c’est un kit que vous retrouverez sur la boutique

Dimanche, mars 17 2013

Programmation Avr, dernière partie

Ok, trois articles sur le sujet ça peut paraître court, mais ça constitue déjà une bonne introduction, qui devrais vous permettre d’envisager la suite par vous-même. Nous allons aujourd’hui nous pencher sur un autre élément essentiel de la programmation avr :

Les interruptions

Imaginez que vous êtes en train de souder un circuit quand tout à coups la sonnette de votre porte d’entrée résonne. Vous pouvez arrêter ce que vous étiez en train de faire (mais rien ne vous y oblige), aller répondre, et revenir à vos soudures. Et c’est exactement comme ça que se passe une interruption dans le monde informatique, ici représentée par la sonnette.

Les interruptions peuvent être matérielles (changement d’état d’une broche, timer qui arrive à une certaine valeur), ou logicielles, et le nombre d’interruption disponibles dépend du modèle d’avr.
Les différentes interruptions disponibles sur votre microcontrolleur sont visibles sur la table des vecteurs d’interruptions de la datasheet (ici, je suis toujours sur l’Attiny85)
Interrupt vectors

Vecteurs d’interruption

Lorsqu’une interruption se produit, l’avr stoppe ce qu’il était en train de faire pour exécuter  la fonction que vous souhaitiez rattacher à cette interruption. Pour ce faire, il utilise une table des vecteurs d’interruption, positionnée au début de sa mémoire flash, afin de faire la correspondance Interruption <–> fonction.

Afin d’utiliser une interruption sur notre avr, nous avons besoin de faire 3 choses :

  • Positionner le bit Enable Interrupt (en général avec la fonction sei(), set global interrupt, mais peut aussi être positionné à la main)
  • Positionner les bits de chaque interruption.
  • et enfin remplir la condition de l’interruption.

Comme d’habitude, nous allons voir ensemble un petit exemple. Ce programme servira à compter les impulsions reçues sur la pin 5 de l’Attiny85. Lorsqu’il arrivera à 200 ou plus, il allumera une led sur la pin 6. Les impulsions pourront être crées avec un bouton poussoir (attention au debounce) ou un générateur de signal.

#include <avr/io.h>
#include <avr/interrupt.h>

//	   ___
//  PB5  *|+  |*  VCC
//  PB3  *|   |*  PB2
//  PB4  *|   |*  PB1   --> sortie led
//  GND  *|___|*  PB0   --> entrée surveillée
//

volatile int count = 0; //compte des interrupts


int main(void)
{
	//configuration de la pin de sortie
	//configuration des interruptions
		//Positionnement du Global Interrupt MaSK register
		//Positionnement du Pin Change Mask Register
		//mise en place des interrupts (set global interrupts)

	for(;;)
	{
		
		
		//Si on a eu 200 impulsions ou plus
		{
			//on alume la led
		}
		//sinon
		{
			//On éteind la led
		}
	}
}


ISR (PCINT0_vect) { //vecteur d'interruption
	// detection de front montant
	{  
		//on ajoute 1 au décompte
	}
	//detection de front descendant
	{
		//ici on ne fais rien, juste pour l'exemple...
	}
} 

Voilà, déjà, on peut remarquer en fin de code la façon dont est définie le vecteur d’interruption. Un autre détail à remarquer est la déclaration de ma variable count. Cette variable étant utilisée à la fois par mon programme principal et par ma fonction, il est impératif de la déclarer en volatil, sous peine de ne jamais la voir s’incrémenter.
Voyons voir maintenant comment déclarer nos interruptions (encore une fois, les valeurs des bits sont tirés de la datasheet, cf page 53, chapitre 9.3.2) :

#include <avr/io.h>
#include <avr/interrupt.h>

//	   ___
//  PB5  *|+  |*  VCC
//  PB3  *|   |*  PB2
//  PB4  *|   |*  PB1   --> sortie led
//  GND  *|___|*  PB0   --> entrée surveillée
//

volatile int count = 0; //compte des interrupts


int main(void)
{
	//configuration de la pin de sortie
	//configuration des interruptions
		//Positionnement du Global Interrupt MaSK register
                GIMSK |= (1 << PCIE); 	//Enable pin change interrupt for PORTB 
		    		        //GIMSK = General Interrupt Mask Register
				        //PCIE = Pin Change Interrupt Enable
		//Positionnement du Pin Change Mask Register
                PCMSK = (1 << PB0);  	//Enable pin change interrupt for PB0 (pcint0)
		  		        //PCMSK = Pin Change Mask Register
		//mise en place des interrupts (set global interrupts)
                sei();

	for(;;)
	{
		
		
		//Si on a eu 200 impulsions ou plus
		{
			//on alume la led
		}
		//sinon
		{
			//On éteind la led
		}
	}
}


ISR (PCINT0_vect) { //vecteur d'interruption
	// detection de front montant
	{  
		//on ajoute 1 au décompte
	}
	//detection de front descendant
	{
		//ici on ne fais rien, juste pour l'exemple...
	}
} 

Quelques petites explications complémentaires s’imposent ici. J’ai décidé de dédier une pin de mon Attiny85 à la surveillance du signal d’entrée, j’ai donc utilisé les Pin Change Interrupt. Mais si j’avais voulu utiliser ma broche pour d’autres choses en parallèle, j’aurais du utiliser les External Interrupt Request (donc positionner le bit INT0 au lieu de PCIE). Il faut également noter que par défaut, une interruption ne peut en interrompre une autre (comprendre : les interruptions sont désactivées le temps du traitement du vecteur d’interruption actuel). Il est cependant possible (mais pas franchement recommandé) de les réactiver en réutilisant sei() à l’intérieur de la déclaration du vecteur d’interruption. De la même manière, si vous souhaitez qu’une portion de votre code ne soit interrompue sous aucun prétexte, vous pouvez utiliser la fonction cei().
Voici maintenant le code complet :

#include <avr/io.h>
#include <avr/interrupt.h>

//	   ___
//  PB5  *|+  |*  VCC
//  PB3  *|   |*  PB2
//  PB4  *|   |*  PB1 
//  GND  *|___|*  PB0   --> entrée surveillée
//

volatile int count = 0; //compte des interrupts


int main(void)
{
	//configuration de la pin de sortie
	DDRB |= (1 << PORTB1); 	//on configure PB1 en tant que sortie
				//DDRB = Port B Data Direction Register

	//configuration des interruptions
	GIMSK |= (1 << PCIE); 	//Enable pin change interrupt for PORTB 
				//GIMSK = General Interrupt Mask Register
				//PCIE = Pin Change Interrupt Enable

	PCMSK = (1 << PB0);  	//Enable pin change interrupt for PB0 (pcint0)
				//PCMSK = Pin Change Mask Register

	sei(); //mise en place des interrupts (set global interrupts)

	for(;;)
	{
		
		
		if(count>=200) //Si on a eu 200 impulsions ou plus
		{
			PORTB |= (1 << PB1);
		}
		else
		{
			PORTB &= ~(1<<PB1);
		}
	}
}


ISR (PCINT0_vect) { //vecteur d'interruption
	if (PINB & (1<<PB0)) // detection de front montant
	{  
		count = count++;
	}
	else //detection de front descendant
	{
		//ici on ne fais rien, juste pour l'exemple...
	}
} 

Bon, passons sur les modifications d’état de la led, déjà abordées auparavant. Il est ici intéressant de noter comment se fait la détection d’un front montant ou descendant : à la suite d’un changement d’état, on lit l’état de la pin PB0, si elle est à l’état haut c’était un front montant, sinon, un front descendant.
Voilà, c’était le dernier article de cette série, qui sera suivi très bientôt d’une application concrète :)

Lundi, janvier 21 2013

Programmation Avr, seconde partie

Bon, j’espère que mon précédent billet vous aura permis de comprendre un peu comment fonctionnait la programmation avr.Il reste néanmoins encore de nombreux points qui pourraient êtres abordés, je voulais vous en présenter encore au moins 2 : les timers et les interruptions. Une fois ces deux points maîtrisés, vous serez capable déjà pas mal de petites choses avec vos microcontrolleurs.

Aujourd’hui : Les timers

Comme son nom peut vous laisser le deviner, un timer sert à comptabiliser le temps écoulé. Son principal intéret réside dans le fait que son exécution se déroule en parallèle du programme principal. Votre timer ne sera donc pas affecté par le nombre de d’instructions de votre programme.
Il est bien sur possible pour le programme principal d’interroger le timer, mais il est également possible de configurer le timer pour renvoyer une valeur directement sur une pin matérielle.
Avant d’aller plus loin sur la mise en place d’un timer, il nous faut aborder la notion de pré-scaller. En effet, si notre timer fonctionnait uniquement à la vitesse de l’horloge, nous serions assez rapidement embêté : à 1MHz, par exemple, un signal de 1mS correspondrait à 1000 cycles d’horloge, et pour un timer 8 bit tel que celui de notre ATTiny85, correspondrait à plus de 3 débordements de valeur. Autant dire que ce n’est pas très pratique.
C’est là qu’intervient le prescaler : il permet de faire fonctionner le timer à un sous-multiple de la fréquence d’origine, en divisant la fréquence par 8, 64, 256, 1024. Bien sur, en utilisant le préscaler, on diminue la résolution, il faut donc l’utiliser à bon escient.

Voyons maintenant ça en pratique : faire clignoter une led, mais sans utiliser les fonctions _delay_ms. La led est connectée sur le port PB1 (pin 6 de l’Attiny85). On veut faire changer l’état de notre Led dix fois par seconde. A 1MHz, il faudrait compter jusqu’à…. 100000, ce qui n’est pas directement possible avec notre registre 8 bits. Nous allons donc utiliser le prescaler, en divisant l’horloge par 1024 : 100000/1024 = 97.65, donc ça rentre.

# include <avr/io.h>
int main ( void )
{
DDRB |= (1 &lt;&lt; PORTB1) ; //On configure PB1 en tant que sortie
//Todo : Configuration du timer avec prescaler de 1024
    for(;;)
    [
      if()//On vérifie si le timer a atteint un dixieme de seconde
      {
       PORTB ^= (1 &lt;&lt; 0) ;//On inverse l'état de la led
       //on réinitialise le timer
       }
    }
}

TCCR0B

La datasheet nous indique que les bits permettant de régler le timer sont les bits CS00 à CS02, le tableau suivant nous expliquant les valeurs à donner en fonction de la configuration voulue.
Clksrc

Dans notre cas, nous voulons un prescaler de 1/64, il faut donc passer les bits CS00 et CS01 à 1, ce qui nous donne :

#include <avr/io.h>
int main ( void )
{
DDRB |= (1 &lt;&lt; PORTB1) ; //On configure PB1 en tant que sortie
TCCR0B |= (1 &lt;&lt; CS00) | (1&lt;&lt;CS02); //Set up timer1 with prescaler 1/1024
    for(;;)
    [
      if()//On vérifie si le timer a atteint un dixieme de seconde
      {
       PORTB ^= (1 &lt;&lt; 0) ;//On inverse l'état de la led
       //on réinitialise le timer
       }
    }
}

Il ne nous reste plus qu’à lire notre timer, pour savoir s’il dépasse 97, et, si c’est le cas, inverser l’état de notre led, et réinitialiser le timer. Ces opérations se font via le Time Counter Register, à savoit TCNT0 sur notre ATTiny85 (il peut y en avoir plus d’un selon votre microcontrolleur). Selon la datasheet toujours, ce registre est accessible en lecture/écriture, ce qui nous permettra la lecture, mais aussi sa remise à Zero. Voici ce que nous donne le code :

#include <avr/io.h>

int main ( void )
{
DDRB |= (1 &lt;&lt; PORTB1) ; //On configure PB1 en tant que sortie
TCCR0B |= (1 &lt;&lt; CS00) | (1&lt;&lt;CS01); //Set up timer1 with prescaler 1/1024
    for(;;)
    [
      if(TCNT0&gt;=97)//On vérifie si le timer a atteint un dixieme de seconde
      {
       PORTB ^= (1 &lt;&lt; 0) ;//On inverse l'état de la led
       TCNT0 = 0;//on réinitialise le timer
       }
    }
}

A bientôt pour la dernière session : les interruptions.

–edit–
L’article a été mis à jour suite à des erreurs qui m’avaient été signalé. N’hésitez pas si vous en trouvez d’autres ;)

Jeudi, janvier 3 2013

Programmation AVR, premiers pas, partie 1

Je programme occasionnellement sur avr (comprendre autre qu’arduino quoi), mais comme c’est très espacé dans le temps, à chaque fois j’ai l’impression de reprendre depuis le début. Du coups, je me suis dit que ça pourrais être bien de faire un petit papier là-dessus, en expliquant un peu les points qui m’ont posé problème à un moment ou un autre. Je ne suis pas un très bon programmeur, donc il y aura peut être des erreurs ou imprécisions, n’hésitez pas à m’en faire part dans les commentaires si c’était le cas :)

On va commencer par un truc simple  : un ATtiny85, une LED, une résistance, et on veut faire clignoter notre led :) . Déjà, premier truc à savoir, c’est qu’il y a plein de façons différentes de le faire, alors on va commencer par du « haut niveau », et on ira voir un peu plus loin ensuite.

attiny85

Brochage de l’attiny85

Le schéma nous indique les différents ports E/S. On constate que plusieurs fonctionnalités leur sont attribués, le choix des pins utilisés se fera en fonction de ces fonctionnalités. Dans notre exemple, on peut utiliser n’importe quelle E/S, mais si par exemple on devais gérer une interruption matérielle, elle se ferait obligatoirement sur les broches PB0, PB1 ou PB2 (PCINT0, PCINT1 et PCIN2T).
Pour notre exemple, on va choisir d’utiliser la pin 5 de l’AtTiny85, nommée PB0. Voici la structure de notre programme

#include <avr/io.h>

int main(void)
{
//initialisation des entrées/sorties

  for(;;) //la boucle principale de notre programme
  {
    //on allume la led
    //on attend quelques millisecondes
    //on eteind la led
    //on attend quelques millisecondes
  }
}

Voilà, la structure du programme ne devrait pas poser de problème particulier à quelqu’un qui a déjà vu au moins une fois dans sa vie un programme en C. La suite en revanche peut en dérouter plus d’un, si vous n’êtes pas habitués à ce genre de syntaxes. Petites explications :
Afin de configurer une broche en sortie, il faut affecter la bonne valeur dans le bon registre ;) Le registre configurant la « direction » des données s’appelle DDRB, pour Port B Data Direction Register.

Le registre DDRB

Le registre DDRB

Par défaut, la valeur des bits de ce registre est à 0, ce qui signifie qu’il est accessible en lecture. Pour le passer en écriture, il suffit de mettre à 1 le bit correspondant. Vous pouvez le faire simplement en tapant :

DDRB = 0x01;//configure le port PB0 en sortie

Mais vous pouvez également tomber sur une syntaxe différente qui peut paraître bizarre au premier abord:

DDRB = 1<<PB0;

Celà veut simplement dire que l’on décale le 1 de x vers la gauche, x étant la valeur de PB0 dans mon exemple.
Imaginons que l’on veuille configurer le port PB5 en sortie, il faudrait selon la datasheet que DDRB = 0×00100000. Nous partons donc d’un DDRB qui vaut 0×00000000.On lui fait subir un premier décalage du bit 1 vers la gauche, on a maintenant DDRB = 0×00000001   (équivalent à 1<<0)
On recommence le décalage et on obtiens DDRB=0×00000010 (équivalent à 1<<1)

On recommence le décalage et on obtiens DDRB=0×00100000 (équivalent à 1<<5), on a donc bien réglé notre port PB5 en tant que sortie.

On peut donc maintenant mettre notre programme à jour, on obtiens donc :

#include <avr/io.h>

int main(void)
{
//initialisation des entrées/sorties
DDRB |= 1<<PB0;
  for(;;) //la boucle principale de notre programme
  {
    //on allume la led
    //on attend quelques millisecondes
    //on eteind la led
    //on attend quelques millisecondes
  }
}

Tant qu’on est dans les calculs binaires, il y a d’autres syntaxes que vous serez amené à croiser, par exemple :

GIMSK |= (1 << PCIE);

Il s’agit là de l’utilisation d’un masque, permettant de ne modifier que le bit qui nous intéresse (via l’utilisation d’un OU binaire). Les opérateurs binaires permettent d’améliorer grandement les performances d’un code, vous trouverez de plus amples explications sur ce sujet ici

Une fois que notre pin à été correctement configurée, il suffit de passer le bit correspondant à 1 pour la faire basculer à l’état 1, et inversement. Pour se faire, il faut écrire le bit correspondant à notre pin dans le registre PORTB. Là encore, c’est la datasheet qui nous aide, en nous fournissant le tableau de correspondances :
PORTB

On peut maintenant compléter notre code :

#include <avr/io.h>

int main(void)
{
//initialisation des entrées/sorties
DDRB |= 1<<PB0;
  for(;;) //la boucle principale de notre programme
  {
    PORTB |= (1<<PB0);//on allume la led
    //on attend quelques millisecondes
    PORTB &= ~(1 << PB0);//on eteind la led
    //on attend quelques millisecondes
  }
}

Voilà, le plus gros est fait, il ne reste plus qu’à gérer les délais. Pour ce premier tutoriel, on ne va pas s’embêter, on utilisera la fonction prévue pour, fournie avec util/delay.h : _delay_ms()
Comme son nom l’indique elle fait attendre le programme pendant un nombre donné de millisecondes. Dans une prochaine version, nous verrons comment utiliser un timer à la place. On dispose maintenant de tout le nécessaire pour terminer notre programme :

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
//initialisation des entrées/sorties
DDRB |= 1<<PB0;
  for(;;) //la boucle principale de notre programme
  {
    PORTB |= (1<<PB0);//on allume la led
    _delay_ms(500);//on attend quelques millisecondes
    PORTB &= ~(1 << PB0);//on eteind la led
    _delay_ms(500);//on attend quelques millisecondes
  }
}

Voilà, pour utiliser notre programme, il reste encore à le compiler et à l’uploader sur le microcontrolleur. Je ne vais pas détailler cette section, elle est déjà largement documentée sur le net (et facilement compréhensible à mon avis). En revanche, je vais vous laisser un fichier Makefile bien pratique, qui permet de gérer la compilation ET l’upload, ainsi que tout un tas d’autres choses, bref, un vrai couteau suisse. (Le Makefile n’est pas de moi, mais je n’arrive pas à en retrouver la source, désolé)