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

← Teil 4. Peripheriegeräte programmieren und Interrupts behandeln


Assembler Code Generator Library für AVR-Mikrocontroller


Teil 5. Entwerfen von Multithread-Anwendungen


In den vorherigen Teilen des Artikels haben wir die Grundlagen der Programmierung mithilfe der Bibliothek erläutert. Im vorherigen Teil haben wir uns mit der Implementierung von Interrupts und den Einschränkungen vertraut gemacht, die bei der Arbeit mit ihnen auftreten können. In diesem Teil des Beitrags werden wir uns mit einer der möglichen Optionen zum Programmieren paralleler Prozesse unter Verwendung der Parallel- Klasse befassen . Die Verwendung dieser Klasse ermöglicht es, die Erstellung von Anwendungen zu vereinfachen, in denen Daten in mehreren unabhängigen Programmströmen verarbeitet werden sollen.


Alle Multitasking-Systeme für Single-Core-Systeme sind einander ähnlich. Multithreading wird durch die Arbeit des Dispatchers implementiert, der jedem Thread ein Zeitfenster zuweist. Wenn es beendet ist, übernimmt es die Kontrolle und gibt dem nächsten Thread die Kontrolle. Der Unterschied zwischen den verschiedenen Implementierungen liegt nur in den Details, daher werden wir uns hauptsächlich mit den spezifischen Merkmalen dieser Implementierung befassen.


Die Einheit der Prozessausführung im Thread ist die Aufgabe. Es kann eine unbegrenzte Anzahl von Aufgaben im System vorhanden sein, aber zu einem bestimmten Zeitpunkt kann nur eine bestimmte Anzahl von Aufgaben aktiviert werden, die durch die Anzahl der Workflows im Dispatcher begrenzt ist. In dieser Implementierung wird die Anzahl der Workflows im Manager-Konstruktor angegeben und kann anschließend nicht mehr geändert werden. Dabei können Threads Aufgaben erledigen oder frei bleiben. Im Gegensatz zu anderen Lösungen wechselt Parallel Manager keine Aufgaben. Damit die Aufgabe die Kontrolle an den Dispatcher zurückgeben kann, müssen entsprechende Befehle in den Code eingefügt werden. Die Verantwortung für die Dauer des Zeitfensters in der Aufgabe liegt somit beim Programmierer, der an bestimmten Stellen im Code Interrupt-Befehle einfügen muss, wenn die Aufgabe zu lange dauert, und das Verhalten des Threads nach Abschluss der Aufgabe bestimmen muss. Der Vorteil dieses Ansatzes besteht darin, dass der Programmierer die Schaltpunkte zwischen Aufgaben steuert, wodurch Sie den Speicher- / Wiederherstellungscode beim Wechseln von Aufgaben erheblich optimieren und die meisten Probleme im Zusammenhang mit dem threadsicheren Datenzugriff beseitigen können.


Um die Ausführung laufender Aufgaben zu steuern, wird eine spezielle Signalklasse verwendet. Das Signal ist eine Bitvariable, deren Einstellung als Freigabesignal zum Starten einer Aufgabe in einem Stream verwendet wird. Signalwerte können entweder manuell oder durch ein diesem Signal zugeordnetes Ereignis eingestellt werden.


Das Signal wird zurückgesetzt, wenn die Aufgabe vom Dispatcher aktiviert wird oder programmgesteuert ausgeführt werden kann.


Aufgaben im System können folgende Zustände haben:


Deaktiviert - Ausgangszustand für alle Aufgaben. Die Aufgabe nimmt den Ablauf nicht auf und die Ausführungskontrolle wird nicht übertragen. Die Rückkehr in diesen Zustand für aktivierte Aufgaben erfolgt nach Abschluss des Befehls.


Aktiviert - Der Status, in dem sich die Aufgabe nach der Aktivierung befindet. Der Aktivierungsprozess ordnet eine Aufgabe einem Ausführungsthread und einem Aktivierungssignal zu. Der Manager fragt die Threads ab und startet die Aufgabe, wenn das Aufgabensignal aktiviert ist.


Blockiert - Wenn eine Aufgabe aktiviert ist, kann ihr bereits ein Signal als Signal zugewiesen werden, das bereits zur Steuerung eines anderen Threads verwendet wird. In diesem Fall wird die aktivierte Aufgabe in den gesperrten Zustand versetzt, um die Mehrdeutigkeit des Programmverhaltens zu vermeiden. In diesem Zustand belegt die Task den Thread, kann jedoch keine Kontrolle erhalten, selbst wenn ihr Signal aktiviert ist. Nach Abschluss von Aufgaben oder beim Ändern des Aktivierungssignals überprüft und ändert der Dispatcher den Status von Aufgaben in den Threads. Wenn die Threads Aufgaben blockiert haben, für die das Signal mit dem freigegebenen übereinstimmt, wird die erste gefundene aktiviert. Bei Bedarf kann der Programmierer Aufgaben unabhängig von der erforderlichen Logik des Programms sperren und entsperren.


Warten - Der Status, in dem sich die Aufgabe befindet, nachdem der Befehl Verzögerung ausgeführt wurde. In diesem Zustand erhält die Aufgabe erst nach Ablauf des erforderlichen Intervalls die Kontrolle. In der Parallel- Klasse werden 16-ms-WDT-Interrupts verwendet, um die Verzögerung zu steuern, wodurch Zeitgeber für Systemanforderungen nicht belegt werden können. Wenn Sie in kleinen Intervallen mehr Stabilität oder Auflösung benötigen , können Sie anstelle der Verzögerung die Aktivierung durch Timersignale verwenden. Es ist zu beachten, dass die Verzögerungsgenauigkeit immer noch gering ist und im Bereich von "Dispatcher-Antwortzeit" - "maximale Zeitschlitzdauer im System + Dispatcher-Antwortzeit" schwankt. Für Aufgaben mit genauen Zeitbereichen sollten Sie den Hybridmodus verwenden, in dem der Timer, der in der Parallel- Klasse nicht verwendet wird, unabhängig vom Aufgabenablauf arbeitet und Intervalle im reinen Interrupt-Modus verarbeitet.


Jede in einem Thread ausgeführte Aufgabe ist ein isolierter Prozess. Dies erfordert die Definition von zwei Datentypen: lokale Daten eines Streams, die nur im Rahmen dieses Streams sichtbar sein und geändert werden sollten, und globale Daten für den Austausch zwischen Flows und den Zugriff auf gemeinsam genutzte Ressourcen. Im Rahmen dieser Implementierung werden globale Daten durch zuvor berücksichtigte Befehle auf Geräteebene erstellt. Um lokale Aufgabenvariablen zu erstellen, müssen sie mit Methoden aus der Aufgabenklasse erstellt werden. Das Verhalten der lokalen Taskvariablen ist wie folgt: Wenn die Task unterbrochen wird, bevor die Steuerung an den Dispatcher übertragen wird, werden alle lokalen Registervariablen im Speicher des Streams gespeichert. Wenn die Steuerung zurückgegeben wird, werden die lokalen Registervariablen wiederhergestellt, bevor der nächste Befehl ausgeführt wird.
Eine Klasse mit der IHeap- Schnittstelle, die der Heap- Eigenschaft der Parallel- Klasse zugeordnet ist, ist für das Speichern lokaler Daten des Streams verantwortlich. Die einfachste Implementierung dieser Klasse ist StaticHeap , die die statische Zuordnung derselben Speicherblöcke für jeden Thread implementiert. Wenn die Aufgaben je nach Bedarf an lokaler Datenmenge sehr unterschiedlich sind, können Sie DynamicHeap verwenden , mit dem Sie die Größe des lokalen Speichers für jede Aufgabe einzeln bestimmen können. Offensichtlich ist der Aufwand für die Arbeit mit dem Stream-Speicher in diesem Fall erheblich höher.


Schauen wir uns nun die Klassensyntax am Beispiel von zwei Streams genauer an, von denen jeder unabhängig einen separaten Portausgang schaltet.


var m = new Mega328 { FCLK = 16000000, CKDIV8 = false }; m.PortB.Direction(0x07); var bit1 = m.PortB[1]; var bit2 = m.PortB[2]; m.PortB.Activate(); var tasks = new Parallel(m, 2); tasks.Heap = new StaticHeap(tasks, 16); 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(); 

Die obersten Zeilen des Programms sind Ihnen bereits bekannt. In ihnen bestimmen wir den Reglertyp und weisen das erste und zweite Bit von Port B als Ausgang zu. Als nächstes folgt die Initialisierung einer Variablen der Parallel- Klasse, wobei wir im zweiten Parameter die maximale Anzahl von Ausführungsthreads bestimmen. In der nächsten Zeile weisen wir Speicher zu, um lokale Variablenflüsse aufzunehmen. Wir haben gleiche Aufgaben, also verwenden wir StaticHeap . Der nächste Codeblock ist die Aufgabendefinition. Darin definieren wir zwei nahezu identische Aufgaben. Der einzige Unterschied ist der Steuerport und die Verzögerung. Um mit lokalen Aufgabenobjekten zu arbeiten, wird ein Zeiger auf die lokale Aufgabe tsk an den Aufgabencodeblock übergeben. Der Aufgabentext selbst ist sehr einfach:


  • Ein lokales Label wird erstellt, um einen unendlichen Schaltzyklus zu organisieren
  • Portstatus ist umgekehrt
  • Die Steuerung wird an den Dispatcher zurückgegeben, und die Task wird für die angegebene Anzahl von Millisekunden in den Wartestatus versetzt
  • Der Rückgabezeiger wird auf den Startblock des Blocks gesetzt und die Steuerung an den Dispatcher zurückgegeben.
    In einem konkreten Beispiel könnte der letzte Befehl natürlich durch einen normalen Befehl ersetzt werden, der an den Anfang des Blocks geht und im Beispiel nur zum Zwecke der Demonstration angegeben wird. Falls gewünscht, kann das Beispiel leicht erweitert werden, um eine große Anzahl von Schlussfolgerungen zu steuern, indem Aufgaben kopiert und die Anzahl der Threads erhöht werden.

Eine vollständige Liste der Task-Abbruchbefehle zum Übertragen der Steuerung an den Dispatcher lautet wie folgt
AWAIT (Signal) - Der Stream speichert alle Variablen im Speicher des Streams und überträgt die Kontrolle an den Dispatcher. Bei der nächsten Aktivierung des Streams werden die Variablen wiederhergestellt und die Ausführung fortgesetzt, beginnend mit der nächsten Anweisung nach AWAIT . Der Befehl dient dazu, die Aufgabe in Zeitschlitze zu unterteilen und die Zustandsmaschine gemäß dem Schema Signal → Verarbeitung 1 → Signal → Verarbeitung 2 usw. zu implementieren.


Der Befehl AWAIT kann ein Signal als optionalen Parameter haben. Wenn der Parameter leer ist, wird das Aktivierungssignal gespeichert. Wenn es im Parameter angegeben ist, werden alle nachfolgenden Taskaufrufe ausgeführt, wenn das angegebene Signal aktiviert wird und die Kommunikation mit dem vorherigen Signal unterbrochen wird.


TaskContinue (Label, Signal) - Der Befehl beendet den Stream und gibt dem Dispatcher die Kontrolle, ohne Variablen zu speichern. Bei der nächsten Aktivierung des Streams wird die Kontrolle auf das Etikettenetikett übertragen . Mit dem optionalen Parameter Signal können Sie das Stream-Aktivierungssignal für den nächsten Anruf überschreiben. Wenn nicht angegeben, bleibt das Signal gleich. Ein Befehl ohne Angabe eines Signals kann verwendet werden, um Zyklen innerhalb einer einzelnen Aufgabe zu organisieren, wobei jeder Zyklus in einem separaten Zeitfenster ausgeführt wird. Es kann auch verwendet werden, um dem aktuellen Thread nach Abschluss des vorherigen Threads eine neue Aufgabe zuzuweisen. Der Vorteil dieses Ansatzes gegenüber dem Zyklus Freigeben eines Threads → Hervorheben eines Streams ist ein effizienteres Programm. Durch die Verwendung von TaskContinue muss der Manager nicht mehr nach einem freien Thread im Pool suchen und garantiert Fehler beim Zuweisen von Threads ohne freie Threads.


TaskEnd () - Löscht den Stream nach Abschluss der Aufgabe. Die Aufgabe endet, der Thread wird freigegeben und kann verwendet werden, um mit dem Befehl Aktivieren eine neue Aufgabe zuzuweisen.


Verzögerung (ms) - Der Stream speichert wie bei der Verwendung von AWAIT alle Variablen im Speicher des Streams und überträgt die Steuerung an den Dispatcher. In diesem Fall wird der Verzögerungswert in Millisekunden im Stream-Header aufgezeichnet. In der Dispatcher-Schleife wird bei einem Wert ungleich Null im Verzögerungsfeld der Fluss nicht aktiviert. Das Ändern der Werte im Verzögerungsfeld für alle Flüsse erfolgt durch Unterbrechen des WDT-Timers alle 16 ms. Wenn der Nullwert erreicht ist, wird das Ausführungsverbot aufgehoben und das Stream-Aktivierungssignal gesetzt. Im Header wird nur ein Einzelbyte-Wert für die Verzögerung gespeichert, was einen relativ engen Bereich möglicher Verzögerungen ergibt. Um längere Verzögerungen zu implementieren, erstellt Delay () eine interne Schleife unter Verwendung lokaler Stream-Variablen.
Die Aktivierung der Befehle im Beispiel erfolgt mit den Befehlen ContinuousActivate und ActivateNext . Dies ist eine spezielle Art der anfänglichen Aufgabenaktivierung beim Start. In der ersten Aktivierungsphase ist garantiert kein einziger ausgelasteter Thread vorhanden, sodass für den Aktivierungsprozess keine vorläufige Suche nach einem freien Thread für eine Aufgabe erforderlich ist und Sie Aufgaben nacheinander aktivieren können. ContinuousActivate aktiviert die Aufgabe im Null-Thread und gibt einen Zeiger auf den Header des nächsten Threads zurück. Die ActivateNext- Funktion verwendet diesen Zeiger, um die folgenden Aufgaben in sequentiellen Threads zu aktivieren.


Im Beispiel wird als Aktivierungssignal das AlwaysOn- Signal verwendet. Dies ist eines der Systemsignale. Sein Zweck bedeutet, dass die Aufgabe immer ausgeführt wird, da dies das einzige Signal ist, das immer aktiviert ist und nicht durch Verwendung zurückgesetzt wird.


Das Beispiel endet mit einem Loop- Aufruf. Diese Funktion startet den Dispatcher-Zyklus, daher sollte dieser Befehl der letzte im Code sein.


Stellen Sie sich ein weiteres Beispiel vor, bei dem die Verwendung der Bibliothek die Struktur des Codes erheblich vereinfachen kann. Es sei ein bedingtes Steuergerät, das ein analoges Signal registriert und es in Form eines HEX-Codes an das Terminal sendet.


  var m = new Mega328(); m.FCLK = 16000000; m.CKDIV8 = false; var cData = m.DREG(); var outDigit = m.ARRAY(4); var chex = Const.String("0123456789ABCDEF"); m.ADC.Clock = eADCPrescaler.S64; m.ADC.ADCReserved = 0x01; m.ADC.Source = eASource.ADC0; m.Usart.Baudrate = 9600; m.Usart.FrameFormat = eUartFrame.U8N1; var os = new Parallel(m, 4); os.Heap = new StaticHeap(os, 8); var ADS = os.AddSignal(m.ADC.Handler, () => m.ADC.Data(cData)); var trm = os.AddSignal(m.Usart.TXC_Handler); var starts = os.AddLocker(); os.PrepareSignals(); var t0 = os.CreateTask((tsk) => { m.LOOP(m.TempL, (r, l) => m.GO(l), (r, l) => { m.ADC.ConvertAsync(); tsk.Delay(500); }); }, "activate"); var t1 = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); var mref = m.ROMPTR(); mref.Load(chex); m.TempL.Load(cData.High); m.TempL >>= 4; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[0]); mref.Load(chex); m.TempL.Load(cData.High); m.TempL &= 0x0F; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[1]); mref.Load(chex); m.TempL.Load(cData.Low); m.TempL >>= 4; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[2]); mref.Load(chex); m.TempL.Load(cData.Low); m.TempL &= 0x0F; mref += m.TempL; mref.MLoad(m.TempL); m.TempL.MStore(outDigit[3]); starts.Set(); tsk.TaskContinue(loop); }); var t2 = os.CreateTask((tsk) => { var loop = AVRASM.NewLabel(); trm.Clear(); m.TempL.Load('0'); m.Usart.Transmit(m.TempL); tsk.AWAIT(trm); m.TempL.Load('x'); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[0]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[1]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[2]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.MLoad(outDigit[3]); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.Load(13); m.Usart.Transmit(m.TempL); tsk.AWAIT(); m.TempL.Load(10); m.Usart.Transmit(m.TempL); tsk.TaskContinue(loop, starts); }); var p = os.ContinuousActivate(os.AlwaysOn, t0); os.ActivateNext(p, ADS, t1); os.ActivateNext(p, starts, t2); m.ADC.Activate(); m.Usart.Activate(); m.EnableInterrupt(); os.Loop(); 

Das soll nicht heißen, dass wir hier viele neue Dinge gesehen haben, aber Sie können etwas Interessantes in diesem Code sehen.


In diesem Beispiel wird zuerst der ADC (Analog-Digital-Wandler) erwähnt. Dieses Peripheriegerät dient zur Umwandlung der Spannung des Eingangssignals in einen digitalen Code. Der Konvertierungszyklus wird von der ConvertAsync- Funktion gestartet , die den Prozess nur startet, ohne auf das Ergebnis zu warten. Wenn die Konvertierung abgeschlossen ist, generiert der ADC einen Interrupt, der das adcSig- Signal aktiviert. Achten Sie auf die Definition des adcSig- Signals. Neben dem Interrupt-Zeiger enthält er auch einen Codeblock zum Speichern von Werten aus dem ADC-Datenregister. Der gesamte Code, der vorzugsweise unmittelbar nach einem Interrupt ausgeführt wird (z. B. Lesen von Daten aus Geräteregistern), sollte sich an dieser Stelle befinden.
Die Konvertierungsaufgabe besteht darin, einen binären Spannungscode in eine vierstellige HEX-Darstellung für unser bedingtes Terminal zu konvertieren. Hier können wir die Verwendung von Funktionen zur Beschreibung sich wiederholender Fragmente zur Reduzierung der Größe des Quellcodes und die Verwendung einer konstanten Zeichenfolge für die Datenkonvertierung beachten.


Das Übertragungsproblem ist unter dem Gesichtspunkt der Implementierung einer formatierten Ausgabe eines Strings interessant, in dem die Ausgabe von statischen und dynamischen Daten kombiniert wird. Der Mechanismus selbst kann nicht als ideal angesehen werden, sondern ist eine Demonstration der Möglichkeiten zur Verwaltung von Handlern. Hier können Sie auch auf die Neudefinition des Aktivierungssignals während der Ausführung achten, wodurch das Aktivierungssignal von ConvS auf TxS und umgekehrt geändert wird .


Zum besseren Verständnis beschreiben wir den Algorithmus des Programms in Worten.


Im Ausgangszustand haben wir drei Aufgaben gestartet. Zwei von ihnen haben inaktive Signale, da das Signal für die Konvertierungsaufgabe (adcSig) am Ende des Lesezyklus des analogen Signals aktiviert wird und ConvS für die Übertragungsaufgabe durch einen Code aktiviert wird, der noch nicht ausgeführt wurde. Daher ist die erste Aufgabe, die nach dem Start gestartet wird, immer die Messung. Der Code für diese Task startet den ADC-Konvertierungszyklus. Danach geht die 500-ms-Task in den Wartezyklus. Am Ende des Konvertierungszyklus wird das Flag adcSig aktiviert , wodurch die Konvertierungsaufgabe ausgelöst wird. In dieser Aufgabe wird ein Zyklus zum Konvertieren der empfangenen Daten in eine Zeichenfolge implementiert. Vor dem Beenden der Aufgabe aktivieren wir das ConvS- Flag, um zu verdeutlichen, dass neue Daten an das Terminal gesendet werden müssen. Der Befehl exit setzt den Rückgabepunkt auf den Beginn der Aufgabe zurück und gibt dem Dispatcher die Kontrolle. Der ConvS- Flag- Satz ermöglicht die Übertragung der Steuerung auf die Übertragungsaufgabe . Nach dem Senden des ersten Bytes der Sequenz ändert sich das Aktivierungssignal in der Task zu TxS . Infolgedessen wird nach Abschluss der Übertragung des Bytes die Übertragungsaufgabe erneut aufgerufen, was zur Übertragung des nächsten Bytes führt. Nachdem das letzte Byte der Sequenz übertragen wurde, gibt die Task das ConvS- Aktivierungssignal zurück und setzt den Rückgabepunkt auf den Beginn der Task zurück. Der Zyklus ist abgeschlossen. Der nächste Zyklus beginnt, wenn die Messaufgabe das Warten abgeschlossen und den nächsten Messzyklus aktiviert hat.


In fast allen Multitasking-Systemen gibt es das Konzept von Warteschlangen für die Interaktion zwischen Threads. Wir haben bereits herausgefunden, dass die Verwendung globaler Variablen zum Datenaustausch zwischen Aufgaben durchaus möglich ist, da das Wechseln zwischen Aufgaben in diesem System ein vollständig kontrollierter Prozess ist. Es gibt jedoch eine Reihe von Aufgaben, bei denen die Verwendung von Warteschlangen gerechtfertigt ist. Daher werden wir dieses Thema nicht außer Acht lassen und sehen, wie es in der Bibliothek implementiert ist.


Um eine Warteschlange in einem Programm zu implementieren, verwenden Sie am besten die RingBuff- Klasse. Die Klasse implementiert, wie der Name schon sagt, einen Ringpuffer mit Schreib- und Abrufbefehlen. Das Lesen und Schreiben von Daten erfolgt über die Befehle Lesen und Schreiben . Lese- und Schreibbefehle haben keine Parameter. Der Puffer verwendet die im Konstruktor angegebene Registervariable als Datenquelle / Empfänger. Der Zugriff auf diese Variable erfolgt über den Parameter IOReg class. Der Status des Puffers wird durch die beiden Flags Ovf und Empty bestimmt , mit deren Hilfe der Überlaufstatus beim Schreiben und der Überlauf beim Lesen ermittelt werden können. Darüber hinaus kann die Klasse den Code ermitteln, der bei Überlauf- / Überlaufereignissen ausgeführt wird. RingBuff hat keine Abhängigkeiten von der Parallel- Klasse und kann separat verwendet werden. Die Einschränkung bei der Arbeit mit der Klasse ist die zulässige Kapazität, die aus Gründen der Codeoptimierung ein Vielfaches der Zweierpotenz (8.16.32 usw.) sein sollte.


Ein Beispiel für die Arbeit mit der Klasse ist unten angegeben.


  var m = new Mega328(); var io = m.REG(); //     16     io. var bf = new RingBuff(m, 16, io) { //    OnOverflow = () => { AVRASM.Comment("   "); }, OnEmpty = () => { AVRASM.Comment("   "); } }; var cntr = m.REG(); cntr.Load(16); //       m.LOOP(cntr, (r, l) => { cntr--; m.IFNOTEMPTY(l); },(r)=> { //         //m.IF(bf.Ovf,()=>{AVRASM.Comment("”)}; bf.IOReg.Load(cntr); //      bf.Write(); //    }); //     m.LOOP(cntr, (r, l) => { m.GO(l); }, (r) => { //         //m.IF(bf.Ovf,()=>{AVRASM.Comment(" ”)}; bf.Read(); //       IOReg //    }); 

Dieser Teil schließt die Übersicht über die Bibliotheksfunktionen ab. Leider gab es eine Reihe von Aspekten bezüglich der Fähigkeiten der Bibliothek, die nicht einmal erwähnt wurden. In Zukunft sind bei Interesse an dem Projekt Artikel geplant, die sich der Lösung spezifischer Probleme mithilfe der Bibliothek und einer detaillierteren Beschreibung komplexer Probleme widmen, für die eine separate Veröffentlichung erforderlich ist.

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


All Articles