Pollin Funk-AVR-Evaluationsboard

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Motivation

Auf dieser Seite geht es um Erfahrungen eines Einsteigers mit dem Bausatz Funk-AVR-Evaluationsboard v1.1 von der Firma Pollin und um die C-Programmierung mithilfe des Boards und AVR-GCC/WinAVR.

In den Beispielprogrammen wird nach Möglichkeit ein ATmega8 mit 12 MHz Quarz Taktquelle verwendet. Die Hardware auf dem Board ist sehr überschaubar (ein Taster, zwei LEDs, s. PDF unter Weblinks), so dass die Beispiele vielleicht zum eigenen Experimentieren anregen z. B. mit einem Steckbrettaufbau wie im AVR Tutorial.

Bitte nutzt die Diskussionsseite oder das Forum, wenn beim Lesen Fragen, Anregungen oder Kritik auftauchen,

Aufbau des Bausatzes

Alle Angaben hier beziehen sich auf die Version v1.1 des Bausatzes.

Mittlerweile (Stand 11/2008) verkauft Pollin die Version v1.2 und in dieser Version ist u.a. der Optokoppler nicht mehr vorhanden bzw. dessen Funktion übernehmen Jumper. Ebenso sind die anderen Leitungen von und zu den Funkmodulen über Jumper verbunden, d.h. man kann die Module elektrisch vom Board trennen (i.U. wichtig beim Fuse-Programmieren s.unten).

Praktisch ist bei dem Aufbau des Bausatzes nichts besonderes zu vermelden. Für einen Ungeübten (mich) hat der Aufbau ca. 2 h gedauert. Gegen Ende der Löterei liess meine Konzentration nach und ich musste ein paar unsaubere Lötstellen mit Entlötlitze nachbehandeln. Besser eine Pause machen.

Technisch/elektrisch siehe unter Weblinks der Erfahrungsbericht von Marco Schmoll (www.controller-designs.de).

Nach der genauen Betrachtung mit einer Lupe und keinen Auffälligkeiten wurde eine 9V Gleichspannung an die Klemme J5 angelegt. LED NETZ leuchtet. LED1 und LED2 sind aus. Mein Netzteil kann den Strom anzeigen. Folgende Werte wurden beobachtet:

  • ca. 20 mA - AVR nicht eingesetzt, MAX232 nicht eingesetzt
  • ca. 22 mA - AVR nicht eingesetzt, MAX232 nicht eingesetzt, RESET gedrückt
  • ca. 25 mA - AVR nicht eingesetzt, MAX232 eingesetzt
  • ca. 26 mA - AVR nicht eingesetzt, MAX232 eingesetzt, RESET gedrückt
  • ca. 30 mA - ATmega8 eingesetzt, MAX232 eingesetzt

Mein Board v1.1 hat ein Problem mit PD7 bei der 28-Pol Fassung (Atmega8 u.a.). Da fehlt - nachgemessen mit Durchgangsprüfer - eine Verbindung zur PD7 an der 40-Pol-Fassung und zu dem entsprechenden Pin an der 40-Pol Wannenbuchse. Die Verbindung PD7-40-Pol zu der 40-pol Wannenbuchse ist vorhanden. Möglicherweise wurde das schon vorher beobachtet: http://robotikportal.de/phpBB2/viewtopic.php?p=321115#321115

Wenn ein 40-poliges IDE-Kabel in eigenen Schaltungen zum Verbinden mit J4 benutzt wird, vorher kontrollieren, ob im Kabel die benötigten Pins 1:1 verbunden/vorhanden sind (Danke an Markus [1], [2], [3])

Einstellen der Taktquelle

Die folgenden Kommandozeilen zur Einstellung der Taktquelle in den AVR Fuses beziehen sich auf einen Windows PC und die ISP Programmiersoftware AVRDUDE. Kann nützlich sein, dafür kleine Batchdateien zu schreiben.

Wenn ein Funkmodul installiert ist, kann es zu Problemen beim ISP-Programmieren kommen [4]. Dies kann sich auch beim Ändern der Fuses auswirken. Im schlimmsten Fall wird der AVR unbrauchbar. Vorher kontrollieren!

Serieller ISP auf dem Board

ATmega8 Fuses lesen

@echo off
echo Serieller ISP auf dem Board
echo Lese ATmega8 Fuses
d:\winavr\bin\avrdude -v -p atmega8 -c ponyser -P com1

Die Schnittstelle COM1 ist an die verwendete Schnittstelle auf dem PC anzupassen. Wichtig ist, dass kein zusätzlicher Parallelport-ISP angeschlossen ist. Wenn doch, wird der Atmega8 nicht erkannt!

ATmega8 1 MHz interner RC-Oszillator

@echo off
echo Serieller ISP auf dem Board
echo Setze ATmega8 Fuses auf 1 MHz interner RC-Oszillator
d:\winavr\bin\avrdude -v -p atmega8 -c ponyser -P com1 -U lfuse:w:0xC1:m -U hfuse:w:0xD9:m

ATmega8 12 MHz Quarz

Wenn das Board nach Anleitung aufgebaut wurde, d.h. in Q2 der 12,000 MHz Quarz eingesetzt wurde, kann man den ATmega8 auf max. 12 MHz einstellen:

@echo off
echo Serieller ISP auf dem Board
echo Setze ATmega8 Fuses auf 12 MHz Quarz
d:\winavr\bin\avrdude -v -p atmega8 -c ponyser -P com1 -U lfuse:w:0x2F:m -U hfuse:w:0xD9:m

Parallelport ISP Typ STK200

ATmega8 Fuses lesen

@echo off
echo Parallelport ISP Typ STK200
echo Lese ATmega8 Fuses
d:\winavr\bin\avrdude -v -p atmega8 -c stk200 -P lpt1
Werkseinstellung Fuses Atmega8 (Anzeige im AVR Fuse Calculator)

Die Schnittstelle LPT1 ist an die verwendete Schnittstelle auf dem PC anzupassen. Ebenso der Pfad zu dem Programm avrdude.exe.

Die ausgelesenen Fuses bei einem Fabrikneuen Atmega8 sollten dem Bild rechts entsprechen.

Siehe auch

  • AVR Fuse Calculator von Mark Hämmerling. In den dortigen Default-Einstellungen ist der Watchdog aktiviert. Lässt man das so, funktioniert das Programm Blinky (s.u.) nicht wie erwartet: Nur LED2 zappelt, LED1 ist meist aus, weil vor dem Umschalten von LED1 der Watchdog den Atmega8 resettet, d.h. das Programm von neuem starten lässt.

ATmega8 1 MHz interner RC-Oszillator

@echo off
echo Parallelport ISP Typ STK200
echo Setze ATmega8 Fuses auf 1 MHz interner RC-Oszillator
d:\winavr\bin\avrdude -v -p atmega8 -c stk200 -P lpt1 -U lfuse:w:0xC1:m -U hfuse:w:0xD9:m

ATmega8 12 MHz Quarz

Wenn das Board nach Anleitung aufgebaut wurde, d.h. in Q2 der 12,000 MHz Quarz eingesetzt wurde, kann man den ATmega8 auf max. 12 MHz einstellen:

@echo off
echo Parallelport ISP Typ STK200
echo Setze ATmega8 Fuses auf 12 MHz Quarz
d:\winavr\bin\avrdude -v -p atmega8 -c stk200 -P lpt1 -U lfuse:w:0x2F:m -U hfuse:w:0xD9:m

Programm ins Flash-ROM schreiben

Im folgenden wird angenommen, dass die zu programmierende Datei im iHEX-Format unter dem Namen atmega8.hex im aktuellen Verzeichnis befindet. Wenn ein Funkmodul installiert ist, kann es zu Problemen beim Flashen kommen [5].

Serieller ISP auf dem Board

@echo off
echo Serieller ISP auf dem Board
echo Programmiere Atmega8 Flash-ROM mit Datei atmega8.hex
d:\winavr\bin\avrdude -p atmega8 -c ponyser -P com1 -e -U flash:w:atmega8.hex

Parallelport ISP Typ STK200

@echo off
echo Parallelport ISP Typ STK200
echo Programmiere Atmega8 Flash-ROM mit Datei atmega8.hex
d:\winavr\bin\avrdude -p atmega8 -c stk200 -P lpt1 -e -U flash:w:atmega8.hex

Beispielprogramme

Blinky

Die beiden LED LED1 und LED2 auf dem Board sollen im 1s Takt wechselweise An und Aus gehen.

Die beiden On-board-LEDs sind active-high geschaltet, d.h. wenn am Pin des AVR ein logische 1 (HIGH Pegel) ausgegeben wird, leuchtet die LED. Wird eine logische 0 (LOW Pegel) ausgegeben, leuchtet die LED nicht. Das ist andersrum als im AVR Tutorial.

Beschaltung des Tasters und der LEDs

/*
    Atmega8
    Pollin Funk-AVR-Evaluationsboard v1.1

    Project -> Configuration Options in AVR Studio:
    Frequency:    1000000 bzw. 12000000
    Optimization: -Os
*/
#include <avr/io.h>
#include <util/delay.h>

// LEDs sind active-high geschaltet
#define LED_AN(LED)	(PORTD |=  (1<<(LED)))
#define LED_AUS(LED)	(PORTD &= ~(1<<(LED)))
#define LED_TOGGLE(LED)	(PORTD ^=  (1<<(LED)))
#define LED1		PD6
#define LED2		PD5
#define TASTER	        PB1

int main(void)
{
  DDRB &= ~(1<<TASTER);          // Port B: Eingang für Taster
  DDRD |= (1<<LED1) | (1<<LED2); // Port D: Ausgang für LED1 und LED2

  // Anfangseinstellung
  LED_AN(LED1);
  LED_AUS(LED2);

  while(1)
  {
    _delay_ms(1000);  // Wert 1000 erlaubt ab avr-libc 1.6
    LED_TOGGLE(LED1);
    LED_TOGGLE(LED2);
  }
}

Tasty

Wenn der On-board-Taster TASTER1 nicht gedrückt ist (Ruhezustand), soll die LED1 leuchten und LED2 soll nicht leuchten. Solange der User den Taster TASTER1 gedrückt hält, soll sich der Zustand der LEDs umkehren.

Der On-board-Taster TASTER1 ist ebenfalls active-high (siehe AVR-GCC-Tutorial) geschaltet, d.h. wenn der Taster geschlossen ist, liegt am Pin PB1 des AVR eine logische 1 (HIGH Pegel) an. Ist der Taster offen, liegt eine eine logische 0 (LOW Pegel) an. Das ist andersrum als im AVR Tutorial.

/*
    Atmega8
    Externer Quarz-Oszillator: 12 MHz

    Pollin Funk-AVR-Evaluationsboard v1.1
*/

#include <avr/io.h>
#include <util/delay.h>

// LEDs sind high-active geschaltet
#define LED_AN(LED)	(PORTD |=  (1<<(LED)))
#define LED_AUS(LED)	(PORTD &= ~(1<<(LED)))
#define LED_TOGGLE(LED)	(PORTD ^=  (1<<(LED)))
#define LED1		PD6
#define LED2		PD5

// TASTER ist high-active geschaltet 
#define TASTER	PB1
#define TASTER_GEDRUECKT()	(PINB & (1<<TASTER))

int main(void)
{
  DDRB &= ~(1<<TASTER);	         // Port B: Eingang für Taster
  DDRD |= (1<<LED1) | (1<<LED2); // Port D: Ausgang für LED1 und LED2

  while(1)
  {
    if (!TASTER_GEDRUECKT())
    {
      // Taster ist nicht (!) gedrückt
      LED_AN(LED1);
      LED_AUS(LED2);
    }
    else
    {
      // Taster ist gedrückt
      LED_AUS(LED1);
      LED_AN(LED2);
    }
  }
}

2-Bit Zähler

Jeder Tastendruck auf TASTER1 soll eine Variable um Eins hochzählen. Der Inhalt der unteren beiden Bits der Zählvariable soll mit den beiden LEDs angezeigt werden.

Das Hochzählen darf nur erfolgen, wenn ein Wechsel von Offen nach Geschlossen stattfindet. Das Programm muss also berücksichtigen, ob ein Wechsel von "Taster offen" zu "Taster geschlossen" stattfindet und ob der Taster in einer Position gehalten wird.

Mit diesem Beispiel kann man grob feststellen, ob der TASTER1 auf dem Board zum Prellen neigt, d.h. wenn sich der Zählerstand nicht wie gewollt pro Tastendruck um Eins erhöht und ob deshalb eine spezielle Routine zur Entprellung erforderlich ist.

Mein Board zeigt bei diesem Programm keine Neigung zum Prellen. Die auf dem Board vorhandene Hardwareentprellung über einen Tiefpassfilter mit C17 330 nF zwischen Taster und GND erfüllt hier ihren Zweck.

/*
    Atmega8
    Externer Quarz-Oszillator: 12 MHz

    Pollin Funk-AVR-Evaluationsboard v1.1
*/

#include <avr/io.h>
#include <util/delay.h>
#include <inttypes.h>

// LEDs sind high-active geschaltet
#define LED_AN(LED)	(PORTD |=  (1<<(LED)))
#define LED_AUS(LED)	(PORTD &= ~(1<<(LED)))
#define LED_TOGGLE(LED)	(PORTD ^=  (1<<(LED)))
#define LED1		PD6
#define LED2		PD5

// TASTER ist high-active geschaltet 
#define TASTER	PB1
#define TASTER_GEDRUECKT()	(PINB & (1<<TASTER))
#define TASTE_AUF 0
#define TASTE_ZU  1

void ausgabe(uint8_t wert)
{
  if (wert & (1<<0)) // Bit 0
    LED_AN(LED1);
  else
    LED_AUS(LED1);

  if (wert & (1<<1)) // Bit 1
    LED_AN(LED2);
  else
    LED_AUS(LED2);
}

int main(void)
{
  uint8_t alter_tastenzustand = TASTE_AUF;
  uint8_t zaehler = 0;

  DDRB &= ~(1<<TASTER);			// Port B: Eingang für Taster
  DDRD |= (1<<LED1) | (1<<LED2);	// Port D: Ausgang für LED1 und LED2

  while(1)
  {
    ausgabe(zaehler);

    if (TASTER_GEDRUECKT() && (alter_tastenzustand == TASTE_AUF))
    {
      // Wechsel von OFFEN nach GESCHLOSSEN
      zaehler++;
      alter_tastenzustand = TASTE_ZU;
    }

    if (!TASTER_GEDRUECKT())
      alter_tastenzustand = TASTE_AUF;
  }
}

8-Bit Zähler mit RS232-Anschluss

Das Beispiel 2-Bit Zähler soll jetzt ausgebaut werden. Im Detail soll der 8-Bit Zählerstand über RS232 auf einen PC ausgegeben werden. Ausserdem soll "ferngesteuert" eine Veränderung des Zählerstands vom PC aus möglich sein.

Auf der PC-Seite wird die Kommunikation mit einem Terminalprogramm z. B. HyperTerm gemacht, so dass hier keine Programmierung notwendig ist. Lediglich die Einstellung der RS232-Schnittstelle (z. B. COM1, 9600/8/N/1) muss passen. Auf der Atmega8-Seite sind zunächst wenige Grundfunktionen für die RS232-Kommunikation zu schreiben.

Die AVR-Grundfunktionen für die RS232-Kommunikation sind eine Funktion für die Initialisierung der µC-eigenen UART-Schnittstelle (Bsp.: UART_init) und eine Funktion für das Senden eines Zeichens (Bsp.: UART_putchar) sowie je eine Funktion für das Warten auf ein Zeichens (Bsp.: UART_getchar) bzw. eine nicht-wartende Funktion um festzustellen, ob ein Zeichen des PCs an der UART-Schnittstelle des µC anliegt (Bsp. UART_peekchar).

Als Programmablauf wurde die UART-Kommunikation mit der technisch relativ einfachen Methode Polling eingerichtet, d.h. das Programm fragt selbst möglichst regelmässig und oft genug für einen sinnvolle Kommunikation die UART Schnittstelle ab, ob Zeichen empfangen wurden oder gesendet werden können. Hier erklärt sich auch der Zweck für die nicht-wartende Funktion UART_peekchar() - wenn kein Zeichen vom PC über RS232 anliegt, soll der µC mit dem bekannten Programmablauf weitermachen, damit die Taster-Eingaben ausgewertet werden. Die technisch meistens vorteilhaftere Alternative wäre die Interruptmethode, d.h. das Hauptprogramm wird automatisch genau dann unterbrochen, wenn ein Zeichen empfangen wurde oder gesendet werden kann und der µC verzweigt in spezielle Grundfunktionen, die sog. Interrupthandler, die sich um die Kommunikation kümmern. Nach dem Abarbeiten der Unterbrechung geht es dann im eigentlichen Hauptprogramm weiter. Ein UART_peekchar-Trick ist bei dieser Programmierweise nicht nötig. Die Programmierweise mit UART-Interrupts könnte Teil eines künftigen Beispiels sein.

Die Grundfunktionen werden von einer allgemeinen Funktion (Bsp.: rs232) verwendet, um den Zählerstand an den PC auszugeben bzw. um Eingaben auf dem PC in einen neuen Zählerstand umzusetzen. Da die Funktion rs232 den Zählerstand ändern soll, wird ihr die Adresse der Zählervariable (&zaehler) übergeben, d.h. es wird hier mit Zeigern (Pointern) gearbeitet.

Per RS232 können komfortabel mehr Informationen ausgegeben werden als über die beiden LEDs. Im Beispiel wird der Zählerstand in drei verschiedenen Zahlensystemen ausgegeben und es wird der Zustand der beiden LEDs übermittelt. Der Clou hinsichtlich Bedienung des Zählers ist, dass per RS232 auch komfortabel mehr Bedienmöglichkeiten eingerichtet werden können als mit dem einfachen Taster: Zusätzlich zum Hochzählen (Plus-Taste) kann z. B. runtergezählt werden (Minus-Taste) oder es kann direkt eine bis zu dreistellige Zahlenfolge eingegeben werden, die zum Setzen des Zählerstands verwendet wird.

Bei der Initialisierung der UART wurden die Einstellungen 9600 Baud, 8 Datenbits, Keine Parity (No Parity) und 1 Stopbit gewählt. 8/N/1 ist ein gängiger Wert für RS232. Bei der Auswahl der Baudrate muss darauf geachtet werden, dass mit der gegebenen Taktrate auf dem Board (hier 12 MHz) nicht jede denkbare RS232-Baudrate gleich gut geeignet ist. Das Register zur Einstellung der Baudrate im µC kann nur mit ganzen Zahlen gefüllt werden, die dann benutzt werden, um durch Teilen die Taktrate der UART aus dem Takt der CPU abzuleiten. Bei einer CPU Taktrate von 12 MHz ergeben folgende Soll-Baudraten (in Baud) die angegebenen Einstellungen des Baudratenregisters (UBRR) und die Ist-Baudraten sowie die Abweichungen zwischen Soll- und Ist-Baudrate in Prozent: Die Formeln zur Berechnung stehen im Atmega8 Datenblatt im Kapitel UART.


Soll-Baudrate UBRR
(bei U2X=0)
Ist-Baudrate
(bei U2X=0)
Abweichung [%]
(bei U2X=0)
UBRR
(bei U2X=1)
Ist-Baudrate
(bei U2X=1)
Abweichung [%]
(bei U2X=1)
300 2499 300 0,0 - - -
1200 624 1200 0,0 1249 1200 0,0
2400 311 2403 0,1 624 2400 0,0
4800 155 4807 0,1 311 4807 0,1
9600 77 9615 0,2 155 9615 0,2
14,4K 51 14423 0,2 103 14423 0,2
19,2K 38 19230 0,2 77 19230 0,2
28,8K 25 28846 0,2 51 28846 0,2
38,4K 18 37473 2,8 38 38461 0,2
57,6K 12 57692 0,2 25 57692 0,2
76,8K 8 83333 8,5 16 78947 2,8
115,2K 5 125000 8,5 12 115384 0,2
230,4K 2 250000 8,5 5 250000 8,5
250K 2 250000 0,0 5 250000 0,0


Bestimmte Baudraten können ohne Abweichung oder mit tolerierbar kleiner Abweichung (max. 0,2%) eingestellt werden, während andere Baudraten immer zu grosse Abweichungen für eine fehlerfreie Kommunikation ergeben. Man kann obige Tabelle (von denen es ähnliche im Datenblatt für andere Taktraten gibt) verwenden oder man kann die UBRR Einstellung im Programmcode berechnen lassen (s. Programmcode unten). Im AVR-GCC-Tutorial ist zusätzlich ganz komfortabel eine Berechnung der Abweichung und eine Warnung bei zu grossen Werten angegeben, so dass man seine Wunschbaudrate in Richtung geringe Abweichung optimieren kann. Im folgenden Code wurde die gängige Baudrate 9600 Baud eingestellt.

Eine letzte grössere Änderung gegenüber dem 2-Bit-Zähler fällt an der Stelle der Taster-Eingabe auf: Jetzt mit der genaueren RS232-Ausgabe wurden Prelleffekte als unregelmässige Sprünge im Zählerstand beobachtet. Deshalb wurde Programmcode für eine einfache Entprellung nach dem Warteschleifenverfahren hinzugefügt. Wenn das Makro ENTPRELLUNG mit 0 definiert ist, entfällt die Entprellung, d.h. das Programm reagiert wie beim 2-Bit Zähler auf den Taster. Um die Entprellung zu aktivieren, wird das Makro ENTPRELLUNG z. B. mit 10, 25 oder 30 definiert und neu kompiliert und geflasht. Die Zahl im Makro gibt eine Wartezeit an, nach der der Zustand des Tasters nochmal geprüft wird. Hier kann man etwas experimentieren, was ein geeigneter Wert wäre.

/*
    Pollin Funk-AVR-Evaluationsboard v1.1
    Atmega8

    Externer Quarz-Oszillator: 12 MHz
    Optimierung: -Os

    RS232-Einstellungen:
    9600 Baud, 8 Bits, No Parity, 1 Stopbit
*/

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>

// LEDs sind high-active geschaltet
#define LED_AN(LED)     (PORTD |=  (1<<(LED)))
#define LED_AUS(LED)    (PORTD &= ~(1<<(LED)))
#define LED_TOGGLE(LED) (PORTD ^=  (1<<(LED)))
#define LED1            PD6
#define LED2            PD5

// TASTER ist high-active geschaltet
#define TASTER             PB1
#define TASTER_GEDRUECKT() (PINB & (1<<TASTER))
#define TASTE_AUF          0
#define TASTE_ZU           1
/* in Millisekunden */
#define ENTPRELLUNG        10

void UART_init(void)
{
    // Baudrate setzen
    // 9600 Baud bei F_CPU 12 MHz
    // (Baudratenfehler = +0,2%)
    UBRRH = (uint8_t) ((F_CPU / (16 * 9600L) - 1) >> 8);
    UBRRL = (uint8_t)  (F_CPU / (16 * 9600L) - 1);

    //      Empfangen,   Senden erlauben
    UCSRB = (1<<RXEN) | (1<<TXEN);

    // Frame Format:              8 Bits,                  No Parity,          1 Stopbit
    UCSRC = (1<<URSEL) | ((1<<UCSZ1) | (1<<UCSZ0)) | ((0<<UPM1) | (0<<UPM0)) | (0<<USBS);
}

// Auf Zeichen im UART Eingang prüfen
uint8_t UART_peekchar(void)
{
    // sofort zurückkehren und Zustand melden
    return UCSRA & (1<<RXC);
}

// Auf Zeichen im UART Eingang warten
uint8_t UART_getchar(void)
{
    // Warte bis UART Eingang voll
    while ( !(UCSRA & (1<<RXC)) )
        ;
    return UDR;
}

// Zeichen in UART Ausgang geben
void UART_putchar(char z)
{
    // Warte bis UART Ausgang frei
    while ( !(UCSRA & (1<<UDRE)) )
        ;
    UDR = z;
}

// Zeichenkette (String) in UART Ausgang geben
void UART_puts(char *s)
{
    while ( *s )
    {
        UART_putchar(*s);
        s++;
    }
}

#define EINGABE_START 	0
#define EINGABE_ABBRUCH EINGABE_START
#define EINGABE_MAX 		3
#define FORMFEED '\014'


void rs232(uint8_t *wert)
{
    static int16_t alter_wert = -1;
    uint8_t anzahl_ziffern;
    uint8_t zeichen;
    char puffer[23];

    // Eingabe über RS232

    // Ohne Warten prüfen, ob Zeichen an UART vorhanden
    if ( UART_peekchar() )
    {
        // Zeichen ist da.

        // Aktuellen Wert ausgeben

        // Neue Seite im Terminal anfordern.
        // Bewirkt ein Löschen des Bildschirms

        UART_putchar(FORMFEED);

        // Einleitender Text, was im folgenden ausgegeben wird
        UART_puts("Alter Z\204hler = "); // \204 ist ä in Oktalschreibweise

        // Zahl in Ziffernfolge umwandeln. Hier zuerst in Dezimalziffern
        itoa((int) *wert, puffer, 10);
        UART_puts(puffer);

        // Dann die gleiche Zahl in Hexadezimalziffern
        UART_puts(" 0x");
        itoa((int) *wert, puffer, 16);
        UART_puts(puffer);

        // Dann die gleiche Zahl in Binärziffern
        UART_puts(" 0b");
        itoa((int) *wert, puffer, 2);
        UART_puts(puffer);

        // Schliesslich noch der Zustand der LEDs auf dem Board senden
        UART_puts( (*wert & 2) ? " LED2-An " : " LED2-Aus");
        UART_puts( (*wert & 1) ? " LED1-An" : " LED1-Aus");

        // Zeilenabschluss zur Formatierung der Anzeige
        UART_puts("\r\n");

        // Neuen Wert eingeben

        // Initialisierung
        UART_puts("Eingabe> ");
        anzahl_ziffern = EINGABE_START;

        // Eingabeschleife
        do
        {
            // 1. bereits Zeichen am UART Eingang abholen
            // in weiteren Schleifen auf neue Zeichen warten
            zeichen = UART_getchar();

            // Zeichen auswerten
            if ( (zeichen == '-') && (anzahl_ziffern == EINGABE_START) )
            {
                // MINUS
                UART_puts(" -1");
                UART_puts("\r\n");
                *wert -= 1;
                break;
            }
            else if ( (zeichen == '+') && (anzahl_ziffern == EINGABE_START) )
            {
                // PLUS
                UART_puts(" +1");
                UART_puts("\r\n");
                *wert += 1;
                break;
            }
            else if ( (zeichen == '\n') || (zeichen == '\r') )
            {
                // ENTER oder RETURN: Abschluss der Eingabe
                break;
            }
            else if ( (zeichen >= '0') && (zeichen <= '9') )
            {
                // Eingabe einer Ziffer
                UART_putchar(zeichen); // Echo
                puffer[anzahl_ziffern++] = zeichen;
                puffer[anzahl_ziffern] = 0;
            }
            else
            {
                // Sonstiges Zeichen ist für uns illegal
                anzahl_ziffern = EINGABE_ABBRUCH;
                UART_puts(" Abbruch.");
                UART_puts("\r\n");
                break;
            }
        }
        while ( anzahl_ziffern < EINGABE_MAX );

        // Eingabe prüfen
        if ( anzahl_ziffern != EINGABE_ABBRUCH )
        {
            // Ziffern in Zahl umwandeln
            int i = atoi(puffer);

            // z.&nbsp;B. erlaubter Wertebereich sei 0 bis 255
            if ( (i >= 0) && (i < 256) )
            {
                UART_puts(" Ok.");
                UART_puts("\r\n");
                *wert = (uint8_t) i;
            }
            else
                UART_puts(" Ung\201ltig.\r\n"); // \201 ist ü in Oktalschreibweise
        }
    }

    // Ausgabe auf RS232 (s.oben)
    // bei nur Änderung des Wertes gegenüber der letzten Ausgabe
    //
    // alter_wert wurde mit -1 initialisiert. Das ist ausserhalb
    // des Wertebereichs von *wert, D.h. spätere Abfrage, ob
    // (alter_wert != *wert) ist, ist beim ersten Durchlauf wie
    // gewünscht wahr,
    //
    if (alter_wert != *wert)
    {
        alter_wert = *wert;
        UART_puts("Z\204hler = ");
        itoa((int) *wert, puffer, 10);
        UART_puts(puffer);
        UART_puts(" 0x");
        itoa((int) *wert, puffer, 16);
        UART_puts(puffer);
        UART_puts(" 0b");
        itoa((int) *wert, puffer, 2);
        UART_puts(puffer);
        UART_puts( (*wert & 2) ? " LED2-An " : " LED2-Aus");
        UART_puts( (*wert & 1) ? " LED1-An" : " LED1-Aus");
        UART_puts("\r\n");
    }
}

void ausgabe_led(uint8_t wert)
{
    if (wert & (1<<0)) // Bit 0
        LED_AN(LED1);
    else
        LED_AUS(LED1);

    if (wert & (1<<1)) // Bit 1
        LED_AN(LED2);
    else
        LED_AUS(LED2);
}

int main(void)
{
    uint8_t alter_tastenzustand = TASTE_AUF;
    uint8_t zaehler = 0;

    DDRB &= ~(1<<TASTER);           // Port B: Eingang für Taster
    DDRD |= (1<<LED1) | (1<<LED2);  // Port D: Ausgang für LED1 und LED2

    UART_init();
    UART_putchar(FORMFEED);

    while (1)
    {
        rs232(&zaehler);

        ausgabe_led(zaehler);

        if ( TASTER_GEDRUECKT() )
        {
#if ENTPRELLUNG
            _delay_ms(ENTPRELLUNG);
            if ( TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
            {
                // Wechsel von OFFEN nach GESCHLOSSEN?
                if ( alter_tastenzustand == TASTE_AUF )
                {
                    zaehler++;
                    alter_tastenzustand = TASTE_ZU;
                }
            }
        }

        if ( !TASTER_GEDRUECKT() )
        {
#if ENTPRELLUNG
            _delay_ms(ENTPRELLUNG);
            if ( !TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
                alter_tastenzustand = TASTE_AUF;
        }
    }
}

Zähler mit LCD-Anzeige

Als nächstes soll noch eine LCD-Anzeige installiert werden.

In meiner Bastelkiste lag noch ein Text-LCD mit dem LCD-Controller HD44780. Erfreulicherweise gibt es für diesen LCD-Controller jede Menge fertigen Beispielcode zur Ansteuerung im Internet u.a. im AVR-GCC-Tutorial.

Um den Beispielcode in der Form einzubinden, wie er im Tutorial zu finden ist und um das mittlerweise angewachsene Projekt übersichtlicher zu machen, werden zusammenhängende Codeteile in Unterdateien ausgelagert. Im folgenden Bild ist zu sehen, wie die einzelnen Dateien heissen und wie sie im AVR Studio eingebunden werden.


PFA Projekt ZLCD.png


Die Datei zaehler_lcd.c enthält die main-Funktion und die neue Ausgabefunktion ausgabe_lcd und die bereits bekannte Funktion ausgabe_led:

/*
    Pollin Funk-AVR-Evaluationsboard v1.1
    Atmega8

    Externer Quarz-Oszillator: 12 MHz
    Optimierung: -Os

    RS232-Einstellungen:
    9600 Baud, 8 Bits, No Parity, 1 Stopbit
*/
#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>

#include "funkhw.h"
#include "rs232.h"
#include "lcd_routines.h"

/* in Millisekunden */
#define ENTPRELLUNG        10

void ausgabe_lcd(uint8_t wert)
{
    static int16_t alter_wert = -1;
    char puffer[23];

    if (wert != alter_wert)
    {
        // Ausgabe nur bei Änderungen
        alter_wert = wert;
        itoa((int) wert, puffer, 10);

        lcd_clear();
        lcd_string("Zaehler = ");
        lcd_string(puffer);
    }
}

void ausgabe_led(uint8_t wert)
{
    if (wert & (1<<0)) // Bit 0
        LED_AN(LED1);
    else
        LED_AUS(LED1);

    if (wert & (1<<1)) // Bit 1
        LED_AN(LED2);
    else
        LED_AUS(LED2);
}

int main(void)
{
    uint8_t alter_tastenzustand = TASTE_AUF;
    uint8_t zaehler = 0;

    DDRB &= ~(1<<TASTER);           // Port B: Eingang für Taster
    DDRD |= (1<<LED1) | (1<<LED2);  // Port D: Ausgang für LED1 und LED2

    UART_init();
    UART_putchar(FORMFEED);

    lcd_init();

    while (1)
    {
        rs232(&zaehler);			// RS232 Eingabe und Ausgabe

        ausgabe_lcd(zaehler); // LCD Ausgabe

        ausgabe_led(zaehler); // LED Ausgabe

        if ( TASTER_GEDRUECKT() )
        {
#if ENTPRELLUNG
            _delay_ms(ENTPRELLUNG);
            if ( TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
            {
                // Wechsel von OFFEN nach GESCHLOSSEN?
                if ( alter_tastenzustand == TASTE_AUF )
                {
                    zaehler++;
                    alter_tastenzustand = TASTE_ZU;
                }
            }
        }

        if ( !TASTER_GEDRUECKT() )
        {
#if ENTPRELLUNG
            _delay_ms(ENTPRELLUNG);
            if ( !TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
                alter_tastenzustand = TASTE_AUF;
        }
    }
}

Die Datei lcd_routines.c enthält die Grundfunktionen für die LCD-Ansteuerung aus dem AVR-GCC-Tutorial.

In der Funktion lcd_enable musste das Timing leicht angepasst werden, weil bei meinem Display mit der 1 µs Pause Störungen aufgetreten waren.

// Ansteuerung eines HD44780 kompatiblen LCD im 4-Bit-Interfacemodus
// http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial
//
// Die Pinbelegung ist über defines in lcd-routines.h einstellbar

#include <avr/io.h>
#include "lcd_routines.h"
#include <util/delay.h>

// sendet ein Datenbyte an das LCD

void lcd_data(unsigned char temp1)
{
    unsigned char temp2 = temp1;

    LCD_PORT |= (1<<LCD_RS);        // RS auf 1 setzen

    temp1 = temp1 >> 4;
    temp1 = temp1 & 0x0F;
    LCD_PORT &= 0xF0;
    LCD_PORT |= temp1;               // setzen
    lcd_enable();

    temp2 = temp2 & 0x0F;
    LCD_PORT &= 0xF0;
    LCD_PORT |= temp2;               // setzen
    lcd_enable();

    _delay_us(42);
}

// sendet einen Befehl an das LCD

void lcd_command(unsigned char temp1)
{
    unsigned char temp2 = temp1;

    LCD_PORT &= ~(1<<LCD_RS);        // RS auf 0 setzen

    temp1 = temp1 >> 4;              // oberes Nibble holen
    temp1 = temp1 & 0x0F;            // maskieren
    LCD_PORT &= 0xF0;
    LCD_PORT |= temp1;               // setzen
    lcd_enable();

    temp2 = temp2 & 0x0F;            // unteres Nibble holen und maskieren
    LCD_PORT &= 0xF0;
    LCD_PORT |= temp2;               // setzen
    lcd_enable();

    _delay_us(42);
}

// erzeugt den Enable-Puls
void lcd_enable(void)
{
    // Bei Problemen ggf. Pause gemäß Datenblatt des LCD Controllers einfügen
    // http://www.mikrocontroller.net/topic/81974#685882
    LCD_PORT |= (1<<LCD_EN);
#if 1
    _delay_us(4); // kurze Pause, Original aus dem Tutorial ist bei meinem Display zu kurz!
#else
    _delay_us(1); // kurze Pause
#endif
    // Bei Problemen ggf. Pause gemäß Datenblatt des LCD Controllers verlängern
    // http://www.mikrocontroller.net/topic/80900
    LCD_PORT &= ~(1<<LCD_EN);
}

// Initialisierung:
// Muss ganz am Anfang des Programms aufgerufen werden.

void lcd_init(void)
{
    LCD_DDR = LCD_DDR | 0x0F | (1<<LCD_RS) | (1<<LCD_EN);   // Port auf Ausgang schalten

    // muss 3mal hintereinander gesendet werden zur Initialisierung

    _delay_ms(15);
    LCD_PORT &= 0xF0;
    LCD_PORT |= 0x03;
    LCD_PORT &= ~(1<<LCD_RS);      // RS auf 0
    lcd_enable();

    _delay_ms(5);
    lcd_enable();

    _delay_ms(1);
    lcd_enable();
    _delay_ms(1);

    // 4 Bit Modus aktivieren
    LCD_PORT &= 0xF0;
    LCD_PORT |= 0x02;
    lcd_enable();
    _delay_ms(1);

    // 4Bit / 2 Zeilen / 5x7
    lcd_command(0x28);

    // Display ein / Cursor aus / kein Blinken
    lcd_command(0x0C);

    // inkrement / kein Scrollen
    lcd_command(0x06);

    lcd_clear();
}

// Sendet den Befehl zur Löschung des Displays

void lcd_clear(void)
{
    lcd_command(CLEAR_DISPLAY);
    _delay_ms(5);
}

// Sendet den Befehl: Cursor Home

void lcd_home(void)
{
    lcd_command(CURSOR_HOME);
    _delay_ms(5);
}

// setzt den Cursor in Zeile y (1..4) Spalte x (0..15)

void set_cursor(uint8_t x, uint8_t y)
{
    uint8_t tmp;

    switch (y)
    {
    case 1:
        tmp=0x80+0x00+x;
        break;    // 1. Zeile
    case 2:
        tmp=0x80+0x40+x;
        break;    // 2. Zeile
    case 3:
        tmp=0x80+0x10+x;
        break;    // 3. Zeile
    case 4:
        tmp=0x80+0x50+x;
        break;    // 4. Zeile
    }
    lcd_command(tmp);
}

// Schreibt einen String auf das LCD

void lcd_string(char *data)
{
    while (*data)
    {
        lcd_data(*data);
        data++;
    }
}

Die Includedatei lcd_routines.h enthält die Leitungszuordnung für die LCD-Ansteuerung und die Prototypen für die LCD Funktionen.

Hier wurden die Signalleitungen von PORTD auf PORTC geändert. Und die Definition von F_CPU wurde mit #if/#endif auskommentiert, weil dieser Wert bereits mit den Projekt Configuration Options im AVR Studio auf 12 MHz gesetzt ist.

// Ansteuerung eines HD44780 kompatiblen LCD im 4-Bit-Interfacemodus
// http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial
//
void lcd_data(unsigned char temp1);
void lcd_string(char *data);
void lcd_command(unsigned char temp1);
void lcd_enable(void);
void lcd_init(void);
void lcd_home(void);
void lcd_clear(void);
void set_cursor(uint8_t x, uint8_t y);

#if 0
// Hier die verwendete Taktfrequenz in Hz eintragen, wichtig!
#define F_CPU 8000000
#endif

// LCD Befehle

#define CLEAR_DISPLAY 0x01
#define CURSOR_HOME   0x02

// Pinbelegung für das LCD, an verwendete Pins anpassen

#if 1
// Änderung des LCD Anschlusses
// Pollin Funk AVR Evaluationsboard v1.1
// DB4 bis DB7 des LCD sind mit PC0 bis PC3 des AVR verbunden
#define LCD_PORT      PORTC
#define LCD_DDR       DDRC
#define LCD_EN        PC5
#define LCD_RS        PC4
#define LCD_DB7       PC3
#define LCD_DB6       PC2
#define LCD_DB5       PC1
#define LCD_DB4       PC0
#else
#define LCD_PORT      PORTD
#define LCD_DDR       DDRD
#define LCD_RS        PD4
#define LCD_EN        PD5
// DB4 bis DB7 des LCD sind mit PD0 bis PD3 des AVR verbunden
#endif

Die Includedatei funkhw.h enthält die gemeinsamen, boardspezifischen Port-Definitionen, die in mehreren Unterdateien benötigt werden:

/*
    Pollin Funk-AVR-Evaluationsboard v1.1
    Atmega8
*/

// LEDs sind high-active geschaltet
#define LED_AN(LED)     (PORTD |=  (1<<(LED)))
#define LED_AUS(LED)    (PORTD &= ~(1<<(LED)))
#define LED_TOGGLE(LED) (PORTD ^=  (1<<(LED)))
#define LED1            PD6
#define LED2            PD5

// TASTER ist high-active geschaltet
#define TASTER             PB1
#define TASTER_GEDRUECKT() (PINB & (1<<TASTER))
#define TASTE_AUF          0
#define TASTE_ZU           1

Die Includedatei rs232.h enthält die Prototypen für die Funktionen in rs232.c:

#ifndef FORMFEED
#define FORMFEED '\014'
#endif

void UART_init(void);

// Auf Zeichen im UART Eingang prüfen
uint8_t UART_peekchar(void);

// Auf Zeichen im UART Eingang warten
uint8_t UART_getchar(void);

// Zeichen in UART Ausgang geben
void UART_putchar(char z);

// Zeichenkette (String) in UART Ausgang geben
void UART_puts(char *s);

void rs232(uint8_t *wert);

Und zum Schluss noch die Datei rs232.c mit den ungeänderten UART-Grundfunktionen und der rs232-Funktion.

/*
    Pollin Funk-AVR-Evaluationsboard v1.1
    Atmega8

    Externer Quarz-Oszillator: 12 MHz
    Optimierung: -Os

    RS232-Einstellungen:
    9600 Baud, 8 Bits, No Parity, 1 Stopbit
*/
#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>

#include "funkhw.h"
#include "rs232.h"

#define EINGABE_START 	0
#define EINGABE_ABBRUCH EINGABE_START
#define EINGABE_MAX 		3

void UART_init(void)
{
    // Bausrate setzen
    // 9600 Baud bei F_CPU 12 MHz
    // (Baudratenfehler = +0,2%)
    UBRRH = (uint8_t) ((F_CPU / (16 * 9600L) - 1) >> 8);
    UBRRL = (uint8_t)  (F_CPU / (16 * 9600L) - 1);

    //      Empfangen,   Senden erlauben
    UCSRB = (1<<RXEN) | (1<<TXEN);

    // Frame Format:              8 Bits,                  No Parity,          1 Stopbit
    UCSRC = (1<<URSEL) | ((1<<UCSZ1) | (1<<UCSZ0)) | ((0<<UPM1) | (0<<UPM0)) | (0<<USBS);
}

// Auf Zeichen im UART Eingang prüfen
uint8_t UART_peekchar(void)
{
    // sofort zurückkehren und Zustand melden
    return UCSRA & (1<<RXC);
}

// Auf Zeichen im UART Eingang warten
uint8_t UART_getchar(void)
{
    // Warte bis UART Eingang voll
    while ( !(UCSRA & (1<<RXC)) )
        ;
    return UDR;
}

// Zeichen in UART Ausgang geben
void UART_putchar(char z)
{
    // Warte bis UART Ausgang frei
    while ( !(UCSRA & (1<<UDRE)) )
        ;
    UDR = z;
}

// Zeichenkette (String) in UART Ausgang geben
void UART_puts(char *s)
{
    while ( *s )
    {
        UART_putchar(*s);
        s++;
    }
}

void rs232(uint8_t *wert)
{
    static int16_t alter_wert = -1;
    uint8_t anzahl_ziffern;
    uint8_t zeichen;
    char puffer[23];

    // Eingabe über RS232

    // Ohne Warten prüfen, ob Zeichen an UART vorhanden
    if ( UART_peekchar() )
    {
        // Zeichen ist da.

        // Aktuellen Wert ausgeben

        // Neue Seite im Terminal anfordern.
        // Bewirkt ein Löschen des Bildschirms

        UART_putchar(FORMFEED);

        // Einleitender Text, was im folgenden ausgegeben wird
        UART_puts("Alter Z\204hler = "); // \204 ist ä in Oktalschreibweise

        // Zahl in Ziffernfolge umwandeln. Hier zuerst in Dezimalziffern
        itoa((int) *wert, puffer, 10);
        UART_puts(puffer);

        // Dann die gleiche Zahl in Hexadezimalziffern
        UART_puts(" 0x");
        itoa((int) *wert, puffer, 16);
        UART_puts(puffer);

        // Dann die gleiche Zahl in Binärziffern
        UART_puts(" 0b");
        itoa((int) *wert, puffer, 2);
        UART_puts(puffer);

        // Schliesslich noch der Zustand der LEDs auf dem Board senden
        UART_puts( (*wert & 2) ? " LED2-An " : " LED2-Aus");
        UART_puts( (*wert & 1) ? " LED1-An" : " LED1-Aus");

        // Zeilenabschluss zur Formatierung der Anzeige
        UART_puts("\r\n");

        // Neuen Wert eingeben

        // Initialisierung
        UART_puts("Eingabe> ");
        anzahl_ziffern = EINGABE_START;

        // Eingabeschleife
        do
        {
            // 1. bereits Zeichen am UART Eingang abholen
            // in weiteren Schleifen auf neue Zeichen warten
            zeichen = UART_getchar();

            // Zeichen auswerten
            if ( (zeichen == '-') && (anzahl_ziffern == EINGABE_START) )
            {
                // MINUS
                UART_puts(" -1");
                UART_puts("\r\n");
                *wert -= 1;
                break;
            }
            else if ( (zeichen == '+') && (anzahl_ziffern == EINGABE_START) )
            {
                // PLUS
                UART_puts(" +1");
                UART_puts("\r\n");
                *wert += 1;
                break;
            }
            else if ( (zeichen == '\n') || (zeichen == '\r') )
            {
                // ENTER oder RETURN: Abschluss der Eingabe
                break;
            }
            else if ( (zeichen >= '0') && (zeichen <= '9') )
            {
                // Eingabe einer Ziffer
                UART_putchar(zeichen); // Echo
                puffer[anzahl_ziffern++] = zeichen;
                puffer[anzahl_ziffern] = 0;
            }
            else
            {
                // Sonstiges Zeichen ist für uns illegal
                anzahl_ziffern = EINGABE_ABBRUCH;
                UART_puts(" Abbruch.");
                UART_puts("\r\n");
                break;
            }
        }
        while ( anzahl_ziffern < EINGABE_MAX );

        // Eingabe prüfen
        if ( anzahl_ziffern != EINGABE_ABBRUCH )
        {
            // Ziffern in Zahl umwandeln
            int i = atoi(puffer);

            // z.&nbsp;B. erlaubter Wertebereich sei 0 bis 255
            if ( (i >= 0) && (i < 256) )
            {
                UART_puts(" Ok.");
                UART_puts("\r\n");
                *wert = (uint8_t) i;
            }
            else
                UART_puts(" Ung\201ltig.\r\n"); // \201 ist ü in Oktalschreibweise
        }
    }

    // Ausgabe auf RS232 (s.oben)
    // bei nur Änderung des Wertes gegenüber der letzten Ausgabe
    //
    // alter_wert wurde mit -1 initialisiert. Das ist ausserhalb
    // des Wertebereichs von *wert, D.h. spätere Abfrage, ob
    // (alter_wert != *wert) ist, ist beim ersten Durchlauf wie
    // gewünscht wahr,
    //
    if (alter_wert != *wert)
    {
        alter_wert = *wert;
        UART_puts("Z\204hler = ");
        itoa((int) *wert, puffer, 10);
        UART_puts(puffer);
        UART_puts(" 0x");
        itoa((int) *wert, puffer, 16);
        UART_puts(puffer);
        UART_puts(" 0b");
        itoa((int) *wert, puffer, 2);
        UART_puts(puffer);
        UART_puts( (*wert & 2) ? " LED2-An " : " LED2-Aus");
        UART_puts( (*wert & 1) ? " LED1-An" : " LED1-Aus");
        UART_puts("\r\n");
    }
}

Uhr

Nachdem die Eingabe- und Ausgaberoutinen vorhanden sind, sollen jetzt die weiteren Innereien des ATmega8 erkundet werden. Als erstes kommt der Timer und damit die erste Interrupt-Programmierung an die Reihe. Und was liegt bei dem Thema näher, als eine Uhr zu programmieren...

Zunächst der Aufbau des Projektes:

  • PFA_uhr.c - Das Hauptprogramm
  • PFA_funkhw.h - Definitionen für das PFA ;-) Board
  • PFA_rs232.c - Erweiterte rs232-Funktionen
  • PFA_rs232.h - Prototypen zu den rs232-Funktionen
  • PFA_uart.c - UART Grundfunktionen (aus ehemaligem rs232.c herausgezogen)
  • PFA_uart.h - Prototypen zu den UART Grundfunktionen
  • lcd-routines.c - Unverändert s.o.
  • lcd-routines.h - Unverändert s.o.

PFA_uhr.c - Das Hauptprogramm

Drei Funktionen werden für die Uhr benötigt. Selbstverständlich eine Funktion zum Anzeigen der Uhrzeit auf dem LCD (Bsp.: uhrzeit_lcd). Und eine Funktion zum Starten der Uhr (Bsp.: uhr_init).

Sowie eine TickTack-Funktion, die regelmässig die Uhrzeit erhöht und zwar egal was das Programm sonst macht (Tasteneingabe, RS232 Ausgabe...)! Diese letzte Funktion heisst ISR(TIMER0_OVF_vect) und ist eine sog. Interrupt Service Routine für den TIMER0 OVerFlow Interrupt. Die Namen solcher ISR Funktionen sind nicht frei wählbar, sondern den einzelnen Interrupts fest zugeordnet. Wie die ISRs heissen, kann man in der Dokumentation zum jeweiligen AVR nachlesen.

Die Auswahl von TIMER0 ist willkürlich. Der ATmega8 hat insgesamt drei Timer, die unterschiedliche Fähigkeiten haben. TIMER0 ist davon der mit der geringsten Ausstattung, aber selbst die ist noch ausreichend, um damit eine Uhr zu programmieren. So können die anderen Timer für anderes freigehalten werden.

TIMER0 ist ein 8-Bit Timer, d.h. er kann so einstellt werden, dass er ab einem Startwert TCNT0 jedesmal um eins hochzählt, wenn er von seiner Taktquelle angetrieben wird. Die Taktquelle für den TIMER0 kann die Taktquelle des AVR sein oder ein durch den sog. Vorteiler (Prescaler) von 8, 64, 256 oder 1024 geteilte CPU-Takt. Welcher Vorteiler verwendet werden soll, wird im Register TCCR0 eingestellt.

Wenn der Endwert 255 (8-Bit!) überschritten wird und die Hochzählerei wieder bei 0 beginnt, kann eine Überlaufunterbrechung (Overflow Interrupt) ausgelöst werden, wenn das vom Programm aus im Register TIMSK so eingerichtet wurde.

Damit beim ersten Einschalten des Timers nicht sofort ein Interrupt ausgelöst wird, löscht man meistens vor dem Einschalten das betreffende sog. Interruptflag (hier im Register TIFR). Das Interruptflag ist innerhalb der CPU dafür entscheidend, ob beim nächsten abzuarbeitenden Maschinenbefehl das Hauptprogramm ausgeführt wird oder ob in die ISR verzweigt wird.

Das Einschalten selbst ist ein zweistufiger Prozess: zunächst stellt man in dem Register TIMSK ein, welchen Interrupt man erlauben will. Anschliessend werden mit der Funktion sei, alle (d.h. global) erlaubten Interrupts auch von der CPU abgeprüft. Die Gegenfunktion zu sei, d.h. Interrupts global sperren wäre die Funktion cli, aber die wird in diesem Beispiel nicht benötigt.

Jetzt geht's ans Rechnen, wie der TIMER0 genau eingestellt werden muss, damit man damit auch eine Uhr programmieren kann.

In der folgenden Tabelle ist abhängig vom Prescaler P (Spalte 1) berechnet, welchen Takt T man erhält, wenn die F_CPU 12 MHz beträgt (zweite Spalte). In der dritten Spalte D1 ist berechnet, wie lange es dauert, wenn der Timer um eins hochzählt. Bzw. in der vierten Spalte bei D2, wenn nach 256 Zählschritten (0 bis 255 dann +1 gibt Überlauf zu 0) der Overflow Interrupt auftritt. In der fünften Spalte N256 ist berechnet, wieviele Overflow-Interrupts man mitzählen müsste, um auf 1 Sekunde zu kommen.

P = Prescaler T = Taktrate [Hz] 1.000.000/(T/P) = D1 = Dauer für +1 [µs] (1.000/(T/P))*256 = D2 = Dauer bis Overflow [ms] 1000/((1.000/(T/P))*256) = N256 = Anzahl Overflows für 1 s
1 12.000.000 0,083 0,021 46875
8 1.500.000 0,667 0,171 5859,38
64 187.500 5,333 0,171 732,42
256 46.875 21,333 8,333 183,11
1024 11.718,75 85,333 21,845 45,78

In diesem Fall wird nur beim Prescaler 1 eine ganze Zahl von Overflow-Interrupts durchlaufen, so dass man nach einer ganzen Zahl von Zählschritten glatt auf 1 Sekunde kommt. Aber wenn man mit diesem Prescaler 1 arbeitet, treten die Interrupts zigtausend Mal auf, so dass das Programm nur noch am sehr schnellen Hochzählen und Aufrufen des Interrupts ist... das ist keine Lösung.

Aber es gibt zwei Auswege für die anderen Prescaler-Werte. Denn man kann ja vorgeben, ab welchem Startwert in Richtung 255 hochgezählt wird und damit kann man festlegen, wieviele Schritte bis zum Overflow vergehen. Vielleicht kann man die Anzahl so geschickt wählen, dass eine ganze Zahl von Overflow Interrupts glatt 1 Sekunde ergibt? Gesucht ist also eine Zahl X, bei der gilt, dass N1/X eine ganze Zahl ergibt. Klar X=1 ist fast immer eine Lösung, nur werden die Zahlen und damit der Zeitverbrauch in der Interruptroutine gigantisch...

P = Prescaler T/P = N1 = Anzahl +1 Schritte für 1 s
1 12.000.000
8 1.500.000
64 187.500
256 46.875
1024 11.718,75

Spielt man es für die interessanten niedrigen Taktraten durch, kann man beim Prescaler 1024 leider keine Zahl X finden, bei der 11.718,75/X ganzzahlig ist. Aber beim Prescaler 256 gibt es mehrere Lösungen:

X 46875/X =
1 46875
3 15625
5 9375
15 3125
25 1875
75 625
125 375

Wählt man also den Startwert so, dass der TIMER0 125 mal hochzählen muss, bis ein Overflow auftritt, hat man nach 375 Overflows genau eine Sekunde lang abgezählt. Um das Einzurichten muss also der TCNT0 Startwert nicht auf 0 (= 256 = 256-0 Schritte bis Overflow) gesetzt werden, sondern auf 256-125 (= -125 bei 8-Bit Rechnung, da dann 256 = 0 gilt). Und der Startwert muss bei jedem Overflow Interrupt wieder auf den berechneten Startwert gesetzt werden. Das kann man durch geschickte Wahl der Timer-Einstellung automatisch machen lassen und erhält das genauste Verfahren.

Aber es gibt noch eine andere (möglicherweise ungenauere) Lösung und die wird im folgenden Programm benutzt:

Man macht so viele 256 Schritt-Overflows, wie es geht, ohne die Sekunde zu überschreiten und für den letzten Rest verringert man die Anzahl der Schritte bis zum Overflow.

Beim Prescaler 256 und F_CPU 12 MHz würden sich in einer Sekunde

[math]\displaystyle{ \frac{12\,000\,000}{256 \cdot 256} = 183{,}10546875 }[/math]

256-Schritt Overflow Interrupts ergeben. Das kann man in 183 volle 256-Schritt Overflows plus 0,10546875 * 256 = 27 Stück 1-Schritt-Overflows bzw. ein 27-Schritt Overflow aufteilen. Insgesamt also 184 Overflow-Interrupts. Im 183. Interrupt muss der Startwert geändert werden und im 184. Interrupt kann der Sekundenzähler erhöht werden.

Die Ungenauigkeit kann in das Programm, weil der Timer ja weiterläuft, während sich das Programm in der ISR befindet. Wenn dann der Startwert vom Programm aus gesetzt wird, berücksichtigt das nicht den Wert der schon bis dahin gezählt ist. Mit dem Makro GENAUIGKEIT kann man in der Richtung etwas experimentieren...

Genug Zahlenspielerei. Der Rest des Programmes ist wieder anschaulicher. Die bereits bekannte Funktion rs232 wurde in die Funktion rs232_io geändert, so dass die Eingabe von Stunden, Minuten und Sekunden möglich ist. Was per RS232 eingegeben werden soll, kann mit dem TASTER1 bestimmt werden: 1. Tastendruck Sekunden, 2. Tastendruck Minuten, 3. Tastendruck Stunden. 4. Tastendruck Anzeige Uhrzeit auf RS232.

//
// Pollin Funk-AVR-Evaluationsboard v1.1
// PFA_uhr.c
// v 1.0
//

/*
    Atmega8

    Externer Quarz-Oszillator: 12 MHz
    Optimierung: -Os

    RS232-Einstellungen:
    9600 Baud, 8 Bits, No Parity, 1 Stopbit
*/

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
#include <avr/interrupt.h>

#include "PFA_funkhw.h"
#include "PFA_rs232.h"
#include "lcd-routines.h"

/* in Millisekunden */
#define ENTPRELLUNG        10

volatile uint8_t stunden;
volatile uint8_t minuten;
volatile uint8_t sekunden;

//
// 0: 183x256 + 1x27
// 1: 183x256 + 1x27 durchlaufendes TCNT0 berücksichtigt
// 2: 325x125
//
#define GENAUIGKEIT 0

#if ( GENAUIGKEIT == 1 )
volatile uint8_t tcnt0;
#endif

#if ( GENAUIGKEIT == 2 )
#define RELOAD 	125
#define COUNT 	375
#endif

ISR(TIMER0_OVF_vect)
{
#if ( GENAUIGKEIT == 2 )
    static uint16_t ticks = 0;

    TCNT0 = 256 - RELOAD;

    ticks += 1;

    if ( ticks == COUNT )
    {
        // 1 Sekunde ist um.
        sekunden += 1;

        if ( sekunden == 60 )
        {
            sekunden = 0;
            minuten += 1;
        }

        if ( minuten == 60 )
        {
            minuten = 0;
            stunden += 1;
        }

        if ( stunden == 24 )
            stunden = 0;

        // nächste Runde
        ticks = 0;
    }
#else
    static uint8_t ticks = 0;

    ticks += 1;

    if ( ticks == 183 )
    {
        // Letzter Zeitabschnitt kürzer, um auf 1s zu kommen
#if ( GENAUIGKEIT == 1 )
        tcnt0 = TCNT0;
        TCNT0 = 256 - 27 + TCNT0;
#else
        TCNT0 = 256 - 27;
#endif
    }
    else if ( ticks == 184 )
    {
        // 1 Sekunde ist um.
        sekunden += 1;

        if ( sekunden == 60 )
        {
            sekunden = 0;
            minuten += 1;
        }

        if ( minuten == 60 )
        {
            minuten = 0;
            stunden += 1;
        }

        if ( stunden == 24 )
            stunden = 0;

        // nächste Runde
        ticks = 0;
    }
#endif
}


void uhr_init(void)
{
    // Rrescaler 256
    TCCR0 = (1<<CS02) | (0<<CS01) | (0<<CS00);

    // Zählregister
#if ( GENAUIGKEIT == 2 )
    TCNT0 = 256 - RELOAD;
#else
    TCNT0 = 0;
#endif

    // Overflow-Flag löschen
    TIFR = (1<<TOV0);

    // Timer0 Overflow enable
    TIMSK |= (1<<TOIE0);

    // Interrupts global einschalten
    sei();
}


void uhrzeit_lcd(void)
{
    static int16_t alter_wert = -1;
    char puffer[4]; // max. 3 Ziffern (uint8_t) plus 1 Nullbyte

    if ( sekunden != alter_wert )
    {
        // Ausgabe nur bei Änderungen
        alter_wert = sekunden;

        lcd_clear();

        itoa((int) stunden+100, puffer, 10);
        lcd_string(&puffer[1]);

        lcd_string(" : ");

        itoa((int) minuten+100, puffer, 10);
        lcd_string(&puffer[1]);

        lcd_string(" : ");

        itoa((int) sekunden+100, puffer, 10);
        lcd_string(&puffer[1]);
    }
}


void ausgabe_led(uint8_t wert)
{
    if (wert & (1<<0)) // Bit 0
        LED_AN(LED1);
    else
        LED_AUS(LED1);

    if (wert & (1<<1)) // Bit 1
        LED_AN(LED2);
    else
        LED_AUS(LED2);
}


int main(void)
{
    uint8_t alter_tastenzustand = TASTE_AUF;
    uint8_t zaehler = 0;

    DDRB &= ~(1<<TASTER);           // Port B: Eingang für Taster
    DDRD |= (1<<LED1) | (1<<LED2);  // Port D: Ausgang für LED1 und LED2

    UART_init();
    UART_putchar(FORMFEED);

    lcd_init();

    uhr_init();

    while (1)
    {
        // Bekannte Uhrzeit auf LCD ausgeben
        uhrzeit_lcd();

        // ggf. Kommando von TASTER1 einlesen
        if ( TASTER_GEDRUECKT() )
        {
#if ENTPRELLUNG
            _delay_ms(ENTPRELLUNG);
            if ( TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
            {
                // Wechsel von OFFEN nach GESCHLOSSEN?
                if ( alter_tastenzustand == TASTE_AUF )
                {
                    zaehler++;
                    alter_tastenzustand = TASTE_ZU;
                }
            }
        }

        if ( !TASTER_GEDRUECKT() )
        {
#if ENTPRELLUNG
            _delay_ms(ENTPRELLUNG);
            if ( !TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
                alter_tastenzustand = TASTE_AUF;
        }

        // Feedback TASTER1 auf LEDs
        ausgabe_led(zaehler); // LED Ausgabe

        // Je nach TASTER1-Eingabe Uhrzeit per RS232 ändern oder anzeigen
        switch (zaehler % 4)
        {
        case 3:
            rs232_io("Stunden = ", &stunden, 0, 23);
            break;

        case 2:
            rs232_io("Minuten = ", &minuten, 0, 59);
            break;

        case 1:
            rs232_io("Sekunden = ", &sekunden, 0, 59);
            break;

        case 0:
        default:
#if ( GENAUIGKEIT == 1 )
            if ( tcnt0 != 0 )
            {
                char puffer[4];
                UART_puts("TCNT0 = ");
                itoa((int) tcnt0, puffer, 10);
                UART_puts(puffer);
                UART_puts("\r\n");
            }
#endif
            rs232_uhrzeit_zeigen(stunden, minuten, sekunden);
            break;
        }
    }
}

// EOF PFA_uhr.c

PFA_funkhw.h - Definitionen für das PFA ;-) Board

//
// Pollin Funk-AVR-Evaluationsboard v1.1
// PFA_funkhw.h
// v 1.0
//

/*
    Atmega8
*/

// LEDs sind high-active geschaltet
#define LED_AN(LED)     (PORTD |=  (1<<(LED)))
#define LED_AUS(LED)    (PORTD &= ~(1<<(LED)))
#define LED_TOGGLE(LED) (PORTD ^=  (1<<(LED)))
#define LED1            PD6
#define LED2            PD5

// TASTER ist high-active geschaltet
#define TASTER             PB1
#define TASTER_GEDRUECKT() (PINB & (1<<TASTER))
#define TASTE_AUF          0
#define TASTE_ZU           1

// EOF PFA_funkhw.h

PFA_rs232.c - Erweiterte rs232-Funktionen

//
// Pollin Funk-AVR-Evaluationsboard v1.1
// PFA_rs232.c
// v 1.0
//

#include "PFA_rs232.h"


static void wert_ausgeben(char *s, uint8_t wert)
{
    char puffer[4]; // uint8_t hat max. 3 Dezimalstellen plus 1 Nullbyte = 4 Bytes

    // Einleitender Text, was im folgenden ausgegeben wird
    UART_puts(s);

    // Zahl in Ziffernfolge umwandeln. Hier zuerst in Dezimalziffern
    itoa((int) wert, puffer, 10);
    UART_puts(puffer);

    // Zeilenabschluss zur Formatierung der Anzeige
    UART_puts("\r\n");
}


void rs232_io(char *s, uint8_t *wert, uint8_t min, uint8_t max)
{
    static int16_t alter_wert = -1;
    uint8_t anzahl_ziffern;
    uint8_t zeichen;
    char puffer[EINGABE_MAX];

    // Eingabe über RS232

    // Ohne Warten prüfen, ob Zeichen an UART vorhanden
    if ( UART_peekchar() )
    {
        // Zeichen ist da.

        // Aktuellen Wert ausgeben

        // Neue Seite im Terminal anfordern.
        // Bewirkt ein Löschen des Bildschirms
        UART_putchar(FORMFEED);

        wert_ausgeben(s, *wert);

        // Neuen Wert eingeben

        // Initialisierung
        UART_puts("Eingabe> ");
        anzahl_ziffern = EINGABE_START;

        // Eingabeschleife
        do
        {
            // 1. bereits Zeichen am UART Eingang abholen
            // in weiteren Schleifen auf neue Zeichen warten
            zeichen = UART_getchar();

            // Zeichen auswerten
            if ( (zeichen == '-') && (anzahl_ziffern == EINGABE_START) )
            {
                // MINUS
                UART_puts(" -1");
                UART_puts("\r\n");
                if (*wert > min)
                    *wert -= 1;
                break;
            }
            else if ( (zeichen == '+') && (anzahl_ziffern == EINGABE_START) )
            {
                // PLUS
                UART_puts(" +1");
                UART_puts("\r\n");
                if (*wert < max)
                    *wert += 1;
                break;
            }
            else if ( (zeichen == '\n') || (zeichen == '\r') )
            {
                // ENTER oder RETURN: Abschluss der Eingabe
                break;
            }
            else if ( (zeichen >= '0') && (zeichen <= '9') )
            {
                // Eingabe einer Ziffer
                UART_putchar(zeichen); // Echo
                puffer[anzahl_ziffern++] = zeichen;
                puffer[anzahl_ziffern] = 0;
            }
            else
            {
                // Sonstiges Zeichen ist für uns illegal
                anzahl_ziffern = EINGABE_ABBRUCH;
                UART_puts(" Abbruch.");
                UART_puts("\r\n");
                break;
            }
        }
        while ( anzahl_ziffern < EINGABE_MAX );

        // Eingabe prüfen
        if ( anzahl_ziffern != EINGABE_ABBRUCH )
        {
            // Ziffern in Zahl umwandeln
            int i = atoi(puffer);

            // z.&nbsp;B. erlaubter Wertebereich
            if ( (i >= min) && (i <= max) )
            {
                UART_puts(" Ok.");
                UART_puts("\r\n");
                *wert = (uint8_t) i;
            }
            else
                UART_puts(" Ung\201ltig.\r\n"); // \201 ist ü in Oktalschreibweise
        }
    }

    // Ausgabe auf RS232 (s.oben)
    // bei nur Änderung des Wertes gegenüber der letzten Ausgabe
    //
    // alter_wert wurde mit -1 initialisiert. Das ist ausserhalb
    // des Wertebereichs von *wert, D.h. spätere Abfrage, ob
    // (alter_wert != *wert) ist, ist beim ersten Durchlauf wie
    // gewünscht wahr,
    //
    if (alter_wert != *wert)
    {
        alter_wert = *wert;
        wert_ausgeben(s, *wert);
    }
}


void rs232_uhrzeit_zeigen(uint8_t stunden, uint8_t minuten, uint8_t sekunden)
{
    static int16_t alte_sekunden = -1;
    char puffer[23];

    if ( alte_sekunden != sekunden )
    {
        // Neue Seite im Terminal anfordern.
        // Bewirkt ein Löschen des Bildschirms
        UART_putchar(FORMFEED);

        // Zahl in Ziffernfolge umwandeln. Hier zuerst in Dezimalziffern
        itoa((int) stunden+100, puffer, 10);
        UART_puts(&puffer[1]);

        UART_puts(" : ");

        // Zahl in Ziffernfolge umwandeln. Hier zuerst in Dezimalziffern
        itoa((int) minuten+100, puffer, 10);
        UART_puts(&puffer[1]);

        UART_puts(" : ");

        // Zahl in Ziffernfolge umwandeln. Hier zuerst in Dezimalziffern
        itoa((int) sekunden+100, puffer, 10);
        UART_puts(&puffer[1]);

        // Zeilenabschluss zur Formatierung der Anzeige
        UART_puts("\r\n");

        alte_sekunden = sekunden;
    }
}

PFA_rs232.h - Prototypen zu den rs232-Funktionen

//
// Pollin Funk-AVR-Evaluationsboard
// PFA_uart.h
// v 1.0
//

#include <avr/io.h>
#include <stdlib.h>
#include "PFA_uart.h"

#ifndef FORMFEED
#define FORMFEED '\014'
#endif

#define EINGABE_START 	0
#define EINGABE_ABBRUCH EINGABE_START
#define EINGABE_MAX 		3

void rs232_io(char * s, uint8_t *wert, uint8_t min, uint8_t max);
void rs232_uhrzeit_zeigen(uint8_t stunden, uint8_t minuten, uint8_t sekunden);

// EOF PFA_uart.h

PFA_uart.c - UART Grundfunktionen (aus ehemaligem rs232.c herausgezogen)

//
// Pollin Funk-AVR-Evaluationsboard v1.1
// PFA_uart.c
// v 1.0
//

#include "PFA_uart.h"

/*
    Atmega8

    Externer Quarz-Oszillator: 12 MHz
    Optimierung: -Os

    RS232-Einstellungen:
    9600 Baud, 8 Bits, No Parity, 1 Stopbit
*/
void UART_init(void)
{
    // Bausrate setzen
    // 9600 Baud bei F_CPU 12 MHz
    // (Baudratenfehler = +0,2%)
    UBRRH = (uint8_t) ((F_CPU / (16 * 9600L) - 1) >> 8);
    UBRRL = (uint8_t)  (F_CPU / (16 * 9600L) - 1);

    //      Empfangen,   Senden erlauben
    UCSRB = (1<<RXEN) | (1<<TXEN);

    // Frame Format:              8 Bits,                  No Parity,          1 Stopbit
    UCSRC = (1<<URSEL) | ((1<<UCSZ1) | (1<<UCSZ0)) | ((0<<UPM1) | (0<<UPM0)) | (0<<USBS);
}


// Auf Zeichen im UART Eingang prüfen
uint8_t UART_peekchar(void)
{
    // sofort zurückkehren und Zustand melden
    return UCSRA & (1<<RXC);
}


// Auf Zeichen im UART Eingang warten
uint8_t UART_getchar(void)
{
    // Warte bis UART Eingang voll
    while ( !(UCSRA & (1<<RXC)) )
        ;
    return UDR;
}


// Zeichen in UART Ausgang geben
void UART_putchar(char z)
{
    // Warte bis UART Ausgang frei
    while ( !(UCSRA & (1<<UDRE)) )
        ;
    UDR = z;
}


// Zeichenkette (String) in UART Ausgang geben
void UART_puts(char *s)
{
    while ( *s )
    {
        UART_putchar(*s);
        s++;
    }
}

// EOF PFA_uart.c

PFA_uart.h - Prototypen zu den UART Grundfunktionen

//
// Pollin Funk-AVR-Evaluationsboard
// PFA_uart.h
//

#include <avr/io.h>

void UART_init(void);

// Auf Zeichen im UART Eingang prüfen
uint8_t UART_peekchar(void);

// Auf Zeichen im UART Eingang warten
uint8_t UART_getchar(void);

// Zeichen in UART Ausgang geben
void UART_putchar(char z);

// Zeichenkette (String) in UART Ausgang geben
void UART_puts(char *s);

// EOF PFA_uart.h

Uhr mit 7-Segment-Anzeige

Die Anzeige auf dem kleinen LCD ist nett, aber eine zusätzliche 7-Segment-Anzeige mit vier Ziffern für Stunden und Minuten wäre schöner!

Aber inzwischen wird es schon knapp mit den freien Pins am ATmega8, weil das LCD am Aufbau installiert bleiben soll und dadurch Port C belegt ist. Daneben sind auch drei I/O-Pins durch Taster und die beiden LEDs.belegt und zwei Pins sind durch die UART weg... Aber zum Glück kann man den Bedarf an Pins durch verschieden Massnahmen senken:

Uhr mit 7-Segment-Anzeige (Pollin Funk-AVR-Evaluationsboard)
  1. Ohne Zusatzmassnahmen bräuchte man 4 Ziffern * 7 Segmente. Gesamtbedarf: 28 Output-Pins ;-(
  2. Erste Reduzierungsstufe: 4 Steuerleitungen für die Stromzufuhr der 4 Ziffern und 7 Steuerleitungen für alle Ziffern gemeinsam. Die Stromzufuhr wird zyklisch jeweils einer Ziffer zugeordnet. Wenn die Ziffer an der Reihe ist, werden die Segmente mit den Segmentsteuerleitungen eingestellt. Gesamtbedarf: 4 + 7 = 11 Output-Pins
  3. Zweite Reduzierungsstufe: Die Segmentsteuerleitungen werden von einem BCD-nach-7Segment-Decoder angesprochen. Um eine BCD-Zahl an den Decoder zu geben werden am AVR 4 Steuerleitungen benötigt. Gesamtbedarf: 4 + 4 = 8 Output-Pins
  4. Dritte Reduzierungsstufe: Die Segmentsteuerleitungen werden von einem Schieberegister angesprochen. Um das Schieberegister zu füttern, werden am AVR 2-3 Steuerleitungen (je nach Art des Schieberegisters) benötigt. Gesamtbedarf: 4 + 2 bis 3 = 6 bis 7 Output-Pins
  5. Vierte Reduzierungsstufe: Die Segmentsteuerleitungen und die Versorgungssteuerleitungen werden von je einem Schieberegister bedient. Und es wird eine gemeinsame Clock-Steuerleitung verwendet. Gesamtbedarf: 2x 2 bis 3 minus 1 = 3 bis 5 Output-Pins
  6. Fünfte Reduzierungsstufe...: Nur 1 Pin zur Ansteuerung, geht das? Auch das geht, wenn z. B. die Ausgabe seriell zu einem weiteren Mikrocontroller geschickt wird, der dann eine eigene Displaysteuerung besitzt.

Zufällig fand sich in meiner Bastelkiste ein BCD-nach-7Segment-Decoder nämlich der CA3161 von Intersil sowie vier grüne Siebensegmentanzeigen von einem früheren Drehzahlmesser-Projekt. Die sollen jetzt dem Recycling zugeführt werden.

Die Verkabelung ist experimentell gestaltet. Als einfaches Steckbrett zum Anschluss an die 40-polige Steckerleiste J4 wird ein 40-poliges IDE Kabel eines geplünderten Rechners benutzt. Von dem 40-poligen Kabel geht es dann über ein längeres 10-poliges Kabel (8 plus +5V und GND) zum Aufbaubrett mit der Zusatzschaltung.

Achtung: Wenn ein solches IDE-Kabel in eigenen Schaltungen benutzt wird, vorher kontrollieren, ob die benötigten Pins 1:1 verbunden sind (Danke an Markus [6])

Plan der Leitungszuordnung:

Funktion Bezeichnung am ATmega8 Pin an ATmega8-16PI Pin an 40-pol. J4 Leitung auf 10-pol. Verbindung Pin an CA3161 Bezeichnung am CA3161 Transistor-Nr.
Versorgung Minuten-Einer PB0 14 9 1 - - Q1
Versorgung Minuten-Zehner PD2 4 28 2 - - Q2
Versorgung Stunden-Einer PD3 5 29 3 - - Q3
Versorgung Stunden-Zehner PD4 (Anm. 1) 6 30 4 - - Q4
Segmente BCD 20 PB2 16 13 5 7 20 -
Segmente BCD 21 PB3 17 14 6 1 21 -
Segmente BCD 22 PB4 18 15 7 2 22 -
Segmente BCD 23 PB5 19 16 8 6 23 -
Uhr mit 7-Segment-Anzeige (Pollin Funk-AVR-Evaluationsboard)

Im nebenstehenden Bild ist der Versuch eines ersten Schaltplans zu sehen. Dabei wurde KiCAD verwendet. Bei der Erstellung des "Spezial-IC" CA3161 war der Quick KICAD Library Component Builder sehr hilfreich. Achtung: Es ist nicht der komplette Aufbau von Spannungsversorgung und Beschaltung des ATmega8 zu sehen, lediglich die Bauteile und Verbindungen für dieses Beispiel sind eingezeichnet!

Bei den 7-Segmentanzeigen handelt es sich um Anzeigen vom Typ Kingbright SA52-11GWA (common anode , 13 mm, grün). Die Versorgung jeweils einer Ziffer mit Strom erfolgt über einen geschalteten PNP-Transistor Typ BC558B mit 1,5 kOhm Basiswiderstand. Die Strombegrenzung für die Anzeige wird über je einen 220Ω Vorwiderstand pro Segmentleitung gemacht. Diese Dimensionierung ist noch nicht optimal, denn die Ziffern können ruhig noch etwas heller leuchten. Bei dieser Dimensionierung benötigt eine Ziffer im Multiplexbetrieb ca. 3 mA Strom (Messung mit Multimeter zwischen Transistor und CA Anschluss). Im Dauerbetrieb ist das absolute Maximum Rating für diese Anzeige 25 mA.

Anm. 1: Bei PD4 heisst es aufpassen! Dieser Pin ist auch über einen 180Ω Vorwiderstand an den Optokoppler angeschlossen, um die Versorgungsspannung eines ggf, installierten RFM-Modus einzuschalten. Das Ganze wirkt dann als starker Pulldown-Widerstand, d.h. diese Leitung an diesem Pin wird im Reset-Zustand LOW gezogen und dadurch öffnet Q4 und lässt Ziffer 4 leuchten, wenn gleichzeitig mein ISP-Adapter (STK200-komp.) an J2 angeschlossen ist und dadurch PB3 (MOSI) und PB5 (SCK) auf LOW gezogen werden... Die anderen Versorgungspins sind im Reset im Tristate-Modus, d.h. die Ziffern 1 bis 3 sind aus, weil Q1 bis Q3 gesperrt sind.

Das führt zu einer grundsätzlichen und wichtigen Überlegung in einem Projekt - Was macht die Periferie, wenn der Mikrocontroller bzw. das Programm darin keine Kontrolle hat? Beispielsweise beim Reset oder beim ISP-Programmieren oder beim Brownout... Im Hinblick auf diese Überlegungen ist die hier verwendete Kombination aus Schaltung und Programm unsicher!

Der Programmcode ist wieder in mehrere Module gegliedert:

  • PFA_uhr_7segment.c - Das Hauptprogramm
  • PFA_7segment.c - Die eigentliche Ansteuerung
  • PFA_7segment.h - Die Prototypen für PFA_7segment.c
  • sowie diese Unveränderten Dateien aus dem Uhr-Beispiel (s.o.): PFA_funkhw.h, PFA_rs232.c, PFA_rs232.h, PFA_uart.c, PFA_uart.h, lcd-routines.c, lcd-routines.h

PFA_uhr_7segment.c - Das Hauptprogramm

Die Ansteuerung der neuen Anzeige (Bsp:led7segment_multiplex) ist im Timer0-Overflow-Interrupt eingeklinkt. Sie wird bei jedem (!) Timer-Overflow-Interrupt aufgerufen, d.h. sie darf nicht zulange arbeiten, damit keine weiteren Interrupts auflaufen. Der Timer wurde anderes als im Uhr-Beispiel eingerichtet, nämlich mit gleichlangen Overflow-Intervallen. Die Steuerdaten für die sieben Segmente pro Ziffer werden bei möglichen Änderungen, d.h. bei einem Sekundenwechsel, vorausberechnet und in einem Feld (Bsp: led7segment_ziffern) gespeichert. Die Initialisierung der neuen Anzeige erfolgt einmalig zu Beginn des Hauptprogramms (Bsp: led7segment_init).

//
// Pollin Funk-AVR-Evaluationsboard v1.1
// PFA_uhr_7segment.c
// v 1.0
//

/*
    Atmega8

    Externer Quarz-Oszillator: 12 MHz
    Optimierung: -Os

    RS232-Einstellungen:
    9600 Baud, 8 Bits, No Parity, 1 Stopbit
*/

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
#include <avr/interrupt.h>

#include "PFA_funkhw.h"
#include "PFA_rs232.h"
#include "PFA_7segment.h"
#include "lcd-routines.h"

/* in Millisekunden */
#define ENTPRELLUNG        10

volatile uint8_t stunden;
volatile uint8_t minuten;
volatile uint8_t sekunden;

//
// 0: 183x256 + 1x27
// 1: 183x256 + 1x27 durchlaufendes TCNT0 berücksichtigt
// 2: 325x125
//
#define RELOAD 	125
#define COUNT 	375


ISR(TIMER0_OVF_vect)
{
    static uint16_t ticks = 0;

    TCNT0 = 256 - RELOAD + TCNT0;

    led7segment_multiplex();

    ticks += 1;
    if ( ticks == COUNT )
    {
        // 1 Sekunde ist um.
        sekunden += 1;

        if ( sekunden == 60 )
        {
            sekunden = 0;
            minuten += 1;
        }

        if ( minuten == 60 )
        {
            minuten = 0;
            stunden += 1;
        }

        if ( stunden == 24 )
            stunden = 0;

        // 1x möglichst viel vorberechnen, damit die Multiplex-Routine schnell ist
#if 1
        // Äktschn beim Entwickeln ;-)
        led7segment_ziffern[0] = (sekunden % 10) << 2;
        led7segment_ziffern[1] = (sekunden / 10) << 2;
        led7segment_ziffern[2] = (minuten % 10) << 2;
        led7segment_ziffern[3] = (minuten / 10) << 2;
#else
        led7segment_ziffern[0] = (minuten % 10) << 2;
        led7segment_ziffern[1] = (minuten / 10) << 2;
        led7segment_ziffern[2] = (stunden % 10) << 2;
        led7segment_ziffern[3] = (stunden / 10) << 2;
#endif
        // nächste Runde
        ticks = 0;
    }
}


void uhr_init(void)
{
    // Rrescaler 256
    TCCR0 = (1<<CS02) | (0<<CS01) | (0<<CS00);

    // Zählregister
    TCNT0 = 0;

    // Overflow-Flag löschen
    TIFR = (1<<TOV0);

    // Timer0 Overflow enable
    TIMSK |= (1<<TOIE0);

    // Interrupts global einschalten
    sei();
}


void uhrzeit_lcd(void)
{
    static int16_t alter_wert = -1;
    char puffer[4]; // max. 3 Ziffern (uint8_t) plus 1 Nullbyte

    if ( sekunden != alter_wert )
    {
        // Ausgabe nur bei Änderungen
        alter_wert = sekunden;

        lcd_clear();

        itoa((int) stunden+100, puffer, 10);
        lcd_string(&puffer[1]);

        lcd_string(" : ");

        itoa((int) minuten+100, puffer, 10);
        lcd_string(&puffer[1]);

        lcd_string(" : ");

        itoa((int) sekunden+100, puffer, 10);
        lcd_string(&puffer[1]);
    }
}


void ausgabe_led(uint8_t wert)
{
    if (wert & (1<<0)) // Bit 0
        LED_AN(LED1);
    else
        LED_AUS(LED1);

    if (wert & (1<<1)) // Bit 1
        LED_AN(LED2);
    else
        LED_AUS(LED2);
}


int main(void)
{
    uint8_t alter_tastenzustand = TASTE_AUF;
    uint8_t zaehler = 0;

    DDRB &= ~(1<<TASTER);           // Port B: Eingang für Taster
    DDRD |= (1<<LED1) | (1<<LED2);  // Port D: Ausgang für LED1 und LED2

    UART_init();
    UART_putchar(FORMFEED);

    lcd_init();

    led7segment_init();

    uhr_init();

    while (1)
    {
        // Bekannte Uhrzeit auf LCD ausgeben
        uhrzeit_lcd();

        // ggf. Kommando von TASTER1 einlesen
        if ( TASTER_GEDRUECKT() )
        {
#if ENTPRELLUNG
            _delay_ms(ENTPRELLUNG);
            if ( TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
            {
                // Wechsel von OFFEN nach GESCHLOSSEN?
                if ( alter_tastenzustand == TASTE_AUF )
                {
                    zaehler++;
                    alter_tastenzustand = TASTE_ZU;
                }
            }
        }

        if ( !TASTER_GEDRUECKT() )
        {
#if ENTPRELLUNG
            _delay_ms(ENTPRELLUNG);
            if ( !TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
                alter_tastenzustand = TASTE_AUF;
        }

        // Feedback TASTER1 auf LEDs
        ausgabe_led(zaehler); // LED Ausgabe

        // Je nach TASTER1-Eingabe Uhrzeit per RS232 ändern oder anzeigen
        switch (zaehler % 4)
        {
        case 3:
            rs232_io("Stunden = ", &stunden, 0, 23);
            break;

        case 2:
            rs232_io("Minuten = ", &minuten, 0, 59);
            break;

        case 1:
            rs232_io("Sekunden = ", &sekunden, 0, 59);
            break;

        case 0:
        default:
            rs232_uhrzeit_zeigen(stunden, minuten, sekunden);
            break;
        }
    }
}

// EOF PFA_uhr_7segment.c

PFA_7segment.c - Die eigentliche Ansteuerung

//
// Pollin Funk-AVR-Evaluationsboard
// PFA_7segment.c
// v 1.0
//

#include "PFA_7segment.h"
#include "PFA_funkhw.h"
#include <avr/delay.h>

uint8_t led7segment_ziffern[ANZAHL_ZIFFERN];


void led7segment_init(void)
{
    DDRB |= (1<<PB0) | (1<<PB2) | (1<<PB3) | (1<<PB4) | (1<<PB5);
    DDRD |= (1<<PD2) | (1<<PD3) | (1<<PD4);

    // Alle Segmente aus
    PORTB |= ((1<<PB2) | (1<<PB3) | (1<<PB4) | (1<<PB5)); // BCD Code 0b1111 für "Blank" an Ausgabepins

    // Spannung aller Ziffern aus (neg. Logik)
    PORTB |= (1<<PB0);
    PORTD |= ((1<<PD2) | (1<<PD3) | (1<<PD4));
}


void led7segment_multiplex(void)
{
    static uint8_t ticks = 0;

    // Spannung aller Ziffern aus (neg. Logik)
    PORTB |= (1<<PB0);
    PORTD |= ((1<<PD2) | (1<<PD3) | (1<<PD4));

    // Segmente der neuen Ziffer berechnen
    PORTB = (PORTB & 0b11000011) | led7segment_ziffern[ticks];
    switch ( ticks )
    {
    case 0:
        PORTB &= ~(1<<PB0);	// Spannung ein
        break;
    case 1:
        PORTD &= ~(1<<PD2);
        break;
    case 2:
        PORTD &= ~(1<<PD3);
        break;
    case 3:
        PORTD &= ~(1<<PD4);
        break;
    default:
        break;
    }

    // auf nächste Ziffer weiterschalten
    ticks += 1;
    ticks %= ANZAHL_ZIFFERN;
}

// EOF PFA_7segment.c

PFA_7segment.h - Die Prototypen für PFA_7segment.c

//
// Pollin Funk-AVR-Evaluationsboard v1.1
// PFA_7segment.h
// v 1.0
//

/*
    Atmega8
*/

#include <avr/io.h>

#define ANZAHL_ZIFFERN	4

extern uint8_t led7segment_ziffern[];

void led7segment_init(void);

void led7segment_multiplex(void);

// EOF PFA_7segment.h

Mit einem Logikanalysator kann das Multiplexen auf den einzelnen Leitungen gemessen werden (wie praktisch, dass am zweckentfremdeten IDE-Kabel eine zweite Buchsenleiste ist). Im folgenden Bild sind 5 Durchläufe der vierstelligen Anzeige dargestellt.

Logikdiagramm der Uhr mit 7-Segment-Anzeige (Pollin Funk-AVR-Evaluationsboard)

In den Spuren 01 bis 04 (blau) liegen die Versorgungssteuerleitungen, die den Multiplexbetrieb steuern, und in den Spuren 05 bis 08 (rot) die BCD-codierten Segmentsteuerleitungen. Ein Zyklus (gelber Bereich) dauert ca. 10,7 ms. Dabei wird für ca. 2,7 ms die Versorgungsspannung einer Ziffer eingeschaltet, während die anderen Ziffern ausgeschaltet sind. Die Logik auf den Versorgungsleitungen ist negiert, da die verwendeten PNP-Transistoren zum Durchschalten der Versorgungsspannung bei log. 0 öffnen und bei log. 1 schliessen (siehe auch Transistor als Schalter). Nach 2,7 ms wird die Versorgungsspannung einer Ziffer gekappt, indem der zugehörige Transistor mit einem HIGH Pegel gesperrt wird und ein LOW Pegel am nächsten Transistor startet die Versorgung und damit die Sichtbarkeit der nächsten Ziffer...

Suppentimer

Motivation (schmackhaft)

Kennt ihr das? Da ist man am bosseln und plötzlich meldet sich der Jieper auf Futter. Was geht schnell genug zwischen Compilerlauf und Flashen - ein leckeres Tütchen Suppe... Während das Reaktionsgemisch köchelt, flugs noch ein paar LEDs eingebaut und Stunden später riecht es aus der Küche komisch und die Nachbarin klopft. Wieder mal die Zeit vergessen!

Aber es naht Abhilfe in Form des Suppentimers ST. In der ersten Ausbaustufe wird die 7-Segment-Uhr aus dem Beispiel vorher dafür genommen und es wird die Software umgeschrieben. UART und LCD werden nicht benötigt, deshalb können die Pin-Zuordnungen etwas bequemer gewählt werden. Z.B. bleiben jetzt die ISP-Leitungen an PORT B frei (Schaltplan siehe bei der Beschreibung von PFA_7segment.c).

Die Bedienung und Anzeige bekommt eine raffinierte Benutzersteuerung:

Der ST zählt nach dem Einschalten oder Reset Sekunden hoch und wenn bei der gewünschten Zahl der Taster 1 gedrückt wird, wird diese Zahl als Alarmzeit in Minuten genommen. Eine Marktrecherche hat nämlich ergeben, dass die typischen Tütchensuppen je nach Sorte im 5 bis 15 Minuten-Bereich köcheln sollen. 5-15 Sekunden zum Setzen des Alarms sind zumutbar, oder?

Zur Korrektur des Alarms kann man entweder Reset drücken, das geht immer, oder man kann Taster 1 nochmal drücken, um die gesetzte Alarmzeit zu löschen.

Bei gesetzter Alarmzeit wird links die Alarmzeit in Minuten angezeigt und rechts die aktuelle Zeit. Wenn die aktuelle Zeit gleich der Alarmzeit ist, schlägt der ST Alarm. Im Moment geschieht das durch Blinken der eingestellten Alarmzeit und Wechselblinken der LEDs (so wie in Blinky). Die aktuelle Zeit läuft weiter, so dass man abschätzen kann, wieviele Lötstellen "doch noch gehen".

Der letzte Bedienschritt ist, dass der Alarm mit einem Tastendruck an Taster 1 bestätigt werden muss. Quasi ein Nervfaktor, denn in einer späteren Hardware-Ausbaustufe könnte man drastischere Mittel einsetzen, z. B. einen 90 dB Piezosummer anwerfen oder so ähnlich.

Bei der Umsetzung in die Steuersoftware (Firmware) hilft es, wenn man den geplanten Ablauf in einem Diagramm aufzeichnet. Eine Methode dafür ist es, ein Zustandsdiagramm für eine Zustandsmaschine zu zeichnen. Solche state charts erleichtern auch das spätere Debuggen und mehr (State charts can provide you with software quality insurance von Peter Mueller auf www.embedded.com).

Die verschiedenen Zustände in obigem Usermanual sind u.a. "Nixmachen" (Sekunden hochzählen), Alarm setzen, Alarmzeit und aktuelle Zeit vergleichen, Alarmzeit löschen, Alarm geben, Alarm abschalten,...

Zustandsdiagramm

In Anlehnung an die erste Testsuppe TS für den ST sind die Zustände in meinem Diagramm als kleine Klöße dargestellt. Die Übergänge oder Aktionen zum Zustandswechsel zwischen den Zuständen sind durch Pfeile dargestellt. Das sind einmal die Tastendrücke oder Ergebnisse der Vergleiche (beschriftete Pfeile) oder auch automatische Übergänge im Programm bzw. drei Warteschleifen (unbeschriftete Pfeile).

Der Quellcode ist gegenüber dem Uhr Beispiel abgespeckt, da ja die UART und LCD Routinen beim ST nicht benötigt werden:

  • PFA_Suppentimer.c - Das Hauptprogramm
  • PFA_7segment.c - Die Ansteuerung der Anzeige (geänderte Pinzuordnung!)
  • PFA_7segment.h - dazugehörige Includedatei
  • PFA_funkhw.h - Definitionen für das PFA ;-) Board (unverändert)

Bevor es in den C-Code geht noch eine kleine Lesehilfe. Der Code enthält vier Hauptversionen mit steigendem Umfang bzw. einer Zusatzfunktion: Stromsparen! Welche Version übersetzt wird, wird durch das Makro SLEEP_CODE in PFA_Suppentimer.c gesteuert:

Zustandsdiagramm mit Schlafmodus
  • 0 - keine Zusatzfunktion. Der ST arbeitet wie oben beschrieben.
  • 1 bis 3 - ST fällt in einen Schlafmodus, wenn die Alarmzeit nicht innerhalb von SCHLAFENSZEIT Sekunden eingegeben wird. Im Schlafmodus wird die Anzeige abgeschaltet. Wie der Schlafmodus im Programm aussieht, steuert der Wert 1, 2 oder 3
    • 1 - Zum Aufwecken aus einem simulierten Schlaf wartet der ST auf einen Tastendruck an Taster 1. Bis der Tastendruck kommt, dreht in einer aus Tasty bekannten Programmschleife Däumchen.
    • 2 - Hier wird die Abfrage in der Programmschleife durch ein im Interrupt gesetztes Flag verlassen. Dazu wird eine kleine externe Hardware benötigt. An PD2 bzw. INT0 ist ein Taster 2 anzuschliessen, der per Interrupt INT0 den ST "aufweckt". Die Art des Interrupts ist hier noch frei wählbar. Noch ist das Schlafen lediglich in einer kleinen Schleife im Hauptprogramm simuliert
    • 3 - Jetzt geht es richtig zur Sache. Der ST fällt, wie im Artikel Sleep Mode beschrieben, in Tiefschlaf. Das Programm läuft nicht mehr. Der Timer pennt. Nur ein mit Taster2 ausgelöster LOW LEVEL (!) Interrupt oder ein Reset wecken den Schläfer auf.


PFA_Suppentimer.c - Das Hauptprogramm

//
// Pollin Funk-AVR-Evaluationsboard v1.1
// PFA_Suppentimer.c
// v 1.0
// v 1.1 - Abfrage Taster in der Hauptschleife
//         Abfrage nur in den Zuständen, in denen Eingabe möglich ist
//

/*
    Atmega8

    Externer Quarz-Oszillator: 12 MHz
    Optimierung: -Os
*/

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>

#include "PFA_funkhw.h"
#include "PFA_7segment.h"


#if 1
// Schneller Suppentimer zum Programmentwickeln
#define TRIGGER sekunden
#else
// Langsamer Suppentimer zum Suppenkochen
#define TRIGGER minuten
#endif

// Betriebszustände des Suppentimers
#define RUHEZUSTAND       0
#define ALARM_SETZEN      RUHEZUSTAND     + 1   // Nicht veränderbar
#define ALARM_PRUEFEN     ALARM_SETZEN    + 1
#define ALARM_LOESCHEN    ALARM_PRUEFEN   + 1
#define ALARM_GEBEN       ALARM_LOESCHEN  + 1
#define ALARM_QUITTIEREN  ALARM_GEBEN     + 1   // Nicht veränderbar


// SLEEP_CODE = Auswahl zur schrittweisen Programmentwicklung
//
// 0: Kein SLEEP
//
// 1: SIMULATION Stufe 1 SLEEP und INT0 werden NICHT benutzt.
//    Aufwachen: TASTER1 auf dem Board
//
// 2: SIMULATION Stufe 2 SLEEP wird NICHT benutzt, aber INT0 wird benutzt.
//    Aufwachen: Externe TASTER2 Hardware an PD2
//
// 3: Echtes SLEEP: SLEEP und INT0 werden benutzt.
//    Aufwachen: Externe TASTER2 Hardware an PD2
//
#define SLEEP_CODE        0

#if ( SLEEP_CODE > 0 )
// Weitere Zustände
#define SCHLAFEN_GEHEN    ALARM_QUITTIEREN  + 1
#define SCHLAFEN          SCHLAFEN_GEHEN    + 1
#define AUFWACHEN         SCHLAFEN          + 1
// Sekunden im RUHEZUSTAND bis Schlafen beginnt
#define SCHLAFENSZEIT     23
#endif /* ( SLEEP_CODE > 0 ) */

#if ( SLEEP_CODE == 2 )
// Hilfsvariable fürs Aufwachen
volatile uint8_t int0_zustand;
#endif /* ( SLEEP_CODE == 2 ) */


/* in Millisekunden */
#define ENTPRELLUNG        10


#define RELOAD 	125
#define COUNT 	375

/*
#define RELOAD 	75
#define COUNT 	625
*/

/*
#define RELOAD 	25
#define COUNT 	1875
*/

volatile uint8_t stunden;
volatile uint8_t minuten;
volatile uint8_t sekunden;


#if ( SLEEP_CODE > 1 )
ISR(INT0_vect)
{
#if ( SLEEP_CODE == 2 )
    int0_zustand = AUFWACHEN;
#endif /* ( SLEEP_CODE == 2 ) */

#if ( SLEEP_CODE == 3 )
    // set_sleep_mode(SLEEP_MODE_IDLE);
#endif /* ( SLEEP_CODE == 3 ) */
}
#endif /* ( SLEEP_CODE > 1 ) */


ISR(TIMER0_OVF_vect)
{
    static uint16_t ticks = 0;

    TCNT0 = 256 - RELOAD + TCNT0;

    led7segment_multiplex();

    ticks += 1;
    if ( ticks == COUNT )
    {
        // 1 Sekunde ist um.
        sekunden += 1;

        if ( sekunden == 60 )
        {
            sekunden = 0;
            minuten += 1;
        }

        if ( minuten == 60 )
        {
            minuten = 0;
            stunden += 1;
        }

        if ( stunden == 24 )
            stunden = 0;

        // nächste Runde
        ticks = 0;
    }
}


void uhr_init(void)
{
    // Prescaler 256 einstellen
    TCCR0 = (1<<CS02) | (0<<CS01) | (0<<CS00);

    // Zählregister initialisieren
    TCNT0 = 256 - RELOAD;

    // Overflow-Flag löschen
    TIFR = (1<<TOV0);

    // Timer0 Overflow erlauben
    TIMSK |= (1<<TOIE0);

    // Interrupts global einschalten
    sei();
}


void ausgabe_led(uint8_t wert)
{
    if (wert & (1<<0)) // Bit 0
        LED_AN(LED1);
    else
        LED_AUS(LED1);

    if (wert & (1<<1)) // Bit 1
        LED_AN(LED2);
    else
        LED_AUS(LED2);
}


#if 0
// Beim Makro darf die Argumentübergabe keine Seiteneffekte haben!
// Die Codegrösse ist bei -Os in beiden Fällen gleich
#define setze_ziffer(pziffer, wert)  *(pziffer) = (wert)
#else
static void setze_ziffer(uint8_t * pziffer, uint8_t wert)
{
    *pziffer = wert;
}
#endif


uint8_t taster1(uint8_t jetzt_zustand, uint8_t naechster_zustand)
{
    static uint8_t alter_tastenzustand = TASTE_AUF;

    if ( TASTER_GEDRUECKT() )
    {
#if ENTPRELLUNG
        _delay_ms(ENTPRELLUNG);
        if ( TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
        {
            // Wechsel von OFFEN nach GESCHLOSSEN?
            if ( alter_tastenzustand == TASTE_AUF )
            {
                jetzt_zustand = naechster_zustand;
                alter_tastenzustand = TASTE_ZU;
            }
        }
    }

    if ( !TASTER_GEDRUECKT() )
    {
#if ENTPRELLUNG
        _delay_ms(ENTPRELLUNG);
        if ( !TASTER_GEDRUECKT() )
#endif /* ENTPRELLUNG */
            alter_tastenzustand = TASTE_AUF;
    }

    return jetzt_zustand;
}


int main(void)
{
    uint8_t zustand = RUHEZUSTAND;
    uint8_t alarmzeit;

    DDRB &= ~(1<<TASTER);           // Port B: Eingang für Taster
    DDRD |= (1<<LED1) | (1<<LED2);  // Port D: Ausgang für LED1 und LED2

    led7segment_init();

    uhr_init();

    while (1)
    {
        switch (zustand)
        {
        case RUHEZUSTAND:
            setze_ziffer(&led7segment_ziffern[0], (sekunden % 10));
            setze_ziffer(&led7segment_ziffern[1], (sekunden / 10));
            setze_ziffer(&led7segment_ziffern[2], (0b00001111));     // BCD "blank"
            setze_ziffer(&led7segment_ziffern[3], (0b00001111));

#if (SLEEP_CODE > 0)
            // SLEEP?
            if ( sekunden >= SCHLAFENSZEIT )
                zustand = SCHLAFEN_GEHEN;
#endif /* (SLEEP_CODE > 0) */

            // Aus diesem Zustand geht es
            // durch Usereingabe (Tastendruck)
            // nächster Zustand dann RUHEZUSTAND+1 = ALARM_SETZEN
            if ( zustand == RUHEZUSTAND )
                zustand = taster1(RUHEZUSTAND, ALARM_SETZEN);
            break;

        case ALARM_SETZEN:
            alarmzeit = sekunden;
            TRIGGER = 0;
            zustand = ALARM_PRUEFEN;
            break;

        case ALARM_PRUEFEN:
            if ( TRIGGER == alarmzeit )
            {
                zustand = ALARM_GEBEN;
            }
            else
            {
                setze_ziffer(&led7segment_ziffern[0], (TRIGGER % 10));
                setze_ziffer(&led7segment_ziffern[1], (TRIGGER / 10));
                setze_ziffer(&led7segment_ziffern[2], (alarmzeit % 10));
                setze_ziffer(&led7segment_ziffern[3], (alarmzeit / 10));
                zustand = taster1(ALARM_PRUEFEN, ALARM_LOESCHEN);
            }
            break;

        case ALARM_LOESCHEN:
            sekunden = 0;
            minuten = 0;
            stunden = 0;
            zustand = RUHEZUSTAND;
            break;

        case ALARM_GEBEN:
            setze_ziffer(&led7segment_ziffern[0], (TRIGGER % 10));
            setze_ziffer(&led7segment_ziffern[1], (TRIGGER / 10));

            // Ziffer 3+4: Alarmzeit und LEDs blinken im Sekundentakt
            if ( (sekunden & 1) )
            {
                ausgabe_led(2);
                setze_ziffer(&led7segment_ziffern[2], (0b00001111));     // BCD "blank"
                setze_ziffer(&led7segment_ziffern[3], (0b00001111));
            }
            else
            {
                ausgabe_led(1);
                setze_ziffer(&led7segment_ziffern[2], (alarmzeit % 10));
                setze_ziffer(&led7segment_ziffern[3], (alarmzeit / 10));
            }

            // Aus diesem Zustand geht es nur durch
            // Usereingabe (Tastendruck) raus!
            // nächster Zustand dann ALARM_GEBEN+1 = ALARM_QUITTIEREN
            zustand = taster1(ALARM_GEBEN, ALARM_QUITTIEREN);
            break;

        case ALARM_QUITTIEREN:
            sekunden = 0;
            minuten = 0;
            stunden = 0;
            zustand = RUHEZUSTAND;
            break;

#if ( SLEEP_CODE > 0 )
        case SCHLAFEN_GEHEN:
            led7segment_exit();

#if ( SLEEP_CODE == 2 )
            int0_zustand = SCHLAFEN;
            // PD2 ist Input, internen Pullup ein
            PORTD |= (1<<PD2);
            // INT0 z.&nbsp;B. bei fallender Flanke
            MCUCR |= (1<<ISC01) | (0<<ISC00);
            // Anstehende INT0 löschen (durch 1 schreiben)
            GIFR |= (1<<INTF0);
            // Wecker stellen: INT0 enable, sei läuft bereits (uhr_init)
            GICR |= (1<<INT0);
#endif /* ( SLEEP_CODE == 2 ) */

#if ( SLEEP_CODE == 3 )
            // Tiefschlaf!
            set_sleep_mode(SLEEP_MODE_PWR_DOWN);
            // PD2 ist Input, internen Pullup ein
            PORTD |= (1<<PD2);
            // ##############################################
            // # ACHTUNG: nur LOW LEVEL Interrupt möglich ! #
            // ##############################################
            MCUCR |= (0<<ISC01) | (0<<ISC00);
            // Anstehende INT0 löschen (durch 1 schreiben)
            GIFR |= (1<<INTF0);
            // Wecker stellen: INT0 enable, sei läuft bereits (uhr_init)
            GICR |= (1<<INT0);
#endif /* ( SLEEP_CODE == 3 ) */

            zustand = SCHLAFEN;
            break;

        case SCHLAFEN:
            ausgabe_led(0);

#if ( SLEEP_CODE == 1 )
            // Tastendrücken abwarten = Aufwachen
            while ( taster1(1, 0) )
                ;
#endif /* SLEEP_CODE == 1 */

#if ( SLEEP_CODE == 2 )
            // Schlafen
            while ( int0_zustand == SCHLAFEN )
                ;
#endif /* ( SLEEP_CODE == 2 ) */

#if ( SLEEP_CODE == 3 )
            // Und weg...
            sleep_mode();
            // Hier geht es bei SLEEP_MODE_PWR_DOWN !!!
            // nur nach einem INT0-Interrupt weiter
#endif /* ( SLEEP_CODE == 3 ) */

            zustand = AUFWACHEN;
            break;

        case AUFWACHEN:
#if (( SLEEP_CODE == 2 ) || ( SLEEP_CODE == 3 ))
            // INT0 disable, sei läuft weiter (siehe uhr_init)
            GICR &= ~(1<<INT0);

            // PD2 bleibt Input, internen Pullup aus
            PORTD &= ~(1<<PD2);
#endif /* ( SLEEP_CODE == 2 ) || ( SLEEP_CODE == 3 ) */

            sekunden = 0;
            minuten = 0;
            stunden = 0;
            led7segment_init();
            zustand = RUHEZUSTAND;
            break;
#endif /* ( SLEEP_CODE > 0 ) */
        }

        if ( zustand != ALARM_GEBEN ) // Blinken nicht stören!
            ausgabe_led(zustand);       // Feedback auf LEDs
    }
}

// EOF PFA_Suppentimer.c
Teil-Schaltplan Suppentimer ST. Details zur 7-Segmentanzeige siehe Beispiel Uhr mit 7-Segmentanzeige

PFA_7segment.c - Die Ansteuerung der Anzeige (geänderte Pinzuordnung!)

Dieser Quellcode enthält die Grundansteuerung der 7-Segment-Anzeige mit den neu zugeordneten Pins. Die Hardware wurde ausserdem um einen Taster 2 (Aufwecktaster) und eine Anschaltung der Versorgungsspannung des Anzeigeteils ergänzt, um tatsächlich einen energiesparenden und erholsamen Schlaf zu haben. OK es geht mehr ums Prinzip, denn der nicht abschaltbare Rest des Evaluationsboards (Netz-LED, Spannungswandler, MAX232) saugen schon noch im zig Milliampere-Bereich.

//
// Pollin Funk-AVR-Evaluationsboard
// PFA_7segment.c
//
// v 1.0
// v 1.1  - Pinbelegung geändert
//

#include "PFA_7segment.h"
#include "PFA_funkhw.h"
#include <avr/delay.h>


uint8_t led7segment_ziffern[ANZAHL_ZIFFERN];
static uint8_t led7segment_enabled = 0;


static void led7segment_power_off(void)
{
    DDRB |= (1<<PB0);
    PORTB &= ~(1<<PB0);
}


static void led7segment_power_on(void)
{
    DDRB |= (1<<PB0);
    PORTB |= (1<<PB0);
}


void led7segment_init(void)
{
    uint8_t i;

    for (i = 0; i < ANZAHL_ZIFFERN; i++)
        led7segment_ziffern[i] = 0b00001111; // BCD Blank

    // Spannung aller Ziffern aus (neg. Logik)
    DDRD |= (1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3);
    PORTD |= (1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3);

    // Versorgungsspannung an
    led7segment_power_on();

    // Alle Segmente der Ziffern aus
    DDRC |= (1<<PC0) | (1<<PC1) | (1<<PC2) | (1<<PC3);
    PORTC |= ((1<<PC0) | (1<<PC1) | (1<<PC2) | (1<<PC3)); // BCD Code 0b1111 für "Blank" an Ausgabepins

    led7segment_enabled = 1;
}


void led7segment_exit(void)
{
    uint8_t i;

    for (i = 0; i < ANZAHL_ZIFFERN; i++)
        led7segment_ziffern[i] = 0b00001111; // BCD Blank

    // RESET-Zustand: Pins Input und Tristate
    DDRD  &= ~((1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3));
    PORTD &= ~((1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3));

    // RESET-Zustand: Pins Input und Tristate
    DDRC  &= ~((1<<PC0) | (1<<PC1) | (1<<PC2) | (1<<PC3));
    PORTC &= ~((1<<PC0) | (1<<PC1) | (1<<PC2) | (1<<PC3));

    // Versorgungsspannung aus
    led7segment_power_off();

    led7segment_enabled = 0;
}


void led7segment_multiplex(void)
{
    static uint8_t ticks = 0;

    if ( led7segment_enabled )
    {
        // Spannung aller Ziffern aus (neg. Logik)
        PORTD |= (1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3);

        // Segmente der neuen Ziffer berechnen
        PORTC = (PORTC & 0b11110000) | led7segment_ziffern[ticks];
        switch ( ticks )
        {
        case 0:
            PORTD &= ~(1<<PD0);	// Spannung ein
            break;
        case 1:
            PORTD &= ~(1<<PD1);
            break;
        case 2:
            PORTD &= ~(1<<PD2);
            break;
        case 3:
            PORTD &= ~(1<<PD3);
            break;
        default:
            break;
        }

        // auf nächste Ziffer weiterschalten
        ticks += 1;
        ticks %= ANZAHL_ZIFFERN;
    }
}

// EOF PFA_7segment.c

PFA_7segment.h - dazugehörige Includedatei

//
// Pollin Funk-AVR-Evaluationsboard v1.1
// PFA_7segment.h
// v 1.1
//

/*
    Atmega8
*/

#include <avr/io.h>

#define ANZAHL_ZIFFERN	4

extern uint8_t led7segment_ziffern[];

void led7segment_init(void);

void led7segment_exit(void);

void led7segment_multiplex(void);

// EOF PFA_7segment.h

Redesign der Hardware

Redesign-Schaltplan Suppentimer ST mit Schieberegister 74HC164

Das IC CA3161 ist nicht mehr gut erhältlich. Deshalb wurde die Schaltung mit einem Schieberegister (8 Bit Serial Shift Register) aus der 74xx Familie umgebaut. Statt dem 74xx595 wie im AVR-Tutorial: Schieberegister beschrieben, wurde das einfachere 74HC164 verwendet. Das 74xx595 hätte ich bestellen müssen und das 74HC164 konnte ich kurzfristig organisieren ;-) Die Routinen zur Ansteuerung befinden sich in der geänderten Datei PFA_7segment.c:

//
// Pollin Funk-AVR-Evaluationsboard
// PFA_7segment.c
//
// v 1.0
// v 1.1  - Pinbelegung geändert (s. Code)
// v 1.2  - Trennung in BCD-Version und Schieberegister-Version
//          Pinbelegung s. Code
//
// PB0 => Spannungsversorgung Anzeige (über NPN Transistor)
//          C: GND von der Anzeige
//          B: PB0 => 2,2 K => B
//          E: GND am PFA
// PD2 => Externer Taster2
//          PD2 => Taster (offen) => 330Ω => GND


#include "PFA_7segment.h"
#include "PFA_funkhw.h"
#include <avr/delay.h>


#define BCD               0
#define SCHIEBEREGISTER   !(BCD)


uint8_t led7segment_ziffern[ANZAHL_ZIFFERN];
static uint8_t led7segment_enabled = 0;


static void led7segment_power_off(void)
{
    DDRB  |=  (1<<PB0);
    PORTB &= ~(1<<PB0);
}


static void led7segment_power_on(void)
{
    DDRB  |= (1<<PB0);
    PORTB |= (1<<PB0);
}


#if SCHIEBEREGISTER
/*
    ###################################
          SCHIEBEREGISTER 74HC164
    ###################################
*/

#define SR_DDR        DDRB
#define SR_PORT       PORTB
#define SR_CLEAR      PB2
#define SR_CLOCK      PB5
#define SR_INPUT      PB3

#define ZIFFERN_DDR   DDRC
#define ZIFFERN_PORT  PORTC
#define ZIFFER1       PC0
#define ZIFFER2       PC1
#define ZIFFER3       PC2
#define ZIFFER4       PC3

static uint8_t segment_tab[] =
{
    // Quelle: AVR-Tutorial auf www.mikrocontroller.net
    /* .db */ 0b11000000 , //    ; 0: a, b, c, d, e, f
    /* .db */ 0b11111001 , //    ; 1: b, c
    /* .db */ 0b10100100 , //    ; 2: a, b, d, e, g
    /* .db */ 0b10110000 , //    ; 3: a, b, c, d, g
    /* .db */ 0b10011001 , //    ; 4: b, c, f, g
    /* .db */ 0b10010010 , //    ; 5: a, c, d, f, g
    /* .db */ 0b10000010 , //    ; 6: a, c, d, e, f, g
    /* .db */ 0b11111000 , //    ; 7: a, b, c
    /* .db */ 0b10000000 , //    ; 8: a, b, c, d, e, f, g
    /* .db */ 0b10010000 , //    ; 9: a, b, c, d, f, g
    0xFF,
    0xFF,
    0xFF,
    0xFF,
    0xFF,
    0xFF
};


void led7segment_init(void)
{
    uint8_t i;

    for (i = 0; i < ANZAHL_ZIFFERN; i++)
        led7segment_ziffern[i] = 0b00001111; // BCD Blank

    // Alle Ziffern aus
    ZIFFERN_DDR  |= ((1<<ZIFFER1) | (1<<ZIFFER2) | (1<<ZIFFER3) | (1<<ZIFFER4));
    ZIFFERN_PORT |= ((1<<ZIFFER1) | (1<<ZIFFER2) | (1<<ZIFFER3) | (1<<ZIFFER4));

    SR_DDR  |=  ((1<<SR_INPUT) | (1<<SR_CLOCK) | (1<<SR_CLEAR)); // Output
    SR_PORT &= ~((1<<SR_INPUT) | (1<<SR_CLOCK) | (1<<SR_CLEAR)); // LOW

    // Versorgungsspannung an
    led7segment_power_on();

    led7segment_enabled = 1;
}


void led7segment_exit(void)
{
    SR_DDR  &= ~((1<<SR_INPUT) | (1<<SR_CLOCK) | (1<<SR_CLEAR)); // Input
    SR_PORT &= ~((1<<SR_INPUT) | (1<<SR_CLOCK) | (1<<SR_CLEAR)); // Pullups aus, Tristate

    // RESET-Zustand: Pins Input und Tristate
    ZIFFERN_DDR  &= ~((1<<ZIFFER1) | (1<<ZIFFER2) | (1<<ZIFFER3) | (1<<ZIFFER4));
    ZIFFERN_PORT &= ~((1<<ZIFFER1) | (1<<ZIFFER2) | (1<<ZIFFER3) | (1<<ZIFFER4));

    // Versorgungsspannung aus
    led7segment_power_off();

    led7segment_enabled = 0;
}

void led7segment_multiplex(void)
{
    static uint8_t ticks = 0;

    if ( led7segment_enabled )
    {
        uint8_t tmp_segmente;
        uint8_t i;

        // Aktuelle Ziffer aus
        switch ( ticks )
        {
        case 0:
            ZIFFERN_PORT |= (1<<ZIFFER1);
            break;
        case 1:
            ZIFFERN_PORT |= (1<<ZIFFER2);
            break;
        case 2:
            ZIFFERN_PORT |= (1<<ZIFFER3);
            break;
        case 3:
            ZIFFERN_PORT |= (1<<ZIFFER4);
            break;
        default:
            break;
        }

        // Nächste Ziffer
        ticks += 1;
        ticks %= ANZAHL_ZIFFERN;
        tmp_segmente = segment_tab[led7segment_ziffern[ticks] & 0x0F];

        // Register leeren
        SR_PORT = (1<<SR_CLOCK) | (0<<SR_CLEAR);
        SR_PORT = (1<<SR_CLOCK) | (1<<SR_CLEAR);

        i = 8;
        do
        {
            i -= 1;
            SR_PORT |= (1<<SR_INPUT);
            SR_PORT &= ~(1<<SR_CLOCK);
            if ( !(tmp_segmente & (1<<i)) )   // MSB zuerst
                SR_PORT &= ~(1<<SR_INPUT);
            SR_PORT |= (1<<SR_CLOCK);
        }
        while (i);

        SR_PORT = (1<<SR_CLOCK) | (1<<SR_CLEAR) | (0<<SR_INPUT);

        // Aktuelle Ziffer ein
        switch ( ticks )
        {
        case 0:
            ZIFFERN_PORT &= ~(1<<ZIFFER1);
            break;
        case 1:
            ZIFFERN_PORT &= ~(1<<ZIFFER2);
            break;
        case 2:
            ZIFFERN_PORT &= ~(1<<ZIFFER3);
            break;
        case 3:
            ZIFFERN_PORT &= ~(1<<ZIFFER4);
            break;
        default:
            break;
        }
    }
}
#endif /* SCHIEBEREGISTER */


#if BCD
/*
    ####################################
        BCD-7Segment-Decoder CA3161
    ####################################
*/


void led7segment_init(void)
{
    uint8_t i;

    for (i = 0; i < ANZAHL_ZIFFERN; i++)
        led7segment_ziffern[i] = 0b00001111; // BCD Blank

    // Spannung aller Ziffern aus (neg. Logik)
    DDRD  |= (1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3);
    PORTD |= (1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3);

    // Versorgungsspannung an
    led7segment_power_on();

    // Alle Segmente der Ziffern aus
    DDRC  |= (1<<PC0) | (1<<PC1) | (1<<PC2) | (1<<PC3);
    PORTC |= (1<<PC0) | (1<<PC1) | (1<<PC2) | (1<<PC3); // BCD Code 0b1111 für "Blank" an Ausgabepins

    led7segment_enabled = 1;
}


void led7segment_exit(void)
{
    uint8_t i;

    for (i = 0; i < ANZAHL_ZIFFERN; i++)
        led7segment_ziffern[i] = 0b00001111; // BCD Blank

    // RESET-Zustand: Pins Input und Tristate
    DDRD  &= ~((1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3));
    PORTD &= ~((1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3));

    // RESET-Zustand: Pins Input und Tristate
    DDRC  &= ~((1<<PC0) | (1<<PC1) | (1<<PC2) | (1<<PC3));
    PORTC &= ~((1<<PC0) | (1<<PC1) | (1<<PC2) | (1<<PC3));

    // Versorgungsspannung aus
    led7segment_power_off();

    led7segment_enabled = 0;
}


void led7segment_multiplex(void)
{
    static uint8_t ticks = 0;

    if ( led7segment_enabled )
    {
        // Spannung aller Ziffern aus (neg. Logik)
        PORTD |= (1<<PD0) | (1<<PD1) | (1<<PD2) | (1<<PD3);

        // Segmente der neuen Ziffer berechnen
        PORTC = (PORTC & 0b11110000) | led7segment_ziffern[ticks];
        switch ( ticks )
        {
        case 0:
            PORTD &= ~(1<<PD0);	// Spannung ein
            break;
        case 1:
            PORTD &= ~(1<<PD1);
            break;
        case 2:
            PORTD &= ~(1<<PD2);
            break;
        case 3:
            PORTD &= ~(1<<PD3);
            break;
        default:
            break;
        }

        // auf nächste Ziffer weiterschalten
        ticks += 1;
        ticks %= ANZAHL_ZIFFERN;
    }
}
#endif /* BCD */


// EOF PFA_7segment.c

Timer1 Interrupt

F: Wie kann man den Timer 1 Compare Match Interrupt auslösen ([7])?

Hier ist ein Beispiel mit dem Timer1 im Modus PWM phase and frequency correct. LED1 blinkt mit 1 Hz per Timer 1 Interrupt und LED2 mit 1 Hz per Software (_delay_ms()):

/*
    Pollin Funk AVR Evaluationsboard
    F_CPU             12000000
    MCU               ATMega8
    Compileoptionen   -Os
*/

#ifndef F_CPU
#define F_CPU 12000000L
#endif

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

ISR (TIMER1_COMPA_vect)
{
  PORTD ^= (1<<PD6);  // toggle LED1
}

void Timer_io_init(void)
{
  // Table 39 im Atmega8 Datenblatt
  // Waveform generation mode: 
  // Mode 9: PWM Phase and frequency correct
  TCCR1A |= (0 << WGM11) | (1 << WGM10);
  TCCR1B |= (1 << WGM13) | (0 << WGM12);

  // Table 40 im Atmega8 Datenblatt
  // PRESCALER: clk/1024
  TCCR1B |= (1 << CS12)  | (0 << CS11)  | (1 << CS10);

  /*
    Wunsch: ca. 1 Hz Blinken
    1 Hz genau - siehe: http://www.mikrocontroller.net/articles/AVR_-_Die_genaue_Sekunde_/_RTC

    Setzen von TOP (OCR1A) = Anzahl der Timer1-Takte von IRQ zu IRQ
    Figure 40 im Atmega8 Datenblatt
    1. /2 wegen 2 Halbperioden pro Sekunde bei 1 Hz
    2. /2 wegen Runterzählen von TOP (=OCR1A) nach BOTTOM (=0) 
       und Hochzählen von BOTTOM (=0) bis TOP (=OCR1A) zwischen 
       den Interrupts
  */

#define DELAY (F_CPU / 1024 / 2 / 2) 
  OCR1A = DELAY; 
  TCNT1 = - DELAY; // Anlauf für 1. Interrupt verlängern

  TIMSK = (1 << OCIE1A);
}

int main(void)
{
  DDRD = (1<<PD6) | (1<<PD5); 
  Timer_io_init();
  sei();
  while(1)
  {
    PORTD ^= (1<<PD5);  // toggle LED2
    _delay_ms(500);
  }
}

RC5 Empfänger

Im Verlauf der Diskussion [8] stellte sich die Frage, wie ein Empfänger für den RC5 Code von Infrarot (IR) Fernbedienungen aufgebaut werden kann.

Vorab eine Anmerkung, die man auch regelmäßig im Forum liest: Nur relativ wenige IR-Fernbedienungen benutzen auch den von Philips erfundenen RC5 Code. Ich habe mir eine preiswerte Universalfernbedienung beschafft, die man auf Philips Geräte einstellen kann.

Der Aufbau des RC5-Codes und vieler anderer IR-Codes ist übrigens sehr gut auf der Seite von Sam Bergmans beschrieben (www.sbprojects.com). Und der Eigenbau eines passenden Senders war ja die Ausgangslage in obiger Diskussion...

TSOP1736 aus: Photo Modules for PCM Remote Control Systems von Vishay

Zum Empfang von RC5-kodierten IR-Signale ist der IR-Empfänger TSOP1736 (PDF) von Vishay geeignet.

Leider ist der TSOP1736 inzwischen abgekündigt. Es gibt aber funktionskompatiblen Ersatz [9] und [10]. Bei den Ersatztypen ist unbedingt die Pinbelegung anhand des Datenblattes zu kontrollieren und ggf. anzupassen.

In der Codesammlung hat Peter Dannegger C-Quelltexte veröffentlicht, die eine Dekodierung von RC5 Signalen auf AVR µCs ermöglichen ([11]). Bei einigen Compilerversionen muss die Rückgabevariable rc5_data volatile gekennzeichnet sein, wenn man mit Optimierung übersetzt ([12]).

Die Quelltexte werden wie üblich in ein AVR-Studio C Projekt aufgenommen. Unter den Projektoptionen wird die 8000000 bei der Taktrate und die Optimierungstufe -Os eingestellt. 8 MHz ist ja der an der kleinsten µC-Fassung zu verbauende Quarz.

Folgende Änderungen sind zur Anpassung an einen Attiny2313 auf dem Pollin Funk AVR Board nötig bzw. hilfreich.

Änderungen in main.h:

  • Anpassen der Includes. Hier ist der Pfad um avr/ zu ergänzen und die veraltete signal.h wird auskommentiert.
#include <avr/io.h>
#include <avr/interrupt.h>
// #include <signal.h>
  • Anpassen des Eingangspins für das Signal vom TSOP1736.
#define xRC5 PD2 // IR input low active (statt PD7)
  • Anpassen der Taktrate. Der Attiny2313 wird auf dem werksmäßigen Pollin Funk AVR Board mit 8 MHz betrieben. Die Einstellung der externen Taktquelle mit den AVR Fuses ist ebenfalls nötig, wenn noch nicht geschehen! Tipps zu Anpassungen für noch geringere Taktraten gibt es in der Diskussion ab Beitrag [13] im Forum.
//#define XTAL 11.0592e6
#define XTAL 8e6 // Attiny2313 auf Pollin Funk AVR Board
  • Sicherstellen, dass die Variable für den Datenaustausch zwischen Hauptprogramm und Timerroutine volatile deklariert ist.
extern volatile uint  rc5_data;        // store result

Änderungen in main.c:

  • Anpassen der Timer-Register. Die Benennung beim Attiny2313 ist anders als im Original
  // TCCR0 = 1<<CS02; //divide by 256 Original
  TCCR0B = 1<<CS02;   //divide by 256 Attiny2313
  • Anpassung der UART-Initialisierung an die Initialisierung des Attiny2313 (siehe andere Beispiele in diesem Artikel)
  • Änderung der DEBUG-LED. Im Original wird PORTB zum Debuggen benutzt. Das wird auf die LED1 auf dem Board geändert.
  // Am Programmanfang einfügen
  DDRD |= (1<<PD6); // PD6 Ausgang
  PORTD |= (1<<PD6); // DEBUG-LED LED1 an
...
  // Auskommentiert
  // DDRB = i; // LED output

Änderungen in rc5.c:

  • Sicherstellen, dass die Variable für den Datenaustausch zwischen Hauptprogramm und Timerroutine volatile definiert ist.
volatile uint  rc5_data;        // store result
  • Einbau der DEBUG-LED. Dazu wird folgendes Codestück zwischen #if 1 und #endif ergänzt.
    if( !tmp || rc5_time > PULSE_1_2 )
    { // start or long pulse time
#if 1
      /*
        Anschluss PD6----###--->|---- GND
      */
      DDRD |= (1<<PD6);   // PD6 auf Ausgang
      PORTD ^= (1<<PD6);  // LED1 an PD6 togglen
#endif
      if( !(tmp & 0x4000) ) // not to many bits

Der TSOP1736 OUT wird - wie in main.h definiert - an PD2 des Attiny2313 angeschlossen. PD2, Vcc und GND können von der 40-poligen Erweiterungsbuchse abgegriffen werden. In meinem Steckbrettaufbau hatte ich den 10 kOhm Pullup-Widerstand **) eingebaut aber die Entstörung der Betriebsspannung *) nicht. Bei einem "echten" Gerät würde ich beide Schaltungsoptionen einbauen.

Schema IR-RX.png

Die Ausgabe des dekodierten Signals (Togglebit Anm.[14], Adresse, Kommando) kann mit einem RS232 Terminalprogramm betrachtet werden, Anschluss des Pollin Boards an die serielle Schnittstelle vorausgesetzt. Die RS232-Einstellungen sind 19200 Baud, 8 Batenbits, No Parity und 1 Stopbit.

Im Forumsbeitrag [15] stellt Alex seine Erweiterung dieser RC5-Routine vor, mit der eine Fallunterscheidung kurzer/langer Tastendruck möglich ist. Damit realisiert Alex auf einer Taste eine EIN/AUS-Funktion (kurzer Tastendruck) und eine DIMMEN-Funktion (langer Tastendruck).

Klickidiklacki

In der Diskussion [16] ist die Frage aufgetaucht, wie man die Compare Output Unit verwenden kann. Das dort angegebene Beispiel, die seltsame Simulation im AVR Studio und die Diskussion haben mich dazu geführt, folgendes Beispiel mit dem Attiny2313 zu erstellen.

Das Programm ist komplett abgespeckt: Der Taster1 wirkt über eine Brücke Pin 10 nach Pin 30 an J4 als externer Taktgeber an Pin T0. Wenn die Anzahl der Tastendrücke gleich dem vorgegehenen Wert 5 in Register OCR0A ist, wird der Ausgang OC0A automatisch umgeschaltet. Da der Ausgang OC0A über eine Brücke an J4 mit der LED1 verbunden ist, wird die LED umgeschaltet.

Die Besonderheit: Alles funktioniert automatisch per Hardware. Zu Programmbeginn wird einmal die Hardware eingestellt, und sie läuft dann autonom parallel zur Software. Die Software ist in diesem Fall eine leere Endlosschleife.

/*
    Beispiel: Compare Output Unit

    Pollin Funk AVR Board
    Attiny2313 @ 8 MHz

                         Attiny2313   J4 
    Taster1 active-high     PB1       Pin 10     --+
    LED1    active-high     PD5       Pin 31 --+   |
                                               |   | Brücke
    T0                      PD4       Pin 30   | --+
    OC0B                    PD5       Pin 31   |   
    OC0A                    PB2       Pin 11 --+  
*/

#include <avr/io.h>
  
int main(void)
{
  DDRB |= (1<<PB2); // PB2 Ausgang OC0A

  //
  // Compare Output Unit einstellen. S 74
  //
  // Vergleichswert
  OCR0A = 5 - 1; // 5x Taster drücken
  // Toggle OC0A bei TCNT0 == OCR0A
  TCCR0A |= (0 << COM0A1) | (1 << COM0A0);
  // CTC Mode
  TCCR0A |= (1 << WGM01) | (0 << WGM00);
  // Externe Clock an T0 bei steigender Flanke (Taster schliesst)
  TCCR0B |= (1 << CS00) | (1 << CS01) | (1 << CS02);   
  
  while (1)
  {
  }

  return 0;
}

Der CTC-Modus bewirkt, dass beim zutreffenden Vergleich (TCNT0 == OCR0A) TCNT0 automatisch wieder auf 0 gesetzt wird. Leider kann dieser Modus mit dem anderen Ausgang OC0B nicht realisiert werden; es hätte sich in diesem Fall gerade angeboten, weil PD5 und OC0B bereits auf dem gleichen Pin liegen (Pin 31 an J4). Ohne den CTC-Modus für OC0B müsste man selbst beim Vergleich TCNT0 per Software zurücksetzen.

Und bitte noch beachten: Es ist nur die seltsame Hardwareentprellung des Pollin-Boards vorhanden. Also nicht wundern, wenn die Zählerei mit dem Taster unzuverlässig ist. Wenn man jedoch eine nichtprellende Taktquelle an T0 benutzt, könnte man das Programm benutzen, um eine ankommende Frequenz z. B. hier durch 5 zu teilen (Bsp. 10 Hz auf 2 Hz runterteilen).

AVR-GCC-Tutorial/Die Timer und Zähler des AVR

Im folgenden ist das Beispiel für Compare Match Mode aus dem AVR-GCC-Tutorial/Die Timer und Zähler des AVR vom Atmega8 auf den Attiny2313 umgeschrieben. Die wesentlichen Änderungen sind mit ### markiert.

/*
   Anpassung des Timer-Beispiels aus dem AVR-GCC-Tutorial
   an den Attiny2313 @ 8MHz auf dem Pollin Funk AVR Board

   Erweiterung: (Lauf-)Zeitausgabe über UART 9600/8N1
*/ 
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdlib.h>
 
//Variablen für die Zeit
volatile unsigned int  millisekunden=0;
volatile unsigned int  sekunde=0;
volatile unsigned int  minute=0;
volatile unsigned int  stunde=0;

void uart_init(void)
{
  // 9600 Baud bei F_CPU 8 MHz: (Baudratenfehler = +0,2%)
  UBRRH = (uint8_t) ((F_CPU / (16 * 9600L) - 1) >> 8);
  UBRRL = (uint8_t)  (F_CPU / (16 * 9600L) - 1);

  //      Empfangen,   Senden erlauben
  UCSRB = (1<<RXEN) | (1<<TXEN);

  // Frame Format:              8 Bits,                  No Parity,          1 Stopbit
  UCSRC = ((1<<UCSZ1) | (1<<UCSZ0)) | ((0<<UPM1) | (0<<UPM0)) | (0<<USBS);
}

int uart_putchar( char c )
{
  if( c == '\n' )
    uart_putchar( '\r' );
 
  loop_until_bit_is_set( UCSRA, UDRE );
  UDR = c;
  return 0;
}

void uart_puts(char *s)
{
  while(*s)
    uart_putchar(*s++);
}

void zeitausgabe(void)
{
  static char last_sekunde = 255;
  char buffer[4];

  if ((last_sekunde != sekunde)) 
  {
    last_sekunde = sekunde;
    itoa(stunde+100, buffer,10);
    uart_puts(buffer+1);
    uart_puts(":");
    itoa(minute+100, buffer,10);
    uart_puts(buffer+1);
    uart_puts(":");
    itoa(sekunde+100, buffer,10);
    uart_puts(buffer+1);
    uart_puts("\r\n");
  }
}

int main(void)
{
   uart_init();

   //Timer 0 konfigurieren auf CTC-Modus Attiny2313
   TCCR0A = (1<<WGM01);            // ###
   TCCR0B = (1<<CS01) | (1<<CS00); // ### Prescaler 8*8 = 64 bei 8 MHz
   OCR0A=125-1;                    // ### Bugfix 20091221
 
   //Compare Interrupt aktivieren
   TIMSK|=(1<<OCIE0A);             // ###

   //Globale Interrupts aktivieren
   sei();

   while(1)
   {
     /*Hier kann die aktuelle Zeit ausgeben werden*/
     zeitausgabe();
   }

   return 0;
}
 
// Der Compare Interrupt Handler
// Wird aufgerufen wenn TCNT0 = 125
ISR (TIMER0_COMPA_vect)            // ###
{
   millisekunden++;
   if(millisekunden==1000)
   {
      sekunde++;
      millisekunden=0;
      if(sekunde==60)
      {
         minute++;
         sekunde=0;
      }
      if(minute ==60)
      {
        stunde++;
        minute=0;
      }
   }
}

Der Bequemlichkeit halber ist eine (Lauf-)Zeitanzeige per UART (9600 Baud 8 Datenbits, 1 Stoppbit, keine Parität) hinzugefügt: So lässt sich das Programm als sekundengenaueaufgelöste Stoppuhr benutzen (RESET-Taster = Neustart).

Noch ein Nachtrag zu diesem Beispiel:

Die ISR und das Anwendungsprogramm (genauer die Routine zeitausgabe()) benutzen gemeinsame Variablen, die auch korrekt als volatile gekennzeichnet sind.

Dennoch versteckt in diesem Programm ein potenzieller Bug: Der Datenzugriff ist nicht atomar! Der Bug macht sich bloß nicht bemerkbar, weil die zeitausgabe weit weniger als eine Sekunde braucht, um die Daten auszugeben.

Angenommen zeitausgabe() braucht länger, was könnte passieren? Die gemeinsamen Variablen vom Typ unsigned int sind größer 1 Byte bzw. der kompletten Zeitstempel stunde:minute:sekunde ist wesentlich größer als 1 Byte. Eine Leseoperation bzw. die Ausgabe benötigt also mehrere Maschinenanweisungen bzw. sogar C-Anweisungen. Ein Interrupt könnte diese Anweisugen z. B. der beiden Bytes von unsigned int sekunde unterbrechen und ggf. das noch nicht gelesene Byte ändern. Oder minute sei bereits über UART ausgegeben, dann vor der Ausgabe von sekunde unterbricht der Timer und erhöht sekunde. Wenn sekunde bereits 59 war, wird minute erhöht und sekunde auf 0 gesetzt. minute war aber bereits ausgegeben... die Anzeige ist dann falsch!

Als Ausweg bietet die avr-libc Hilfsmittel zur Sicherstellung des atomaren Datenzugriffes. Mit Hilfe des Makros ATOMIC_BLOCK können Interrupts gesperrt werden, wenn die Anweisungen im {}-Block ausgeführt werden.

#include <util/atomic.h>

void zeitausgabe(void)
{
  static char last_sekunde = 255;
  char buffer[4];
  unsigned int tmp_stunde;
  unsigned int tmp_minute;
  unsigned int tmp_sekunde;

  // Übersetzung mit der Compileroption 
  // -std=c99 oder -std=gnu99 erforderlich
  ATOMIC_BLOCK(ATOMIC_FORCEON)
  {
    // Schnappschuss anfertigen
    // Timerinterrupt ist gesperrt!
    // Diese Anweisungen knapp halten!
    tmp_stunde = stunde;
    tmp_minute = minute;
    tmp_sekunde = sekunde;
  }

  // Schnappschuss aufarbeiten
  if ((last_sekunde != tmp_sekunde)) 
  {
    last_sekunde = tmp_sekunde;
    itoa(tmp_stunde+100, buffer,10);
    uart_puts(buffer+1);
    uart_puts(":");
    itoa(tmp_minute+100, buffer,10);
    uart_puts(buffer+1);
    uart_puts(":");
    itoa(tmp_sekunde+100, buffer,10);
    uart_puts(buffer+1);
    uart_puts("\r\n");
  }
}

Weiteres kann man dazu im Artikel Interrupt und AVR-GCC-Tutorial - Variablen größer 1 Byte und in der Dokumentation der avr-libc nachlesen.

"Scotty, Handbremse!"

Die Frage in [17] hat mich neugierig gemacht, wie das Clock Prescale Register CLKPR arbeitet. Achtung: Bei großen Clock Prescale Werten vorher unbedingt dies lesen.

Im AVR Studio (4.12 SP2) konnte im Originalprogramm beim ATtiny13 als auch später beim ATtiny2313 in der Simulation (Debugger) kein Unterschied festgestellt werden egal ob mit oder ohne Clock Division Factor simuliert wurde.

Daraufhin wurde das Originalprogramm auf das Pollin Funk AVR Board "angepasst", d.h. LED1 und LED2 blinken wechselweise und der Taster dient als Eingabe:

  • Taster loslassen und RESET ausführen, und das Programm arbeitet ohne Clock Division Factor
  • Taster gedrückt halten und RESET ausführen, und das Programm arbeitet mit Clock Division Factor. Sobald die LEDs blinken, kann der Taster losgelassen werden.
 
; http://www.mikrocontroller.net/topic/164079#1565907
; Blink2.asm Blinker mit Unterprogramm (aus Franzis Lernpaket)
; modifiziert 20100122 für Attiny2313 @ 8 MHz auf Pollin Funk AVR Hardware

.include "tn2313def.inc"

.org 0x0000
      rjmp Anfang

Anfang:
; Stack einrichten weil rcall/ret verwendet wird
; Nicht nötig bei ATtiny13 (Originalcode)
; Aber nötig bei ATtiny2313
      ldi r16,LOW(RAMEND)
      out SPL,r16

; Speed Divider Set
      IN r16,PINB
      SBRC r16,PINB1     ; Taster losgelassen => kein Nurhalbekraft
      rcall Nurhalbekraft

; Ports initialisieren
      ldi r16,((1<<PORTD6)|(1<<PORTD5))
      out DDRD,r16       ; als Ausgang einrichten, Rest Eingang

; Endlose Arbeitsschleife: Wechselbinkler mit LED1 und LED2
L_Schleife:
      ldi r16,(1<<PORTD5)
      out PORTD,r16
      rcall Warten
      ldi r16,(1<<PORTD6)
      out PORTD,r16
      rcall Warten
      rjmp L_Schleife

; Hilfsroutinen

; Speed Divider Set
Nurhalbekraft:
      in r17, SREG        ; Statusregister in Hilfsregister retten
      cli                 ; Global Interrupts disablen
      ldi r16,(1<<CLKPCE) ; 1, Clock Prescaler Change Enable
      out CLKPR,r16       ; 1, Clock Prescale Register "öffnen"
      ldi r16,(1<<CLKPS0) ; 1, Sollwert Clock Division Factor 2
      out CLKPR,r16       ; 1, Clock Prescale Register "beschreiben"
      ; max. 4 Takte Regel ist eingehalten.
      out SREG, r17       ; Statusregister aus Hilfsregister restaurieren
      ret                 ; zurück zum Aufrufer (rcall)

; Warten
; ca. 150ms bei 8 MHz
Warten:
      Ldi   r18,8*10
L0_Warten:
      Ldi   r16,20
L1_Warten:                ;äußere Schleife
      Ldi   r17,250
L2_Warten:                ;innere Schleife
      dec   r17
      brne  L2_Warten
      dec   r16
      brne  L1_Warten
      dec   r18
      brne  L0_Warten
      ret

Ergebnis: Der Clock Division Factor ist auf der Hardware aktiv bzw. der Simulator (bei meinem AVR Studio) ist fehlerhaft. Probieren geht über Studieren (Simulieren) :)

Tasty Reloaded

Peter Dannegger hat in "Entprellen für Anfänger" eine neue vereinfachte Entprellroutine vorgestellt. Hier ein Minimalbeispiel in dem dieses Verfahren verwendet wird:

// Target: Attiny2313 @ 8 MHz Pollin Funk AVR Board

#include <avr/io.h>

#ifndef F_CPU
#define F_CPU 8e6   // ###
#endif
#include <util/delay.h>

/****************************************************/
/*                                                  */
/*  Not so powerful Debouncing Macro                */
/*  No Interrupt needed                             */
/*                                                  */
/*  Author: Peter Dannegger                         */
/*                                                  */
/****************************************************/

// Modification for active high push button

#define debounce( port, pin )                                        \
({                                                                   \
  static uint8_t flag = 0; /* new variable on every macro usage */   \
  uint8_t i = 0;                                                     \
                                                                     \
  if( flag ){                  /* check for key release: */          \
    for(;;){                   /* loop... */                         \
      if( (port & 1<<pin) ){   /* ... until key pressed or ... */    \
        i = 0;                 /* 0 = bounce */                      \
        break;                                                       \
      }                                                              \
      _delay_us( 98 );         /* * 256 = 25ms */                    \
      if( --i == 0 ){          /* ... until key >25ms released */    \
        flag = 0;              /* clear press flag */                \
        i = 0;                 /* 0 = key release debounced */       \
        break;                                                       \
      }                                                              \
    }                                                                \
  }else{                       /* else check for key press: */       \
    for(;;){                   /* loop ... */                        \
      if( !(port & 1<<pin) ){  /* ... until key released or ... */   \
        i = 0;                 /* 0 = bounce */                      \
        break;                                                       \
      }                                                              \
      _delay_us( 98 );         /* * 256 = 25ms */                    \
      if( --i == 0 ){          /* ... until key >25ms pressed */     \
        flag = 1;              /* set press flag */                  \
        i = 1;                 /* 1 = key press debounced */         \
        break;                                                       \
      }                                                              \ 
    }                                                                \
  }                                                                  \
  i;                           /* return value of Macro */           \
})

/*
  Debouncing Example
*/

int main(void)
{
  // push button @ PB1 in active high configuration
  DDRB &= ~(1<<PB1);
  // PORTB |= 1<<PB1; // not needed because active high

  // LED @ PD5 in active high configuration
  DDRD |=   1<<PD5;

  for(;;){
    if( debounce( PINB, PB1 ) )
      PORTD ^= 1<<PD5; // Toggle LED
  }
}

Der Codebedarf für die debounce()-Teilfunktion liegt bei meinem System bei nur 54 Bytes und der SRAM Bedarf bei 1 Byte (WinAVR-20071221, -Os).

Peter schreibt: "Man kann sogar das Programm so ändern, daß beide Flanken zurückgegeben werden (als 1 bzw. 2), dazu muß nur eine Zuweisung auf "i = 2;" geändert werden."

Die Änderung betrifft diesen Teil, bei dem statt

 
        i = 0;                 /* 0 = key release debounced */       \

zu schreiben wäre:

        i = 2;                 /* 2 = key release debounced */       \

Dann schaltet im Beispiel die LED beim Drücken und Loslassen um!

Blinky Reloaded

Die Frage von Markus und die Grundroutine in [18] zum Tutorial LED-Fading von Falk bringt mich dazu, das mit dem Attiny2313 auch mal zu probieren.

Im folgenden Quelltext werden die LED1 und LED2 des Pollinboards langsam und wechselseitig auf- und abgeblendet. Damit die 8-Bit PWM Signale zu den LED gelangen können, wird eine Drahtbrücke zwischen Pin 12 und Pin 32 an der 40-poligen Jumperleiste J4 angebracht.

#include <avr/io.h>
#include <util/delay.h>

/*
    F_CPU = 8 MHz
    Compileroption -Os

    Attiny2313     POLLIN FUNK AVR   BRÜCKE
    =======================================
    PB2   OC0A                11 an J4
    PD5   OC0B    PD5/LED2    31 an J4
    PB3   OC1A                12 an J4 --+
    PB4   OC1B                13 an J4   |
                  PD6/LED1    32 an J4 --+
*/

#if 0
// Beispiel mit linearem Verlauf der PWM Werte
unsigned char pwmtable_8D[32] = {
   0*8,  1*8,  2*8,  3*8,  4*8,  5*8,  6*8,  7*8,
   8*8,  9*8, 10*8, 11*8, 12*8, 13*8, 14*8, 15*8,
  16*8, 17*8, 18*8, 19*8, 20*8, 21*8, 22*8, 23*8,
  24*8, 25*8, 26*8, 27*8, 28*8, 29*8, 30*8, 31*8 
};
#endif 

// Beispiel mit nichtlinearem Verlauf der PWM Werte
unsigned char pwmtable_8D[32] = {
0, 1, 2, 2, 2, 3, 3, 4, 5, 6, 7, 8, 10, 11, 
13, 16, 19, 23, 27, 32, 38, 45, 54, 64, 76,
91, 108, 128, 152, 181, 215, 255
};

void init_fade(void) 
{
  DDRB = (1<<PB2) | (1<<PB3) | (1<<PB4);
  DDRD = (1<<PD5);

  // 8-Bit Timer0 
  // Clear OC0A/OC0B on compare-match when up counting
  // Set OC0A/OC0B on compare-match on TOP
  TCCR0A = (1<<COM0A1) | (1<<COM0B1) | (1<<WGM01) | (1<<WGM00); // 0xA3 Mode 3 Fast-PWM TOP=0xFF
  TCCR0B = (1<<CS02);  // precaler 256 -> ~122 Hz PWM frequency 

  // 16-Bit Timer1
  // Clear OC0A/OC0B on compare-match
  // Set OC0A/OC0B on compare-match on TOP (=0xFF)
  TCCR1A = (1<<COM1A1) | (1<<COM1B1) | (1<<WGM10); // // 0xA1
  TCCR1B = (1<<WGM12);  // 0x08 Mode 5 non-inverted PWM on OC1A, 8 Bit Fast PWM
  TCCR1B |= (1<<CS02);  // precaler 256 -> ~122 Hz PWM frequency 
} 

void fade(void) 
{
  unsigned char tmp;

  for(tmp=0; tmp<=31; tmp++){
    OCR0B = *(pwmtable_8D+31-tmp); // LED2 ausfaden
    OCR1A = *(pwmtable_8D+tmp);    // gleichzeitig LED1 einfaden
    _delay_ms(50);
  }

  _delay_ms(1000);
    
  for(tmp=0; tmp<=31; tmp++){
    OCR0B = *(pwmtable_8D+tmp);    //LED2 wieder einfaden
    OCR1A = *(pwmtable_8D+31-tmp); //LED1 ausfaden
    _delay_ms(50);
  }

  _delay_ms(1000);
}

int main(void)
{
  init_fade();

  while(1)
  {
    fade();
  }
}

Aufgepasst? Im Quelltext sind pro Timer zwei PWMs aktiviert, aber nur je eine wird für LED1 und LED2 benutzt. Die doppelte Aktivierung ist natürlich nicht notwendig, sondern das ist eine Vorschau auf später, weil Markus sein Programm auf vier LEDs erweitern will.

"Dreh' am Rad"

Seltsamerweise boomen derzeit (2/2010) die ADC-Themen im Forum. Da konnte ich mich nicht zurückhalten. Das muss ich auch probieren und bin wieder zurück auf den Atmega8 gewechselt, weil der Attiny2313 keinen ADC besitzt.

Für dieses ADC Beispiel braucht man einen verstellbaren Widerstand (Potentiometer (Wikipedia)). Dessen Schleiferbahn wird zwischen der Versorgungsspannung Vcc und GND angeschlossen. Der Schleifkontakt wird mit ADC2 (PC2) am Atmega8 verbunden. Das Potentiometer wirkt damit als Spannungsteiler, wobei R1+R2 aus dem Artikel der Gesamtwiderstand des Potentiometers sind und die Stellung des Schleifkontakts die Widerstände R1 und R2 verändert. Ich habe ein 10K lineares Potentiometer aus der Bastelkiste benutzt.

Im Programm wird die Ausgangsspannung des Spannungsteilers gegen die Referenzspannung AVcc gemessen und als 10-Bit ADC-Wert digitalisiert (angepasste Funktion ReadChannel() aus dem AVR-GCC-Tutorial).

Als Schmankerl wird der ADC-Wert auch benutzt, um einen Stufenschalter nachzubilden. Diese Anregung habe ich beim Stöbern im Zeitschriftenladen bekommen. In der Zeitschrift Funkamateur 2/2010 beschreibt DH1LD sein Projekt "Virtueller Stufenschalter für den Mikrocontroller", geschrieben in BASCOM-AVR.

Als Anzeige gibt es eine einfache Hardwareanzeige: Die LED2 blinkt umgekehrt proportional zum ADC-Wert (kleiner Wert = schnellstes Blinken). Und es gibt zwei Optionen für die UART-Anzeige an einem Terminal (9600/8N1 Einstellung): Eine Ziffernanzeige und eine "billige" Balkenanzeige.

Das Projekt besteht aus den folgenden drei Quelltexten. Die UART-Routinen wurden bereits beim Zähler mit RS232 benutzt.

Datei PFA_ADC_10K.c

/*
    ADC-Beispiel

    Pollin Funk AVR Board
    Atmega8 @ 12 MHz

    Boardeigene Hardware
    ====================
    LED2
    UART 9600/8N1

    Zusätzliche Hardware:
    ====================
    ca. 10K lineares Potentiometer 
    Schleiferbahn zwischen Vcc und GND 
    Schleiferkontakt zu ADC2 (= PC2)

    Bugfix 20100212
 */

#include <avr/io.h>
#include <stdlib.h>       // utoa()
#include <string.h>       // strlen()
#include "PFA_uart_m8.h"

/*
   ADC 
   Single Conversion Modus
   Vref == AVcc
   für F_CPU 12 MHz angepasst
   Arithm. Mittelwert aus 4 Einzelmessung
 */
uint16_t ReadChannel(uint8_t mux)
{
  uint16_t result;
  uint8_t i;
 
  mux |= (1<<REFS0);   // AVCC als Referenz         ###
  ADMUX = mux;         // Referenz und Kanal setzen ###

  /* 
     Frequenzvorteiler setzen und ADC enablen

     ADC soll optimal mit 50 kHz bis 200 kHz laufen
     12000000/200000 = 60
     12000000/50000  = 240
     => Vorteiler 
     64   (1<<ADPS2) | (1<<ADPS1)
     128  (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0)
   */
  ADCSRA = (1<<ADEN) | (1<<ADPS2) | (1<<ADPS1);  // ###
 
  /* 
     Nach dem Wechsel der ADC Referenz (oder dem Wechsel eines 
     differentiellen Kanals) wird ein "Dummy-Readout" empfohlen. 
     Man ermittelt also einen ungenauen ADC Wert und verwirft diesen. 

     Auch wenn es sich hier um eine Dummy_ADC_Wandlung handelt, muss
     das ADCW Register anschliessend gelesen werden, sonst wird das
     Ergebnis der eigentlichen Messung (s.u.) nicht übernommen!
   */
  ADCSRA |= (1<<ADSC); // eine Wandlung "single conversion" auslösen
  while ( ADCSRA & (1<<ADSC) ) 
  {
    ; // auf den Abschluss der Konvertierung warten 
  }
  result = ADCW;  
 
  /* 
     Eigentliche Messung 
     Mittelwert aus 4 aufeinanderfolgenden Wandlungen 
   */
  result = 0; 
  for( i=0; i<4; i++ )
  {
    ADCSRA |= (1<<ADSC);  // eine Wandlung "single conversion" auslösen
    while ( ADCSRA & (1<<ADSC) ) 
    {
      ; // auf den Abschluss der Konvertierung warten 
    }
    result += ADCW;		    // Wandlungsergebnisse aufaddieren
  }
  ADCSRA &= ~(1<<ADEN);   // ADC disablen (spart Strom)
  result /= 4;            // arithm. Mittelwert bilden
  return result;
}

/*
   Stufenschalter für ADC Werte
   
   Inspiriert durch:
   DH1LD
   "Virtueller Stufenschalter für den Mikrocontroller"
   BASCOM-AVR Routine
   Funkamateur 2/2009
 */
#define MESSBEREICH    1024  /* 0..1023 */
#define STUFENZAHL     11    /* 1..STUFENZAHL :) */
#define HYSTERESE      (5*(MESSBEREICH/STUFENZAHL)/100) /* ca. 5% */
#define STUFE(a)       ((STUFENZAHL*(a)+MESSBEREICH)/MESSBEREICH)

uint8_t stufenschalter(uint16_t adcw)
{
  static uint8_t last_s = 0;
  uint8_t tmp_s = 0;

  // Vorläufige Stufe berechnen
  tmp_s = STUFE(adcw);

  // Korrektur für Hysterese
  if ((tmp_s-last_s == 1) && (adcw > HYSTERESE))
  {
    tmp_s = STUFE(adcw - HYSTERESE);
  }
  else if ((tmp_s-last_s == -1) && (adcw < MESSBEREICH-HYSTERESE))
  {
    tmp_s = STUFE(adcw + HYSTERESE);
  }

  last_s = tmp_s;
  return last_s;
}


/*
   Zahlenwerte über UART ausgeben
   Leichte Pufferung um die Ausgabemenge rel. klein zu halten
 */
void werte_anzeigen(uint16_t adcval, uint8_t schalterpos)
{
  static uint16_t last_adcval = 0xFFFF;
  static uint8_t last_schalterpos = 0xFF;
  uint8_t crlf = 0;
  char buffer[5];
  uint8_t i;

  if (last_adcval != adcval)
  {
    // Neuen ADC-Wert mit vier Stellen ausgeben
    last_adcval = adcval;
    UART_puts("ADC=[");
    utoa(adcval, buffer, 10);
    for (i=4-strlen(buffer); i; i--)
      UART_putchar(' ');
    UART_puts(buffer);
    UART_putchar(']');
    crlf = 1;
  }

  if (last_schalterpos != schalterpos)
  {
    // Neue Schalterposition mit zwei Stellen ausgeben
    last_schalterpos = schalterpos;
    UART_puts("  Schalter=[");
    utoa(schalterpos, buffer, 10);
    for (i=2-strlen(buffer); i; i--)
      UART_putchar(' ');
    UART_puts(buffer);
    UART_putchar(']');
    crlf = 1;
  }

  if (crlf)
    UART_puts("\r\n");
}


/*
   Schalterposition als horiz. Balken 
   über UART ausgeben
 */
void balkenanzeige(uint8_t schalterpos)
{
  static uint8_t last_schalterpos = 0xFF;
  uint8_t i;

  if ( last_schalterpos != schalterpos )
  {
    UART_puts("\r|");
    for(i=0; i<schalterpos; i++)
      UART_puts("###|");
    for(i=schalterpos; i<STUFENZAHL; i++)
      UART_puts("---|");
    last_schalterpos = schalterpos;
    UART_putchar('\r');
  }

}


int main(void)
{
  uint16_t adcval;
  uint8_t schalterpos;
  uint16_t i;

  UART_init();
  UART_putchar('\014'); // FORMFEED
  UART_puts("PFA ADC Beispiel\r\n\r\n");

  DDRD = (1<<PD5); // PD5 Ausgang für LED2

  for(i=0;;i++)
  {
    adcval = ReadChannel(2); 
    schalterpos = stufenschalter(adcval);

#if 0
    /* UART-Anzeige Option 1 */
    werte_anzeigen(adcval, schalterpos); 
#else
    /* UART-Anzeige Option 2 */
    balkenanzeige(schalterpos);          
#endif

    /*
       LED2 blinken lassen
       Blinkfrequenz umgekehrt proportional zum ADC-Wert 
     */
    if (i >= adcval)
    {
      i = 0;
      PORTD ^= (1<<PD5); // Toggle LED2 mit XOR
    }
  }
}

Datei PFA_uart_m8.c

/*
    UART-Routinen für Pollin Funk AVR Board
    Atmega8 @ 12 MHz

    9600/8N1
*/

#include "PFA_uart_m8.h"

void UART_init(void)
{
    // Bausrate setzen
    UBRRH = (uint8_t) ((F_CPU / (16 * BAUD) - 1) >> 8);
    UBRRL = (uint8_t)  (F_CPU / (16 * BAUD) - 1);

    //      Empfangen,   Senden erlauben
    UCSRB = (1<<RXEN) | (1<<TXEN);

    // Frame Format:              8 Bits,                  No Parity,          1 Stopbit
    UCSRC = (1<<URSEL) | ((1<<UCSZ1) | (1<<UCSZ0)) | ((0<<UPM1) | (0<<UPM0)) | (0<<USBS);
}

// Auf Zeichen im UART Eingang prüfen
uint8_t UART_peekchar(void)
{
    // sofort zurückkehren und Zustand melden
    return UCSRA & (1<<RXC);
}

// Auf Zeichen im UART Eingang warten
uint8_t UART_getchar(void)
{
    // Warte bis UART Eingang voll
    while ( !(UCSRA & (1<<RXC)) )
        ;
    return UDR;
}

// Zeichen in UART Ausgang geben
void UART_putchar(char z)
{
    // Warte bis UART Ausgang frei
    while ( !(UCSRA & (1<<UDRE)) )
        ;
    UDR = z;
}

// Zeichenkette (String) in UART Ausgang geben
void UART_puts(char *s)
{
    while ( *s )
    {
        UART_putchar(*s);
        s++;
    }
}

Datei PFA_uart_m8.h

/*
    UART-Routinen für Pollin Funk AVR Board
    Atmega8 @ 12 MHz

    9600/8N1
*/

#ifndef PFA_UART_M8_H
#define PFA_UART_M8_H

#include <avr/io.h>

// 9600 Baud bei F_CPU 12 MHz => Baudratenfehler = +0,2%
#define BAUD 9600L

void UART_init(void);

// Auf Zeichen im UART Eingang prüfen
uint8_t UART_peekchar(void);

// Auf Zeichen im UART Eingang warten
uint8_t UART_getchar(void);

// Zeichen in UART Ausgang geben
void UART_putchar(char z);

// Zeichenkette (String) in UART Ausgang geben
void UART_puts(char *s);

#endif /* PFA_UART_M8_H */

Die Versorgung muss stimmen

Mit dem ADC kann man auch ohne zusätzliche Schaltung eine unbekannte bzw. im einfachen Batteriebetrieb fallende Versorgungsspannung Vcc (AVcc) messen, Dazu wird die zumessende Versorgungsspannung Vcc als Referenz Aref des ADC eingestellt und damit wird die bekannte Spannung der internen Bandgapreferenz Vbg gemessen. Dann wird gerechnet...

/*
    ADC-Beispiel #2: 
    Messung der Vcc mit Hilfe der internen Bandgap-Refernez

    Pollin Fun AVR Board
    Atmega8 @ 12 MHz

    Boardeigene Hardware
    ====================
    UART 9600/8N1

    Zusätzliche Hardware:
    ====================
    -
 */

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>       // utoa()
#include <string.h>       // strlen()
#include "PFA_uart_m8.h"

#define NUM_DUMMY_ADC 8
#define NUM_MESS_ADC  256    // max. 64 wenn uint16_t adcval
#define V_BANDGAP     1300UL // Atmega8

uint16_t vcc_messung(void) 
{
  uint32_t adcval;
  uint16_t i;

  /*
    Aref : AVcc
    Input: Interne 1.3V Bandgap-Referenz
   */
  ADMUX  = (1<<REFS0)|(1<<MUX3)|(1<<MUX2)|(1<<MUX1);

  /* 
     Frequenzvorteiler setzen und ADC enablen

     ADC soll optimal mit 50 kHz bis 200 kHz laufen
     12000000/200000 = 60
     12000000/50000  = 240
     => Vorteiler 
     64   (1<<ADPS2) | (1<<ADPS1)
     128  (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0)
   */
  ADCSRA = (1<<ADEN)|(1<<ADPS2)|(1<<ADPS1);

  /*
     MEHRERE Dummymessungen?
     http://www.mikrocontroller.net/topic/98469#853068

     Experimentieren!
   */
  for(i=0; i<NUM_DUMMY_ADC; i++)
  {
    ADCSRA |= (1<<ADSC);
    while (ADCSRA&(1<<ADSC)) 
    {
    }
    adcval = ADCW;
  }

  /*
     Tatsächliche Messung: 
     Mittelwert aus NUM_MESS_ADC Messungen
   */
  adcval = 0;
  for(i=0; i<NUM_MESS_ADC; i++)
  {
    ADCSRA |= (1<<ADSC);
    while (ADCSRA&(1<<ADSC)) 
    {
    }
    adcval += ADCW;
  }

  ADCSRA &= ~(1<<ADEN); // ADC disable
  return (adcval + NUM_MESS_ADC/2)/NUM_MESS_ADC;
}

/*
   Zahlenwerte über UART ausgeben
   Leichte Pufferung um die Ausgabemenge rel. klein zu halten
 */
void vcc_werte_anzeigen(uint16_t adcval, uint16_t vcc)
{
  static uint16_t last_adcval = 0xFFFF;
  static uint16_t last_vcc = 0xFF;
  uint8_t crlf = 0;
  char buffer[6];
  uint8_t i;

  if (last_adcval != adcval)
  {
    last_adcval = adcval;
    UART_puts("ADC=[");
    utoa(adcval, buffer, 10);
    for (i=5-strlen(buffer); i; i--)
      UART_putchar(' ');
    UART_puts(buffer);
    UART_putchar(']');
    crlf = 1;
  }

  if (last_vcc != vcc)
  {
    last_vcc = vcc;
    UART_puts("  Vcc=[");
    utoa(vcc, buffer, 10);
    for (i=5-strlen(buffer); i; i--)
      UART_putchar(' ');
    UART_puts(buffer);
    UART_puts("] mV");
    crlf = 1;
  }

  if (crlf)
    UART_puts("\r\n");
}

int main(void)
{
  uint16_t adcval;
  uint16_t vcc;

  UART_init();
  UART_putchar('\014'); // FORMFEED
  UART_puts("PFA ADC Vcc Messung\r\n\r\n");

 while(1)
  {
    adcval = vcc_messung();
    /*
        Allgemeine ADC Gleichung (10-Bit):
        V_mess = (V_ref * ADC_mess) / 1024

        Einstellungen:
        V_mess = V_Bandgap (bekannt!)
        V_ref  = Vcc (unbekannt!)
        => 
        Vcc = V_ref = (V_Bandgap * 1024) / ADC_mess
     */
    if (adcval)
    {
      vcc = (V_BANDGAP * 1024) / adcval; // in mV
      vcc_werte_anzeigen(adcval, vcc); 
    }
    else
    {
      UART_puts("ADC Einstellungen kontrollieren (adcval=0)\r\n");
    }
  }
}

Temperaturmessung mit DS1621

Und es war Sommer...
Nein, die riesige 7-Segmentanzeige ist nicht Teil dieses Beispiels :)

In diesem Beispiel soll die Zimmertemperatur regelmäßig gemessen werden und von einem Atmega8 über RS232 an einen PC mit Terminalprogramm übertragen werden.

Der Temperatursensor DS1621 von Maxim kann über den I2C bzw. TWI Bus an AVR angeschlossen werden. Herz der Kommunikation mit dem Sensor ist die Bibliothek TWIMASTER von Peter Fleury (http://jump.to/fleury). Darin wird eine Hardware-TWI-Kommunikation abgewickelt.

Ich hoffe der "Schaltplan" mit den vier Strippen, den beiden Pull-Up-Widerständen und der Adresseinstellung ist verständlich. Melden, wenn das nicht klar ist. Als Pull-Up-Widerstände habe ich 2,2 KOhm genommen, weil diese gerade vorhanden waren und im empfohlenen Bereich (1-10 KOhm) lagen. Details zum I2C-Bus und dessen Hardware finden sich übrigens auf http://www.i2c-bus.org/i2c-primer/

/*
   (Hardware)-TWI Kommunikation mit DS1621 

   HARDWARE:
   Pollin Funk-AVR-Evaluationsboard v1.1
   Atmega8 @ 12 MHz (-Os)
   Maxim DS1621 Temperaursensor mit I2C Interface

   LIBRARY:
   I2C master library using hardware TWI interface
   Peter Fleury <pfleury@gmx.ch>  http://jump.to/fleury
   v 1.3 2005/07/02 11:14:21

   AUSGABE:
   UART 9600 Baud 8-N-1
 
   SCHALTUNG: 
   Pin     Pin            Pin
   Atmega8 J4   Funktion  DS1621
   =========================================
   27 PC4  5 --- SDA ---- 1 -------------+
                                         |
   28 PC5  6 --- SCL ---- 2 ------+      |
                                  |      |
                Tout      3 NC    |      |
                                  |      |
   GND    35 --- GND ---- 4 ---+  |      |
                               |  #      #
                DS1621         |  # Rpu1 # Rpu2
                A2        5 ---+  # 2.2K # 2.2K
                Geräteadresse  |  #      #
                A1        6 ---+  |      |
                Device = 0     |  |      |
                A0        7 ---+  |      |
                                  |      |
   Vcc    36 --- Vcc ---- 8 ------+------+


   http://www.i2c-bus.org/i2c-primer/
   Rpu Pull-up resistance (a.k.a. I2C termination)
   Rpu commonly ranges from 1 kOhm to 10 kOhm
*/

#include <avr/io.h>
#include <util/delay.h>
//#include "PFA_funkhw.h"
#include "PFA_uart.h"   // hier Umdefinition von DEBUG_PRINT mögl.
#include "i2cmaster.h"

#define DS1621_A0       0
#define DS1621_A1       1
#define DS1621_A2       2
#define DS1621_DEVICE   ((0<<DS1621_A2)|(0<<DS1621_A1)|(0<<DS1621_A0))
#define DS1621_ADDRESS  (0x90 | DS1621_DEVICE)
#define MY_SDA          PC4
#define MY_SCL          PC5

void DS1621_init(void)
{
  DEBUG_PRINT("DS1621_init:\r\n");
  DEBUG_PRINT("vor i2c_init\r\n");
  i2c_init();
  DEBUG_PRINT("nach i2c_init\r\n");
  i2c_start(DS1621_ADDRESS + I2C_WRITE);
  DEBUG_PRINT("nach i2c_start\r\n");
  i2c_write(0xEE);
  DEBUG_PRINT("nach i2c_write\r\n");
	i2c_stop();
  DEBUG_PRINT("nach i2c_stop\r\n");
  i2c_start(DS1621_ADDRESS + I2C_WRITE);
  DEBUG_PRINT("nach i2c_start\r\n");
  i2c_write(0xAA);
  DEBUG_PRINT("nach i2c_write\r\n");
  i2c_stop();
  DEBUG_PRINT("nach i2c_stop\r\n\r\n");
  _delay_ms(200);
}


int16_t DS1621_temperatur(void)
{
  int16_t temperatur = 0;

  DEBUG_PRINT("DS1621_temperatur:\r\n");
  DEBUG_PRINT("vor i2c_start\r\n");
 	i2c_start(DS1621_ADDRESS + I2C_READ);
  DEBUG_PRINT("nach i2c_start\r\n");
  temperatur = i2c_readAck();
  DEBUG_PRINT("nach i2c_readAck\r\n");
  i2c_readNak();
  DEBUG_PRINT("nach i2c_readNAK\r\n");
  i2c_stop();
  DEBUG_PRINT("nach i2c_stop\r\n\r\n");
  return temperatur;
}


int main( void )
{
  UART_init();

  // FORMFEED + Intro
  UART_puts("\014(Hardware)-TWI Atmega8<->DS1621\r\n\r\n"); 

  DS1621_init();

  while (1)
  {
    char s[5];
    int16_t temperatur;
    uint8_t i;

    // DS1621 auslesen
    temperatur = DS1621_temperatur();

    // Für UART aufbereiten    
    if (temperatur < 0)
    {
      temperatur *= -1; 
      s[0] = '-';
      // todo
    }
    else
      s[0] = '+';
    
    temperatur %= 1000;
    s[1] = temperatur/100 + '0';
    temperatur %= 100;
    s[2] = temperatur/10  + '0';
    temperatur %= 10;
    s[3] = temperatur + '0';
    s[4] = 0;

    UART_puts(s);
    UART_puts(" \370C\r\n"); // Gradzeichen C

    // Nächste Messung in 10s
    for (i=0; i<10; i++)
      _delay_ms(1000);
  }
}

// EOF PFA_TWI_DS1621.c

Die C-Quelltexte, das DS1621 Datenblatt und eine fertige Intel-HEX-Datei befinden sich im Archiv PFA_TWI_DS1621.zip.

Zu Beginn von main() wird die UART mit 9600-8N1 initialisiert. Anschliessend wird der Sensor initialisiert. Als Geräteadresse (0x90) ist dabei im Datenblatt fest vorgegeben und die Adressbits A0, A1, A2 für einen von 8 möglichen DS1621 an einem I2C Bus sind auf LOW eingestellt bzw. mit GND verbunden.

Die eigentliche Messung ist ein einfacher Aufruf der Funktion DS1621_temperatur(). Die Aufbereitung des Ergebnisses ist primitiv gemacht und die Temperatur wird als Text im 10s Abstand an den PC gesendet.

Temperaturen unter 0°C sind nicht getestet. Man sieht auch an DEBUG_PRINT(), dass ich selbst am experimentieren bin. Wer die Debugtexte lesen möchte, kann in PFA_uart.h DEBUG_PRINT() entsprechend definieren.

Analog Comparator

Die inzwischen gelösten Probleme von "guest" ([19]) mit dem Analog Comparator haben mich zum Experimentieren mit dieser Funktion angeregt. Hier im Wiki findet man auch Infos zum Analog Comparator im AVR-GCC-Tutorial: AC (Analog_Comparator).

Man braucht für die folgenden drei Beispiele z. B. den Atmega8 und eine Drahtbrücke z. B. an dem 40-poligen Stecker J4. Die "Schaltung" arbeitet dann mit der LED2 auf dem Board und mit dem TASTER1 auf dem Board. Die Drahtbrücke verbindet den Taster (Pin PB1 Pinnummer 10 an J4 mit AIN1(PD7) Pinnummer 33 an J4). Die Vergleichsspannung für AIN0 wird auf die interne Bandgap-Referenz gesetzt, so dass kein externer Spannungsanschluss an ANI0(PD6) benötigt wird. Die LED1 an diesem Anschluss bleibt unbenutzt und stört hier nicht.

Das erste Beispiel zeigt eine grundsätzliche Funktion des Analog Comparators. Die beiden Eingänge AIN0 (int. Bandgap) und AIN1 (0V oder 5V über TASTER1) werden miteinander verglichen und wenn sich der Vergleich ändert, wird der Interrupt ausgelöst.

Hierbei wird der Interrupt nicht abhängig vom IST-Zustand des Vergleichs ausgelöst, sondern abhängig von der Änderung zu einem vorhergehenden Zustand:

  • wenn sich der Vergleich ändert (ACIS1 0 und ACIS0 0, toggle Wechsel): Beispiel 1
  • wenn ein Wechsel von AIN1 > AIN0 nach AIN1 < AIN0 stattfindet (ACIS1 1 und ACIS0 0, falling fallende Flanke)
  • wenn ein Wechsel von AIN1 < AIN0 nach AIN1 > AIN0 stattfindet (ACIS1 1 und ACIS0 0, rising steigende Flanke): Beispiele 2 und 3

Es besteht auch die Möglichkeit das Vergleichssignal OCR vor der Hardware, die die Änderung oben bewertet, abzugreifen und auszugeben. Damit kann man direkt, d.h. statisch im IST-Zustand ohne Infos über den vorhergehenden Zustand AIN0 mit AIN1 vergleichen. Das ist in den folgenden Beispielen aber nicht eingebaut.

/*
    Atmega8 @ 12 MHz

    Analog Comparator Beispiel #1

    Funktion
    ========
    Taster1 triggert den Interrupt ANA_COMP_vect 
    und in der ISR wird die LED getoggelt

    Hardware (Pollin Funk AVR Board):
    ================================
    Interne Bandgap als AIN0 (s.u.)
    
    Taster1 über Brücke an AIN1 (PD7) legen
    => Brücke PB1 (#10 an J4) <----> PD7 (#33 an J4)

    Active high LED an PD5
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

ISR(ANA_COMP_vect)
{  
  PORTD ^= (1<<PD5);  // LED toogle
}

int main() 
{  
  DDRD = 1<<PD5; // LED Ausgang

  /*
    Atmega8 Datenblatt S. 193
    PD7 = AIN1 = Analog comperator negative input
    PD6 = AIN0 = Analog comperator positive input

    Register ACSR
    =============
    ACD:  Analog comparator disable
    ACBG: Analog Bandgap an AIN0
    ACIE: Analog comparator interrupt enable
    ACIC: Analog comparator input capture enable
    ACIS1/ACIS0: Analog comparator interrupt mode select
      00 toggle
      01 reserved
      10 falling
      11 rising
   */
  ACSR |= (1<<ACBG);    
  ACSR |= (1<<ACIE);    
  sei();
      
  while(1)
  {
  }
}

Beim zweiten Beispiel löst ein Drücken des Tasters einen Interrupt aus (Trigger). Dadurch wird die LED2 in der ISR eingeschaltet. Nach einer Wartezeit von 1s wird die LED2 dann im Hauptprogramm ausgeschaltet. Ein weiterer Tastendruck innerhalb der Wartezeit verlängert die Wartezeit nicht, d.h. es wird nicht nachgetriggert. Weil _delay_ms() benutzt wird, muss der Quelltext mit Optimierung übersetzt werden (z. B. -Os Option).

/*
    Atmega8 @ 12 MHz

    Analog Comparator Beispiel #2

    Funktion
    ========
    Taster1 triggert den Interrupt ANA_COMP_vect 
    und in der ISR wird die LED angeschaltet.
    In main() wird die LED 1s nach dem auslösenden 
    Tastendruck ausgeschaltet.Ein neuer Interrupt 
    innerhalb des 1s Wartens kann das Ausschlaten 
    nicht verzögern (Nicht nachtriggerbar)

    Hardware (Pollin Funk AVR Board):
    ================================
    Interne Bandgap als AIN0 (s.u.)
    
    Taster1 über Brücke an AIN1 (PD7) legen
    => Brücke PB1 (#10 an J4) <----> PD7 (#33 an J4)

    Active high LED an PD5
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

volatile uint8_t led_ist_an;

ISR(ANA_COMP_vect)
{  
  PORTD |= (1<<PD5);  // LED an
  led_ist_an = 1;
}

int main() 
{  
  DDRD = 1<<PD5; // LED Ausgang

  /*
    Atmega8 Datenblatt S. 193
    PD7 = AIN1 = Analog comperator negative input
    PD6 = AIN0 = Analog comperator positive input

    Register ACSR
    =============
    ACD:  Analog comparator disable
    ACBG: Analog Bandgap an AIN0
    ACIE: Analog comparator interrupt enable
    ACIC: Analog comparator input capture enable
    ACIS1/ACIS0: Analog comparator interrupt mode select
      00 toggle
      01 reserved
      10 falling
      11 rising
   */
  ACSR |= (1<<ACBG);    
  ACSR |= (1<<ACIS1) | (1<<ACIS0);
  ACSR |= (1<<ACIE);    
  sei();
      
  while(1)
  {
    if ( led_ist_an )
    {
      _delay_ms(1000);
      PORTD &= ~(1<<PD5); // LED aus
      led_ist_an = 0;
    }
  }
}

Beim dritten Beispiel löst ein Drücken des Tasters einen Interrupt aus (Trigger). Dadurch wird die LED2 in der ISR eingeschaltet. Nach einer Wartezeit von 1s wird die LED2 dann im Hauptprogramm ausgeschaltet. Im Gegensatz zu Beispiel Zwei verlängert weiterer Tastendruck innerhalb der Wartezeit Wartezeit nicht, d.h. es wird nachgetriggert. Weil _delay_ms() benutzt wird, muss der Quelltext mit Optimierung übersetzt werden (z. B. -Os Option).

/*
    Atmega8 @ 12 MHz

    Analog Comparator Beispiel #3

    Funktion
    ========
    Taster1 triggert den Interrupt ANA_COMP_vect 
    und in der ISR wird die LED angeschaltet.
    In main() wird die LED 1s nach dem LETZTEN
    Tastendruck ausgeschaltet. Eine neu auftretender 
    Interrupt kann also das Ausschalten verzögern 
    (Nachtriggern).

    Hardware (Pollin Funk AVR Board):
    ================================
    Interne Bandgap als AIN0 (s.u.)
    
    Taster1 über Brücke an AIN1 (PD7) legen
    => Brücke PB1 (#10 an J4) <----> PD7 (#33 an J4)

    Active high LED an PD5
*/
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

volatile uint8_t trigger;

ISR(ANA_COMP_vect)
{  
  PORTD |= (1<<PD5);  // LED an
  trigger = 1;
}

int main() 
{  
  DDRD = 1<<PD5; // LED Ausgang
  
  /*
    Atmega8 Datenblatt S. 193
    PD7 = AIN1 = Analog comperator negative input
    PD6 = AIN0 = Analog comperator positive input

    Register ACSR
    =============
    ACD:  Analog comparator disable
    ACBG: Analog Bandgap an AIN0
    ACIE: Analog comparator interrupt enable
    ACIC: Analog comparator input capture enable
    ACIS1/ACIS0: Analog comparator interrupt mode select
      00 toggle
      01 reserved
      10 falling
      11 rising
   */
  ACSR |= (1<<ACBG);    
  ACSR |= (1<<ACIS1) | (1<<ACIS0);
  ACSR |= (1<<ACIE);    
  sei();
      
  while(1)
  {
    if ( trigger )
    {
      do
      {
        trigger = 0;
        _delay_ms(1000);
      } while ( trigger );
      PORTD &= ~(1<<PD5); // LED aus
    }
  }
}

Das Warten mit _delay_ms() in den Beispielen würde man in einer "echten" Anwendung anders lösen. Statt in einer Warteschleife Däumchen zu drehen, muss der Atmega8 ja normalerweise weitere Funktionen abarbeiten. Man könnte z. B. einen Timer benutzen und regelmäßig eine Abfrage mithilfe einer selbstgeschriebenen Timeoutfunktion machen, ob die Wartezeit bereits verstrichen ist.

Pennen bis der Hund bellt

In [20] fragt Alex, wie man mit dem Watchdog einen AVR aus dem Sleep Mode wecken kann. Diese nützliche Anwendung des Watchdogs ist zwar im Artikel AVR-Tutorial: Watchdog genannt, war aber bisher noch ohne Beispielprogramm. Das soll hier ergänzt werden.

/*
    Atmega8 @ 12 MHz --- ODER --- Attiny2313 @ 8 MHz
    Pollin-Funk-AVR-Board
*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/wdt.h>
#include <avr/sleep.h>
#include <util/delay.h>

#define MS_AKTIV 100

// Aktivierten Watchdog nach Reset erstmal schnell abschalten
// http://www.nongnu.org/avr-libc/user-manual/group__avr__watchdog.html
void get_mcusr(void) \
__attribute__((naked)) \
__attribute__((section(".init3")));

#if defined(__AVR_ATmega8__)
#define MCUSR MCUCSR
#endif

uint8_t mcusr_mirror __attribute__ ((section (".noinit")));
void get_mcusr(void)
{
  mcusr_mirror = MCUSR;
  MCUSR = 0;
  wdt_disable();
}

// LED5 auf Pollin-Funk-AVR-Board als Arbeitsanzeige
inline void LED5_an(void)
{
  DDRD |= (1<<PD5);  // Ausgang
  PORTD |= (1<<PD5); // active high LED an
}

inline void LED5_aus(void)
{
  PORTD &= ~(1<<PD5); // active high LED aus
  DDRD &= ~(1<<PD5);  // Eingang
}

// LED6 auf Pollin-Funk-AVR-Board als Sleepanzeige
inline void LED6_an(void)
{
  DDRD |= (1<<PD6);  // Ausgang
  PORTD |= (1<<PD6); // active high LED an
}

inline void LED6_aus(void)
{
  PORTD &= ~(1<<PD6); // active high LED aus
  DDRD &= ~(1<<PD6);  // Eingang
}

#if defined(__AVR_ATtiny2313__)
ISR(WDT_OVERFLOW_vect)
{
}
#endif

int main(void)
{
  uint16_t ms = 0;

  LED6_aus();
  wdt_enable(WDTO_2S);

  while(1)
  {
    // Beispielhafte Arbeitsroutine
    // Mit Watchdogüberwachung gegen schlechtes Benehmen
    wdt_reset();
    LED5_an();
    _delay_ms(1);
    ms++;

    // Prüfen, ob die Arbeitsroutine MS_AKTIV ms gelaufen ist
    // wenn ja, dann SLEEP bis Watchdog nach der Zeit WDTO_2S weckt...
    if (ms == MS_AKTIV)
    {
      LED5_aus();
      LED6_an();
      set_sleep_mode(SLEEP_MODE_PWR_DOWN);
      wdt_reset();
#if defined(__AVR_ATtiny2313__)
      cli();
      WDTCSR |= (1<<WDIE); // WDT Interrupt enable
#endif
      sleep_enable();
#if defined(__AVR_ATtiny2313__)
      sei();
#endif
      sleep_cpu();
      sleep_disable();
      ms = 0;
    }
  }  
}

Die Arbeitsweise beim Atmega8:

Die LED5 blitzt durch LED5_an() und die Schleife über den Zähler ms für MS_AKTIV Millisekunden auf. Ist in der if-Abfrage ms gleich MS_AKTIV, geht der Atmega8 geht nach dem Abschalten der LED5 und Anschalten der LED6 an der Stelle sleep_cpu() in den Schlafmodus. Nach der Zeit WDTO_2S wird der zu Programmstart mit WDTO_2S aufgesetzte Watchdog aktiv und weckt den AVR aus dem Schlafmodus. Bei dem Atmega8 wird durch den Watchdog immer ein System Reset ausgelöst, Dies erkennt man daran, dass LED6 zu Beginn von main ausgeschaltet wird, d.h. flackert. Ohne Reset würde LED6 dauernd angeschaltet bleiben.

Die Arbeitsweise beim Attiny2313:

Der Watchdog des Attiny2313 kann nicht nur einen System Reset durchführen, so wie beim Atmega8 oben, sondern es ist auch möglich einen Interrupt mit entsprechender ISR auszuführen. Dazu muss man im WDTCSR Register einstellen, ob nur der Interrupt ausgeführt wird oder ob zuerst der Interrupt ausgeführt wird (beispielsweise zur Datenrettung ins EEPROM oder zur Anzeige einer Statusmeldung) und dann beim nächsten auftretenden Watchdog der System Reset.

Im Beispielprogramm wurde unmittelbar vor dem Sleep der Interrupt Modus plus System Reset Modus eingestellt. Die ISR kann dabei leer sein - es ist nichts zu tun - aber die ISR muss natürlich vorhanden sein. Beim Aufwachen aus dem Sleep löscht der AVR automatisch das WDIE Bit und der nächste Watchdog wird zu einem System Reset führen, d.h. man hat die normale Watchdogfunktion zur Überwachung seines Programms.

Den Unterschied zwischen Atmega8 und Attiny2313 Programm sieht man an dem Verhalten der LED6. Beim Attiny2313 bleibt die LED6 dauerhaft an, d.h.der Programmteil zwischen main und while wird nur einmal beim Power-Up bzw. sonstigen Resets außer den Watchdog-Resets durchlaufen.

Forenbeiträge

Scratchpad

(Vorgemerktes für neue Abschnitte, Ergänzungen und Korrekturen)

  • Differentielle ADC-Messung [25]
  • Mittelwertbildung und gleitender Durchschnitt [26], [27]
  • LED Dimmen über ADC [28]
  • RC5 Erweiterung [29]
  • Piezosummer ohne Elektronik [35]

Weblinks