Nach dieser Lektion sollten Sie:
Im Video werden im ersten Teil (bis 15min30sec) die Grundkonzepte für die AVR Programmierung erklärt.
Ab der 16. Minute geht es in die Assembler Programmierung. Dies ist nicht Teil des Kurses Mikroprozessortechnik.
Nach dieser Lektion sollten Sie:
File
» New
» Project…
Einfuehrung_v01
und drücken Sie auf Ok
88
einOk
========== Build: 1 succeeded or up-to-date, 0 failed, 0 skipped ==========
lautenSolution
» Einfuehrung_v01
» Output Files
Einfuehrung_v01.hex
und wählen Sie Pfad und Name ausMicro
»AVR
»atmega
»mega88
mega88
per Drag and Drop in den Arbeitsbereich (rechter, beiger Teil des Fensters)mega88-1
dargestellt seinProperties
im Kontextmenu die Änderung von Load firmware
Einfuehrung_v01.hex
ein und öffnen Sie dieses
Sie sollten sich nach der Übung die ersten Kenntnisse mit dem Umgang der Umgebung angeeignet haben. Zum Festigen des der Fähigkeiten bieten sich folgende Aufgaben an:
Open Mcu Monitor.
» RAM Table
) PORTB
und der Mehrzweckregister R0…R31
(eintragen von z.B. PORTB
unter Name in der ersten Zeile und drücken von <Return>, oder anklicken bzw. markieren des Registers mit dem Cursor und <Return>). Simulation
kann die Geschwindigkeit geändert werden.
Das Bit im DDRx (Data Direction Register) wählt die Richtung des Pins aus. Wenn dort logisch Eins geschrieben wird, wird der entsprechende Pin als Ausgangspin konfiguriert. Wenn dort logisch Null geschrieben wird, wird der entsprechende Pin als Eingangspin konfiguriert.
Das Bit im PORTx Register hat mehrere Eigenschaften: Wenn das gewünschte Bit in PORTx logisch Eins geschrieben wird und der Pin als Ausgangspin konfiguriert ist, wird der Portpin auf high (eins) gesetzt. Wenn das gewünschte Bit in PORTx logisch Null geschrieben wird und der Pin als Ausgangspin konfiguriert ist, wird der Portpin auf Low (Null) getrieben.
Auch wenn ein Pink als Eingangspin konfiguriert wurde, hat PORTx eine Funktion. Wenn in diesem Fall das gewünschte Bit in PORTx logisch eins geschrieben wird, wird der Pull-up-Widerstand aktiviert. Ein Pull-up-Widerstand ist ein höherohmiger Widerstand (im Bereich $20~\rm k\Omega$ … $100~\rm k\Omega$), der bei nicht weiter verbundenem Pin den ausgegebenen Wert auf logisch Eins zieht. Um den Pull-up-Widerstand auszuschalten, muss das gewünschte Bit in PORTx logisch Null geschrieben werden oder der Pin muss als Ausgangspin konfiguriert werden.
Das Einlesen der Signale wird in einem späteren Kapitel erklärt.
[IC Name] „datasheet“ site:[Herstellername].com filetype:pdf
, da es sich beim Datasheet um ein PDF handelt.
Leider gibt es gerade bei dem ATMEGA88 auch ein veraltetes Datenblatt, welches just das ist, wass sich z.B. über Google leichter finden lässt.
in diesem Fall muss also über die Herstellerseite gesucht werden, bzw. bei der Suchmaschine ATMEGA88
eingeben und anschließend auf die Herstellerseite klicken.
Zum Lesen der Datenblätter empfiehlt sich ein Download und die Betrachtung über einen PDF-Viewer, welcher ein Inhaltsverzeichnis als Seitenleiste ermöglicht (z.B. Acrobat Reader). Ansonsten ist das Inhaltsverzeichnis häufig auch auf den hinteren Seiten des Datenblatts zu finden.
Die gesuchte Pinbelegung ist für den ATmega88 konkret auf Seite 3 unter „1. Pin Configurations“:
Falls Sie mit dem englischen Datenblatt Probleme haben, kann ich die deutsche Übersetzung des ATmega88 empfehlen, welcher sich im Wesentlichen ähnlich verhält. Beachten Sie aber, dass viele weitere Datenblätter nur in englisch vorhanden sind. Ein Umgang mit der englischen Dokumentation sollte also erlernt werden.
Die Anschlüsse GND (Masse) und VCC (Versorgung) sind in der Simulation automatisch verbunden. Der Microcontroller ist also stets betriebsbereit.
In Realität wird die Taktfrequenz durch die Randbedingungen wie Performance, Stromverbrauch, Spannungsversorgung oder Platinengröße vorgegeben. Die reale Hardware des ATmega 88 beinhaltet einen internen Taktgeber mit 8 MHz, welche durch einen Teiler auf 1 MHz reduziert sind. Über einen externen Quarz an den Pins PB6 und PB7 kann bei der realen Hardware (mit weiteren Einstellung der Fuses) ein externen Takt eingespeist werden. Häufig werden hierbei - neben ganzzahligen MHz - auch Vielfache von 256 genutzt, wie z.B. 12,288 MHz. Dies vereinfacht das exakte Abzählen von (Milli)Sekunden, da intern hierzu 8-Bit-Zähler genutzt werden können. Der Takt kann bei der realen Hardware in der Regel nicht zur Laufzeit beliebig geändert werden, sondern liegt fest vor.
Der Controller kenn bei der Ausführung des Codes die Taktfrequenz nicht. Sollen genaue Bruchteile einer Sekunde erzeugt werden, muss dem Compiler die Frequenz über einen define mitgeteilt werden. Im Code geschieht dies über #define F_CPU 8000000UL
In der Simulation kann über rechte Maustaste auf den uC » Eigenschaften » MHz
die Frequenz zwischen 1kHz und 100MHz beliebig geändert werden. Dies ist in Realität nicht möglich. Die höchste Frequenz ist laut Datenblatt 16MHz.
In diesem Fall wäre also die Lösung entweder den Wert von F_CPU
oder die Frequenz in der Simulation anzupassen.
Im Prinzip würde folgender Code das auch ermöglichen:
#define F_CPU 8000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { // Die Zahl in folgender Zeile gibt die Bitposition in der nächsten Zeile an: // 76543210 DDRB=0b00000100; // B2 soll als Ausgang genutzt werden while (1) { PORTB = 0b00000100; // Das Bit für B2 wird gesetzt; am Ausgang liegt VCC an _delay_ms(1000); PORTB = 0b00000000; // Das Bit für B2 wird gelöscht; am Ausgang liegt 0V an _delay_ms(1000); } }
Hierbei gibt es aber mehrere Probleme:
Achten Sie also darauf, dass die Umsetzung im Code genau das tut was gewünscht ist. In diesem Fall soll nur das Bit B2 manipuliert und alle anderen unverändert belassen werden.
Hiermit ergibt sich die Frage, wie genau nur ein Bit in einem Register geändert werden kann. Dies ist über die Funktionen der logischen Bitmanipulation möglich (UND, ODER, etc.). Hierzu wird das gewünschte Register mit einer Maskierung verknüpft.
Über die Disjunktion (ODER bzw. in C über |
) mit logisch Eins können nur bestimmte Bitpositionen auf logisch Eins gesetzt werden:
Register-Byte .... R01 = 0b01100101 (Beispielwert)
Die fett markierten Bits mit logisch Eins in der Maske MSK gewährleisten also, dass die Bits in der Disjunktion
Maske ............ MSK = 0b00001100
Disjunktion .. R01|MSK = 0b01101101
R01|MSK
gesetzt sind.
In C wäre hierfür R01 = R01|MSK;
zu schreiben - R01 ist im LED-Code PORTB; in MSK sollte nur das Bit 2 gesetzt sein. Die Programmiersprache C bietet die Möglichkeit mathematische Operatoren auch noch etwas umzuschreiben: R01 |= MSK;
Auch für das Setzen eines einzelnen Bits in MSK kann eine logische Bitmanipulation genutzt werden: das Verschieben aller Bits nach links. Der Code 1«2
erzeugt ein Byte mit dem Inhalt 0b00000100
. Für den Neuling mag 1«2
etwas schwerer zu lesen sein - für den fortgeschrittenen Entwickler ist dies leichter zu lesen. Daneben bietet die inkludierte Bibliothek <avr/io.h>
die Möglichkeit auf weitere defines zuzugreifen, z.B. PB2 welches durch 2 ersetzt wird. Damit ließe sich der Code für des Setzen eines Bits schreiben zu:
PORTB |= 1<<PB2; // Das Bit für B2 wird gesetzt; am Ausgang liegt VCC an
Für das Löschen kann ein ähnliches Konzept genutzt werden. Statt eine „VerODERung mit 1“ (im Code R01|0b01000000
) zum Setzen, muss hier eine „VerUNDung mit 0“ (im Code R01|0b10111111
) genutzt werden:
Register-Byte .... R01 = 0b01100101 (Beispielwert)
Maske ............ MSK = 0b11110011
Konjunktion .. R01&MSK = 0b01100001
Die Maske MSK kann hier durch die Negation des gesetzten Bits erfolgen:
Orginalmaske .............. ORG_MSK = 0b00001100
negierte Maske ..... MSK = ~ORG_MSK = 0b11110011
Der Code ergibt sich dann zu:
PORTB &= ~(1<<PB2); // Das Bit für B2 wird gelöscht; am Ausgang liegt 0V an
#
sind nur Compileranweisungen. Der Compiler setzt den C-Code in maschinenlesbaren Code um. Die Compileranweisungen werden aber nicht in maschinenlesbaren Code umgesetzt, sondern weisen den Compiler an verschiedene Dinge zu tun. #define
speziell weist den Compiler an eine dargestellte Zeichenfolge durch eine andere zu ersetzen. Beispiel:
#define DUMMY 5 ... int main(void) { ... A = DUMMY + 2; ... }
In diesem Beispiel wird vor der Übersetzung des Codes die Zeichenfolge DUMMY
durch 5
ersetzt und dann erst kompiliert. Beim Ersetzen wird keine Typisierung (Datentypen) beachtet. Weiterhin wird der definierte Code nicht vorher berechnet, was zu Problemen bei der Priorisierung führen kann:
#define DUMMY1 500 #define DUMMY2 5 + 3 ... int main(void) { ... uint8_t a = DUMMY1; // DUMMY1 ist größer als 500 uint8_t b = DUMMY2 * 2; // Es wird 5 + 3 *2 = 11 ausgegeben, und nicht (5 + 3)*2 = 16 ... }
Prinzipiell kann also #define
für Zahlenwerte genutzt werden, sofern die obengenannten Grenzen beachtet werden. Diese Probleme lassen sich für Zahlenwerte über die Verwendung von static const umgehen (z.B. static const int var = 5;
). In Microcontrollern werden von der Programmierumgebung häufig auch defines vorgegeben (z.B. PORTB
, PB2
). Der große Vorteil von Compiler-Definitionen besteht darin, dass so die verschiedene eher schwer lesbare, aber häufig verwendete Codeschnipsel anschaulicher umschrieben werden können. Folgende defines werden beispielsweise häufig genutzt:
#define SET_BIT(BYTE, BIT) ((BYTE) |= (1 << (BIT))) // Bit Zustand in Byte setzen #define CLR_BIT(BYTE, BIT) ((BYTE) &= ~(1 << (BIT))) // Bit Zustand in Byte loeschen #define TGL_BIT(BYTE, BIT) ((BYTE) ^= (1 << (BIT))) // Bit Zustand in Byte wechseln (toggle)
#define F_CPU 8000000UL #define SET_BIT(BYTE, BIT) ((BYTE) |= (1 << (BIT))) // Bit Zustand in Byte setzen #define TGL_BIT(BYTE, BIT) ((BYTE) ^= (1 << (BIT))) // Bit Zustand in Byte wechseln (toggle) #define LED_WAIT_TIME 1000 // Dauer bis zum Taktwechsel am Pin #define LED_PIN PB2 // Pin an dem die LED anschlossen ist #include <avr/io.h> #include <util/delay.h> int main(void) { SET_BIT(DDRB, LED_PIN); while (1) { TGL_BIT(PORTB, LED_PIN); _delay_ms(LED_WAIT_TIME); } }
Goto Implementation
(Alternativ <Alt>+<G>) . Wird beispielsweise PB2 eingegeben und die Implementierung angezeigt, ist zu sehen, dass PB2 auch nur ein define ist: #define PB2 2
. Selbiges ist bei DDRD zu finden; dort wird aber über den define der Text durch _SFR_IO8(0x05)
ersetzt. _SFR_IO8(0x05)
ist ein Befehl für den Microcontroller, um auf ein Special Function Register des Input-Output-Registerbereichs zugreifen, welches im Speicher unter 0x05
abliegt.