Inhaltsverzeichnis:

AVR Assembler Tutorial 3: 9 Schritte
AVR Assembler Tutorial 3: 9 Schritte

Video: AVR Assembler Tutorial 3: 9 Schritte

Video: AVR Assembler Tutorial 3: 9 Schritte
Video: AVR Assembly Tutorial: Part 1 (Basic Commands) 2024, Juli
Anonim
AVR-Assembler-Tutorial 3
AVR-Assembler-Tutorial 3

Willkommen zu Tutorial Nummer 3!

Bevor wir anfangen, möchte ich einen philosophischen Punkt ansprechen. Haben Sie keine Angst, mit den Schaltungen und dem Code zu experimentieren, die wir in diesen Tutorials erstellen. Ändern Sie die Leitungen, fügen Sie neue Komponenten hinzu, nehmen Sie Komponenten heraus, ändern Sie Codezeilen, fügen Sie neue Zeilen hinzu, löschen Sie Zeilen und sehen Sie, was passiert! Es ist sehr schwer etwas kaputt zu machen und wenn ja, wen interessiert das? Nichts, was wir verwenden, einschließlich des Mikrocontrollers, ist sehr teuer und es ist immer lehrreich, zu sehen, wie Dinge scheitern können. Sie werden nicht nur herausfinden, was Sie beim nächsten Mal nicht tun sollten, sondern vor allem auch wissen, warum Sie es nicht tun sollten. Wenn Sie ein bisschen wie ich sind, als Sie ein Kind waren und ein neues Spielzeug bekamen, dauerte es nicht lange, bis Sie es in Einzelteilen hatten, um zu sehen, wie es richtig funktionierte? Manchmal wurde das Spielzeug irreparabel beschädigt, aber keine große Sache. Wenn man einem Kind erlaubt, seine Neugier bis hin zu zerbrochenem Spielzeug zu erforschen, wird es zu einem Wissenschaftler oder Ingenieur anstelle eines Tellerwäschers.

Heute werden wir eine sehr einfache Schaltung verdrahten und dann ein bisschen in die Theorie einsteigen. Tut mir leid, aber wir brauchen die Werkzeuge! Ich verspreche, dass wir dies in Tutorial 4 nachholen werden, wo wir ernsthaftere Strecken bauen werden und das Ergebnis ziemlich cool sein wird. Die Art und Weise, wie Sie alle diese Tutorials durchführen müssen, ist jedoch sehr langsam und nachdenklich. Wenn Sie sich einfach durcharbeiten, die Schaltung erstellen, den Code kopieren und einfügen und dann ausführen, wird es sicher funktionieren, aber Sie werden nichts lernen. Sie müssen über jede Zeile nachdenken. Pause. Experiment. Erfinden. Wenn Sie es so machen, werden Sie am Ende des 5. Tutorials damit fertig sein, coole Sachen zu bauen und keine Nachhilfe mehr benötigen. Ansonsten schaust du einfach zu, anstatt zu lernen und zu erschaffen.

Auf jeden Fall genug Philosophie, los geht's!

In diesem Tutorial benötigen Sie:

  1. Ihr Prototyping-Board
  2. eine LED
  3. Anschlussdrähte
  4. ein Widerstand von 220 bis 330 Ohm
  5. Das Instruction Set Manual: www.atmel.com/images/atmel-0856-avr-instruction-se…
  6. Das Datenblatt: www.atmel.com/images/Atmel-8271-8-bit-AVR-Microco…
  7. ein anderer Quarzoszillator (optional)

Hier ist ein Link zur vollständigen Sammlung von Tutorials:

Schritt 1: Aufbau der Schaltung

Bau der Schaltung
Bau der Schaltung

Die Schaltung in diesem Tutorial ist extrem einfach. Wir werden im Wesentlichen das "blink" -Programm schreiben, also brauchen wir nur Folgendes.

Schließen Sie eine LED an PD4, dann an einen 330-Ohm-Widerstand und dann an Masse an. d.h.

PD4 - LED - R(330) - GND

und das ist alles!

Die Theorie wird jedoch hart umkämpft…

Schritt 2: Warum brauchen wir die Kommentare und die Datei M328Pdef.inc?

Ich denke, wir sollten damit beginnen, zu zeigen, warum die Include-Datei und die Kommentare hilfreich sind. Keines von ihnen ist wirklich notwendig und Sie können Code ohne sie auf die gleiche Weise schreiben, assemblieren und hochladen und es wird perfekt laufen (obwohl Sie ohne die Include-Datei möglicherweise einige Beschwerden vom Assembler erhalten - aber keine Fehler).

Hier ist der Code, den wir heute schreiben werden, außer dass ich die Kommentare und die Include-Datei entfernt habe:

. Gerät ATmega328P

.org 0x0000 jmp a.org 0x0020 jmp ea: ldi r16, 0x05 out 0x25, r16 ldi r16, 0x01 sts 0x6e, r16 sei clr r16 out 0x26, r16 sbi 0x0a, 0x04 sbi 0x0b, 0x04 cb: cbi 0x0b, 0x04 rcall c rjmp bc: clr r17 d: cpi r17, 0x1e brne d ret e: inc r17 cpi r17, 0x3d brne PC+2 clr r17 reti

ziemlich einfach oder? Haha. Wenn Sie diese Datei zusammengestellt und hochgeladen haben, wird die LED mit einer Geschwindigkeit von 1 Blinken pro Sekunde blinken, wobei das Blinken 1/2 Sekunde dauert und die Pause zwischen den Blinkvorgängen 1/2 Sekunde dauert.

Ein Blick auf diesen Code ist jedoch kaum aufschlussreich. Wenn Sie Code wie diesen schreiben würden und ihn in Zukunft ändern oder wiederverwenden möchten, würden Sie es schwer haben.

Lassen Sie uns also die Kommentare einfügen und die Datei wieder einfügen, damit wir einen Sinn daraus machen können.

Schritt 3: Blink.asm

Hier ist der Code, den wir heute besprechen werden:

;************************************

; geschrieben von: 1o_o7; Datum:; Version: 1.0; Datei gespeichert als: blink.asm; für AVR: atmega328p; Taktfrequenz: 16MHz (optional);************************************; Programmfunktion:---------------------; zählt Sekunden durch Blinken einer LED ab;; PD4 - LED - R(330 Ohm) - GND;;--------------------------------------------------.nolist.include "./m328Pdef.inc".liste;==============; Deklarationen:.def temp = r16.def overflows = r17.org 0x0000; Speicherort (PC) des Reset-Handlers rjmp Reset; jmp kostet 2 CPU-Zyklen und rjmp kostet nur 1; es sei denn, Sie müssen mehr als 8k Bytes springen; du brauchst nur rjmp. Einige Mikrocontroller daher nur; habe rjmp und nicht jmp.org 0x0020; Speicherort des Timer0-Überlaufhandlers rjmp overflow_handler; gehe hier hin, wenn ein Timer0-Überlauf-Interrupt auftritt;============ Reset: ldi temp, 0b00000101 out TCCR0B, temp; setze die Clock Selector Bits CS00, CS01, CS02 auf 101; dies versetzt Timer Counter0, TCNT0 in den FCPU/1024-Modus; also tickt es bei der CPU freq/1024 ldi temp, 0b00000001 sts TIMSK0, temp; setze das Timer Overflow Interrupt Enable (TOIE0)-Bit; des Zeitgeberunterbrechungsmaskenregisters (TIMSK0) sei; globale Interrupts aktivieren -- äquivalent zu "sbi SREG, I" clr temp out TCNT0, temp; den Timer/Counter auf 0 initialisieren sbi DDRD, 4; PD4 auf Ausgang setzen;======================; Hauptteil des Programms: blink: sbi PORTD, 4; LED auf PD4 einschalten rcall delay; Verzögerung beträgt 1/2 Sekunde cbi PORTD, 4; LED am PD4 ausschalten rcall delay; Verzögerung beträgt 1/2 Sekunde rjmp blinken; Schleife zurück zur Startverzögerung: clr overflows; setze overflows auf 0 sec_count: cpi overflows, 30; vergleiche Anzahl der Überläufe und 30 brne sec_count; Verzweigen Sie zurück zu sec_count, wenn nicht gleich ret; wenn 30 Überläufe aufgetreten sind, kehren Sie zu blink overflow_handler zurück: inc overflows; addiere 1 zu den Überläufen Variable cpi overflows, 61; vergleiche mit 61 Brne PC+2; Programmzähler + 2 (nächste Zeile überspringen) wenn ungleich clr überläuft; wenn 61 Überläufe aufgetreten sind, den Zähler auf Null zurücksetzen reti; Rückkehr von Unterbrechung

Wie Sie sehen können, sind meine Kommentare jetzt etwas kürzer. Sobald wir wissen, was die Befehle im Befehlssatz tun, müssen wir dies nicht in Kommentaren erklären. Wir müssen nur erklären, was aus Sicht des Programms vor sich geht.

Wir werden Stück für Stück besprechen, was all dies bewirkt, aber zuerst versuchen wir, eine globale Perspektive zu bekommen. Der Hauptteil des Programms funktioniert wie folgt.

Zuerst setzen wir Bit 4 von PORTD mit "sbi PORTD, 4", dies sendet eine 1 an PD4, wodurch die Spannung an diesem Pin auf 5 V gelegt wird. Dadurch wird die LED eingeschaltet. Wir springen dann zum Unterprogramm "Verzögerung", das 1/2 Sekunde zählt (wir werden später erklären, wie es funktioniert). Wir kehren dann zum Blinken und Löschen von Bit 4 auf PORTD zurück, das PD4 auf 0 V setzt und somit die LED ausschaltet. Wir verzögern dann eine weitere 1/2 Sekunde und springen dann mit "rjmp blink" wieder zum Anfang des Blinkens zurück.

Sie sollten diesen Code ausführen und sehen, dass er tut, was er soll.

Und da hast du es! Das ist alles, was dieser Code physisch tut. Die interne Mechanik des Mikrocontrollers ist etwas komplizierter und deshalb machen wir dieses Tutorial. Lassen Sie uns also jeden Abschnitt der Reihe nach besprechen.

Schritt 4:.org-Assembler-Direktiven

Wir wissen bereits aus unseren vorherigen Tutorials, was die Assembler-Direktiven.nolist,.list,.include und.def tun. Schauen wir uns also zuerst die 4 Codezeilen an, die danach folgen:

.org 0x0000

jmp Reset.org 0x0020 jmp overflow_handler

Die.org-Anweisung teilt dem Assembler mit, wo im "Programmspeicher" die nächste Anweisung abgelegt werden soll. Während Ihr Programm ausgeführt wird, enthält der "Programmzähler" (abgekürzt als PC) die Adresse der aktuell ausgeführten Zeile. In diesem Fall, wenn der PC auf 0x0000 steht, wird der Befehl "jmp Reset" an diesem Speicherort angezeigt. Der Grund, warum wir jmp Reset an dieser Stelle ablegen möchten, ist, dass der PC beim Start des Programms oder beim Zurücksetzen des Chips an dieser Stelle mit der Ausführung von Code beginnt. Wie wir sehen, haben wir es gerade angewiesen, sofort zum Abschnitt mit der Bezeichnung "Zurücksetzen" zu "springen". Warum haben wir das gemacht? Das bedeutet, dass die letzten beiden Zeilen oben nur übersprungen werden! Wieso den?

Nun, da wird es interessant. Sie müssen jetzt einen PDF-Viewer mit dem vollständigen ATmega328p-Datenblatt öffnen, auf das ich auf der ersten Seite dieses Tutorials hingewiesen habe (deshalb ist es Punkt 4 im Abschnitt "Sie benötigen"). Wenn Ihr Bildschirm zu klein ist oder Sie bereits viel zu viele Fenster geöffnet haben (wie bei mir), können Sie das tun, was ich tue, und es auf einen Ereader oder Ihr Android-Telefon legen. Sie werden es die ganze Zeit verwenden, wenn Sie vorhaben, Assemblercode zu schreiben. Das Coole ist, dass alle Mikrocontroller auf sehr ähnliche Weise organisiert sind. Wenn Sie sich also daran gewöhnt haben, Datenblätter zu lesen und daraus zu codieren, werden Sie es fast trivial finden, dasselbe für einen anderen Mikrocontroller zu tun. Wir lernen also tatsächlich, alle Mikrocontroller in gewisser Weise zu verwenden und nicht nur den atmega328p.

Okay, blättern Sie im Datenblatt auf Seite 18 und sehen Sie sich Abbildung 8-2 an.

So wird der Programmspeicher im Mikrocontroller eingerichtet. Sie können sehen, dass es mit der Adresse 0x0000 beginnt und in zwei Abschnitte unterteilt ist; einen Anwendungs-Flash-Abschnitt und einen Boot-Flash-Abschnitt. Wenn Sie kurz auf Seite 277 Tabelle 27-14 verweisen, sehen Sie, dass der Anwendungs-Flash-Abschnitt die Speicherorte von 0x0000 bis 0x37FF und der Boot-Flash-Abschnitt die restlichen Speicherorte von 0x3800 bis 0x3FFF belegt.

Übung 1: Wie viele Speicherorte befinden sich im Programmspeicher? D.h. Konvertieren Sie 3FFF in Dezimalzahlen und addieren Sie 1, da wir bei 0 zu zählen beginnen. Da jeder Speicherplatz 16 Bit (oder 2 Byte) breit ist, wie groß ist die Gesamtzahl der Bytes des Speichers? Wandeln Sie dies nun in Kilobyte um und denken Sie daran, dass ein Kilobyte 2^10 = 1024 Bytes enthält. Der Boot-Flash-Abschnitt reicht von 0x3800 bis 0x37FF, wie viele Kilobyte sind das? Wie viele Kilobyte Speicher bleiben uns übrig, um unser Programm zu speichern? Mit anderen Worten, wie groß darf unser Programm sein? Schließlich, wie viele Codezeilen können wir haben?

Nun gut, da wir nun alles über die Organisation des Flash-Programmspeichers wissen, fahren wir mit unserer Diskussion der.org-Anweisungen fort. Wir sehen, dass der erste Speicherplatz 0x0000 unsere Anweisung enthält, zu unserem Abschnitt zu springen, den wir mit Reset bezeichnet haben. Jetzt sehen wir, was die Anweisung ".org 0x0020" bewirkt. Es besagt, dass die Anweisung in der nächsten Zeile am Speicherplatz 0x0020 platziert werden soll. Die Anweisung, die wir dort platziert haben, ist ein Sprung zu einem Abschnitt in unserem Code, den wir mit "overflow_handler" bezeichnet haben… Um das herauszufinden, blättern wir im Datenblatt auf Seite 65 und werfen einen Blick auf Tabelle 12-6.

Tabelle 12-6 ist eine Tabelle mit "Reset- und Interrupt-Vektoren" und zeigt genau, wohin der PC gehen wird, wenn er einen "Interrupt" empfängt. Wenn Sie sich zum Beispiel Vektornummer 1 ansehen. Die "Quelle" des Interrupts ist "RESET", was als "Externer Pin, Power-on-Reset, Brown-out-Reset und Watchdog-System-Reset" definiert ist, was bedeutet, wenn einer von Wenn diese Dinge mit unserem Mikrocontroller passieren, beginnt der PC mit der Ausführung unseres Programms am Programmspeicherplatz 0x0000. Was ist dann mit unserer.org-Richtlinie? Nun, wir haben einen Befehl an der Speicherstelle 0x0020 platziert und wenn Sie die Tabelle nach unten schauen, werden Sie sehen, dass bei einem Timer/Counter0-Überlauf (von TIMER0 OVF kommend) alles ausgeführt wird, was sich an der Stelle 0x0020 befindet. Wann immer das passiert, springt der PC an die Stelle, die wir mit "overflow_handler" bezeichnet haben. Cool oder? Sie werden in einer Minute sehen, warum wir dies getan haben, aber lassen Sie uns diesen Schritt des Tutorials zunächst mit einer Randbemerkung beenden.

Wenn wir unseren Code sauberer und aufgeräumter machen wollen, sollten wir die 4 Zeilen, über die wir gerade diskutieren, wirklich durch die folgenden ersetzen (siehe Seite 66):

.org 0x0000

rjmp-Reset; PC = 0x0000 reti; PC = 0x0002 reti; PC = 0x0004 reti; PC = 0x0006 reti; PC = 0x0008 reti; PC = 0x000A … reti; PC = 0x001E jmp overflow_handler: PC = 0x0020 reti: PC = 0x0022 … reti; PC = 0x0030 reti; PC = 0x0032

Wenn also ein bestimmter Interrupt auftritt, wird er nur "reti", was "Rückkehr vom Interrupt" bedeutet, und sonst passiert nichts. Aber wenn wir diese verschiedenen Interrupts niemals "aktivieren", dann werden sie nicht verwendet und wir können Programmcode an diesen Stellen einfügen. In unserem aktuellen "blink.asm"-Programm werden wir nur den Timer0-Überlauf-Interrupt aktivieren (und natürlich den Reset-Interrupt, der immer aktiviert ist) und kümmern uns nicht um die anderen.

Wie "aktivieren" wir dann den Timer0-Überlauf-Interrupt? … das ist das Thema unseres nächsten Schrittes in diesem Tutorial.

Schritt 5: Timer/Zähler 0

Timer/Zähler 0
Timer/Zähler 0

Schauen Sie sich das obige Bild an. Dies ist der Entscheidungsfindungsprozess des "PC", wenn ein äußerer Einfluss den Fluss unseres Programms "unterbricht". Das erste, was es tut, wenn es ein Signal von außen erhält, dass ein Interrupt aufgetreten ist, ist, dass es überprüft, ob wir das "Interrupt-Enable"-Bit für diesen Interrupt-Typ gesetzt haben. Wenn dies nicht der Fall ist, führt es einfach unsere nächste Codezeile aus. Wenn wir dieses spezielle Interrupt-Aktivierungsbit gesetzt haben (so dass an dieser Bitstelle eine 1 anstelle einer 0 steht), wird überprüft, ob wir "globale Interrupts" aktiviert haben oder nicht, wenn nicht, geht es wieder zur nächsten Zeile des Codes und fahren Sie fort. Wenn wir auch globale Interrupts aktiviert haben, geht es zum Programmspeicherplatz dieses Interrupt-Typs (wie in Tabelle 12-6) gezeigt und führt den Befehl aus, den wir dort platziert haben. Sehen wir uns also an, wie wir all dies in unserem Code implementiert haben.

Der mit Zurücksetzen gekennzeichnete Abschnitt unseres Codes beginnt mit den folgenden zwei Zeilen:

Zurücksetzen:

ldi temp, 0b000000101 out TCCR0B, temp

Wie wir bereits wissen, lädt dies in temp (d. h. R16) die unmittelbar folgende Zahl, die 0b00000101 ist. Dann schreibt es diese Nummer mit dem Befehl "out" in das Register namens TCCR0B. Was ist dieses Register? Kommen wir nun zu Seite 614 des Datenblatts. Dies ist in der Mitte einer Tabelle, die alle Register zusammenfasst. Unter Adresse 0x25 finden Sie TCCR0B. (Jetzt wissen Sie, woher die Zeile "out 0x25, r16" in meiner unkommentierten Version des Codes stammt). Wir sehen an dem obigen Codesegment, dass wir das 0. Bit und das 2. Bit gesetzt und den Rest gelöscht haben. Wenn Sie sich die Tabelle ansehen, können Sie sehen, dass wir CS00 und CS02 eingestellt haben. Kommen wir nun zum Kapitel im Datenblatt namens "8-bit Timer/Counter0 with PWM". Lesen Sie insbesondere Seite 107 dieses Kapitels. Sie werden die gleiche Beschreibung des Registers "Timer/Counter Control Register B" (TCCR0B) sehen, die wir gerade in der Registerübersichtstabelle gesehen haben (also hätten wir direkt hierher kommen können, aber ich wollte, dass Sie sehen, wie die Übersichtstabellen verwendet werden zum späteren Nachschlagen). Das Datenblatt enthält weiterhin eine Beschreibung der einzelnen Bits in diesem Register und ihrer Funktion. Wir überspringen das alles vorerst und blättern zu Tabelle 15-9. Diese Tabelle zeigt die "Clock Select Bit Description". Schauen Sie nun in dieser Tabelle nach, bis Sie die Zeile finden, die den Bits entspricht, die wir gerade in diesem Register gesetzt haben. Die Zeile sagt "clk/1024 (vom Prescaler)". Dies bedeutet, dass Timer/Counter0 (TCNT0) mit einer Rate abläuft, die der CPU-Frequenz geteilt durch 1024 entspricht. Da unser Mikrocontroller von einem 16-MHz-Quarzoszillator gespeist wird, bedeutet dies, dass die Rate, mit der unsere CPU Befehle ausführt, 16 Millionen Anweisungen pro Sekunde. Die Rate, mit der unser TCNT0-Zähler tickt, beträgt dann 16 Millionen/1024 = 15625 Mal pro Sekunde (versuchen Sie es mit verschiedenen Taktauswahlbits und sehen Sie, was passiert - erinnern Sie sich an unsere Philosophie?). Lassen Sie uns die Nummer 15625 für später im Hinterkopf behalten und zu den nächsten beiden Codezeilen übergehen:

ldi-Temp, 0b00000001

sts TIMSK0, temp

Dies setzt das 0. Bit eines Registers namens TIMSK0 und löscht den Rest. Wenn Sie sich Seite 109 im Datenblatt ansehen, werden Sie feststellen, dass TIMSK0 für "Timer/Counter Interrupt Mask Register 0" steht und unser Code das 0. Bit mit dem Namen TOIE0 gesetzt hat, das für "Timer/Counter0 Overflow Interrupt Enable" steht. … Dort! Jetzt sehen Sie, worum es geht. Wir haben jetzt das "Interrupt Enable Bit gesetzt", wie wir es von der ersten Entscheidung in unserem Bild oben wollten. Jetzt müssen wir nur noch "globale Interrupts" aktivieren und unser Programm kann auf diese Art von Interrupts reagieren. Wir werden in Kürze globale Interrupts aktivieren, aber bevor wir das tun, hat Sie vielleicht etwas verwirrt. Warum zum Teufel habe ich den Befehl "sts" verwendet, um in das TIMSK0-Register zu kopieren, anstatt das übliche "out"?

Immer wenn Sie sehen, dass ich eine Anweisung verwende, die Sie noch nie gesehen haben, sollten Sie als Erstes Seite 616 im Datenblatt aufschlagen. Dies ist die "Befehlssatz-Zusammenfassung". Suchen Sie nun die Anweisung "STS", die ich verwendet habe. Es sagt, es nimmt eine Zahl aus einem R-Register (wir haben R16 verwendet) und den Speicherort k "Direkt in SRAM speichern" (in unserem Fall von TIMSK0). Warum mussten wir also "sts" verwenden, das 2 Taktzyklen benötigt (siehe letzte Spalte in der Tabelle), um in TIMSK0 zu speichern, und wir brauchten nur "out", das nur einen Taktzyklus benötigt, um zuvor in TCCR0B zu speichern? Um diese Frage zu beantworten, müssen wir zu unserer Registerübersichtstabelle auf Seite 614 zurückkehren. Sie sehen, dass sich das TCCR0B-Register an der Adresse 0x25, aber auch an (0x45) befindet, richtig? Dies bedeutet, dass es sich um ein Register im SRAM handelt, aber auch um eine bestimmte Art von Register, die als "Port" (oder E/A-Register) bezeichnet wird. Wenn Sie sich die Befehlsübersichtstabelle neben dem Befehl "out" ansehen, werden Sie sehen, dass Werte aus den "Arbeitsregistern" wie R16 genommen und an einen PORT gesendet werden. So können wir beim Schreiben in TCCR0B "out" verwenden und uns einen Takt sparen. Schlagen Sie nun TIMSK0 in der Registertabelle nach. Sie sehen, dass es die Adresse 0x6e hat. Dies liegt außerhalb des Bereichs der Ports (die nur die ersten 0x3F-Speicherorte des SRAM sind) und daher müssen Sie darauf zurückgreifen, den sts-Befehl zu verwenden und dafür zwei CPU-Taktzyklen zu benötigen. Bitte lesen Sie jetzt Anmerkung 4 am Ende der Tabelle mit der Zusammenfassung der Anweisungen auf Seite 615. Beachten Sie auch, dass sich alle unsere Eingangs- und Ausgangsports, wie PORTD, am unteren Rand der Tabelle befinden. Zum Beispiel ist PD4 Bit 4 an der Adresse 0x0b (jetzt sehen Sie, wo das ganze 0x0b-Zeug in meinem unkommentierten Code herkommt!).. Okay, kurze Frage: Haben Sie "sts" in "out" geändert und sehen Sie, was? das passiert? Denken Sie an unsere Philosophie! mach es kaputt! nimm nicht nur mein Wort.

Okay, bevor wir weitermachen, blättern Sie für eine Minute auf Seite 19 im Datenblatt. Sie sehen ein Bild des Datenspeichers (SRAM). Die ersten 32 Register im SRAM (von 0x0000 bis 0x001F) sind die "Allzweck-Arbeitsregister" R0 bis R31, die wir ständig als Variablen in unserem Code verwenden. Die nächsten 64 Register sind die I/O-Ports bis 0x005f (dh diejenigen, über die wir gesprochen haben, die diese ungeklammerten Adressen neben sich in der Registertabelle haben, die wir mit dem Befehl "out" anstelle von "sts" verwenden können). der nächste Abschnitt des SRAM enthält alle anderen Register in der Übersichtstabelle bis zur Adresse 0x00FF, und der Rest ist interner SRAM. Kommen wir nun kurz zu Seite 12. Dort sehen Sie eine Tabelle der "allgemeinen Arbeitsregister", die wir immer als unsere Variablen verwenden. Siehst du die dicke Linie zwischen den Zahlen R0 bis R15 und dann R16 bis R31? Aus diesem Grund verwenden wir immer R16 als kleinstes und ich werde im nächsten Tutorial etwas mehr darauf eingehen, wo wir auch die drei indirekten 16-Bit-Adressregister X, Y und Z benötigen gehen wir aber noch darauf ein, da wir es jetzt nicht brauchen und wir uns hier genug verzetteln.

Blättern Sie eine Seite zurück zu Seite 11 des Datenblatts. Sie sehen oben rechts ein Diagramm des SREG-Registers? Sie sehen, dass Bit 7 dieses Registers "I" genannt wird. Gehen Sie nun die Seite nach unten und lesen Sie die Beschreibung von Bit 7…. Yay! Es ist das Global Interrupt Enable-Bit. Das ist es, was wir einstellen müssen, um die zweite Entscheidung in unserem obigen Diagramm zu passieren und Timer-/Zähler-Überlauf-Interrupts in unserem Programm zuzulassen. Die nächste Zeile unseres Programms sollte also lauten:

sbi SREG, I

welches das mit "I" bezeichnete Bit im SREG-Register setzt. Stattdessen haben wir jedoch die Anweisung verwendet

sei

stattdessen. Dieses Bit wird in Programmen so oft gesetzt, dass sie es einfach einfacher gemacht haben.

Okay! Jetzt haben wir die Überlauf-Interrupts einsatzbereit, damit unser "jmp overflow_handler" immer dann ausgeführt wird, wenn einer auftritt.

Bevor wir weitermachen, werfen Sie einen kurzen Blick auf das SREG-Register (Status Register), da es sehr wichtig ist. Lesen Sie, was die einzelnen Flaggen darstellen. Insbesondere werden viele der von uns verwendeten Anweisungen diese Flags ständig setzen und überprüfen. Zum Beispiel werden wir später den Befehl "CPI" verwenden, was "sofort vergleichen" bedeutet. Sehen Sie sich die Übersichtstabelle der Anweisungen für diese Anweisung an und beachten Sie, wie viele Flags sie in der Spalte "Flags" setzt. Dies sind alles Flags in SREG und unser Code wird sie setzen und ständig überprüfen. Beispiele sehen Sie in Kürze. Schließlich ist das letzte Bit dieses Codeabschnitts:

clr temp

out TCNT0, temp sbi DDRD, 4

Die letzte Zeile hier ist ziemlich offensichtlich. Es setzt nur das 4. Bit des Datenrichtungsregisters für PortD, wodurch PD4 auf OUTPUT gesetzt wird.

Der erste setzt die Variable temp auf Null und kopiert diese dann in das TCNT0-Register. TCNT0 ist unser Timer/Counter0. Dadurch wird es auf Null gesetzt. Sobald der PC diese Zeile ausführt, startet der Timer0 bei Null und zählt mit einer Rate von 15625-mal pro Sekunde. Das Problem ist folgendes: TCNT0 ist ein "8-Bit"-Register, oder? Was ist also die größte Zahl, die ein 8-Bit-Register aufnehmen kann? Nun, 0b11111111 ist es. Dies ist die Zahl 0xFF. Welches ist 255. Siehst du also, was passiert? Der Timer läuft 15625-mal pro Sekunde weiter und jedes Mal, wenn er 255 erreicht, "überläuft" er und geht wieder auf 0 zurück. Gleichzeitig mit dem Zurückgehen auf Null sendet es ein Timer-Overflow-Interrupt-Signal. Der PC bekommt das und weißt du, was er jetzt macht, oder? Ja. Es geht zum Programmspeicherplatz 0x0020 und führt den dort gefundenen Befehl aus.

Groß! Wenn du noch bei mir bist, dann bist du ein unermüdlicher Superheld! Lasst uns weitergehen…

Schritt 6: Überlaufhandler

Nehmen wir also an, dass das Timer/Zähler0-Register gerade übergelaufen ist. Wir wissen jetzt, dass das Programm ein Interrupt-Signal empfängt und 0x0020 ausführt, was den Programmzähler, PC anweist, zum Label "overflow_handler" zu springen. Der folgende Code ist der Code, den wir nach diesem Label geschrieben haben:

overflow_handler:

inc überläuft cpi überläufe, 61 brne PC+2 clr überläufe reti

Das erste, was es tut, ist die Variable "overflows" (das ist unser Name für das Allzweck-Arbeitsregister R17) zu erhöhen, dann "vergleicht" es den Inhalt von Overflows mit der Zahl 61. Der Befehl cpi funktioniert so, dass er einfach subtrahiert die beiden Zahlen und wenn das Ergebnis Null ist, wird das Z-Flag im SREG-Register gesetzt (ich sagte Ihnen, wir würden dieses Register die ganze Zeit sehen). Wenn die beiden Zahlen gleich sind, ist das Z-Flag eine 1, wenn die beiden Zahlen nicht gleich sind, ist es eine 0.

Die nächste Zeile sagt "brne PC+2", was "Zweig wenn nicht gleich" bedeutet. Im Wesentlichen überprüft es das Z-Flag in SREG und wenn es KEINE Eins ist (dh die beiden Zahlen sind nicht gleich, wenn sie gleich wären, würde das Null-Flag gesetzt), verzweigt der PC zu PC+2, was bedeutet, dass er den nächsten überspringt Zeile und geht direkt zu "reti", das vom Interrupt an die Stelle zurückkehrt, an der er sich im Code befand, als der Interrupt eintraf. Wenn der brne-Befehl eine 1 im Null-Flag-Bit findet, würde er nicht verzweigen und stattdessen einfach zur nächsten Zeile weitergehen, die clr überläuft und sie auf 0 zurücksetzt.

Was ist das Nettoergebnis von all dem?

Nun, wir sehen, dass dieser Handler jedes Mal, wenn es einen Timer-Überlauf gibt, den Wert von "overflows" um eins erhöht. Die Variable "overflows" zählt also die Anzahl der auftretenden Überläufe. Immer wenn die Zahl 61 erreicht, setzen wir sie auf Null zurück.

Warum in aller Welt sollten wir das jetzt tun?

Mal sehen. Denken Sie daran, dass unsere Taktfrequenz für unsere CPU 16 MHz beträgt und wir sie mit TCCR0B "vorskaliert" haben, sodass der Timer nur mit einer Rate von 15625 Zählungen pro Sekunde zählt, richtig? Und jedes Mal, wenn der Timer einen Zählerstand von 255 erreicht, läuft er über. Das bedeutet, dass es 15625/256 = 61,04 Mal pro Sekunde überläuft. Wir verfolgen die Anzahl der Überläufe mit unserer Variablen "overflows" und vergleichen diese Zahl mit 61. Wir sehen also, dass "overflows" einmal pro Sekunde 61 beträgt! Unser Handler setzt also jede Sekunde "Überläufe" auf Null zurück. Wenn wir also einfach die Variable "Überläufe" überwachen und jedes Mal notieren würden, wenn sie auf Null zurückgesetzt wird, würden wir in Echtzeit von Sekunde zu Sekunde zählen (Beachten Sie, dass wir im nächsten Tutorial zeigen werden, wie Sie eine genauere Verzögerung in Millisekunden auf die gleiche Weise, wie die Arduino "Delay" -Routine funktioniert).

Jetzt haben wir die Timer-Überlauf-Interrupts "behandelt". Stellen Sie sicher, dass Sie verstehen, wie dies funktioniert, und fahren Sie dann mit dem nächsten Schritt fort, bei dem wir diese Tatsache nutzen.

Schritt 7: Verzögerung

Nachdem wir nun gesehen haben, dass unsere Routine "overflow_handler" für den Timer-Überlauf-Interrupt-Handler die Variable "overflows" einmal pro Sekunde auf Null setzt, können wir diese Tatsache nutzen, um eine "Delay"-Subroutine zu entwerfen.

Sehen Sie sich den folgenden Code unter unserer Verzögerung an: label

verzögern:

clr überläuft sec_count: cpi-Überläufe, 30 brne sec_count ret

Wir werden dieses Unterprogramm jedes Mal aufrufen, wenn wir eine Verzögerung in unserem Programm benötigen. Es funktioniert so, dass zuerst die Variable "overflows" auf Null gesetzt wird. Dann tritt es in einen Bereich mit der Bezeichnung "sec_count" ein und vergleicht Überläufe mit 30, wenn sie nicht gleich sind, verzweigt es zurück zum Label sec_count und vergleicht immer wieder usw auf unserem Timer-Interrupt-Handler inkrementiert weiterhin die Variable overflows und ändert sich daher jedes Mal, wenn wir hier herumgehen. Wenn overflows schließlich gleich 30 ist, verlässt es die Schleife und kehrt dorthin zurück, wo wir delay: from aufgerufen haben. Das Nettoergebnis ist a Verzögerung von 1/2 Sekunde

Übung 2: Ändern Sie die overflow_handler-Routine wie folgt:

overflow_handler:

Inc überläuft reti

und führen Sie das Programm aus. Ist etwas anders? Warum oder warum nicht?

Schritt 8: Blinzeln

Schauen wir uns abschließend die Blinkroutine an:

blinken:

sbi PORTD, 4 Rufverzögerung cbi PORTD, 4 Rufverzögerung rjmp blinken

Zuerst schalten wir PD4 ein, dann rufen wir unsere Delay-Subroutine auf. Wir verwenden rcall, damit der PC, wenn er zu einer "ret"-Anweisung kommt, in die Zeile nach rcall zurückkehrt. Dann verzögert die Verzögerungsroutine um 30 Zählungen in der Überlaufvariablen, wie wir gesehen haben, und dies ist fast genau 1/2 Sekunde, dann schalten wir PD4 aus, verzögern eine weitere 1/2 Sekunde und gehen dann wieder zum Anfang zurück.

Das Nettoergebnis ist eine blinkende LED!

Ich denke, Sie werden jetzt zustimmen, dass "blink" wahrscheinlich nicht das beste "Hallo Welt"-Programm in Assembler ist.

Übung 3: Ändern Sie die verschiedenen Parameter im Programm so, dass die LED mit unterschiedlichen Geschwindigkeiten blinkt, z. B. eine Sekunde oder 4 Mal pro Sekunde usw. Übung 4: Ändern Sie sie so, dass die LED unterschiedlich lange an- und ausgeht. Zum Beispiel für 1/4 Sekunde an und dann für 2 Sekunden aus oder so ähnlich. Übung 5: Ändern Sie die TCCR0B-Taktauswahlbits auf 100 und gehen Sie dann in der Tabelle weiter nach oben. An welchem Punkt ist es nicht mehr von unserem "hello.asm"-Programm aus Tutorial 1 zu unterscheiden? Übung 6 (optional): Wenn Sie einen anderen Quarzoszillator haben, z. B. einen 4 MHz oder 13,5 MHz oder was auch immer, tauschen Sie Ihren 16 MHz-Oszillator aus auf Ihrem Steckbrett für das neue und sehen Sie, wie sich dies auf die Blinkrate der LED auswirkt. Sie sollten jetzt in der Lage sein, die genaue Berechnung durchzugehen und genau vorherzusagen, wie sich dies auf die Rate auswirkt.

Schritt 9: Fazit

Für alle, die es bis hierher geschafft haben, herzlichen Glückwunsch!

Mir ist klar, dass es ziemlich schwer ist, sich zu quälen, wenn Sie mehr lesen und nachschlagen als verkabeln und experimentieren, aber ich hoffe, Sie haben die folgenden wichtigen Dinge gelernt:

  1. So funktioniert der Programmspeicher
  2. So funktioniert SRAM
  3. So suchen Sie nach Registern
  4. Anleitungen nachschlagen und wissen, was sie tun
  5. So implementieren Sie Interrupts
  6. Wie der CP den Code ausführt, wie der SREG funktioniert und was bei Interrupts passiert
  7. Wie man Loops und Sprünge macht und im Code herumhüpft
  8. Wie wichtig es ist, das Datenblatt zu lesen!
  9. Sobald Sie wissen, wie Sie all dies für den Atmega328p-Mikrocontroller tun, wird es ein relativer Kinderspiel sein, alle neuen Controller zu lernen, an denen Sie interessiert sind.
  10. So ändern Sie die CPU-Zeit in Echtzeit und verwenden sie in Verzögerungsroutinen.

Jetzt, da wir viel Theorie aus dem Weg geräumt haben, sind wir in der Lage, besseren Code zu schreiben und kompliziertere Dinge zu kontrollieren. Im nächsten Tutorial werden wir genau das tun. Wir werden eine kompliziertere, interessantere Schaltung bauen und sie auf unterhaltsame Weise steuern.

Übung 7: Brechen Sie den Code auf verschiedene Weise auf und sehen Sie, was passiert! Wissenschaftliche Neugier Baby! Jemand anderes kann das Geschirr richtig spülen?Übung 8: Stellen Sie den Code mit der Option "-l" zusammen, um eine Listendatei zu generieren. D.h. "avra -l blink.lst blink.asm" und sehen Sie sich die Listendatei an. Extra Credit: Der unkommentierte Code, den ich zu Beginn angegeben habe, und der kommentierte Code, den wir später besprechen, unterscheiden sich! Es gibt eine Codezeile, die anders ist. Kannst du es finden? Warum spielt dieser Unterschied keine Rolle?

Ich hoffe, du hattest Spaß! Bis zum nächsten Mal…

Empfohlen: