Assembler-Codegenerator-Bibliothek für AVR-Mikrocontroller. Teil 1

Teil 2. Erste Schritte →


Assembler Code Generator Library für AVR-Mikrocontroller


Teil 1. Erste Bekanntschaft


Guten Tag, liebe Chabrowiten. Ich möchte Sie auf das nächste (von den vielen verfügbaren) Projekt zur Programmierung beliebter Mikrocontroller der AVR-Serie aufmerksam machen.


Es wäre möglich, viel Text auszugeben, um zu erklären, warum dies notwendig ist. Schauen Sie sich stattdessen nur Beispiele an, wie es sich von anderen Lösungen unterscheidet. Alle Erklärungen und Vergleiche mit vorhandenen Programmiersystemen werden bei Bedarf analysiert. Die Bibliothek wird gerade fertiggestellt, sodass die Implementierung einiger Funktionen möglicherweise nicht optimal erscheint. Außerdem sollen einige der Aufgaben, die dem Programmierer in dieser Version zugewiesen sind, weiter optimiert oder automatisiert werden.


Also fangen wir an. Ich möchte sofort klarstellen, dass das präsentierte Material keinesfalls als vollständige Beschreibung betrachtet werden sollte, sondern nur als Demonstration einiger Merkmale der entwickelten Bibliothek, um zu verstehen, wie interessant dieser Ansatz für die Leser sein kann.


Wir werden nicht von der vorherrschenden Praxis abweichen und mit einem klassischen Beispiel beginnen, einer Art "Hallo Welt" für Mikrocontroller. Wir blinken nämlich die LED, die an einen der Prozessorzweige angeschlossen ist. Öffnen wir VisualStudio von Microsoft aus (jede Version reicht aus) und erstellen eine Konsolenanwendung für C #. Für diejenigen, die sich nicht auskennen, ist die für die Arbeit ausreichende Community Edition absolut kostenlos.


Eigentlich ist der Text selbst wie folgt:


Quellcode Beispiel 1
using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT; m.PortB.Activate(); m.LOOP(m.TempL, (r, l) => m.GO(l), (r) => { m.PortB[0].Toggle();}); Console.WriteLine(AVRASM.Text(m)); } } } 

Damit alles funktioniert und Sie genau die Bibliothek benötigen, die ich vertrete.
Nach dem Kompilieren und Ausführen des Programms sehen wir in der Konsolenausgabe das folgende Ergebnis dieses Programms.


Kompilierungsergebnis von Beispiel 1
 #include “common.inc” RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 L0000: in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL xjmp L0000 .DSEG 

Wenn Sie das Ergebnis in eine Umgebung kopieren, die mit dem AVR-Assembler arbeiten und die Common.inc-Makrobibliothek verbinden kann (die Makrobibliothek ist auch eines der Bestandteile des vorgestellten Programmiersystems und arbeitet mit NanoRTOSLib zusammen ), kann dieses Programm auf einem Emulator oder einem realen Chip kompiliert und überprüft werden Stellen Sie sicher, dass alles funktioniert.


Betrachten Sie den Quellcode des Programms genauer. Zunächst ordnen wir der Variablen m den verwendeten Kristalltyp zu. Stellen Sie als nächstes den digitalen Ausgangsmodus für das Nullbit von Port B des Kristalls ein und aktivieren Sie den Port. Die nächste Zeile sieht etwas seltsam aus, aber ihre Bedeutung ist recht einfach. Darin sagen wir, dass wir eine Endlosschleife organisieren wollen, in deren Körper wir den Wert des Nullbits von Port B in das Gegenteil ändern. Die letzte Zeile des Programms visualisiert tatsächlich das Ergebnis von allem, was zuvor in Form von Assembler-Code geschrieben wurde. Alles ist sehr einfach und kompakt. Und das Ergebnis unterscheidet sich praktisch nicht von dem, was man in Assembler schreiben könnte. Der Ausgabecode kann nur zwei Fragen enthalten: die erste - warum den Stack initialisieren, wenn wir ihn immer noch nicht verwenden, und welche Art von xjmp ? Die Antwort auf die erste Frage und gleichzeitig eine Erklärung, warum Assembler anstelle eines vorgefertigten HEX ausgegeben wird, lautet wie folgt: Das Ergebnis in Form eines Assemblers ermöglicht es Ihnen, das Programm weiter zu analysieren und zu optimieren, sodass der Programmierer Codefragmente auswählen und ändern kann, die ihm nicht gefallen. Und die Initialisierung des Stapels wurde zumindest aus den Gründen belassen, dass Sie ohne Verwendung des Stapels nicht viele Programme erstellen können. Wenn es Ihnen jedoch nicht gefällt, können Sie es gerne aufräumen. Die Ausgabe an den Assembler ist für diesen Zweck vorgesehen. Xjmp ist ein Beispiel für die Verwendung von Makros, um die Lesbarkeit des Ausgabe-Assemblers zu verbessern . Insbesondere ist xjmp ein Ersatz für jmp und rjmp mit der richtigen Substitution in Abhängigkeit von der Länge des Übergangs.


Wenn Sie das Programm mit einem Chip füllen, sehen wir natürlich kein Blinken der Diode, obwohl sich der Pin-Zustand ändert. Es passiert einfach zu schnell, um es durch die Augen zu sehen. Daher betrachten wir das folgende Programm, in dem wir weiterhin mit einer Diode blinken, aber damit es sichtbar ist. Zum Beispiel ist eine Verzögerung von 0,5 Sekunden durchaus geeignet: nicht zu schnell und nicht zu langsam. Es wäre möglich, viele verschachtelte Schleifen mit NOPs zu erstellen, um eine Verzögerung zu bilden. Wir werden diesen Schritt jedoch überspringen, da der Beschreibung der Funktionen der Bibliothek nichts hinzugefügt wird, und sofort die Gelegenheit nutzen, die verfügbare Hardware zu verwenden. Wir ändern unsere Anwendung wie folgt.


Quellcode Beispiel 2
 using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT; m.PortB.Activate(); m.WDT.Clock = eWDTClock.WDT500ms; m.WDT.OnTimeout = () => m.PortB[0].Toggle(); m.WDT.Activate(); m.EnableInterrupt(); var loop = AVRASM.newLabel(); m.GO(loop); Console.WriteLine(AVRASM.Text(m)); } } } 

Offensichtlich ähnelt das Programm dem vorherigen, daher werden wir nur berücksichtigen, was sich geändert hat. In diesem Beispiel haben wir zunächst WDT (Watchdog Timer) verwendet. Für Arbeiten mit großen Verzögerungen, die keine extreme Genauigkeit erfordern, ist dies die beste Option. Um dies zu verwenden, müssen Sie lediglich die erforderliche Frequenz festlegen, indem Sie den Teiler über die Eigenschaft WDT.Clock festlegen und die Aktionen festlegen, die zum Zeitpunkt des Auslösens des Ereignisses ausgeführt werden müssen, indem Sie den Code über die Eigenschaft WDT.OnTimeout definieren. Da wir Interrupts benötigen, um zu funktionieren, müssen sie mit dem Befehl EnableInterrupt aktiviert werden. Der Hauptzyklus kann jedoch durch einen Dummy ersetzt werden. Darin haben wir noch nichts vor. Daher deklarieren und setzen wir ein Label und machen einen bedingungslosen Übergang, um einen leeren Zyklus zu organisieren. Wenn Sie LOOP mehr mögen - bitte. Das Ergebnis wird sich nicht ändern.
Schauen wir uns im Finale den resultierenden Code an.


Kompilierungsergebnis von Beispiel 2
 #include “common.inc” jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop jmp WDT ;Watchdog Timer Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 ldi TempL, (1<<WDCE) | (1<<WDE) sts WDTCSR,TempL ldi TempL, 0x42 sts WDTCSR,TempL sei L0000: xjmp L0000 WDT: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .DSEG 

Diejenigen, die mit diesem Prozessor vertraut sind, werden zweifellos eine Frage haben, wohin mehrere weitere Interruptvektoren gegangen sind. Hier haben wir die folgende Logik verwendet - wenn der Code nicht verwendet wird - wird der Code nicht benötigt. Daher endet die Interrupt-Tabelle mit dem zuletzt verwendeten Vektor.
Trotz der Tatsache, dass das Programm die Aufgabe perfekt bewältigt, mag es den wählerischsten möglicherweise nicht, dass die Anzahl der möglichen Verzögerungen begrenzt ist und der Schritt zu grob ist. Daher werden wir einen anderen Weg in Betracht ziehen und gleichzeitig sehen, wie die Arbeit mit Timern in der Bibliothek organisiert ist. In dem Mega328-Kristall, der als Probe genommen wird, gibt es bis zu 3 davon. 2 8-Bit und ein 16-Bit. Die Architekten haben sich sehr bemüht, so viele Funktionen wie möglich in diese Timer zu investieren, daher ist ihre Einstellung ziemlich umfangreich.


Zunächst berechnen wir, welcher Zähler für unsere Verzögerung von 0,5 Sekunden verwendet werden soll. Wenn wir die Kristalltaktfrequenz von 16 MHz nehmen, ist es selbst mit dem maximalen Peripherieteiler unmöglich, innerhalb des 8-Bit-Zählers zu bleiben. Daher werden wir den einzigen uns zur Verfügung stehenden 16-Bit-Timer1-Zähler nicht komplizieren und verwenden.


Infolgedessen nimmt das Programm die folgende Form an:


Quellcode Beispiel 3
 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) {var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; m.PortB.Activate(); m.Timer1.Mode = eWaveFormMode.CTC_OCRA; m.Timer1.Clock = eTimerClockSource.CLK256; m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256); m.Timer1.OnCompareA = () => bit1.Toggle(); m.Timer1.Activate(); m.EnableInterrupt(); m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { }); Console.WriteLine(AVRASM.Text(m)); } } } 

Da wir den Hauptgenerator als Taktquelle für unseren Timer verwenden, müssen Sie für die korrekte Berechnung der Verzögerung die Prozessortaktfrequenz, die Teilereinstellung und die periphere Taktsicherung angeben. Der Haupttext des Programms ist das Einstellen des Timers auf den gewünschten Modus. Hier wird absichtlich ein Deliberator von 256 und nicht ein Maximum für die Taktung gewählt, denn wenn Sie einen Teiler von 1024 für die erforderliche Taktfrequenz von 500 ms auswählen, die wir erhalten möchten, wird eine Bruchzahl erhalten.


Der resultierende Assembler-Code unseres Programms sieht folgendermaßen aus:


Kompilierungsergebnis von Beispiel 3
 #include “common.inc” jmp RESET reti ; IRQ0 Handler nop reti ;IRQ1 Handler nop reti ;PC_INT0 Handler nop reti ;PC_INT1 Handler nop reti ;PC_INT2 Handler nop reti ;Watchdog Timer Handler nop reti ;Timer2 Compare A Handler nop reti ;Timer2 Compare B Handler nop reti ;Timer2 Overflow Handler nop reti ;Timer1 Capture Handler nop jmp TIM1_COMPA ;Timer1 Compare A Handler RESET: ldi r16, high(RAMEND) out SPH,r16 ldi r16, low(RAMEND) out SPL,r16 outi DDRB,0x1 outiw OCR1A,0x7A12 outi TCCR1A,0 outi TCCR1B,0xC outi TCCR1C,0x0 outi TIMSK1,0x2 outi DDRB,0x1 sei L0000: xjmp L0000 TIM1_COMPA: push r17 push r16 in r16,SREG push r16 in TempL,PORTB ldi TempH,1 eor TempL,TempH out PORTB,TempL pop r16 out SREG,r16 pop r16 pop r17 reti .DSEG 

Es scheint schon nichts mehr zu kommentieren zu geben. Wir initialisieren die Geräte, konfigurieren Interrupts und genießen das Programm.


Das Durcharbeiten von Interrupts ist der einfachste Weg, um Programme für die Arbeit in Echtzeit zu erstellen. Leider ist es nicht immer möglich, zwischen parallelen Aufgaben zu wechseln, die nur Interrupt-Handler verwenden, um diese Aufgaben auszuführen. Die Einschränkung ist das Verbot der Behandlung verschachtelter Interrupts, was dazu führt, dass der Prozessor bis zum Beenden des Prozessors nicht auf alle anderen Interrupts reagiert, was zu einem Verlust von Ereignissen führen kann, wenn der Prozessor zu lange läuft.


Eine Lösung besteht darin, den Ereignisregistrierungscode und dessen Verarbeitung zu trennen. Der parallele Multithread-Verarbeitungskern aus der Bibliothek ist so organisiert, dass der Interrupt-Handler beim Eintreten eines Ereignisses nur das angegebene Ereignis registriert und bei Bedarf die minimal erforderlichen Datenerfassungsvorgänge ausführt und die gesamte Verarbeitung im Hauptstrom ausgeführt wird. Der Kernel prüft nacheinander, ob unverarbeitete Flags vorhanden sind, und fährt, falls gefunden, mit der entsprechenden Aufgabe fort.


Die Verwendung dieses Ansatzes vereinfacht den Entwurf von Systemen mit mehreren asynchronen Aufgaben, sodass Sie jede für sich betrachten können, ohne sich auf die Probleme beim Wechseln von Ressourcen zwischen Aufgaben konzentrieren zu müssen. Betrachten Sie als Beispiel die Implementierung von zwei unabhängigen Aufgaben, von denen jede ihre Ausgabe mit einer bestimmten Verzögerung umschaltet.


Quellcode Beispiel 4
 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 4); tasks.Heap = new StaticHeap(tasks, 64); var t1 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit1.Toggle(); tsk.Delay(32); tsk.TaskContinue(loop); },"Task1"); var t2 = tasks.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); bit2.Toggle(); tsk.Delay(48); tsk.TaskContinue(loop); }, "Task2"); var ca = tasks.ContinuousActivate(tasks.AlwaysOn, t1); tasks.ActivateNext(ca, tasks.AlwaysOn, t2); ca.Dispose(); m.EnableInterrupt(); tasks.Loop(); Console.WriteLine(AVRASM.Text(m)); } } } 

In dieser Aufgabe konfigurieren wir den Null- und den ersten Ausgang von Port B so, dass der Wert ausgegeben und von 0 auf 1 und umgekehrt geändert wird, mit einer Periode von 32 ms für Null und 48 ms für den ersten Ausgang. Für die Verwaltung jedes Ports ist eine separate Aufgabe verantwortlich. Das erste, was zu beachten ist, ist die Definition einer Instanz von Parallel. Diese Klasse ist der Kern des Aufgabenmanagements. In seinem Konstruktor bestimmen wir die maximal zulässige Anzahl gleichzeitig laufender Threads. Das Folgende ist eine Speicherzuordnung zum Speichern von Datenströmen. Die im Beispiel verwendete StaticHeap-Klasse weist jedem Stream eine feste Anzahl von Bytes zu. Um unser Problem zu lösen, ist dies akzeptabel, und die Verwendung einer festen Speicherzuordnung im Vergleich zu dynamisch vereinfacht die Algorithmen und macht den Code kompakter und schneller. Im weiteren Verlauf des Codes beschreiben wir eine Reihe von Aufgaben, die unter der Kontrolle des Kernels ausgeführt werden sollen. Sie sollten auf die asynchrone Funktion Verzögerung achten, mit der wir eine Verzögerung bilden. Seine Besonderheit ist, dass beim Aufrufen dieser Funktion die erforderliche Verzögerung in den Stream-Einstellungen festgelegt wird und die Steuerung auf den Kernel übertragen wird. Nach Ablauf des festgelegten Intervalls gibt der Kernel die Kontrolle über den Befehl nach dem Befehl Delay an die Task zurück. Ein weiteres Merkmal der Aufgabe ist das Programmieren des Verhaltens des Aufgabenflusses nach Abschluss im letzten Aufgabenbefehl. In unserem Fall sind beide Tasks so konfiguriert, dass sie in einer Endlosschleife ausgeführt werden, wobei die Steuerung am Ende jedes Zyklus zum Kernel zurückkehrt. Wenn erforderlich, kann durch Ausführen einer Aufgabe der Thread freigegeben oder zur Ausführung einer anderen Aufgabe weitergeleitet werden.


Der Grund für das Aufrufen der Aufgabe besteht darin, das dem Aufgabenablauf zugewiesene Signal zu aktivieren. Das Signal kann sowohl programmgesteuert als auch Hardware durch Interrupts von Peripheriegeräten aktiviert werden. Ein Taskaufruf setzt das Signal zurück. Eine Ausnahme bildet das vordefinierte AlwaysOn-Signal, das sich immer im aktiven Zustand befindet. Auf diese Weise können Aufgaben erstellt werden, die in jedem Abfragezyklus gesteuert werden. Die LOOP-Funktion ist erforderlich, um die Hauptausführungsschleife aufzurufen. Leider wird die Größe des Ausgabecodes bei Verwendung von Parallel bereits erheblich größer als in den vorherigen Beispielen (ca. 600 Befehle) und kann im Artikel nicht vollständig zitiert werden.


Und für süße - so etwas wie ein Live-Projekt, nämlich ein digitales Thermometer. Alles ist wie immer einfach. Ein digitaler Sensor mit einer SPI-Schnittstelle, einer 4-stelligen 7-Segment-Anzeige und mehreren Verarbeitungsthreads, um die Dinge kühl zu halten. In einem fahren wir einen Zyklus für die dynamische Anzeige, in einem anderen Ereignisse, die einen Temperaturlesezyklus auslösen, im dritten lesen wir die vom Sensor empfangenen Werte und konvertieren sie von einem Binärcode in BCD und dann in einen Segmentcode für einen dynamischen Anzeigepuffer.


Das Programm selbst ist wie folgt.


Quellcode Beispiel 5
 using NanoRTOSLib; using System; namespace ConsoleApp { class Program { static void Main(string[] args) { var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var led7s = new Led_7(); led7s.SegPort = m.PortC; led7s.Activate(); m.PortD.Direction(0xFF); m.PortD.Activate(); m.PortB[0].Mode = ePinMode.OUT; var tc77 = new TC77(); tc77.CS = m.PortB[0]; tc77.Port = m.SPI; m.Timer0.Clock = eTimerClockSource.CLK64; m.Timer0.Mode = eWaveFormMode.Normal; var reader = m.DREG("Temperature"); var bcdRes = m.DREG("digits"); var tmp = m.BYTE(); var bcd = new BCD(reader, bcdRes); m.subroutines.Add(bcd); var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 64); var tmrSig = os.AddSignal(m.Timer0.OVF_Handler); var spiSig = os.AddSignal(m.SPI.Handler, () => { m.SPI.Read(m.TempL); m.TempL.MStore(tmp); }); var actuator = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureAsync(); tsk.Delay(16); tsk.TaskContinue(loop); }, "actuator"); var treader = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); tc77.ReadTemperatureCallback(os, reader, tmp); reader >>= 7; m.CALL(bcd); tsk.TaskContinue(loop); }, "reader"); var display = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); m.PortD.Write(0xFE); m.TempQL.Load(bcdRes.Low); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFD); m.TempQL.Load(bcdRes.Low); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xFB); m.TempQL.Load(bcdRes.High); m.TempQL &= 0x0F; led7s.Show(m.TempQL); os.AWAIT(); m.PortD.Write(0xF7); m.TempQL.Load(bcdRes.High); m.TempQL >>= 4; led7s.Show(m.TempQL); os.AWAIT(); tsk.TaskContinue(loop); }, "display"); var ct = os.ContinuousActivate(os.AlwaysOn, actuator); os.ActivateNext(ct, spiSig, treader); os.ActivateNext(ct, tmrSig, display); tc77.Activate(); m.Timer0.Activate(); m.EnableInterrupt(); os.Loop(); Console.WriteLine(AVRASM.Text(m)); } } } 

Es ist klar, dass dies kein Arbeitsentwurf ist, sondern nur eine technologische Demo, die die Fähigkeiten der NanoRTOS-Bibliothek demonstrieren soll. In jedem Fall sind weniger als 100 Quellzeilen und weniger als 1 KB Ausgabecode ein gutes Ergebnis für eine funktionsfähige Anwendung.


In den folgenden Artikeln möchte ich im Falle eines Interesses an diesem Projekt näher auf die Prinzipien und Merkmale der Programmierung mit dieser Bibliothek eingehen.

Source: https://habr.com/ru/post/de462409/


All Articles