HomeProjekteMicroprozessorenPollinboard 

Pollinboard

Weitersagen:

Als Einstieg in die Mikrocontroller-Welt empfehle ich das Atmel AVR Evaluations-Board, oder kurz „Pollinboard”. Es hat bereits einige brauchbare Hardware für den Anfang an Bo(a)rd, für größere Anwendungszwecke braucht man aber eine ganz eigene Platine oder zumindest eine Erweiterung für das Pollinboard.

Auf dieser Projektseite werde ich jeden Abschnitt jeweils mit den Programmiersprachen C und Assembler beschreiben, um die Unterschiede und Gemeinsamkeiten hevorzuheben und einen leichten Einstieg in beide Sprachen zu ermöglichen.

Aufbau

Es gibt zwar das fertige Pollinboard zum Bestellen, das ist aber knapp acht Euro teurer und macht nicht so viel Spaß.

Bestellliste bei Pollin

Um das Netzteil anschließen zu können, habe ich einige Modifikationen vorgenommen:

Allgemein — C

Der grundsätzliche Aufbau einer C-Datei (mit der Endung .c) sieht folgendermaßen aus:

C-Code:
#include <avr/io.h>    // Header-Datei einbinden. In io.h sind die Registernamen definiert, die im späteren Verlauf genutzt werden.

int main(void) {       // Hier beginnt das eigentliche Programm. Jedes C-Programm beginnt mit den Anweisungen in der Funktion main.
  DDRD  = [Zahl];      // Definieren von I/O im Data Direction Register Port D, s.u.
  PORTD = [Zahl];      // Strom an die Ausgänge im Port D legen

  while(1) {           // Endlosschleife
    // wiederkehrende Abläufe, bspw. Abfragen
  }

  // wird nie erreicht, da sonst nach Programmende der Zustand des Controllers undefiniert wäre
  return 0;
}

Danach muss die Datei kompiliert werden. Dazu benutzt man ein makefile oder ruft die folgenden Befehle in einem Terminal auf (natürlich mit angepasstem µC und Dateinamen):

Terminal:

$ avr-gcc -mmcu=atmega8 -I. -Wall -Wstrict-prototypes -Wundef -std=gnu99 datei.c -o datei.o
$ avr-objcopy -O ihex -R .eeprom datei.o datei.hex

Erst jetzt kann man die entstandene .hex-Datei auf den µC übertragen.

Allgemein — Assembler

Assembler ist eine relativ einfach zu verstehende Sprache, die sehr Hardware-orientiert arbeitet und so eine größtmögliche Nutzung des geringen Speicherplatzes auf einem µC bietet. Das Assembler-Programm (Endung .asm) wird eins zu eins in Maschinencode umgesetzt. Diesen Vorgang nennt man Assemblieren.

Die verschiedenen µC-Hersteller bieten alle eigene Versionen für ihre jeweiligen Produkte an, die leider auch alle ein wenig unterschiedlich arbeiten. Deshalb ist hier, wo mit Atmels Prozessoren gearbeitet wird, immer AVR Assembler gemeint, wenn einfach nur von Assembler die Rede ist. Doch auch nicht in allen mit AVR Assembler arbeitenden µCs (also Atmels AVR-Familie) sind alle Befehle implementiert, die meisten können aber auf allen Geräten ohne Bedenken genutzt werden.

Das Grundgerüst einer Assembler-Datei (mit der Endung .asm) sieht folgendermaßen aus:

AVR Assembler-Code: (Klicke auf Befehle, um mehr über sie zu erfahren)
.include "m8def.inc"    ; lädt verwendete Konstanten (z.B. Registernamen)
; (verwendbare Include-Files findet man unter /usr/share/avra)

; Im Datenblatt des jeweiligen Microcontrollers werden feste
; Adressen definiert, an denen bestimmte Sprungbefehle erwartet
; werden. Dies wird vor allem später bei Interrupts wichtig.

; In Assembler kann man eine hexadezimale Zahl entweder mit
; "0x" oder "$" als Präfix definieren: 0xf8 = $f8
.org $0000              ; 0x00 = Reset-Vektor: wird nach Reset (also am Start) aufgerufen
  rjmp mein_Startlabel

.org $0013              ; beim ATmega8 wird ab hier kein Sprungbefehl mehr erwartet

mein_Startlabel:        ; Label, das einen Codeabschnitt benennt
  sbi DDRD, [Bitnr.]    ; Definieren von I/O im Data Direction Register Port D, s.u.
  sbi PORTD, [Bitnr.]   ; Strom an die Ausgänge im Port D legen
  ; mach irgendwas
  
; Nachdem er alle Befehle aus "mein_Startlabel" abgearbeitet hat,
; läuft er in das Label "Weiter" hinein

Weiter:
  ; mach was anderes, das immer wieder gemacht werden muss
  rjmp Weiter           ; springt wieder zurück an den Anfang von "Weiter"

Danach muss die Datei assembliert werden. Dazu verwenden wir den Compiler english avra. Dieser ist zwar größtenteils kompatibel zu Atmels eigenem Compiler, unterstützt aber einige zusätzliche Direktiven (alle dunkelrot markierten Befehle, die mit einem Punkt beginnen). Auch hier kann man ein makefile schreiben oder den folgenden Befehl einfach so in einem Terminal aufrufen (natürlich mit angepasstem Dateinamen):

Terminal:

$ avra -I "/usr/share/avra" datei.asm

Erst jetzt kann man die entstandene .hex-Datei auf den µC übertragen.

Definieren von I/O

Man kann selbst bestimmen, welche Pins Eingänge und welche Ausgänge sein sollen (daher auch die Bezeichnung I/O: In / Out — Eingang / Ausgang). Das muss für jeden Port einzeln gemacht werden.

Pin Nr.76543210
Beispiel:
out = Ausgang → 1
in = Eingang → 0 (Null)
egal
0
out
1
out
1
in
0
in
0
in
0
egal
1
egal
1
0b01100011

Pinbelegungen

Diese Liste gilt für den ATmega8, ATmega16, ATmega32, ATmega8535 und den ATtiny2313.

FunktionPin
Taster 1PD2
Taster 2PD3
Taster 3PD4
LED1PD5
LED2PD6
SummerPD7
(ATtiny2313: n. c.)

LEDs während Tastendruck einschalten

Unser Ziel ist es, die LED1 einzuschalten, solange der Taster 1 gedrückt ist, LED2 einzuschalten, solange Taster 3 gedrückt ist und beide LEDs zum Leuchten zu bringen, solange Taster 2 gedrückt ist.

C-Code:
#include <avr/io.h>

int main(void) {
  DDRD  = 0b01100000;  // PD5 und PD6 (LEDs) sind Ausgänge
  PORTD = 0x00;        // Keinen Strom an die Ausgänge legen

  while(1) {
    if(PIND & (1 << PD3) || (PIND & (1 << PD2) && PIND & (1 << PD4))) {
      // Taster 2 oder Taster 1 und 3 gedrückt  Beide LEDs anmachen
      PORTD = 0x60;
    }
    else if(PIND & (1 << PD2)) {
      // Taster 1 gedrückt  LED1 anmachen
      PORTD = 0x20;
    }
    else if(PIND & (1 << PD4)) {
      // Taster 3 gedrückt  LED2 anmachen
      PORTD = 0x40;
    }
    else {
      // kein Taster gedrückt  LEDs ausmachen
      PORTD = 0x00;
    }
  }

  return 0;
}
AVR Assembler-Code: (Klicke auf Befehle, um mehr über sie zu erfahren)
.include "m8def.inc"

.org $0000    ; Reset
  rjmp Start

.org $0013

Start:
  sbi DDRD, 5           ; LED1 als Ausgang festlegen
  sbi DDRD, 6           ; LED2 als Ausgang festlegen

Schleife:
  cbi PORTD, 5          ; LED1 ausschalten
  cbi PORTD, 6          ; LED2 ausschalten
  
  sbic PIND, 3          ; nächsten Befehl überspringen, wenn Taster 2 nicht gedrückt
  rcall BeideLEDsAn
  
  sbic PIND, 2          ; Taster 1
  sbi PORTD, 5          ; LED1 anschalten
  
  sbic PIND, 4          ; Taster 3
  sbi PORTD, 6          ; LED2 anschalten
  
  rjmp Schleife

BeideLEDsAn:
  sbi PORTD, 5
  sbi PORTD, 6
  
  ret                   ; zurück zu Aufrufestelle (rcall)

LEDs bei Tastendruck invertieren (toggle)

Jetzt zeigt sich, dass eine kleine Veränderung in der Funktionalität große programmiertechnische Änderungen mit sich bringt: Wir wollen mit einem Tastendruck die entsprechende LED einschalten, ein weiteres Drücken soll sie wieder ausschalten. Der Taster 2 soll beide LEDs invertieren.

C-Code:
#include <avr/io.h>

int main(void) {
  DDRD  = 0b01100000;
  PORTD = 0x00;

  short led1 = 0;
  short led2 = 0;

  while(1) {
    if(PIND & ((1 << PD2) | (1 << PD3) | (1 << PD4))) {
      // irgendein Taster gedrückt

      if(PIND & (1 << PD3)) {
        // Taster 2 gedrückt  beide LEDs in die Warteschlange setzen
        led1 = 1;
        led2 = 1;
      }
      else {
        if(PIND & (1 << PD2)) {
          // Taster 1 gedrückt  LED1 in Warteschlange setzen
          led1 = 1;
        }
        if(PIND & (1 << PD4)) {
          // Taster 3 gedrückt  LED2 in Warteschlange setzen
          led2 = 1;
        }
      }
    }
    else {
      // kein Taster gedrückt  Warteschlange abarbeiten

      if(led1 == 1) {         // LED1 in Warteschlange
        led1 = 0;             // LED1 aus Warteschlange herausnehmen
        PORTD ^= (1 << PD5);  // XOR an PD5 anwenden: 1 wenn bisher 0, 0 wenn bisher 1
      }
      if(led2 == 1) {
        led2 = 0;
        PORTD ^= (1 << PD6);
      }
    }
  }

  return 0;
}
AVR Assembler-Code: (Klicke auf Befehle, um mehr über sie zu erfahren)
.include "m8def.inc"

; Alternative Namen für die Register vergeben
.def tmp = r1
.def bitmuster = r16

.org $0000
  rjmp Start

.org $0013

Start:
  sbi DDRD, 5
  sbi DDRD, 6

Schleife:
  sbic PIND, 2                ; nächsten Befehl überspringen, wenn Taster 1 nicht gedrückt
  rcall Taster1
  
  sbic PIND, 3
  rcall Taster2
  
  sbic PIND, 4
  rcall Taster3
  
  rjmp Schleife

Taster1:
  in tmp, PORTD               ; PORTD in tmp einlesen
  ldi bitmuster, 0b00100000   ; Da wo eine 1 ist wird invertiert, der Rest bleibt gleich
  eor tmp, bitmuster          ; XOR an tmp mit dem Bitmuster anwenden
  out PORTD, tmp              ; tmp wieder nach PORTD schreiben
Taster1_Schleife:
  sbis PIND, 2                ; wenn Taster 1 noch gedrückt, nicht zurückgehen...
  ret
  rjmp Taster1_Schleife       ; ... sondern nochmal prüfen

Taster2:
  in tmp, PORTD
  ldi bitmuster, 0b01100000
  eor tmp, bitmuster
  out PORTD, tmp
Taster2_Schleife:
  sbis PIND, 3
  ret
  rjmp Taster2_Schleife

Taster3:
  in tmp, PORTD
  ldi bitmuster, 0b01000000
  eor tmp, bitmuster
  out PORTD, tmp
Taster3_Schleife:
  sbis PIND, 4
  ret
  rjmp Taster3_Schleife

Blinklicht

Ein etwas komplexeres Programm erlaubt uns, die LEDs entweder abwechselnd oder zusammen blinken zu lassen. Der Modus kann dabei mit dem Taster 2 gewechselt werden. Außerdem kann man die Wartezeit zwischen den einzelnen Zyklen mit Taster 1 und 3 einstellen.

C-Code:
#include <avr/io.h>
#ifndef F_CPU
  #warning "F_CPU war noch nicht definiert, wird nun mit 3686400 definiert"
  #define F_CPU 800UL
#endif
#include <util/delay.h>           // enthält die Warteroutine _delay_ms(...)

void long_delay(uint16_t ms) {
  while(ms) {
    _delay_ms(1);
    ms--;
  }
}

int main(void) {
  DDRD = (1 << PD5) | (1 << PD6);
  PORTD = (1 << PD6);             // nur LED2 einschalten

  short change_mode = 0;          // abwechselnd / zusammen blinken
  short zeit_kleiner = 0;
  short zeit_groesser = 0;
  short zeit = 100;

  while(1) {
    if(PIND & (1 << PD3)) {
      change_mode = 1;
    }
    else if(change_mode) {
      change_mode = 0;
      PORTD ^= (1 << PD5);
    }
    else if(PIND & (1 << PD2)) {
      zeit_kleiner = 1;
    }
    else if(zeit_kleiner) {
      zeit_kleiner = 0;
      zeit -= 20;
      if(zeit < 0) {
        zeit = 0;
      }
    }
    else if(PIND & (1 << PD4)) {
      zeit_groesser = 1;
    }
    else if(zeit_groesser) {
      zeit_groesser = 0;
      zeit += 20;
      if(zeit > 1000) {
        zeit = 1000;
      }
    }

    PORTD ^= 0x60;                // Toggle PD5 und PD6 (LEDs)
    long_delay(zeit);             // Warte 100 Millisekunden
  }

  return 0;
}
AVR Assembler-Code: (Klicke auf Befehle, um mehr über sie zu erfahren)
TODO
Teile diese Seite!    RSS-Feed abonnieren:
RSS