Ü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:
- Registerprogrammierung der tinyAVR-Serie (Teil 1)
- I/O Steuerung – Modul PORTx (Teil 1)
- Ein- / Ausgänge neu zuordnen – Modul PORTMUX (Teil 1)
- Das Eventsystem – Modul EVSYS (Teil 1)
- Real-Time Counter – Modul RTC (Teil 1)
- Watchdog Timer – Modul WDT (Teil 1)
- A/D-Wandler – Modul ADCn (Teil 1)
- Timer A – Modul TCAn
- Timer B – Modul TCBn
- Configurable Custom Logic – Modul CCL
- Die tinyAVR MCUs gehen schlafen – Modul SLPCTRL
- Timer D – Modul TCDn (Teil 3)
Timer A – Modul TCAn
Zunächst ein paar allgemeine Anmerkungen zum Timer A:
- Alle Vertreter der tinyAVR Serien besitzen genau einen Timer A (TCA0).
- TCA0 kann als 16-Bit-Timer betrieben werden (Single Mode) oder in zwei 8-Bit-Timer geteilt werden (Split Mode).
- TCA0 ist an den Systemtakt gekoppelt, er kann aber im Register TCA0.SINGLE.CTRLA bzw. TCA.SPLIT.CTRLA mit einem Teiler (Prescaler) verlangsamt werden.
- 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()
unddelay()
nutzen. Änderungen des Prescalers machen diese Funktionen damit unbrauchbar. Ihr könnt abermillis()
/micros()
anderen Timern zuordnen oder auch deaktivieren (Arduino IDE → Werkzeuge). - 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.
- 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.
- 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.
Die Bitgruppe CLKSEL (Clock Select) ist für den Teiler zuständig. Die folgenden Bit Group Configuration Masks stehen zur Auswahl:
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
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:
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.
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:
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.
Mithilfe der Bit Group Configuration EVACT bestimmt ihr, welche Events gezählt werden, bzw. wie gezählt wird:
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):
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.
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.
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.
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.
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.
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.
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.
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:
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:
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:
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:
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.
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(); }
Hallo Wolle,
sehr schöner Artikel.
Eine Frage dazu:
Benutzen die ATtiny’s den gleichen Befehlssatz wie ein ATmega324?
Gruß
Wölle
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
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
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
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
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
SUPER
und vielen Dank!
Gruß
Wölle
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.
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
…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.