UBasic-avr

Aus BraLUG-Wiki

Wechseln zu: Navigation, Suche


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.

Inhaltsverzeichnis

Motivation

Testausgabe

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

Testhardware Testhardware mit "Peripherie"


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.

Kontakt

Uwe Berger

'Persönliche Werkzeuge