Atmel AVR Mikrocontroller mit OpenSuse: USB Nutzung

Serielle Schnittstellen sind an heutigen PCs immer seltener vorhanden. Der logische Ersatz des alten RS232 Standards ist die USB-Schnittstelle.

Neuere AVR Controller unterstützen den USB-Standard direkt, indem auf dem Chip USB-Controller-Funktionen integriert wurden. Die „älteren“ AVR-Modelle besitzen selbst keine USB-Unterstützung. Dies ist jedoch kein Hindernis, da externe USB-Controller am Markt verfügbar sind.

Bei der Nutzung von RS232 (ohne USB) wird die UART-Funktionalität des AVR Controllers genutzt (UART=Universal Asynchronous Receiver and Transmitter). Diese erzeugt aus eingehenden Bytes einen Bit-Strom. Der Bit-Strom besteht aus den Nutz-Bits (also den 8 Bits des Datenbytes) sowie Steuerbits, die die UART selbsttätig in den Datenstrom einfügt und die das RS232 Leitungsprotokoll umsetzen. Wesentlich ist dabei das immer eingefügte Start-Bit, welches den Start eine Bytes markiert und optionale weitere Start-Bits und optionale Stop-Bits sowie ein Paritätsprüfungsbit. Die Anzahl der Start/Stop-Bits, Parität und vor allem die Baudrate wird über Kontrollregister der UART geregelt. Da der AVR Controller üblicherweise mit +5V betrieben wird, die RS232 Physik aber mit höheren Spannungen (+-12V) funktioniert, wird nur noch ein externer Chip zur Umsetzung der Spannungspegel benötigt. Dies ist typischerweise ein Baustein der Art MAX232.


Der MAX232 für die „alte“ RS232 Schnittstelle, hier in SMD-Bauform, es sind aber auch DIL-Gehäuse verfügbar

Bei der Nutzung von USB mit dem AVR gibt es unterschiedliche Konzepte:

  • V-USB, einer reinen Software-Implementierung der USB-Anbindung. Dabei werden einfach Port-Pins eines AVRs als USB-Leitungen verwendet und die gesamte USB-Logik im AVR implementiert. Diese Lösung ist simpel, aber leider nicht allzu schnell.
  • Nutzung eines externen USB-Controller-Chips. Dieser Ansatz ist im folgenden ausführlich beschrieben.

AVR mit externem USB-Controller

Am Markt verfügbar sind diverse USB Controller Chips. Ein beliebter Kandidat ist der FT232. FTDI hat diesen Chip sicher bewusst ähnlich benannt wie den „alten“ MAX232. Ganz grob formuliert: Während der MAX232 die Signale eines Mikrocontrollers auf RS232-Physik umsetzt, kann man beim FT232 sagen, dass dieser die Signale des Controllers auf die USB-Physik umsetzt.

Damit ist auch schon in etwa beschrieben, wie der AVR mit den F232 verbunden wird. Wie schon beim reinen RS232 wird die UART genutzt, um aus einem Datenbyte einen Bitstrom zu erzeugen. Dieser Bitstrom wird in den FT232 eingespeist. Den „Rest“, also die Umsetzung in USB-Pakete, erledigt der FT232. Die umgekehrte Richtung funktioniert genauso, der FT232 extrahiert aus dem USB-Datenstrom die für ihn bestimmten Bits und legt diese an den Eingang der AVR UART. Diese macht daraus in gewohnter Weise Datenbytes.
Man könnte also auch sagen, dass der serielle Datenstrom im USB-Datenstrom getunnelt wird.


Der FT232RL für USB, nur in SMD Gehäusen verfügbar

PC-seitige Voraussetzung

Wenn AVR und USB-Controller an ihre Betriebsspannung angelegt werden und der USB-Controller per USB-Kabel mit einem PC verbunden wird, meldet sich der USB-Controller mit einer Art eindeutiger Kennung beim USB-System des PCs an (VendorId + ProductId). Beim FT232 ist dies z.B. idVendor=0403 und idProduct=6001. Anhand dieser Parameter kann das Subsystem den dazu passenden USB-Treiber laden und verwenden. Beim FT232 und unter OpenSuse gilt:

  • Der Treiber von FTDI ist bereits im Standardkernel verfügbar und wird automatisch geladen, wenn der FT232 sich am USB-System anmeldet
  • Die Geräteklasse des FT232 ist eine serielle Schnittstelle. Linux bindet das Gerät daher über eine /dev/ttyUSBxx Gerätedatei ins System ein.

Unter Windows muss vermutlich der Treiber von FTDI nachinstalliert werden.
Web-Adresse zu den Treibern: http://www.ftdichip.com/FTDrivers.htm

(unter Windows noch zu machen)

Die dmesg-Ausgabe unter Linux bringt folgendes:

[ 4251.658015] usb 6-2: new full speed USB device using uhci_hcd and address 2
[ 4251.847030] usb 6-2: New USB device found, idVendor=0403, idProduct=6001 
[ 4251.847034] usb 6-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 4251.847037] usb 6-2: Product: FT232R USB UART
[ 4251.847040] usb 6-2: Manufacturer: FTDI
[ 4251.847042] usb 6-2: SerialNumber: A9009NnL
[ 4251.886707] usbcore: registered new interface driver usbserial
[ 4251.886722] USB Serial support registered for generic
[ 4251.886758] usbcore: registered new interface driver usbserial_generic
[ 4251.886761] usbserial: USB Serial Driver core
[ 4251.894285] USB Serial support registered for FTDI USB Serial Device
[ 4251.894362] ftdi_sio 6-2:1.0: FTDI USB Serial Device converter detected
[ 4251.894424] usb 6-2: Detected FT232RL 
[ 4251.894428] usb 6-2: Number of endpoints 2
[ 4251.894431] usb 6-2: Endpoint 1 MaxPacketSize 64
[ 4251.894433] usb 6-2: Endpoint 2 MaxPacketSize 64
[ 4251.894435] usb 6-2: Setting MaxPacketSize 64
[ 4251.896108] usb 6-2: FTDI USB Serial Device converter now attached to ttyUSB0 
[ 4251.896125] usbcore: registered new interface driver ftdi_sio
[ 4251.896127] ftdi_sio: v1.5.0:USB FTDI Serial Converters Driver

Aus der dmesg-Ausgabe erkennt man auch die maximale Paketgröße von 64 Bytes.

Wenn man nun AVR-seitig ein kleines Programm schreibt, dass über eine UART Daten ausgibt und man das bisher durch einen an die UART angeschlossenen MAAX232 als RS232-Datenstrom über die serielle Schnittstelle eingelesen hatte, kann man nun dieselben Daten via USB einlesen, wenn man einfach die UART mit den FT232 verbindet. Da der FT232 z.B. als /dev/ttyUSB0 eingebunden wurde, kann man PC-seitig einfach ein Terminalprogramm starten und statt /dev/tty… nun /dev/ttyUSB… als zu verwendende Gerätedatei angeben. Im Terminal werden die Daten dann wie gewohnt dargestellt. Ich verwende für solche Sachen das Programm kermit, und dort legt man die zu verwendende Schnittstelle fest mit:
    set line /dev/ttyUSB0

Hier der Start von kermit und die Ausgaben des AVRs:

dennis@socraggio:~> kermit 
 C-Kermit 8.0.211, 10 Apr 2004, for Linux 
  Copyright (C) 1985, 2004, 
   Trustees of Columbia University in the City of New York. 
 Type ? or HELP for help. 
 (/home/dennis/) C-Kermit>set line /dev/ttyUSB1 
 (/home/dennis/) C-Kermit>c 
 Connecting to /dev/ttyUSB1, speed 19200 
  Escape character: Ctrl- (ASCII 28, FS): enabled 
 Type the escape character followed by C to get back, 
 or followed by ? to see other options. 
 ---------------------------------------------------- 
 21178 (mo=0;fa=0;ma=0;be=0),steps (r/d): 0000/0000, (mark=X,A=-60,B=60,act=0), last cmd: (nop)

Die letzte Zeile, beginnend mit 21178… ist die vom AVR erzeugte Ausgabezeile.

Daten via USB lesen und schreiben

Als nächstes soll betrachtet werden, wie man von einem eigenen Programm aus via USB kommuniziert.

Linux

Da der FT232 linux-seitig wie ein normaler serieller Port aussieht, kann man auch die normalen Kommandos zu Nutzung des Ports verwenden. Alles was man zur Programmierung eines seriellen Ports wissen muß findet man in der „Serial Programming HowTo„.
Die zu nutzenden Funktionen sind:

  • open(), read(), write(), close() für den Datentransfer
  • Für die Konfiguration des Ports die Funktionen tcgetattr(),  tcsetattr() und tcflush()

Im erwähnten HowTo ist auch Beispielcode zu finden.

Nutzbare Geschwindigkeit

Da der FT232 genauso angesprochen wird wie ein altes Terminal, sind auch die Definitionen für die Baud-Raten gültig. Ich habe einige Tests gemacht, hier die Ergebnisse. Die Daten wurden immer in 64 Byte großen Blöcken aus dem Hauptprogramm im AVR an die UART übergeben.

19200 und 38400 Baud (oder noch weniger) sind kein Problem. Da ich auch eine Zeitmessung durchführe und die tatsächliche Transferrate (Nettorate) berechne, stelle ich fest, dass diese ziemlich genau 80% der Baudrate beträgt (durch Start- und Stopbit verursacht, die allgemein bekannten Baudraten-Werte sind also Brutto-Zahlen: 8 Nutzbits+2 Protokollbits ergeben  den Faktor von 8/10).

Bei 57600 Baud gibt es unreproduzierbare Lesefehler.
Nachlesen bringt die Erkenntnis, dass die Baudraten unterschiedlich gut unterstützt werden, je nach Quarzfrequenz des Controllers. Durch Teilung und Rundungen entsteht eine leichte Abweichung in der tatsächlich gefahrenen Baudrate, die dann im PC eventuell nicht sauber erkannt wird. Ein Rechner, der zu einer Taktfrequenz die unterschiedlichen Rundungsfehler zu den Baudraten berechnet findet sich hier: http://www.gjlay.de/helferlein/avr-uart-rechner.html

Ich probiere auch 115.200 und 230.400 Baud, die bei 16Mhz eine ziemlich große Abweichung von den Sollwerten haben. Und tatsächlich erkennt mein PC nichts im Datenstrom.

500.000 Baud haben bei 16Mhz einen Fehler von 0. Ich stelle es mutig ein und tatsächlich, diese Rate wird wieder sauber erkannt. Auch 250.000 Baud hätten einen Fehler von 0, aber das kann mein PC nicht. Unter Linux/GNU-gcc findet man die möglichen Baud-Raten übrigens unter /usr/include/bits/termios.h. Diese Datei wird via „#include <termios.h>“ indirekt eingebunden. Die Baudraten sind Defines der Art „B57600“ und gehen bis 4Mega-Baud.

Schließlich probiere ich noch 1, 2 und 4 MBaud. Mehr als 4MBaud kann der PC nicht und auch beim FT232 ist laut Foren irgendwo bei 3MBaud Schluss.
1 MBaud funktioniert ebenfalls sehr gut. Dabei kann ich Netto-Datenraten von 55KByte/s (etwa 445KBit/s) messen. Das ist ein sehr hoher Wert, den ich eigentlich nicht erwartet hätte.
2 und 4 MBaud bringen keine brauchbaren Ergebnisse, im Dump des Datenstrom kann ich keine korrekt übertragenen Daten erkennen.

Vertiefende Infos, Links

Atmel AVR Mikrocontroller mit OpenSuse

Mikrocontroller der Firma Atmel beinhalten eine CPU sowie RAM und EEPROM auf einem Chip. Es gibt Controller mit unterschiedlichen Leistungsmerkmalen, allen gemeinsam sind aber Grundgegebenheiten (Aufbau, Assembler-Dialekt). Im Umfeld dieser Controller hat sich eine starke, weltweite Community gebildet, so dass sowohl hardwaremäßig als auch softwaremäßig auf zahlreiche Vorleistungen zurückgegriffen werden kann.

Im folgenden ist der generelle Umgang mit AVR Mikrocontrollern beschrieben. Es wurde ausschließlich Open Source Software benutzt.

Wegen der Verbreitung der AVR-Controller gibt es viele C-Bibliotheken, die komplexere Tätigkeiten ausführen können. So gibt es zum Beispiel Bibliotheken für LCD-Ansteuerung, Echtzeituhransteuerung, RS232-Ansteuerung etc., so dass man nicht alles selbst programmieren muss.

Für fast jedes Problem mit dem AVR findet man im Internet schon Informationen oder fertige Lösungen. Ein bisschen hiervon wird im folgenden vorgestellt.

Inhaltsübersicht

Das „Hello World“ für AVR

Im folgenden ist ein einfache Programm dargestellt, das eine LED auf dem Board blinken lässt.

Eine Verzögerungsschleife (Funktion delay_ms) sorgt dafür, dass man das An- und Ausgehen der LED auch wahrnehmen kann. Die dort genannten Werte variieren nach der Taktfrequenz des Controllers, sollten aber für 8-12 Mhz tun.

Die LED hängt im Beispiel an Port D, Bit 1. Die ganzen Spezifika des Controllers sind in Include-Files avr/*.h abgelegt (Teil der Software von Atmel). Dort ist auch festgelegt, welches Register bei einem bestimmten Controller (z.B. ATMega8) für Port D, Pin 1 angesprochen werden soll. Dies verbirgt sich hinter der Präprozessor-Makro „PD1“. Inhaltlich wird das Bit 1 des Ports D auf Ausgabe geschaltet, und dann in einer while-Schleife abwechselnd auf 0 und auf 1 gesetzt. Zwischen den Wechseln wird jeweils gewartet. Im Ergebnis ist das ein Blinken.

#include <avr/io.h>
 
 void delay_ms(unsigned short ms)
 /* delay for a minimum of <ms> */
 /* with a 1Mhz clock, the resolution is 1 ms */
 {
    unsigned short outer1, outer2;
    outer1 = 50*12;
 
    while (outer1) {
       outer2 = 1000;
       while (outer2) {
          while ( ms ) ms--;
             outer2--;
       }
                 outer1--;
    }
 }
 
 void main(void)
 {
    /* INITIALIZE */
    /* enable PD1 as output */
    DDRD|= _BV(PD1);
 
    /* BLINK, BLINK ... */
    while (1) {
       /* pin=0, means LED on */
       PORTD &= ~_BV(PD1);
       delay_ms(500);
       /* pin=1, means LED off */
       PORTD|= _BV(PD1);
       delay_ms(500);
    }
 }

Port Pins abfragen und beschreiben

Jeder AVR besitzt eine Menge von Ports. Pro Port gibt es normalerweise 8 Pins. Ein Port kann als Eingang oder als Ausgang konfiguriert werden.

Wenn es ein Eingang ist, kann man dort den anliegenden logischen Wert abfragen (z.B. von einem Taster/Schalter).

Wenn es ein Ausgang ist, kann man dort einen logischen Wert setzen, der von der Peripherie (z.B. eine angeschlossene LED) genutzt wird.

Ein Port korrespondiert zu einem 8-Bit-Register des AVR. D.h. wenn man z.B. dort einen Wert hineinschreibt, hat dies Auswirkungen auf den zugeordneten Pin. So korrespondiert z.B. das niederwertigste Bit von Port A zu dem Pin namens PA0. Falls dieses Port nun als Ausgabeport konfiguriert wurde und man schreibt nun eine „1“ in die Bitposition 0 des Registers von Port A, so wird der Pin PA0 logisch 1.

Vor Nutzung eines Ports muss man es zunächst als Ein- oder Ausgang konfigurieren. Es gibt in der Sprache C und bei Nutzung der libavrc diverse Makros für Register und Bitpositionen, die man nutzen sollte:

PORTA, PORTB, … :  Portnamen für Ausgangsports
PINA, PINB, … : Portnamen für Eingangsports
DDRA, DDRB, … : Register, die die Konfiguration (Eingang, Ausgang) festlegen
PA0, PA1, …, PB0, PB1, … : Bitpositionen bzw. Pin-Namen (Ausgang)
PINA0, PINA1, … : Bitpositionen bzw. Pin-Namen (Eingang)

Alles folgende am Beispiel des Ports D (DDRD, PORTD, PIND), Pin 3 (PD3,PIND3). Die Makros PORTD und PD3 sind dabei für die Ausgabe, die Makros PIND und PIND3 für die Eingabe zu nutzen.

Konfiguration als Ausgang:

DDRD |= _BV(PD3); // Bit auf 1 setzen

_BV(x) ist hierbei ein Makro, welches  in einem Byte die Bit-Position x auf logisch 1 setzt, alle anderen Bits sind auf 0. Dieser Wert wird nun mit dem momentanen Inhalt von DDRD geodert und damit an dieser Stelle logisch 1. Alle anderen Werte bleiben unverändert.

Setzen des Bits:

PORTD |= _BV(PD3);

Löschen des Bits:

PORTD &= ~_BV(PD3);

Konfiguration als Eingang:

DDRD &= ~_BV(PIND3);

Lesen des Bits:

bitvalue = bit_is_set(PIND, PIND3);
// oder: 
bitvalue = PIND & _BV(PIND3) 
// oder auch mit
bitvalue = bit_is_clear(PIND, PIND3);

wobei bit_is_set() und bit_is_clear() wieder Makros der avrlibc sind.

Damit lassen sich alle Pins des AVR für Ein- und Ausgabezwecke nutzen. Allerdings muss beachtet werden, dass manche Pins zum Teil mit mehreren Funktionen belegt sind und daher nicht völlig frei genutzt werden können.

Schließlich besitzen die ATmega AVRs auch noch pro Input-Pin einen internen Pullup-Widerstand der irgendwo zwischen 20 und 50KOhm bemessen ist. Defaultmäßig ist dieser Pullup-Widerstand abgeschaltet. Wenn man ihn nutzen will, kann er eingeschaltet werden, indem man auf den bereits als Eingang konfigurierten Pin eine logische 1 schreibt. Dies versteht der AVR als Aufforderung, den Pullup-Widerstand zuzuschalten. Code hierzu:

DDRD &= ~_BV(PIND3);
PORTD |= _BV(PD3);

Tasteneingabe

Wie kann man mit einem Taster oder einem Schalter eine zuverlässige Eingabe für den AVR machen? Einfach ein Port abfragen langt nicht, denn der elektrische Vorgang des „Prellens“ eines Schalters produziert kein klares und eindeutiges Ein- oder Aus-Signal. Es werden pro Einschalten oder Ausschalten gewissermaßen mehrere Einsen und Nullen erzeugt, bis der Schalter/der Taster zur Ruhe kommt.


Im Bild dargestellt ist der Vorgang des Prellens beim Drücken (Übergang von 0 nach 1) und beim Loslassen (Übergang von 1 nach 0) des Tasters. Dasselbe physikalische Problem existiert auch bei der Verwendung von Schaltern statt Tastern oder auch wenn man einfach zwei Leitungen zusammendrückt.

Man hat also die Aufgabe des Entprellens. Dies kann softwaretechnisch wie folgt gelöst werden (Beispiel für Taste an Port C0):

void wait_until_key_pressed(void)
 {
     unsigned char temp1, temp2;
     unsigned int i;
 
     do {
         temp1 = PINC;                  // read input
         for(i=0;i<65535;i++)
                 ;
         temp2 = PINC;                  // read input
         temp1 = (temp1 & temp2);       // debounce input
     } while ( temp1 & _BV(PINC0) );
 
     loop_until_bit_is_set(PINC,PINC0); // wait until key is released
 }

Das oben dargestellte Entprellen von Tasten wird durch „aktives“ Warten, also das Durchlaufen von Verzögerungsschleifen implementiert. Während des Abarbeitens der Schleife kann der AVR im Hauptprogramm nichts anderes tun, es wird also Rechenleistung verschwendet. Dies kann durch die Abfrage des Tasterzustands innerhalb einer Interrupt-Routine verbessert werden. Im folgenden ist die Implementierung für einen Taster gezeigt.

 // in main()
 // Werte, die die key1-Variable annehmen kann 
 #define KEY_CLEAR 0
 #define KEY_SHORT_PRESSED 1
 
 // key1 enthaelt den Zustand des Tasters/Schalters
 volatile uint8_t key1;
 // Zaehler fur "Taste gedrueckt"
 volatile key1_presscount;
 
 
 // Zaehlerwert, ab dem ein Tastendruck erkannt wird
 #define SHORT_PRESS_VAL 100
 
 ...
   // Abfrage des Tasters
  if (key1==KEY_SHORT_PRESSED) {
   execute_some_command();
   key1 = KEY_CLEAR; // re-enable key input
 
 ...

Und im folgenden der Codeteil für die (Timer-) Interrupt Service Routine

// in ISR
 ...
 if (key1==KEY_CLEAR) {
  // read pin
  bitval = bit_is_clear(PINC,PINC0);
  if (bitval)
   key1_presscount++; // key is pressed, increment press count
  else
   key1_presscount=0; // key is released, reset press count
 
  // if key is pressed at least for the time required to increment key1_presscount to
  // the value of SHORT_PRESS_VAL, we have detected a (short) key press
  if (key1_presscount>SHORT_PRESS_VAL)
         key1=KEY_SHORT_PRESSED;
 }
 
 ...
 
 }

Setzen der Fuse-Bits, so dass ein externer Quarz genutzt wird

Der AVR kann entweder von einem internen Taktgeber oder einem externen Takt gespeist werden. Fabrikneu ist der AVR auf seinen internen Taktgeber eingestellt. Dies ist ein RC-Glied, welches auf 1 MHz eingestellt ist. Damit kann man schon vieles machen und spart sich eine externe Beschaltung. Wenn man eine RS232-Schnittstelle bedienen will, sind so allerdings nur bis maximal 4800 Baud möglich. Wenn man den AVR höher takten will, muss man ihn so umstellen, dass er einen externen Takt nutzt. Dieser kann von einem externen RC-Glied, einem externen Quarz oder einem externen Oszillator kommen. Je nach externer Beschaltung müssen die zugehörigen Fuse–Bits (CKSEL*) passend gesetzt werden. Das Setzen und Auslesen der Fuse-Bits kann mit dem Programm ponyprog geschehen. Für das Beispiel eines ATmega 32 hier der fabrikneue Zustand (zum Auslesen den Button „Read“ drücken):


Fuses im ATMega 32 in fabrikneuem Zustand. Ein Häkchen heißt bei ponyprog, dass die Fuses nicht gesetzt sind. Bei anderen Tools ist dies u.U. genau anders herum.

Und hier der Zustand nach dem Umstellen auf einen externen Quarz. Das Schreiben der Fuses erfolgt nach Drücken des Buttons „Write“.
Achtung, je nach AVR-Typ kann das anders aussehen. Entweder Handbuch oder verlässliche Internet-Quelle konsultieren. Andernfalls kann es passieren, dass der AVR nicht mehr weiter verwendet werden kann.

Der externe Quarz wird auf „übliche Weise“ mit 2x22pf Keramikkondensatoren gegen Masse an den Pins XTAL1 und XTAL0 angeschlossen.

Auch mit dem Tool „avrdude“ können die Fuses gelesen und beschrieben werden. Leider können die Fuses aber nicht mit ihrem Namen angesprochen werden.

Beispiele:
Abfragen der Fuses bei einem ATmega 644 über den seriellen Anschluss:

avrdude -v -p m644 -c ponyser -P /dev/ttyS1 -U lfuse:r:-:i

Abfragen der Fuses  für einen 644P via USB-Anschluss

avrdude -v -p m644p -c avrisp2 -P usb:884:92 -U lfuse:r:-:i

Umstellung eines ATmega32 auf Nutzung eines externen Quarzes über seriellen Anschluss:

avrdude -v -p m32 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xcf:m

Umstellung eines ATmega644 auf Nutzung eines externen Quarzes über über seriellen Anschluss:

avrdude -v -p m644 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xdf:m

Umstellung eines ATmega644 auf Nutzung eines externen Quarzes über USB:

avrdude -v -p m644 -c avrisp2 -P usb:884:92 u -U lfuse:w:0xff:m -U hfuse:w:0xdf:m

Umstellung eines ATmega8 auf Nutzung eines externen Quarzes über USB:

avrdude -v -p m8 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xd9:m

Umstellung eines ATmega644P auf Nutzung eines externen Quarzes über USB:

avrdude -v -p m644p -c avrisp2 -P usb:884:92 -U lfuse:w:0xff:m -U hfuse:w:0xdf:m -U efuse:w:0xff:m

Die aus der Belegung der Fuses resultierenden HEX-Werte und auch die Kommandozeile für AVR Dude können mittels des „AVR Fuse Calculator berechnet werden: http://www.engbedded.com/fusecalc

Verwendung von Timern

Die AVR Controller besitzen meist mehrere Timer. Mit diesen lassen sich zyklisch wiederkehrende Aufgaben bewältigen. Es gibt 8-Bit und 16-Bit Timer. Ein Timer kann z.B. so programmiert werden, dass er bei Ablauf eines voreingestellten Sollwerts einen Interrupt auslöst. Es existieren zahlreiche andere Funktionen in Verbindung mit Timern. Ausprobiert habe ich den Interrupt-Fall mit einem 8-Bit Timer am Mega 32.

Man kann mithilfe des Timers eine Interrupt-Routine aufrufen und/oder spezielle Pins des AVRs (OCn) periodisch setzen und löschen. Mit letzterer Funktion kann man ziemlich einfach eine stabile Frequenz an einem Pin des AVRs erzeugen und muss sich nach der Initialisierung des Timers nicht mehr um die Frequenzerzeugung kümmern (Waveform Generation Mode).

Mittels des folgenden Fragments wird eine Variable definiert, die vom der Timer Interrupt Service Routine verändert wird. Variable, die von Interrupts beschrieben werden und die auch vom Hauptprogramm genutzt werden sollen, müssen als „volatile“ definiert werden:

volatile uint8_t timer2;    // timer value

Die eigentliche Service Routine zählt die definierte Variable hoch:

// Interrupt Service Routine 
ISR(TIMER2_COMP_vect) {
 timer2++; 
}

Die folgende Funktion kann nun von der Anwendung aus genutzt werden, um einen definierten Zeitraum nichts zu tun:

inline void sleep(uint16_t milli ) {
     uint8_t t=4;
     int i;
     
     for(i=0; i<milli; i++) {
        timer2 = 0;
        while (timer2 < t);
     }
 }

Hier als Anwendungsbeispiel eine Art „Uhr“, die in einer Endlosschleife alle Sekunde eine Art Uhrzeit ausgibt:

int main(void) {
     char buf[32];
     int h=0,m=0,s=0;
 
     ...
 
     // CTC mode - clear on compare match
     TCCR2 =  _BV(WGM21);   
     // clock prescaler - CS21 means prescaler 8, see atmega manual
     TCCR2 = _BV(CS21);  
      
     // compare value - to be calculated, see atmega manual
     OCR2  = 250; 
 
     // if interrupt is required, enable it - Ooutput Compare Interrupt
     TIMSK |= _BV(OCIE2);    
     sei(); // Interrupts freigeben
 
     //  if output pin OC2 shall be used, set register flag - toggle on match
     // not used here
     //TCCR2 |= _BV(COM20);
 
     ...
 
     for(;;) {
         sleep(1000);
         s++;
         if (s>=60) {
             s=0;
             m++;
             if (m>=60) {
                m=0;
                 h++;
                 if (h>=24) {
                     h=0;
                 }
             }
         }
         sprintf( buf, "%02d:%02d:%02dr", h, m, s);
         DO(buf);
      }
 }

CTC Modus

Die Zeiten für den Timer werden durch den Prescaler-Wert und den Compare-Wert des Timers festgelegt.

Der Prescaler-Wert ist ein Teiler, der auf die Taktfrequenz des Controllers angewandt wird. Z.B. lässt sich so die Taktfrequenz von 8 MHz mit dem Teiler 64 auf 125 Khz herunterteilen oder mit dem Teiler 8 auf 1Mhz. Mit dieser Frequenz wird der Timer getaktet.

Der Compare-Wert ist der Wert, bei dessen Erreichen der Interrupt ausgelöst wird. Wenn man also z.B. einen Teiler von 64 hat und den Compare-Wert auf 62 einstellt, wird der Interrupt bei 8Mhz Takt 992,06-mal pro Sekunde ausgelöst, also fast genau jede Millisekunde.

Die zugrundeliegende Formel beim ATmega32 ist laut Handbuch:

f_OCn = f_CLK / (2*N*(1+OCRn))

wobei:
f_OCn: Frequenz an Pin OCn
f_CLK: Taktfrequenz des AVRs
N: Prescaler Wert
OCRn: Wert des Registers OCRn

Umgestellt nach OCRn gilt:

OCRn = (f_CLK / (2*N*f_OCn)) – 1

Im folgenden  sind diese beiden Formeln als JavaScript verfügbar.

CTC Modus: Berechnung der Frequenz aus Taktfrequenz, Prescaler und OCR-Wert

OCR value (0..255 for 8-Bit-OCR, 0..65535 for 16 Bit OCR)
Prescaler value
CPU clock value [Hz]Resultierende Frequenz an OCn berechnenFrequency [Hz]

CTC Modus: Berechnung von OCR aus Taktfrequenz, Prescaler und gewünschter Frequenz

Desired frequency value [Hz]
Prescaler value
CPU clock value [Hz]Benötigten OCR-Wert berechnenOCR value (0..255 for 8-Bit-OCR, 0..65535 for 16 Bit OCR)

Wer allerdings auf der Basis eine Uhr bauen will, wird feststellen, dass die Zeiten nicht genau stimmen. Dies liegt daran, das durch die Integerrechnung Ungenauigkeiten entstehen. Durch Probieren mit den Werten kann man eine gewisse Genauigkeit erreichen. Rein rechnerisch kommen für 8Mhz Taktfrequenz und dem Ziel, eine millisekundengenaue Uhr zu bauen folgende Werte heraus: Vorteiler 8 ergibt bei 8 MHz CPU-Takt 1 MHz Takt für den Zähler. Wenn die Interrupt-Service alle Millisekunde ausgelöst werden soll, müsste man den OCR2-Wert auf 499 setzen, was aber wegen der Beschränkung auf 8 Bit (bei einem 8-Bit-Timer) nicht geht. Man kann ihn aber auf 250 setzen und eine umliegende Schleife 2 mal ausführen. Wenn man das so macht, wird der Zähler etwas zu langsam laufen. Ein Vergleich mit einer Stoppuhr zeigt nach 10 Minuten eine Abweichung (zu langsamer Lauf) von etwa einer Sekunde, also 0,17%.

16Bit-Timer: Unter http://www.mikrocontroller.net/articles/AVR_-_Die_genaue_Sekunde_/_RTC ist die Nutzung des 16-Bit-Timers für eine Uhr gut beschrieben. Eine damit aufgebaute Uhr zeigt bei mir auch nach 40 Minuten keine Gangabweichung. Der Compare-Wert des 16Bit-Timers kann als 16Bit-Wert deutlich höhere Werte annehmen.  Ansonsten ist die Programmierung vom Ablauf her ähnlich wie beim 8Bit Zähler.

Spezielle Infos zu AVR Timern:  http://www.roboternetz.de/wissen/index.php/Timer/Counter_(Avr)

Analog-Digital Umwandlung

In den meisten AVR Mikrocontrollern ist auch ein AD-Konverter (ADC) enthalten. Dieser kann mit 8- oder 10-Bit-Auflösung Spannungen, die an mehreren Kanälen (Pins) anliegen, konvertieren. Er kann einmalig konvertieren oder auch fortlaufend.

Für das Testen dieses Features startet man mit einer Funktion, die die eigentliche AD-Konversion durchführt. Ich habe dazu die Funktion ReadChannel, wie bei www.mikrocontroller.net beschrieben ( http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Der_interne_ADC_im_AVR) verwendet. Die Werte für ADPS2, ADPS1 und ADPS0 müssen aus der Taktfrequenz der CPU berechnet werden. Die Formel ist ebenfalls unter obiger URL zu finden.

#include <inttypes.h>
#include <avr/io.h>
 
/* Funktion übernommen aus http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial#Der_interne_ADC_im_AVR */
uint16_t ReadChannel(uint8_t channel ) {
   uint8_t i;
   uint16_t result;
 
   ADCSRA = _BV(ADEN) | _BV(ADPS2) | _BV(ADPS1);    // Frequenzvorteiler setzen auf 64 und ADC aktivieren
 
   ADMUX = channel; // Kanal waehlen
   ADMUX |= _BV(REFS1) | _BV(REFS0); // interne Referenzspannung nutzen
 
   /* nach Aktivieren des ADC wird ein "Dummy-Readout" empfohlen, man liest
      also einen Wert und verwirft diesen, um den ADC "warmlaufen zu lassen" */
   ADCSRA |= _BV(ADSC); // eine ADC-Wandlung
   while ( ADCSRA & _BV(ADSC) ) {
      ;     // auf Abschluss der Konvertierung warten
   }
   result = ADCW;  
   // ADCW muss einmal gelesen werden,
   // sonst wird Ergebnis der nächsten Wandlung
   // nicht übernommen.
 
   /* Eigentliche Messung - Mittelwert aus 4 aufeinanderfolgenden Wandlungen */
   result = 0;
   for( i=0; i<4; i++ ) {
      ADCSRA |= _BV(ADSC); // eine Wandlung "single conversion"
      while ( ADCSRA & _BV(ADSC) ) {
         ;   // auf Abschluss der Konvertierung warten
      }
      result += ADCW; // Wandlungsergebnisse aufaddieren
   }
   ADCSRA &= ~_BV(ADEN); // ADC deaktivieren (2)
   result /= 4; // Summe durch vier teilen = arithm. Mittelwert
   return result;
}

Im Hauptprogramm:

#define VREF 2.56 /* value of (interna) reference voltage) */
 
/*
 ** function prototypes
 */
uint16_t ReadChannel(uint8_t channel);
 
int main(void) {
   int value;
   char buf[32];
   float f;
 
   DDRC &=~ (1 << PC3); /* Pin PC3 input */
 
   DI();
   /* now enable interrupt, since UART library is interrupt controlled */
   sei();
   DO("after init usartnr");
 
   while (1) {
      value = ReadChannel(3); // channel 3 is PC3
      f = value;
      f = (f*VREF)/1024.0; // 256 for 8 bits, 1024 for 10 bits; VREF=2.56 for internal ref.
      sprintf( buf, "Measuring %.3f Volt (raw value = %d)r", (double)(f), value  );
      DO(buf);
      dowait();
   }
}

Beim Linken muss eine zusätzliche printf-Bibliothek, die auch Floats ausgeben kann, hinzugebunden werden (libprintf_flt). Diese Bibliothek wird normalerweise nicht eingebunden, weil sie deutlich größere Executables produziert.

Projekt -> Properties -> C/C++ Build -> Settings -> Tool Settings, dort unter „Linker“ -> Other options“ folgendes eintragen:

und unter „Linker -> Libraries“ die zusätzliche Library für Floats einbinden:

Dann produziert das Programm bei Anlegen einer Spannung an Pin C3 folgende Ausgabe:

Ein parallel angeschlossenes Messgerät zeigt 1,869 Volt an, also so ziemlich korrekt 🙂

Schwingt mein Quarz?

Beim Basteln mit Quarzen ist manchmal unklar, ob der Quarz überhaupt etwas tut. So habe ich Probleme gehabt, einen DS1302 mit einem Uhrenquarz (32768Hz) zum Laufen zu bringen. Letztendlich habe ich das Thema aufgegeben. Die folgende Schaltung ist schwingfreudig und einfach aufzubauen.

Ich habe damit Quarze mit Frequenzen zwischen 1,8Mhz und 10Mhz problemlos zum Schwingen gebracht.

Integrierte Quarzoszillatoren lassen sich ebenfalls zuverlässig als Schwingungsgeber benutzen und sind besonders einfach zu beschalten. Das Bild unten zeigt die Beschaltung. Ein Oszillator mit 49,4563 Mhz bringt beispielsweise 2,5V Amplitude bei einer Betriebsspannung von 7,5V.


Ein integrierter Quarz Oszillator

Beschaltung eines integrierten Quarz Oszillators (Die Beschaltung der Pins scheint bei allen diesen Bausteinen gleich zu sein, aber genau weiss ich das nicht, also im Zweifelsfall Datenblatt nutzen). Unten rechts geht 0V dran, oben links die Betriebsspannung. Das Ausgangssignal geht oben rechts ab. Eine Ecke des Bausteins ist wirklich eckig, die anderen nur abgerundet, damit hat man die Grundorientierung

Schwingt der Quarz an meinem AVR?

Falls der AVR völlig tot scheint und auf nichts mehr reagiert, z.B. bei der ISP-Programmierung, ist es sinnvoll, zu prüfen ob der Quarz am AVR überhaupt schwingt. Wenn der AVR auf Quarzbetrieb umgestellt ist und keine Schwingungen vom Quarz kommen, kommt vom AVR keine Reaktion mehr.

Am Quarz fliessen allerdings so geringe Ströme, dass bei niederohmigen Meßmethoden die Schwingung abreist. Dies ist also ein schönes Beispiel, bei dem die Messung das Meßergebnis vollkommen bestimmen kann.

Mit einem Oszilloskop mit hohem Eingangswiderstand ist die Messung möglich. Die Tastköpfe von Oszilloskopen sind bei der Stellung 10:1 (oder gar 100:1, wenn man das hat) sehr hochohmig. Dann habe ich an einem ATmega644 mit 3,3V Betriebsspannung beispielsweise eine Spannungsamplitude direkt am Quarz gemessen. Bei 8Mhz waren es 1,7V an einem und 0,72V an dem anderen Pin.

Austausch der vorhandenen avr-libc durch eine neuere Version

Falls man Quellcode aus dem Internet bezieht, kann es vorkommen, dass dieser mit neueren Versionen der avr-libc entwickelt wurde. Man merkt das daran, dass Funktionen vom Compiler und dem Linker als unbekannt gemeldet werden. Man kann dann versuchen, ein neueres Paket für seine Plattform zu installieren. Wenn man OpenSuse und Yast regelmäßig nutzt, hat man allerdings schon das neueste Paket für diese Plattform. Man kann dann aber die avr-libc selbst compilieren. Ich habe dies wie folgt gemacht:

  1. Download der neuesten avr-libc von http://www.nongnu.org/avr-libc/. Die Source findet man unter  http://download.savannah.gnu.org/releases/avr-libc/
  2. Compilieren der avr-libc. Hier kann man nach dem README und dem INSTALL vorgehen. Diesen Schritt würde ich als Non-Root machen, um zu verhindern, dass man sich mit falschen Parametern die vorhandene avr-libc zerschießt. Ich habe das configure-Kommando wie folgt aufgerufen:
    ./configure –build=`./config.guess` –host=avr –prefix=/home/dennis/avrlibc
    Damit wird die libc unterhalb meines Homeverzeichnisses gebaut.
  3. Statt einem „make install“ habe ich dann per Hand die beiden erzeugten Verzeichnisse (avr/include, avr/lib) in das Verzeichnis kopiert, in dem die alte Bibliothek liegt. Dies ist bei OpenSuse /opt/cross. Die dort vorhandenen Verzeichnisse avr/lib und avr/include habe ich vorher gesichert.
  4. Zum Schluss noch aus dem gesicherten lib-Verzeichnis die ldscripts in das neue lib-Verzeichnis kopieren:
    cp -a ./lib.orig/ldscripts ./lib

Danach sollte ein Kompilieren wieder möglich sein und die neue Bibliothek wird benutzt.

„Echte Projekte“

Über obige Funktionstests hinausgehende Aktivitäten:

Weiterführende Links