NB: Ce tuto est réalisé sur RPI zéro mais fonctionne de la même manière sur les autres plateformes RPI (avec les mêmes broches GPIO).

Aujourd'hui on va parler d'électronique et plus spécifiquement d'un capteur qui mesure les distances grâce à des ultrasons. Nous allons aussi voir comment étalonner ce capteur pour avoir des mesures plus proches de la réalité.

Principe de fonctionnement

On va faire un petit rappel ici, le HC-SR04 fonctionne en émettant une série de 8 impulsions à 40 Khz. Pour déclencher ces impulsions il faut mettre la broche "trig" à l'état haut pendant 10 microsecondes.
La distance est proportionnelle au temps à l'état haut de la broche "echo". Elle se calcule avec la formule suivante:

(temps à l'état haut de "echo" * vitesse du son) / 2

Et oui on divise par deux pour prendre en compte le fait que l'onde sonore fait un aller-retour.

Pour plus de détails vous pouvez vous référer à la datasheet.

NB: Le délai minimum entre 2 mesures doit être d'au moins 60 microsecondes pour être certain que les échos de la mesure précédente ne vont pas polluer la mesure en cours.

Mesurer la distance

On va donc faire un montage du capteur en utilisant un pont diviseur de tension qui abaissera la valeur de 5v de l'état haut de la broche "echo" à 3,3v (tension qui évitera de cramer notre carte Raspberry).

Pour les valeurs de résistance j'ai fait confiance au tutoriel officiel de Raspberry.

Source: https://tutorials-raspberrypi.com/raspberry-pi-ultrasonic-sensor-hc-sr04/

Le code de base

Pour commencer nous allons nous contenter de récupérer la distance sans étalonnage.

NB: Pour ne pas faire un programme trop gourmand en ressources on va utiliser une interruption. En effet utiliser une boucle while pour chronométrer le temps à l'état haut de la broche echo oblige le programme à être au premier plan durant toute l'attente du signal sur la broche echo.

# -*- coding: utf-8 -*-

##Libraries
import RPi.GPIO as GPIO
import sys
import time

#set GPIO Pins
GPIO_TRIGGER = int(sys.argv[1]) #18
GPIO_ECHO = int(sys.argv[2]) #24

#GPIO Mode (BOARD / BCM)
GPIO.setmode(GPIO.BCM)

#set GPIO direction (IN / OUT)
GPIO.setup(GPIO_TRIGGER, GPIO.OUT)
GPIO.setup(GPIO_ECHO, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

def distance():
    # set Trigger to HIGH
    GPIO.output(GPIO_TRIGGER, False)
    time.sleep(0.00001)

    GPIO.output(GPIO_TRIGGER, True)

    # set Trigger after 0.01ms to LOW
    time.sleep(0.00001)
    GPIO.output(GPIO_TRIGGER, False)

    StartTime = time.time()
    StopTime = time.time()

    timestat = time.time()
    # save StartTime
    while GPIO.input(GPIO_ECHO) == 0:
        StartTime = time.time()
        if StartTime - timestat > 0.06:
            print("TIMED OUT")
            break

    timestat = time.time()
    # save time of arrival
    while GPIO.input(GPIO_ECHO) == 1:
        #print("GPIO_INP_1")
        StopTime = time.time()
        if StopTime - timestat > (0.06):
            print("TIMED OUT")
            break

    # time difference between start and arrival
    TimeElapsed = StopTime - StartTime
    distance = (TimeElapsed * 34300) / 2

    return distance


if __name__ == '__main__':
    try:
        while True:
          dist = distance()
          print("Measured Distance = %.1f cm" % dist)
          # wait 60 ms before new mesurement
          time.sleep(0.06)

    # Reset by pressing CTRL + C
    except Exception as inst:
        print("Unexpected error:", sys.exc_info()[0])
        print(type(inst))
        print(inst.args)
        print(inst)
        print("Measurement stopped by User")
        GPIO.cleanup()

Et on lance le script avec cette commande

NB: les valeurs des broches de "trig" et "echo" dépendent de votre montage (ça coute rien de le préciser).

 python3 test_Simple_Read.py 18 24

Avec ce script on évite de bloquer le capteur dans un mode où il attendrait indéfiniment une réponse qui ne viendrait pas.

C'est pas mal pour un début mais comme on peut le constater, les valeurs de retour varies beaucoup... Ce n'est pas très précis.

Je vous entends déjà me dire, "il suffit de faire la moyenne de ces valeurs !" Et là je vous arrête de suite, ce n'est pas si simple.

Imaginons que je veuille mettre mon capteur de distance sur un mobile... Il suffirait qu'il passe l'angle d'un mur et patatra, la valeur de la moyenne est toute faussée.

Pour améliorer la mesure on va plutôt utiliser la médiane, qui a l'avantage par rapport à la moyenne de ne pas être dépendante des valeurs extrêmes.

Pour ça on va modifier le code pour qu'il fasse 10 mesures (valeur arbitraire) et qu'il nous renvoie la médiane de ces valeurs.

NB: je ne mets ici que les parties de code qui ont changées

# Ne pas oublier d'importer numpy as np
	while True:
          data = []
          for i in xrange(10):
              dist = distance()
              data.append(dist)
              # wait 60 ms before new mesurement
              time.sleep(0.06)
          dist = np.median(dist)
          print("Measured Distance = %.1f cm" % dist)

C'est mieux !

Mais y'a encore un problème...

Parfois sans raisons apparentes, le programme est faussé dans sa mesure. Ce peut être le symptôme de beaucoup de chose, par exemple, le fait qu'on utilise python (donc langage interprété) sur un processeur mono cœur qui fait du multi threading.

La première chose que l'on peut faire c'est un calcul de médiane avec fenêtre glissante.

NB: j'ai choisi de prendre une fenêtre glissante de 10 mesures.

Comme on le voit sur le graphique suivant, la médiane à fenêtre glissante atténue les valeurs extrêmes. Ca rajoute un peu de latence dans le calcul de la distance réelle.

En rouge: la mesure sans filtre. En bleu la mesure avec filtrage par médiane avec fenêtre glissante

On voit en effet que lorsque les valeurs renvoyées par le capteurs sont des extrêmes alors la médiane n'en tient pas compte.

Le code complet:

# -*- coding: utf-8 -*-

##Libraries
import RPi.GPIO as GPIO
import sys
import time
import numpy as np
from pprint import pprint

#set GPIO Pins
GPIO_TRIGGER = int(sys.argv[1]) #18
GPIO_ECHO = int(sys.argv[2]) #24

#GPIO Mode (BOARD / BCM)
GPIO.setmode(GPIO.BCM)

#set GPIO direction (IN / OUT)
GPIO.setup(GPIO_TRIGGER, GPIO.OUT)
GPIO.setup(GPIO_ECHO, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

def distance():
    # set Trigger to HIGH
    GPIO.output(GPIO_TRIGGER, False)
    time.sleep(0.00001)

    GPIO.output(GPIO_TRIGGER, True)

    # set Trigger after 0.01ms to LOW
    time.sleep(0.00001)
    GPIO.output(GPIO_TRIGGER, False)

    StartTime = time.time()
    StopTime = time.time()

    timestat = time.time()
    # save StartTime
    while GPIO.input(GPIO_ECHO) == 0:
        StartTime = time.time()
        if StartTime - timestat > 0.06:
            print("TIMED OUT")
            break

    timestat = time.time()
    # save time of arrival
    while GPIO.input(GPIO_ECHO) == 1:
        #print("GPIO_INP_1")
        StopTime = time.time()
        if StopTime - timestat > (0.06):
            print("TIMED OUT")
            break

    # time difference between start and arrival
    TimeElapsed = StopTime - StartTime
    distance = (TimeElapsed * 34300) / 2

    return distance


def write_results(results, file):
    #logging.info("Open file")
    try:
        f = open(file, "a+")
        f.write(results + "\n")
        #logging.info("Write file: " + results)
        f.close()
        #logging.info("Close file")
    except IOError as x:
        print('error writing file : ' + x )

def init(data):
    print("Init start")
    for i in range(len(data)):
        data.append(distance())
    print("Init done")

if __name__ == '__main__':
    try:
        data = [10]
        init(data)

        while True:
            dist = distance()
            data.pop(0)
            data.append(dist)
            write_results(str(dist), sys.argv[3]) # Dist without filter
            dist = np.median(data)
            write_results(str(dist), sys.argv[4]) # Dist with median filter
            print("Measured Distance = %.1f cm" % dist)
            time.sleep(0.06)

    # Reset by pressing CTRL + C
    except Exception as inst:
        print("Unexpected error:", sys.exc_info()[0])
        print(type(inst))
        print(inst.args)
        print(inst)
        print("Measurement stopped by User")
        GPIO.cleanup()
 python3 testHCSR04.py 17 18 distWithouFilter DistMedianFilter

Une méthode pour éliminer les valeurs aberrante

On peut tenter d'améliorer le résultat en utilisant la méthode de l'écart interquartile.

L'idée est simple, on va mesurer pour 10 valeurs de notre fenêtre glissante le premier et le troisième quartile.  Et ensuite appliquer la formule suivante:

q1 = np.quantile(data, 0.25)
q3 = np.quantile(data, 0.75)
median = np.median(data)
dist1 = round((q1 + 2*median + q3)/4, 1)

Comme on peut le voir cette nouvelle métrique donne des résultats très proches de la médiane.

# -*- coding: utf-8 -*-

##Libraries
import RPi.GPIO as GPIO
import sys
import time
import numpy as np
from pprint import pprint

#set GPIO Pins
GPIO_TRIGGER = int(sys.argv[1]) #18
GPIO_ECHO = int(sys.argv[2]) #24

#GPIO Mode (BOARD / BCM)
GPIO.setmode(GPIO.BCM)

#set GPIO direction (IN / OUT)
GPIO.setup(GPIO_TRIGGER, GPIO.OUT)
GPIO.setup(GPIO_ECHO, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)

def distance():
    # set Trigger to HIGH
    GPIO.output(GPIO_TRIGGER, False)
    time.sleep(0.00001)

    GPIO.output(GPIO_TRIGGER, True)

    # set Trigger after 0.01ms to LOW
    time.sleep(0.00001)
    GPIO.output(GPIO_TRIGGER, False)

    StartTime = time.time()
    StopTime = time.time()

    timestat = time.time()
    # save StartTime
    while GPIO.input(GPIO_ECHO) == 0:
        StartTime = time.time()
        if StartTime - timestat > 0.06:
            print("TIMED OUT")
            break

    timestat = time.time()
    # save time of arrival
    while GPIO.input(GPIO_ECHO) == 1:
        #print("GPIO_INP_1")
        StopTime = time.time()
        if StopTime - timestat > (0.06):
            print("TIMED OUT")
            break

    # time difference between start and arrival
    TimeElapsed = StopTime - StartTime
    distance = (TimeElapsed * 34300) / 2

    return distance


def write_results(results, file):
    #logging.info("Open file")
    try:
        f = open(file, "a+")
        f.write(results + "\n")
        #logging.info("Write file: " + results)
        f.close()
        #logging.info("Close file")
    except IOError as x:
        print('error writing file : ' + x )

def init(data):
    print("Init start")
    for i in range(len(data)):
        data.append(distance())
    print("Init done")

if __name__ == '__main__':
    try:
        data = [10]
        init(data)

        while True:
            dist = distance()
            data.pop(0)
            data.append(dist)

            write_results(str(dist), sys.argv[3]) # Dist without filter
            dist = np.median(data)
            write_results(str(dist), sys.argv[4]) # Dist with median filter

            q1 = np.quantile(data, 0.25)
            q3 = np.quantile(data, 0.75)
            median = np.median(data)
            dist1 = round((q1 + 2*median + q3)/4, 1)
            write_results(str(dist1), sys.argv[5]) # Dist with median quartile filter

            print("Measured Distance = %.1f cm" % dist)
            time.sleep(0.06)

    # Reset by pressing CTRL + C
    except Exception as inst:
        print("Unexpected error:", sys.exc_info()[0])
        print(type(inst))
        print(inst.args)
        print(inst)
        print("Measurement stopped by User")
        GPIO.cleanup()
python3 testHCSR04.py 17 18 distWithouFilter DistMedianFilter DistQuartilMedianFilter

NB: Il ne s'agit là qu'une des nombreuses méthodes pour éliminer les valeurs aberrantes. Pour approfondir le sujet je conseil de s'intéresser au test Tau de Thomson.

Update #1:

J'ai découvert qu'il existait maintenant un module HC_SR04 dans la librairie gpiozero.

Il donne de bon résultats et semble intégrer une méthode pour éviter les valeurs aberrantes.

Je mets içi un bout de code pour tester cette librairie.

NB: Mesurement est ici une mémoire partagée. Ce programme est lancé dans un thread séparé du programme principal et la mémoire partagée permet la communication entre les deux processus. Il sera nécessaire de l'adapter pour l'utiliser seul.

#!/usr/bin/env python3
#-*-coding:utf8-*-

from gpiozero import DistanceSensor
from time import sleep

def run_mesurment(trig, echo, mesurment):
    sensor = DistanceSensor(echo=echo, trigger=trig, max_distance=4, queue_len=10)
    while True:
            mesurment.value = sensor.distance*100
            sleep(.1)