[RPi3] Entfernung über Ultraschallsensor messen

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
robin_
User
Beiträge: 48
Registriert: Montag 3. August 2020, 17:59

Hi,

Ich hab einen RPi3b und versuche eine Entfernungsmesserung durchführen zu können. Dafür hab ich den folgenden Ultraschallsensor gekauft:
HC-SR04. Der funktioniert so, dass der TRIGGER-Pin kurz ein High bekommt und man dann beim ECHO-Pin die Dauer des highs misst. Diesen Wert muss man mit der Schallgeschwindigkeit multiplizieren und halbieren, das sollte dann die Distanz sein.

Das Ganze klappt insofern nicht, dass die Distanzen zw. 4 und 250 cm schwanken, unabhängig ob der Sensor ins 'leere' zielt oder ich etwas unmittelbar davor stelle.
Aus dem Datenblatt geht hervor:
berührungslose Messungen in einem Bereich von 20 bis 4000 Millimeter mit einer Genauigkeit von 3 Millimetern durchführen.
Mir ist klar, dass das sehr günstiges Zeugs ist, ich erwarte keine mm Genauigkeit, aber schon halbwegs realistische Werte. Ich kann auch 30 mal messen und dann den Median Wert nehmen, das wäre kein Problem, ich will eig. nur die Wassertankhöhe meiner Kaffeemaschine messen.

Weil man laut Datenblatt 5V braucht hab ich einen Spannungswandler, dessen Pins wie folgt belegt sind: Seite 3 oben

Dementsprechend ist die Verdrahtung wie folgt:


- Pin 1,4,7 kriegen Ground (vom Level shifter), 14 kriegt 5V (pin 2 vom RPi).
- Der GPIO4 geht in Pin 2 (1A) der Output 1Y davon geht zum Trigger Pin des Sensors.
- GPIO17 (ECHO-Pin, als Input definiert) geht in Pin5 (2A) und Output 2Y ist mit dem ECHO-Pin vom Sensor verbunden.

Ich bin mir nicht 100% sicher, ob das mit dem ECHO richtig ist: Muss das 5V High vom Sensor in den 'Output 2Y' vom Wandler, der dann das ganze in 3,3V in 2A ausspuckt?
Hoffe das ganze war halbwegs verständlich.

Nun noch zum Code:

Code: Alles auswählen

import time
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
TRIG = 4
ECHO = 17
GPIO.setup(TRIG,GPIO.OUT)
GPIO.setup(ECHO,GPIO.IN)
GPIO.output(TRIG, False)
time.sleep(2)
try: # Main program loop
    while True: 
        GPIO.output(TRIG, True)
        time.sleep(0.00001)
        GPIO.output(TRIG, False)
        while GPIO.input(ECHO) == 0:
            ...    
        pulse_start = time.time()
        while GPIO.input(ECHO) == 1:
            ...
        pulse_end = time.time()
        pulse_duration = pulse_end - pulse_start
        distance = pulse_duration * 17150
        distance = round(distance, 2)
        print("Distance is {} cm".format(distance))
        time.sleep(2)
# Scavenging work after the end of the program
except KeyboardInterrupt: 
    ...
finally:
     GPIO.cleanup()
Das entspricht im groben und ganzen den Beispiel aus dem Datenblatt, ich hab auch schon einen anderen Sensor ausprobiert - ähnliches Ergebnis.

Vielen Dank!
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Was steht dann da statt dem …?

Allgemein ist das ausmessen von digitalen Ereignissen mit einem dicken OS wie Linux so eine Sache. Da passiert zu viel, als das da Präzision so einfach garantiert ist. Gerade in einer doch eher langsamen und garbage collecteten sprache wie Python.

Ich persönlich würde auf dem Pi zu pigpio greifen, weil das durch bestimmte Mechanismen sicherstellt, das die Abfrage der Pins sehr präzise zyklisch erfolgt. Und in Python dann als Zeitstempel weitergibt.

Oder noch eher auf einen Micro aus der Arduino Familie wechseln. Der Pi ist mit dem Wasserstand eh unterfordert, und dafür dann teuer, unzuverlässig und Strom hungrig.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Und wenn schon RPi.GPIO, dann benutzt man GPIO.wait_for_edge anstatt das selbst per bussy-loop zu programmieren.
`as` bei Import ist zum Umbenennen da, GPIO wird aber gar nicht umbenannt, warum dann dieser komplizierte Import?
Warnungen sind dazu da, dass man sie behebt, nicht dass man sie ignoriert.
Man schreibt nicht irgendwo mystische Zahlen in den Code, sondern definiert die als Konstanten am Anfang des Programms.
Gerundet wird bei der Ausgabe, nicht innerhalb des Programms.
`...` ist kein Ersatz für `pass`
Eingerückt wird immer mit 4 Leerzeichen pro Ebene.

Code: Alles auswählen

import time
from RPi import GPIO

TRIG = 4
ECHO = 17
SOUND_VELOCITY_IN_MM_S = 343200


def initialize():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(TRIG, GPIO.OUT)
    GPIO.setup(ECHO, GPIO.IN)
    GPIO.output(TRIG, False)


def main():
    try:
        initialize()
        time.sleep(2)
        while True: 
            GPIO.output(TRIG, True)
            time.sleep(0.00001)
            GPIO.output(TRIG, False)
            GPIO.wait_for_edge(ECHO, GPIO.RAISING)
            pulse_start = time.monotonic()
            GPIO.wait_for_edge(ECHO, GPIO.FALLING)
            pulse_end = time.monotonic()
            pulse_duration = pulse_end - pulse_start
            distance = 0.5 * SOUND_VELOCITY_IN_MM_S * pulse_duration
            print("Distance is {:.2f} cm".format(distance))
            time.sleep(2)
    except KeyboardInterrupt: 
        pass
    finally:
        GPIO.cleanup()

if __name__ == "__main__":
    main()
robin_
User
Beiträge: 48
Registriert: Montag 3. August 2020, 17:59

__deets__ hat geschrieben: Mittwoch 2. August 2023, 07:23 Was steht dann da statt dem …?
Du meinst in den Whiles? Das ist einfach nur ein Platzhalter, wie pass.

Also, eigentlich wollte ich das auf einem Pi Pico mit Rust erledigen, aus den Gründen die du schon gesagt hast (Echtzeit etc).
Da ich das nicht hinbekommen hatte, wollte ich die prinzipielle Funktionsweise mal mit Micropython auf dem Pico testen. Das hatte soweit geklappt, allerdings hab ich immer eine gleichbleibende Impulsdauer bekommen, unabhängig davon, wie weit/ ob etwas vorm Sensor stand.

Ich glaube ich werde es nochmal am Pico + Micropython probieren und dann erneut berichten.

@Sirius:
Das Code Beispiel hatte ich nur kopiert um zu testen obs damit funktioniert. Das mit den Konstanten ist mir klar. Und wegen der 4 Leerzeichen: In VSCode wird das als 4 Leerzeichen interpretiert, scheint beim kopieren hierhin anders zu sein.

Und danke für den Hinweis mit 'GPIO.wait_for_edge'. Werde es ausprobieren :)

Gruß
Robin
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Naja, es gibt ... als Platzhalter nicht. Wenn da pass steht, warum steht da nicht pass? Wenn da andere Dinge stehen, koennen die eben schon verantwortlich fuer den nicht-determinismus sein.

Auf dem Pico kann man sowas wahrscheinlich auch machen mit den PIO-units, siehe zb https://forum.micropython.org/viewtopic.php?t=12785 - damit kannst du harte Echtzeit mit der Bequemlichkeit von Python kombinieren.
robin_
User
Beiträge: 48
Registriert: Montag 3. August 2020, 17:59

Nein, da stehen nur die ...
Danke für den Link, ich schau mir das mal an:)

Nochmal zu meiner Verdrahtung, weil ich da noch ein Anfänger bin: Hört sich das von meiner Beschreibung richtig an?

Danke.
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ok, ich wusste nicht, dass das geht... ich habe immer nur pass benutzt. Damit scheidet das dann aus.

Ansonsten klingt das alles ok, die Dinger sind notorisch frickelig. Da kommt es auch mal dazu, dass gar kein pulse kommt. Das macht das ganze dann noch aetzender.
Benutzeravatar
__blackjack__
User
Beiträge: 13116
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: Ich benutze ``pass`` als Platzhalter der da *stehen bleiben soll* und ``...`` als Platzhalter wenn das später noch durch tatsächlichen Code ersetzt werden soll/muss. Und habe so als Faustregel, dass Code mit ``...`` eher nicht in der Versionsverwaltung laden sollte. Ist also eher etwas kurzfristiges/flüchtiges bei mir.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
robin_
User
Beiträge: 48
Registriert: Montag 3. August 2020, 17:59

Hi,

ich hab jetzt paar Stunden versucht mit in StateMachines einzulesen und hab mir auch das verlinkte Beispiel angeschaut.
So recht werde ich noch nicht schlau draus, vor allem der Part wo es ums Messen geht, wie lange das ECHO-Signal High ist.

Im Endeffekt muss ich doch in jedem Step checken ob der Pin noch positiv ist und dann wieder zum Anfang der Schleife springen und dabei y um 1 verringern, korrekt?
Wenn ich es richtig verstehe, prüft man mit 'jmp(pin, "jump_to_some_Label") ob der pin noch HIGH ist (nehme an den ersten Pin des inputs?).
Aber wie ich dann das Y-Register -1 rechne hab ich noch nicht raus.

Naja, hier mal mein Code bisher:

Code: Alles auswählen

from machine import Pin
import rp2
import time

"""
Pico runs at 125 MHz. The min. state machine (sm) frequency is 1908 Hz.
If the frequency is set to 1MHz, it should result in one cycle every 1 us, thats enough accuracy.

TRIGGER is set to gpio 5 (pin 7) and ECHO is set to gpio 6 (pin 9).

- Set trigger to HIGH for 10us
- Count duration of ECHO beeing HIGH
"""

@rp2.asm_pio()
def measure():
    
    # gpio 5 is HIGH for 10us
    label("trigger")
    set(pins, 1)
    nop()                   [8] 
    set(pins, 0)

    wait(0, pin, 1)          # wait for a LOW on ECHO, relative to base which starts at gpio 5

    set(y, 0)
    mov(y, inv(y))
    
    wait(1, pin, 1)          # wait for a HIGH on ECHO - need to start counting now...
    label("loop")    
    
    jmp(pin, "loop")
    
    
    wait(0, pin, 1)          # wait for a LOW on ECHO - calculae the difference

    push()  


def main(sm):
    """Read the output FIFO."""

    while True:
        print("Testing...")
        time.sleep(1)
    

if __name__ == "__main__":
    sm = rp2.StateMachine(0, measure, freq=1000000, in_base=Pin(5), set_base=Pin(6), )
    sm.active(1)
    # in_base and set_base shift the 'starting index' of pins, meaning that  for inputs, the first pin is now our desired gpio 5.

    main(sm)
Hinterher würde ich dann mit sm.get() das FIFO auslesen wollen. Aktuell blockt das einfach nur das Programm - weil ja nichts drin ist.

Gruß Robin
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich habe heute mal selbst versucht da was mit Rust & PIO zu backen. Und muss Abbitte leisten: ich habe die PIOs überschätzt. Sind ein cooles IO Feature, aber zählen können sie nicht. Bleiben klassische GPIO IRQs. Das geht ja auch in uPy. Oder Rust.
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

So, und nochmal tiefer geschaut. Sie koennen doch zaehlen, auf eine sehr spezifische Art: durch dekrementieren. Und wenn man eines der X/Y-Register mit 0xffff fuellt, kann der runtergezaehlt werden, bis man den Puls ausgemessen hat. Da man die Dinger wohl auch von aussen "resetten" kann, sollte man es damit tatsaechlich hinbekommen, auf Pulse zu warten. Aber das ganze mit Rust und dessen HAL ist schon ziemlich komplex, ich muss schauen, wieviel Zeit ich darauf verwandt bekomme, und ob das irgendwohin fuehrt.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

ich muss mal wieder kurz nach fragen. @__deets__ wieso verwendest du Rust für deine Experimente? Ist MP zu langsam?

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nein, das ist bestimmt ausreichend im konkreten Fall. Aber ich will gerne Rust vertiefen. Python kann ich schon. Und vieles geht in uPy nicht, was der Pico kann. ZB DMA etc pp. Solche Sachen muss man dann in C/C++/Rust machen.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Okay , vielen Dank.

Bin gespannt, wie deine weiteren Experimente verlaufen.


Viele Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
robin_
User
Beiträge: 48
Registriert: Montag 3. August 2020, 17:59

Hi,

Mein Plan war die PIOs in MP hinzubekommen und dann das ganze nach Rust zu überführen - ebenfalls um mal ein wenig Rust gesehen zu haben.
Falls ich da weitergekommen bin, meld' ich mich nochmal.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

__deets__ hat geschrieben: Mittwoch 2. August 2023, 14:00 Ok, ich wusste nicht, dass das geht... ich habe immer nur pass benutzt. Damit scheidet das dann aus.

Ansonsten klingt das alles ok, die Dinger sind notorisch frickelig. Da kommt es auch mal dazu, dass gar kein pulse kommt. Das macht das ganze dann noch aetzender.
Jedes gültige Statement kann verwendet werden. Docstrings gehen auch.

Code: Alles auswählen

while True:
    "Endlosschleife"
Die 3 Punkte ist der Alias für Ellipsis.
Ursprünglich wurde und wird es für Numpy verwendet, als Platzhalter für eine Dimension.

Dann kamen die TypeHints und die ... wurden dann für Klassen- und Funktionsdefinitionen in .pyi-Dateien verwendet.

Sieht dann so aus:

Code: Alles auswählen

def foo(value: int) -> int: ...
Pydantic nutzt es für Field als Angabe, dass ein Wert benötigt wird.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
robin_
User
Beiträge: 48
Registriert: Montag 3. August 2020, 17:59

Hm,
ich kriegs leider immer noch nicht gebacken mit dem PIO.

Was mich stutzig macht: Ich habe Videos gesehen, wo genau mein Sensor, HC-SR04 mit Micropython verwendet wird. Und das ziemlich genau. Ich bin gerade überfordert; Muss ja dann eig. was an der Verkabelung liegen denke ich?

Code: Alles auswählen

from machine import Pin
import rp2
import time

"""
Pico runs at 125 MHz. The min. state machine (sm) frequency is 1908 Hz.
If the frequency is set to 1MHz, it should result in one cycle every 1 us, thats enough accuracy.

TRIGGER is set to gpio 6 (pin 9) -> Set pin
ECHO is set to gpio 7 (pin 10) -> Input pin

- Set trigger to HIGH for 10us
- Count duration of ECHO beeing HIGH
"""

@rp2.asm_pio()
def measure():
    # wrap_target()

    # pull (and stall if empty) from TX FIFO into OSR = Output shift registers
    pull(block)
    mov(x,osr) # 1_000_000 * 5 is the max. number of steps we wait for a a echo signal (works like a timeout i guess)
    

    # gpio 6 is HIGH for 10us
    set(pins, 1)       
    nop()                   [8] 
    set(pins, 0)

    wait(1, gpio, 7)          # wait high on pin ECHO, afterwards: Count how long the 

    label("loop")
    jmp(pin, "dec")       # True if pin (jmp_pin is set), meaning: Is this pin still HIGH
    jmp("output")           # Gets called when ECHO pin is low (again)
    
    
    label("dec")
    jmp(x_dec, "loop")

    label("output")
    mov(osr,x)
    push()       # pushes isr into RX FIFO; noblock means it overwrites existing values, thus making the FIFO beeing up-to-date


def main(sm):
    """Read the output FIFO."""

    while True:
        print("Testing...")
        time.sleep(1)
    

if __name__ == "__main__":
    sm = rp2.StateMachine(0, measure, freq=1000000, in_base=Pin(7), set_base=Pin(6), jmp_pin=Pin(7))
    print("Init")
    sm.active(1)
    print("Activate")

    # 32 bit integer has range of: [−2_147_483_648; 2_147_483_647] -> max timeout to search for ECHO is therefore 2147 [sec] at 1MHz cycle frequency

    num_steps = 1_000_000 * 5 # wait 5 seconds
    sm.put(num_steps)

    time.sleep(1)

    print("After sleep")
    output = sm.get()
    print("We got: ", output)
Naja, hiermit klappts auch nicht. Ich würde gerne ein Bild anhängen aber hier kann man keine hochladen, oder?

Gruß Robin
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Hochladen nicht. Aber du kannst eins auf imgur hochladen. Und die haben nen BB-Code Knopf, der produziert hier den Einbettungen-Code.
__deets__
User
Beiträge: 14544
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und noch ein Nachtrag: ich wuerde das wie schon erwaehnt erstmal ohne PIOs machen, nur mit normalen Pin-IRQs. Ausserdem ist bei sowas natuerlich die Verwendung eines Oszis eine gute Idee, fall vorhanden. Damit man das mal beurteilen kann, was da wirklich auf den Leitungen passiert.
Antworten