tinyAVR-Serie Teil 3: Timer D

Über den Beitrag

Im dritten und letzten Teil dieses Beitrages über die tinyAVR-Serie geht es um den Timer/Counter D, kurz: Timer D. Dieser Timer ist in vielerlei Hinsicht speziell. Vor allem unterscheidet ihn, dass er im Gegensatz zu den Timern A und B nur in der tinyAVR-Serie 1 zum Einsatz kommt.

Wie schon bei den anderen Teilen dieser Beitragsserie möchte ich darauf hinweisen, dass ich auch beim Timer D nicht auf alle Funktionalitäten in voller Tiefe eingehen kann. Die Beispielsketche habe ich für einen ATtiny1614 und zum Teil für einen ATtiny3216 geschrieben. Wenn ihr andere Typen verwendet, mag die eine oder andere Anpassung notwendig sein. Das dürfte sich aber auf die Zuordnung bestimmter Funktionen zu bestimmten Pins beschränken. Wie auch immer: Ein Blick in das Datenblatt ist anzuraten. 

Hier ein Überblick über den Inhalt aller Beiträge dieser Reihe:

Achtung: schwer verdaulicher Beitrag!

Der Timer D ist ziemlich komplex und viele Aspekte, die ich hier behandele, bedingen sich gegenseitig. Es ist deswegen schwer, das Thema didaktisch schön aufzubauen. Vieles müsst ihr nach dem Motto „learn now, understand later“ beim ersten Lesen zunächst hinnehmen. Das eine oder andere wird vielleicht erst mit den Beispielsketchen, die ganz zum Schluss kommen, richtig klar. Auch mir hat der Timer D zwischenzeitlich einige Kopfschmerzen bereitet!

Kurzer Überblick über den Timer D

Hier ein paar ausgewählte Eigenschaften des Timer D:

  • 12-Bit Timer/Counter
  • Takt: System, 20 MHz Oszillator oder extern, asynchron
  • Nur die Vertreter der tinyAVR-Serie 1 besitzen einen Timer D (und zwar genau einen, nämlich TCD0).
  • Vier „Waveform Generation Modes“ für die Ausgabe:
    • One Ramp Mode
    • Two Ramp Mode
    • Four Ramp Mode
    • Dual Slope Mode
  • Zwei Input Channels für Events.
  • Zwei Ausgänge.
  • Vier Compare-Register bzw. zwei Paare:
    • A: Compare-Register CMPASET / CMPACLR
    • B: Compare-Register CMPBSET / CMPBCLR
  • Jeder TCD-Zyklus besteht aus den Phasen:
    • DTA (Dead Time A) → OTA (On Time B) → DTB (Dead Time B) → OTB (On Time B)

Bitte beachten: Wenn ihr das Boardpaket megaTinyCore von Spence Konde einsetzt, dann wird der Timer D in der Standardeinstellung für millis() / delay() und analogWrite() genutzt. Änderungen an den Einstellungen des Timer D können diese Funktionen unbrauchbar machen. Ihr könnt die Voreinstellungen in der Arduino IDE unter Werkzeuge → millis()/micros() Timer bzw. PWM Pins ändern.

Asynchronität des Timer D

Der Takt des TCDn-Kerns ist asynchron zum Peripherietakt. Das hat ein paar Nebenwirkungen. Fast alle TCDn-Register müssen, nachdem ihr sie beschrieben habt, erst synchronisiert werden, bevor sie ihre Wirkung entfalten. Und ist eine Synchronisierung in Gange, könnt ihr einige Einstellungen nicht vornehmen, sondern müsst warten, bis die laufende Synchronisierung abgeschlossen ist. Auch könnt ihr den Counter nicht direkt auslesen, sondern nur indirekt über die beiden Capture-Register. Insgesamt ist die Nutzung des Timer D dadurch vergleichsweise anspruchsvoller, weil man viel mehr falsch machen kann. Andererseits ist der Timer D ein echtes Spezialtool.

Lohnt der Aufwand? Braucht man die Features des Timer D? Das zu beantworten überlasse ich euch!

Die Register des Timer D (Modul TCDn)

Registertypen

Bevor wir in die einzelnen Register gehen, muss ich noch auf die Unterscheidung zwischen statischen Registern, doppelt gepufferten Registern, Anweisungs- / Aktivierungsregistern und normalen Registern / Read-Only-Registern eingehen. Diese Registertypen unterscheiden sich vor allem dadurch, unter welchen Voraussetzungen ihr sie beschreiben könnt und unter welchen Bedingungen sie synchronisiert werden.

Anweisungs- und Aktivierungsregister (Command / Enable): In diese Kategorie fallen lediglich das Kontrollregister E (TCDn.CTRLE) und das ENABLE-Bit im Kontrollregister A (TCDn.CTRLA). Die Synchronisierung erfolgt automatisch, sofern der Timer D aktiviert ist. Änderungen in TCDn.CTRLE könnt ihr nur vornehmen, wenn das CMDRDY-Bit im Statusregister (TCDn.STATUS) gesetzt ist (dazu kommen wir noch).  Für Änderungen des ENABLE-Bits muss das ENRDY-Bit im Statusregister gesetzt sein.

Doppelt gepufferte Register: Die doppelt gepufferten Register könnt ihr beschreiben, sofern das CMDRDY-Bit im Statusregister gesetzt ist. Ihre Einstellungen werden erst dann in die TCDn Domain übernommen, wenn ihr ein SYNC Command sendet oder wenn ihr den Timer D aktiviert.

  • Doppelt gepufferte Register sind: DELCTRL, DELVAL, DITCTRL, DITVAL, DBGCTRL, COMPASET, COMPBSET, CMPACLR, CMPBCLR.

Statische Register: Diese Register können nicht beschrieben werden, solange der Timer D aktiv (enabled) ist. Eine Ausnahme ist das ENABLE-Bit in TCDn.CTRLA (wäre ja auch blöd, wenn nicht!).

  • Statische Register sind: CTRLA bis CTRLD, EVCTRLA/B, INPUTCTRLA/B, FAULTCTRL.

Normale/Read-Only Register: Diese Register unterliegen keinen Restriktionen hinsichtlich der Synchronisierung, d. h. ihr könnt sie jederzeit beschreiben bzw. lesen.

  • Normale Register: INTCTRL, INTFLAGS.
  • Read-Only Register: CAPTUREA, CAPTUREB, STATUS

Kontrollregister A – TCDn.CTRLA

Timer D Register: 
TCDn.CTRLA
Kontrollregister A – TCDn.CTRLA

Im Kontrollregister nehmt ihr die folgenden Einstellungen vor:

  • CLKSEL (Clock Select): Auswahl des Taktgebers. Zur Auswahl stehen:
    • 20MHZ: Interner 20 MHz Oszillator (Default)
    • EXTCLK: Externer Oszillator
    • SYSCLK: Systemtakt
  • CNTPRES (Counter Prescaler): Prescaler für Timer Counter D
    • DIV1 (Default), DIV4, DIV32: Teiler
  • SYNCPRES (Synchronization Prescaler): Prescaler für die Synchronisation
    • DIV1 (Default), DIV2, DIV4, DIV8: Teiler
  • ENABLE: aktviert TCDn (Default: disabled)

Der effektive Takt für die Ausgabe an WOx beträgt:

    \[ \text{freq} = \text{TCDn\_clock} \cdot \text{counter\_prescaler} \cdot \text{synchronization\_prescaler} \]

Das ENABLE-Bit könnt ihr nur setzen, wenn das ENRDY-Bit im Statusregister gesetzt ist. Das stellt ihr sicher, indem ihr mit while(!(TCD0.STATUS & TCD_ENRDY_bm)){;} darauf wartet.

Kontrollregister B – TCDn.CTRLB

Timer D Register: 
TCDn.CTRLB
Kontrollregister B – TCDn.CTRLB

Das Kontrollregister B (CTRLB) legt den Waveform Generation Modus fest. Ihr habt die Optionen:

  • ONERAMP: One Ramp Mode (Default)
  • TWORAMP: Two Ramp Mode
  • FOURRAMP: Four Ramp Mode
  • DS: Dual Slope Mode

Ich gehe später detailliert auf die verschiedenen Modi ein.

Kontrollregister C – TCDn.CTRLC

Timer D Register: 
TCDn.CTRLC
Kontrollregister C – TCDn.CTRLC

Die voreingestellten Ausgänge für die durch CMPASET/COMPACLR und COMPBSET/COMPBCLR erzeugten Signale (Waveform A/B) liegen auf WOA bzw. WOB. Schaut im Datenblatt unter „I/O Multiplexing and Considerations“, welche Pins das sind. Alternativ könnt ihr die Signale an WOC und WOD ausgeben, sofern die Pins auf eurem ATtiny ausgeführt sind. Im Kontrollregister C stellt ihr ein, welches Signal welchem alternativen Ausgang zugeordnet wird:

  • CMPDSEL (WOD):
    • PWMA: Waveform A (Bit-Wert 0, Default)
    • PWMB: Waveform B (Bit-Wert 1)
  • CMPCSEL (WOC):
    • PWMA: Waveform A (Bit-Wert 0, Default)
    • PWMB: Waveform B (Bit-Wert 1)

Das sind die Datenblattangaben. Hier ist allerdings irgendetwas schiefgelaufen. Ihr erhaltet beispielsweise die gleichzeitige Ausgabe von WOA an WOC und WOD mit:

TCD0.CTRLC |= TCD_CMPDSEL_PWMA_gc | TCD_CMPCSEL_PWMB_gc;

Aktiviert ihr das FIFTY-Bit, dann werden die Einstellungen für CMPASET und CMPACLR zwangsweise auch für CMPBSET und CMPBCLR übernommen bzw. andersherum. Gebt ihr alle Werte ein, dann gelten die, die ihr zuletzt eingegeben habt.

Die anderen zwei Bits machen Folgendes:

  • AUPDATE (Automatically Update): Automatische Synchronisierung am Ende des TCD-Zyklus. Genauer gesagt wird die Synchronisierung mit dem Beschreiben von CMPBCLRH, also dem oberen Byte von CMPBCLR, ausgelöst.
  • CMPOVR (Compare Out Value Overwrite): Ist dieses Bit gesetzt, dann bestimmen die Einstellungen in TCDn.CTRLD das Ausgabelevel der verschiedenen TCD-Phasen.

Kontrollregister D – TCDn.CTRLD

Timer D Register: 
TCDn.CTRLD
Kontrollregister D – TCDn.CTRLD

Die Einstellungen dieses Registers werden nur aktiv, wenn ihr das CMPOVR-Bit in CTRLC setzt.

Um die Wirkung dieses Kontrollregisters zu verstehen, benötigt ihr Kenntnisse über die Waveform Generation Modes. Ggf. überspringt ihr dieses Kapitel und kommt später wieder zurück.

Nehmen wir ein Beispiel. Im Four Ramp Mode ändert sich das Ausgabelevel an WOA / WOB in der Standardeinstellung wie folgt:

  1. DTA (Dead Time A): WOA und WOB sind LOW
  2. OTA (On Time A): WOA ist HIGH, WOB ist LOW
  3. DTB (Dead Time B): WOA und WOB sind LOW
  4. OTB (On Time A): WOA ist LOW, WOB ist HIGH

Das entspricht der Einstellung:

TCD0.CTRLC = TCD_CMPOVR_bm;
TCD0.CTRLD = TCD_CMPBVAL_3_bm | TCD_CMPAVAL_1_bm; 

Wollt ihr jetzt beispielsweise, dass nicht nur WOB, sondern auch WOA während OTB auf HIGH ist, dann stellt ihr das folgendermaßen ein:

TCD0.CTRLC = TCD_CMPOVR_bm;
TCD0.CTRLD = TCD_CMPBVAL_3_bm | TCD_CMPBVAL_1_bm | TCD_CMPAVAL_1bm; 

Ich hoffe, das macht das Prinzip klar. Wichtig ist: Das Setzen von CMPOVR lässt alle Ausgänge in allen Phasen auf LOW gehen. Für alle Phasen, in denen ihr ein HIGH-Level an WOA oder WOB erzeugen wollt, müsst ihr das entsprechende CMPxVAL[y]-Bit setzen.

Kontrollregister E – TCDn.CTRLE

Timer D Register: 
TCDn.CTRLE
Kontrollregister E – TCDn.CTRLE

Die Bits des Kontrollregisters E (CTRLE) bewirken Folgendes:

  • DISEOC (Disable at End of TCD Cycle Strobe): TCDn wird am Ende des TCD Zyklus automatisch deaktiviert.
  • SCAPTUREA / SCAPTUREB (Software Capture A/B Strobe): löst bei der nächsten Synchronisierung einen Software Capture zu Capture A bzw. B aus.
  • RESTART (Restart Strobe): löst bei der nächsten Synchronisierung einen Neustart des Timer D aus.
  • SYNC (Synchronize Strobe): die Inhalte der doppelt gepufferten Register (z.B. CMPxSET, CMPxCLR – siehe Datenblatt) werden bei der nächsten Synchronisierung in die TCD Domain übernommen.
  • SYNEOC (Synchronize End of TCD Cycle Strobe): die Inhalte der doppelt gepufferten Register werden am Ende des TCD Zyklus in die TCD Domain übernommen.

Für das Kontrollregister E (CTRLE) gilt:

  • Es handelt sich um ein Strobe-Register. Seine Bits werden nach der nächsten Synchronisierung gelöscht. 
  • Ein Beschreiben von CTRLE löscht das CMDRDY-Bit im STATUS-Register. Mit der nächsten Synchronisierung wird das CMDRDY-Bit wieder gesetzt.
  • Das Setzen des DISEOC-Bits löscht das ENRDY-Bit bis zum Abschluss des TCD-Zyklus.

Event-Kontrollregister A/B – TCDn.EVCTRLA / TCDn.EVCTRLB

Timer D Register: Event-Kontrollregister A bzw. B
Event-Kontrollregister A bzw. B

Der Timer D hat zwei Inputkanäle für Events, nämlich A und B. Die Event-Kontrollregister steuern, wie sich TCDn bei eingehenden Eventsignalen verhält. Ich beginne hier mal beim untersten Bit:

  • TRIGEI (Trigger Event Input Enable): Aktiviert den Eingang von Signalen als Trigger für den Inputkanal. Ist TRIGEI nicht gesetzt, haben die Eventsignale keine Wirkung (Default: disabled).
  • ACTION (Event Action):
    • FAULT: Das Event löst einen sogenannten Fault aus (Default). Der Fault kann unter anderem den Zähler stoppen oder einen Sprung zu einem anderen Compare-Zyklus auslösen. Was der Fault genau bewirkt, legt ihr im Register INPUTCTRLA bzw. B fest.
    • CAPTURE: Das Event löst einen Capture und einen Fault aus. Bei einem Capture wird der Zählerstand, je nach Inputkanal, in das CAPTUREA- bzw. CAPTUREB-Register geschrieben.
  • EDGE (Edge Selection):
    • FALL_LOW: Die fallende Kante oder der LOW Zustand des Events ist der Trigger (Default).
    • RISE_HIGH: Die steigende Kante oder der HIGH Zustand des Events ist der Trigger.
  • CFG (Event Configuration):
    • NEITHER: „Keine Extras“ aktiviert (Default).
    • FILTER: Das Event muss über vier Taktzyklen hinweg detektiert werden, damit es als valide gilt.
    • ASYNC: Mit diesem Bit sorgt ihr dafür, dass ein asynchrones Event direkt auf den Ausgang wirkt.

Interrupt-Kontroll- und Interrupt-Flagregister – TCDn.INTCTRL / TCDn.INTFLAGS

Timer D Register: Interrupt-Kontroll-Register / Interrupt-Flag-Register
Interrupt-Kontrollregister / Interrupt-Flagregister – TCDn.INTCTRL / TCDn.INTFLAGS

Der Timer D hat drei Interrupts, die ihr durch das Setzen der entsprechenden Bits im Interrupt-Kontrollregister aktiviert:

  • TRIGA / TRIGB (Trigger A/B Interrupt Enable): Eingehende Eventsignale über den Input A bzw. B lösen einen Interrupt aus.
  • OVF (Counter Overflow): Das Bit aktiviert den Overflow-Interrupt. Der Top-Wert des Counters ist grundsätzlich CMPBCLR.

Wurde ein Interrupt ausgelöst, dann wird das entsprechende Bit im „baugleichen“ Interrupt-Flagregister INTFLAGS gesetzt. Um es zu löschen, müsst ihr es mit einer „1“ überschreiben, also z. B. TCD0.INTFLAGS = TCD_OVF_bm;. Klingt unlogisch, ist aber so.

Statusregister – TCDn.STATUS

Timer D Register: Statusregister
Statusregister – TCDn.STATUS

Die Bits PWMACTA/B werden gesetzt, wann immer WOA oder WOB von HIGH nach LOW oder umgekehrt wechseln. Um die Bits zu löschen, müsst ihr sie mit einer „1“ überschreiben.

Wie schon zuvor beschrieben, könnt ihr den Timer D nicht zu jedem beliebigen Zeitpunkt im Kontrollregister A (CTRLA) aktivieren. Ihr müsst warten, bis das ENRDY-Bit gesetzt ist. Unter bestimmten Bedingungen kann der Timer D auch keine neuen Anweisungen entgegennehmen. In dem Fall ist das CMDRDY-Bit gelöscht. Das Setzen der folgenden Bits löscht CMDRDY:

  • TCDn.CTRLC: AUPDATE / Schreiben in das CMPBCLRH-Register
  • TCDn.CTRLE: SYNCEOC, SYNC, RESTART, SCAPTUREA/B

Input-Kontrollregister A/B – TCDn.INPUTCTRLA / TCDn.INPUTCTRLB

TImer D Register - TCDn.INPUTCTRLA / TCDn.INPUTCTRLB
Input-Kontrollregister A bzw. B – TCDn.INPUTCTRLA / TCDn.INPUTCTRLB

Ein eingehendes Eventsignal löst, je nach Einstellung im EVCTRLA/B-Register, einen Capture und/oder einen sogenannten Fault aus. Der Fault wirkt auf den Zähler und den Output. Wie er das tut, legt ihr über den Inputmodus im Input-Kontrollregister A bzw. B fest. Im Datenblatt findet ihr schematische Darstellungen zu den Inputmodi.

Hier, als Beispiel, der Inputmodus „WAIT“:

Wirkung des Inputmodes WAIT
Wirkung des Inputmodes WAIT

WOA und WOB werden bei Eingang des Eventsignals im WAIT-Modus gestoppt. Der Counter springt zum nächsten Compare-Zyklus, also z. B. von OTA zu DTB. Solange das Event andauert, wartet der Timer D. Da es insgesamt elf verschiedene Inputmodi gibt, werde ich nicht jeden einzelnen hier besprechen können. In einem meiner Beispielsketche könnt ihr die Inputmodes selbst ausprobieren.

Zu beachten: Im Two und Four Ramp Mode funktionieren alle Inputmodi, im One Ramp und Dual Slope Mode jedoch nur eine Auswahl (siehe Datenblatt).

Fault-Kontrollregister – TCDn.FAULTCTRL

Timer D Register - TCDn.FAULTCTRL
Fault-Kontrollregister – TCDn.FAULTCTRL

Im Fault-Kontrollregister FAULTCTRL aktiviert ihr die Standardausgänge WOA und WOB bzw. die alternativen Ausgänge WOC und WOD. Außerdem legt ihr den Level des Ausgangs nach Eingang eines Eventsignals oder nach einem Reset (außer: Power-On-Reset) fest.

  • CMPxEN (Compare x Enable): aktiviert den Ausgang WOx
  • CMPx (Compare x Value): Ist das Bit gesetzt, ist der Ausgang WOx nach einem Reset (außer Power-On-Reset) oder bei einem Eventsignal HIGH, sonst LOW.

Weitere Register

Auf die folgenden Register gehe ich nicht im Detail ein. Ein Teil dürfte selbsterklärend sein, für die anderen schaut ins Datenblatt.

  • DLYCTRL (Delay Control): Kontrollregister für Delays.
  • DLYVAL (Delay Value): Der Delay-Wert.
  • DITCTRL (Dither Control): Kann man eine bestimmte Frequenz nicht exakt darstellen, kann man sich mit einem Dither-Wert behelfen. Das Prinzip ist das gleiche wie bei Schaltjahren.
  • DITVAL (Dither Value): Der Dither-Wert.
  • DBGCTRL (Debug Control): Einstellungen für den Debug-Modus.
  • CAPTUREA / CAPTUREB (Capture A/B): 12-Bit Capture-Register.
  • CMPASET / CMPBSET (Compare A/B Set): 12-Bit Register für die Compare-Set-Werte.
  • CMPACLR / CMPBCLR (Compare A/B Clear): 12-Bit Register für die Compare-Clear-Werte.

Waveform Generation Modi

Ich komme jetzt noch einmal auf die Waveform Generation Modi zurück. Diese legen fest, welche Wirkung die CMPxSET und CMPxCLR auf die Ausgabe haben.

One Ramp Modus

Grundsätzlich durchläuft ein TCD-Zyklus die Phasen DTA (Dead Time A), OTA (On Time A), DTB (Dead Time B) und OTB (On Time B). Das ist im One Ramp Modus der Fall, wenn CMPASET < COMPACLR < CMPBSET < CMPBCLEAR:

One Ramp Modus mit CMPASET < CMPACLR < CMPBSET < CMPBCLR
One Ramp Modus mit CMPASET < CMPACLR < CMPBSET < CMPBCLR

Falls eure Werte von der Reihenfolge abweichen, können bestimmte Phasen auch wegfallen. Wenn etwa euer CMPBSET kleiner als CMPASET ist, dann gibt es keine DTB-Phase:

One Ramp Modus mit CMPBSET < CMPASET < CMPACLR < CMPBCLR
One Ramp Modus mit CMPBSET < CMPASET < CMPACLR < CMPBCLR

Der TCD-Zyklus endet immer mit dem Erreichen von CMPBCLR. Das ist unabhängig vom Modus. Sind CMPACLR oder COMPxSET größer als CMPBCLR, sind diese Werte wirkungslos.

Da CMPBCLR der Top-Wert des Counters ist, bestimmt er die Frequenz des TCD-Zyklus:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{(\text{CMPBCLR} +1)} \]

Two Ramp Modus

Wie es der Name vermuten lässt, besteht ein TCD-Zyklus im Two Ramp Mode aus zwei Rampen. Die Erste endet mit CMPACLR. Danach wird der Counter „genullt“ und die zweite Rampe läuft bis CMPBCLR. 

Timer D - Two Ramp Modus
Two Ramp Modus

Hier ist es für die Abfolge egal, ob CMPACLR kleiner oder größer als CMPBCLR ist. Für die Frequenz des TCD-Zyklus gilt:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{(\text{CMPACLR} +1 + \text{CMPBCLR}+1)} \]

Denkt also daran, dass selbst, wenn ihr CMPACLR auf 0 setzt, DTA immer noch eine Länge von einem TCD Count hat.

Four Ramp Modus

Im Four Ramp Modus bekommen alle CMPxSET- und CMPxCLR-Werte ihre eigene Rampe. Die CMPxSET-Rampen erzeugen die Dead Time x Phasen, die CMPxCLR-Rampen erzeugen die On Time x Phasen. Überschneidungen der Phasen sind nicht möglich.

Four Ramp Modus

Die Frequenz des TCD-Zyklus ist:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{(\text{CMPASET} +\text{CMPACLR} +\text{CMPBSET} + \text{CMPBCLR}+4)} \]

Dual Slope Modus

Im Dual Slope Modus zählt der Counter von CMPBCLR herunter bis 0 und dann wieder hinauf bis CMPBCLR. Ein Compare-Match von CMPASET lässt WOA beim Herunterzählen auf HIGH gehen, beim Hinaufzählen auf LOW. Mit CMPBSET verhält es sich umgekehrt. Im ersten TCD-Zyklus beginnt WOB allerdings im LOW-Zustand und geht erst bei dem Compare-Match mit CMPBSET auf der aufsteigenden Seite auf HIGH. Die folgenden Zyklen sehen wie folgt aus:

Timer D - Dual Slope Modus
Dual Slope Modus

CMPACLR ist im Dual Slope Modus ohne Bedeutung.

Für die Frequenz des TCD-Zyklus gilt:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{2 \cdot (\text{CMPBCLR} +1)} \]

Beispielsketche

Nun zur Praxis – welch Erlösung nach so viel Information.

Zu beachten: Das Boardpaket megaTinyCore nutzt sowohl den Timer D als auch andere in meinen Sketchen verwendete Timer für millis(), analogWrite(), Servo usw. Diese Funktionen können u. U. ihren Dienst verweigern. Für die Beispielsketche empfehle ich in der Arduino IDE die Einstellung „millis()/micros() Timer: RTC (no micros)“ oder „TCB0 (breaks tone() and Servo)“

Waveform Generation Modi Beispiele

Basissketch

Im ersten Beispiel schauen wir uns die Einstellung der Waveform Generation anhand des Two Ramp Modus an. Hier zunächst der Sketch:

void setup(){
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;

   /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm; // enable WOA and WOB   

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_TWORAMP_gc;
    TCD0.CMPASET = 499;   // DTA = ~8.3 %
    TCD0.CMPACLR = 1999;  // OTA = 25 %
    TCD0.CMPBSET = 2999;  // DTB = 50 %
    TCD0.CMPBCLR = 3999;  // OTB = ~16.7 % 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV4_gc | TCD_SYNCPRES_DIV8_gc | TCD_ENABLE_bm; 
}

void loop(){} 

Wenn ihr Einstellungen am Timer D vornehmt, müsst ihr eine bestimmte Reihenfolge einhalten und für einige Register Vorkehrungen treffen, bevor ihr sie beschreibt:

  1. Falls der Timer D aktiviert ist, müsst ihr ihn deaktivieren, indem ihr das ENABLE-Bit im Kontrollregister A (CTRLA) löscht. Das mache ich im Sketch mittels TCD0.CTRLA = 0;. Um die anderen Einstellungen beizubehalten, könntet ihr das Bit auch selektiv mittels TCD0.CTRLA &= ~TCD_ENABLE_bm; löschen.
    • In TCD0.CTRLA könnt ihr nur schreiben, wenn das ENRDY-Bit im Statusregister gesetzt ist. Das stellt die Zeile 3 sicher.
  2. Wenn ihr die Ausgänge WOx benutzen wollt, dann müsst ihr die entsprechenden Pins auf OUTPUT setzen. Welche Pins das sind, findet ihr im Datenblatt.
  3. Um die Ausgänge zu aktivieren, setzt ihr die entsprechenden CMPxEN-Bits im Fault-Kontrollregister FAULTCTRL.
    • Um zuvor den Schreibschutz des FAULTCTRL-Registers aufzuheben, schreibt ihr die Bitgruppe IOREG (unlock protected I/O registers) in das CCP-Register (Configuration Change Protection).
  4. Dann wählt ihr den Waveform Generation Modus und setzt die Werte für CMPxSET und CMPxCLR.
  5. Im letzten Schritt aktiviert ihr den Timer D und stellt ggf. den Taktgeber, den Counter-Prescaler und den Synchronisierungs-Prescaler ein. 
Besprechung der Compare-Werte

Als Taktgeber habe ich den internen 20 MHz Oszillator gewählt. Der Counter-Prescaler ist 4, der Synchronisierungs-Prescaler ist 8. Dadurch ist die effektive TCD-Zählerfrequenz = 20 MHz / 32 = 625 kHz.

Die Frequenz des TCD-Zyklus ist:

    \[ f_{\text{TCD\_cycle}} = \frac{f_{\text{TCD\_cnt}}}{(\text{CMPACLR} +1 + \text{CMPBCLR}+1)} = \frac{625000}{6000} = ~104 [\text{Hz}] \]

Den Duty-Cycle (DC) berechne ich am Beispiel des WOB-Ausganges:

    \[ \text{DC}_{\text{WOB}} = \frac{ (\text{CMPBCLR} + 1) - (\text{CMPBSET} + 1)}{(\text{CMPACLR} +1 + \text{CMPBCLR}+1)} = \frac{1000}{6000} \stackrel{\wedge}= ~16.7 [\%] \]

Die Berechnungen sind natürlich spezifisch für den Two Ramp Modus!

Jetzt könnt ihr mal verschiedene Modi und Einstellungen für CMPxCLR und CMPxSET ausprobieren. Die Wirkung auf die Ausgabe an WOA und WOB lässt sich mit dem Oszilloskop gut verfolgen. Aber da nicht jeder ein Oszilloskop hat, gibt es jetzt noch die Slow Motion Variante.

Slow Motion Variante

Selbst wenn wir den Systemtakt als Taktgeber nehmen, den Systemtakt auf 1 MHz reduzieren und die Prescaler für den Zähler und die Synchronisierung auf das Maximum setzen (32 bzw. 8), ist die Periode eines TCD-Zyklus im One Ramp Mode maximal (32 * 8 * 4096) / 1 MHz = ~1.05 Sekunden. Das ist ein wenig kurz, um die TCD-Phasen optisch mit LEDs zu verfolgen.

Deshalb „bauen“ wir uns mithilfe des Timer A einen externen Taktgeber an WO1 (PB1 auf meinem ATtiny14), den wir mit dem Eingang für externe Taktgeber (EXTCLK = PA3) verbinden.

Dann hängen wir noch einen Taster an PA1, den brauchen wir aber erst später.

Hier der Aufbau:

Schaltung für die (meisten) Beispielsketche
Schaltung für die (meisten) Beispielsketche

Und hier der Sketch:

void setup(){
    setupExternalClock();
 
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;

    /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm; 

    /* Change Output Polarity */
    // TCD0.CTRLC = TCD_CMPOVR_bm; 
    // TCD0.CTRLD = TCD_CMPBVAL_2_bm | TCD_CMPAVAL_0_bm;    

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_TWORAMP_gc;
    TCD0.CMPASET = 499;  //DTA = ~8.3 %
    TCD0.CMPACLR = 1999; //OTA = 25 %
    TCD0.CMPBSET = 2999; //DTB = 50 %
    TCD0.CMPBCLR = 3999; //OTB = ~16.7 % 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_EXTCLK_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
}

void loop(){} 

void setupExternalClock(){
    /* Crate a slow external clock for TCD0 using TCA0 */
    PORTB.DIRSET = PIN1_bm;  // PB1 = WO1 (TCA0) for many tinyAVR MCUs, please adjust
    //PORTA.DIRCLR = PIN3_bm; // redundant
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; 
    TCA0.SPLIT.LPER = 77; // PWM frequency = 20 MHz/(78 * 256) = ~1 kHz
    TCA0.SPLIT.LCMP1 = 39; // Duty cycle = 39 / (77 + 1) * 100 = 50 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV256_gc | TCA_SPLIT_ENABLE_bm; // split mode, divider 256
    TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP1EN_bm; // enable output LCMP1 (WO1 = PB1 on ATtiny1614)
}

 

Vom vorherigen Sketch unterscheidet sich die Slow Motion Sketch lediglich in der zusätzlichen Funktion setupExternalClock(), die uns ein 1kHz Signal zur Verfügung stellt und durch die Auswahl des externen Taktgebers im Kontrollregister A (Zeile 26). Dank des 1 kHz Signals entspricht ein Timer Count einer Millisekunde. Der TCD-Zyklus hat eine Periode von 6 Sekunden, die beiden LEDs leuchten 1.5 bzw. 1 Sekunde. Nun könnt ihr selbst mit den Waveform Generation Modi und den anderen Einstellungen herumspielen.

Eine weitere Anregung

Wenn ihr Lust habt, entkommentiert die Zeilen 16 und 17 → HIGH und LOW werden dadurch auf der jeweiligen Rampe getauscht. Oder ihr setzt CMPAVAL_1 und CMPAVAL_3 wodurch ihr die Ausgaben an WOA und WOB an WOA vereint.

Inputmode Beispiele

Auch beim Austesten der Inputmodi kommt uns die Verlangsamung über den externen Taktgeber entgegen, denn die Darstellung der Inputmodi ist selbst am Oszilloskop eine gewisse Herausforderung.

void setup(){
    setupExternalClock();
    
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;
    
    setupPA1Event();

   /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm; // | TCD_CMPA_bm | TCD_CMPB_bm;  

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_FOURRAMP_gc;
    TCD0.CMPASET = 499;  //DTA = 0.5s
    TCD0.CMPACLR = 1999; //OTA = 2s
    TCD0.CMPBSET = 1499; //DTB = 1.5s
    TCD0.CMPBCLR = 2999; //OTB = 3s 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_EXTCLK_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
}

void loop(){} 

void setupExternalClock(){
    /* Create a slow external clock for TCD0 using TCA0 */
    PORTB.DIRSET = PIN1_bm;  // PB1 = WO1 (TCA0) for many tinyAVR MCUs, please adjust
    //PORTA.DIRCLR = PIN3_bm; // redundant
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; 
    TCA0.SPLIT.LPER = 77; // PWM frequency = 20 MHz/(78 * 256) = ~1 kHz
    TCA0.SPLIT.LCMP1 = 39; // Duty cycle = 39 / (77 + 1) * 100 = 50 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV256_gc | TCA_SPLIT_ENABLE_bm; // split mode, divider 256
    TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP1EN_bm; // enable output LCMP1 (WO1 = PB1 on ATtiny1614)
}

void setupPA1Event(){
    /* Event setup: Event = PA1->LOW */
    PORTA.PIN1CTRL |= PORT_PULLUPEN_bm; // A1 pullup
    PORTMUX.CTRLA |= PORTMUX_EVOUT0_bm; // Event output needs to be enabled
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is async. event generator for channel 0
    EVSYS.ASYNCUSER6 = EVSYS_ASYNCUSER6_ASYNCCH0_gc;  // ASYNCUSER6 = TCD0_EV0 (Input A) = channel 0 user 

    /* Event Control TCD0 */
    TCD0.EVCTRLA = TCD_CFG_ASYNC_gc | TCD_EDGE_FALL_LOW_gc | TCD_ACTION_FAULT_gc | TCD_TRIGEI_bm;
    TCD0.INPUTCTRLA = TCD_INPUTMODE_WAIT_gc;   
}

 

Der Sketch baut auf dem vorherigen Beispiel wave_form_generation_slow.ino auf. Folgendes kommt hinzu bzw. wurde modifiziert:

  • Es kommt der Four Ramp Mode zum Einsatz.
  • Die Funktion setupPA1Event() definiert den Pin A1 als asynchronen Eventgenerator und TCD0 als Event-User. Für „Event-Nachhilfe“ schaut hier. Was das Event bewirkt, wird in den TCD0-Registern EVTCTRLA und INPUTCTRLA definiert:
    • TCD_CFG_ASYNC_gc: das Event wirkt sofort auf die Ausgabe.
    • TCD_EDGE_FALL_LOW_gc: Eventbedingung ist die fallende Flanke bzw. der LOW-Zustand.
    • TCD_ACTION_FAULT_gc: das Event wirkt auf die Ausgabe, kein Capture.
    • TCD_TRIGEI_bm: Trigger Event Input Enable.
    • TCD_INPUTMODE_WAIT_gc: Inputmode WAIT.

Drückt den Taster und schaut, was passiert. Viel Spaß beim Spielen mit den verschiedenen Inputmodi! In Zeile 15 könntet ihr auch einmal die Bits CMPA bzw. CMPB setzen. Solange ihr dann den Taster drückt, leuchten die LEDs.

Änderungen der Timer D Einstellungen im laufenden Betrieb

Bisher haben wir die Einstellungen am Timer D nur einmalig im setup() vorgenommen. Wenn wir die Einstellungen „im laufenden Betrieb“ vornehmen wollen, müssen wir wieder ein paar mehr Dinge beachten als bei den Timern A und B. Als Beispiel dimmen wir zwei LEDs in maximaler Auflösung (also mit 4096 veschiedenen Duty-Cycles) und nutzen dabei den Dual Slope Modus. Jeder Duty-Cycle soll nur für einen TCD Zyklus zur Anwendung kommen und dann erhöht bzw. reduziert werden. Dafür möchte ich zwei Varianten vorstellen.

LED Dimmer – Variante 1

Hier zunächst der Sketch:

void setup(){
    /* Disable Timer D */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;

    /* Port Init */
    PORTA.DIR |= PIN4_bm | PIN5_bm;  
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc;    
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm;    
    
    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_DS_gc;
    TCD0.CMPBCLR = 4095; // TCD cycle freq: 20 MHZ / (2 * 4096) = ~2.441 kHz
}

void loop(){
    for(int i=0; i<=4095; i++){ // maximum resolution
        TCD0.CMPASET = i;
        TCD0.CMPBSET = i;  
        TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
        while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;} // Check Readiness
        TCD0.CTRLE = TCD_DISEOC_bm; // disable at end of TCD Cycle
        while(!(TCD0.STATUS & TCD_ENRDY_bm)){;} // Wait for end of Cycle
    }
    for(int i=4094; i>0; i--){ 
        TCD0.CMPASET = i;
        TCD0.CMPBSET = i; 
        TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
        while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;} // Check Readiness
        TCD0.CTRLE = TCD_DISEOC_bm; // disable at end of TCD Cycle
        while(!(TCD0.STATUS & TCD_ENRDY_bm)){;} // Wait for end of cycle
    }
} 

 

Die Frequenz der TCD-Zyklen ist unter diesen Bedingungen 20 MHz / (2 * 4096) = ~2.441 kHz. Einmal hoch- oder herunterdimmen passiert in 4096 Schritten, also in ca. 4096 / 2441 = ~1.678 Sekunden. Ich habe mit der Stoppuhr die Zeit für 10 vollständige Dimm-Zyklen genommen und das Ergebnis stimmte gut überein.

Worum es hier aber eigentlich geht, ist die Timereinstellung. Zunächst ist der Timer D disabled. Wir legen CMPASET und CMPSET fest und starten den Timer. Um zu erreichen, dass der Timer D nur einen Zyklus durchläuft, setzen wir das DISEOC-Bit (Disable at End of Cycle). Bevor wir das tun, überprüfen wir mit TCD0.STATUS & TCD_CMDRDY_bm, ob CTRLE dazu bereit ist. Das Ende des TCD-Zyklus stellen wir durch Prüfung des ENRDY-Bits fest. Wenn es gesetzt ist, ist der Zyklus beendet und wir starten von Neuem.

Der Sketch ist komplett blockierend, weil wir den Status des ENRDY-Bits permanent abfragen, um keine Zeit zwischen den Zyklen zu verlieren. Das ist nicht besonders schön, aber es ging mir vor allem darum, das Prinzip zu verdeutlichen. 

LED-Dimmer – Variante 2

Alternativ können wir uns über das Ende des TCD-Zyklus auch durch den TCD-Overflow-Interrupt informieren lassen. Genau das macht der folgende Sketch:

volatile bool endOfCycle = false;

 ISR(TCD0_OVF_vect){  // ISR for Timer D overflow
    TCD0.INTFLAGS = TCD_OVF_bm; // Clear the interrupt flag (needed!)
    endOfCycle = true;   
}
 
 void setup(){
    /* Disable Timer D */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;

    /* Port Init */
    PORTA.DIR |= PIN4_bm | PIN5_bm;  
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc;    
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm;    
    
    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_DS_gc;
    TCD0.CMPBCLR = 4095;
    TCD0.CTRLC = TCD_AUPDATE_bm; // automatic sync 
    TCD0.INTCTRL = TCD_OVF_bm;
        
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_20MHZ_gc | TCD_CNTPRES_DIV1_gc | TCD_ENABLE_bm; 
}

void loop(){
    static bool ascending = true;
    static int cmpValue = 0; 
    if(endOfCycle){
        if(ascending){
            if(cmpValue < 4095){
                cmpValue++;
            }
            else{
                cmpValue--;
                ascending = false;
            }
        }
        else{
            if(cmpValue > 0){
                cmpValue--;
            }
            else{
                cmpValue++;
                ascending = true;
            }
        }
        TCD0.CMPASET = cmpValue;
        TCD0.CMPBSET = cmpValue;  
        TCD0.CMPBCLR = 4095; // needed to initiate synchronisation
        // while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;}
        // TCD0.CTRLE = TCD_SYNC_bm;
        endOfCycle = false;
    }
} 

 

Wenn das Ende des Zyklus erreicht ist, werden die nächsten CMPASET und CMPBSET-Werte berechnet. Das übernimmt die verschachtelte if(ascending){...} Konstruktion. Durch das Setzen des AUPDATE-Bits (Zeile 23) wird am Ende des Zyklus automatisch eine Synchronisierung angefordert. Dann kommt ein entscheidender Punkt: Damit die Synchronisierung tatsächlich ausgelöst wird, muss CMPBCLRH (also das High Byte von CMPBCLR) beschrieben werden. Deshalb schreiben wir noch einmal die 4095 in CMPBCLR (Zeile 54), obwohl wir den Wert ja nicht verändern. Das war im vorherigen Beispiel nicht notwendig, da wir den Timer für jeden TCD-Zyklus aktviert (enabled) haben und dadurch die CMPxSET-Werte dabei übernommen wurden.

Anstelle des SYNC-Bits könntet ihr auch das SYNCEOC-Bit setzen. Kleine Pedanterie: dann müsstet ihr es aber auch vor der Timer D Aktivierung (Zeile 26/27) einmal setzen, denn sonst gibt es im ersten TCD-Zyklus kein Update.

Alternativ könnt ihr die Zeilen 23 und 54 auskommentieren und die Zeilen 55 und 56 entkommentieren. Dadurch fordert ihr die Synchronisation „manuell“ an. Dabei dürft ihr nicht vergessen, zuvor das CMDRDY-Bit zu prüfen. 

Dieser Sketch ist nicht blockierend. Wenn ihr allerdings loop() um zeitaufwendige Tätigkeiten erweitert, könnte das den Dimm-Zyklus verlängern.

Input Capture

Ihr könnt den Timer D Counter nur indirekt über die Register CAPTUREA oder CAPTUREB auslesen. Die Übernahme des Zählerstandes in das Capture-Register erfolgt über Events oder softwaregesteuert durch Setzen des SCAPTUREA- oder SCAPTUREB-Bits.

Capture – eventgesteuert

Im ersten Beispiel schauen wir uns die eventgesteuerte Variante an. Als Grundlage dient der Sketch input_mode_testing.ino, wobei ich Folgendes geändert bzw. ergänzt habe: 

  • Das durch den Tasterdruck eingehende Event löst den Trigger A Interrupt aus. Die Einstellung erfolgt im Interrupt-Kontrollregister. Die ISR setzt die Bool Variable trigEvent auf true und das können wir dann in loop() prüfen.
  • Im Event-Kontrollregister wählen wir für das ACTION-Bit die Einstellung TCD_ACTION_CAPTURE_gc,  d. h. das eingehende Eventsignal löst neben dem Trigger Interrupt auch einen Capture und einen Fault aus. 
    • Mit dem Inputmode NONE hat der Fault allerdings keine weitere Auswirkung.
    • Der Capture notiert uns den Zählerstand beim Tasterdruck.
  • Als Waveform habe ich den Four Ramp Modus ausgewählt. DTA und DTB sind auf das Minimum reduziert (4 Millisekunden). OTA und OTB sind auf 8 Sekunden eingestellt.

Auch hier kommt wieder – rein zu Schulungs- bzw. Darstellungszwecken – der externe Taktgeber zum Einsatz. Ihr müsst nur setUpExternalClock() entfernen und den Taktgeber in Zeile 35 ändern, um wieder schneller zu werden.

volatile bool trigEvent = false;

ISR(TCD0_TRIG_vect){  // ISR for Timer D Trigger
    TCD0.INTFLAGS = TCD_TRIGA_bm; // Clear the interrupt flag (needed!)
    trigEvent = true; 
}
 
 void setup(){
    Serial.begin(115200);
    setupExternalClock();
    
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;
    
    setupPA1Event();

   /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm;  

    /* Enable Trigger A Interrupt */
    TCD0.INTCTRL = TCD_TRIGA_bm;

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_FOURRAMP_gc;
    TCD0.CMPASET = 0;  //DTA = 4 ms
    TCD0.CMPACLR = 1999; //OTA = 8 s
    TCD0.CMPBSET = 0; //DTB = 4 ms
    TCD0.CMPBCLR = 1999; //OTB = 8 s 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_EXTCLK_gc | TCD_CNTPRES_DIV4_gc | TCD_ENABLE_bm; 
}

void loop(){
    if(trigEvent){
        Serial.println(TCD0.CAPTUREA);
        trigEvent = false;
    }
} 

void setupExternalClock(){
    /* Create a slow external clock for TCD0 using TCA0 */
    PORTB.DIRSET = PIN1_bm;  // PB1 = WO1 (TCA0) for many tinyAVR MCUs, please adjust
    //PORTA.DIRCLR = PIN3_bm; // redundant
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; 
    TCA0.SPLIT.LPER = 77; // PWM frequency = 20 MHz/(78 * 256) = ~1 kHz
    TCA0.SPLIT.LCMP1 = 39; // Duty Cycle = 39 / (77 + 1) * 100 = 50 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV256_gc | TCA_SPLIT_ENABLE_bm; // split mode, divider 256
    TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP1EN_bm; // enable output LCMP1 (WO1 = PB1 on ATtiny1614)
}

void setupPA1Event(){
    /* Event setup: Event = PA1->LOW */
    PORTA.PIN1CTRL |= PORT_PULLUPEN_bm; // A1 pullup
    PORTMUX.CTRLA |= PORTMUX_EVOUT0_bm; // Event output needs to be enabled
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is async. event generator for channel 0
    EVSYS.ASYNCUSER6 = EVSYS_ASYNCUSER6_ASYNCCH0_gc;  // ASYNCUSER6 = TCD0_EV0 (Input A) = channel 0 user 

    /* Event Control TCD0 */
    TCD0.EVCTRLA = TCD_CFG_ASYNC_gc | TCD_EDGE_FALL_LOW_gc | TCD_ACTION_CAPTURE_gc | TCD_TRIGEI_bm;
    TCD0.INPUTCTRLA = TCD_INPUTMODE_NONE_gc;   
}

 

Ihr seht also, wie die zwei LEDs abwechselnd 8 Sekunden lang leuchten. Bei einem Druck auf den Taster seht ihr den aktuellen Zählerstand. Das ist alles. Nicht sonderlich spannend, aber macht das Prinzip hoffentlich klar. 

Software Capture

Bei der softwaregesteuerten Variante lassen wir die Eventsteuerung mittels Taster weg. Entsprechend gibt es auch keinen Trigger-Interrupt. Die beiden LEDs leuchten unverändert im Four Ramp Modus jeweils acht Sekunden. Den Zählerstand lassen wir uns alle zwei Sekunden ausgeben.

Dazu setzen wir das SCAPTUREB-Bit im Kontrollregister E. Zuvor prüfen wir, ob das CMDRY-Bit gesetzt ist, d. h. ob das Kontrollregister E Kommandos entgegennehmen kann.

Hier der Sketch:

void setup(){
    Serial.begin(115200);
    setupExternalClock();
    
    /* Disable Timer D, necessary if enabled before */
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = 0;
    
    /* Port Init */
    PORTA.DIR = PIN4_bm | PIN5_bm;  // WOA / WOB (ATtiny1614)
    
    /* Enable Output Channels */
    CPU_CCP = CCP_IOREG_gc; // enable write protected register  
    TCD0.FAULTCTRL = TCD_CMPAEN_bm | TCD_CMPBEN_bm;  

    /* TCD0 Init */
    TCD0.CTRLB = TCD_WGMODE_FOURRAMP_gc;
    TCD0.CMPASET = 0;  //DTA = 4 ms
    TCD0.CMPACLR = 1999; //OTA = 8 s
    TCD0.CMPBSET = 0; //DTB = 4 ms
    TCD0.CMPBCLR = 1999; //OTB = 8 s 
    while(!(TCD0.STATUS & TCD_ENRDY_bm)){;}
    TCD0.CTRLA = TCD_CLKSEL_EXTCLK_gc | TCD_CNTPRES_DIV4_gc | TCD_ENABLE_bm; 
}

void loop(){
    while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;} // check readiness
    TCD0.CTRLE = TCD_SCAPTUREB_bm; 
    while(!(TCD0.STATUS & TCD_CMDRDY_bm)){;} // wait for synchronisation
    Serial.println(TCD0.CAPTUREB);
    delay(2000);
} 

void setupExternalClock(){
    /* Create a slow external clock for TCD0 using TCA0 */
    PORTB.DIRSET = PIN1_bm;  // PB1 = WO1 (TCA0) for many tinyAVR MCUs, please adjust
    //PORTA.DIRCLR = PIN3_bm; // redundant
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; // needed for the Arduino board package
    TCA0.SPLIT.LPER = 77; // PWM frequency = 20 MHz/(78 * 256) = ~1 kHz
    TCA0.SPLIT.LCMP1 = 39; // Duty cycle = 39 / (77 + 1) * 100 = 50 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV256_gc | TCA_SPLIT_ENABLE_bm; // split mode, divider 256
    TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP1EN_bm; // enable output LCMP1 (WO1 = PB1 on ATtiny1614)
}

 

Wichtig ist die Zeile 29. Sie prüft, ob die Capture-Anweisung schon synchronisiert wurde. Wenn ihr sie auskommentiert, dann werdet ihr sehen, dass ihr keine aktuellen Zählerstände bekommt. Der erste ausgegebene Zählerstand nach dem Umspringen der LED ist so groß, dass er offensichtlich noch zur Leuchtphase der anderen LED gehört. Zwischen dem Anfordern des Zählerstandes und dem tatsächlichen Capture vergeht also eine gewisse Zeit.

Einstellungen von CMPxSET / CMPxCLR kopieren

Wenn ihr die Einstellungen für CMPASET und CMPACLR auf CMPBSET und CMPBCLR (oder andersherum) kopieren wollt, dann setzt ihr einfach das FIFTY-Bit im Kontrollregister C:

TCD0.CTRLC |= TCD_FIFTY_bm;

Die jeweils letzte Einstellung für CMPxSET / CMPxCLR wird dupliziert. Ihr könnt das mit dem Sketch wave_form_generation_slow.ino ausprobieren. Einen eigenen Sketch spare ich mir.

WOC und WOD nutzen

Ob ihr WOC und WOD anstelle von WOA und WOB nutzen könnt, hängt davon ab, ob die Ausgänge tatsächlich als Pin ausgeführt sind. Beim ATtiny1614, den ich für die meisten Beispiele genutzt habe, liegen WOC und WOD auf PC0 und PC1. Diese Pins sind nur bei der 24-Pin VQFN Version vorhanden. Da ich den nicht habe, bin ich stattdessen auf einen ATtiny3216 ausgewichen.

Auch hier könntet ihr den Sketch wave_form_generation_slow.ino entsprechend abändern:

PORTC.DIR = PIN1_bm | PIN0_bm; // WOD / WOC (z.B. ATtiny3216)

/* assign PWMx to WOx */ 
TCD0.CTRLC |= TCD_CMPDSEL_PWMB_gc | TCD_CMPCSEL_PWMA_gc; 

/* Enable Output Channels */ 
CPU_CCP = CCP_IOREG_gc; // enable write protected register 
TCD0.FAULTCTRL = TCD_CMPCEN_bm | TCD_CMPDEN_bm;

2 thoughts on “tinyAVR-Serie Teil 3: Timer D

  1. Super – vielen Dank für Deine tolles Guide – Top!

    Ich habe nur eine Frage zur Benachrichtigungsfunktion unten auf Deiner Seite, bezüglich Checkbox: „Ja, informiere mich über neue Beiträge! Inform me about new posts! You will NOT be notified about new or follow-up comments! “

    By default ist die Checkbox nicht aktiviert. Muss man den Haken beim Absenden von Kommentaren setzen, um weiterhin über Deine neuen Blog-Beiträge informiert zu werden?

    1. Hi, danke! Es gibt zwei Arten der Benachrichtigung. Zum einen für Kommentare und zum anderen für neue Beiträge. Wenn du neue Beiträge abonniert hast, dann sollte es egal sein, ob du den „Beitrags-Haken“ bei den Kommentaren setzt oder nicht. Bei den Komentar-Abos gibt es die Unterkategorien „Alle Kommentare“ oder nur „Nur für den aktuellen Beitrag“. Ich benutze zwei unterschiedliche WordPress-Plugins dafür (falls dir das etwas sagt) und es sind zwei getrennte Datenbanken.

      Aus den Beitragsbenachrichtigungen fliegst du nach einem Jahr raus, wenn du nie auf die Links der Benachrichtigungen klickst. Ohne diese Maßnahme würde ich irgendwann mehr Karteileichen als aktive Abonnenten haben. Mit der E-Mail-Adresse, die du für diesen Kommentar hier verwendet hast, sehe ich dich nur bei den Kommentar-Abonnenten, Kaegorie „Alle“.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert