tinyAVR-Serie 0, 1, 2 programmieren – Teil 1

Ü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!):

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):

TCBn.CTRLA-Register der tinyAVR Serie
TCBn.CTRLA-Register

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.

tinyAVR PORTx Register Summary
PORTx Register Summary (geringe individuelle Abweichungen möglich)

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 entspricht PORTx.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:

Arduino Nano Every / Nano 4808 Register - PORTn.PINxCTRL
PORTn.PINxCTRL Register

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):

ISC[2:0] Bit Group Configurations
ISC[2:0] Bit Group Configuration

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:

Register Summary PORTMUX (Beispiel 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:

EVSYS.CHANNELn der tinyAVR Serie 2
Register EVSYS.CHANNELn

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:

Event Generatoren der tinyAVR Serie 2 - einige Beispiele
Event Generatoren der tinyAVR Serie 2

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:

Register EVSYS.USERn tinyAVR Serie 2
Register EVSYS.USERn

Hier ein Auszug aus der Tabelle der zur Verfügung stehenden Event User:

Event User der tinyAVR Serie 2

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 Register Summary (Beispiel ATtiny1614/1616/1617)
EVSYS Register Summary (Beispiel ATtiny1614/1616/1617)

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

tinyAVR Serie: Register RTC.CLKSEL
Register RTC.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.

tinyAVR Register: RTC.CALIB
Register RTC.CALIB

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).

tinyAVR Register: RTC.CTRLA
Register RTC.CTRLA

Über den PRESCALER könnt ihr den RTC bis zu einem Faktor von 32768 verlangsamen:

PRESCALER[3:0] Bit Group Configurations
PRESCALER[3:0] Bit Group Configurations

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“.

tinyAVR Register: RTC.INTCTRL / RTC.INTFLAGS
Register RTC.INTCTRL / RTC.INTFLAGS

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.

tinyAVR Register: RTC.PITCTRLA
Register RTC.PITCTRLA
PERIOD[3:0] Bit Group Configurations
PERIOD[3:0] Bit Group Configurations

PIT Interrupt Control Register PITINTCTRL

In PITINTCRTL aktiviert ihr den PIT Interrupt. PITINTFLAGS ist wiederum „baugleich“.

RTC.PITINTCTRL / RTC.PITINTFLAGS
Register RTC.PITINTCTRL / RTC.PITINTFLAGS

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.

tinyAVR Register: WDT.CTRLA
Register WDT.CTRLA

Für PERIOD und WINDOW könnt ihr folgende Einstellungen vornehmen:

WINDOW[3:0] / PERIOD[3:0] Bit Group Configurations
WINDOW[3:0] / PERIOD[3:0] Bit Group Configurations

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.  Ein wdt_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

tinyAVR Register: WDT.STATUS
Register WDT.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.

Output wdt.ino  -  links: unverändert, Mitte: "if (seconds == 5)", rechts: "if(seconds ==3)"
Output wdt.ino – links: unverändert, Mitte: „if (seconds == 5)“, rechts: „if(seconds ==3)“

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:

Register Summary ADCn
Register Summary ADCn der tinyAVR Serie 0 und 1

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:

Register Summary ADCn 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;
    }
}

17 thoughts on “tinyAVR-Serie 0, 1, 2 programmieren – Teil 1

  1. 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?

    1. 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

      1. 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

        1. 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!

          1. 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

            1. 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.

              1. 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.

            2. 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.

              1. 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.

  2. 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!

    1. 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
      .

Schreibe einen Kommentar

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