Home: www.rowalt.de
Keine Angst vor Mikrokontrollern!
Der einfache Einstieg in die Welt der AVRs (6)
Roland Walter, DL7UNO

Die aktuellen Qelltexte und Compilate zu den Beiträgen

Vorweg: Ich komme mit der Beantwortung von Mails nicht mehr hinterher. Ich würde gerne auch weiterhin bei der Beantwortung der vielen kleinen Detailfragen helfen, aber es ist einfach nicht mehr zu schaffen. Mails an mich sind deshalb trotzdem nicht sinnlos; soweit möglich, werde ich auf Einzelprobleme in den Artikeln oder im FAQ-Bereich meiner Homepage eingehen. Bitte nehmen Sie mir das nicht übel, aber ich muß ja auch noch zum Arbeiten kommen ;-)

Noch einmal UART
Für viele Leute scheint die serielle Schnittstelle ein schwer zu bändigendes Etwas zu sein, weshalb ich hier schweren Herzens noch einige Ergänzungen zur UART einfügen will. In etlichen Mails wurde mir berichtet, daß zwar alle Einzelschritte leicht zu verstehen sind. Probleme bereitet aber die Vorstellung, eine Reihe von Bytes via UART nach und nach zu empfangen, während man zwischendurch ganz andere Programmschritte abarbeitet. Aus den mir zugeschickten Listings ließen sich einige sehr typische Probleme und Denkfehler extrahieren, auf die ich jetzt in zusammengefaßter Form eingehen will.

Um einfacher vorgehen zu können, setzen viele Leute zum UART-Empfang die Bascom-Funktion Input ein. Diese Funktion arbeitet intern nach der Poll-Methode und kehrt erst zurück, wenn alle angegebenen Bytes empfangen wurden. Will man mit Input z.B. Zeichen in einen String einlesen, der mit 10 Bytes Länge dimensioniert wurde, dann kehrt Input erst zurück, wenn tatsächlich 10 Bytes empfangen wurden, vorher nicht(!). Das kann beim Test im Terminalprogramm den Eindruck erwecken, als wenn das Programm hängengeblieben ist. Analog gilt dies für Integer: Hat man eine Variable z.B. als WORD dimensioniert, dann kehrt Input erst zurück, wenn 2 Bytes empfangen wurden - ein WORD besteht nun einmal aus zwei Bytes. Das ist das erste Problem, bei dem viele hängenbleiben.
Das zweite Problem ist die Ineffizienz der Input-Funktion. Soll Input z.B. 10 Bytes einlesen, dann wartet man, bis endlich alle 10 Zeichen empfangen wurden. Bis dahin kann man nichts, aber auch gar nichts anderes tun. Wenn wir 9.600 Baud bei 8N1 annehmen, dann muß der AVR bei 10 Bytes mindestens 1/96 Sekunde lang warten, was bei 10 MHz AVR-Taktfrequenz 104167 Takten Totzeit entspricht. Das ist schon eine sehr bedeutende Verschwendung von Rechenkapazität, die man sich nur leisten sollte, wenn der AVR ohnehin nichts anderes zu tun hat. Das folgende Listing zeigt einen praktikablen Weg zum Einlesen eines Strings ohne Input.
'0011.BAS: Einfacher String-Empfang
$Regfile = "2313def.dat"
$Crystal = 3686400
$Baud    = 9600

Dim s As String*10

On URXC OnRxD
Enable URXC
Enable Interrupts

Main:
  If Len(s) > 9 Then
    Print s
    s = ""
  End If
Goto Main

OnRxD:
  s = s + Chr(UDR)
Return
Der String "s" wird im Listing mit 10 Bytes Länge dimensioniert. Sobald ein neues Zeichen via UART eingetroffen ist, wird dieses in der Interrupt-Routine an den bereits enthaltenen Text angehängt. Als Anwendungsbeispiel wird der String dann in der Hauptschleife permanent überprüft und ausgegeben, sobald er voll ist.
Das Vorgehen String=String+Zeichen ist leicht zu verstehen, aber auf der Assemblerebene betrachtet recht aufwendig. Die Operation benötigt bei einer Stringlänge von einem Byte schon 30 Takte und bei jedem weiteren Byte 5 Takte mehr (bei 10 Bytes also 75 Takte). Aber man vergleiche das einmal mit der Zeit bei Input - es liegen Welten dazwischen...
Das Entscheidende ist, daß der AVR jetzt zwischen dem Eintreffen der einzelnen Bytes nicht mehr blockiert wird und andere Programmteile abgearbeitet sowie andere Interrupts ausgelöst werden können.

Wer das Vorgehen String=String+Zeichen nicht mag, kann mit einem kleinen Trick optimal vorgehen: In Bascom ist es möglich, ein und denselben SRAM-Platz mit mehreren Variablen zu belegen. Wir können z.B. anweisen, daß unsere String-Variable auf Adresse &H60 gesetzt werden soll. Gleichzeitig können wir ein Feld mit 11 Bytes dimensionieren, das auf der gleichen Adresse liegen soll (ein 10 Byte langer String benötigt 11 Bytes Platz). Damit können wir ganz einfach und effizient über völlig verschiedene Methoden auf denselben Speicher zugreifen. Schauen Sie sich bitte das folgende Listing an.
'0011.BAS: Optimierter String-Empfang
$Regfile = "2313def.dat"
$Crystal = 3686400
$Baud    = 9600

Dim s As String*10 At &H60
Dim b(11) As Byte At &H60 Overlay
Dim n As Byte

On URXC OnRxD
Enable URXC
Enable Interrupts

Main:
  If n > 9 Then
    Print s
    n = 0
  End If
Goto Main

OnRxD:
  Incr n
  b(n) = UDR
Return
Der String wird auf Adresse &H60 im AVR abgelegt. Dies ist die Adresse des ersten Bytes im SRAM (da der AT90S2313 über 128 Bytes SRAM vefügt, ist die höchste SRAM-Adresse &HDF). Quasi "darüber", also auf die selbe Adresse, legen wir ein Feld, das genauso lang wie der String ist. Das ist alles. Der Trick ist nur, daß wir beim Füllen des Strings in unserem Anwendungsfall am effizientesten mit einem Feld arbeiten können, während in anderen Fällen die Arbeit mit einem String günstiger sein kann. Welche Wege man in der Praxis gehen will, hängt natürlich immer von der Anwendung ab. Die Anwendung des Overlay-Tricks kostet weder zusätzlichen Code, noch SRAM. Es ist vielleicht eher wie die Auswahl zwischen Buddelschippe, Schaufel und Bagger bei ein und dem selben Haufen Sand.

Und noch ein drittes UART-Mißverständnis trat wiederholt auf: Der UART-Empfangs-Interrupt meldet, daß ein Zeichen empfangen wurde. In der Interrupt-Routine kann man deshalb auch immer nur ein Zeichen auslesen. Zum Beispiel macht es daher keinen Sinn, in der Interrupt-Routine den Bascom-Befehl Input einzusetzen, um doch mehrere Zeichen empfangen zu können.

Jetzt aber endlich zur Fortsetzung der Serie. Die letzte Folge schloß mit dem 8-Bit-Timer ab. Wie angekündigt soll jetzt der 16-Bit-Timer beschrieben werden.

Timer1
Zur Erinnerung: Der AT90S2313 verfügt über zwei voneinander unabhängige Timer. Timer0 ist 8 Bit breit und kann deshalb von &H00 bis &HFF (0 bis 255) zählen und Timer1 ist 16 Bit breit und kann deshalb von &H0000 bis &HFFFF (0 bis 65535) zählen. Das ist zunächst der auffallendste Unterschied zwischen den beiden Timern.
Darüber hinaus hat Timer1 aber zusätzlich eine Vergleichsfunktion, kann als Pulsweitenmodulator (anderer Name: Pulsabstandsmodulator) dienen und hat außerdem eine Capture-Funktion, bei der ein externes Signal die Ausgabe des aktuellen Zählerstandes bewirken kann. Bei der Verwendung der Capture-Funktion kann als Signaleingang von Timer1 außerdem auch noch der Analog-Komparator des AT90S2313 verwendet werden. Timer1 hat also wirklich weitaus mehr Möglichkeiten als Timer0.
Davon abgesehen, daß Timer1 etliche Funktionen mehr hat hat, wird er in Bascom im Prinzip genau wie Timer0 behandelt. Nur beim direkten Zugriff auf die AVR-Register muß beachtet werden, daß Timer1 andere Register als Timer0 verwendet und außerdem müssen die 16-Bit-Werte getrennt als HiByte und LoByte in 8-Bit-Register gegeben oder aus diesen gelesen werden.
Ich spare mir die Erörterung der Funktionen, die bei Timer0 und Timer1 gleich sind. Die drei Beispiellistings zum Timer0 können grundsätzlich auch mit Timer1 verwendet werden - man muß lediglich TIMER0 durch TIMER1 ersetzen und sinnvollerweise den Namen der Interrupt-Routine von OnTimer0 in OnTimer1 ändern. Wenn man den Wert des Timers in eine Variable auslesen will, sollte diese Variable bei Timer1 natürlich nicht als Byte (8 Bit), sondern als Word (16 Bit) dimensioniert worden sein.

Timer1-Capture
Am besten springen wir gleich mit einem praktischen Beispiel ins kalte Wasser, denn was ein Timer als solcher ist, wissen wir ja bereits von Timer0.
Gleich vorweg will ich noch auf einen Mini-Bug hinweisen, der zumindest bis Bascom-Version 1.11.6.7 bestand: Wenn man Timer1 im Capture-Modus benutzt, stellt der AVR ein Störfilter zur Verfügung, das man bei Bedarf einschalten kann. Das Störfilter läßt das Eingangssignal nur dann durch, wenn es über 4 Takte hinweg stabil ist. Dafür gibt es beim Befehl Config Timer1 die Einstellmöglichkeit "Noise Cancel=1/2" ... oder zumindest hätte es so heißen müssen. Stattdessen muß man im Sourcecode (falsch) "Noice Cancel=" eingeben. Der Fehler ist auch genau so in der Bascom-Hilfe angegeben. Behalten Sie das im Hinterkopf - benötigen werden wir es jetzt nicht.
Beim folgenden Listing verwenden wir AVRTerm als Signalquelle und zur Anzeige. Sie müssen also eine Verbindung per Nullmodem-Kabel herstellen. Außerdem verbinden Sie bitte den AVR-Pin PD6/ICP mit dem RTS-Ausgang des MAX232. Die RTS-Checkbox in AVRTerm wird unsere Signalquelle sein. Wenn die Checkmarke gesetzt ist, liegt am RTS-Pin des MAX232 HIGH an. Wenn die Checkmarke entfernt ist, führt dieser Pin LOW-Pegel. Die "Hardware"-Taster verwenden wir nicht, weil wir dann das bekannte Tasten-Prellen am Hals hätten.

Verdrahtung für die Capture-Programme

Wir wollen jetzt die Zeit messen, die zwischen zwischen zwei LOW/HIGH-Flanken vergeht. Nachdem Sie das AVR-Programm geladen und die Verbindung mit AVRTerm hergestellt haben klicken Sie dazu bitte rhytmisch mit der Maus auf die RTS-Checkbox in AVRTerm. Klicken Sie bitte zunächst schnell hintereinander und werden Sie dann allmählich langsamer. Im Terminalfenster sollten jetzt die Meßwerte in Hundertstel Sekunden angezeigt werden. Beachten Sie bitte auch, daß eine LOW/HIGH-Flanke immer nur in dem Augenblick entsteht, wenn Sie die Checkmarke setzen. Anders ausgedrückt: Für eine vollständige Periode sind immer zwei Mausklicks erforderlich.

Die RTS-Checkbox in AVRTerm

Schauen wir uns jetzt das Programm an:
'0013.BAS:  InputCapture mit Timer1 (Einfache Stopuhr)
'Hardware:  MAX232 an PD0/PD1
'           Nullmodemkabel zum PC
'           Pin ICP/PD6 an RTS
'--------------------------------------
$Regfile = "2313def.dat"
$Crystal = 3686400  '3,6864MHz-Quarz
$Baud    = 9600

Dim wCount As Word

On ICP1 OnCapture   'Interrupt-Routine

Config Timer1=Timer,Prescale=1024,Capture Edge=Rising

Enable ICP1         'Capture-Interrupt
Enable Interrupts   'Interrupts global

Main:               'Hauptschleife
Goto Main
'--------------------------------------
OnCapture:          'Timer1-Capture
  wCount = Timer1   'Erst Wert sichern
  Timer1 = 0        'Sofort Timer-Reset
  wCount = wCount \ 36  '1/100 Sekunde
  Print wCount;" ";
Return
Unser AVR wird mit 3,6864MHz getaktet. Der Timer1-Takt wird mit der Vorteilung durch 1024 auf 3600Hz eingestellt. Das ist sozusagen die maximale Auflösung unserer Stopuhr. Durch die verwendete Vorteilung erhalten wir alle 3600Hz/65536=0,055Hz=18,2sec einen Timer1-Überlauf, was für unseren kleinen Test erst einmal nicht stört. Der Capture-Interrupt soll an der steigenden Flanke des Eingangssignals ausgelöst werden (Capture Edge=Rising).
Der Capture-Interrupt ICP1 (nicht mit dem Overflow-Interrupt OVF1 zu verwechseln) wird jedesmal ausgelöst, wenn am AVR-Pin ICP der Pegel von LOW nach HIGH wechselt.
Als erste Aktion in der Interrupt-Routine wird der aktuelle Zählerstand in eine Variable gesichert und unmittelbar anschließend Timer1 auf Null zurückgesetzt. Vergessen wir nicht: Timer1 muß ja unabhängig von unserem Code sofort weiterzählen und schon der Sprung in die Interrupt-Routine und das Auslesen/Rücksetzen des Zählers hat bereits Zeit gekostet.
Bis zum nächsten Capture-Interrupt vergehen jetzt noch viele Takte, sodaß wir uns nun eine ganze Menge Zeit für die Bearbeitung des Meßwertes nehmen können. In unserem Listing leisten wir uns zum Beispiel eine Division, um den Zählerstand in anwenderfreundliche Hundertstel Sekunden umzurechnen. Da der AVR über keine Hardware-Division verfügt, muß diese Bascom-intern per Software gelöst werden, was 235 bis 251 Takte kostet. Und außerdem müssen wir auch noch abwarten, bis der Meßwert per UART abgeschickt wurde. Da wir das alles in der Interrupt-Routine tun, ist der Capture-Eingang solange "taub". Darauf weise ich nur grundsätzlich hin - wie gesagt, in unserem Fall geschieht das alles noch immer schnell genug.
Noch ein wichtiger Hinweis zur Division: Bascom kann lediglich eine Integer-Division durchführen, was man in der Hilfe schnell übersehen könnte. Wenn Sie eine Fließkommazahl angeben, rundet Bascom diesen Wert vor der Division einfach auf die nächste Ganzzahl. Wenn man in einer praktischen Anwendung Dezimalzahlen auf einer LCD-Anzeige darstellen will, ist man wegen der eingeschränkten Divisionsmöglichkeiten gut beraten, z.B. einen 10MHz-Quarz einzusetzen.

Jetzt wäre die Frage logisch, wie man die Auflösung der Stopuhr erhöhen kann, ohne daß Fehler durch Timer1-Überläufe ins Spiel kommen. Das ist ganz einfach möglich. Wir nehmen zum Capture-Interrupt einfach noch den Overflow-Interrupt hinzu und zählen so die Anzahl der Timer-Overflows. Zum Schluß (nach dem Capture-Interrupt) wird dann die Anzahl der Overflows und der aktuelle Zählerstand zum eigentlichen Meßwert zusammenaddiert. Das ist schon alles. Und weil das eine wichtige praktische Rolle spielt, gebe ich das erweiterte Stopuhr-Listing vollständig wieder:
'0014.BAS:  InputCapture mit Timer1 (2) (Frequenzzähler)
'Hardware:  MAX232 an PD0/PD1
'           Nullmodemkabel zum PC
'           Pin ICP/PD6 an RTS
'--------------------------------------
$Regfile = "2313def.dat"
$Crystal = 3686400  '3,6864MHz-Quarz
$Baud    = 115200   'Hohe Baudrate

Dim lCount As Word At &H60
Dim wCountLo As Word At &H60 Overlay
Dim wCountHi As Word At &H62 Overlay

On ICP1 OnCapture   'Interrupt-Routine
On OVF1 OnOverflow  'Interrupt-Routine

Config Timer1=Timer,Prescale=1,Capture Edge=Rising

Enable ICP1         'Capture-Interrupt
Enable OVF1         'Overflow-Interrupt
Enable Interrupts   'Interrupts global

Main:               'Hauptschleife
Goto Main
'--------------------------------------
OnCapture:          'Timer1-Capture
  wCountLo = Timer1 'Erst Wert sichern
  Timer1 = 0        'Sofort Timer-Reset
  Print lCount;" ";
  wCountHi = 0
Return
'--------------------------------------
OnOverflow:
  Incr wCountHi    'lCount=lCount+65536
Return
Die erweiterte Stopuhr arbeitet diesmal mit voller Taktfrequenz und damit maximaler Auflösung. Für den Zählerstand wird hier eine LONG-Variable (32 Bit) verwendet. Das HiWord (die obersten 16 Bit) wird bei jedem Timer1-Overflow um 1 erhöht und das LoWord wird zunächst nicht angefaßt. Sobald der Capture-Interrupt ausgelöst wird, setzen wir den Timer1-Zählerstand dann ins LoWord. Das ist zusammen nichts anderes als eine Addition und läuft über den schon besprochenen Overlay-Trick auf die denkbar effektivste Weise. Der Zählerstand wird diesmal ohne Umrechnung ans Terminalprogramm geschickt. Zeitprobleme bei der UART-Ausgabe wurden entschärft, indem einfach eine höhere Baudrate gesetzt wird - damit wird man die Bytes schneller los. Auch das ist eine Lösung ;-)

Noch ein Wort zu den beiden letzten Listings: Als Verbesserung könnte man z.B. beim Timer1-Reset gleich einen Wert voreinstellen. Und außerdem sollte man natürlich nicht davon ausgehen, daß Windows auch nur annähernd sofort auf einen Mausklick reagiert. Es geht hier einzig und allein um eine schnell zu durchschauende grundsätzliche Demonstration der Capture-Funktion. Den Feinschliff überlasse ich Ihnen.

Wer übrigens in der Bascom-Hilfe danach sucht, wie man den den Analog-Komparator als Timer1-Capture-Eingang benutzt, wird nicht fündig. Diese Funktion wurde offensichtlich einfach vergessen. Ich habe das als Bug gemeldet und muß die Beschreibung wohl oder übel später nachreichen. Wer gerne direkt in den AVR-Registern herumwühlt, steht hier aber nicht im Regen, denn über das direkte Setzen von Bits im "Analog Comparator Control and Status Register" ACSR kommt man auch zum Ziel.

Die Einsatzgebiete für die Capture-Funktion sind schier unendlich. Beispielsweise wird bei der Funkübertragung von Analogwerten gerne die Pulsabstandsmodulation verwendet. Dabei entspricht dann die Größe eines Analogwertes dem zeitlichen Abstand zwischen zwei Pulsen. Dieser Pulsabstand kann mit der Capture-Funktion bequem festgestellt werden. Will man mehrere Kanäle übertragen, dann fügt man einen Synchronisationskanal hinzu, der kleiner ist, als die anderen Kanäle jemals sein können.
Ein weiteres Anwendungsbeispiel wäre ein sehr preiswerter programmierbarer und softwareseitig abgleichbarer Frequenzähler. Wer das tatsächlich vorhat, dem kann ich gleich noch ein paar Tips geben: Die meisten käuflichen LCD-Anzeigen basieren auf dem Hitachi-LCD-Controller HD44780A oder einem kompatiblen Controller. Und dafür hat Bascom eine ganze Reihe von Funktionen fix und fertig vorzuliegen. Wer LCD-Anzeigen mit 2 mal 16 Zeichen billig sucht, wird auf www.j-bitzer.de fündig. Dort gibt es diese Anzeigen gebraucht für 4 Euro. Ich habe mich bereits ausreichend versorgt, deshalb kann ich den Tip jetzt auch weitergeben.

(Wird fortgesetzt)