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.
Es gibt zwar das fertige Pollinboard zum Bestellen, das ist aber knapp acht Euro teurer und macht nicht so viel Spaß.
Um das Netzteil anschließen zu können, habe ich einige Modifikationen vorgenommen:
Der grundsätzliche Aufbau einer C-Datei (mit der Endung .c) sieht folgendermaßen aus:
#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):
$ 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.
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:
.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 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):
$ avra -I "/usr/share/avra" datei.asm
Erst jetzt kann man die entstandene .hex-Datei auf den µC übertragen.
I
/OMan 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. | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
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 |
Diese Liste gilt für den ATmega8, ATmega16, ATmega32, ATmega8535 und den ATtiny2313.
Funktion | Pin |
---|---|
Taster 1 | PD2 |
Taster 2 | PD3 |
Taster 3 | PD4 |
LED1 | PD5 |
LED2 | PD6 |
Summer | PD7 (ATtiny2313: n. c.) |
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.
#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;
}
.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)
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.
#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;
}
.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
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.
#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;
}
TODO