Oszilloskop-Programm
Verfasst: 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.
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:
Die Funktionsweise sollte sich aus der umfangreichen und ausführlichen Kommentierung ergeben.
Hier möchte ich nur noch zwei Aspekte genauer erläutern:
.
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)
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.
.