UBasic-avr
Aus BraLUG-Wiki
Achtung/Hinweis: diese Projektseite entspricht nicht mehr dem aktuellen Stand der Entwicklung! Die aktuellsten Informationen zum BASIC-Interpreter sind auf der Projektseite auf mikrocontroller.net zu finden. Im dortigen SVN sind auch die neuesten Quellcode-Versionen verfügbar.
Motivation
Welcher Mikrocontroller-Programmierer kennt das Problem nicht: man hat eine schicke Firmware auf den MC gebrannt, braucht schnell eine neue (einfache) Funktionalität und will/kann nicht gleich an den C-Code der Firmware ran. Was liegt also näher, Funktionen beliebig "nachladen" und ausführen zu lassen? Dieses Ansinnen mit Binär-Code-Fragmenten zu machen, dürfte ein schwieriges, wenn nicht sogar unmögliches Unterfangen sein. Und man ist wieder von einem Kompiler abhängig. Script-Sprachen sind da viel besser geeignet, da sie verständlicher und leichter zu programmieren sind. Voraussetzung ist dabei natürlich, dass ein entsprechender Script-Interpreter auf der Zielplattform verfügbar ist.
Jetzt könnte man natürlich einen eigenen und u.U. speziell für Mikrocontroller designden Sprachsyntax entwickeln und implementieren (Bsp. ECMDSript bei ethersex). Viel sinnvoller erscheint es aber, wenn man auf alt bewährte und bekannte Dinge zurückgreift: jeder (alte) Programmierer hat mal mit Basic angefangen oder zumindestens davon gehört! Der Zufall wollte es, dass Adam Dunkle ein C-Gerüst für einen kleinen und ressourcenschonenden TinyBasic-Interpreter (uBasic) veröffentlicht hat. Mit minimalen Modifikationen ist das Ding auch auf einem AVR sofort lauffähig und beeindruckt durch den geringen Ressourcen-Verbrauch.
Auf dieses Gerüst aufsetzend, entsteht hier AVR-uBasic.
Ziel ist es, einen universellen Basic-Interpreter zu haben, der in AVR-Programme einfach eingebunden werden kann. Die Basic-Programme sollen dann über vorhandene Schnittstellen (seriell, Ethernet o.ä.) geladen werden oder sind auf einem externen Speichermedium (SD-Card, Dataflash o.ä.) verfügbar und einlesbar.
Diese Seite soll zum "Mitüberlegen" anregen. Vielleicht hat ja jemand noch tolle Ideen, Anmerkungen oder auch Kritik...
Testumgebung
Erste Versuche mit dem originalen uBasic kann man auf jeder Plattform machen, für die es auch einen Standard-C-Kompiler gibt. Das funktioniert auf Anhieb. Bei eigenen Experimenten habe ich ein paar kleinere Bugs gefunden und teilweise bereits behoben.
Interessant wird die Sache, wenn man zusätzlich Mikrocontroller-spezifische Befehle implementieren möchte. Dazu braucht man natürlich eine entsprechende Hardware-Umgebung. Ich habe mir dazu ein einfaches Board auf Lochraster mit folgenden Komponenten in der jeweiligen Standardbeschaltung aufgebaut:
- Mega168; 16MHz-Quarz; Reset-Taster
- MAX232 für die serielle Schnittstelle
- ISP-Anschluss, über den die Schaltung auch mit Strom versorgt wird
- ein paar herausgeführte I/O-Pins, um mal einen Taster oder eine LED anschliessen zu können
Über die serielle Schnittstelle können zu Testzwecken Basic-Programme geladen und gestartet werden (gesteuert über die Hauptschleife im Testprogramm). Die Ausgaben (PRINT-Befehl, Debug-Ausgaben) erfolgen ebenfalls über diese Schnittstelle.
Ursprünglich hatte ich damit angefangen den Basic-Quelltext mit in das C-Testprogramm reinzuschreiben. Das ist aber beim Testen diverser Kombinationen von Basic-Elementen schon etwas mühselig: C-Quelltext anpassen, übersetzen, flashen... Deshalb wurde recht schnell eine Möglichkeit geschaffen Basic-Quelltext über die serielle Schnittstelle nachzuladen und zu starten. Die entsprechenden Befehle dazu sind im Quelltext von main.c "dokumentiert" und mit Sicherheit nicht die tollste Lösung, aber zum Testen reicht es erstmal.
Mal abgesehen von den etwas beschränkten Möglichkeiten für Ein-/Ausgabe und Speicher, ist dieses kleine Mikrocontroller-Board leistungsstärker, als ein KC oder ähnliche damalige Home-Computer...;-)
Funktionsweise des Interpreters
Der uBasic-Interpreter unterteilt sich in zwei Komponenten:
- dem Tokenizer (tokenizer.*)
- dem eigentlichen uBasic (ubasic.*)
Der Tokenizer analysiert den zu interpretierenden Code schrittweise nach ihm bekannten Syntaxelementen. Dazu sind in tokenizer.c diverse Schlüsselwörter (keywords[]) und Einzelzeichen (singlechar()) definiert, nach denen der Programmtext durchsucht wird. Desweiteren stellt tokenizer.c diverse Funktionen zur Verfügung, um das aktuelle Token abzufragen, die Tokenanalyse fortzuführen sowie bei einigen Token deren Wert zurückzugeben (String, Variable, Wert etc.).
In ubasic.c ist die vorgeschriebene Reihenfolge der einzelnen Tokenelemente, welche damit den eigendlichen Basic-Syntax ausmachen, und die daraus resultierenden Reaktionen, teilweise über mehrere Prozeduren verteilt, implementiert. Dabei wird hauptsächlich zwischen Statements (statement()) und Expression-Elementen (expr() -> term() -> factor()...) unterschieden.
Einige der Funktionen/Prozeduren werden rekursiv aufgerufen (vorallem expr() in ubasic.c). Dies bringt, gerade bei den beschränkten Ressourcen auf einem Mikrocontroller, die Gefahr mit sich, dass der Stack überlaufen könnte. Hautsächlich könnte dies bei sehr komplexen Expressions auftreten. D.h., also, dem Interpreter bei solchen Problemen nicht zu komplexe Basic-Ausdrücke mit vielen Klammern vorwerfen.
Durch diese sinnvolle und konsquente Aufteilung der Interpreter-Funktionen ist es relativ leicht möglich weiter Syntax-Elemente hinzuzufügen. Der "Durchlauf" durch die einzelnen Interpreter-Elemente ist leicht zu verstehen. Einfach mal den Quelltext lesen!
Einbinden in eigene AVR-Programme
Das Einbinden des Interpreters in eigene Programme geht relativ einfach. Prinzipiell ist nur ubasic.h entsprechend zu includieren. Ggf. sind die Defines PRINTF (für Basic-Befehl print) und DEBUF_PRINTF (für evt. Debug-Ausgaben) in tokenizer.c, ubasic.c und ubasic_call.c an die eigene Ausgaberoutinen anzupassen. Desweiteren kann man in ubasic_config.h teilweise den Basic-Sprachumfang steuern. Bei Verwendung des Basic-Befehls call sollte man sich ubasic_call.* genauer anschauen.
Das abzuarbeitende Basic-Programm muss in dem Char-Array stehen, welches der Prozedur ubasic_init() übergeben wird. Wie das Programm in dieses Char-Array gelangt, ist jedem selbst überlassen. Die einzelnen Basic-Zeilen in dem Array müssen mit '\n' (0x0A) abgeschlossen sein.
Zur Abarbeitung des Basic-Programmes ist ungefähr folgender Konstrukt im eigenen Programm einzubauen:
ubasic_init(program); do { ubasic_run(); } while(!ubasic_finished());
ubasic_init() setzt einen internen Pointer auf den Anfang des Programmtextes und initialisiert ein paar weitere interne Variablen.
Die folgende do-while-Schleife wird solange abgearbeitet, bis das Basic-Programm endet, wobei ubasic_run() jeweils immer genau eine Basic-Zeile abarbeitet. Es ist also z.B. denkbar, in dieser Schleife auch noch das zu tun/aufzurufen, was wärend des Basic-Programmlaufes "parallel" im Mikrocontroller abgearbeitet werden soll. Anmerkung: es kann aber kein zweites Basic-Programm parallel laufen!
Eine weitere Variante, um die Abarbeitung des Basic-Programmes in einer bestehenden Hauptschleife einzubauen, könnte ungefähr so aussehen:
while (1) { ... // irgendwo Programm laden und ubasic_init(program) aufrufen ... if (!ubasic_finished) ubasic_run; ... }
Als Referenz für das Einbinden des Basic-Interpreters in eigene Programme, ist das Studium von main.c, welche im Quellcode-Archiv enthalten ist, angeraten.
Derzeitiger Sprachumfang
Siehe auch Dokumentation im Quelltextarchiv.
Prinzipiell sind folgende allgemeinen Basic-Elemente vorhanden:
- for next
- goto, gosub
- if then else
- print (auf Standardausgabe)
- Grundrechenoperationen
AVR-spezifische Befehle sind vorhanden:
- Setzen/Lesen des EEPROM
- Konfiguration/Setzen/Lesen I/O-Ports
- ADC auslesen (noch nicht vollständig implementiert...)
Mittels des Basic-Befehls call kann der Funktionsumfang des Interpreters drastisch erweitert werden.
Innerhalb von ubasic_config.h können AVR- und Basic-spezifische Dinge an- bzw. ausgeschaltet werden.
Bei Fehlern im Basic-Programm wird die Verarbeitung in der entsprechenden Zeile abgebrochen und eine beschreibende Fehlermeldung auf der Standard-Ausgabe ausgegeben.
Weitere Spracherweiterungen sollten ohne Probleme möglich sein. Siehe dazu auch die Beschreibung zur Funktionsweise des Interpreters bzw. den Quelltext selbst.
Desweiteren wären do-while- oder repeat-until-Schleifen manchmal ganz hilfreich, erfordern aber schon etwas mehr Aufwand bei die Implementierung (und Speicherplatz im wertvollen RAM --> siehe Funktionsweise von for-next oder gosub).
Ein paar mehr oder weniger sinnvolle Beispielprogramme
"Standard"-Basic-Programme
for-next, gosub-return, print
10 gosub 100 15 a=30 20 for i = 1 to a step 10 30 print "i=", i 40 next i 50 end 100 print "subroutine" 110 return
abs(), not()
10 print abs(-8) 20 print not(7) 30 end
Zufallswert
10 srand 20 for v = 1 to 2000 30 y=rand(9) 40 if y=0 then a=a+1 50 if y=1 then b=b+1 60 if y=2 then c=c+1 70 if y=3 then d=d+1 80 if y=4 then e=e+1 90 if y=5 then f=f+1 100 if y=6 then g=g+1 110 if y=7 then h=h+1 120 if y=8 then i=i+1 130 if y=9 then j=j+1 140 next v 150 print a,b,c,d,e,f,g,h,i,j 160 end
Kommentar
10 rem Das ist ein Kommentar 20 print "Hallo..." 30 end
Call-Befehl
Aufruf von internen C-Funktionen mit eventuell vorhandenen Parameter- und/oder Rückgabewerten via Basic:
10 call("a") 20 print call("c",0) 30 wait 500 40 call("b", 0) 50 wait 500 60 goto 10 70 end
VPOKE, VPEEK
Zugriff auf interne C-Variablen via Basic:
10 vpoke("a")=123 20 print vpeek("a") 30 end
AVR-spezifische Dinge
EEPROM setzen/lesen
10 epoke(2)=55 20 epoke(3)=11 30 a=epeek(2) 40 print a 50 print epeek(3)+3 60 end
Endlosschleife mit wait-Befehl
10 a=1 20 print a 30 wait 1000 40 a=a+1 50 goto 20 60 end
I/O-Port auslesen
10 print in(b,0) 20 end
ADC auslesen
10 print adc(0) 20 end
Blinklichter
Variante 1
10 dir(b,1)=1 20 out(b,1)=1 30 wait 1000 40 out(b,1)=0 50 wait 1000 60 goto 20 70 end
Variante 2
10 dir(b,1)=1 20 a=0 30 out(b,1)=a 40 if a=1 then a=0 else a=1 50 wait 1000 60 goto 30 70 end
Variante 3
10 dir(b,1)=1 20 a=0 30 a=a+1 40 if (a%2)=1 then out(b,1)=0 else out(b,1)=1 50 wait 1000 60 goto 30 70 end
Aktuelle Quellen
Die aktuellsten Quellen sind im SVN auf mikrocontroller.net zu finden. In diesen Repository sind auch noch weitere Versionen anderer Entwickler enthalten, die leicht differierende Zielvorstellungen hatten. Die einzelnen Entwickler-Versionen werden auf dieser dazugehörigen Projektseite vorgestellt und erläutert.