Oszilloskop-Programm

Hier werden einzelne Projekte mit MicroPython vorgestellt
Antworten
Heinrichs
Beiträge: 200
Registriert: Do 21. Okt 2010, 18:31

Oszilloskop-Programm

Beitrag von Heinrichs » Fr 17. Okt 2025, 17:30

Um es vorweg klarzustellen: Ein richtiges Oszilloskop kann das Programm nicht ersetzen, aber immerhin lassen sich damit periodische Signale (z. B. Sinus, Dreieck, Sägezahn) mit Frequenzen bis zu 500 Hz anzeigen; dabei wird zusätzlich die Periodendauer (in ms) und die maximale Spannung des Signals angezeigt. Die maximale Spannung liegt bei ca. 2,2 V; sollte diese bei Ihrer Signalquelle größer sein, bietet sich es sich an, einen Spannungsteiler (Potentiometer) einzusetzen. Die Abb. 1 zeigt einen typischen Screenshot.


Oszi_1a.jpg
Abb. 1: TTGO mit Oszi-Programm
Oszi_1a.jpg (149.54 KiB) 60 mal betrachtet


Anschließen der Signalquelle

Zunächst werden die Massen (GND) von Signalquelle und TTGO verbunden; dann wird der Ausgang der Signalquelle mit dem Pin 36 des TTGO verbunden. Bitte beachten Sie: Die Spannungswerte von der Signalquelle dürfen 3,6 V nicht überschreiten; ansonsten könnte der Mikrocontroller zerstört werden.



Das Messprogramm


Um Signale mit Frequenzen bis zu etwa 500 Hz aufzeichnen zu können, trenne ich die "Messphase" von der "Auswertungsphase" (Diagramm und Messwerte anzeigen). Die Auswertung kostet nämlich relativ viel Zeit; würde sie simultan mit der Messung erfolgen, könnten nur Signale mit recht kleinen Frequenzen korrekt dargestellt werden.

Diese Trennung können Sie leicht auch im Quellcode wiederfinden:

Code: Alles auswählen


# oszi_3_1.py

# Darstellung von U-t-Diagrammen auf dem Display des TTGO
# Bestimmung der Maximalspannung und der Periodendauer (bis etwa 500 Hz)

# www.forum.g-heinrichs.de
# 17.10.2025

# Maximale Spannung sollte kleiner oder gleich 2.2 V sein (ggf. zusätzliches Potentiometer benutzen).
# Zeitraster (vertikale Linien) im Abstand von 20 Pixeln, s. u.
# Nach jeder Messreihe pausiert das Programm für 2 s; in dieser Zeit kann das Programm abgebrochen werden, indem man die Taste A (oben links) länger als 2 s drückt.
# Die vom Programm angezeigten Werte für die Schwingungsdauer stimmen mit den Werten, die ich mit einem DS203-Oszi gemessen habe, bis auf wenige Prozent Abweichung überein.

####################################### ACHTUNG #################################################
#                                                                                               #
#                   Die angelegte Spannung darf 3,6 V nicht überschreiten!                      #
#                        Ansonsten kann der ESP32 beschägigt werden.                            #
#                                                                                               #
#################################################################################################

basic_cycle_time = 180 # Zeit in us für einen einzigen Messschleifendurchlauf (ohne Auswertungen und Anzeigen): Diese Zeit entsricht dem x-Abstand von 2 benachbarten Pixeln.
scale = 1 # nur ganze Zahlen! Sinnvolle Werte: 1 (bei hohen Frequenzen) - 5 (bei niedrigen Frequenzen). 
# Je größer scale, desto mehr wird der Graph horizontal gestaucht.
# Also: Je größer die Periodendauer ist, desto größer sollte der scale-wert gewählt werden.
waiting_time_between_2_measurements = 2 # in s
version_name = 'Oszi 3.1'

from time import sleep, sleep_us, ticks_us, ticks_diff
from machine import Pin, ADC, SPI
import vga1_8x16 as font1 
import vga1_bold_16x32 as font2  
import st7789
import gc

###################################### Initialisierungen und Funktionen ##########################################

print(version_name, 'gestartet...')

# Display
spi = SPI(1, baudrate=20000000, polarity=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(spi, 135, 240,  reset=Pin(23, Pin.OUT), cs=Pin(5, Pin.OUT), dc=Pin(16, Pin.OUT), backlight=Pin(4, Pin.OUT), rotation=3)
# Landscape
display.init()
bg = st7789.color565(200,200,255) # hellgraue Hintergrundfabe

# Taster  A
button = Pin(0, Pin.IN, Pin.PULL_UP) # Taster Ta0 zum Anhalten der Messung nach einem Durchlauf 2 s lang drücken 

# A/D-Wandler
pot = ADC(Pin(36))  # ADC0 (direkt neben 3 V)
pot.atten(ADC.ATTN_11DB)  # Messbereich: 0 - 3.6 V;
pot.width(ADC.WIDTH_12BIT) # 12 Bit: 0-4095


################################# Funktionen #################################

def titel(t, version_name): # t: Anzeigedauer in s
  display.text(font2, version_name, 50, 25, st7789.GREEN, st7789.BLACK)  
  display.text(font2, 'Welcome!', 50, 60, st7789.GREEN, st7789.BLACK)
  display.text(font1, 'www.g-heinrichs.de', 42, 115, st7789.WHITE, st7789.BLACK)  
  sleep(t)
  
def get_T(tabel, U_mi, U_ma): # liefert Periodendauer T (in ms)
  # Zeiten ta und tb von  benachbarten "kritischen Punkten" ermitteln
  # alle Zeiten in ms
  U_crit = (U_mi + U_ma)/2
  # print('U_crit = ', U_crit) # zu Testzwecken
  # print(tabel) # zu Testzwecken
  # 1. krit. Punkt...
  ta = 0
  for i in range(227 - 8 - 1):
    t_1, U_1 = tabel[i]
    t_2, U_2 = tabel[i+1]
    if U_1 < U_crit and U_2 >= U_crit: # krit. Pkt. der 1. steigenden Flanke
      ta = t_2
      break
  if ta == 0:
    print('Keinen 1. kritischen Punkt gefunden! Ggf. delta_t vergrößern!')  
    return 0 # Ersatzwert für T
  else:
    tb = 0
    for j in range(i+2, 227 - 8 - 1):        
      t_1, U_1 = tabel[j]
      t_2, U_2 = tabel[j+1]
      if U_1 < U_crit and U_2 >= U_crit: # krit. Pkt. der 2. steigenden Flanke
        tb = t_2
        break
    if tb == 0:
      print('Keinen 2. kritischen Punkt gefunden! Ggf. delta_t vergrößern!')   
      return 0 # Ersatzwert für T
    else:    
      T = tb - ta
      return T # in ms      
    

######################################### Hauptprogramm #######################################

i0 = 9 # Horizontaler Wert für Startposition des Graphens (in Pixel-Einheiten)
i = i0 # entspricht aktuellem Zeitwert (in Pixel-Einheiten)
U_max = 0.0 # maximaler Wert der gemessenenen Spannung; wird in der folgenden Schleife angepasst
U_min = 3.0 # minimaler Wert der gemessenenen Spannung; wird in der folgenden Schleife angepasst
U_t_tabel = [] # Liste für U-t-Tabelle

display.fill(st7789.BLACK)
titel(2, version_name)

time_list = [] # Zum Testen
stop = False
# Messreihe aufzeichnen und in der Liste U-t-tabel speichern
while not stop: # ... solange wie nicht die Stop-Taste (A) betätigt worden ist
  # Anfang des Messintervalls
  t1 = ticks_us()
  pot_value = pot.read() # Rohwert für die Spannung ermitteln
  # print(pot_value) # Zur Kontrolle
  voltage = 0.00077*pot_value
  U_t_tabel.append((i*basic_cycle_time*scale*1E-3, voltage)) # aktueller Zeitwert i*basic_cycle_time*scale*1E-3 in ms
  if voltage > 3.0: # Spannungswertanzeige begrenzen
    voltage = 3.0
  if voltage > U_max:
    U_max = voltage # U_max anpassen
  if voltage < U_min:
    U_min = voltage # U_min anpassen    
  i += 1 # nächster Messpunkt
  sleep_us(basic_cycle_time*(scale-1)) # auch für i = 0 etwas warten (sleep_us(0) kostet auch Zeit)
  # Ende der Messwerterfassung für einen Zyklus 

  if i > 227: #  Auswertung
    # U-t-Diagramm zeichnen  
    # print(U_t_tabel)
   
    stop = not button.value()
    
    # Display löschen und für neue Messreihe vorbereiten...
    if stop:
      print('Ende der Messung')
      display.fill(bg)
      display.text(font2, 'ENDE', 92, 40, st7789.BLACK, bg)
      # Die "while not stop"-Schleife wird abgebrochen.
      
    else: # Auswertung, Anzeige von der Periodendauer T, der Maximalspannung U_max und dem U-t-Diagramm; Startwerte für neuen Schleifendurchlauf
      # Periodendauer T bestimmen  
      T = get_T(U_t_tabel, U_max, U_min) 
      # print(T) # zum Testen
      
      # Anzeigen...
      display.fill(bg)
      # Rahmen fuer Graph mit Skala
      display.rect(9, 30, 220, 95, st7789.BLUE)
      for sk in range(9, 245, 20): # Skalenstriche im Abstand von 20 Pixel
        display.vline(sk, 30, 95, st7789.BLUE)
      # Diagramm anzeigen...
      # print(U_t_tabel) # zum Testen
      for j in range(227 - 8 - 1):
        dummy, voltage = U_t_tabel[j]
        U_int=int(voltage/3.0*120) # Spannungswert in Pixel-Einheiten
        display.pixel(j+9, 122 - U_int, st7789.RED) # 2 übereinander liegende Pixel 
        display.pixel(j+9, 122 - U_int - 1, st7789.RED)
      # Versionsname u. Messwerte anzeigen
      display.text(font1, version_name, 10, 5, st7789.BLACK, bg)
      display.text(font1, str(T)[:4]+' ms', 100, 5, st7789.RED, bg)
      display.text(font1, str(U_max)[:4] +' V', 172, 5, st7789.BLUE, bg) # U_max ausgeben

      # Startwerte für nächste Messreihe, insbesondere auch U_t_tabel "löschen";
      i = i0
      U_max = 0.0
      U_min = 3.0
      U_t_tabel = []
      # garbage collection erzwingen:
      # Ohne diese erzwungene Bereinigung würde diese von Micropython zu irgendwelchen (unkontrollierten) Zeitpunkten durchgeführt werden.
      # Wenn das während der Messphase geschieht, kann dies zu Sprüngen/Knicken im Graphen führen.  
      gc.collect()
      # warten
      sleep(waiting_time_between_2_measurements)

Die Funktionsweise sollte sich aus der umfangreichen und ausführlichen Kommentierung ergeben.

Hier möchte ich nur noch zwei Aspekte genauer erläutern:
  • Zur Darstellung des Signal benutze ich nur die pixel-Methode. Das hat zur Folge, dass man bei hohen Frequenzen nicht mehr eine durchgezogene Linie sieht, sondern nur einzelne Punkte. Wer möchte kann stattdessen auf die line-Methode zurückgreifen; damit kann eine gerade Linie zwischen zwei Messpunkten erzeugt werden. Allerdings benötigt diese Methode deutlich mehr Zeit.
  • Am Ende einer Messreihe erzwingt das Programm mit dem Befehl gc.collect() eine so genannte garbage collection. Dadurch werden frei gewordene Speicherplätze (wie z. B. hier von der umfangreichen Liste U_t_table) bereinigt. Micropython führt solche Bereinigungen bei Bedarf auch selbstständig durch. Nun kostet dieser Vorgang etwas Zeit (in unserem Fall wenige Millisekunden). Wenn dies während der Messphase geschieht, dann werden in dieser Phase keine Messwerte aufgenommen. In diesem Fall führt dies zu einen Sprung bzw. Knick im Graphen (vgl. Abb. 2). Weil in unserem Programm nun am Ende jeder Mess- und Ausgabe-Schleife (vor einer längeren Pause!) eine solche Bereinigung erzwungen wird, verzichtet Micropython auf das ungewünschte selbständige Bereinigen.

Oszi_2a.jpg
Abb. 2: Sprung im Graphen
Oszi_2a.jpg (83.51 KiB) 56 mal betrachtet


.

Antworten