RPi-Pico OLED-Oszilloskop                 


Elektronik-Labor  Projekte  Mikrocontroller  Raspberry            



Kürzlich habe ich zum ersten Mal ein OLED-Display an den Pico angeschlossen. Die Ansteuerung ist nicht schwer. Man muss die Datei ssd1306 mit einbinden, die zuerst in den Pico geladen werden muss. Die ersten Versuche verliefen erfolgreich. Deshalb habe ich gleich eine richtige Anwendung angestrebt: Ein ganz kleines Oszilloskop, sozusagen ein Picoskop.

#OLEDoszi2.py Scope
from machine import Pin, I2C
from ssd1306 import SSD1306_I2C
import machine
import time
import array


i2c = I2C(0, scl=Pin(9),sda=Pin(8),freq=100000)
oled = SSD1306_I2C(128,64,i2c)
u2 = machine.ADC(2)
ad_in = Pin(28, Pin.IN)

x = array.array('i', [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])
while (True):
for i in range(128):
x[i]=63-u2.read_u16()//1366
for t in range(70):
a=1+1
oled.fill(0)
oled.text("3.3V 10ms/div",5,0)
oled.line(0, 60, 0, 64, 1)
oled.line(20, 60, 20, 64, 1)
oled.line(40, 60, 40, 64, 1)
oled.line(60, 60, 60, 64, 1)
oled.line(80, 60, 80, 64, 1)
oled.line(100, 60, 100, 64, 1)
oled.line(120, 60, 120, 64, 1)

for i in range(128):
u=x[i]
if (i>0):
oled.line(i, u0, i+1, u, 1)
u0=u
oled.show()
time.sleep(1)



In der oberen Zeile habe ich die Einstellungen angegeben. Ganz unten stehen Punkte für die Skalenteile im Abstand 10 ms. Das Oszillogramm zeigt ein 50-Hz-Signal. Für die Messung habe ich einfach die Hand nahe an den Eingang gehalten, sodass die Brummspannung eingekoppelt wurde.


µPython  Optimierungen von Andreas

Das Variablen-Gespiele mit a = 1+1 kann man auch außerhalb des Programmlaufs mit "ticks_us()" messen.  Deswegen habe ich mal den Code effizienter umgeschaltet, so dass du für weitere Experimente auch einen Triggerpunkt hast.

#OLEDosziV2.01.py Scope
from machine import Pin, I2C, ADC
from ssd1306 import SSD1306_I2C
from time import sleep, sleep_us, ticks_us, ticks_diff

def read(adc, wait = 315):
    sleep_us(wait)
    return 63 - adc.read_u16() // 1366

oled = SSD1306_I2C(128, 64, I2C(0, scl=Pin(1), sda=Pin(0), freq=400_000))
adc = ADC(2)
u0 = 63

while True:
    start = ticks_us()
    array = [read(adc, 328) for _ in range(128)]
    lauf = ticks_diff(ticks_us(), start)
    oled.fill(0)
    oled.text("3.3V  10ms/div",5,0)
    oled.line(0, 60, 0, 64, 1)
    oled.line(20, 60, 20, 64, 1)
    oled.line(40, 60, 40, 64, 1)
    oled.line(60, 60, 60, 64, 1)
    oled.line(80, 60, 80, 64, 1)
    oled.line(100, 60, 100, 64, 1)
    oled.line(120, 60, 120, 64, 1)

    for i in range(0,128):
        u=array[i]
        if i > 0:
            oled.line(i, u0, i+1, u, 1)
        u0=u
    oled.show()
    sleep(1)


Damit kannst du mit dem zweiten Wert (hier noch 328) beim Aufruf den Triggerpunkt verschieben. Array ist nicht wirklich notwendig, weil du das auch mit einer LIST machen. Und eine Bitte  noch:  Diese C++ Unart alles in () zu setzen wie bei "while" oder "if" ist in Python verpönt, und führt nur zu Ausführungsverzögerungen im nsek Bereich.  Sagen wir mal so, die CPU muss mehr machen als notwendig. Je nach Klammerninhalt sind das mind. 7 Takte zusätzlich, was du auch mit "ticks_cpu()" nachmessen kannst .

So kannst du auch bei LIST als Array-Ersatz statt

liste = []
for _ in range(128):
    liste.append(0)

folgendes schreiben:

liste = [0 for _in range(128)]

Du musst auch eine LIST in der Größe nicht vordefinieren. Ausgehend von deinem Basis Programm musste ich das WAIT als Übergabeparamter zur Zeitsteuerung von 309 auf 328 erhöhen, damit die Kurve auf dem OLED wieder still steht. Damit du einen Anhaltspunkt erhältst, wie die Durchlaufzeit der 128 Abfragen ist, habe ich noch die Variable "lauf" eingefügt, damit du eine bessere Möglichkeit hast zu sehen, wie hoch doch die Laufzeitunterschiede sein können,  und du damit mehr Spielraum für weitere Versuche hast

Ebenso ist es nicht notwendig einen ADC Pin, welcher als ADC Eingang genutzt wird noch zusätzlich als Pin.IN zu definieren. Das hat  jetzt keine Auswirkung auf den Programmlauf, aber wenn der Speicher auf Grund der Programmgröße knapp wird, kann man sich solche Zeilen einfach ersparen. Und man importiert nur die Funktionen aus einer Bibliothek die man wirklich braucht. Du hattest erst PIN und I2C importiert, dann aber wegen des ADCs noch einmal die gesamte Bibliothek. Irgendwie Platzverschwendung.

Falls du ZThonny als IDE nutzt, in der Kommando Zeile kannst du eine Bibliothek importieren, und dann mit der Erweiterung über das Menü Ansicht, Variablen und Objektinspektor ganz gezielt nachschauen, was diese Bibliothek enthält. So kannst du auch schreiben:

from machine import Pin as GPIO, I2C, ADC as Analogeingang

Damit heißen dann die Funktionen im Programmaufruf GPIO statt Pin, und Analogeingang statt ADC.

Antwort BK: Hallo Andreas, ich bin dir sehr dankbar für die Hinweise. Ich war nie ein richtiger Programmierer. Bei mir läuft das so, wie das Basteln mit dem Lötkolben, wenn es funktioniert, bin ich fertig. Die Zeitverzögerung mit einer Zählschleife war mir auch schon etwas peinlich. Ich hatte nur die Millisekunden gefunden und brauchte was kürzeres. Da habe ich es so gemacht wie vor über 40 Jahren schon in BASIC. Aber deine Tipps werden mir bei weiteren Programmen sehr helfen.


Picoskop mit umschaltbarer Zeitbasis



Nach den Hinweisen von Andreas habe ich das Programm noch mal aufgebohrt. Es gibt nun acht Zeitmessbereiche von 100 ms/div bis 0,5 ms/div. Die Zeitmessung beruht jetzt auf ticks_us(), einer unbeirrbaren Mikrosekunden-Uhr im Hintergrund. Für jede Messung wird der Zeitpunkt nach dem Start einer Serie berechnet und dann in einer Schleife abgewartet.

Für die schnellsten Messungen war es nötig, den Controller auf 180 MHz hochzutakten. Für die Umschaltung der Messbereiche wird nur ein Tastschalter gebraucht. Weil er sich mit seinen vier Beinchen so breit macht, mussten zwei Einmgänge, p5 und p7 initialisiert werden. Das Programm braucht nur p5 auszuwerten. Wenn man lange drückt, wird die Zeitauflösung höher, bei einem kurzen Druck wird sie gröber. Nach dem Start sind 5 ms/div eingestellt.

Zusätzlich wurde mit einem PWM-Ausgang ein Testsignal mit 1 kHz erzeugt. Damit gibt es ein Referenzsignal wie an vielen großen Oszilloskopen. Es wird hier gerade durch ein Tiefpassfilter mit 2,4 k und 100 nF geformt.

#OLEDoszi2.py Scope
from machine import Pin, I2C, ADC, PWM, freq as CPU_freq
from ssd1306 import SSD1306_I2C
from time import sleep, sleep_us, ticks_us

CPU_freq(180000000)
u2 = machine.ADC(2)
ad_in = Pin(28, Pin.IN)
p5 = Pin(5, Pin.IN, Pin.PULL_UP)
p7 = Pin(7, Pin.IN, Pin.PULL_UP)
i2c = I2C(0, scl=Pin(1),sda=Pin(0),freq=400000)
oled = SSD1306_I2C(128,64,i2c)
pwm1 = PWM(Pin(2))
pwm1.freq(1000)
pwm1.duty_u16(32768)

timebase = [('3.3V  100ms/div', 5000),
            ('3.3V   50ms/div', 2500),
            ('3.3V   20ms/div', 1000),
            ('3.3V   10ms/div', 500),
            ('3.3V    5ms/div', 250),
            ('3.3V    2ms/div', 100),
            ('3.3V    1ms/div', 50),
            ('3.3V  0.5ms/div', 25)]
n=4
x= [0 for _ in range(128)]

while True:
    txt = timebase [n][0]  
    dt = timebase [n][1]
    start = ticks_us()
    for i in range(128):
        x[i]=u2.read_u16()
        start+=dt
        while start > ticks_us():
            pass
    oled.fill(0)
    oled.text(txt,5,0)
    for j in range(7):
        oled.line(j * 20, 60, j * 20, 64, 1)  
    for i in range(128):
        u=63-x[i]//1366
        if (i>0):
            oled.line(i, u0, i+1, u, 1)
        u0=u
    oled.show()
    t=0
    for i in range(20):
        sleep (0.05)
        if p5.value()==0:
            t+=1
    if t>0:       
        n-=1
    if t> 7:
        n+=2
    if n<0:
        n=7
    if n>7:
        n=0

(Update 10.2.23 18 Uhr)



Hier sieht man das originale Rechtecksignal in der schnellsten Einstellung. Wenn man genau hinsieht, erkennt man eine leichte Zeitverschiebung gegenüber den Skalierungsstrichen. Das ist ein Hinweis darauf, dass die 180 MHz bei diesem Versuch noch nicht ganz ausreichten. Mit der letzten Änderung wurde das Programm noch etwas schneller, jetzt passt es.



Bei 1 ms/div stimmte die Skala dagegen genau. Das gewählte Zeitmessverfahren arbeitet also wie gewünscht.


Picoskop mit Signalgenerator



Nach dem Vorbild von Dietrichs Geschwindigkeitsmesser habe ich das Display nun in der oberen Reihe angeschlossen und mit einem Zwischenstecker höher gelegt. SCL und SDA liegen jetzt an GP19 und GP18. Die I2C-Taktrate wurde im Interesse der hohen Datensicherheit auf 100 kHz reduziert. Der Taster zur Änderung der Abtastrate wurde weiter nach rechts verlegt. Damit ist nun Platz für insgesamt fünf PWM-Ausgänge mit Frequenzen zwischen 100 Hz und 30 MHz, die man immer mal brauchen kann.

#OLEDoszi3.py Scope
from machine import Pin, I2C, ADC, PWM, freq as CPU_freq
from ssd1306 import SSD1306_I2C
from time import sleep, sleep_us, ticks_us

CPU_freq(180000000)
u2 = machine.ADC(2)
ad_in = Pin(28, Pin.IN)
sw = Pin(15, Pin.IN, Pin.PULL_UP)
sw2 = Pin(13, Pin.IN, Pin.PULL_UP)
i2c = I2C(1, scl=Pin(19),sda=Pin(18),freq=100000)
oled = SSD1306_I2C(128,64,i2c)
pwm0 = PWM(Pin(0))
pwm0.freq(100)      #100 Hz
pwm0.duty_u16(32768)
pwm1 = PWM(Pin(2))
pwm1.freq(1000)     #1 kHz
pwm1.duty_u16(32768)
pwm2 = PWM(Pin(4))
pwm2.freq(10000)    #10 kHz
pwm2.duty_u16(32768)
pwm3 = PWM(Pin(6))
pwm3.freq(100000)   #100 kHz
pwm3.duty_u16(32768)
pwm3 = PWM(Pin(6))
pwm3.freq(1000000)  # 1 MHz
pwm3.duty_u16(32768)
pwm3 = PWM(Pin(8))
pwm3.freq(30000000)  # 30 MHz
pwm3.duty_u16(32768)

timebase = [('3.3V  100ms/div', 5000),
            ('3.3V   50ms/div', 2500),
            ('3.3V   20ms/div', 1000),
            ('3.3V   10ms/div', 500),
            ('3.3V    5ms/div', 250),
            ('3.3V    2ms/div', 100),
            ('3.3V    1ms/div', 50),
            ('3.3V  0.5ms/div', 25)]
n=7
x= [0 for _ in range(128)]
while True:
    txt = timebase [n][0]  
    dt = timebase [n][1]
    start = ticks_us()
    for i in range(128):
        x[i]=u2.read_u16()
        start+=dt
        while start > ticks_us():
            pass
    oled.fill(0)
    oled.text(txt,5,0)
    for j in range(7):
        oled.line(j * 20, 60, j * 20, 64, 1)  
    for i in range(128):
        u=63-x[i]//1366
        if (i>0):
            oled.line(i, u0, i+1, u, 1)
        u0=u
    oled.show()
    t=0
    for i in range(20):
        sleep (0.05)
        if sw.value()==0:
            t+=1
    if t>0:       
        n-=1
    if t> 7:
        n+=2
    if n<0:
        n=7
    if n>7:
        n=0

Die Rechtecksignale bis 10 kHz kann der Poco selbst noch darstellen. Die höheren Frequenzen dienen zum Test mit anderen Geräten. Das 30-MHz-Signal wird an meinem 20-MHz-Hameg noch mit 2 Vss angezeigt, allerdings als Sinus. Das Oszilloskop arbeitet selbst wie ein wirksames Tiefpassfilter.