micro:bit-Logger

von Ralf Beesner
Elektronik-Labor    Projekte    Microbit 







Überblick 

Eigentlich mag ich den BBC micro:bit nicht, aber er wird vermutlich recht populär werden, so dass man kaum an ihm vorbei kommt - und MicroPython auf dem micro:bit finde ich durchaus interessant. Da ich gerade mit Python-GUIs herumbastele, habe ich mal versucht, meinen AD210-Nachbau und das darauf laufende BASCOM-Programm durch den micro:bit und ein vergleichbares MicroPython-Programm zu ersetzen. Das gelang jedoch nicht 1:1.

Entstanden sind zwei Programme: eines läuft auf dem micro:bit und sendet "auf Befehl" die ADC-Spannungen an den Pins 0-2 per USB zum PC, das andere läuft auf dem PC und stellt die Messwerte in einer grafischen Benutzeroberfläche (GUI) dar.

Die micro:bit-Lösung hat den Charme, dass man sie sie "mal eben" ausprobieren kann, ohne löten zu müssen.

Die serielle Schnittstelle des micro:bit

Die serielle Verbindung zum micro:bit fungiert in erster Linie als Kommandozeile für das darauf laufende MicroPython. Man kann die serielle Schnittstelle aber auch aus MicroPython-Anwendungsprogrammen heraus nutzen.

Der Versuch, das AD210-Protokoll direkt auf den micro:bit umzusetzen, schlug jedoch fehl, weil die serielle Verbindung nicht Byte-transparent ist. Empfängt der Micropython-Interpreter auf der seriellen Schnittstelle z.B. ein "rohes" Byte "4" (also nicht das ASCII-Zeichen für die Ziffer "4"), reicht er das Byte nicht an das Anwendungsprogramm weiter, sondern interpretiert es als (Ctrl-D) und löst einen "soft reset" aus.

Ähnliches gilt auch für die Senderichtung; versucht man, ADC-Werte als "rohe" Bytes vom micro:bit aus zu senden, macht der Interpreter aus allen Bytes über 127 ein UTF8-Zeichen, das aus 2 Bytes besteht. Man sollte daher nur Strings aus "druckbaren" ASCII-Zeichen verwenden. Ich habe daher das AD210-Protokoll mit ASCII-Zeichen nachempfunden. Das geänderte Protokoll ist im Kopf des Quelltextes beschrieben:

# AD210 Nachbau
# es waren einige Aenderungen erforderlich, weil der UART 
# nicht Byte-transparent ist
# Beispiele:
# Bytes > 127 werden in 2 Zeichen Unicode umgewandelt
# Byte 4 loest einen soft reset aus

# Befehle
# u: Kennung abfragen, Antwort: U
# a: ADC0 abfragen, Antwort: String(!) 0...1023
# b: ADC1 abfragen, Antwort: String(!) 0...1023
# c: ADC2 abfragen, Antwort: String(!) 0...1023
# e + 0....1023 : PWM an Pin2 

# license: wtfpl
# http://www.wtfpl.net/txt/copying/

from microbit import *
uart.init(115200)
pin2.set_analog_period(10)

while (1):

    if (uart.any() != 0):
        char_command = uart.read(1)
        command = ord(char_command)

        # Kennung zuruecksenden - diese Variante funktioniert eigenartigerweise nicht:
        # if char_command == "u":
        # daher war oben mit "ord" die Umrechnung in eine Zahl erforderlich und es werden Zahlen abgefragt:
        if command == 117:
            uart.write("U")

         # Analogwert des Pin0 senden
        elif command == 97:
            value = pin0.read_analog()
            value_str = str(value)
            uart.write(value_str)

        # Analogwert des Pin1 senden
        elif command == 98:
            value = pin1.read_analog()
            value_str = str(value) 
uart.write(value_str) # Analogwert des Pin2 senden elif command == 99: value = pin2.read_analog() value_str = str(value)
uart.write(value_str) # PWM an Pin2 ausgeben elif command == 101: mystring = uart.read() try: pwm = int(mystring)
if pwm > 1023: pwm=1023 pin2.write_analog(pwm) except: pass


Der PWM-Befehl funktioniert jedoch nicht gescheit. Wenn mehrere Bytes in rascher Folge an den micro:bit gesendet werden, treten sporadisch Übertragungsfehler auf. Das wäre nicht weiter schlimm, wenn es auf dem micro:bit den Befehl "flushInput()" gäbe, mit dem man vorsorglich vor jedem Zyklus den eventuellen "Zeichenmüll" wegräumen könnte.

Aber das serielle micro:bit-Modul ist sehr primitiv und bietet diese Möglichkeit nicht. Durch fehlerhaft übertragene Zeichen kommt die serielle Übertragung völlig außer Tritt.

Die Bitrate beträgt 115200 bit/s, weil das die "natürliche" Bitrate ist, auf die der micro:bit nach einem Reset zurückfällt und mit der man im Repl-Modus des mu-Editor mit dem micro:bit kommuniziert. Das vermeidet Konfusion und scheinbare Fehlfunktionen - z.B. wenn unklar ist, ob der micro:bit gerade im Repl-Modus ist oder gerade das selbstgeschriebene Programm läuft.

Messbereich

Legt man die zu messenden Spannungen über Vorwiderstände von ca. 10 kOhm (Schutz gegen versehentliches "Grillen" des micro:bit) direkt an die Pins 0 bis 2, erhält man einem Messbereich von 0 ... 3,3V. Man kann auch wie beim AD210-Nachbau die Messspannung im Verhältnis 9:1 herunter teilen (Spannungsteiler aus 1 MOhm und 120 kOhm) und erhält dann einen Messbereich 0...30V.

Selbst völlig "krumme" Teilerverhältnisse lassen sich verwenden, da man im PC-GUI-Programm ohnehin den Umrechnungsfaktor anpassen muss, mit dem die ADC-Werte 0 .... 1023 in Spannungen umgerechnet werden.

Der Vorteiler sollte jedoch nicht zu hochohmig werden, da die micro:bit-Eingänge intern über Pullup-Widerstände von 10 MOhm auf "high" gezogen werden, was die Messwerte merkbar verfälschen kann.

GUI-Programm(e)

Eigentlich sind es zwei - ubit-logger_gui_2ch.py ist die Variante mit PWM, die nicht gescheit funktioniert, und ubit-logger_gui_3ch.py. Der Code entspricht weitgehend der oben verlinken Version für das AD210.

Beim Eintragen des Logging-Intervalls muss man einen Punkt statt eines Kommas als Dezimaltrenner verwenden (hmmm... das könnte man ja eigentlich auch automatisch im Programm-Code abfangen...).

Die Drei-Kanal-Variante des micro:bit-Loggers ist im Vergleich zur originalen AD210-Version optisch etwas umgestellt, die GUI-Elemente für den Schieberegler und die dazugehörige Funktion sind entfernt.

Obwohl das ganz übler Stil ist, habe ich den Code für die ADC-Abfrage einfach mit copy&paste verdreifacht, statt eine separate Funktion anzulegen, die dreimal aufgerufen wird. So ist es übersichtlicher, einfacher nachvollziehbar und funktioniert genau so gut.

Kinder, wenn Ihr mal richtige Python-Profis werden wollt: nicht nachmachen. Allenfalls als schlechtes Beispiel verwenden!


Download des Quellcodes:  0517-ubit-logger.zip

#!/usr/bin/env python

# license: wtfpl
# http://www.wtfpl.net/txt/copying/

import thread
import time
import serial
from Tkinter import * import tkMessageBox

file_is_open = 0 #----- thread, der unabhaengig von der gui laeuft, vermeidet blocking: def get_values(): port, ser = choose_serial() text8 = port
time_old = time.time() scale_old=0 pwm = "0.00" divider = 1023/3.3 # Messbereich 3,3v (ohne Spannungsteiler) while (1): ## ADC0 abfragen ser.flushInput() ser.write("a") res0 = ser.read(5) res01 = float(res0) res02 = "{0:2.2f}".format(res01/divider) lb_adc0.delete(1.0 , 2.0) lb_adc0.insert(INSERT,str(res02)+"V") ## ADC1 abfragen ser.flushInput() ser.write("b") res1 = ser.read(5) res11 = float(res1) res12 = "{0:2.2f}".format(res11/divider) lb_adc1.delete(1.0 , 2.0) lb_adc1.insert(INSERT,str(res12)+"V") ## ADC2 abfragen ser.flushInput() ser.write("c") res2 = ser.read(5) res21 = float(res2) res22 = "{0:2.2f}".format(res21/divider) lb_adc2.delete(1.0 , 2.0) lb_adc2.insert(INSERT,str(res22)+"V") ## ins File schreiben: if (file_is_open == 1): interval = get_interval() time_new = time.time() if ((time_new - time_old) >= interval): time_old = time_new
try: ltime = time.localtime()
h, m , s = ltime[3:6] time_string = "{0:02d}:{1:02d}:{2:02d}".format(h,m,s) logfile.write (time_string + chr(9) + res02 + chr(9) + res12 +chr(9) + res22 + "\r") # logfile.write (time_string + chr(9) + res0 + chr(9) + # res1 + "\r") except: print "writing failed" #---- Ende thread ----- def get_interval(): get_interval = entry5.get()
try: interval= float(get_interval) except: interval = 1 return interval


def start_log(): global logfile
global file_is_open

filename = "uBitLogger.csv" getfilename= entry6.get() if getfilename != "": filename = getfilename
logfile = open (filename,"a+") # +a: append file_is_open = 1 print "filename=", filename


def stop_log(): global file_is_open
try: logfile.close() file_is_open = 0 except: pass #----- uBitLogger suchen ----- def choose_serial(): global port_selected
global ser

speed = "115200" portnames = scan_serial() print "found", portnames
ubitport ="" for port in portnames: try: ser = serial.Serial(port,speed,timeout=0.02) time.sleep(0.1) ser.flushInput() ser.flushOutput() ser.write("u") time.sleep(0.1) if (ser.inWaiting() != 0): x = ser.read() if (x == "U"): # Lebenszeichen des uBitLogger print "uBitLogger connected to" , port
text8["text"] = "Port: " + port
ubitport = port
break except: pass if len(ubitport) == 0: print "no uBitLogger found" tkMessageBox.showerror("Error", "kein uBitLogger gefunden!") return ubitport, ser

#----- vorhandene serielle Schnittstellen suchen ----- def scan_serial(): portnames = [] # Windows for i in range(127): try: name = "COM"+str(i) s = serial.Serial(name ) s.close() portnames.append (name) except: pass # Linux (HID serial) for i in range(8): try: name = "/dev/ttyACM"+str(i) s = serial.Serial(name) s.close() portnames.append (name) except: pass if len(portnames) == 0: print "no serial found" tkMessageBox.showerror("Error", "Keine serielle Schnittstelle gefunden!")
quit() return portnames



#----- Programm beenden ------ def ende(): print("Ende !") try: thread.stop(get_values, ()) file_is_open = 0 ser.write("e"+ "0") ser.close() logfile.close() except: pass main.destroy() # ----- GUI-Elemente --------------- main = Tk() #frame1 frame1 = (Frame(main)) frame1.pack() text1 = Label(frame1, text="Steuerprogramm fuer uBitLogger", font="bold") text1.pack(anchor="w",padx=20) #frame3 # ADC0 frame3 = (Frame(main)) frame3.pack(anchor="w", padx = 10) label0 = Label(frame3, text="ADC0") label0.pack(side="left") lb_adc0 = Text(frame3, bg="orange", width=5, height=1, font=(12)) lb_adc0.pack(side="left", padx=6, pady=10) # ADC1 label1 = Label(frame3, text="ADC1") label1.pack(side="left") lb_adc1 = Text(frame3, bg="orange", width=5, height=1, font=(12)) lb_adc1.pack(side="left", padx=6) # ADC2 label2 = Label(frame3, text="ADC2") label2.pack(side="left") lb_adc2 = Text(frame3, bg="orange", width=5, height=1, font=(12)) lb_adc2.pack(side="left",padx=6) #frame5, Eingabe Intervall frame5 = (Frame(main)) frame5.pack(anchor = "w") entry5 = Entry(frame5, width=5,text="0.1") entry5.pack(side="left", padx = 10) text5 = Label(frame5, text="Intervall der Logfile-Eintraege (in s) ") text5.pack(side="left") #frame6, Eingabe Logfile-Name frame6 = (Frame(main)) frame6.pack(anchor="w") entry6 = Entry(frame6, width=16) entry6.pack(side="left", padx = 10) text6 = Label(frame6, text = "Name des Logfiles ") text6.pack(side="left", pady=10) #frame7, Start/Stop Log frame7 = (Frame(main)) frame7.pack(anchor="w") widget_start = Button(frame7, text="Start Log",command=start_log) widget_start.pack(side="left", padx = 10) widget_stop = Button(frame7, text="Stop Log",command=stop_log) widget_stop.pack(side="left") #frame8, Quit-Button text8 = Label(main, text= "") text8.pack(side="left", padx = 10, pady = 10) button1 = Button(main, text="Quit",command=ende) button1.pack(side="right", padx = 10) # ----- Main Loop--------------- thread.start_new_thread(get_values, ()) # thread fuer Abfrage der seriellen Schnitttstelle main.mainloop() # ruft GUI auf




Elektronik-Labor   Projekte   Microbit