Böcker Sirius STK1 Starter-Kit (Teil 3): D/A-Wandler, Timer, Interrupts

 

Im 3. Teil unseres Mikrocontroller-Kurses mit dem Böcker Sirius Starter-Kit STK1 werden wir uns mit einer Vielzahl von weiteren PSoC-spezifischen Aspekten beschäftigen: dem D/A-Wandler, den Timern und den Interrupts. Außerdem kommt auch wieder unsere LCD-Anzeige zum Einsatz. 

 

Öffnen Sie ein neues Projekt, vergeben Sie einen passenden Namen, wählen Sie für den Ziel-Controller wieder den CY8Y24423, oder den Controller, der sich auf Ihrem Böcker Merkur PEv1-Board befindet und als Sprache "C". Nachdem Sie den Dialog bestätigt haben, öffnet sich das Startfenster des Designers in der mittlereweile schon vertrauten Ansicht. Um Wiederholungen zu vermeiden, werde ich auf grundlegende Funktionen, die im Tutorial 1 oder im Tutorial 2 besprochen wurden, nicht mehr eingehen. Bei Unklarheiten schauen Sie am besten dort nach. 

 

Für die Ausgabe von Nachrichten und Daten fügen wir zunächst ein LCD-Usermodul hinzu, nennen es in "LCD" um und wählen im  "parameters window" als Schnittstelle Port 2. Nun suchen wir im "User Modules Katalog" im Eintrag DAC das Usermodul DAC9 heraus und fügen es durch Doppelklick in unser Projekt ein. Dabei handelt es sich um einen D/A-Wandler mit einer Auflösung von 9 Bit und einer maximalen Taktrate von 125 kSamples/s. Das "parameters window" sollte für das DAC9-Modul wie folgt geändert werden:

 

parameters_dac9.png

 

Als Name wählen wir "DAC", weil wir nur mit einem DAC-Modul arbeiten. Für die analoge Ausgabe gibt es nur die Wahl zwischen "disable" und "AnalogOutBus_0". Schauen wir uns im Routing-Bereich einmal an, was das für uns bedeutet.

 

analogOutBus_0.png

 

Der DAC ist mit AnalogOutBus_0 verbunden und dieser lässt wiederum zwei Alternativen zu: OFF und Port_0_3. Im Grunde könnten wir durchaus mit dieser Einstellung leben, da wir allerdings die Möglichkeiten des PSoC tiefer ergründen wollen, geben wir uns damit nicht zufrieden. Einen anderen Pin erreichen wir jedoch nur durch das Verschieben des DAC-Moduls, genauer gesagt, durch das Verschieben des MSB-Teils des DAC-Moduls, denn dort befindet sich der Ausgang.

 

Für dieses Vorhaben können Sie den DAC mit der Maus greifen und ihn über die freien Modulplätze schieben. Geeignete Orte nehmen beim Darüberschieben die Farbe des Moduls an. Alternativ dazu können Sie auch mit einem rechten Mausklick auf das Modul im "Workspace Explorer" und Wahl von "Next Allowed Placement" den nächsten freien und erlaubten Modulplatz anzeigen lassen. Für uns gibt es nur eine passende Lösung, die wie folgt aussieht:

 

DAC_placement.png

 

Nun lässt sich der Ausgang des DAC-Moduls mit "AnalogOutBus_1" verbinden und diesen wiederum mit dem Port_0_5. Die Verbindung wird uns auch grafisch dargestellt:

 

connect_P_0_5.png
 

 

Endgültig sieht das "parameters window" des DAC-Moduls nun wie folgt aus:

 

parameters_dac9b.png
 

Für unser Vorhaben ist noch das Datenformat wichtig. Wir können zwischen "OffsetBinary", einer 2er-Komplement-Darstellung für vorzeichenbehaftete Werte und dem Format "2 Byte Sign and Magnitude" wählen. Wir wollen vorzeichenlose Werte zwischen 0 und 512 verwenden und wählen "OffsetBinary". 

 

Interessant ist in diesem Zusammenhang eine weitere Einstellung, die wir aber unter "Ref Mux" im Fenster "Global Ressources" finden. Mit dieser Einstellung wählen wir den Ausgangsbereich, auf den sich die Werte aus dem D/A-Wandler beziehen:

 

ref_mux.png

 

Für "BandGap" setzen Sie dabei den Wert der internen Referenzquelle (1,3 V) ein und erhalten so den Referenzbereich für den D/A-Wandler. Für den Eintrag "(Vdd/2) +/- (Vdd/2)" ergibt sich z. B. ein Bereich von 2,5 +/- 2,5 also 0 bis 5. Für BandGap +/- BandGap wären es 1,3 +/- 1,3 also 0 bis 2,6 V. Für unsere Zwecke ist (Vdd/2) +/- (Vdd/2) gut geeignet. 

 

Als Konfiguration für die Hardware fehlt uns nur noch der Takt, der maximal 125 kHz betragen darf. Für einen D/A-Wandler gibt es keine Notwendigkeit, mit einem geringeren Takt zu arbeiten, deshalb werden wir diese Frequenz auch nutzen. Da wir unseren PSoC mit einem Takt von 24 MHz betreiben, können wir diesen Wert einfach durch 192 teilen. Dieser Takt wäre in VC3 auch direkt einstellbar, wenn Sie nämlich als Taktquelle für VC3 SysClk/1 angeben und als Teiler (VC3 Divider) den Wert 192. Leider steht uns VC3 im DAC-Modul nicht als Takt zur Verfügung, wie ein Klick auf die möglichen Quellen zeigt:

 

clock_source.png

 

Sowohl VC1 als auch VC2 lassen als Teiler maximal einen Wert von 16 zu, also müssen wir beide wie folgt kombinieren: 

 

dac_clock.png
 

Zunächst teilen wir für VC1 den SysClk durch 16 und dann für VC2 noch einmal VC1 durch 12, womit wir auch auf 125 kHz kommen. Da sich in unserem Fall das DAC-Modul auf beide analoge Spalten aufteilt, müssen wir auch für beide Spalten als Taktquelle VC2 setzen (s. rosa Pfeile an den Multiplexern):

 

clock_source_2.png
 

 

Jetzt haben wir alle Einstellungen für die Konfiguration vorgenommen und können einen ersten Versuch unternehmen, ob unsere Schaltung so funktioniert wie geplant. Wir öffnen dazu die automatisch erstellte Datei "main.c" und ändern sie wie folgt ab:

 

main_1.png

 

Im Grunde gibt es dazu nicht viel zu erklären: einfach den DAC starten und mit DAC_WriteBlind(iWert) den Wert in einer Endlos-Schleife ausgeben. Der Parameter DAC_FULLPOWER bestimmt das für das Modul die maximale Leistung zur Verfügung gestellt wird. Diesen Parameter finden Sie bei Modulen, die einen analogen Platz besetzen. Sie können mit einer Reduzierung Energie sparen, die elektrischen Eigenschaften (z. B. maximale Taktfrequenz, Linearität) werden dadurch aber verschlechtert. 

 

Wir erstellen das Programm mit F6 bzw. "Generate/Build" im Build-Menü, laden es auf unser Böcker PEv1 Merkurboard und aktivieren die Versorgung des Programmers. Am Pin 0.5 liegen nun erwartungsgemäß 2,5 V an. Als nächste Aufgabe werden wir ein Sägezahlsignal erstellen, indem wir den Wert für den D/A-Wandler einfach in einer Schleife hoch zählen. Der Code sieht folgendermaßen aus, das Signal am Ausgang sehen Sie darunter:

 

Saegezahn.png

 

Oszilloskop.png

 

Hinweis: Auch wenn wir den D/A-Wandler im weiteren Verlauf nicht mehr benötigen, können Sie ihn ruhig im Projekt belassen.

 

SleepTimer: die einfachste Timer-Variante

 

Für einfache Timeraufgaben ist der SleepTimer ideal geeignet, denn er belegt keinen weiteren Modulplatz. Da es sich um eine reine Software-Implementation handelt, finden wir ihn im Modulkatalog nicht unter "Timer" sondern unter "Misc Digital". Klicken sie ihn zwei Mal an und er erscheint im Workspace-Explorer. Wir ändern seine Eigenschaften wie folgt ab:

 

sleepTMR1.png
 

Als Name wählen wir STMR und da wir den Timer nur für kurze Zeitschleifen brauchen, reichen uns 2 Byte. Den SleepTimer können wir mit 4 Frequenzen betreiben: 512 Hz, 64 Hz, 8 Hz und 1 Hz. Dieser Wert lässt sich im "Global Ressoruces"-Fenster einstellen, programmtechnisch aber jederzeit ändern. Wir wählen für unseren ersten Versuch eine Frequenz von 64 Hz und nehmen die Einstellung im Programm vor. Als ersten Test werden wir einfach eine Zählschleife programmieren, die von 1 bis 10 zählt und jede halbe Sekunde den neuen Wert der Schleife auf der LCD-Anzeige ausgibt. Der Programmcode dazu sieht folgendermaßen aus:

 

timer_1.png

 

In Zeile 7 finden wir ein Makro, mit dem wir sämtliche Interrupts freischalten können. Da der Timer mit Interrupts arbeitet, ist dieser Aufruf notwendig. Danach folgen die obligatorischen Start()-Funktionen für die Anzeige und den SleepTimer. Die Funktion in Zeile 11 setzt die Timer-Frequenz auf 64 Hz, dafür stehen die symbolischen Kontanten zur Verfügung, die sich aus dem Modulnamen (STMR) und der Angabe _512_HZ, _64_HZ, _8_HZ oder _1_HZ zusammensetzt. Eine Beschreibung der LCD-Funktionen finden Sie im 2. Tutorial. Die einzige Funktion, die wir aus dem Timer nutzen, ist _TickWait(). Wir übergen einen Integer-Wert, der angibt, wie lange die Funktion warten soll: 32 Ticks bei 64 Hz ergibt eine halbe Sekunde. 

 

 
 

Eine weitere interessante Funktion des SleepTimers ist der Aufruf _SyncWait(BYTE bTicks, BYTE bMode). Der Parameter bTicks bestimmt wieder die Anzahl der Zyklen und mit bMode lässt sich eine der beiden Betriebsarten wählen:

 

_FORCE_RELOAD: Beim Aufruf wird der Zähler sofort mit dem Wert bTicks geladen und die Funktion dann verlassen

_WAIT_RELOAD: Zunächst wird der Zähler auf Null gezählt, dann neu geladen und die Funktion verlassen. 

 

Mit dieser Funktion lassen sich z. B. Time-out-Funktionen entwickeln oder Synchronisationen durchführen, so dass eine Schleife mit mehreren Verzweigungen z. B. immer im selben Zeitraster aufgerufen werden kann.

 

Wir wollen uns nun einen anderen Timer anschauen und in diesem Zusammenhang werden wir uns auch um die Verarbeitung von Interrupts kümmern.

 

Bitte (nicht) unterbrechen!

 

Interrupts (Unterbrechnungen) können im PSoC sowohl durch die Anschluss-Pins als auch durch bestimmte Modulereignisse ausgelöst werden. Mögliche Quellen sind z, B.:

 

  • Ablauf des Timers,
  • Überschreiten eines Wertes im Komparator,
  • Empfangsereignis auf einer Schnittstelle,
  • fallende oder steigende Flanke an einem bestimmten Anschluss-Pin.

 

Bei einem Interrupt verzweigt das Programm unverzüglich an eine bestimmte Stelle, der Interrupt-Service-Routine (ISR), führt die dort angegebenen Befehle aus und springt dann zurück an die Stelle, an der sich das Programm beim Aufruf des Interrupts befand. Da das Programm aber nach der Rückkehr den alten Kontext wieder herstellen muss, werden die wichtigsten Speicherstellen und Register automatisch gespeichert. Würde dies nicht geschehen, wüsste das Programm z. B. gar nicht wo es weiter laufen soll. 

 

Wir werden uns zunächst die Verabeitung eines Timer-Interrupts anschauen.

 

Entfernen Sie den SleepTimer und klicken Sie dafür auf das Usermodul Timer16, einem 16-Bit breiten Timer. Diesmal werden zwei zusätzliche digitale Blöcke belegt und im "Parameters-Window" erscheint eine Vielzahl an Einstellmöglichkeiten. Für unsere Zwecke wählen Sie die folgenden Werte:

 

Timer8_parameter.png
 

 

Wir benennen den Timer in TMR um und entscheiden uns beim Takt für das interne 32-kHz-Signal. Mit Capture lässt sich eine Bedingung bestimmen, bei der der momentane Zählerwert in das Compare-Register geschrieben wird. Das benötigen wir nicht, ebenso wenig sollen die Signal "TerminalCountOut" und "ComparOut" auf externe Pins geschaltet werden. Wichtig hingegen ist für uns der Eintrag in "Period". Hier geben wir den Zählerstartwert mit 32000 an. Bei 32 kHz würde unser Timer genau 1 Sekunde brauchen, bis er auf 0 gezählt hat. Mit "CompareValue" geben wir den Wert an, bei dem ein Interrupt ausgelöst und der Zähler wieder neu gestartet wird. In unserem Fall belassen wir es auf "0". Als "CompareType" wählen wir, dass der Interrupt dann ausgelöst werden soll, wenn der Timerwert 0 oder kleiner als 0 ist. Der "Interrupt Type" steht auf "Terminal Count", d. h. nach Ablauf des Zählers erfolgt eine Auslösung und die Taktquelle ist mit dem Systemtakt synchronisiert. Die beiden letzten Einträge legen fest, ob das Ausgangssignal von "TerminalCount" eine volle oder eine halbe Taktperiode läuft und ob das Capture-Signal aktiv high oder low ist. 

 

Wir wollen nun ein Programm schreiben, das von der Wirkung her identisch mit dem vorher beschriebenen Beispiel ist. Allerdings ist der Weg auf dem wir das lösen ein ganz anderer. Das Programm gibt in einer Endlos-Schleife immer nur die Variable iWert aus. Der Timer zählt von 32000 mit einer Frequenz von 32 kHz herunter und löst bei 0 einen Interrupt aus. Bei jedem Interrupt soll die Variable hoch gezählt werden. Die main-Funktion des Programm sieht dann wie folgt aus:

 

timer_interrupt_1b.png
 

 

 

Die globale Interrupt-Freigabe haben wir mit dem Makro "M8C_EnableGInt" schon kennen gelernt. Der Name bedeutet so viel wie: "Freigabe aller globalen Interrupts im M8C-Kern". Danach erfolgt der Start der beiden Module und die Angabe des Zählerstartwertes. Das ist zwar schon im "parameters window" geschehen, falls wir den Wert aber ändern wollen, können wir dies im Programm tun und müssen nicht extra die Konfiguration neu erstellen lassen. Der nächste Befehl ist die spezifische Interrupt-Freigabe des Timers.

 

Doch im Moment würde das Programm für immer in der while-Schleife verbleiben, auch wenn ein Interrupt ausgelöst wird, denn wohin soll das Programm springen? Dazu müssen wir noch drei Schritte durchführen:

 

timer_interrupt_2.png
 

In Zeile 4 führen wir die Deklaration der Interrupt-Routine durch und in Zeile 28 wird sie definiert. Die Endung _isr hat sich für die Bezeichnung von Interrupt-Service-Routinen eingebürgert und so können Sie auch sofort erkennen, dass es keine gewöhnliche Funktion ist, zwingend ist das aber nicht.

 

Nun bleibt immer noch eine entscheidende Frage zu klären: "Woher weiß das Programm, dass "timer_isr" die Interrupt-Routine für den Timer ist? Der Name ist ja willkürlich und es könnten durchaus mehrere Module im Programm sein oder auch Anschluss-Pins, die Interrupts auslösen. Diese letzte Frage klären wir durch den 3. oben angekündigten Schritt. Zu jedem Modul, das Interrupts auslösen kann, finden Sie eine passende Assembler-Datei im "Workspace Explorer":

 

workspace_explorer_1.png

 

In unserem Fall ist das die Datei TMRINT.asm. Diese Datei besteht immer aus dem von Ihnen gewählten Modulnamen (hier TMR) und den Zusatz INT. Öffnen Sie nun diese Datei im Editor und suchen Sie die folgende Stelle:

 

timer_isr_1.png

 

Im Kommentar (Zeile 75 bis 78) heißt es nun, dass man die folgenden 3 Zeilen aktivieren soll. Das stimmt allerdings nicht so allgemein. Wir haben die Interrupt-Behandlung über die #pragma Anweisung durchgeführt und dieses Pragma nimmt uns einiges an Arbeit ab, was in dieser Assembler-Routine mit den Makros "PRESERVE_CPU_CONTEXT" und "RESTORE_CPU_CONTEXT" noch einmal erledigt und zu Fehlern führen würde. Also gehen wir anders vor: Wir löschen alle drei Zeilen und ersetzen sie durch einen ljmp-Befehl auf unsere Interrupt-Service-Routine. Damit das Assemblerprogramm weiß, dass es sich um eine C-Routine handelt, müssen wir dem Namen einen Unterstrich voranstellen:

 

timer_isr_2.png

 

Speichern Sie die Datei wieder ab und erstellen Sie das Programm komplett neu durch F6. Die Variable wird wie erwartet hoch gezählt und auf dem Display angezeigt.

 

Je nachdem, ob und mit welchem Controller Sie bisher gearbeitet haben, mag das Prozedere beim PSoC zunächst ein wenig komliziert erscheinen, der Ablauf ist aber im Grunde ganz logisch und geht sehr schnell in Fleisch und Blut über. Er ist auch für alle Module absolut identisch und besteht immer aus den Schritten:

 

1. Interrupt-Quelle in der Hardware-Konfiguration einstellen

2. ISR-Routine mit dem #pragma interrupt_handler deklarieren

3. ISR-Routine im Programm definieren

4. ljmp-Befehl _my_isr_routine in der jeweiligen nameINT.asm Datei eintragen

5. Globale und spezielle Interrupts im Programm freigeben

 

Die Behandlung von Interrupts an Anschluss-Pins läuft minimal anders ab. Zunächst definieren Sie in den Pinout-Einstellungen den entsprechenden Anschluss und wie der Interrupt ausgelöst wird (steigende Flanke, fallende Flanke oder Signalwechsel). Nachdem Sie die Konfigurationsdateien erzeugt haben, steht Ihnen anstelle der nameINT.asm-Datei nun die Datei PSoCGPIOINT.asm für den Eintrag der _my_isr_routine zur Verfügung. Im Programm müssen Sie dann lediglich noch mit dem Befehl: 

M8C_EnableIntMask (INT_MSK0, INT_MSK0_GPIO);

die Interrupts für Anschluss-Pins frei geben. Der Rest bleibt identisch.

 

Noch ein Hinweis zur Gestaltung der ISR-Routine: Halten Sie den Umfang möglichst gering und lagern Sie so viel Funktionalität wie möglich aus der ISR-Routine aus. Auf keinen Fall sollten Modulfunktionen nutzen, die wiederum Interrupts benötigen, wie beispielsweise die LCD-Anzeige. Während der ISR-Berbeitung sind weitere Interrupts gesperrt, damit der Stack nicht überlaufen kann. Sie können aber am Anfang der ISR-Routine die Interrupts wieder frei geben, wenn dies unumgänglich ist. Dies kann allerdings (wie bei jedem Controller) zu unkontrollierbarem Verhalten führen.  

 

Ich hoffe, es hat Ihnen Spaß gemacht und Sie sind neugierig auf weitere Informationen rund um diesen faszinierenden Controller. Im nächsten Teil geht es im digitalen Bereich um die EEPROM-Module und die serielle Schnittstelle, im analogen Bereich schauen wir uns die Filterfunktionen an. Ich würde mich freuen, wenn Sie wieder mit dabei sind. 

 

Für weitere Informationen und Bestellungen des Böcker PSoC-Starter-Kits einfach auf nachfolgenden Link klicken:

logo_klein.png Böcker Sirius STK1 Starter-Kit                   
 

 

Falls Sie Fragen zum Thema haben, wenden Sie sich einfach an uns.

Wir helfen Ihnen gern weiter!

 

Ihr Team von Böcker Systemelektronik

 

Hinweis: Unser Tutorialangebot wird in unregelmäßigen Abständen erweitert. Wenn Sie sofort über eine Neuerscheinung informiert werden möchten, tragen Sie sich bitte hier in unsere Benachrichtigungsliste "Tutorials" ein. Sie können diesen unverbindlichen Service jederzeit in Ihrem Kundenkonto oder per E-Mail wieder abbestellen.  

 

 

Copyright © Böcker Systemelektronik