UBasic-avr
Aus BraLUG-Wiki
Inhaltsverzeichnis |
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. Das ist aber noch Zukunftsmusik. Das jetzige Stadium dieses Projektes ist noch als "pre-alpha" zu bezeichnen. 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 Datei referenz.txt im Quelltext-Archiv, fuer die genaue Syntaxbeschreibung.
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. Siehe dazu call_referenz.txt sowie ubasic_call.*.
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 (siehe dazu auch referenz.txt im Quelltext-Archiv).
Denkbar sind natürlich noch weitere Mikrocontroller-spezifische Befehle, je nachdem, welche Hardware vorhanden ist. Z.B.:
- 1wire
- TWI (I2C)
- Ausgabe auf LCD
- usw.
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
10 call("a") 20 print call("c",0) 30 wait 500 40 call("b", 0) 50 wait 500 60 goto 10 70 end
Die Funktionen a, b und c sind Bespiele und sollen nur die Möglichkeiten des call-Befehles verdeutlichen. Unterschied zwischen den einzelnen Funktionen ist:
- a: keine Parameter und Rückgabewert
- b: ein Parameter und kein Rückgabewert
- c: ein Parameter und Rückgabewert
Zur Implementierung eigener Funktionen siehe die Quellcode-Files ubasic_call.* sowie ubasic_call.txt.
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
Download (Version vom 03.06.2010)
Anmerkung: uBasic-ARV befindet sich noch in der Entwicklungsphase. Es kann gut sein, dass sich noch einige Bugs in der Software befinden bzw. nicht alles ausgetestet ist!