Assembler-Codegenerator-Bibliothek fĂŒr AVR-Mikrocontroller. Teil 4

← Teil 3. Indirekte Adressierung und Flusskontrolle
Teil 5. Entwerfen von Multithread-Anwendungen. →


Assembler Code Generator Library fĂŒr AVR-Mikrocontroller


Teil 4. Programmieren von PeripheriegerÀten und Behandeln von Interrupts


In diesem Teil des Beitrags werden wir uns, wie versprochen, mit einem der beliebtesten Aspekte der Mikrocontroller-Programmierung befassen - nĂ€mlich der Arbeit mit PeripheriegerĂ€ten. Es gibt zwei gĂ€ngige AnsĂ€tze fĂŒr die periphere Programmierung. Erstens weiß das Programmiersystem nichts ĂŒber PeripheriegerĂ€te und bietet nur Mittel fĂŒr den Zugriff auf GerĂ€testeuerungsports. Dieser Ansatz unterscheidet sich praktisch nicht von der Arbeit mit GerĂ€ten auf Assembler-Ebene und erfordert eine grĂŒndliche Untersuchung des Zwecks aller Ports, die mit dem Betrieb eines bestimmten PeripheriegerĂ€ts verbunden sind. Um die Arbeit der Programmierer zu erleichtern, gibt es spezielle Programme, deren Hilfe jedoch in der Regel mit der Generierung einer Sequenz der anfĂ€nglichen GerĂ€teinitialisierung endet. Der Vorteil dieses Ansatzes ist der vollstĂ€ndige Zugriff auf alle Peripheriefunktionen, und der Nachteil ist die KomplexitĂ€t der Programmierung und die große Menge an Programmcode.


Die zweite Arbeit mit PeripheriegerÀten erfolgt auf der Ebene der virtuellen GerÀte. Der Hauptvorteil dieses Ansatzes ist die Einfachheit der GerÀteverwaltung und die FÀhigkeit, mit ihnen zu arbeiten, ohne auf die jeweilige Hardwareimplementierung einzugehen. Der Nachteil dieses Ansatzes ist die EinschrÀnkung der FÀhigkeiten von PeripheriegerÀten durch den Zweck und die Funktionen des emulierten virtuellen GerÀts.


Die NanoRTOS-Bibliothek implementiert einen dritten Ansatz. Jedes PeripheriegerÀt wird von einer speziellen Klasse beschrieben, deren Zweck darin besteht, die Einrichtung und den Betrieb des GerÀts zu vereinfachen und gleichzeitig seine volle FunktionalitÀt beizubehalten. Es ist besser, die Funktionen dieses Ansatzes anhand von Beispielen zu demonstrieren. Beginnen wir also.


Beginnen wir mit dem einfachsten und gebrĂ€uchlichsten PeripheriegerĂ€t - dem digitalen Ein- / Ausgangsanschluss. Dieser Port kombiniert bis zu 8 KanĂ€le, von denen jeder unabhĂ€ngig fĂŒr die Ein- oder Ausgabe konfiguriert werden kann. Eine Klarstellung auf 8 bedeutet, dass die Controller-Architektur die Möglichkeit impliziert, alternative Funktionen fĂŒr einzelne Port-Bits zuzuweisen, was deren Verwendung als Wasser- / Ausgangsports ausschließt, wodurch die Anzahl der verfĂŒgbaren Bits verringert wird. Setup und weitere Arbeiten können sowohl auf der Ebene eines separaten Bits als auch auf der Ebene des gesamten Ports ausgefĂŒhrt werden (Schreiben und Lesen aller 8 Bits mit einem Befehl). Der in den Beispielen verwendete Mega328-Controller verfĂŒgt ĂŒber 3 Ports: B, C und D. Im Ausgangszustand sind aus Sicht der Bibliothek die Entladungen aller Ports neutral. Dies bedeutet, dass fĂŒr ihre Aktivierung der Verwendungsmodus angegeben werden muss. Bei dem Versuch, auf einen nicht aktivierten Port zuzugreifen, generiert das Programm einen Kompilierungsfehler. Dies geschieht, um mögliche Konflikte bei der Zuweisung alternativer Funktionen zu vermeiden. Um Ports in den Eingabe- / Ausgabemodus zu schalten, verwenden Sie die Mode- Befehle, um den Einzelbitmodus festzulegen, und Direction , um den Modus aller Portbits mit einem Befehl festzulegen. Aus programmtechnischer Sicht sind alle Ports gleich und ihr Verhalten wird von einer Klasse beschrieben.


var m = new Mega328(); m.PortB[0].Mode = ePinMode.OUT;// 0   B    m.PortC.Direction(0xFF);//       m.PortB.Activate(); //   m.PortC.Activate(); //  C //   m.PortB[0].Set(); //  0   B  1 m.PortB[0].Clear();//  0   B  0 m.PortB[0].Toggle();//  0   B   m.PortC.Write(0b11000000);// 6  7        var rr = m.REG(); //     rr.Load(0xC0); m.PortC.Write(rr);//      rr var t = AVRASM.Text(m); 

Das obige Beispiel zeigt, wie die Datenausgabe ĂŒber Ports organisiert werden kann. Die Arbeit mit Port B erfolgt hier auf der Ebene einer Kategorie und mit Port C auf der Ebene des gesamten Ports. Beachten Sie den Aktivierungsbefehl Activate () . Sein Zweck besteht darin, im Ausgabecode eine Folge von GerĂ€teinitialisierungsbefehlen gemĂ€ĂŸ den zuvor festgelegten Eigenschaften zu erzeugen. Daher verwendet der Befehl Activate () immer den Satz von eingestellten Parametern, der zum Zeitpunkt der AusfĂŒhrung aktuell ist. Betrachten Sie ein Beispiel fĂŒr das Lesen von Daten von einem Port.


  m.PortB.Activate(); //  B m.PortC.Activate(); //  C Bit dd = m.BIT(); //     Register rr = m.REG(); //     m.PortB[0].Read(dd); //  0   B m.PortC.Read(rr);//      rr var t = AVRASM.Text(m); 

In diesem Beispiel wurde ein neuer Bit- Datentyp angezeigt. Das nĂ€chste Analogon dieses Typs in Hochsprachen ist der Bool- Typ. Der Bit- Datentyp wird zum Speichern nur eines Informationsbits verwendet und ermöglicht die Verwendung seines Werts als Bedingung fĂŒr Verzweigungsoperationen. Um Speicherplatz zu sparen, werden Bitvariablen wĂ€hrend der Speicherung so zu Blöcken zusammengefasst, dass in einem ROZ-Register 8 Variablen vom Typ Bit gespeichert werden. ZusĂ€tzlich zu dem beschriebenen Typ enthĂ€lt die Bibliothek zwei weitere Bitdatentypen: Pin , der dieselbe FunktionalitĂ€t wie Bit hat, jedoch E / A- und Mbit- Register zum Speichern von Bitvariablen im RAM-Speicher verwendet. Mal sehen, wie Sie Bitvariablen verwenden können, um Zweige zu organisieren


 m.IF(m.PortB[0], () => AVRASM.Comment(",   = 1")); var b = m.BIT(); b.Set(); m.IF(b, () => AVRASM.Comment(",   b ")); 

Die erste Zeile ĂŒberprĂŒft den Status des Eingangsports, und wenn an Eingang 1 der Code des bedingten Blocks ausgefĂŒhrt wird. Die letzte Zeile enthĂ€lt ein Beispiel, in dem eine Variable vom Typ Bit als Verzweigungsbedingung verwendet wird.


Das nĂ€chste hĂ€ufig verwendete PeripheriegerĂ€t kann als Hardware-ZĂ€hler / Timer betrachtet werden. In AVR-Mikrocontrollern verfĂŒgt dieses GerĂ€t ĂŒber eine Vielzahl von Funktionen und kann je nach Einstellung verwendet werden, um eine Verzögerung zu erzeugen, einen MĂ€ander mit einer programmierbaren Frequenz zu erzeugen, die Frequenz eines externen Signals zu messen und auch als Multimode-PWM-Modulator. Im Gegensatz zu E / A-Ports verfĂŒgt jeder der Mega328-Timer ĂŒber einzigartige Funktionen. Daher wird jeder Timer durch eine separate Klasse beschrieben.


Betrachten wir sie genauer. Als Signalquelle jedes Timers können sowohl ein externes Signal als auch der interne Takt des Prozessors verwendet werden. Mit den Hardwareeinstellungen des Mikrocontrollers können Sie die Verwendung der vollen Frequenz fĂŒr PeripheriegerĂ€te konfigurieren oder den einzelnen Splitter fĂŒr alle PeripheriegerĂ€te um 8 einschalten. Da der Mikrocontroller den Betrieb in einem weiten Frequenzbereich ermöglicht, muss fĂŒr die korrekte Berechnung der Timer-Teilerwerte fĂŒr die erforderliche Verzögerung wĂ€hrend der internen Taktung die Prozessorfrequenz angegeben werden und Prescaler-Modus. Daher hat der Abschnitt mit den Timer-Einstellungen die folgende Form


 var m = new Mega328(); m.FCLK = 16000000; //   m.CKDIV8 = false; //     //    Timer1 m.Timer1.Clock = eTimerClockSource.CLK256; //   m.Timer1.OCRA = (ushort)((0.5 * m.FCLK) / 256); //    A m.Timer1.Mode = eWaveFormMode.CTC_OCRA; //    m.Timer1.Activate(); //    Timer1 

Zum Einstellen des Timers mĂŒssen Sie natĂŒrlich die Dokumentation des Herstellers lesen, um den richtigen Modus auszuwĂ€hlen und den Zweck der verschiedenen Einstellungen zu verstehen. Die Verwendung der Bibliothek erleichtert jedoch das Arbeiten mit dem GerĂ€t und behĂ€lt die Möglichkeit, alle GerĂ€temodi zu verwenden.


Jetzt schlage ich eine kleine Ablenkung von der Beschreibung der Verwendung bestimmter GerĂ€te vor und diskutiere, bevor ich fortfahre, das Problem des asynchronen Betriebs. Der Hauptvorteil von PeripheriegerĂ€ten besteht darin, dass sie bestimmte Funktionen ausfĂŒhren können, ohne CPU-Ressourcen zu verwenden. Bei der Organisation der Interaktion zwischen dem Programm und dem GerĂ€t kann KomplexitĂ€t auftreten, da die Ereignisse, die wĂ€hrend des Betriebs des PeripheriegerĂ€ts auftreten, in Bezug auf den CodeausfĂŒhrungsfluss in der CPU asynchron sind. Synchrone Interaktionsmethoden, bei denen das Programm Zyklen zum Warten auf den gewĂŒnschten GerĂ€testatus enthĂ€lt, machen fast alle Vorteile der Peripherie als unabhĂ€ngige GerĂ€te zunichte. Effizienter und bevorzugter ist der Interrupt-Modus. In diesem Modus fĂŒhrt der Prozessor kontinuierlich den Code des Hauptthreads aus und schaltet den AusfĂŒhrungsthread bei Auftreten des Ereignisses auf seinen Handler um. Am Ende der Verarbeitung kehrt die Steuerung zum Hauptthread zurĂŒck. Die VorzĂŒge dieses Ansatzes liegen auf der Hand, seine Verwendung kann jedoch durch die KomplexitĂ€t des Aufbaus erschwert werden. Um im Assembler einen Interrupt zu verwenden, mĂŒssen Sie:


  • Stellen Sie die richtige Adresse in der Interrupt-Tabelle ein.
  • Konfigurieren Sie das GerĂ€t selbst so, dass es mit Unterbrechungen arbeitet.
  • Beschreiben der Interrupt-Behandlungsfunktion
  • Sorgen Sie dafĂŒr, dass alle verwendeten Register und Flags erhalten bleiben, damit der Interrupt den Fortschritt des Hauptthreads nicht beeintrĂ€chtigt
  • Aktivieren Sie globale Interrupts.

Um die Programmierung der Arbeit durch Interrupts zu vereinfachen, enthalten die Beschreibungsklassen fĂŒr BibliotheksperipheriegerĂ€te die Eigenschaften eines Ereignishandlers. Um die Arbeit mit einem PeripheriegerĂ€t ĂŒber Interrupts zu organisieren, mĂŒssen Sie nur den Code fĂŒr die Verarbeitung des erforderlichen Ereignisses beschreiben, und die Bibliothek nimmt alle anderen Einstellungen selbst vor. Kehren wir zur Timer-Einstellung zurĂŒck und ergĂ€nzen sie mit der Definition des Codes, der ausgefĂŒhrt werden soll, wenn die Schwellenwerte fĂŒr den Vergleich der Timer-VergleichskanĂ€le erreicht sind. Angenommen, wir möchten, dass beim Auslösen von Schwellenwerten der VergleichskanĂ€le bestimmte Bits der E / A-Ports beim Überlaufen zurĂŒckgesetzt werden. Mit anderen Worten, wir möchten unter Verwendung eines Zeitgebers die Funktion des Erzeugens eines PWM-Signals an ausgewĂ€hlten beliebigen Ports mit einem Arbeitszyklus implementieren, der durch die OCRA- Werte fĂŒr den ersten und OCRB fĂŒr den zweiten Kanal bestimmt wird. Mal sehen, wie der Code in diesem Fall aussehen wird.


 var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var bit1 = m.PortB[0]; bit1.Mode = ePinMode.OUT; var bit2 = m.PortB[1]; bit2.Mode = ePinMode.OUT; m.PortB.Activate(); //  0  1   B   //     m.Timer0.Clock = eTimerClockSource.CLK; m.Timer0.OCRA = 50; m.Timer0.OCRB = 170; m.Timer0.Mode = eWaveFormMode.PWMPC_TOP8; //   m.Timer0.OnCompareA = () => bit1.Set(); m.Timer0.OnCompareB = () =>bit2.Set(); m.Timer0.OnOverflow = () => m.PortB.Write(0); m.Timer0.Activate(); m.EnableInterrupt(); //  //   m.LOOP(m.TempH, (r, l) => m.GO(l), (r) => { }); 

Der Teil bezĂŒglich der Einstellung der Timer-Modi wurde bereits frĂŒher betrachtet. Gehen wir also gleich zu den Interrupt-Handlern ĂŒber. In diesem Beispiel werden drei Handler verwendet, um zwei PWM-KanĂ€le mit einem Timer zu implementieren. Der Code der Handler ist ziemlich offensichtlich, aber es kann sich die Frage stellen, wie das zuvor erwĂ€hnte Speichern des Zustands implementiert wird, so dass der Interrupt-Aufruf die Logik des Hauptthreads nicht beeinflusst. Die Lösung, in der alle Register und Flags gespeichert sind, sieht eindeutig redundant aus. Daher analysiert die Bibliothek den Ressourcenverbrauch im Interrupt und speichert nur das erforderliche Minimum. Die leere Hauptschleife bestĂ€tigt die Idee, dass die Aufgabe der kontinuierlichen Erzeugung mehrerer PWM-Signale ohne die Teilnahme des Hauptprogramms funktioniert.


Es ist zu beachten, dass die Bibliothek einen einheitlichen Ansatz fĂŒr die Arbeit mit Interrupts fĂŒr alle Beschreibungsklassen fĂŒr PeripheriegerĂ€te implementiert. Dies vereinfacht die Programmierung und reduziert Fehler.


Wir werden die Arbeit mit Interrupts weiter untersuchen und eine Situation betrachten, in der das Klicken auf die an den Eingangsports angebrachten SchaltflĂ€chen bestimmte Aktionen des Programms hervorrufen sollte. In dem Prozessor, den wir betrachten, gibt es zwei Möglichkeiten, Interrupts zu erzeugen, wenn sich der Status der Eingangsports Ă€ndert. Am weitesten fortgeschritten ist die Verwendung des externen Interrupt-Modus. In diesem Fall können wir fĂŒr jede der Schlussfolgerungen separate Interrupts generieren und die Reaktion nur auf ein bestimmtes Ereignis (Front, Rezession, Level) konfigurieren. Leider gibt es nur zwei davon in unserem Kristall. Eine andere Methode ermöglicht es Ihnen, mittels Interrupts eines der Bits des Eingangsports zu steuern. Die Verarbeitung ist jedoch komplizierter, da das Ereignis auf Portebene auftritt, wenn sich das Eingangssignal eines der konfigurierten Bits Ă€ndert, und die weitere KlĂ€rung der Ursache des Interrupts auf Algorithmenebene durch Software erfolgen sollte .


Zur Veranschaulichung werden wir versuchen, das Problem der Steuerung des Status der Portausgabe mit zwei Tasten zu lösen. Einer von ihnen sollte den Wert des von uns angegebenen Ports auf 1 setzen und der andere zurĂŒcksetzen. Da es nur zwei SchaltflĂ€chen gibt, werden wir die Möglichkeit nutzen, externe Interrupts zu verwenden.


  var m = new Mega328(); m.PortD[0].Mode = ePinMode.OUT; m.PortD.Write(0x0C); // pull-up   m.INT0.Mode = eExtIntMode.Falling; //  INT0  . m.INT0.OnChange = () => m.PortD[0].Set(); //      1 m.INT1.Mode = eExtIntMode.Falling; //  INT1  . m.INT1.OnChange = () => m.PortD[0].Clear(); //     //  m.INT0.Activate(); m.INT1.Activate(); m.PortD.Activate(); m.EnableInterrupt(); //   //  m.LOOP(m.TempL, (r, l) => m.GO(l), (r, l) => { }); 

Durch die Verwendung externer Interrupts konnten wir unser Problem so einfach und klar wie möglich lösen.


Die programmgesteuerte Verwaltung externer Ports ist nicht die einzig mögliche Möglichkeit. Insbesondere haben Timer eine Einstellung, mit der sie den Ausgang des Mikrocontrollers direkt steuern können. Dazu mĂŒssen Sie in der Timer-Einstellung den Ausgangssteuerungsmodus angeben


 m.Timer0.CompareModeA = eCompareMatchMode.Set; 

Nach dem Aktivieren des Timers erhÀlt das 6. Bit von Port D eine alternative Funktion und wird von einem Timer gesteuert. Somit können wir ein PWM-Signal am Prozessorausgang nur auf Hardwareebene erzeugen, indem wir nur Software verwenden, um die Signalparameter einzustellen. Wenn wir gleichzeitig versuchen, mithilfe der Bibliothekstools den Besetzt-Port als Eingabe- / Ausgabe-Port zu verwenden, wird auf Kompilierungsebene ein Fehler angezeigt.


Das letzte GerĂ€t, das wir in diesem Teil des Artikels betrachten werden, ist die serielle USART-Schnittstelle. Die FunktionalitĂ€t dieses GerĂ€ts ist sehr breit, aber bisher werden wir nur einen der hĂ€ufigsten AnwendungsfĂ€lle fĂŒr dieses GerĂ€t ansprechen.


Der beliebteste Anwendungsfall fĂŒr diesen Port ist das Anschließen eines seriellen Terminals an Eingabe- / Ausgabetextinformationen. Der Teil des Codes bezĂŒglich der Porteinstellungen in diesem Fall kann wie folgt aussehen


 m.FCLK = 16000000; //   m.CKDIV8 = false; //     m.Usart.Mode = eUartMode.UART; //    UART m.Usart.Baudrate = 9600; //   9600  m.Usart.FrameFormat = eUartFrame.U8N1; //   8N1 

Die angegebenen Einstellungen stimmen mit den Standardeinstellungen des USART in der Bibliothek ĂŒberein, daher können sie im Programmtext teilweise oder vollstĂ€ndig ĂŒbersprungen werden.


Stellen Sie sich ein kleines Beispiel vor, in dem wir statischen Text an das Terminal ausgeben. Um den Code nicht aufzublasen, beschrÀnken wir uns auf die Ausgabe an das Terminal der klassischen "Hallo Welt!" zu Beginn des Programms.


  var m = new Mega328(); var ptr = m.ROMPTR(); //      m.CKDIV8 = false; m.FCLK = 16000000; //      m.Usart.Mode = eUartMode.UART; m.Usart.Baudrate = 9600; m.Usart.FrameFormat = eUartFrame.U8N1; //         m.Usart.OnTransmitComplete = () => { ptr.MLoadInc(m.TempL); m.IF(m.TempL!=0,()=>m.Usart.Transmit(m.TempL)); }; m.Usart.Activate(); m.EnableInterrupt(); //   var str = Const.String("Hello world!"); //   ptr.Load(str); //     ptr.MloadInc(m.TempL); //    m.Usart.Transmit(m.TempL); //   . m.LOOP(m.TempL, (r, l) => m.GO(l), (r,l) => { }); 

In diesem Programm wird aus dem Neuen die Deklaration der Konstantenzeichenfolge str . Die Bibliothek legt alle konstanten Variablen im Programmspeicher ab. Um mit ihnen arbeiten zu können, mĂŒssen Sie den ROMPtr- Zeiger verwenden. Die Datenausgabe an das Terminal beginnt mit der Ausgabe des ersten Zeichens der Zeichenfolgenfolge. Danach geht die Steuerung sofort in die Hauptschleife, ohne auf das Ende der Ausgabe zu warten. Der Abschluss des ByteĂŒbertragungsprozesses verursacht einen Interrupt, in dessen Handler das nĂ€chste Zeichen der Zeile gelesen wird. Wenn das Zeichen nicht gleich 0 ist (die Bibliothek verwendet das nullterminierte Format zum Speichern von Zeichenfolgen), wird dieses Zeichen an die serielle Schnittstelle gesendet. Wenn wir das Ende der Zeile erreichen, wird das Zeichen nicht an den Port gesendet und der Sendezyklus endet.


Der Nachteil dieses Ansatzes ist der feste Interrupt-Verarbeitungsalgorithmus. Die serielle Schnittstelle darf nur zur Ausgabe statischer Zeichenfolgen verwendet werden. Ein weiterer Nachteil dieser Implementierung ist das Fehlen eines Mechanismus zur Überwachung der Hafenbelegung. Wenn Sie versuchen, mehrere Leitungen nacheinander zu senden, kann es vorkommen, dass die Übertragung vorheriger Leitungen unterbrochen oder die Leitungen gemischt werden.


Effektivere Methoden zur Lösung dieses und anderer Probleme sowie zur Arbeit mit anderen PeripheriegerÀten werden wir im nÀchsten Teil des Beitrags sehen. Darin werden wir uns die Programmierung mit der speziellen Parallel Task Management-Klasse genauer ansehen.

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


All Articles