Vorträge und Workshops/FORTH auf AVR

Aus Magrathea Laboratories e.V.
Wechseln zu: Navigation, Suche

Was ist FORTH

Forth ist eine Programmiersprache und eine Laufzeitumgebung in einem. Dadurch das beides sehr minimalistisch ist, kann FORTH auch auf einem kleinen Mikrocontroller ausgeführt werden.

Die Sprache FORTH und Ihre erste Implementierung ist schon einige Jahre alt. Sie wurde 1969 entwickelt und implementiert. Der Entwickler kaufte sich einen Rechner zur Steuerung eines Teleskops ohne zugehöriger Software. Da er alle Komponenten zur Bedienung des Rechners, ein Betriebssystem, eine Hochsprache und eine Entwicklungsumgebung selber schreiben musste hat er diese in einem System, dem FORTH-System vereint.

Der aufbau der Laufzeitumgebung basiert auf einer Stack-Maschine. Die einzelnen Wörter für die Maschine können auf einem Eingabe-Prompt eingegeben werden und werden direkt ausgeführt. Zusätzlich können auch neue Wörter definiert werden, die nach Abschluss der Eingabe kompiliert werden und direkt im System verfügbar sind.

Umgekehrte Polnische Notation

Etwas gewöhnungsbedürftig sind die UPN-Ausdrücke, die aus der Verwendung der Stack-Maschine entsteht. Dabei werden erst die Operanten und dann der Operator angegeben:

5 7 +

Errechnet die Summe aus 5 und 7.

Durch diese Schreibweise können klammern vermieden werden und die Reihenfolge der Operationen kann eindeutig bestimmt werden:

4 5 + 4 1 - *

Entspricht der Formel

(3 + 5) * (4 - 1)

Hier werden erst die beiden Operanden 3 und 5 auf dem Stack abgelegt. Danach werden die beiden Operanden von der Addition-Operation (+) vom Stack genommen und durch das Ergebnis der Addition ersetzt. Danach werden die Nächsten Operanden 4 und 1 auf den Stack gelegt. Der Stack beinhaltet jetzt die Werte 8, als Ergebnis der vorherigen Addition, 1 und 4. Jetzt wird wieder eine Subtraktion-Operation (-) durchgeführt. Diese Nimmt die beiden Operanden vom Stack (4 und 1) und legt das Ergebnis der Subtraktion wieder auf dem Stack ab. Jetzt hat der Stack die Werte 8 und 3. Als letztes wird noch die Multiplikation-Operation (*) durchgeführt. Diese nimmt die Operanden vom Stack (8 und 3) und legt das Ergebnis der Multiplikation wieder auf dem Stack ab. Jetzt ist das Ergebnis auf dem Stack verfügbar.


amForth

amForth ist eine Build-Umgebung um ein FORTH-System auf AVR-Mikrocontrollern zu portieren.

Installation

Es müssen einige Pakete installiert werden:

apt-get install libxml-libxml-perl subversion

Jetzt muss die amForth-Umgebung heruntergeladen werden:

svn co https://amforth.svn.sourceforge.net/svnroot/amforth/trunk amforth
cd amforth

Prozessor-Definitionen

Die Prozessor-Definitionen von Atmel sind leider nicht frei und können nicht mit ausgeliefert werden. Sie sind aber in der kostenlos erhältlichen Entwicklungsumgebung AVR Studio 4 von Atmel enthalten.

Das AVR Studio 4 kann hier heruntergeladen werden. Danach muss das Setup durchgeführt werden:

wine AvrStudio4Setup.exe

Die Entpackten Dateien können nun kopiert werden:

mkdir -p Atmel
cd Atmel
cp -r ~/.wine/drive_c/Program\ Files/Atmel/AVR\ Tools/Partdescriptionfiles/ .

Nun müssen die Definitionen noch so umgewandelt werden, dass amforth sie auch verwenden kann:

cd Partdescriptionfiles
../../tools/pd2amforth

Jetzt können die Prozessor-Definitionen kopiert werden:

cp -r at* ../../core/devices
cd ..

Danach müssen noch einige Assembler-Files und der Assembler selbst kopiert werden:

cp -r ~/.wine/drive_c/Program\ Files/Atmel/AVR\ Tools/AvrAssembler2/Appnotes Appnotes2
cp ~/.wine/drive_c/Program\ Files/Atmel/AVR\ Tools/AvrAssembler2/avrasm2.exe .
cd ..

Anlegen einer Applikation

Applikationen werden in dem Ordner appl gespeichert. Dort kann eine Kopie des Ordners template verwendet werden um eine neue Applikation anzulegen:

cd appl
cp -r template mood
cd mood
mv template.asm mood.asm

Jetzt können die Einstellungen, wie z.B. CPU, Geschwindigkeit oder Anschlüsse gesetzt werden. Dafür kann die Datei makefile angepasst werden. Als erstes muss der Wert der Variable TARGET auf den Namen der Applikation gestellt werden:

TARGET=mood

MCU=atmega168

LFUSE=0xC7
HFUSE=0xDF
EFUSE=0x01

USB=-c ponyser -P /dev/ttyUSB0

Da wir auch die extended fuses verwenden möchten, muss die Datei makefile noch etwas modifiziert werden. In den beiden letzten Targets read-fuse und write-fuse müssen die Kommentare getauscht werden, so das alles was auskommentiert ist, wieder einkommentiert wird und umgekehrt. Der Ausschnitt sieht nun so aus:

read-fuse:
# $(AVRDUDE) $(AVRDUDE_FLAGS) -U hfuse:r:-:h -U lfuse:r:-:h -U lock:r:-:h
  $(AVRDUDE) $(AVRDUDE_FLAGS) -U hfuse:r:-:h -U lfuse:r:-:h -U efuse:r:-:h -U lock:r:-:h

write-fuse:
# $(AVRDUDE) $(AVRDUDE_FLAGS) -U hfuse:w:$(HFUSE):m -U lfuse:w:$(LFUSE):m
  $(AVRDUDE) $(AVRDUDE_FLAGS) -U efuse:w:$(EFUSE):m -U hfuse:w:$(HFUSE):m -U lfuse:w:$(LFUSE):m

Es müssen noch einige Konstanten in der Datei mood.asm angepasst werden:

.equ F_CPU = 14745600

.set WANT_ISR_RX = 0
.set WANT_ISR_TX = 0

.equ want_fun = 1

Hier können auch einige Wörter und Definitionen für den entsprechenden Prozessor aktiviert werden:

; enable some ports
.set WANT_PORTB = 1
.set WANT_PORTC = 1
.set WANT_PORTD = 1

; enable UART
.set WANT_USART0 = 1

; enable timer
.set WANT_TIMER_COUNTER_0 = 1
.set WANT_TIMER_COUNTER_1 = 1
.set WANT_TIMER_COUNTER_2 = 1

Jetzt kann der erste Build gestartet werden:

make clean && make

Aufspielen der Applikation

Die fertig gebaute Applikation kann jetzt auf den Mikrocontroller aufgespielt werden:

make write-fuse && make install


FORTH-Syntax und -Befehle

Beispiel-Code: MOOD

Das Beispiel MOOD ist ein einfaches FORTH-Beispiel um ein Mood-Light zu realisieren.

Das Mood-Light

An das Board sind drei LEDs mit dem Farben Rot, Grün und Blau angeschlossen. Diese LEDs sind an jeweils einen der Timer-Ausgänge des Mikrocontrollers angeschlossen.

Die Helligkeit der LEDs soll von außen gesteuert werden. Dafür wird der RS232-Anschluss aufgetrennt, so dass der Ausgang eines Boards mit dem Eingang des nächsten Boards verbunden wird. Die Adressierung innerhal der Kette wird über Dekrementierung der ID erreicht. Wenn die ID in einem empfangenen Paket 0 ist werden die Werte aus dem Paket als Helligkeit für die LEDs interpretiert. Wenn die ID nicht 0 ist, wird die ID dekrementiert und das Paket wird an das nächste Board weitergeleitet.

Ein Paket besteht zusätzlich zu der ID noch aus den drei Helligkeits-Werten für die drei LEDs. Der Aufbau eines Paketes sieht wie folgt aus:

Bit offset Länge Typ Name Beschreibung
0 8 uint8_t ID Die ID des Boards gezählt vom aktuellen Board
8 8 uint8_t RED Helligkeit der roten LED
16 8 uint8_t GREEN Helligkeit der roten LED
24 8 uint8_t BLUE Helligkeit der roten LED

Die Helligkeitswerte der LEDs befinden sich zwischen 0x00 für ausgeschaltete LED und 0xFF für volle Helligkeit der LED.

Die Helligkeit der LEDs soll über Puls-Weiten-Modulation mit Hilfe der Timer gesteuert werden.

Kommentare

Kommentare werden in FORTH durch das Zeichen \ (Backslash) eingeleitet. Alles vom Auftreten des Zeichens bis zum ende der Zeile wird ignoriert. Wichtig ist dabei aber, dass nach dem Bachslash noch ein Leerzeichen folgen muss.

Worte und Funktionen

Worte und Funktionen sind umgangssprachlich das selbe. Wissenschaftlich richtig ist der Begriff Wort, da es sich bei einem FORTH-System um eine Stack-Maschine handelt. Ich werde versuchen, den Begriff Wort zu verwenden, wo es möglich ist.

Ausgabe des Stacks

Es gibt zwei Worte, die bei der Entwicklung sehr Hilfreich sind:

.
Das Wort . (Punkt) gibt den obersten Wert des Stacks aus. Dabei wird der Wert vom Stack entfernt
.s
Das Wort .s gibt den Inhalt des Stacks aus ohne ihn zu verändern.

Erste Worte

Als erstes werden einige Hilfswörter definiert. Diese Wörter erleichtern die Arbeit mit dem FORTH-System auf dem Mikrocontroller erheblich. Gleichzeitig soll der Code auch eine erste Einführung in FORTH geben.

.bit

Erstellt eine Bit-Maske in der das gegebene Bit gesetzt ist.

: .bit        \ Definition des Wortes ".bit"
  1           \ Schiebt eine 1 auf den Stack
  swap        \ Tauscht die beiden oberen Werte des Stack
  lshift      \ Schiebt die 1 um den gegebenen Wert nach links
;             \ Ende des Wortes

Das Wort .BIT erwartet einen Wert auf dem Stack. Dieser Wert wird von Stack entfernt und durch eine Bit-Maske ersetzt, in der das Bit mit der Nummer des vorher entfernten Wertes gesetzt ist.

Die nachfolgende Tabelle beschreibt den Ablauf folgender eingabe:

5 .bit .
Stack-Inhalt Operation Beschreibung
5 Der erste Wert auf dem Stack definiert die Nummer des Bits, das gesetzt sein soll. Der Wert muss vor dem Aufruf des Wortes .bit definiert werden.
5 1 Hinzufügen des Wertes 1 auf den Stack.
5, 1 swap Tauchen der beiden Werte auf dem Stack.
1, 5 lshift Shift des Wertes 1 um 5 Positionen nach links. 1 und 5 werden entfernt und Ergebnis 32 (1 << 5) wird auf dem Stack abgelegt.
32 . Ausgabe des Wertes. Der Wert wird vom Stack entfernt.

trip

Dupliziert den gegebenen Wert zwei mal. Danach liegt der Wert drei mal auf dem Stack.

: trip       \ Definition des Wortes "trip"
  dup        \ Dupliziert den obersten Wert auf dem Stack
  dup        \ Dupliziert den obersten Wert auf dem Stack
;

c|=

Verbindet den Wert an der gegebenen Adresse mit dem gegebenen Wert mit bitweisem Oder.

: c|=        \ Definition des Wortes "c|="
  over       \ Duplizieren des zweiten Wertes auf dem Stack.
  c@         \ Holten des aktuellen Wertes von gegebener Adresse. Die Adresse wird vom Stack entfernt und durch den Wert ersetzt.
  or         \ Durchführen der Oder-Operation
  swap       \ Tauschen von Adresse und neuem Wert.
  c!         \ Setzen des neuen Wertes an der gegebenen Adresse. Wert und Adresse werden vom Stack entfernt.
;

c&=

Verbindet den Wert an der gegebenen Adresse mit dem gegebenen Wert mit bitweisem Und.

: c&=        \ Definition des Wortes "c&="
  over       \ Duplizieren des zweiten Wertes auf dem Stack.
  c@         \ Holten des aktuellen Wertes von gegebener Adresse. Die Adresse wird vom Stack entfernt und durch den Wert ersetzt.
  and        \ Durchführen der Und-Operation
  swap       \ Tauschen von Adresse und neuem Wert.
  c!         \ Setzen des neuen Wertes an der gegebenen Adresse. Wert und Adresse werden vom Stack entfernt.
;

c~=

Verbindet den invertierten Wert an der gegebenen Adresse mit dem gegebenen Wert mit bitweisem Und.

: c~=         \ Definition des Wortes "c~="
  ivert       \ Invertieren des gegebenen Wertes
  c&=         \ Verbinden der Werte mit Und-Operation.
;

+bit und -bit

Setzt und entfernt auf der gegebenen Adresse das Bit mit der gegebenen Nummer. Erwartet Adresse und Bit-Nummer.

: +bit        \
  .bit        \ Konvertieren von Bit-Nummer in Bit-Maske
  c|=         \ Setzen der Bit-Maske
;
: -bit        \
  .bit        \ Konvertieren von Bit-Nummer in Bit-Maske
  c~=         \ Entfernen der Bit-Maske
;

PWM

Als erstes müssen die entsprechenden Pins als Ausgänge definiert werden:

: init_ddr
  DDRB 1 +bit
  DDRB 3 +bit
  DDRD 6 +bit
;
init_ddr

Da die LEDs mit Pull-Down-Wiederständen auf Ground gezogen werden sollten die LEDs jetzt alle an sein.

Die LEDs müssen jetzt ausgeschaltet werden. Dafür müssen die entsprechenden Pins auf 1 gesetzt werden:

: init_port
  PORTB 1 +bit
  PORTB 3 +bit
  PORTD 6 +bit
;
init_port

Jetzt sollten alle LEDs wieder ausgeschaltet sein.

Jetzt können die Timer für PWM konfiguriert werden. Dabei wird jeder Timer so konfiguriert, dass er Permanent von 0 bis 255 und wieder von 255 bis 0 zählt. Wenn der entsprechende Wert erreicht wurde wird der entsprechende Pin beim heraufzählen auf 1 gesetzt und die LED ausgeschaltet und beim herunterzählen wieder auf 0 gesetzt und die LED eingeschaltet. Die Initialisierung wird in dem Wort init zusammengefasst:

: init_pwm
  7 .bit      \ Setzen von COM0A1: Der Pin wird gesetzt
  6 .bit or   \ Setzen von COM0A0: Der Pin wird auf 1 gesetzt
  0 .bit or   \ Setzen von 8-bit PWM
  trip        \ Verdreifachen der Maske
  TCCR0A c!   \ Schreiben der Maske in das Register für Timer 0
  TCCR1A c!   \ Schreiben der Maske in das Register für Timer 1
  TCCR2A c!   \ Schreiben der Maske in das Register für Timer 2
  
  0 .bit      \ Setzen des Timers auf CPU-Takt ohne Teiler
  trip        \ Verdreifachen der Maske
  TCCR0B c!   \ Schreiben der Maske in das Register für Timer 0
  TCCR1B c!   \ Schreiben der Maske in das Register für Timer 1
  TCCR2B c!   \ Schreiben der Maske in das Register für Timer 2
;

Um die Compare-Register für die entsprechenden LEDs besser angesprochen werden können werden drei Konstanten definiert:

: LED_R OCR1AL ;
: LED_G OCR2A ;
: LED_B OCR0A ;

Bei der Definition der roten LED ist nur das Low-Byte relevant, da der 16-Bit-Timer so konfiguriert ist, dass der maximale Wert 255 ist.

Jetzt können Worte definiert werden, die das Compare-Register der Timer für die Entsprechenden LEDs setzen. Damit wird die Helligkeit der entsprechenden LED definiert:

: led_r! LED_R c! ;
: led_g! LED_G c! ;
: led_b! LED_B c! ;

Die Worte nehmen den zu setzenden Wert vom Stack und schalten den entsprechenden Timer so, dass der Wert als Helligkeit für die entsprechende LED verwendet wird.

Als Abschluss kann noch ein Wort definiert werden, das alle drei LEDs gleichzeitig setzt. Die drei Helligkeitswerte werden vom Stack genommen und den Timern zu gewissen:

: leds!
  led_b!
  led_g!
  led_r!
;

Die Reihenfolge der Worte zum setzen der einzelnen LEDs ist umgekehrt, da die Werte in der Reihenfolge R, G, B auf den Stack geschoben werden. Nun muss der oberste Wert, der Wert für B, als erstes verarbeitet werden.

UART

Die Ansteuerung der UART ist in amforth sehr einfach. Mit den Worten rx und tx können einzelne Bytes empfangen und gesendet werden.

Es ist problemlos möglich für die Datenübertragung und die Programmierung die selbe Schnittstelle zu verwenden. Beim Aufruf des Wortes rx wird der Interpreter unterbrochen und auf die Eingabe eines Zeichens gewartet. Das empfangene Byte ist dann auf dem Stack verfügbar. Beim Aufruf des Wortes tx wird das oberste Byte auf dem Stack direkt versendet.

Um Werte einfach weiterleiten zu können wird das Wort echo definiert. Das Wort empfängt ein Byte von der UART und versendet das Byte direkt wieder über die UART:

: echo
  rx          \ Empfangen des Bytes
  tx          \ Versenden des Bytes
;

Das folgende Wort behandelt ein Steuer-Paket, wie oben definiert:

: handle      \ Bearbeitet ein Steuer-Paket
  rx          \ Empfangen der ID
  dup 0= if   \ Duplizieren der ID und vergleich mit 0
    rx rx rx  \ Empfangen der drei Helligkeitswerte für die LEDs
    leds!     \ Setzen der LEDs
    drop      \ Verwerfen der ID
  else
    -1 tx     \ Dekrementieren und versenden der ID
    echo      \ Weiterleiten der drei Helligkeitswerte
    echo      \ ...
    echo      \ ...
  then        \ Ende des if-Blocks
;

Der Main-Loop

Das Programm soll nicht nur ein Paket interpretieren, sondern es soll in einer schleife auf neue Pakete warten und diese interpretieren. Dafür wird das Wort run definiert:

: run
  begin       \ Starten des Main-Loops
    handle    \ Ein Paket bearbeiten
  again       \ Wiederholung des Loops ohne Abbruchbedingung
;

Die Schleife kann nur durch einen Resets des Kontrollers abgebrochen werden.

Starten der Anwendung nach einem Reset

Damit der Mikrocontroller das Programm direkt nach dem Reset wieder startet muss eine das Wort run als turnkey definiert werden. Als turnkey wird das Wort bezeichne, das direkt nach dem Reset ausgeführt wird.

Alle Funktionen des Codes sollten gut geprüft sein, bevor turnkey verändert wird, da es keine Möglichkeit gibt wieder auf den Interpreter zuzugreifen. Der Controller muss neu geflasht werden um wieder Zugriff zum Interpreter zu erhalten.

Um run als turnkey zu definieren, wird der Eintrag für das bestehende turnkey überschrieben. Dafür wird eine eigenes Wort mood-turnkey definiert, dass noch einige Initialisierungen des FORTH-Systems durchführt:

: mood-turnkey
  init-user   \ Initialisieren der User-Area
  
  +usart      \ Initialisieren der UART
  +int        \ Interrupts einschalten
  
  init_ddr    \ Initialisieren der Ausgänge
  init_port   \ Initialisieren der Ports
  init_pwm    \ Initialisieren der Timer
  
  run         \ Starten des Main-Loops
;

Jetzt kann turnkey mit mood-turnkey überschrieben werden:

' mood-turnkey is turnkey

Abschluss

Das Mood-Light ist fertig.

Hier noch die FORTH-Applikation: Datei:Mood.tar.gz.

Python-Bibliothek

Zusätzlich gibt es noch eine Python-Bibliothek, mit der sich das Mood-Light ansprechen lässt: Datei:Mood-python.tar.gz.

Die Python-Bibliothek beinhaltet die Bibliothek zum ansprechen der Mood-Lights inklusive Animation. Zusätzlich sind auch noch beispiele enthalten, wie eine GUI und ein Command-Line Tool zum setzen der Farbe, sowie eine einfache Animation.

Mood-gui.png

Die GUI kann mit folgendem Befehl aufgerufen werden:

cd mood-gui/src/ && PYTHONPATH=../../mood/src python mood_gui.py /dev/ttyUSB0 0