ATmega8-Programmierung

1 Vorbereitung

Erstellung der Programme mit Hilfe einer IDE:

  1. Installieren Sie AtmelStudio[1]
  2. Laden Sie das myAVR Prog Tool[2] herunter und speichern Sie es in dem Ordner 'C:\Program Files (x86)\Atmel\Studio'
  3. (Eventuell muss noch der USB-Teiber[3] installiert werden.)
  4. Starten Sie das AtmelStudio und tragen Sie unter Tools - External Tools... das 'myAVRProgTool' ein[4].
    Der Eintrag unter 'Arguments' muss lauten: $(ItemDir)debug\$(ItemFileName).hex
  5. (Alternativ können Sie auch das myAVRProgTool im Explorer als Standardprogramm für .hex-Dateien eintragen.)
  6. Erstellen Sie ein neues Projekt File - New - Project - GCC C Executable Project
  7. Wählen Sie anschließend den 'ATmega8' aus
  8. Schreiben Sie Ihr Programm in der main.c
  9. Erzeugen Sie mit Build Solution (F7) die .hex-Datei.
  10. Wenn Sie das Programm zum ersten Mal brennen, rufen Sie zuerst die Funktion Debug - Start Without Debugging (Ctrl+Alt+F5) auf und klicken Sie die Fehlermeldung weg. Damit können Sie direkt aus dem AtmelStudio das myAVRProgTool starten, wobei der Pfad und Dateiname der hex-Datei bereits übergeben werden (Vor dem Brennen trotzdem immer kontrollieren).
  11. Rufen Sie das myAVRProgTool auf, wählen Sie auf dem 'Hardware'-Dialog das 'myAVR Board MK2 USB'. Stellen Sie den richtigen COM-Port und den Controller (ATmega8) ein.

2 Programmierung mit C/C++

Mikrocontroller sind bereits vollständige Rechner, die neben der Recheneinheit auch über einen Programm- und Arbeitsspeicher und Ein- und Ausgabeports verfügen. Sie werden oft in technischen Geräten zur Steuerung und Regelung eingesetzt [5], [6].

Programme für Mikrocontroller werden meist in C, C++ oder Assembler erstellt. Das Übertragen des kompilierten Programms auf den Mikrocontroller wird programmieren oder brennen genannt. Das in der Schule verwendete Board[7] ist mit einem ATmega8 bestückt und hat ein Programmiergerät mit USB-Schnittstelle integriert, sodass der Controller direkt auf dem Board gebrannt werden kann.

2.1 Links

2.2 Einfache Programme

Grundgerüst eines einfachen Programms, das die LED an Port D - Pin 5 ein- und ausschaltet.

Zuerst wird die Konstante gesetzt, damit die Verzögerung genutzt werden kann #define F_CPU 3686400. Danach werden die Definitionsdateien 'avr/io.h' und 'util/delay.h' geladen.

Im Hauptprogramm werden zunächst alle erforderlichen Initialisierungen gemacht, im Beispiel wird der Pin 5 des Ports D über das Datenrichtungsregister als Ausgang definiert DDRD |= ( 1<<PD5 );.

In der Endlosschleife wird der Verabeitungscode eingefügt. Hier wird z.B. der Pin 5, an dem die LED angeschlossen ist, zunächst eingeschaltet. Nach einer Wartezeit von 1 Sekunde wird Pin 5 wieder abgeschaltet und erneut gewartet. Danach beginnt die Schleife von vorne.

Arbeitsauftrag:

  1. Projekt in AtmelStudio anlegen: AtmelStudio starten: File - New - Project - GCC C Executable Project - ATmega8. Danach im SolutionExplorer die Datei 'main.c' öffnen.
  2. Die Datei 'Documents\Atmel Studio\7.0\[Projektname]\[Projektname]\Debug\[Projektname].hex' muss auf den Mikrocontroller übertagen werden.
  3. Erstellen Sie Ihr erstes Projekt indem Sie drei LEDs auf dem myAVR-Board nacheinander ein- und und anschließend wieder ausschalten.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'LED_an_aus.c' ab.
  4. Ein Freund von Ihnen möchte für seine Modelleisenbahn eine Ampel bauen, die realistische Lichtzeichen gibt. Ihnen kommt sofort die Idee, dafür einen Mikrocontroller einzusetzen.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Ampel.c' ab.

2.3 Taster auf dem Board abfragen

In diesem Beispiel wird die LED geschaltet, solange der Taster gedrückt ist.

Für den Eingang an Pin 2 muss der interne Pull-Up-Widerstand aktiviert werden, da der Taster gegen Masse schaltet (Zeile 8).
PORTD |= ( 1<<PD2 );

Anschließend kann der Zustand der Pins des Ports D mit der Bitmaske verglichen werden, in der Bit 2 gesetzt ist. Das Ergebnis muss invertiert werden, da der Taster gegen Masse schaltet (Zeile 12).
if( !( PIND & ( 1 << PD2 ) ) )

Arbeitsauftrag:

  1. Erstellen Sie ein Programm, das eine LED einschaltet, wenn Taster 1 gedrückt wird und diese wieder abschaltet, wenn Taster 2 gedrückt wird.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Taster.c' ab.
  2. Ändern Sie das Programm so ab, dass die rote LED blinkt, wenn Sie Taster 1 gedrückt halten.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Blinklicht.c' ab.
  3. Entwickeln Sie ein Programm, dass die vier Zustände der beiden Taster wir folgt darstellt.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Zustaende.c' ab.
    • kein Taster - LEDs aus
    • Taster 1 - LED grün an
    • Taster 2 - LED gelb an
    • Taster 1+2 - LED rot an
  4. Erstellen Sie ein Programm, das bei jedem Tastendruck einen Wert von 0 bis 7 hochzählt und anschließend wieder bei 0 beginnt. Das Ergebnis soll binär auf den LEDs dargestellt werden. Nutzen Sie als Datentyp uint8_t für den Zähler. Lagern Sie die binäre Darstellung der Zahl in eine Funktion[8] aus.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Zaehler_01.c' ab.

2.4 Taster entprellen

Beim Abfragen des Tasters haben Sie sicher festgestellt, dass der Microcontroller nicht immer wie erwartet schaltet. Ursache hierfür ist das Prellen der Taster, d.h. bei der Betätigung werden mehrere Schaltimpulse gegeben. Um dem entgegen zu wirken, können prellfreie Schalter verwendet werden, die allerdings sehr teuer sind. Eine weitere Möglichkeit besteht in elektronischen Schaltungen, was jedoch einen zusätzlichen Aufwand nach sich zieht.

Die einfachste und preiswerteste Möglichkeit ist das softwaretechnische Entprellen[9]. Für zeitunkritische Anwendungen reicht es aus, nach jedem Tastendruck eine Wartezeit einzufügen. Bei zeitkritischen Anwendungen ist diese unnötige Belastung des Prozessors sehr ungünstig.

Arbeitsauftrag:

  1. Öffnen Sie die Datei 'Zaehler_01.c'. Nach jedem Tastendruck soll eine bestimmte Zeit vergehen, bis der nächste Tastendruck akzeptiert wird. Testen Sie, welche Zeit hierfür ausreichend ist.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Zaehler_02.c' ab.
  2. Öffnen Sie die Datei 'Zaehler_01.c' und entprellen Sie alle Tasterabfragen mit Hilfe der Flankenerkennung.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Zaehler_03.c' ab.
  3. Stellen Sie die Verfahren vergleichend gegenüber.

2.5 Interrupt-Techniken

In den bisherigen Beispielen wurde ständig abgefragt, ob ein Taster gedrückt wurde. Dieses Verfahren wird Polling genannt. Im richtigen Leben gehen Sie auch nicht ständig zur Haustür, um zu sehen, ob jemand davor steht. Stattdessen gehen Sie erst, wenn jemand klingelt. Diese Aufgabe übernehmen beim Mikrocontroller die Interrputs.

Tritt ein Ereignis ein (z.B. Reset, Externer Interrupt, Timer oder UART), wird in einem Statusregister das entsprechende Bit (Interruptflag) gesetzt und das Hauptprogramm unterbrochen. Anschließend wird das Flag wieder zurückgesetzt und die Interrupt-Service-Routine (ISR) gestartet. Ist diese beendet, wird zum Hauptprogramm zurückgekehrt[10]. Welche Interrupts der ATmega an welchem Port erwartet, kann dem Datenblatt (Seite 2 - Pin Configuration) und dem QEEWiki entnommen werden. Die Interrupt-Vektoren sind auf Seite 46 aufgelistet.

Die Konfiguration erfolgt in den folgenden Schritten[11]:

  • Include-Datei einbinden #include <avr/interrupt.h>
  • ISR erstellen, die nach Auslösen des Interrupts automatisch vom Prozessor aufgerufen wird ISR( INT0_vect ){ ... }.
    Hierin dürfen keine umfangreichen Berechnung oder lange Warteschleifen programmiert werden.
  • im Hauptprogramm (main(){ ... }) das Modul für die Interruptbearbeitung konfigurieren (z.B. Externer Interrupt, Timer)
    • PullUp-Widerstand für den Eingangspin aktivieren
    • externen Interrupt 0 an Pin PD2 über Aktivierungs-Bit im General Interrupt Control Register einschalten:
      GICR |= ( 1 << INT0 );
    • um im MCU Control Register konfigurieren, dass bei steigender Flanke ein Interrupt ausgelöst wird:
      MCUCR |= ( 1 << ISC01 ) | ( 1 << ISC00 );
      Siehe Datenblatt: Seite 66-68.
    • Interruptverarbeitung freigeben sei(); - hierdurch wird im SREG-Register des Bit 7 gesetzt.
  • Variable, die vom Hauptprogramm und der ISR benutzt werden, müssen volatile[12] deklariert werden.

Arbeitsauftrag:

  1. Erstellen Sie ein Programm, das in der Endlosschleife die LEDs wie ein Lauflicht schaltet. Wenn Taster 1 gedrückt wird, sollen alle LEDs dreimal gleichzeitig blinken. Danach soll das Lauflicht weiter laufen.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'LauflichtInterrupt_01.c' ab.
  2. Ergänzen Sie das Zähler-Programm (Zaehler_02.c) so, dass mit einem zweiten Taster die Werte und die Anzeige zurückgesetzt werden können. Das Rücksetzen der Werte soll unabhängig von der Verzögerungszeit möglich sein. Dazu muss das Rücksetzen über einen Interrupt erfolgen. Wählen Sie die Verzögerungszeit dazu relativ lang, um den gewünschten Effekt beobachten zu können.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'ZaehlerInterrupt_01.c' ab.

2.6 Timer/Counter

Ein Timer läuft nach seiner Aktivierung eigenständig und zählt entweder durch einen externen Impuls (Taster), quarzgesteuert oder durch den Prozessortakt. Nach Erreichen eines Wertes wird entweder ein Signal an einem Ausgabepin oder ein Interrupt erzeugt.

Verschiedene Modi:

  • Normal-Modus - läuft bis zu seinem Maximalwert, bei Overflow wird ein Interrupt erzeugt. Um mit dieser Methode eine exakte Zeit zu messen, muss der Zähler vorgeladen werden: TCNT1=n; das muss nach jedem Overflow erfolgen.
  • Clear Timer on Compare Match Mode (CTC) - Overflow bei voreingstelltem Wert
  • Synchroner Modus - Zähler läuft mit Prozessortakt (Timer0 - 8Bit und Timer1 - 16Bit)
  • Asychroner Modus - Zähler läuft quarzgesteuert (nur Timer2)

Das folgende Beispiel verwendet den 16Bit Timer1, die Beschreibung der Register kann im Datenblatt auf den Seiten 96-101 und im QEEWiki nachgeschlagen werden. Die Konfiguration erfolgt in den folgenden Schritten[13]:

  • Include-Datei einbinden #include <avr/interrupt.h>
  • ISR erstellen, die bei Erreichen des Vergleichswertes A ausgeführt wird: ISR( TIMER1_COMPA_vect ){ ... }
  • im Hauptprogramm (main(){ ... }) den Timer konfigurieren:
    • CTC-Mode im Timer/Counter Control Register einstellen:
      TCCR1B |= ( 1<<WGM12 );
    • Prescaler (Vorteiler) auf 256 einstellen:
      TCCR1B |= ( 1<<CS12 );
    • Vergleichswert ermitteln (n sec * CPU-Takt/Prescaler)-1 und in Output Compare Register speichern:
      OCR1A = ( n * F_CPU / 256 ) - 1;
    • Interrupt im Timer Interrupt Mask Register konfigurieren - auslösen, wenn Vergleichswert A erreicht:
      TIMSK |= ( 1<<OCIE1A );
    • Interruptverarbeitung freigeben sei();

Arbeitsauftrag:

  1. Erstellen Sie ein Programm, das Timer-gesteuert eine LED im Sekundentakt blinken lässt.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Timer_01.c' ab.

2.7 PulsWeitenModulation - PWM

Zur Ansteuerung z.B. von Motoren oder LEDs mit minimalen Verlusten ist die Erzeugung von Rechtecksignalen sinnvoll, da sich die Ausgangsspannung eines Mikrocontrollers nicht regeln lässt. Die Fläche dieser Rechtecke ist ein Maß für die zugeführte Energie[14]. Der Abstand der Impulse muss dabei so kurz wie möglich gewählt werden, dass diese nicht wahrgenommen werden können. Der Mittelwert der Ausgangsspannung berechnet sich wie folgt: Upwm = U * (Tein / Tein+Taus) oder: Upwm = U * Tein *fpwm

Vorsicht: Für das jeweilige Anwendungsgebiet muss die Ausgangsspannung berücksichtigt werden, damit beispielsweise eine LED nicht mit zu hoher Spannung betrieben wird. Die Verwendbarkeit ist bei jeder Schaltung vorher zu prüfen.

2.7.1 Eigenständige Steuerung durch den Controller

Der Timer1, der bereits im vorherigen Kapitel benutzt wurde, unterstützt verschiedene PWM-Modi. Siehe Datenblatt auf den Seiten 88 bis 100 und QEEWiki.

Die Konfiguration erfolgt in den folgenden Schritten:

  • Eingänge (PD2 und PD3) und Ausgänge (PB1) konfigurieren
  • PWM-Mode 5 im Timer/Counter Control Register einstellen: Fast-PWM, 8bit
    TCCR1A |= ( 1<<WGM10 ); und TCCR1B |= ( 1<<WGM12 );
  • Prescaler auf 256 einstellen:
    TCCR1B |= ( 1<<CS12 );
  • nicht invertierender PWM-Modus
    TCCR1A |= ( 1<<COM1A1 );
  • Startwert vorbelegen - LED ist aus
    OCR1A = 0;
  • im Verarbeitungsteil werden die beiden Taster abgefragt, die den Wert in OCR1A in den Grenzen zwischen 0 und 255 erhöhen oder verringern. Hierbei ist eine Verzögerung von 15ms empfehlenswert.

2.7.2 Emulation der PWM

Da der ATmega8 nur wenige Timer hat, die den PWM-Modus unterstützen, kann es unter Umständen nötig werden, das Verhalten an einem anderen Ausgangs-Pin zu emulieren. Hierzu muss eine Schleife programmiert werden, deren Frequenz konstant ist und die Dauer des Signals variabel. Achten Sie darauf, dass der Parameter von _delay_ms(); eine Konstante sein muss. Nutzen Sie die Schleife, um die Zeit zu variieren.

Arbeitsauftrag:

  1. Konfigurieren Sie den ATmega8 so, dass mit dem Taster1 eine LED langsam heller wird und mit dem Taster2 langsam dunkler. Nutzen Sie die integrierte PWM des Controllers.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'PWM_01.c' ab.
  2. Erweitern Sie das Programm 'PMW_01.c' um die Emulation der PWN so, dass eine zweite LED entgegengesetzt der ersten heller oder dunkler wird.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'PWM_02.c' ab.

2.8 LC-Display steuern

Die Laborsets beinhalten neben dem ATmega-Board auch ein LC-Display mit Hitachi HD44780 Controller[15]. Um das Display verwenden zu können, müssen Sie die Dateien lcd_routines.c und lcd_routines.h runterladen und im gleichen Ordner wie die Quelltextdateien speichern. (Wenn Sie das AtmelStudio verwenden, legen Sie hiermit die Dateien neu an und fügen den Inhalt ein, da das Hinzufügen existierender Datei einen Fehler verursacht.) Die C-Datei muss in Ihrem Quelltext inkludiert werden. Bei Verwendung des AtmelStudios muss auch die stdlib.h inkludiert werden.

Wie in der technischen Beschreibung zu sehen ist, belegt das Display die Pins PD2 bis PD7 und PB0 bis PB1. Diese können dann nicht mehr für andere Zwecke, z.B. Ein- oder Ausgänge, genutzt werden.

  • Im Initialisierungsteil muss das Display zunächst initialisiert werden.
    lcd_init();
  • Bevor der Inhalt verändert wird, sollte das Display gelöscht werden.
    lcd_clear();
  • Vor der Ausgabe muss die genaue Position gesetzt werden.
    lcd_setcursor( 0, 1 ); (Anfang der ersten Zeile)
  • Danach kann der Text ausgegeben werden.
    lcd_string( "Ausgabe" );
  • Sollen Zahlen ausgegeben werden, müssen diese zunächst umgewandelt werden.
    itoa( iZahl, szBuffer, 10 );[16] wandelt eine integer-Variable als Dezimalzahl in einen String um (ist in der 'stdlib.h' deklariert) oder:
    sprintf( szBuffer, "Wert: %d", iZahl );[17], [18] schreibt eine integer-Variable und Text in den Buffer (ist in der 'stdio.h' deklariert).
  • Nutzen Sie für float-Variablen die Funktion ftoa.c.


Arbeitsauftrag:

  1. Schreiben Sie ein Programm, das beim Druck auf Taster1 eine LED einschaltet und im Display den Text 'LED an' ausgibt und bei Taster2 die LED wieder ausschaltet und den Text 'LED aus' ausgibt.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'LCD_01.c' ab.
  2. Erweitern Sie das Programm so, dass eine Variable mit Taster1 eine Variable von 0 bis 255 hochzählt und den aktuellen Wert im Display anzeigt. Taster2 soll die Variable wieder bis 0 runterzählen.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'LCD_02.c' ab.
  3. Erweitern Sie andere Programme auf sinnvolle Weise mit der Anzeige von Werten, z.B. die Programme PWM_01.c und PWM_02.c indem Sie den relativen Wert der Rechteckgröße in Prozent ausgeben.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter einem anderen Namen ab.

2.9 Analog-Digital-Umsetzer

In den bisherigen Beispielen wurden nur digitale Eingänge betrachtet. In der Praxis werden oft Licht- oder Temperatursensoren oder Potentiometer eingesetzt, die analoge Werte in Form eines variablen Widerstandes und damit einer Spannung liefern[19].

Der ATmega8 bringt hierzu die folgenden Voraussetzungen mit:

  • Auflösung 10Bit - es können maximal 1024 Zwischenwerte bestimmt werden
  • Abtastgeschwindigkeit einstellbar von 50 bis 200 kHz
  • Messbereich bis 5V - einfach durch Spannungsteiler zu erweitern

Es stehen die folgenden Modi zur Verfügung:

  • Single Conversion - einzelne Messung, wobei das Ergebnis verfälscht sein kann.
  • Free Running - laufende Messungen
  • Interrupt gesteuert
  • Auto Trigger - auslösen der Messung z.B. durch Timer (nur bei neueren Modellen)

Für das Wandeln analoger Werte stehen die sechs Eingänge ADC0 bis ADC5 (Pins PC0 bis PC5) zur Verfügung. Siehe Datenblatt auf den Seiten 189 bis 201 und QEEWiki, [20], [21].

Die Konfiguration erfolgt in den folgenden Schritten:

  • Im ADC Multiplexer Select Register den Eingang über die Bits MUX0 .. MUX3 (Siehe Seite 199) und ...
  • ... und die Referenzspannung (5V) einstellen, die am Pin AREF anliegen muss. (Funktioniert auch ohne, wird dann allerdings ungenau.)
    ADMUX |= ( 1<<REFS0 );
  • Im ADC Control und Status Register den Konverter mit ADC Enable einschalten ...
    ADCSRA |= ( 1<<ADEN );
  • ... und den Prescaler auf 64 einstellen. die Abtastrate sollte zwischen 50 und 200 kHz liegen: Fadc = Fcpu / Prescaler = 3686400 / 64 = 57,6kHz
    ADCSRA |= ( 1<<ADPS2 ) | ( 1<<ADPS1 );
  • Danach mit ADC Start Conversion starten
    ADCSRA |= ( 1<<ADSC );

Wenn die Konvertierung abgeschlossen ist, was ca. 13 Taktzyklen dauert, steht der Wert im ADC-Register zur Verfügung.

  • Entweder mit einer Schleife warten und das ADSC-Bit überprüfen ...
    while( ADCSRA & ( 1<<ADSC ) );
    ... und nach der Verarbeitung die nächste Konvertierung anstoßen
    ADCSRA |= ( 1<<ADSC );
  • ... oder den Free Running Mode einschalten für kontinuierliche Messungen
    ADCSRA |= ( 1<<ADFR );
  • ... oder ADC Interrupt Flag und ADC Interrupt Enable einschalten und die entsprechende Interrupt Service Routine (ADC_vect) zur Verarbeitung nutzen.
    ADCSRA |= ( 1<<ADIF ) | ( 1<<ADIE );

Arbeitsauftrag:

  1. Konfigurieren Sie den ATmega8 so, dass er im Single Conversion Mode einen Poti abfragt und bei Erreichen eines Schwellwertes eine LED einschaltet, die bei Unterschreiten des Schwellwertes wieder abgeschaltet wird.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'ADC_SC_01.c' ab.
  2. Ändern sie das Programm so ab, dass der ATmega8 im Free Running Mode läuft.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'ADC_FRM_01.c' ab.
  3. Ändern sie das Programm so ab, dass die LED über einen Interrupt geschaltet wird.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'ADC_INT_01.c' ab.
  4. Konfigurieren Sie den ATmega8 so, dass eine LED durch Drehen eines Potentiometers heller bzw. dunkler wird. Nutzen Sie für die Veränderung der Helligkeit die PWM.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'ADC_PWM_01.c' ab.

3 Themenübergreifende Aufgaben

  1. Simulieren Sie mit dem ATmega8 eine Schaltschranküberwachung. Der Temperaturfühler wird durch den Lichtsensor ersetzt und der Lüfter durch eine LED. Das Öffnen der Schaltschranktür wird durch einen Taster simuliert.
    Die Drehzahl des Lüfters soll temperaturabhängig gesteuert werden. Sobald die Tür geöffnet wird, soll der Lüfter abgeschaltet werden und die rote LED soll blinken. Nach dem Schließen der Tür erlischt die rote LED und der Lüfter läuft weiter.
    Kommentieren Sie den Quellcode und speichern Sie diesen unter dem Namen 'Schaltschrank_01.c' ab.

4 Links

5 Literatur*

  • Jürgen Wolf: C von A bis Z. Rheinwerk Computing, Bonn 2009, ISBN: 978-3-8362-1411-7[22]

[*] Bei der angegebenen Literatur handelt es sich nicht um Schulbücher. Der Erwerb ist für den Unterricht nicht erforderlich und nicht vorgeschrieben. Die Bücher geben die Möglichkeit, sich tiefer in ein Teilgebiet ein zu arbeiten.