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 bischen 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 von WinAVR). 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 Preprozessor-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 muß 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 setzten
_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 muß 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. Coder 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 Durtchlaufen von Verzögerungsschleifen implementiert. Während des Abarbeitens der Scheife kann der AVR im Hauptprogramm nichts anderes tuen, 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 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":

Die geänderten Fuses. 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 Anschluß:
 avrdude -v -p m644 -c ponyser -P /dev/ttyS1 -U lfuse:r:-:i
und für einen 644P via USB-Anschluß
 avrdude -v -p m644p -c avrisp2 -P usb:884:92 -U lfuse:r:-:i

Mit dem Kommando
    avrdude -v -p m32 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xcf:m
habe ich bei einem ATmega 32 auf die Nutzung eines externen Quarzes umgestellt. Mittels
    avrdude -v -p m644 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xdf:m
geht dasselbe bei einem ATmega 644 und mit
    avrdude -v -p m8 -P /dev/ttyS1 -c pony -u -U lfuse:w:0xff:m -U hfuse:w:0xd9:m
bei einem fabrikneuen ATmega8.
Und mit
 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
geht das Umstellen bei einem ATmega644P via USB.

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.

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;

    ...

    TCCR2 = (1<<CS21) | (1<<WGM21);    // Prescaler von 8 und CTC
    OCR2  = 250; // Compare-Wert
    TIMSK |= (1<<OCIE2);    
    sei(); // Interrupts freigeben

    ...

    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:%02d\r", h, m, s);
        DO(buf);
     }
}

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 125 einstellt, wird der Interrupt einmal pro Millisekunde ausgelöst. 

Wer allerdings auf der Basis eine Uhr bauen will, wird feststellen, dass die Zeiten nicht genau stimmen. Dies liegt daran, dass der auszuführende Code auch Zyklen kostet. Dieses summiert sich auf, so dass die mathematisch berechneten Werte ungenau sind. Durch probieren 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 1000 setzen, was aber wegen der Beschränkung auf 8 Bit nicht geht. Man kann ihn aber auf 250 setzen und eine umliegende Schleife 4 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 usart\n\r");

        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,869Volt an, also so ziemlich korrekt :-)

I2C EEPROM-Ansteuerung

I2C (auch TWI=Two Wire Interface genannt) ist ein sehr hardware-naher De Facto-Standard, mit dem zwei oder mehrere Geräte oder auch Bausteine über einen gemeinsamen Bus miteinander kommunizieren können. Geräte können am Bus als Master oder als Slave teilnehmen. Über die Geräteadresse können mehrere Geräte unterschieden werden. Von verschiedenen Herstellern werden Chips angeboten, die das I2C Protokoll beherrschen. Die größeren AVRs bieten auch Unterstützung für dieses Protokoll. 

Die Kommunikation erfolgt bei I2C über zwei Leitungen, SCK (serial clock) und SDA (serial data). Grob gesprochen wird pro Clock-Impuls ein Datenbit übertragen. Über diesem grundlegenden Protokoll sind bestimmte Abfolgen der übertragbaren Kommandos vorgeschrieben. Die Kommandos werden inhaltlich durch das jeweilige Gerät oder den jeweiligen Chip bestimmt. Die grundlegende Spezifikation von I2C ist unter http://www.nxp.com/acrobat/literature/9398/39340011.pdf zu finden. 

Ein interessanter Chip sind die EEPROMs der Reihe 24Cxxx. Ich habe ein EEPROM 24C512 getestet. Der Wert "xxx" steht dabei für die Speichergröße in KBits (02...1024). Das 24C512 mit 512 KBit=64KByte kann mittels I2C gelesen und beschrieben werden. Das EEPROM kann benutzt werden, wen die Größe des On-Chip EEPROMs des AVR nicht mehr ausreicht.

Peter Fleury hat auch hierfür eine Bibliothek geschrieben, bei der der AVR als I2C Master auftritt. Diese Bibliothek kann unter  http://homepage.hispeed.ch/peterfleury/i2cmaster.zip heruntergeladen werden. Als Beispiel ist der Zugriff auf ein 24C02 im ZIP File enthalten. Die kleineren EEPROMs kommen mit einer 8-Bit Adresse aus, für die größeren wie das 24C512 benötigt man eine 16-Bit-Adresse. 

Pin-Belegung des 24C512.

Das 24C512 hat eine 2-Bit Adresse, so dass 4 solcher Bausteine am I2C-Bus unterschieden werden können. Die "Bausteinklasse" 24Cxxx hat den festen Adressteil 1010xxxx (Binär) bzw. 0xA. Die Chip-Adresse wird festgelegt, indem z.B. A0=A1=0 an GND gelegt wird. Dann kann man mit der I2C-Adresse 0xA0 diesen Chip ansprechen. Ein anderer, bei dem A0=1 und A1=0 gelegt wurde, ist mit 0xA1 anzusprechen. A0 und A1 sind 0, wenn sie nicht anders beschaltet werden. "WP" heißt "Write Protect", ein Hardware-Schreibschutz, der aktiv ist, wenn WP auf Vcc gelegt wird. Wird er offen gelassen oder auf GND gelegt, kann man das EEPROM auch beschreiben.

Simple Beschaltung des 24C512

Achtung: Auf dem Pollin Experimentierboard ist der WP-Anschluss leider fest auf Vcc gelegt, d.h. man kann ein normal eingestecktes EEPROM nicht beschreiben. Abhilfe schafft Einstecken mit abgebogenem WP-Pin.

24C512 auf dem Pollin Experimentierboard 2.0. 
Da bei diesem Board der WP-Pin fest auf Vcc liegt, 
muss man das EEPROM mit abgebogenem
PIN einstecken, um es beschreiben zu können.

Im folgenden ist der beispielhafte Schreibzugriff dargestellt (der Code von Peter Fleury wurde im wesentlichen nur um die 2-Byte Adressierung erweitert):

    #include "i2cmaster.h"

    #define Dev24C512  0xA0      // device address of EEPROM 24C512, see datasheet

int main(void)
{
    unsigned char val, ret;
    int addr;

    i2c_init(); // init I2C interface

    ret = i2c_start(Dev24C512+I2C_WRITE); // set device address and write mode
    if ( ret ) {
        /* failed to issue start condition, possibly no device found */
        printf("failed to issue start condition, possibly no device found.\n");
        i2c_stop();
    } else {
        /* write 0x75 to eeprom address 0x05 (Byte Write) */
        /* issuing start condition ok, device accessible */
        addr=5;
        val=0x75;
        printf("writing %0x to address %0x\n", val, addr );
        ret = i2c_write( (addr/256) );                       // write hi address
        if (ret!=0)
            printf("error in writing value, ret=%0x\n", ret );
        ret = i2c_write(  (addr%256) );                       // write lo address
        if (ret!=0)
            printf("error in writing value, ret=%0x\n", ret );
        ret = i2c_write(val);                       // ret=0 -> Ok, ret=1 -> no ACK
        //printf("ret=%0x\n", ret);
        if (ret!=0)
            printf("error in writing value, ret=%0x\n", ret );
        i2c_stop();                            // set stop conditon = release bus
    }

Die ganzen dargestellten Prüfungen der Return-Werte sind nicht unbedingt nötig, aber bei der Fehlersuche hilfreich. Das Lesen geschieht wie folgt:

        i2c_start_wait(Dev24C512+I2C_WRITE);     // set device address and write mode
        printf("reading value from address %0x: ", addr );
        ret = i2c_write( (addr/256) );  // write hi address
        ret = i2c_write(  (addr%256) ); // write lo address
        i2c_rep_start(Dev24C512+I2C_READ); // set device address and read mode
        ret = i2c_readNak(); // read one byte
        printf("read value=%x\n", ret );
        i2c_stop();

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 Kompileren wieder möglich sein und die neue Bibliothek wird benutzt.

"Echte Projekte"

Über obige Funktionstests hinausgehende Aktivitäten:

Arbeiten unter Windows

Das Arbeiten unter Windows habe ich hier beschrieben. Da ich aber nur noch unter Linux arbeite, ist der Windows-Text veraltet und wird nicht weiter gepflegt.

Weiterführende Links