Oscilloscope

Introduction

Premier montage
      Câblage
      Codes pour générer les signaux

L'oscilloscope
      Version "basique"
      Version "à fréquence maximale"
      Version "à temps contrôlé"

Premiers résultats
      Observations
      Courbes obtenues

Montée en fréquence
      Signal rectangulaire
      Signal MLI ou PWM

Autres modèles d'oscilloscopes
      Mesures simultanées
      Mesures en temps réel

Premier bilan

Repousser les limites
      Un octet par mesure
      Réglage du CAN
      En pratique

Introduction

L'objectif de ce projet est :

Premier montage

Câblage

Les premiers essais sont effectués en basse fréquence : 0,5 Hz. Pour tester l'oscilloscope, un générateur de signaux rectangulaire ou sinusoïdal est bien utile. Ce n'est pas évident de réaliser un tel générateur de signaux, avec une carte Arduino. Par contre, il est facile de faire varier l'intensité lumineuse d'une DEL. De couleur blanche, placée a proximité immédiate d'une photorésistance, elle permettra de recueillir un signal à afficher sur l'ordinateur. Le montage est positionné dans un endroit sombre de préférence.

On utilise donc deux cartes Arduino. Une carte fait varier l'intensité lumineuse d'une DEL. L'autre carte récupère le signal pour l'envoyer à l'ordinateur. Un programme en Python affiche les données du signal.

Certains numéros associés aux connecteurs d'une carte Arduino sont précédés du signe "~". Cela signifie qu'ils peuvent fonctionner selon deux modes :

C'est ce type de connecteur qui est choisi. Le câblage est le suivant :

La résistance qui protège la DEL ne doit pas avoir une valeur trop importante, pour éviter de limiter son intensité lumineuse. Les données sont les suivantes :

La résistance minimale se calcule avec la loi d'Ohm :

Codes pour générer les signaux

Signal rectangulaire :

#define DEL 6

void setup() {
  pinMode(DEL, OUTPUT);
}
void loop() {
  digitalWrite(DEL, HIGH);
  delay(500);
  digitalWrite(DEL, LOW);
  delay(1500);
}

Signal sinusoïdal :

#define DEL 6
float periode=2;

void setup() {
  pinMode(DEL, OUTPUT);
}
void loop() {
  for (float t=0; t<periode; t=t+0.01) {
    analogWrite(DEL, (int)(127.5+127.5*sin(t*2*PI/periode)));
    delay(10);
  }
}

L'oscilloscope

Version "basique"

Code sur la carte Arduino

Le signal lu (0 à 1023) est envoyé à l'ordinateur toutes les 0,05 s.

void setup() {  
  Serial.begin(115200); 
}
void loop() {
  Serial.println(analogRead(0));
  delay(50);
}

Tracé sur une durée choisie

La bibliothèque matplotlib est mise à contribution. La cinquième ligne du programme est à adapter selon le nom du port USB. Les valeurs sont lues toutes les 0,05 s, pendant 8 s.

#!/usr/bin/python3
import time
import serial
import matplotlib.pyplot as plt
portUSB=serial.Serial('/dev/ttyUSB0', 115200)

temps=[]
tension=[]
duree=8
t=0
dt=0.05

while (t<duree) :
  temps.append(t)
  tension.append(0.004887*float(portUSB.readline()))
  t=t+dt
  time.sleep(dt)

plt.figure("Oscilloscope")
plt.xlabel("Temps en s")
plt.ylabel("Tension en V")
plt.grid(True)
plt.plot(temps,tension)
plt.show()

Version "à fréquence maximale"

Code sur la carte Arduino

Le nombre maximal de mesures par unité de temps s'obtient... avec le code le plus simple!

void setup() {  
  Serial.begin(115200); 
}
void loop() {
  Serial.println(analogRead(0));
}

Tracé sur une durée choisie

Le code est presque identique au précédent. Le temps dt entre deux mesures est déterminé empiriquement. La première valeur envoyée à l'ordinateur engendre parfois une erreur, ne pouvant être convertie en nombre. Ce problème est ici corrigé.

#!/usr/bin/python3
import serial
import matplotlib.pyplot as plt
portUSB=serial.Serial('/dev/ttyUSB0', 115200)

temps=[]
tension=[]
duree=8
t=0
dt=0.000422

while (t<duree) :
  try :
    temps.append(t)
    tension.append(0.004887*float(portUSB.readline()))
    t=t+dt
  except :
    pass

plt.figure("Oscilloscope")
plt.xlabel("Temps en s")
plt.ylabel("Tension en V")
plt.grid(True)
plt.plot(temps, tension)
plt.show()

Version "à temps contrôlé"

Code sur la carte Arduino

Le temps s'obtient avec la fonction micros(). La fonction millis() est utilisable, mais les résultats obtenus sont moins bons lorsque la fréquence du signal à mesurer augmente.

void setup() {  
  Serial.begin(115200); 
}
void loop() {
  Serial.print(micros());
  Serial.print(" ");
  Serial.println(analogRead(0));
}

Tracé sur une durée choisie

Le code ressemble au précédent. La première valeur envoyée à l'ordinateur engendre parfois une erreur. Quand ce n'est pas le cas, elle est parfois fausse. La première valeur lue n'est donc pas traitée. Une première valeur de temps est crée artificiellement pour permettre à la boucle de fonctionner. Elle est détruite une fois l'ensemble des valeurs récupérées.

#!/usr/bin/python3
import serial
import matplotlib.pyplot as plt
portUSB=serial.Serial('/dev/ttyUSB0', 115200)

temps=[]
tension=[]
duree=0.05

lu=portUSB.readline()
temps.append(0)
while (temps[-1]<duree) :
  lu=portUSB.readline().split()
  temps.append(0.000001*float(lu[0]))
  tension.append(0.004887*float(lu[1]))
del temps[0]

plt.figure("Oscilloscope")
plt.xlabel("Temps en s")
plt.ylabel("Tension en V")
plt.grid(True)
plt.plot(temps, tension)
plt.show()

Premiers résultats

Observations

Les essais

Les premiers essais, effectués en basse fréquence, engendrent des courbes similaires avec les trois types d'oscilloscope. On remarque que la résistance de la photorésistance :

Les oscilloscopes

Remarque

Dans la troisième version de l'oscilloscope, le temps est contrôlé par la carte Arduino. Cependant, il est aussi possible de le contrôler par l'ordinateur, avec le code ci-dessous. Le Python n'est pas assez rapide, les essais ont montré des résultats... moins réguliers.

import time
...
debut=time.time()
while (temps[-1]<duree) :
    temps.append(time.time()-debut)
...

Courbes obtenues

Résultat avec le signal rectangulaire :

Et avec le signal sinusoïdal :

Montée en fréquence

Signal rectangulaire

Un signal rectangulaire est crée et lu directement, avec la troisième version de l'oscilloscope, sans passer par la photorésistance. Les résultats obtenus sont corrects jusqu'à une fréquence de 100 Hz environ (les 8 s sont remplacés par 0,05 s).

Code pour obtenir un signal rectangulaire à 100 Hz :

#define DEL 6

void setup() {
  pinMode(DEL, OUTPUT);
}
void loop() {
  digitalWrite(DEL, HIGH);
  delay(5);
  digitalWrite(DEL, LOW);
  delay(5);
}

Résultat obtenu :

Signal MLI ou PWM

Un signal MLI (modulation de largeur d'impulsions) ou, en anglais, PWM (pulse width modulation) permet de commander un servomoteur. L’intervalle de temps entre chaque impulsion, autrement dit la période, est généralement de 20 ms. Les impulsions ont une durée de 1 ms pour la position angulaire 0°, et une durée de 2 ms pour la position angulaire 180°. On est à la limite de ce que peut visualiser l'oscilloscope.

Code pour générer le signal MLI :

#include <Servo.h>
Servo leServo;

void setup() {
  leServo.attach(6);
}
void loop() {
  leServo.write(70); // Position angulaire de 70°
}

Résultat obtenu pour la position 180° :

Et la position 0° :

Autres modèles d'oscilloscopes

Mesures simultanées

Une carte Arduino possède six entrées analogiques. Il est donc possible d'effectuer six mesures en même temps. On va ici relever quatre tensions qui correspondent à la commande d'un moteur pas à pas.

Code qui génère les signaux

// Numéros des connecteurs
#define C1 8
#define C2 9
#define C3 10
#define C4 11
// Paramètres du moteur pas à pas
#define nbPas 20
#define vitesseRotation 250
// Objet associé au moteur
#include <Stepper.h>
Stepper moteur(nbPas, C1, C2, C3, C4);

void setup() {
  pinMode(C1, OUTPUT);
  pinMode(C2, OUTPUT);
  pinMode(C3, OUTPUT);
  pinMode(C4, OUTPUT);
  moteur.setSpeed(vitesseRotation);
}
void loop() {
  moteur.step(20000);
}

Code adapté à la mesure de quatre tensions

void setup() {  
  Serial.begin(115200); 
}
void loop() {
  Serial.print(micros());
  Serial.print(" ");
  Serial.print(analogRead(0));
  Serial.print(" ");
  Serial.print(analogRead(1));
  Serial.print(" ");
  Serial.print(analogRead(2));
  Serial.print(" ");
  Serial.println(analogRead(3));
}

Code adapté à l'affichage des quatre tensions

#!/usr/bin/python3
import serial
import matplotlib.pyplot as plt
portUSB=serial.Serial('/dev/ttyUSB0', 115200)

tension1=[]
tension2=[]
tension3=[]
tension4=[]
temps=[]
duree=0.05

lu=portUSB.readline()
temps.append(0)
while (temps[-1]<duree) :
  lu=portUSB.readline().split()
  temps.append(0.000001*float(lu[0]))
  tension1.append(0.004887*float(lu[1]))
  tension2.append(0.004887*float(lu[2]))
  tension3.append(0.004887*float(lu[3]))
  tension4.append(0.004887*float(lu[4]))
del temps[0]

plt.figure("Oscilloscope")
plt.xlabel("Temps en s")
plt.ylabel("Tension en V")
plt.grid(True)
plt.plot(temps, tension1, color='#2277bb') # Bleu
plt.plot(temps, tension2, color='#ff7700') # Orange
plt.plot(temps, tension3, color='#22aa22') # Vert
plt.plot(temps, tension4, color='#dd2222') # Rouge
plt.show()

Courbes obtenues

Mesures en temps réel

Il s'agit de créer une animation montrant l'évolution de la tension en fonction du temps. Cette animation peut se présenter de multiples manières.

En théorie, les données devraient s'afficher au fur et à mesure qu'elles arrivent. En pratique, la mise à jour de l'affichage prend du temps, il est difficile de produire une animation fluide avec un tracé précis. L'idée est de récupérer plusieurs valeurs entre entre deux images consécutives, en cherchant le meilleur compromis.

Coté Arduino, la tension est envoyée dix fois par seconde :

void setup() {  
  Serial.begin(115200); 
}
void loop() {
  Serial.print(micros());
  Serial.print(" ");
  Serial.println(analogRead(0));
  delay(100);
}

Dans le code ci-dessous, la première lecture n'est pas prise en compte. La deuxième lecture permet à la boucle de fonctionner. Ensuite, deux lectures sont faites avant chaque mise à jour de l'affichage. Les essais ont montré une animation de cinq images par seconde environ.

#!/usr/bin/python3
import serial
import matplotlib.pyplot as plt
portUSB=serial.Serial('/dev/ttyUSB0', 115200)

temps=[]
tension=[]
duree=25

plt.figure("Oscilloscope")

lu=portUSB.readline()
lu=portUSB.readline().split()
temps.append(0.000001*float(lu[0]))
tension.append(0.004887*float(lu[1]))
while (temps[-1]<duree) :
    lu=portUSB.readline().split()
    temps.append(0.000001*float(lu[0]))
    tension.append(0.004887*float(lu[1]))
    lu=portUSB.readline().split()
    temps.append(0.000001*float(lu[0]))
    tension.append(0.004887*float(lu[1]))
    plt.ylim(-0.2,5.2)
    plt.xlabel("Temps en s")
    plt.ylabel("Tension en V")
    plt.grid(True)
    plt.plot(temps, tension, color='#2277bb')
    plt.pause(0.000001)

Exemple de charges et décharges d'un condensateur (attention à la polarité) de 2200 μF ralenties par une résistance de 270 Ω :

Premier bilan

Ces oscilloscopes présentent des avantages et des limites.

Avantages

Limites

Référence sur
le connecteur
Tensions (V)
MinimaleMaximale
GND 0 5
3.3V-3,31,7
5V -5 0

Repousser les limites

Un octet par mesure

Au lieu d'envoyer 1 à 4 octets représentant les caractères allant de 0 à 9, suivis de 2 octets pour le retour chariot et le saut de ligne, la carte Arduino UNO peut aussi transmettre 2 octets désignant des nombres compris entre 0 et 1023... éventuellement suivis du saut de ligne. Et pour une plage de valeurs de 0 à 255 jugée satisfaisante, 1 seul octet par mesure suffit :

void setup() {
  Serial.begin(115200);
}
void loop() {
  Serial.write(analogRead(0)/4);
}

Pour cette dernière solution, le code en Python devient :

#!/usr/bin/python3
import serial
import matplotlib.pyplot as plt
portUSB=serial.Serial('/dev/ttyUSB0', 115200)

temps=[]
tension=[]
duree=0.05
t=0
dt=0.000112

while (t<duree) :
  tension.append(0.0196*portUSB.read()[0])
  temps.append(t)
  t=t+dt

plt.figure("Oscilloscope")
plt.xlabel("Temps en s")
plt.ylabel("Tension en V")
plt.grid(True)
plt.plot(temps,tension)
plt.show()

Le temps dt entre deux mesures, déterminé empiriquement, montre une nette augmentation de la fréquence : Environ 9000 mesures par seconde.

Réglage du CAN

Le CAN (convertisseur analogique-numérique) ou DAC en anglais (digital analog converter) fonctionne par cycles, pour des raisons technologiques. Ces cycles dépendent d'une horloge dont la fréquence peut être modifiée. En l'augmentant, on diminue le temps nécessaire pour effectuer une mesure. Le CAN, système relativement complexe, se configure avec 5 registres de 8 bits :

Les 3 bits ADPS2, ADPS1 et ADPS0 du registre ADCSRA déterminent le rapport entre la fréquence principale de l’Arduino et celle du CAN. Par défaut, ces 3 bits sont à 1.

ADPS2ADPS1ADPS0Rapport
111128
11064
10132
10016
0118
0104
0012
0002

Le code ci-dessous permet de connaître la valeur par défaut du registre ADCSRA. On obtient 135 soit 10000111 en notation binaire.

void setup() {
  Serial.begin(115200);
}

void loop() {
  Serial.println(ADCSRA);
  delay(1000);
}

Le code ci-dessous permet :

int lu;
unsigned long debut;

void setup() {
  Serial.begin(115200);
  ADCSRA=134;
}

void loop() {
  debut=millis();
  for (int i=0; i<1000; i++) {
    lu=analogRead(0);
  }
  Serial.print("Temps de lecture en µs : ");  
  Serial.println(millis()-debut);
  delay(1000);
}

Ce test a permis de trouver les valeurs suivantes :

ADCSRATemps de
lecture (µs)
DécimalBinaire
13510000111112 (parfois 111 ou 113)
1341000011056 (parfois 55 ou 57)
1331000010128 (parfois 27 ou 29)
1321000010015 (parfois 14 ou 16)
131100000118 (parfois 9)
130100000105 (parfois 4 ou 6)

En pratique

Le signal à lire, rectangulaire, s'obtient avec la fonction tone(). Par exemple, pour une fréquence de 1000 Hz :

void setup() {
  pinMode(6, OUTPUT);
  tone(6, 1000);
}
void loop() { }

En réglant la valeur de ADCSRA à 134 :

void setup() {
  Serial.begin(115200);
  ADCSRA=134;
}
void loop() {
  Serial.write(analogRead(0)/4);
}

En réglant dt à 0,000061 s et en effectuant des mesures sur 0,005 s :

#!/usr/bin/python3
import serial
import matplotlib.pyplot as plt
portUSB=serial.Serial('/dev/ttyUSB0', 115200)

temps=[]
tension=[]
duree=0.005
t=0
dt=0.000061

while (t<duree) :
  tension.append(0.0196*portUSB.read()[0])
  temps.append(t)
  t=t+dt

plt.figure("Oscilloscope")
plt.xlabel("Temps en s")
plt.ylabel("Tension en V")
plt.grid(True)
plt.plot(temps,tension)
plt.show()

On obtient un peu plus de 16000 mesures pas seconde :