tinyAVR-Serie Teil 2: Timer A/B, CCL, SLPCTRL

Über den Beitrag

Das ist der zweite Teil meines Beitrages über die tinyAVR Serie 0, 1 und 2. Im ersten Teil hatte ich erklärt, wie die Registerprogrammierung für die tinyAVR Serie grundsätzlich funktioniert und bin auf einige Module eingegangen. In dieser Fortsetzung geht es um die Timer A und B, die Configurable Custom Logic (CCL) und das Sleep Control Modul (SLPCTRL). Es fehlt noch der Timer D, der bei der tinyAVR Serie 1 zum Einsatz kommt. Mit diesem beschäftige ich mich (bzw. euch 🙂 ) im dritten Teil.

Hier der Überblick und die Links zu allen Themen:

Timer A – Modul TCAn

Zunächst ein paar allgemeine Anmerkungen zum Timer A:

  1. Alle Vertreter der tinyAVR Serien besitzen genau einen Timer A (TCA0).
  2. TCA0 kann als 16-Bit-Timer betrieben werden (Single Mode) oder in zwei 8-Bit-Timer geteilt werden (Split Mode).
  3. TCA0 ist an den Systemtakt gekoppelt, er kann aber im Register TCA0.SINGLE.CTRLA bzw. TCA.SPLIT.CTRLA mit einem Teiler (Prescaler) verlangsamt werden.
  4. In der Standardeinstellung des megaTinyCore Boardpaketes ist der TCA0 Prescaler auf 64 festgelegt. Überdies befindet sich der Timer im Split Modus, um damit das Maximum an PWM-Pins herausholen zu können. Änderungen an den Einstellungen können sich entsprechend auf die PWM-Funktion auswirken. Hinzu kommt, dass die Vertreter der tinyAVR 0-Serie TCA0 standardmäßig für millis(), micros() und delay() nutzen. Änderungen des Prescalers machen diese Funktionen damit unbrauchbar. Ihr könnt aber millis() / micros() anderen Timern zuordnen oder auch deaktivieren (Arduino IDE → Werkzeuge).
  5. TCA0 zählt im normalen (Single) Modus bis zum Maximalwert PER. Es stehen drei Compare-Channels zur Verfügung. Die Compare-Werte heißen CMP0, CMP1 und CMP2.
  6. Im geteilten Modus (Split Modus) werden aus dem 16-Bit Timer zwei 8-Bit Timer mit je 3 Compare-Werten. Daraus lassen sich 6 PWM-Ausgänge erzeugen. Die PWM-Frequenz ist für je drei Ausgänge identisch.
  7. Die Register für den normalen Modus sprecht ihr mit TCA0.SINGLE.Registername an, die Register für den Split Modus mit TCA0.SPLIT.Registername.

Das ist eine ultrakurze Zusammenfassung. Ich empfehle euch die Dokumentation von Spence Konde zum Thema Timer und PWM.

Timer A Register für den normalen (Single) Modus

Einige Register sind im normalen und Split Modus identisch. Das habe ich dann entsprechend vermerkt.

Dieser Beitrag ist keine vollständige Registerdokumentation, sondern eher eine Starthilfe. Schaut am besten parallel in das Datenblatt.

Control A Register TCAn.CTRLA

Im Control Register CTRLA stellt ihr den Takt bzw. den Teiler ein und aktiviert den Timer Counter A.

tinyAVR - TCAn.CTRLA Register
TCAn.CTRLA Register

Die Bitgruppe CLKSEL (Clock Select) ist für den Teiler zuständig. Die folgenden Bit Group Configuration Masks stehen zur Auswahl:

tinyAVR - CLKSEL Optionen für TCA0
CLKSEL – Optionen

Mit dem ENABLE Bit aktiviert ihr den Timer TCA0. Ihr könnt ja mal die Einstellung von CTRLA mit Serial.println(TCA0.SPLIT.CTRLA, BIN) prüfen. Das Ergebnis sollte 0b1011 sein, d. h. der Timer ist aktiviert und der Teiler ist 64. Ihr braucht den Timer also nicht selbst zu aktivieren. Und noch einmal: Bei Verwendung des megaTinyCore Paketes müsst ihr bei Änderung der TCA0-Einstellungen die Nebenwirkungen beachten.

Das RUNSTDBY Bit gibt es nur für die tinyAVR Serie 2. Wenn ihr es setzt, dann schickt ihr damit den Timer/Counter A in den Standby-Modus.

Control B Register TCAn.CTRLB

TCAn.CTRLB Register
TCAn.CTRLB Register

Mit den Compare-n-Enable-Bits (CMPnEN) aktiviert ihr die Ausgänge der Compare Channel. Wo sich die Ausgänge befinden, findet ihr im Datenblatt im Abschnitt „I/O Multiplexing and Considerations“. Die Ausgänge heißen WOx und korrespondieren mit CMPx. Zum Teil lassen sich die Ausgänge alternativen Pins zuordnen (Modul PORTMUX). Aber nicht alle tinyAVR-Vertreter haben alle Ausgänge (und die Alternativen) tatsächlich als Pin ausgeführt.

Auf das Auto Lock Update Bit (ALUPD) gehe ich hier nicht ein. Die Bitgruppe WGMODE[2:0] legt den Wave Form Generation Modus fest:

tinyAVR Serie: WGMODE Optionen für TCA0 im Normal Mode
Wave Form Generation Mode Optionen

Control D Register TCAn.CTRLD

Im Control D Register stellt ihr den Split Modus ein. Dabei ist es egal, ob ihr die Einstellung über TCA0.SINGLE.CTRLD oder TCA0.SPLIT.CTRLD vornehmt.

tinyAVR Register: TCAn.SINGLE.CTRLD
Register TCAn.SINGLE.CTRLD / TCAn.SPLIT.CTRLD

Interrupt Control Register TCAn.INTCTRL

Im Interrupt Control Register legt ihr fest, welche Interrupts aktiviert werden sollen. Es stehen Interrupts für die Compare Matches und den Overflow zur Verfügung:

tinyAVR Register: TCAn.SINGLE.INTCTRL / TCAn.SINGLE.INTFLAGS
TCAn.SINGLE.INTCTRL / TCAn.SINGLE.INTFLAGS

Zum Anzeigen der Interrupts gibt es das „baugleiche“ Interrupt Flagregister INTFLAGS, das dieselben Bitnamen verwendet.

Event Control Register TCAn.EVCTRL

Ihr könnt den Timer A so einstellen, dass er nicht über den Systemtakt, sondern externe Events gesteuert wird. Dazu setzt ihr das „Enable Count On Event Input“ Bit CNTEI im Event Control Register EVCTRL.

tinyAVR Register: TCAn.SINGLE.EVCTRL
TCAn.SINGLE.EVCTRL

Mithilfe der Bit Group Configuration EVACT bestimmt ihr, welche Events gezählt werden, bzw. wie gezählt wird:

EVACT Bit Group Configurations
EVACT Bit Group Configurations

Mit den ersten beiden Einstellungen zählt ihr die Flanken, d.h. die Anzahl der Events. Mit den beiden anderen Einstellungen zählt ihr so lange über den Systemtakt (ggf. mit Divider), wie der durch das Event verursachte Zustand anhält.

Weitere Register

Dann gibt es noch eine Reihe weitere Register, die lediglich Zahlen enthalten:

  • CNT (16 Bit): ist das Counter-Register zum Timer A.
  • PER (16 Bit): ist der TOP-Wert für die meisten Wave Form Generation Modes.
  • CMP0, CMP1, CMP2 (16 Bit): enthalten die Compare-Werte.
  • PERBUF: ihr könnt PER direkt in das PER-Register schreiben. Unter Umständen ist aber eine sofortige Änderung von PER (z. B. inmitten einer PWM-Periode) unerwünscht. Schreibt ihr PER in PERBUF, so wird das neue PER erst bei Erreichen des alten PER übernommen.
  • CMP0BUF, CMP1BUF, CMP2BUF: siehe PERBUF.

Im normalen (Single) Modus hat der Timer/Counter A eine Größe von 16 Bit. Er lässt sich aber auch in zwei 8-Bit-Timer teilen (Split Modus). Im normalen Modus stehen drei Compare Channels zur Verfügung, im Split Modus sind es sechs. Entsprechend lassen sich 3 bzw. 6 PWM-Ausgänge mithilfe des Timer A realisieren (sofern die Output-Pins vorhanden sind).

Beispielsketche für den Timer A im normalen Modus

Nach der ganzen Theorie gibt es endlich wieder Beispielsketche.

Overflow Interrupt

Wir beginnen ganz einfach mit einem Overflow Interrupt. Den nutzen wir, um eine LED an PA2 zu toggeln.

ISR(TCA0_OVF_vect){  // ISR for Timer A overflow
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // Clear the interrupt flag (needed!)
    PORTA.OUTTGL = PIN2_bm; // Toggle PA2 
}

void setup() {
    PORTA.DIRSET = PIN2_bm; // PA2 auf OUTPUT
    TCA0.SINGLE.CTRLD = 0; // change to Single Mode
    TCA0.SINGLE.PERBUF = 62499; // Set TOP for Timer A 
    // TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV64_gc | TCA_SINGLE_ENABLE_bm; // redundant
    TCA0.SINGLE.CTRLB = 0; // no output, wave form: normal;
    TCA0.SINGLE.INTCTRL = TCA_SINGLE_OVF_bm; // enable Timer A overflow interrupt
}

void loop() {}

Bei einer Taktrate von 20 MHz, einem Teiler von 64 und einem PER von 62499 (= 62500 Schritte) ist die Überlauffrequenz 20000000 / (64 * 62500) = 5 Hz.

Single Slope PWM

Im nächsten Beispiel erzeugen wir ein PWM-Signal mit einer Frequenz von 250 Hz und einem Duty Cycle von 20 % an WO2 (Ausgang für CMP2). Welchem Pin WO2 zugeordnet ist, erfahrt ihr im Datenblatt. Die 250 Hz erreichen wir, indem wir PER auf 1249 setzen (bei 20 MHz). Der Duty Cycle ist CMPn / (PER +1).

void setup() {
    PORTB.DIRSET = PIN2_bm;  // PB2 = WO2 for many tinyAVR MCUs, please adjust if necessary!
    TCA0.SINGLE.CTRLD = 0; 
    // TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV64_gc | TCA_SINGLE_ENABLE_bm; // redundant
    TCA0.SINGLE.PERBUF = 1249;
    TCA0.SINGLE.CMP2BUF = 250;
    TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
}

void loop() {}

Auf einem ATtiny202/402 ist WO2 dem Pin PA2 zugeordnet (nur mal so als Beispiel). Entsprechend müsstet ihr im Beispiel PORTB durch PORTA ersetzen.

Frequency Modus

Im Frequency Modus zählt der TCA0 Counter bis CMP0 anstelle von PER. Bei CMP0 angekommen, wird der CMP0 Ausgang (WO0) getoggelt. Damit ist der Duty Cycle in diesem Modus 50 % festgelegt. Hier ein kleines Beispiel:

void setup() {
    PORTB.DIRSET = PIN0_bm;  // PB0 = WO0 for many tinyAVR MCUs, please adjust if necessary!
    TCA0.SINGLE.CTRLD = 0; 
    TCA0.SINGLE.CMP0 = 1249;
    TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_WGMODE_FRQ_gc;
}

void loop() {}

Bei 20 MHz ist die Toggle-Frequenz 250 Hz, d.h. die PWM-Frequenz beträgt 125 Hz.

Dual Slope Modi

In den Dual Slope Modi zählt TCA0.CNT von 0 bis PER hoch und dann wieder hinunter bis 0. Im Falle eines Compare Matches beim Hochzählen geht der Ausgang CMPx auf LOW und beim Hinunterzählen auf HIGH. Dadurch ist die PWM-Frequenz ggü. dem Single Slope Modus halbiert.

void setup() {
    PORTB.DIRSET = PIN0_bm;  // PB0 = WO0 for many tinyAVR MCUs, please adjust if necessary!
    TCA0.SINGLE.CTRLD = 0; 
    // TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV64_gc | TCA_SINGLE_ENABLE_bm; // redundant
    TCA0.SINGLE.PERBUF = 1249;
    TCA0.SINGLE.CMP0BUF = 250;
    TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_WGMODE_DSBOTTOM_gc;
}

void loop() {}

Die WG Modes DSTOP, DSBOTTOM  und DSBOTH unterscheiden sich lediglich hinsichtlich ihres Overflow-Verhaltens. Im Falle von DSTOP wird der Overflow bei PER erreicht, für DSBOTTOM ist es 0 und für DSBOTH bei 0 und PER. Das PWM-Signal ist bei allen Dual Slope Modi identisch, sofern alle anderen Parameter auch identisch sind.

Events zählen

In diesem Beispiel steuern wir den Timer A über Events. Ich habe den Sketch für den ATtiny1614 geschrieben. Da sich die Event-Module der verschiedenen tinyAVR Serien recht stark voneinander unterscheiden (siehe Teil 1 des Beitrages), sind für den tinyAVR Vertreter eurer Wahl ggf. einige Anpassungen notwendig (gute Übung!).

Als Event dient hier eine steigende Flanke an PB1. Dazu verbindet ihr PB1 über einen Taster mit GND. PB1 zieht ihr über den internen Pull-up-Widerstand auf HIGH Niveau. Das Event wird also beim Loslassen des Tasters erzeugt. Ihr könnt die Logik auch umdrehen, braucht dann aber noch einen externen Pull-Down Widerstand.

Der Sketch informiert uns, wenn der Taster 10 bzw. 20 Male gedrückt wurde:

volatile bool tca0Ovf = false;  // Flag for TCA OVF
volatile bool cmp0Match = false;  // Flag for CMP0 Match

ISR(TCA0_OVF_vect){  // TCA0 Overflow ISR
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; /* Clear the interrupt flag */
    tca0Ovf = true;
}

ISR(TCA0_CMP0_vect){  // TCA0/CMP0 compare match ISR
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_CMP0_bm; /* Clear the interrupt flag */
    cmp0Match = true;
}

void setup() {
    Serial.begin(115200);
    PORTB.DIRCLR = PIN1_bm; // PB1 as input 
    PORTB.PIN1CTRL = PORT_PULLUPEN_bm; // Pullup enable
    EVSYS.SYNCCH1 = EVSYS_SYNCCH1_PORTB_PIN1_gc; // PB1 is sync. event generator
    EVSYS.SYNCUSER0 = EVSYS_SYNCUSER0_SYNCCH1_gc;  // TCA0 is sync. event user
    TCA0.SINGLE.CTRLD = 0; // set single mode
    TCA0.SINGLE.CTRLA = 0;  // switch off the timer, set clock divider to DIV1 
    TCA0.SINGLE.PER = 19;  // TCA0 counter limit
    TCA0.SINGLE.CMP0 = 9;  // Compare match value
    TCA0.SINGLE.CNT = 0;  // Set TCA0 counter to 0
    TCA0.SINGLE.EVCTRL = TCA_SINGLE_EVACT_POSEDGE_gc | TCA_SINGLE_CNTEI_bm; // count pos. edge of events (RISING) & enable event count
    TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc; // normal mode => TOP = PER
    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm; // clear the overflow interrupt flag
    TCA0.SINGLE.INTCTRL = TCA_SINGLE_CMP0_bm | TCA_SINGLE_OVF_bm; // enable CMP0 and OVF interrupts 
    TCA0.SINGLE.CTRLA = TCA_SINGLE_ENABLE_bm;
}

void loop(){
    if(cmp0Match){
        Serial.println("Key has been pressed 10 times");
        cmp0Match = false;
    }
    
    if(tca0Ovf){
        Serial.println("Key has been pressed another 10 times");
        Serial.println("Timer/Counter A restarts");
        tca0Ovf = false;
    }
    // Serial.println(TCA0.SINGLE.CNT);  // uncomment to see the TCA0 Counter
    //delay(100); 
}

 

Ich habe versucht, den Code durch Kommentare zu erklären. Ich hoffe, dass das ausreichend verständlich ist. Ein paar Anmerkungen sind aber trotzdem noch notwendig:

  • Damit der Timer A uns während der Einstellungen nicht stört, schalten wir ihn ab.
  • Wir lassen uns per Compare 0 Interrupt und Overflow Interrupt informieren, wenn der Taster zehn bzw. zwanzig Male gedrückt wurde.
    • Die Interrupts werden erst ausgelöst, wenn CMP0 und PER überschritten werden. Deswegen hat CMP0 den Wert 9 und PER ist 19.
  • Das Einstellen des Compare 0 Registers und des PER Registers über die Buffer Register (CMP0BUF, PERBUF) ist hier ungeeignet. Ihr müsstet den Taster so oft drücken, bis ihr das zuvor eingestellte PER erreicht. Erst dann wird das neue PER aus PERBUF übernommen. Bei einem 16-Bit Counter kann das lange dauern!
  • Wegen des Tasterprellens müsst ihr den Taster weniger als zehnmal drücken, um die Limits zu erreichen.

Timer A Register im Split Modus

Im Split Modus wird der Timer A in einen High- und einen Low-Teil geteilt. Entsprechend steht euch anstelle des 16-Bit CNT Registers ein 8-Bit HCNT und ein 8-Bit LCNT Register zur Verfügung. Diese beiden Counter Register zählen im selben Takt, aber unabhängig voneinander. Auch das PER Register und die Compare Register werden geteilt. Aus PER wird HPER und LPER, aus CMPn wird HCMPn und LCMPn usw.

Die Ausgänge für den Timer A im Split Modus aktiviert ihr im Control B Register (TCA0.SPLIT.CTRLB):

tinyAVR Register: TCAn.SPLIT.CTRLB
Register TCAn.SPLIT.CTRLB

Die LCMPnEN Bits sind für die Ausgänge WOn zuständig, die HCMPnEN Bits für die Ausgänge WO(n+2).

Im Control D Register könnt ihr Interrupts für die Low Compare Channels setzen. Für die High Compare Channels stehen keine Interrupts zur Verfügung.

Im Split Modus zählen das HCNT und das LCNT Register ausschließlich abwärts. Entsprechend gibt es keine Overflow-, sondern nur Underflow-Interrupts („UNF“). Die Bits zum Aktivieren der Interrupts sind HUNF und LUNF.

tinyAVR Register: TCAn.SPLIT.CTRLB
Register TCAn.SPLIT.CTRLB

Die zugehörigen Namen der Interruptvektoren bildet ihr nach dem Schema TCA0_Interruptbit_vector, also z. B. TCA0_LCMP1_vector.

Beispielsketch für den Timer A im Split Modus

Dann probieren wir den Timer A im Split Modus einmal aus. Ziel ist es, ein PWM-Signal mit Duty-Cycle von 25 % an WO3 (HCMP0) zu erzeugen. Dafür ist der High Counter des Compare Channel 0 zuständig. WO3 sollte für alle tinyAVR Vertreter auf PA3 liegen. Prüft das im Zweifelsfall aber nach. Ich habe nicht jedes Datenblatt angeschaut!

void setup() {
    PORTA.DIRSET = PIN3_bm;  // PA3 = WO3 for many tinyAVR MCUs, please adjust
    TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; // needed for the Arduino board package
    TCA0.SPLIT.HPER = 255; // PWM frequency = 20 MHz/(64 * 256) = ~1.22 kHz
    TCA0.SPLIT.HCMP0 = 64; // Duty cycle = 64 / (255 + 1) * 100 = 25 %
    TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV64_gc | TCA_SPLIT_ENABLE_bm; // redundant for megaTinyCore
    TCA0.SPLIT.CTRLB = TCA_SPLIT_HCMP0EN_bm; // enable output HCMP0 (WO3)
}

void loop() {}

Timer B – Modul TCBn

Die tinyAVR MCUs besitzen ein oder zwei 16Bit-Timer B (TCB0 / TCB1):

  • 0-Serie: 1 Timer B
  • 1-Serie bis 8 kB Flash: 1 Timer B
  • 1-Serie ab 16 kB Flash: 2 Timer B
  • 2-Serie: 2 Timer B

Auch hier gilt, dass das megaTinyCore Boardpaket den/die Timer B zum Teil für millis(), micros(), delay(), tone() oder die Servo-Funktionen benutzt. Änderungen an den Einstellungen von TCBn können diese Funktionen unbrauchbar machen. Ihr könnt die Funktionen aber z. T. auch anderen Timern zuordnen. Schaut in der Arduino IDE unter dem Menüpunkt Werkzeuge.

An welchen Pins die Ausgänge für die TCBn Compare Matches liegen und welche Alternativen zur Verfügung stehen, erfahrt ihr im Datenblatt unter „I/O Multiplexing and Considerations“. Die Bezeichnung der Ausgänge ist leider nicht konsistent. So findet ihr beispielsweise „TCBn WO“, „n, WO“ oder „WOn“. Die Zuordnung von Ausgängen zu alternativen Pins nehmt ihr über das PORTMUX Modul vor.

Timer B Register

Auch hier möchte ich zunächst noch einmal darauf hinweisen, dass mein Beitrag nicht alle vorhandenen Register abdeckt.

Control A Register TCBn.CTRLA

Die wichtigsten Einstellungen in den Control A Registern der Timer B sind die Taktrate (CLKSEL[2:0]) und ENABLE. Für die Taktrate stehen der Systemtakt, der halbe Systemtakt oder der Takt des Timer A zur Auswahl.

tinyAVR Register: TCBn.CTRLA
Register TCBn.CTRLA

Die Voreinstellung im megaTinyCore Paket für CLKSEL ist CLKTCA, d. h. es kommt der Systemtakt mit einem Teiler von 64 zur Anwendung.

Control B Register TCBn.CTRLB

Im Control B Register legt ihr einen der acht Count Modes fest, die den Timer B zu einem vielseitig einsetzbaren Werkzeug machen. Ich liste die Count Modes hier erst einmal nur auf, denn sie lassen sich am besten anhand der noch folgenden Beispiele erklären.

tinyAVR Register TCBn.CTRLB
Register TCBn.CTRLB
CTNMODE[2:0] Bit Group Configurations
CTNMODE[2:0] Bit Group Configurations

Die anderen Bits bewirken:

  • ASYNC (Asynchronous Enable): erlaubt asynchrone Updates des TCB Output Signals im Single Shot Modus.
  • CCMPINIT (Compare / Capture Pin Initial Value): setzt das anfängliche Level des Output Pins (0 = LOW, 1 = HIGH).
  • CCMPEN (Compare / Capture Output Enable): aktiviert den Compare / Capture Output. Im Gegensatz zum Timer A gibt es nur einen Compare Output pro Timer B.

Interrupt Control Register TCBn.INTCTRL

Das Interrupt Control Register ist sehr übersichtlich. Ihr könnt dort den Capture Interrupt aktivieren. Die Bedingung für die Auslösung des Interrupts hängt vom Count Mode ab. Ich komme darauf in den Beispielen zurück. Eine Übersicht findet ihr im Datenblatt bei der Registerbeschreibung von TCBn.INTFLAGS.

tinyAVR Register: TCBn.INTCTRL
TCBn.INTCTRL / TCBn.INTFLAGS

Der Interruptvektor heißt TCBn_INT_vect und nicht – wie man erwarten würde – TCBn_CAPT_vect. Das ist etwas verwirrend.

Event Control Register TCBn.EVCTRL

Auch die Wirkung des EDGE Bits im Event Control Register EVCTRL hängt vom Count Mode ab. Wartet auf die Beispiele oder schaut euch die Übersicht im Datenblatt in der Registerbeschreibung von TCBn.EVTCTRL an.

Das FILTER Bit aktiviert einen Rauschfilter, der dafür sorgt, dass der Eventzustand vier Takte lang konstant sein muss, bevor das Event als valide gilt.

Das Capture Event Input Enable Bit CAPTEI aktiviert den Event Eingang. Im Gegensatz zum Timer A kann der Timer B keine Flanken (Events) zählen, sondern die Flanke startet oder stoppt den Zähler.

tinyAVR Register: 
TCBn.EVCTRL
TCBn.EVCTRL

Weitere Register

Das 16-Bit Register CNT enthält – wenig überraschend – den Zählerstand des Timer B. Genau genommen handelt es sich um zwei Register, die ihr gemeinsam über TCBn.CNT oder getrennt über TCBn.CNTL und TCBn.CNTH ansprechen könnt.

Das Capture / Compare Register CCMP hat, wie der Name schon vermuten lässt, verschiedene Aufgaben. Als Compare Register steuert es Interrupts und PWM, als Capture Register nimmt es Counterstände als Messwerte auf.

Beispielsketche für die Timer B

Wir werden uns jetzt für jeden Count Mode einen Beispielsketch anschauen. Die Sketche müssen für den tinyAVR Vertreter eurer Wahl ggf. hinsichtlich der Pins und ggf. auch hinsichtlich des Eventsystems angepasst werden.

Periodic Interrupt

Im Periodic Interrupt Mode zählt der Timer bis CCMP, löst den CAPT Interrupt aus (sofern aktiviert) und beginnt wieder von vorn. In dem folgenden Beispielsketch nutzen wir den CAPT Interrupt, um eine LED an einem geeigneten Pin zu toggeln. Da das megaTinyCore Paket den Timer A Takt (Voreinstellung DIV64) verwendet und wir als CCMP den Wert 62500 einstellen, ist die Toggle-Frequenz bei 20 MHz: 20 MHz / (64 * 62500) = 5 Hertz. 

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
    PORTA.OUTTGL = PIN2_bm; /* Toggle PA2 */
}

void setup() {
    PORTA.DIRSET = PIN2_bm;
    PORTA.OUTSET = PIN2_bm;
    // TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc | TCA_SINGLE_ENABLE_bm;
    TCB0.CCMP = 62500; // Toggle frequency = 5 Hz @ 20 MHz
    TCB0.INTCTRL = TCB_CAPT_bm;
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // use TCA clock / enable
}

void loop() {}

Wenn ihr die Zeile 9 entkommentiert, erhöht ihr den Clock Divider von 64 auf 1024, also um den Faktor 16. Die Toggle-Frequenz verringert sich damit auf 0.3125 Hz (→ Periode 3.2 s).  Aber, nicht vergessen: das Ändern des Timer A Taktes hat bei Verwendung des megaTinyCore Paketes ggf. Auswirkungen auf millis() & Co.

Dieser Sketch sollte unverändert auf jedem tinyAVR Mikrocontroller funktionieren.

8 Bit PWM Modus

PWM-Signale sind mithilfe der Timer B sehr einfach darstellbar. Das untere Byte des Compare Registers CCMP, nämlich CCMPL, legt zusammen mit dem Takt die PWM-Frequenz fest. Der Duty Cycle ergibt sich aus dem Verhältnis von CCMPH (also dem oberen Byte) zu CCMPL. Die Register CCMPH und CCMPL könnt ihr getrennt oder zusammen beschreiben.

    \[ DutyCycle = \frac{\text{CCMPH}}{\text{CCMPL + 1}} \cdot 100\;\; [\%] \]

void setup() {
    PORTA.DIRSET = PIN5_bm; // PA5 = TCB0 WO for ATtiny1614, please adjust
    TCB0.CCMP = 0x40FF;
    /* Alternative:
    TCB0.CCMPL = 0xFF; // 255
    TCB0.CCMPH = 0x40; */ // 64
    TCB0.CTRLB |= TCB_CCMPEN_bm | TCB_CNTMODE_PWM8_gc;
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm;
}

void loop(){}

In diesem Beispiel verwenden wir den Timer A Takt. D.h. die Frequenz beträgt 20 MHz / (64 * 256) = ~1.22 kHz. Der Duty Cycle ist 25 %.

Single Shot Modus

Im Single Shot Modus zählt der Timer bis CCMP und stoppt dann, bis er wieder auf einen Wert kleiner als CCMP zurückgesetzt wird. Um den folgenden Sketch auszuprobieren, hängt ihr eine LED an den Ausgang für TCB0 (= PA5 für ATtiny1614).

void setup() {
    PORTA.DIRSET = PIN5_bm; // PA5 = TCB0 WO for ATtiny1614, please adjust
    TCB0.CTRLA = 0; // disable TCB0
    TCB0.CCMP = 62500; // 0.2s signal length
    TCB0.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_SINGLE_gc; // enable output / single shot mode
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // enable TCB0, use TCA clock
}

void loop(){
    delay(1000);
    TCB0.CNT = 0; // restarts the counter
}

Sofern euer ATtiny mit 20 MHz läuft und ihr den Timer A Takt nicht verändert habt, wird die LED im Sekundentakt für 0.2 Sekunden aufleuchten.

Jetzt verbinden wir das noch mit dem Event System. Dazu hängt ihr einen Taster an einen geeigneten Pin. Die andere Seite des Tasters hängt ihr an GND. Ich habe den folgenden Sketch für einen ATtiny1614 geschrieben und als Pin PB1 gewählt. Ein Tasterdruck (Event Generator) soll den Timer TCB0 (Event User) starten.

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    PORTA.DIRSET = PIN5_bm; // PA5 (TCB0 WO / ATtiny1614) as output
    PORTB.DIRCLR = PIN1_bm; // PB1 as input
    PORTB.PIN1CTRL = PORT_PULLUPEN_bm;
    TCB0.CTRLA = 0; // disable TCB0
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 as event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 WO (PA5 on the ATtiny1614)
    TCB0.CCMP = 62500; // 0.2s signal length
    TCB0.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_SINGLE_gc; // single shot mode
    TCB0.EVCTRL = TCB_EDGE_bm | TCB_CAPTEI_bm; // counter starts at both edges(?) / enable capture event input
    TCB0.INTCTRL = TCB_CAPT_bm; // enable interrupt
    TCB0.CNT = 62500; // prevents an unwanted, initial signal at PA5
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // enable TCB0, use TCA clock
}

void loop(){}

Laut Datenblatt, Kapitel 21.5.3, sollte der Timer bei gesetztem EDGE Bit nur bei einer fallenden Flanke (negative Edge), sprich beim Drücken des Tasters und nicht beim Loslassen, erfolgen. Die LED leuchtet aber bei beiden Ereignissen auf. Vielleicht einfach ein Fehler im Datenblatt? Bei nicht gesetztem EDGE Bit macht die LED, was sie soll: sie leuchtet nur beim Loslassen auf (steigende Flanke). Jedenfalls gilt das, sofern euer Taster nicht prellt.

Wenn ihr die Zeile 16 auskommentiert, dann werdet ihr sehen, dass die LED bei jedem MCU Reset kurz aufleuchtet. Das Starten des Timers lässt ihn bei 0 los zählen und die LED leuchtet entsprechend. Setzt ihr den Timer Counter auf CCMP, passiert das nicht. 

Anpassungen für andere tinyAVR MCUs

Wie schon mehrfach erwähnt: die Sketche müssen ggf. angepasst werden, je nach verwendetem ATtiny. Hier ein paar Beispiele:

  • Unverändert läuft der Sketch auf einem ATtiny1604.
  • Auf einem ATtiny402 gibt es beispielsweise keinen PORTB. Mit folgenden Anpassungen funktionierte es:
.....    
PORTA.DIRSET = PIN6_bm; // PA6 (= WO0 / ATtiny402) as output
PORTA.DIRCLR = PIN1_bm; // PA1 as input
.....
EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 as event generator
EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // TCB0 as event user
.....
  • Auf den tinyAVR MCUs der Serie 2 sind größere Anpassungen notwendig (siehe auch Teil 1 des Beitrages). Mit diesen Änderungen funktionierte es auf einem ATtiny3226:
.....
PORTA.DIRSET = PIN5_bm; // PA5 (0,WO0 / ATtiny3226) as output
PORTA.DIRCLR = PIN1_bm; // PA1 as input
.....
EVSYS.CHANNEL0 = EVSYS_CHANNEL0_PORTA_PIN1_gc; // PA1 as event generator
EVSYS.USERTCB0CAPT = EVSYS_USER_CHANNEL0_gc; // TCB0 as event user
.....

Das sollte euch auf die richtige Spur bringen, um die noch folgenden Beispielsketche selbst anzupassen. Ich werde mich auf den ATtiny1614 beschränken.

Input Capture Time-Out Check Mode

Der Time-Out Check Mode wird durch das Eventsystem gesteuert. In diesem Modus startet ein Event-Signal den Timer bei 0, das nächste Signal stoppt ihn. Wenn EDGE gesetzt ist, startet die fallende Flanke den Timer und die steigende Flanke stoppt ihn. Ist EDGE nicht gesetzt, ist es umgekehrt. Erreicht der Timer Counter CCMP, wird ein Interrupt ausgelöst und der Timer Counter läuft weiter. 

Das probieren wir folgendermaßen aus: ein Taster an einem geeigneten Pin (hier: PA1) dient als Event Generator, der TCB0 im Time-Out-Modus startet. Solange der Taster gedrückt ist, zählt TCB0 hoch. CCMP und den Timer Takt stellen wir so ein, dass CCMP nach einer Sekunde erreicht ist. Wenn dieser Fall eintritt, wird ein Interrupt ausgelöst und die ISR toggelt eine LED an einem anderen geeigneten Pin (hier: PA2). Wird der Taster vor Erreichen von CCMP losgelassen, gibt es keinen Interrupt und die Board-LED toggelt nicht. Beim nächsten Tasterdruck startet der Timer wieder bei 0.

Damit das Ganze nicht zu schnell geht, habe ich den Timer A entschleunigt und als Takt für den Timer B genommen. Und zum wiederholten Mal: Änderungen der Einstellungen an TCA0 und TCB0 können Nebenwirkung auf millis() & Co. haben.

Hier der auf das Wesentliche reduzierte Sketch (für einen ATtiny1614):

#define TCB_TIMEOUT_VALUE (19531) // = 1s with the settings below

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
    PORTA.OUTTGL = PIN2_bm; /* Toggle PA2 */
}

void setup() {
    PORTA.DIRSET = PIN2_bm; // PA2 as output
    PORTA.DIRCLR = PIN1_bm; // PA1 as input
    PORTA.PIN1CTRL = PORT_PULLUPEN_bm; // enable pull-up at PA1
        
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // TCB0 is event user

    TCB0.CCMP = TCB_TIMEOUT_VALUE;
    TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc | TCA_SINGLE_ENABLE_bm; // slow down Timer A
    TCB0.CTRLB = TCB_CNTMODE_TIMEOUT_gc; // set time-out mode
    TCB0.INTCTRL = TCB_CAPT_bm; // enable capture interrupt
    TCB0.EVCTRL = TCB_EDGE_bm | TCB_CAPTEI_bm; // start counter on negative edge
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // use timer A clock, enable TCB0
}

void loop(){}

Mit einem 20 MHz Systemtakt, einem Teiler von 1024 und einem CCMP von 19531 erreicht der Counter CCMP nach 19531 * 1024 / 20000000 = ~1 Sekunde. Wenn ihr den Taster dauerhaft gedrückt haltet, läuft der Counter immer weiter und erreicht alle 65536 * 1024 / 20000000 = ~3.36 Sekunden CCMP.

Damit ihr besser verfolgen könnt, bei welchem Counterstand ihr den Taster losgelassen habt, habe ich hier noch eine Komfortvariante des Sketches:

#define TCB_TIMEOUT_VALUE (19531) // = 1s with the settings below
volatile bool keyReleased = false;

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
    PORTA.OUTTGL = PIN2_bm; /* Toggle PA2 */
}

ISR(PORTA_PORT_vect){ // ISR for button release
    PORTA.INTFLAGS = PIN1_bm;
    keyReleased = true;
}

void setup() {
    Serial.begin(115200);
    PORTA.DIRSET = PIN2_bm; // PA2 as output
    PORTA.DIRCLR = PIN1_bm; // PA1 as input
    PORTA.PIN1CTRL = PORT_PULLUPEN_bm | PORT_ISC_RISING_gc; // pull-up / interrupt
        
    EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH0_gc; // TCB0 is event use

    TCB0.CCMP = TCB_TIMEOUT_VALUE;
    TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc | TCA_SINGLE_ENABLE_bm; // slow down Timer A
    TCB0.CTRLB = TCB_CNTMODE_TIMEOUT_gc; // enable compare/capture output, time-out mode
    TCB0.INTCTRL = TCB_CAPT_bm; // enable capture interrupt
    TCB0.EVCTRL = TCB_EDGE_bm | TCB_CAPTEI_bm; // start counter on negative edge
    TCB0.CTRLA = TCB_CLKSEL_CLKTCA_gc | TCB_ENABLE_bm; // use timer A clock, enable TCB0
}

void loop(){
    if(keyReleased){
        Serial.println(TCB0.CNT);
        keyReleased = false;
    }
}

Sofern der angezeigte Wert auf dem seriellen Monitor kleiner als 19351 ist, wird die LED nicht getoggelt.

Input Capture On Event Mode

Jetzt kommen noch vier Input Capture Modi auf euch zu, mit denen ihr Pulsweiten und / oder Frequenzen messen könnt. Als zu vermessendes Objekt verwenden wir für alle Beispiele ein PWM-Signal mit einem Duty Cycle von ~75 % an PB1, welches wir auf einem ATtiny1614 mit analogWrite(6, 192) erzeugen (192 / 255 * 100 = ~75.29 %). PB1 ist der Event Generator, der Timer B ist der Event User.

Als Erstes betrachten wir den Input Capture On Event Mode. In diesem Modus zählt der Counter permanent von 0 bis 65535, läuft über und beginnt wieder von vorn. Je nachdem, ob ihr das EDGE Bit gesetzt habt oder nicht, schreibt der Timer bei einer fallenden oder steigenden Flanke den aktuellen Counter Wert in das CCMP Register und löst einen Interrupt aus. In der ISR des Capture Interrupts lesen wir das CCMP Register aus. Die Differenz zum letzten Wert ist die Periode des PWM-Signals in Timer Counter Zählschritten.

Im Falle eines Counter-Überlaufes zwischen zwei Interrupts ist die Differenz negativ und wir müssen 65636 addieren.

volatile long cnt = 0;
volatile long lastCnt = 0;

ISR(TCB0_INT_vect){
    cnt = TCB0.CCMP - lastCnt; // Read the period
    lastCnt = TCB0.CCMP;
    if(cnt < 0){   // period is neg. when TCB0 was overflowed
        cnt += 65536;
    }
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    Serial.begin(115200);
    analogWrite(6, 192); // the event source 
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 is event user
    TCB0.CTRLA = 0; // Stop TCB0
    TCB0.CTRLB = TCB_CNTMODE_CAPT_gc;  // use input capture on event mode
    TCB0.EVCTRL =  TCB_CAPTEI_bm; // enable capture event input
    TCB0.INTCTRL = TCB_CAPT_bm;  // enable capture interrupt
    TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // use system clock, enable TCB0
    delay(10);
}

void loop(){
    Serial.println(cnt);
    delay(1000);
}

Das Ergebnis war 16320. analogWrite() wird über den Timer A Takt mit einem Teiler von 64 gesteuert. Unabhängig vom Duty Cycle wird alle 255 Zähler (nicht 256!) eine steigende Flanke und eine fallende Flanke erzeugt.

Kleiner Exkurs: analogWrite() wird durch ein PWM-Signal des Timer A im Split Mode erzeugt. Prüft ihr den Top-Wert mit Serial.println(TCA0.SPLIT.HPER) bzw. Serial.println(TCA0.SPLIT.LPER) erhaltet ihr als Ergebnis 254 (= 255 Schritte). Ein analogWrite(x, 255) erzeugt ein digitalWrite(x, HIGH). So ist sichergestellt, dass man einen Duty Cycle von 100 % darstellen kann.

Das PWM-Signal hat also eine Frequenz von 20 MHz / (64 * 255) = ~1225.5 Hz bzw. eine Periode von 0.000816 Sekunden. Der Timer B zählt im Systemtakt. Damit entsprechen 16320 Zählschritte einer Frequenz von 20 MHz / 16320 = ~1225.5 Hz. Passt also perfekt!

Input Capture Frequency Measurement Mode

Der Input Capture Frequency Measurement Mode tut im Prinzip dasselbe wie der Input Capture On Event Mode, nur dass der Zähler nach dem Erhalt des Eventsignals wieder auf 0 gesetzt wird. Das hat den Vorteil, dass uns CCMP direkt und ohne Differenzberechnungen die Periode liefert.

Eigentlich ist die Bezeichnung „Frequency Measurement“ irreführend. Es müsste „Period Measurement“ heißen.

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    Serial.begin(115200);
    analogWrite(6, 192); // the event source
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 is event user
    TCB0.CTRLA = 0;
    TCB0.CTRLB = TCB_CNTMODE_FRQ_gc; // enable input capture frequency mode
    TCB0.EVCTRL = TCB_EDGE_bm | TCB_CAPTEI_bm; // enable capture event input 
    TCB0.INTCTRL = TCB_CAPT_bm;  // enable capture event input
    TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // use system clock, enable TCB0
  
}

void loop(){
    Serial.print("Period in TCB0 counts: ");
    Serial.println(TCB0.CCMP + 1);
    float frq = 20000000.0 / ((TCB0.CCMP + 1) * 1.0);
    Serial.print("Frequency [Hz]: ");
    Serial.println(frq);
    delay(1000);
}

In diesem Beispielsketch habe ich die Berechnung der Frequenz in den Sketch integriert. Interessant ist, dass man hier noch 1 zum CCMP Wert addieren muss, um auf das richtige Ergebnis zu kommen. Ein CCMP Wert von 16319 sind 16320 Schritte – das macht ja eigentlich auch Sinn. Nur, warum man beim Input Capture On Event Mode 1 nicht addieren muss, ist zumindest mir bislang nicht wirklich klar geworden.

Ausgabe timer_b_input_capture_frq.ino
Ausgabe timer_b_input_capture_frq.ino

Input Capture Pulse-Width Measurement Mode

Der Input Capture Pulse-Width Measurement Mode ermöglicht euch die Messung der Pulsweite von Signalen. Im Gegensatz zum Frequency Measurement Mode, bei dem nur die steigende oder die fallende Flanke einen Effekt hatte, wird bei diesem Modus der Zähler bei einer Flanke „genullt“ und bei der anderen Flanke erfolgt der Input Capture, also das Schreiben des Zählerstandes in CCMP.

ISR(TCB0_INT_vect){
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    Serial.begin(115200);
    analogWrite(6, 192); // the event source
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 is event user
    TCB0.CTRLA = 0;
    TCB0.CTRLB = TCB_CNTMODE_PW_gc; // enable pulse width measurement mode
    TCB0.EVCTRL = TCB_CAPTEI_bm; // enable capture event input
    TCB0.INTCTRL = TCB_CAPT_bm; 
    TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // use system clock, enable TCB0
    delay(10);
}

void loop(){
    Serial.print("High Period in TCB0 counts: ");
    unsigned int highCnt = (TCB0.CCMP + 1);
    Serial.println(highCnt);
    float pw = (highCnt * 1.0) / 20.0;  // cnt divided by 20 MHz
    Serial.print("Pulse width [µs]: ");
    Serial.println(pw);
    Serial.println();
    delay(1000);
}

Als Ergebnis erhielt ich:

Ausgabe timer_b_input_capture_pw.ino
Ausgabe timer_b_input_capture_pw.ino

Das entspricht exakt den Erwartungen. Die PWM Periode umfasste 16320 Zähler in 255 Schritten. 192 Schritte entsprechen 16320 * 192 / 255 = 12288.

Wenn ihr das EDGE Bit im Event Control Register setzt (Zeile 12), dann wertet der Sketch die LOW Phase aus. 

Input Capture Frequency and Pulse-Width Measurement Mode

Der Input Capture Frequency and Pulse-Width Measurement Mode kombiniert die Frequenz- und die Pulsweitenmessung. Dabei können natürlich nicht beide Werte in CCMP gespeichert werden. Stattdessen funktioniert der Modus folgendermaßen (bei nicht gesetztem EDGE-Bit):

  • Erste positive Flanke (Pulse beginnt): Der Counter wird zurückgesetzt und startet.
  • Negative Flanke (Pulse endet): Input Capture, d.h. der Counterstand wird in CCMP geschrieben.
  • Zweite positive Flanke (Periode endet): Counter stoppt, Interrupt wird ausgelöst.

Da wir die Periode (aus CCMP) und die Pulsweite (Counterstand bei Interrupt) kennen, können wir den Duty-Cycle ausrechnen. Und genau das macht der nächste Sketch:

volatile unsigned int period = 0;

ISR(TCB0_INT_vect){
    period = TCB0.CNT + 1; // Read the period
    TCB0.INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
}

void setup() {
    Serial.begin(115200);
    analogWrite(6, 192); // the event source
    EVSYS.ASYNCCH1 = EVSYS_ASYNCCH1_PORTB_PIN1_gc; // PB1 is event generator
    EVSYS.ASYNCUSER0 = EVSYS_ASYNCUSER0_ASYNCCH1_gc; // TCB0 is event user
    TCB0.CTRLA = 0;
    TCB0.CTRLB = TCB_CNTMODE_FRQPW_gc;
    TCB0.EVCTRL = TCB_CAPTEI_bm; // TCB_EDGE_bm | TCB_CAPTEI_bm;
    TCB0.INTCTRL = TCB_CAPT_bm;
    TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; // use system clock, enable TCB0
}

void loop(){
    Serial.print("Period in TCB0 counts: ");
    Serial.println(period);
    Serial.print("High Period in TCB0 counts: ");
    Serial.println(TCB0.CCMP + 1);
    float frq = 20000000.0 / (period * 1.0);
    Serial.print("Frequency [Hz]: ");
    Serial.println(frq);
    float dc = (TCB0.CCMP + 1) * 1.0 / (period * 1.0) * 100.0;
    Serial.print("Duty Cycle [%]: ");
    Serial.println(dc);
    Serial.println();
    delay(1000);
}

Hier das Ergebnis:

Ausgabe timer_b_input_capture_frq_and_pw.ino
Ausgabe timer_b_input_capture_frq_and_pw.ino

Configurable Custom Logic – Modul CCL

Mit dem Configurable Custom Logic Modul CCL bekommen die tinyAVR MCUs Fähigkeiten, für die ihr sonst externe Logic ICs einsetzen müsstet. Es geht aber noch weit darüber hinaus.

Das CCL Modul ist in sogenannten Look-Up Tables (LUTs) organisiert. Zu jeder LUT gehören drei Eingänge und ein Ausgang.

Register des CCL Moduls

Ich gebe zu diesem Modul nur einen groben Überblick. Hier erst einmal die Registerübersicht:

Register Summary Modul CCL, Beispiel ATtiny1614
Register Summary Modul CCL, Beispiel ATtiny1614

Jede LUT hat ihr eigenes TRUTH Register. Dort legt ihr fest, welche Input Level Kombination wahr (true) ist. Beispiel: Für LUT0 definieren wir, dass LUT0-IN2 = HIGH, LUT0-IN1 = LOW und LUT0-IN0 = LOW der Zustand „true“ sein soll. D. h.: HIGH/LOW/LOW ⇒ 0b100 = 4. Das wiederum bedeutet, dass in TRUTH0 das Bit Nr. 4 zu setzen ist (und nicht, dass TRUTH0 = 4 ist!).

Jetzt kommt das Geniale: Die Inputs müssen keine I/O Pins sein, sondern es kann sich beispielsweise auch um Events, Timer, USARTs oder den Ausgang einer anderen LUT handeln. Wenn also bestimmte Zustände verschiedener Module in eurem Programm eine Aktion erfordern, dann könnt ihr euch mit der CCL die Abfrage dieser Zustände und einiges an if- und switch-Kombinationen sparen. Die Inputs legt ihr über die Input Select Bitgroups (INSELn[3:0]) fest.

Tritt der Zustand „true“ ein, könnt ihr euch über einen Interrupt informieren lassen. Alternativ oder zusätzlich könnt ihr das OUTEN Bit setzen, sodass bei „true“ der zugehörige Ausgang von LOW auf HIGH geht.

Über die SEQSELn[3:0] Bit Groups könnt ihr 2 LUTs miteinander kombinieren.

Beispielsketch für das CCL-Modul

Als einfaches Beispiel prüfen wir den Zustand der LUT0 zugeordneten I/O-Pins. Für den ATtiny1614 (und viele andere) gilt:

  • LUT0-IN2 = PA2
  • LUT0-IN1 = PA1
  • LUT0-IN0 = PA0
  • LUT0-OUT = PA4

Unsere True-Bedingung soll sein: 2LUT0-IN2 = HIGH, LUT0-IN1 = LOW und LUT0-IN0 = LOW. Die Eingänge ziehen wir mit den internen Pull-Up Widerständen auf HIGH. Ein Tasterdruck soll sie auf LOW bringen. Werden die Taster an LUT0-IN1 und LUT0-IN0 gedrückt, ist die True-Bedingung erfüllt und der Ausgang LUT0-OUT geht auf HIGH. Das lassen wir uns mit einer LED anzeigen. Hier die Schaltung:

Schaltung zu ccl_basic.ino, Beispiel ATtiny1614
Schaltung zu ccl_basic.ino am Besipiel ATtiny1614

Und hier der zugehörige Sketch:

void setup() {
    PORTA.DIRCLR = PIN2_bm | PIN1_bm | PIN0_bm; // PA0 = LUT0-IN0 as input, etc.
    //PORTA.DIRSET = PIN4_bm; // not necessary!     
    PORTA.PIN0CTRL = PORT_PULLUPEN_bm; // PULLUP for PA0 = LUT0-IN0
    PORTA.PIN1CTRL = PORT_PULLUPEN_bm; // PULLUP for PA1 = LUT0-IN1
    PORTA.PIN2CTRL = PORT_PULLUPEN_bm; // PULLUP for PA2 = LUT0-IN2
    CCL.LUT0CTRLB = CCL_INSEL0_IO_gc | CCL_INSEL1_IO_gc; // select I/O as input
    CCL.LUT0CTRLC = CCL_INSEL2_IO_gc; // select I/O as Input
    CCL.TRUTH0 = 0x10; // LUT2-IN2 = HIGH, LUT1-IN2 = LOW, LUT0-IN2 = LOW ==> TRUE
    CCL.LUT0CTRLA = CCL_OUTEN_bm | CCL_ENABLE_bm; // OUT Enable / LUT0 enable
    CCL.CTRLA = CCL_ENABLE_bm; // CCL_ENABLE
}

void loop(){}

Die Prüfung der Zustände und die Ausgabe läuft an der CPU vorbei. Das ist ausgesprochen ressourcenschonend.

Das megaTinyCore Boardpaket hat für das CCL Modul eine eigene Bibliothek namens Logic implementiert. Vielleicht ist das für einige leichter als die Registerprogrammierung. Ihr findet dazu auch einige Beispielsketche als Teil des Boardpakets.

Die tinyAVR MCUs gehen schlafen – Modul SLPCTRL

Schlafen gehen – das passt doch als letztes Kapitel!

Registereinstellungen für SLPCTRL

Das Modul SLPCTRL besitzt lediglich das Register CTRLA, in dem ihr den Schlafmodus auswählt (SMODE[1:0]) und das SLPCTRL-Modul durch das SEN-Bit aktiviert. Das Setzen des SEN-Bits alleine lässt den Mikrocontroller aber nicht einschlafen. Dafür nutzt ihr sleep_cpu() aus avr/sleep.h.

tinyAVR Register: SLPCTRL.CTRLA
Register: SLPCTRL.CTRLA

Als Bit Group Configurations für SMODE[1:0] habt ihr folgende Optionen:

  • IDLE: Idle – der leichteste Schlaf; alles aktiv, bis auf die CPU.
  • STDBY: Stand-by – die mittlere Einstellung.
  • PDOWN: Power Down – Tiefschlaf.

Je tiefer der Schlaf, desto geringer der Stromverbrauch und desto limitierter die Weckmethoden. Welche Peripherien in welchem Schlafmodus zur Verfügung stehen, findet ihr im Datenblatt, Abschnitt SLPCTRL. Der Stand-by-Modus ist dabei am flexibelsten. Viele Module besitzen ein RUNSTBY-Bit, das festlegt, ob das Modul im Stand-by-Modus zur Verfügung stehen soll oder nicht.

Beispielsketch für SLPCTRL

Im folgenden Beispiel schicken wir den tinyAVR in den Power Down Modus und wecken ihn nach einer Sekunde mithilfe des Periodic Timer Interrupts (PIT).

 #include<avr/sleep.h>

ISR(RTC_PIT_vect){
    RTC.PITINTFLAGS = 1;
}

void setup(){
    Serial.begin(115200);
    RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;
    RTC.PITCTRLA = RTC_PERIOD_CYC32768_gc | RTC_PITEN_bm;
    RTC.PITINTCTRL = RTC_PI_bm;   
    SLPCTRL.CTRLA = SLPCTRL_SMODE_PDOWN_gc | SLPCTRL_SEN_bm;  
}

void loop(){
    Serial.println("Going to sleep...");
    Serial.flush();
    sleep_cpu();
}

10 thoughts on “tinyAVR-Serie Teil 2: Timer A/B, CCL, SLPCTRL

  1. Hallo Wolle,

    sehr schöner Artikel.
    Eine Frage dazu:
    Benutzen die ATtiny’s den gleichen Befehlssatz wie ein ATmega324?

    Gruß
    Wölle

    1. Hi Wölle, auf die Ebene der Befehlssätze gehe ich normal nicht. Aber ich habe mal spaßeshalber die Instruction Set Summary aus den Datenblättern des ATtiny804/1604 und des ATmega328P nebeneinandergelegt. Ich habe nicht jede Zeile verglichen, aber es sieht ziemlich identisch aus. Im Zweifelsfall müsstest du selbst mal in die Datenblätter schauen.

      VG, Wolle

      1. Hallo Wolle,

        vielen Dank für Deine schnelle Antwort.
        Da habe ich mich wohl nicht klar ausgedrückt, meine Frage bezog sich eher auf die C-Programmierung.
        Ich habe viele fertige C-Funktionen (GPIO, UART, TWI, usw.) für den ATmega, kann ich diese 1:1 auch für den ATtiny3226 benutzen?

        Gruß
        Wölle

        1. Hallo Wölle,

          das geht leider nicht 1:1. Zum einen haben die tinyAVRs nicht dieselben Register. Zum anderen werden die Register anders angesprochen. Aus PORTA |= (1<<PA1); wird z.B. PORTA.OUTSET = PIN1_bm;. Das ist der Nachteil, wenn man auf Registerebene programmiert. Ich persönlich nutze so weit wie möglich Arduino Code. Auf Registerbene programmiere ich dann die Dinge, die nicht durch Arduino abgedeckt sind, wie beispielsweise bestimmte Aspekte bei den Timern.

          VG, Wolfgang

          1. Hallo Wolle,

            vielen Dank, dass hatte ich mir schon gedacht!
            Kannst Du mir eine Webseite oder ein Tutorial für die C-Programmierung empfehlen?

            Gruß
            Wölle

            1. Schau mal hier in Teil 1:

              https://wolles-elektronikkiste.de/tinyavr-serie-0-1-2-programmieren-teil-1#reg_programming_tinyavr

              Und vielleicht hier:
              https://ww1.microchip.com/downloads/en/Appnotes/AVR1000b-Getting-Started-Writing-C-Code-for-AVR-DS90003262B.pdf

              Damit und mit dem Datenblatt deines ausgewählten tinyAVR-Vertreters solltest du die Dinge eigentlich übertragen können. Zu beachten ist, dass sich die tinyAVRs der Serien 0, 1 und 2 in gewissen Aspekten unterscheiden. Also wirst du selbs da Anpassungen vornehmen müssen.
              VG, Wolfgang

  2. Hallo Wolfgang,

    Danke für deine Mühe, welche du dir hiermit gemacht hast.
    Soweit konnte ich all deine Beispiele auch praktisch reproduzieren.

    Vollkommen klar wie man bei einem PWM Signal sowohl die Frequenz wie auch die High-Perioden Dauer zu ermitteln. Dennoch schaffe ich es nicht, wahrscheinlich wegen einer falschen Methode einfach das PWM- Signal so auszuwerten, das ich bei einem beliebigen PWM-Eingngssignal ( externe Signalquelle ) das eigentlich Input Duty mit einer Auflösung float 0 bis 100% mit einer Nachkommastelle, bzw mit 12 Bit Integer herauszulösen.

    Vor allem habe ich das Problem, wenn seitens der Signalquelle keine PWM Signal ausgegeben wird, also der Eingangspegel für einen längeren Zeitpunkt auf LOW gezogen bleibt. ( Hier bricht dann einfach nach einigen Minuten die serielle Ausgabe ab )

    Hintergrund ist eigentlich, dass eine System ( AVR ) aller 100 ms eine RAM-Variable anhand des Eingangssignal beschreibt, und ein weiterer Timer anhand dieses erfassten Dutys über eine Case-Funktion ( sieben Bereiche jeweils von – bis ) über 3 GPIOs verteilt verschiedene Funktionen ( 2x Schalten On/Off, PWM Ausgabe) ausführt / auslöst.

    Eigentlich ist der meinige Wunsch eine doch recht „komplizierte“ PWM Auswerte- und Durchleitungsfunktion, weil eine direkte Ansteuerung mit einem reinen PWM Signal zu einer Drehzahlreglung für funktionelle Störungen in der Ausgabe = Antriebseinheit sorgt.

    1. Hallo Andre,

      ich habe heute den Sketch timer_b_input_capture_frq_and_pw.ino genommen und die Zeile mit dem analogWrite herausgenommen, sonst alles belassen. PB1 habe ich auf GND gelegt. Über einen FTDI Adapter lasse ich mir die Werte ausgeben:
      Period in TCB0 counts: 0
      High Period in TCB0 counts: 1
      Frequency [Hz]: inf
      Duty Cycle [%]: inf

      Der Sketch läuft jetzt seit einer guten Stunde ohne Abbruch. Keine Ahnung warum er bei dir die Arbeit nach ein paar Minuten verweigert.

      Dann habe ich mal einen Arduino Nano genommen und damit ein PWM Signal erzeugt (analogWrite(6,100)). Dann habe ich die GNDs des Nano und des ATtiny1614 verbunden und das PWM Signal des Nano auf den Pin PB1 des Attiny1614 losgelassen:
      Period in TCB0 counts: 20327
      High Period in TCB0 counts: 8019
      Frequency [Hz]: 983.91
      Duty Cycle [%]: 39.45

      Passt also alles. Ich denke, ich brauche mehr Details, um ggf. helfen zu können (welcher ATtiny, PWM Frequenz, vielleicht eine Schaltung oder Foto, usw.). Gerne auch per mail (wolfgang.ewald@wolles-elektronikkiste.de).

      Dein Vorhaben (4. Absatz) habe ich noch nicht verstanden. Aber zunächst egal, denn erst einmal muss ja die einfache PWM-Analyse mit externer Quelle laufen.

      VG, Wolfgang

    2. …und eine Fhlermöglichkeit noch: wenn du meine Einstellungen übernimmst, dann darf die PWM-Frequenz nicht kleiner sein als 20000000 / 2^16 = ~305. Denn dann läuft der TCB0 Counter über und die Berechnung ergibt keinen Sinn mehr. In dem Fall müsste der Takt von TCB0 verringert werden.

Schreibe einen Kommentar

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