RPi-Pico MicroPython Speed-Test                 

von Andreas                                

Elektronik-Labor  Projekte  Mikrocontroller  Raspberry            




Ab der Version 1.19.x (ab 06/2022) ist MicroPython sehr viel näher an Python 3 gerückt. Viele Dinge sind jetzt deutlich besser und schneller geworden. Eine gute Übersicht bietet das Buch Python 3, Das umfassende Handbuch von Johannes Ernesti und Peter Kaiser,  https://www.rheinwerk-verlag.de/python-3-das-umfassende-handbuch/.

Neu ist eine Möglichkeit der schnelleren Funktionsaufrufe, bei der man die Funktion wie eine Variable im RAM gehalten: read = adc.read_u16. Diese Programmiermethode nennt man "lookup".  In einer Schleife val = [read() for _ in range(Count)] ist nun die Ausführung wesentlich schneller, allerdings wird auch viel mehr RAM verbraucht. Auch eine Zählschleife ohne eigentliche Zählvariable for _ in range(Count) ist neu und effizienter. Jedoch muss man beachten, dass jede LOOKUP Definition auch RAM belegt, weil dieser Teil kompiliert im RAM erhalten bleibt. Daher sollte man wirklich darauf achten, wo wird wirklich Ausführungsgeschwindigkeit benötigt, damit man sich den RAM nicht permanent zumüllt. Jede Variable / Bounded Methode, die nur temporär benötigt wird, kann man jederzeit mit "del" wieder aus dem Arbeitsspeicher entfernen. Eine andere Methode ist, dass man komplexe Ablaufe in Funktionen (mit "def" beginnend ) auslagert. Jede Variable, die im Hauptprogrammteil __main__ initialisiert wird, belegt auch oder ab ihrer Initialisierung RAM. Hingegen sind lokal Variablen, die erst in Funktionen initiiert werden und werden mit Beendigung der Funktion auch wieder aus dem RAM entfernt. Ausnahme bildet die hier schon genannte Methode eines Lookup.

Neu ist nun auch die "F-String-Formatierung", wie sie in den aktuellen Python-Versionen ab 3.6 schon implementiert sind. Weiterhin die Möglichkeit, den Prozessortakt frei einzustellen. Zum Overclocken muss man zuerst die Einschalt-Clock Frequenz auslesen, und in einer Variablen die nicht mit "del" gelöscht werden darf, zwischengespeichert werden. Hier ist jedoch zu beachten, dass der µPython-Interpreter nur Geschwindigkeiten zwischen 20 und 240 MHz akzeptiert, wenn eine Konnektivität via USB besteht. In diesem Bereich werden auch die korrekten Funktionen aller Schnittstellen I²C / SPI / UART / USB sichergestellt. Im Standalone-Betrieb, bei einer Vcc von 5,0V über PIN 40 oder einer Vcc ab 3,3 V über PIN39 kann man, solange kein Zugriff auf die Bus-Schnittstellen stattfindet, auch den CPU Takt temporär auf bis zu 480 MHz anheben. Hier muss man dann allerdings aufpassen, dass man keinen HW-IRQ via Pin.irq, Timer, oder Statemachine aktiv hat. Um Überhitzungen im Dauerbetrieb zu vermeiden, sollte man den CPU-Takt wieder auf Default setzen, wenn diese Rechen-Power nicht benötigt wird, oder einen CPU Kühlkörper verwenden.

Für eine Laufzeitdiagnose kann man aus der Bibliothek "utime" mit "ticks_us()" (Microsekunden ), "ticks_ms()" (Millisekunden ) und "ticks_cpu()" (CPU-Takte )  nutzen, um sehr genaue Messungen durchzuführen. Diese Funktionen werden hier benutzt, um die Programmlaufzeiten genauer zu untersuchen und die Ergebnisse auszuwerten.



from machine import Pin, ADC, freq as CPU_freq
from utime import ticks_us, ticks_diff

def messen(frequency):
    print(f'CPU Freq : {(frequency/1_000_000):3.0f} MHz')
    start=ticks_us()
    val = [read() for _ in range(Count)]
    lauf=ticks_diff(ticks_us(), start)
    print('Messen mit Funktions-Lookup')
    print(f'Laufzeit für {Count:5d} Messungen = {(lauf/1_000):3.3f} ms')
    print(f'Sampling: {(1 / ((lauf / Count) / 1_000)):6.1f} KSPS.\n')
    del val

    start=ticks_us()
    val = [adc.read_u16() for _ in range(Count)]
    lauf=ticks_diff(ticks_us(), start)
    print('Messen ohne Funktions-Lookup')
    print(f'Laufzeit für {Count:5d} Messungen = {(lauf/1_000):3.3f} ms')
    print(f'Sampling: {(1 / ((lauf / Count) / 1_000)):6.1f} KSPS.\n')
    print('-'*40)
    del val

Count = 10_000
CPU_Frequency = (20,40,60,80,100,120,125,133,166,200,240)

OLD_CPU = CPU_freq()
adc = ADC(2)
read = adc.read_u16 # Lookup

for clock in CPU_Frequency:
    c = int(clock * 1_000_000)
    CPU_freq(c)
    messen(c)

CPU_freq(OLD_CPU)



Das Ergebnis:


MicroPython v1.19.1 on 2022-06-18; Raspberry Pi Pico with RP2040

Type "help()" for more information.
>>> %Run -c $EDITOR_CONTENT
CPU Freq :  20 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 469.286 ms
Sampling:   21.3 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 532.685 ms
Sampling:   18.8 KSPS.

----------------------------------------
CPU Freq :  40 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 273.374 ms
Sampling:   36.6 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 277.920 ms
Sampling:   36.0 KSPS.

----------------------------------------
CPU Freq :  60 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 195.416 ms
Sampling:   51.2 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 216.443 ms
Sampling:   46.2 KSPS.

----------------------------------------
CPU Freq :  80 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 154.013 ms
Sampling:   64.9 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 169.757 ms
Sampling:   58.9 KSPS.

----------------------------------------
CPU Freq : 100 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 126.824 ms
Sampling:   78.8 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 139.467 ms
Sampling:   71.7 KSPS.

----------------------------------------
CPU Freq : 120 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 110.184 ms
Sampling:   90.8 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 120.679 ms
Sampling:   82.9 KSPS.

----------------------------------------
CPU Freq : 125 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 106.860 ms
Sampling:   93.6 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 116.969 ms
Sampling:   85.5 KSPS.

----------------------------------------
CPU Freq : 133 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 102.429 ms
Sampling:   97.6 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 112.285 ms
Sampling:   89.1 KSPS.

----------------------------------------
CPU Freq : 166 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 87.147 ms
Sampling:  114.7 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 94.719 ms
Sampling:  105.6 KSPS.

----------------------------------------
CPU Freq : 200 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 76.756 ms
Sampling:  130.3 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 83.073 ms
Sampling:  120.4 KSPS.

----------------------------------------
CPU Freq : 240 MHz
Messen mit Funktions-Lookup
Laufzeit für 10000 Messungen = 68.628 ms
Sampling:  145.7 KSPS.

Messen ohne Funktions-Lookup
Laufzeit für 10000 Messungen = 73.871 ms
Sampling:  135.4 KSPS.

----------------------------------------
>>>

Der Controller wurde hier bis 240 MHz übertaktet und hat dabei eine Abtastrate von 145,7 Kilosamples pro Sekunde erreicht. Damit wird mit  MicroPython sogar ein Audiosampling mit 96 kSPS möglich.

Abschlussbemerkung: Auch wenn der RP2040 unter / mit C/C++ Samplingsraten bis 217 kSPS erreicht, sollte man nicht vergessen, dass µPython eine Interpreter-Sprache ist. Für einfache bis gehobene Regel- und Steuerungsaufgaben, sowie für Einsteiger-Experimente sogar für Echtzeitanwendungen, wenn auf User-Events reagiert werden muss ("Pin.irq(trigger Pin.IRQ_FALLING, ())" oder "Pin.irq(trigger Pin.IRQ_RISING, ())" ) erreicht µPython hier ansprechende Reaktionszeiten im zweistelligen µSek Bereich. Man beachte auch, dass dieses Test-Programm nur im primären CORE der CPU ausgeführt wird.



Nachtrag: Ein Programm zur Geschwindigkeitsoptimierung

from utime import ticks_cpu, ticks_diff

print('LIST füllen')
print('Liste mit Werten füllen')
print('List klasssich mit append ergänzen')

start = ticks_cpu()
table = [] # LIST anlegen
for i in range(1_000):
    table.append(i)
print(ticks_diff(ticks_cpu(), start), 'CPU Takte\n')
del table

print('Liste über Schachtlung füllen')
start = ticks_cpu()
table = [i for i in range(1_000)]
print(ticks_diff(ticks_cpu(), start), 'CPU Takte\n')
del table

print('Liste mit Platzhaltern füllen')
print('List klasssich mit append ergänzen mit Schlefenvariable')
start = ticks_cpu()
table = [] # LIST anlegen
for i in range(1_000):
    table.append(0)
print(ticks_diff(ticks_cpu(), start), 'CPU Takte\n')
del table

print('Liste mit Platzhaltern füllen')
print('List klasssich mit append ergänzen ohne Schlefenvariable')
start = ticks_cpu()
table = [] # LIST anlegen
for _ in range(1_000):
    table.append(0)
print(ticks_diff(ticks_cpu(), start), 'CPU Takte')
print('Hier macht eine Schleife ohne Zählvariable keinen Sinn\n')
del table

print('Liste über Schachtlung füllen')
start = ticks_cpu()
table = [0 for _ in range(1_000)]
print(ticks_diff(ticks_cpu(), start), 'CPU Takte')
print('hier macht die Schleife ohne Zählvariable Sinn\n')
del table

# Vorbereitung
table = [i for i in range(1_000)]
print('Werte aus einer LIST heraussortieren mit 1 Parameter')
start = ticks_cpu()
sortierung = []
for value in table :
    if value > 738:
        sortierung.append(value)
print(ticks_diff(ticks_cpu(), start), 'CPU Takte\n')
del sortierung

print('Sortierung mit 1 Parameter innerhalb einer Schachtlung')
start = ticks_cpu()
sortierung = [value for value in table if value > 500]
print(ticks_diff(ticks_cpu(), start), 'CPU Takte\n')
del sortierung

# If - Else Abfragen
temp = 13.7 # Vorbereitng
print('IF-Else Abfragen in klassicher Aufteilung')
start = ticks_cpu()
if temp > 15:
    result = True
else:
    result = False
print(ticks_diff(ticks_cpu(), start), 'CPU Takte\n')

print('IF-Else Abfragen in einem Block')
start = ticks_cpu()
result = (True if temp > 15 else False)
print(ticks_diff(ticks_cpu(), start), 'CPU Takte\n')


Ergebnisse:

MicroPython v1.19.1 on 2022-06-18; Raspberry Pi Pico with RP2040

Type "help()" for more information.
>>> %Run -c $EDITOR_CONTENT
LIST füllen
Liste mit Werten füllen
List klasssich mit append ergänzen
12740 CPU Takte

Liste über Schachtlung füllen
3337 CPU Takte

Liste mit Platzhaltern füllen
List klasssich mit append ergänzen mit Schlefenvariable
11177 CPU Takte

Liste mit Platzhaltern füllen
List klasssich mit append ergänzen ohne Schlefenvariable
11210 CPU Takte
Hier macht eine Schleife ohne Zählvariable keinen Sinn

Liste über Schachtlung füllen
3228 CPU Takte
hier macht die Schleife ohne Zählvariable Sinn

Werte aus einer LIST heraussortieren mit 1 Parameter
9192 CPU Takte

Sortierung mit 1 Parameter innerhalb einer Schachtlung
4938 CPU Takte

IF-Else Abfragen in klassicher Aufteilung
67 CPU Takte

IF-Else Abfragen in einem Block
31 CPU Takte