Régulation PID, faire de jolis graphiques 2/2

La dernière fois, je vous ai présenté une méthode essentiellement graphique, permettant une bonne approximation des paramètres PID de votre régulateur. Encore faut-il savoir comment tracer les graphiques, c’est ce que je me propose de vous expliquer dans cet article. Il est bien sûr évident que cette méthode peut être utilisée pour tracer des graphiques à partir de tout type de données, du moment que les données arrivent via un port série.
La première étape va consister à récupérer les données du port série sur un PC. Comme souvent, il existe plusieurs façon de faire, je vous présenterais celle que j’ai utilisé ici : un petit script python.

import serial
import sys

serialport = serial.Serial("/dev/ttyACM0", 9600, timeout=1)
line = []

while True:
    for c in serialport.read():
        line.append(c)
        if c == '\n':
            for s in line:
                f=open('myfile','a')
                sys.stdout.write(s)
                f.write(s)
                f.close
            line = []
            break

serialport.close()

Ce petit script prend les caractères arrivant sur le port série, jusqu’au caractère marquant la fin de ligne, puis écrit la ligne dans un fichier. Ca peut éventuellement suffire pour tracer un graphique… Mais dans ce cas précis, on veut pouvoir également envoyer une consigne au régulateur, la modifier afin de suivre la réaction de ce dernier. Problème : comment envoyer des données sans perturber la lecture des données et ne pas sauter de mesure ?
Là encore, il y a plusieurs méthodes possibles, certaines plus complexes que d’autres. Celle que j’ai choisi permet de garder un programme simple, mais ne fonctionnera que sous linux. Ceux sous un autre système chercheront du côté du multithreading…

import serial
import sys
import select

serialport = serial.Serial("/dev/ttyACM0", 9600, timeout=1)
line = []

while True:
        while sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
          ligne = sys.stdin.readline()
          if ligne:
            print(ligne)
            serialport.write(ligne+'\r\n')
          else: # an empty line means stdin has been closed
            print('eof')
            exit(0)
        else:
            for c in serialport.read():
                    line.append(c)
                    if c == '\n':
                        for s in line:
                                f=open('myfile','a')
                                sys.stdout.write(s)
                                f.write(s)
                                f.close
                                line = []
                    break

serialport.close()

Ah oui, j’ouvre et ferme le fichier a chaque écriture, de manière à pouvoir accéder aux données pendant l’exécution du programme. Ca permet de visualiser l’évolution du graphique en temps réel.
Les données sont envoyées par le microcontrolleur sous la forme . Je n’ai pas de mesure de temps, chaque échantillon étant pris à un intervalle d’une seconde, il suffit de compter les échantillons pour connaître le temps écoulé.

Passons maintenant au graphique lui-même. Pour cela, j’utilise le logiciel Gnuplot, outil très puissant de tracé de courbes dont nous n’utiliseront ici qu’une infime quantité de ses possibilités. Lorsque vous lancez gnuplot en ligne de commande, vous vous retrouvez avec un shell dans lequel vous pourrez lancer vos commandes gnuplot.

plot 'myfile' using 1 smooth bezier title 'temperature', 
'myfile' using 2  with line title 'CO', 
'myfile' using 3 with line title 'Setpoint'
 plot 'myfile' using 1 

Indique que l’on va tracer une courbe correspondant aux données de la première colonne du fichier

smooth bezier

Indique que l’on veut lisser les données. Il n’est pas toujours intéressant de lisser les données, par exemple, ici la colonne 3 correspondant à la consigne n’est pas lissée, ce qui permet de visualiser le moment exact du changement de consigne. Par contre, dans le cas des valeurs mesurées, cela permet de s’affranchir du bruit sur les mesures.

title 'temperature'

Légende de la courbe. Pratique dès qu’il y a plus d’une courbe.

Par défaut, gnuplot va afficher le résultat à l’écran. Pour pouvoir enregistrer le résultat dans un fichier il faut taper les instructions suivantes :

set terminal "png"
set output "monfichier.png"
replot

Nota : avec la commande replot (ou avec le bouton replot de la gui), vous pouvez rafraîchir les données affichées, de manière à visualiser en continu vos données…
Il y a certainement plein d’amélioration possibles à ma méthode, mais je vous la présente car elle a le mérite d’être simple et rapide à mettre en oeuvre, tout en fournissant de bons résultats :)

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.

Continue reading

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

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 :)

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.

Continue reading