Pollin Funk-AVR-Evaluationsboard
Aufbau
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 lies 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).
Funktionstests
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 bei AVR nicht eingesetzt, MAX232 nicht eingesetzt
- ca. 22 mA bei AVR nicht eingesetzt, MAX232 nicht eingesetzt, RESET gedrückt
- ca. 25 mA bei AVR nicht eingesetzt, MAX232 eingesetzt
- ca. 26 mA bei AVR nicht eingesetzt, MAX232 eingesetzt, RESET gedrückt
- ca. 30 mA bei ATmega8 eingesetzt, MAX232 eingesetzt
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.
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
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 RC
@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
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 RC
@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
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.
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
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
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.
<c> /*
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);
}
} </c>
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.
<c>
/*
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);
}
}
}
</c>
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.
<c>
/*
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;
}
}
</c>
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.
<c>
/*
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.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;
}
}
}
</c>
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.
Die Datei zaehler_lcd.c enthält die main-Funktion und die neue Ausgabefunktion ausgabe_lcd und die bereits bekannte Funktion ausgabe_led:
<c>
/*
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;
}
}
}
</c>
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.
<c>
// 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++;
}
}
</c>
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.
<c>
// 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
</c>
Die Includedatei funkhw.h enthält die gemeinsamen, boardspezifischen Port-Definitionen, die in mehreren Unterdateien benötigt werden:
<c>
/*
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
</c>
Die Includedatei rs232.h enthält die Prototypen für die Funktionen in rs232.c:
<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);
</c>
Und zum Schluss noch die Datei rs232.c mit den ungeänderten UART-Grundfunktionen und der rs232-Funktion.
<c>
/*
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.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");
}
}
</c>
Eine Uhr
Nachdem jetzt ein paar 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 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 das ist noch genug, um damit eine Uhr zu machen und die anderen Timer für anderes freizuhalten.
TIMER0 ist ein 8-Bit Timer, d.h. kann so einstellt werden, dass er ab einem Startwert TCNT0 jedesmal um eins hochzählt, wenn er von seiner Taktquelle angesprochen wird.
Die Taktquelle für den TIMER0 kann die Taktquelle des AVR sein oder der durch den sog. Vorteiler (Prescaler) von 8, 64, 256 oder 1024 geteilte CPU-Takt. Welcher Takt verwendet werden soll, wird im Register TCCR0 eingestellt.
Wenn der Endwert 255 (8-Bit!) überschritten wird und er 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 vor dem Einschalten meistens 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, die in diesem Beispiel aber nicht benötigt wird.
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 N 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) = N = 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 |
Bei keinem anderen Prescaler als 1 kann man also bequem eine ganze Zahl von Overflow-Interrupts abpassen, so dass man nach einer ganzen Zahl von Zählschritten glatt auf 1 Sekunde kommt. Und wenn man mit dem Prescaler 1 arbeitet, treten die Interrupts zigtausend Mal auf, so dass das Programm nur noch am nutzlosen Zählen ist... das ist keine Lösung. 12 MHz ist einfach ein ungünstiger Ausgangstakt für eine Uhr...
Aber es gibt zwei Auswege. 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 N/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 = N = 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 (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 * 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.
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.
Die Ungenauigkeit kommt 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.
<c>
// // 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;
ISR(TIMER0_OVF_vect)
{
static uint8_t ticks = 0;
ticks += 1;
if ( ticks == 183 )
{
// Letzter Zeitabschnitt kürzer, um auf 1s zu kommen
- if 1
TCNT0 = 256 - 27; // ungenau
- else
TCNT0 = 256 - 27 + TCNT0; // genauer
- 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;
}
}
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();
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.c
</c>
PFA_funkhw.h - Definitionen für das PFA ;-) Board <c>
// // 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
</c>
PFA_rs232.c - Erweiterte rs232-Funktionen <c>
// // 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.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; }
}
</c>
PFA_rs232.h - Prototypen zu den rs232-Funktionen <c>
// // 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
</c>
PFA_uart.c - UART Grundfunktionen (aus ehemaligem rs232.c herausgezogen) <c>
// // 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
</c>
PFA_uart.h - Prototypen zu den UART Grundfunktionen <c>
// // 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
</c>
(Wird fortgesetzt)
Forenbeiträge
Weblinks
- Datenblatt Funk-AVR-Evaluationsboard (PDF) Best.nr. 810046 von www.pollin.de (derzeit ist das Board nicht im Angebot Stand 3/2008)
- Datenblatt Atmega8 (PDF)
- Erfahrungsbericht von Marco Schmoll (www.controller-designs.de)

