Über den Beitrag
In meinem Beitrag megaTinyCore nutzen hatte ich gezeigt, wie ihr Sketche auf die ATtiny Mikrocontroller der tinyAVR ® Serien 0, 1 und 2 mithilfe des Boardpaketes von Spence Konde hochladet. In den nächsten beiden Beiträgen gehe ich auf die Programmierung auf Registerebene ein. Das wird relevant, wenn ihr z.B. die Timer der tinyAVR Mikrocontroller nutzen wollt oder spezielle Funktionen wie etwa das Event System oder die Configurable Customer Logic (CCL).
Ein Überblick über die tinyAVR Familie und ihre technischen Eigenschaften hatte ich hier schon gegeben.
Die Datenblätter der tinyAVR Mikrocontroller haben mehrere hundert Seiten. Das kann ich hier natürlich nicht komplett abbilden. Funktionen, die ihr auch bequem über den Arduino-Sprachumfang steuern könnt (Wire, SPI, usw.) werde ich hier nicht behandeln. Aber auch sonst musste ich mich thematisch beschränken. Ein Blick ins Datenblatt lohnt also auf jeden Fall!
Folgendes erwartet euch in diesem Beitrag und seinen Fortsetzungen (die noch nicht fertig sind!):
- Registerprogrammierung der tinyAVR-Serie
- I/O Steuerung – Modul PORTx
- Ein- / Ausgänge neu zuordnen – Modul PORTMUX
- Das Eventsystem – Modul EVSYS
- Real-Time Counter – Modul RTC
- Watchdog Timer – Modul WDT
- A/D-Wandler – Modul ADCn
- Timer A – Modul TCAn (Teil 2)
- Timer B – Modul TCBn (Teil 2)
- Configurable Custom Logic – Modul CCL (Teil 2)
- Die tinyAVR MCUs gehen schlafen – Modul SLPCTRL (Teil 2)
- Timer D – Modul TCDn (Teil 3)
Serielle Ausgabe
Und noch eine Anmerkung vorab: in einigen Sketchen nutze ich Serial.print()
. Um damit eine Ausgabe auf den seriellen Monitor zu zaubern, nehmt ihr einen USB-zu-TTL Adapter und verbindet RX mit TX, TX mit RX und GND mit GND. Port einstellen nicht vergessen. Falls ihr die Sketche nicht per UPDI, sondern ohnehin schon mit Bootloader und USB-zu-TTL Adapter hochladet, dann geht die Ausgabe natürlich direkt.
Registerprogrammierung der tinyAVR-Serie
Ich werde nun versuchen, die Grundprinzipien der Registerprogrammierung für die tinyAVR Serie in kurzen Worten zu erklären. Wer es ausführlich haben möchte, der schaue in die Application Note TB3262 von Microchip.
Alles ist ein wenig anders
Der folgende Blinksketch für den Pin A2 vermittelt einen ersten Eindruck:
void setup() { PORTA.DIRSET = PIN2_bm; // set PA2 (selectively!) to OUTPUT } void loop() { PORTA.OUTSET = PIN2_bm; // set PA2 to HIGH delay(200); PORTA.OUTCLR = PIN2_bm; // set PA2 to LOW delay(200); }
Das unterscheidet sich recht stark von der euch wahrscheinlich geläufigeren Schreibweise à la PORTA |= (1<<PA2)
.
Register
Die Register sind in Modulen organisiert. Die Module sind Strukturen (Datentyp struct), deshalb seht ihr in dem Blinksketch die Punkt-Schreibweise. Der Name der Struktur ist das Modul, die Register sind Elemente dieser Struktur, z.B.
- PORTA.DIRSET:
- Modul: PORTA
- Register: DIRSET
Bit Masks und Bit Group Configuration Masks
Um Bit Masks und Bit Group Configuration Masks zu verstehen, schauen wir uns als Beispiel das Control A Register (CTRLA) des Timer B0 an. Das Modul heißt TCB. Da die tinyAVR Mikrocontroller aber zum Teil zwei Timer B haben (0 und 1), wird dem Modulnamen die Nummer angehängt. Allgemein ausgedrückt: „MODULNAMEn.REGISTERNAME“. Das CTRLA Register des Timer B0 heißt somit: TCB0.CTRLA.
Die TCBn.CTRLA Register besitzen verschiedene Bits und die Bitgruppe CLKSEL (Clock Select):
Zum Setzen einzelner Bits kommen die Bit Masks zum Einsatz. Sie werden nach folgender Struktur gebildet:
- MODUL_BIT_bm
Bei der Bit Mask wird die Nummer des Moduls weggelassen. Um das ENABLE Bit im Timer B0 zu setzen, schreibt ihr also:
TCB0.CTRLA |= TCB_ENABLE_bm;
Bei Bitgruppen (auch Bitfelder genannt) kommen die Bit Group Configuration Masks zu Einsatz, die sich folgendermaßen zusammensetzen:
- MODUL_BITGRUPPE_CONFIGURATION_gc
Als Beispiel legen wir fest, dass der Timer B0 die TCA Clock benutzt:
TCB0.CTRLA |= TCB_CLKSEL_TCA0_gc;
CLKTCA entspricht dem CLKSEL-Wert 0x2 bzw. 0b10. Im Register befindet sich CLKSEL an Bitposition 1 und 2, ist also um 1 Bit nach links verschoben. Deshalb hat TCB_CLKSEL_TCA0_gc den Wert 0x4 bzw. 0b100.
Wenn ihr die Bit Group TCB_CLKSEL maskieren wollt, gibt es dafür die Bit Group Mask. Hier die allgemeine Form:
- MODUL_BITGRUPPE_gm
TCB_CLKSEL_gm hat den Wert 0x6 bzw. 0b110, wie ihr einfach mit Serial.println(TCB_CLKSEL_gm, BIN)
prüfen könnt.
Bit Positions und Bit Group Positions
Dann gibt es noch die Bit Positions und die Bit Group Positions:
- MODUL_BIT_bp
- MODUL_BITGRUPPE_gp
Sie geben, wenig überraschend, die Position des Bits oder der Bitgruppe im Register an. Wenn ihr also den aktuellen CLKSEL Wert des Timers TCB0 ermitteln wollt, könntet ihr das so machen:
Serial.println( ((TCB0.CTRLA & TCB_CLKSEL_gm) >> TCB_CLKSEL_gp) );
Das klingt vielleicht alles erst einmal ein wenig kompliziert, aber nach kurzer Zeit gewöhnt man sich an diese Schreibweise. Es ist ein gewisser Schreibaufwand, es zahlt sich aber aus, denn dadurch ist der Code auch nach einigen Monaten noch gut verständlich.
Orientierung im Datenblatt
Diese Art, Register zu programmieren, ist anfangs gewöhnungsbedürftig. Was bei der Orientierung hilft, ist das Datenblatt des betreffenden tinyAVR Vertreters. Es ist nach den Modulen geordnet und zu jedem Modul gibt es eine „Register Summary“ mit Links zu den einzelnen Registern, wo ihr dann die Bit Masks und Bit Group Masks findet.
Außerdem empfehle ich, im Datenblatt in den Abschnitt „I/O Multiplexing and Considerations“ zu schauen. Dort steht in einer Tabelle, welche Pins welche Funktionen haben.
I/O Steuerung – Modul PORTx
PORTx Register
DIR-, OUT- und IN-Register
Wie ihr in der Register Summary oben seht, gibt es eine Reihe von Registern, deren Bezeichnung mit „DIR“ oder „OUT“ beginnt. Die „DIR …“ Register steuern, welche Pins als INPUT oder OUTPUT dienen. Mithilfe der „OUT …“ Register legt ihr den Pinlevel fest, also HIGH oder LOW. Der zweite Namensteil bedeutet:
- SET: setzt Bits in OUT und DIR → OUTPUT bzw. HIGH
- CLR: löscht die Bits in OUT und DIR → INPUT bzw. LOW
- TGL: toggelt den Zustand, d. h. kehrt ihn um.
Ihr könnt auch direkt in die DIR- und OUT-Register schreiben, aber das Schöne an OUTSET, OUTCLR, OUTTGL und ihren DIR-Pendants ist, dass die Anweisungen Pin-selektiv erfolgen. Beispiel:
PORTx.OUT = PINy_bm;
setzt den Pin y (mit y = 0 bis 7) des Port x auf HIGH und alle anderen Pins des Port x auf LOW. Dabei ist x = A, B oder C, je nachdem, welche Ports vorhanden sind.PORTx.OUTSET = PINy_bm;
wirkt nur auf den Pin y, d. h. die Anweisung entsprichtPORTx.OUT |= PINy_bm;
.
Mithilfe der IN-Register könnt ihr den Zustand der Pins auslesen (digitalRead()
, sozusagen). Wenn ihr hingegen ein Bit in das PORTx.IN Register schreibt, dann wird das entsprechende Bit in PORTx.OUT getoggelt. Das IN-Register ist das Pendant zum PINx Register der traditionellen AVR-Mikrocontroller.
Kontrollregister PINxCTRL
Jeder Pin besitzt ein eigenes Kontrollregister, nämlich PINxCTRL:
Das INVEN Bit ermöglicht euch den INPUT und OUTPUT Wert zu invertieren. PULLUPEN aktiviert den internen Pull-up-Widerstand. Mit ISC legt ihr das Interruptverhalten fest oder schaltet die Digitalfunktion des Pins ganz ab (INPUT_DISABLE):
Beispielsketche für PORTx
Pins schalten
Ich komme noch einmal auf den Blink-Sketch zurück und möchte zeigen, dass es noch andere Möglichkeiten gibt, um dieselbe Wirkung zu erreichen:
void setup() { PORTA.DIRSET = PIN2_bm; // set PA2 to OUTPUT // PORTA.DIR |= (1<<2); // PORTA.DIR |= (1<<PIN2_bp); // bp = bit position } void loop() { PORTA.OUTSET = PIN2_bm; // set PE2 to HIGH // PORTA.OUT |= PIN2_bm; delay(200); PORTA.OUTCLR = PIN2_bm; // PORTA.OUT &= ~PIN2_bm; delay(200); // Alternative: toggle pin: // PORTA.OUTTGL = PIN2_bm; // delay(200); }
Pinlevel auslesen
Hier ein Beispiel, wie ihr einzelne Pins auslesen könnt. Dazu hängt ihr eine LED an PA2 und einen Taster, der beim Drücken ein HIGH Signal gibt, an PA1. Solange ihr den Taster drückt, leuchtet die LED.
void setup() { PORTA.DIRSET = PIN2_bm; // PA1: OUTPUT PORTA.DIRCLR = PIN1_bm; // PA2: INPUT } void loop(){ if(PORTA.IN & PIN1_bm){ PORTA.OUTSET = PIN2_bm; } else{ PORTA.OUTCLR = PIN2_bm; } }
Interrupt einrichten
Es folgt ein einfaches Beispiel für einen Interrupt an PA1. Abgesehen vom Taster, der beim Drücken ein LOW Signal geben soll, verwendet ihr den Aufbau des letzten Beispiels. Beachtet, dass der Interrupt nicht automatisch durch den Aufruf der ISR gelöscht wird. Ihr müsst das Bit im zuständigen Interruptflagregister „manuell“ löschen.
volatile bool event = false; ISR(PORTA_PORT_vect){ PORTA.INTFLAGS = PIN1_bm; // clear interrupt (writing a "1" clears the bit!) event = true; } void setup() { PORTA.DIRSET = PIN2_bm; PORTA.DIRCLR = PIN1_bm; // redundant in this case PORTA.PIN1CTRL = PORT_PULLUPEN_bm | PORT_ISC_BOTHEDGES_gc; // Pull-Up / Interrupt n both edges } void loop(){ if(event){ PORTA.OUTSET = PIN2_bm; delay(1000); PORTA.OUTCLR = PIN2_bm; event = false; } }
Sowohl beim Drücken als auch beim Loslassen des Tasters wird ein Interrupt ausgelöst. Das fällt erst richtig auf, wenn man den Taster länger als eine Sekunde drückt.
Virtuelle Ports
Wenn ihr Portmanipulation à la PORTA.OUT = (1 << 2);
einsetzt, solltet ihr anstelle der „normalen“ Portregister die virtuellen Portregister nutzen. Dazu setzt ihr einfach ein „V“ vor den Port, also: VPORTA.OUT = (1 << 2);
. Die virtuellen Ports sind Kopien der Ports im unteren Adressraum. Portmanipulationen werden mit den virtuellen Ports schneller durchgeführt. Die Ausführung des folgenden Codes benötigte auf einem ATtiny3226 bei 20 MHz 501 Millisekunden:
for(unsigned long i=0; i<1000000;i++){ PORTA.OUT = 0; PORTA.OUT = (1<<2); }
Derselbe Code, jedoch mit VPORTA.OUT anstelle von PORTA.OUT benötigte 400 Millisekunden. Das sind keine riesigen Unterschiede, aber immerhin.
Die zur Verfügung stehenden VPORTx Register sind DIR, OUT, IN und INTFLAGS.
Ein- / Ausgänge neu zuordnen – Modul PORTMUX
Die Register des Moduls PORTMUX erlauben es euch, bestimmte Funktionen alternativen Pins zuzuordnen. Schaut am besten in der PORTMUX Register Summary nach, welche Funktionen das sind. Hier, als Beispiel, die PORTMUX Register Summary des ATtiny3224/6/7:
Als Beispiel leiten wir den Ausgang 1 des Timers TCA0 um. Das zuständige PORTMUX Register heißt TCAROUTEA. Durch das Setzen des zuständigen Bits wechselt ihr vom Standard zu Alternative:
PORTMUX.TCAROUTEA |= PORTMUX_TCA01_bm;
Das Eventsystem – Modul EVSYS
Das Eventsystem ist richtig cool! Es erlaubt bestimmten Peripherie-Einheiten („Peripherals“), also Timern, ADC, USART, usw., Signale an andere Peripherie-Einheiten zu senden und das ohne Beteiligung der CPU. Etwas später im Beitrag lassen wir den RTC-Timer regelmäßig ADC-Messungen auslösen. Normalerweise würdet ihr das wahrscheinlich über delay()
, millis()
oder Timer-Interrupts realisieren. Alle diese Optionen haben aber gewisse Nachteile.
Zur Übertragung der Signale stehen 3 oder 6 Kanäle (Channel) zur Verfügung. Den Sender des Signals nennt man den Generator, der Empfänger ist der User. Viele User haben ein eigenes Event Control Register, in dem weitere Einstellungen vorgenommen werden. Diese Register sind Teil der Peripherie-Module.
Generatoren und User können synchron oder asynchron zum Systemtakt sein. Wenn ein asynchroner Generator ein Eventsignal an einen synchronen User schickt, muss das Signal erst synchronisiert werden, was ein paar Systemtakte dauert. Wichtig ist erst einmal nur, dass ihr von der Unterscheidung gehört habt.
EVSYS-Modul der tinyAVR Serie 2
Register CHANNELn – Definition des Generators
In den Registern CHANNELn (mit n = 0 bis 5) legt ihr den Generator fest:
Die meisten Generatoren können alle Channel nutzen. Es gibt aber auch Ausnahmen. Beispielsweise können die Pins des Port A nur die Channel 0 bis 3 benutzen. Hier ein kleiner Auszug aus dem Datenblatt des ATtiny3224/6/7:
Sucht euch einen geeigneten Kanal aus und weist seinem Register (EVSYS.CHANNELn) den Generator zu. Den Ausdruck für den Generator setzt ihr nach folgendem Rezept zusammen:
„EVSYS_“ + „CHANNELn_“ + Peripheral_ + Output_ + „gc“
Also beispielsweise: EVSYS_CHANNEL0_PORTA_PIN1_gc;
Register USERn – Definition der User
Die Event-User legt ihr in den Registern USERn fest:
Hier ein Auszug aus der Tabelle der zur Verfügung stehenden Event User:
Die vollständige(n) Tabelle(n) findet ihr im Datenblatt im EVSYS-Kapitel bei den Registerbeschreibungen. Den Namen der USER-Register setzt ihr folgendermaßen zusammen:
„EVSYS.USER“ + „Peripheral“ + „Input“
Also beispielsweise EVSYS.USEREVSYSEVOUTA
. Dem USER-Register weist ihr den Kanal zu, von dem aus gesendet wird.
Ein einfaches Eventsystem-Beispiel
Zum besseren Verständnis schauen wir uns ein kleines Beispiel an (getestet auf ATtiny3226). Hängt eine LED an PA2 und einen Taster, der den Pin beim Drücken LOW zieht, an PA1.
Jetzt nutzen wir den PA1 als Event Generator und EVOUTA (PA2) als User. Für den Port A stehen uns die Channel 0 bis 3 zur Verfügung. Im Beispiel nutzen wir den Channel 0.
void setup() { PORTA.DIRCLR = PIN1_bm; // PA1 INPUT PORTC.DIRSET = PIN2_bm; // PA2 (=EVOUTA) OUTPUT PORTA.PIN1CTRL |= PORT_PULLUPEN_bm; EVSYS.CHANNEL0 = EVSYS_CHANNEL0_PORTA_PIN1_gc; // PA1 is event generator for channel 0 EVSYS.USEREVSYSEVOUTA = EVSYS_USER_CHANNEL0_gc; // EVOUTA is user of event channel 0 } void loop(){}
PA2, bzw. EVOUTA, spiegelt den Zustand von PA1 wider. Im Normalzustand leuchtet die LED, beim Drücken des Tasters geht sie aus. Das mag auf den ersten Blick nicht beeindrucken, auf den zweiten aber schon. Weder fragen wir den Level von PA1 ab, noch haben wir einen externen Interrupt dafür eingerichtet. Überdies findet ihr keine Anweisung im laufenden Programm, dass die LED an- oder ausgehen soll. Die Vorgänge werden im Hintergrund gesteuert, ohne dass euer laufendes Programm irgendetwas dafür tun muss. Später folgen sinnvollere Anwendungen.
EVSYS der tinyAVR Serie 0 und 1
Das Eventsystem der tinyAVR Serien 0 und 1 unterscheidet sich von dem der Serie 2. Es gibt separate Register für die synchronen und asynchronen Generatoren und User. Das Grundprinzip ist aber dasselbe und deswegen stelle ich das hier nicht noch einmal im Detail vor. Schaut selbst ins Datenblatt, sucht nach dem Kapitel EVSYS und geht vielleicht erst einmal in die Register Summary. Da bekommt ihr einen Überblick und könnt von da aus in die Register navigieren.
EVSYS Beispielsketch für die tinyAVR Serien 0 und 1
Als Starthilfe gibt es noch einen Beispielsketch für Vertreter der tinyAVR Serie 1 und 0. Er macht im Prinzip dasselbe wie das Beispiel für die Serie 2. Ich habe den Sketch auf einem ATtiny1604 und ATtiny1614 getestet.
void setup() { PORTA.DIRCLR = PIN1_bm; // PA1 INPUT PORTA.DIRSET = PIN2_bm; // PA2 (=EVOUT0) OUTPUT PORTA.PIN1CTRL |= PORT_PULLUPEN_bm; PORTMUX.CTRLA |= PORTMUX_EVOUT0_bm; // Event output needs to be enabled for tinyAVR 0/1 EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_PORTA_PIN1_gc; // PA1 is async. event generator for channel 0 EVSYS.ASYNCUSER8 = EVSYS_ASYNCUSER8_ASYNCCH0_gc; // ASYNCUSER8 = EVOUT0 = channel 0 user } void loop(){}
Neben den schon genannten Unterschieden fällt hier auf, dass das „x“ bei den Eventausgängen EVOUTx eine Zahl und kein Buchstabe ist. Außerdem ist zu beachten, dass der Eventausgang (EVOUTx) im Register PORTMUX.CTRLA aktiviert werden muss. Und schließlich ist noch zu erwähnen, dass die SYNCUSER- und ASYCUSER-Register nicht nach ihrem User benannt werden, sondern durchnummeriert werden. Die Nummern findet ihr im Datenblatt bei der Beschreibung der (A)SYNCUSER-Register.
Real-Time Counter – Modul RTC
Das RTC-Modul bietet zwei Funktionen, nämlich den 16-Bit Real-Time Counter (RTC) und den Periodic Interrupt Timer (PIT). Beide greifen auf denselben Taktgeber zurück, ansonsten können die Funktionen unabhängig voneinander genutzt werden. Der Takt liegt maximal bei 32768 Hz, ihr steuert mit dem Modul also eher langsamere Prozesse.
Die Register des RTC-Moduls
Angenehmerweise ist die Registerlandschaft des RTC-Moduls für alle tinyAVR Vertreter fast identisch. Unterschiede gibt es bei der Kalibrierung und der Auswahl der Taktgeber.
Clock Selection Register CLKSEL
Im CLKSEL Register stellt ihr den Taktgeber ein. Es stehen folgende Optionen zur Verfügung:
- INT32K_gc: Liefert 32768 Hz durch den internen Ultra Low-Power Oszillator OSCULP32K.
- INT1K_gc: Wie INT32K, aber mit Teiler 32, also 1024 Hz.
- TOSC32K_gc: Liefert 32768 Hz durch XOSC32K oder einen externen Taktgeber an TOSC1.
- gilt nur für die tinyAVR Serien 1 und 2.
- EXTCLK_gc: Externer Taktgeber an EXTCLK.
Crystal Frequency Calibration Register CALIB (nur tinyAVR Serie 2)
Eine richtig tolle Eigenschaft des RTC-Moduls der Serie 2 ist, dass ihr den zugrundeliegenden Taktgeber mithilfe des CALIB Registers kalibrieren könnt.
ERROR[6:0] ist die Abweichung in ppm. Ist SIGN gesetzt, wird der Takt entsprechend schneller. In diesem Fall müsst ihr den Prescaler mindestens auf DIV2 setzen. Ist SIGN nicht gesetzt, wird der Takt langsamer.
RTC Control Register A CTRLA
Im CTRLA Register legt ihr fest, ob das RTC Modul im Stand-by-Modus weiterlaufen soll (RUNSTDBY). Überdies (de-)aktiviert ihr die Kalibrierung (CORREN, falls verfügbar) und ihr schaltet das Modul ein (RTCEN).
Über den PRESCALER könnt ihr den RTC bis zu einem Faktor von 32768 verlangsamen:
RTC Interrupt Control Register INTCTRL
Für den RTC stehen euch ein Compare und ein Overflow Interrupt zur Verfügung. Das korrespondierende Interrupt Flag Register INTFLAG ist „baugleich“.
PIT Control Register A CTRLA
Der Periodic Interrupt Timer macht schlicht das, was sein Name vermuten lässt: Er löst regelmäßig einen Interrupt aus. Im PITCTRLA Register aktiviert ihr den PIT und stellt die Periode ein.
PIT Interrupt Control Register PITINTCTRL
In PITINTCRTL aktiviert ihr den PIT Interrupt. PITINTFLAGS ist wiederum „baugleich“.
Beispielsketche für RTC und PIT
RTC Beispielsketch
Als einfaches Beispiel für die Programmierung des RTC folgt ein Sketch, der ein „Zeitlupen-PWM“ mit einer Periode von 2 Sekunden und einem Duty Cycle von 75 % an PA2 erzeugt.
ISR(RTC_CNT_vect){ if(RTC.INTFLAGS & RTC_CMP_bm){ // check for CMP interrupt RTC.INTFLAGS = RTC_CMP_bm; // delete CMP interrupt flag PORTA.OUTCLR = PIN2_bm; // PA2 = LOW } if(RTC.INTFLAGS & RTC_OVF_bm){ // check for OVF interrupt RTC.INTFLAGS = RTC_OVF_bm; // delete OVF interrupt flag PORTA.OUTSET = PIN2_bm; // PA2 = HIGH } } void setup() { delay(1); PORTA.DIRSET = PIN2_bm; RTC.CLKSEL = RTC_CLKSEL_INT32K_gc; // set internal clock @ 32768 Hz RTC.PER = 16383; // TOP = 16383 RTC.CMP = 12288; // Compare value = 12288 RTC.INTCTRL = RTC_CMP_bm | RTC_OVF_bm; // enable CMP and OVF interrupts RTC.CTRLA = RTC_PRESCALER_DIV4_gc | RTC_RTCEN_bm; // Prescaler = 4 and enable RTC } void loop(){}
Da für den Overflow- und den Compare-Interrupt nur ein Interruptvektor (RTC_CNT_vect) zur Verfügung steht, müssen wir über die Interrupt Flags prüfen, welches Ereignis den Interrupt ausgelöst hat.
Als Prescaler ist 4 eingestellt, d.h. der RTC zählt mit einer Frequenz von 32768 / 4 = 8192 Hz. PER ist 16383 und damit läuft der RTC alle 16384 Schritte über, was einer Überlauffrequenz von 2 Sekunden entspricht. Der Compare Value beträgt 12288, also 75 % von PER +1.
PIT Beispielsketch 1
Der Beispielsketch für den PIT ist noch schlichter. Mit einer Periode von 16384 und einem Takt von 32768 Hz wird alle 0.5 Sekunden ein Interrupt ausgelöst. Wir nutzen ihn hier, um PA2 zu toggeln.
ISR(RTC_PIT_vect){ RTC.PITINTFLAGS = RTC_PI_bm; // clear PIT interrupt flag PORTA.OUTTGL = PIN2_bm; // toggle PA2 } void setup() { PORTA.DIRSET = PIN2_bm; RTC.CLKSEL = RTC_CLKSEL_INT32K_gc; // set internal clock @ 32768 Hz RTC.PITINTCTRL = RTC_PI_bm; // enable PIT interrupt RTC.PITCTRLA = RTC_PERIOD_CYC16384_gc | RTC_PITEN_bm; // interrupt frq.: 2 Hz } void loop(){}
PIT Beispielsketch 2
Dasselbe könnt ihr aber auch ohne Interrupt erreichen, indem ihr PIT als Event Generator nutzt. Das Event-Signal hat einen Duty Cycle von 50 %. Hier in der Version für einen ATtiny1614:
void setup() { PORTA.DIRSET = PIN2_bm; RTC.CLKSEL = RTC_CLKSEL_INT1K_gc; // set internal clock @ 1.024 kHz RTC.PITCTRLA = RTC_PITEN_bm; PORTMUX.CTRLA |= PORTMUX_EVOUT0_bm; // Event output needs to be enabled for tinyAVR 0/1 EVSYS.ASYNCCH3 = EVSYS_ASYNCCH3_PIT_DIV1024_gc; // Event freq. = 1024 / 1024 = 1 [Hz] EVSYS.ASYNCUSER8 = EVSYS_ASYNCUSER8_ASYNCCH3_gc; } void loop(){}
Watchdog Timer – Modul WDT
Die Register des WDT Moduls
Der Watchdog Timer verhält sich auf allen Vertretern der tinyAVR Familie gleich. Zwei Eigenschaften finde ich dabei besonders bemerkenswert:
- Der WDT hat keinen Interrupt-Vektor.
- Ein WDT Timeout führt auf jeden Fall zum Reset.
Seinen Takt erhält der Watchdog Timer (WDT) vom Ultra Low-Power Oscillator, OSCULP32K.
Control A Register CTRLA
Die zeitlichen Vorgaben für den WDT nehmt ihr im Kontrollregister A, CTRLA, vor.
Für PERIOD und WINDOW könnt ihr folgende Einstellungen vornehmen:
Wenn ihr nur einen Wert für PERIOD festlegt und WINDOW auf 0 lasst, dann läuft der WDT im normalen Modus. Erfolgt in der Zeitspanne, die ihr mit PERIOD festlegt, kein Watchdog Reset, wird ein Systemreset ausgeführt.
Ist WINDOW ungleich null, dann befindet ihr euch im Window Modus. Der ist ein wenig schwieriger zu erklären und wird vielleicht erst durch den Beispielsketch richtig klar. Im Window Modus gilt:
- Nach der Initialisierung oder einem
wdt_reset()
befindet sich der WDT in der durch WINDOW festgelegten Periode. Das Datenblatt nennt das die „closed window time-out period“ (TOWDTW). In dieser Periode kann der WDT nicht zurückgesetzt werden. Einwdt_reset()
innerhalb dieser Periode führt zu einem sofortigen Systemreset. - Nach der TOWDTW beginnt die „normal window time-out period“ (TOWDT), deren Länge ihr mit PERIOD festlegt. Erfolgt in dieser Zeit ein
wdt_reset()
, dann beginnt das Spiel wieder von vorn mit der TOWDTW
. - Erfolgt im Zeitraum TOWDTW + TOWDT kein
wdt_reset()
, wird ein Systemreset ausgeführt.
Status Register STATUS
Durch das Setzen des LOCK-Bits schützt ihr die Einstellungen des WDT vor Veränderungen. Ihr könnt es nur im Debug Modus wieder löschen.
Wenn ihr Daten in das CTRLA-Register schreibt, müssen diese zur WDT Clock Domain übertragen werden. Für die Zeit der Synchronisierung wird das SYNCBUSY-Bit gesetzt.
Beispielsketch für den WDT
Der Normal-Mode sollte keinen Beispielsketch benötigen. Deswegen schauen wir uns nur einen Sketch für den Window-Modus an. Die Einstellung für den WDT ist: TOWDTW = TOWDT = 4 Sekunden.
#include <avr/wdt.h> void setup(){ Serial.begin(115200); Serial.println("*******************"); Serial.println("Watchdog Timer Test"); Serial.println("Sketch starts..."); CPU_CCP = CCP_IOREG_gc; // allow changes WDT.CTRLA = WDT_WINDOW_4KCLK_gc | WDT_PERIOD_4KCLK_gc; // 4s period and window CPU_CCP = CCP_IOREG_gc; WDT.STATUS = WDT_LOCK_bm; while(WDT.STATUS & WDT_SYNCBUSY_bm){} // wait until WDT is synchronized wdt_reset(); int seconds = 0; while(seconds < 100){ // if(seconds == 5){ // try also 3 // wdt_reset(); // } Serial.print(seconds); Serial.println(" seconds"); delay(1000); seconds++; } } void loop(){}
Wenn ihr den Sketch unverändert laufen lasst, zählt die while() Schleife hoch, bis TOWDTW + TOWDT
= 8 Sekunden vergangen sind. Die letzte Ausgabe ist „7 seconds“, weil mit dem Erreichen der achten Sekunde keine Zeit mehr für die Ausgabe bleibt.
Wenn ihr die Zeilen 15 bis 17 entkommentiert, gibt es bei Sekunde fünf, also in der „erlaubten“ TOWDT Periode ein wdt_reset()
. Danach läuft der WDT weitere acht Sekunden durch, bis der Systemreset durchgeführt wird.
Wenn ihr dann noch Zeile 15 so abändert, dass der wdt_reset()
nach drei Sekunden erfolgt, gibt es einen sofortigen Systemreset, da ihr euch in der „verbotenen“ TOWDTW Periode befindet.
Und was soll das Ganze? Normalerweise beißt der Watchdog, wenn der Sketch irgendwo hängen bleibt. Mit der Window-Methode könnt ihr den Systemreset herbeiführen, wenn eine Aktion zu früh ausgeführt wird, weil beispielsweise irgendein anderer Schritt, aus welchen Gründen auch immer, nicht ausgeführt wurde.
A/D-Wandler – Modul ADCn
Auch der A/D-Wandler der tinyAVR Mikrocontroller hätte sicherlich einen separaten Beitrag verdient. Ich halte aber auch dieses Kapitel aber kurz und verweise wieder einmal auf die Datenblätter.
Die ADC Module der tinyAVR Serien 0 und 1 unterscheiden sich grundlegend von denen der Serie 2.
ADCn der tinyAVR Serie 0/1
Hier die für alle Vertreter der Serie 0 und 1 identische Registerstruktur:
Einige ausgewählte Registereinstellungen möchte ich erläutern:
- RUNSTBY: Erlaubt den Betrieb des ADC im Stand-by-Modus.
- RESSEL: Setzt ihr das Bit, wird die Auflösung von 10 auf 8 Bit gesenkt.
- FREERUN: Kontinuierlicher Modus.
- SAMPNUM: Ihr könnt bis zu 64 Ergebnisse pro Messung aufsummieren. 2^6 Ergebnisse mal 2^10 Auflösung ergibt ein Ergebnis mit einer Größe von 2^16, das noch in das Resultregister RES passt.
- PRESC: Teiler für die Taktrate des ADC.
- REFSEL: Auswahl der Referenzspannung. Zur Auswahl stehen: INTREF (Internal Reference), VDDREF und VREFA (External Reference, nicht für ). Verwirrenderweise steht im Datenblatt „INTERNAL“ und „VDD“ anstelle von INTREF bzw. VDDREF.
- SAMPLEN: Verlängert die Messzeit bis zum maximalen Faktor 31.
- MUXPOS: Legt den Eingang für den ADC fest. Die Tabelle mit den Bit Group Configurations findet ihr im Datenblatt.
- STARTCONV: Startet eine Messung.
Die interne Referenz wird im Register VREF.CTRLA spezifiziert. Dazu stellt ihr die Bit Group ADC0REFSEL[2:0] ein. Die Bit Group Configurations heißen VREF_ADC0REFSEL_xxx_gc mit
- xxx = 0V55, 1V1, 2V5V, 4V34 V oder 1V5 für 0.55, 1.1, 2.5, 4.34 bzw. 1.5 Volt.
Beispielsketche für den ADC (tinyAVR Serie 0/1)
Einfache A/D Messung (Serie 0/1)
Im folgenden Beispielsketch messen wir die Spannung an A2. Dabei nutzen wir die interne 4.34 V Referenzspannung.
void setup() { Serial.begin(115200); PORTA.PIN2CTRL = PORT_ISC_INPUT_DISABLE_gc; // Disable digital buffer ADC0.CTRLA = ADC_ENABLE_bm; // Enable ADC ADC0.CTRLC = ADC_REFSEL_INTREF_gc | ADC_PRESC_DIV16_gc; // use internal reference / prescaler: 16 VREF.CTRLA = VREF_ADC0REFSEL_4V34_gc; // use internal 4.34 V reference ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; // use A3 as input ADC0.SAMPCTRL = 0; } void loop(){ ADC0.COMMAND = ADC_STCONV_bm; // Start Conversion while((ADC0.INTFLAGS & ADC_RESRDY_bm) == 0){} // Waiting for the result float result = ADC0.RES * 4340.0 / 1024.0; Serial.print("Voltage [mV]: "); Serial.println(result); delay(1000); }
ADC0 – RTC controlled (Serie 0/1)
Ich komme noch einmal auf das Eventsystem zurück. Der folgende Sketch nutzt den RTC, um alle zwei Sekunden eine A/D-Wandlung zu initiieren.
volatile bool adcReady = false; ISR(ADC0_RESRDY_vect){ adcReady=true; } void setup() { Serial.begin(115200); PORTA.PIN3CTRL = PORT_ISC_INPUT_DISABLE_gc; ADC0.CTRLA = ADC_ENABLE_bm; ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_REFSEL_INTREF_gc | ADC_PRESC_DIV16_gc; VREF.CTRLA = VREF_ADC0REFSEL_4V34_gc; ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; // A3 ADC0.SAMPCTRL = 0; ADC0.EVCTRL = ADC_STARTEI_bm; // initiate measurement by event ADC0.INTCTRL = ADC_RESRDY_bm; // enable result ready interrupt RTC.CLKSEL |= RTC_CLKSEL_INT32K_gc; // use internal 32 kHz clock RTC.PER = 63; // overflow after 63 (64 steps) RTC.CTRLA = RTC_PRESCALER_DIV1024_gc | RTC_RTCEN_bm; // OVF frequency = 0.5 Hz EVSYS.ASYNCCH0 = EVSYS_ASYNCCH0_RTC_OVF_gc; // RTC overflow is event generator EVSYS.ASYNCUSER1 = EVSYS_ASYNCUSER1_ASYNCCH0_gc; // ADC0 is event user } void loop(){ if(adcReady){ float result = ADC0.RES * 4340.0 / 1024.0; Serial.print("Voltage [mV]: "); Serial.println(result); adcReady = false; } }
Ohne Eventsystem müsstet ihr einen Timer Interrupt einrichten oder eine Konstruktion mit delay()
oder millis()
wählen, um Messungen im 2-Sekundentakt zu starten. Dann müsstet ihr noch warten oder euch über einen weiteren Interrupt informieren lassen, wenn das Messergebnis vorliegt. Im obigen Beispiel hingegen reicht ein einziger Interrupt, alles andere läuft im Hintergrund.
ADCn der tinyAVR Serie 2
Hier die Registerübersicht für das ADCn Modul der tinyAVR-Serie 2:
Wichtige technische Unterschiede sind:
- Der ADC hat eine maximale Auflösung von 12 Bit.
- Das Eingangssignal kann bis zu 16-fach verstärkt werden.
- Die Optionen für die Referenzspannungen sind: 1V024, 2V048, 2V500 oder 4V096 für 1.024, 2.048, 2.500 bzw. 4.096 Volt.
Beispielsketche für den ADC (tinyAVR Serie 2)
Einfache A/D Messung (Serie 2)
So würde dann der Sketch für eine einfache ADC-Wandlung aussehen:
void setup() { Serial.begin(115200); PORTA.PIN3CTRL = PORT_ISC_INPUT_DISABLE_gc; // Disable digital buffer ADC0.CTRLA = ADC_ENABLE_bm; // Enable ADC ADC0.CTRLC = ADC_REFSEL_4096MV_gc; ADC0.CTRLB = ADC_PRESC_DIV16_gc; // use internal reference / prescaler: 16 ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; // use A3 as input } void loop(){ ADC0.COMMAND = ADC_MODE_SINGLE_12BIT_gc | ADC_START_IMMEDIATE_gc; // Start Conversion while((ADC0.INTFLAGS & ADC_RESRDY_bm) == 0){} // Waiting for the result float result = ADC0.RESULT; // * 4096.0 / 4096.0 = 1 Serial.print("Voltage [mV]: "); Serial.println(result); delay(1000); }
ADC0 – RTC controlled (Serie 2)
Und hier noch die „Übersetzung“ der RTC-gesteuerten ADC-Wandlung für die Serie 2:
volatile bool adcReady = false; ISR(ADC0_RESRDY_vect){ adcReady=true; } void setup() { Serial.begin(115200); PORTA.PIN3CTRL = PORT_ISC_INPUT_DISABLE_gc; ADC0.CTRLC = ADC_REFSEL_4096MV_gc; ADC0.CTRLB = ADC_PRESC_DIV16_gc; // use internal reference / prescaler: 16 ADC0.MUXPOS = ADC_MUXPOS_AIN3_gc; // use A3 as input ADC0.COMMAND = ADC_MODE_SINGLE_12BIT_gc | ADC_START_EVENT_TRIGGER_gc; ADC0.INTCTRL = ADC_RESRDY_bm; // enable result ready interrupt RTC.CLKSEL |= RTC_CLKSEL_INT32K_gc; // use internal 32 kHz clock RTC.PER = 63; // overflow after 63 (64 steps) RTC.CTRLA = RTC_PRESCALER_DIV1024_gc | RTC_RTCEN_bm; // OVF frequency = 0.5 Hz EVSYS.CHANNEL0 = EVSYS_CHANNEL0_RTC_OVF_gc; // RTC overflow is event generator EVSYS.USERADC0START = EVSYS_USER_CHANNEL0_gc; // ADC0 is event user } void loop(){ if(adcReady){ float result = ADC0.RESULT * 4096.0 / 4096.0; Serial.print("Voltage [mV]: "); Serial.println(result); adcReady = false; } }
Danke für diesen sehr informativem Beitrag zu den neuen ATtiny Controllern!
Alle rennen gerade der KI hinterher, doch mich beeindruckt die Programmierung einer batteriebetriebenen Schaltung deutlich mehr. – Du triffst hier genau den Nerv!
Ich stolpere noch mit den Serial.print() Ausgaben bei einem ATtiny 412.
Das BLINK Programm läuft, doch ich kann kein Ausgangssignal für die Textausgabe am TX-Pin messen.
Hat jemand einen Tipp für mich?
Hi Pit,
erst einmal Danke fürs Feedback. Zum Serial.print(): Ich habe keinen 412, nur einen 402, der aber denselben Pinout hat. Nur um sicherzugehen: Pin 2 = PA6 = Arduino Pin 0 = TX, Pin 3 = PA7 = Arduino Pin 1 RX. Klappt bei mir. Sehr komisch!
void setup() {
Serial.begin(115200);
}
void loop(){
Serial.println(„Hallo Wolle“);
delay(1000);
}
Gibt aus, was es soll. Vielleicht brennst du noch einmal den Bootloader? Auch wenn du keinen benutzt, denn da werden einige Einstellungen vorgenommen.
VG, Wolfgang
Danke Wolfgang,
Ich ging erst von PA2 aus, doch das scheint laut Datenblatt ein optionaler TXD zu sein.
An PA6 kann ich auch mit dem Oszi TX Funkverkehr erkennen, doch es scheint ein Pullup Widerstand aktiviert zu sein, da mein PA2 auf high liegt.
Danke für den Tipp mit dem Bootloader! Ich werde heute Abend versuchen ihn damit wieder ‚einzurenken‘.
Food 4 Thought: Ein Beispiel basierend auf I2C, auslesen eines Sensors und Ausgabe auf einem I2C Display mit den neuen ATtinys, wäre ein für mich willkommener Artikel.
Doch ich weiss noch nicht ob dafür der vorhandene Speicherplatz reicht.
Danke und weiter so mit Deinem erfrischenden Blog!
-Pit
Für alle Interessierten: PA6 ist tatsächlich der TX- Ausgang. Die Angabe PA1 bezieht sich wohl auf alternatives Multiplexing.
Mit dem Arduino Nano kann ich mit dem geladenen jtag2updi die Ausgabe nicht mitlesen. Die RX-LED blinkt, doch scheinbar ist durch das geladene Programm das Auslesen der seriellen Schnittstelle nicht mehr möglich.
Hab jetzt einfach einen ESP32 auf einem anderen USB-Port angeschlossen und lese hier die serielle Ausgabe des ATtiny 412 mit.
Mein I2C Versuch scheiterte übrigens kläglich mit der eingebundenen aufgrund von Speicherplatzproblemen.
Ich vermute hier muss man versuchen I2C nativ einzubinden.
Allen frohes Basteln!
Ja, 4 KB Flash ist nicht viel. Da bleibt nicht viel Platz für große Bibliotheken.
Gut, dass es geht! Und danke für die Anregung.
Beim Thema „Ausblick“ Deines Artikels „megaTinyCore nutzen“ wolltest Du auf 2 der erwähnten Möglichkeiten der Programmierung später noch mal detailliert eingehen.
Hier würde ich es begrüssen, wenn Du den Serial Monitor erwähnst.
Dieser funktioniert bei mir nicht ganz wie erwartet mit jtag2updi.
Das Flashen geht tadellos, die RX-Daten lassen auch die RX-LED auf dem Arduino Nano aufblinken, doch ich schaffte es nicht die Daten im Serial Monitor anzuzeigen.
Zu dem Thema erwähnt Spence Conde ebenfalls Probleme bei seinem ATtiny 412 Development Board; Das kann aber auch spezifisch genau für diesen ATtiny Typen nur gelten.
Allen viel Spass am vorweihnachtlichen Basteln!
-Pit
Ich habe angekündigt, dass ich auf ein oder zwei Vertreter der tinyAVR Reihe näher eingehe. Ich habe mich insofern unentschieden, dass ich hier auf die gesamte Reihe detailliert eingehe.
Wie Serial prinzipiell mit den tinyAVR Vertretern funktioniert habe ich beschrieben. Wenn es mit einzelnen Vertreter spezielle Probleme gibt, dann ist das ärgerlich, aber ich kann mir nicht alle der fast 40 tinyAVR Vertreter zulegen und durchtesten.
Mit den ATtinys 402, 1604, 1614, 1626 und 3226 klappt Serial bzw die Ausgabe auf dem seriellen Monitor.
Sicher Ewald, alle Typen durchzutesten kann keiner verlangen!
Kaum macht man’s richtig, schon funktioniert es: Ich habe mein USB-TTL Kabel erhalten und kann den TX-Port wie beschrieben auslesen.
Zuvor versuchte ich das Arduino Mega Board auch zum Auslesen zu nutzen, das war wohl eine weitere Fehlerquelle.
Und noch ein kurzer Nachtrag: Spence Konde sagt lediglich, dass die Serial Pins des ATtiny412 bei Verwendung seines Boardpaketes mit Version < 2.0.0 den alternativen Ausgängen zugeordnet ist. Da wir schon bei Version 2.6.8 sind, dürfte das nur in Ausnahmefällen relevant sein. Oder beziehst du dich auf irgendetwas anderes? Wenn es Probleme mit Serial gibt, dann müsste ich genau wissen, was du gemacht hast und wie sich das Problem äußert, um zu helfen.
Genau das Multiplexing meinte ich. Am Oszilloskop konnte ich ja schon erkennen, auf welchem Port gesendet wird.
I2C: Habe nun mittels der Register einen I2C-Sensor ohne anbinden können. Die File Size ohne die grosse Lib liegt nun bei verträglichen 1,7 KB.
Freut mich, wenn es jetzt hinhaut!
Hallo, cool zu hören, dass es noch Leute gibt, die auf Register-Ebene programmieren! Das erinnert mich an die Anfänge der Mikrocontroller-Programmierung.
Vor einiger Zeit habe ich ein YouTube-Video gesehen, in dem jemand die CCL- und Event-Logik der TinyAVR-Serie mit der megatinyCore-Library in Arduino angesprochen hat. Es scheint also auch in der Arduino-Umgebung möglich zu sein.
Hier ist der Link zum Video:
https://www.youtube.com/watch?v=nR5M5UrQ18k
Grüße aus dem Norden!
Hallo Roland,
danke fürs Feedback. Die Sketche in diesen Beitrag sind in der Arduino Umgebung programmiert. Sonst würde ich nicht setup() und loop(), sondern main() und while(1) verwenden. Das Youtube Video schaue ich mir später in Ruhe an.
VG, Wolfgang
.
Excellent tutorial. Thanks and waiting for next part
Vielen Dank für Deine wertvolle Anleitung, freue mich auf den nächsten Teil.
:O)
Vielen Dank!