Wir binden den Lua-Interpreter in das Projekt für den Mikrocontroller (stm32) ein.



In relativ großen Anwendungen ist ein wesentlicher Teil des Projekts die Geschäftslogik. Es ist praktisch, diesen Teil des Programms auf einem Computer zu debuggen und ihn dann in das Projekt für den Mikrocontroller einzubetten, wobei zu erwarten ist, dass dieser Teil ohne Debugging genau so ausgeführt wird, wie er beabsichtigt war (Idealfall).

Da die meisten Programme für Mikrocontroller in C / C ++ geschrieben sind, verwenden sie für diese Zwecke normalerweise abstrakte Klassen, die Schnittstellen zu Entitäten auf niedriger Ebene bereitstellen (wenn ein Projekt nur mit C geschrieben wird, werden häufig Funktionszeigerstrukturen verwendet). Dieser Ansatz bietet den erforderlichen Abstraktionsgrad gegenüber dem Eisen, ist jedoch mit der Notwendigkeit einer ständigen Neukompilierung des Projekts mit anschließender Programmierung des nichtflüchtigen Speichers des Mikrocontrollers mit einer großen binären Firmware- Datei behaftet.

Es gibt jedoch noch einen anderen Weg: Verwenden Sie eine Skriptsprache, mit der Sie Geschäftslogik in Echtzeit auf dem Gerät selbst debuggen oder Arbeitsskripte direkt aus dem externen Speicher laden können, ohne diesen Code in die Mikrocontroller-Firmware aufzunehmen.

Ich habe Lua als Skriptsprache gewählt.

Warum Lua?


Es gibt mehrere Skriptsprachen, die Sie in ein Projekt für einen Mikrocontroller einbetten können. Ein paar einfache BASIC-ähnliche, PyMite, Pawn ... Jedes hat seine Vor- und Nachteile, von denen eine Diskussion nicht in der Liste der in diesem Artikel behandelten Themen enthalten ist.

Kurz darüber, was speziell lua gut ist - finden Sie im Artikel "Lua in 60 Minuten". Dieser Artikel hat mich sehr inspiriert und für eine detailliertere Untersuchung des Themas habe ich den offiziellen Leitfaden des Autors der Sprache Robert Jeruzalimsky " Programming in Lua " (verfügbar in der offiziellen russischen Übersetzung) gelesen.

Ich möchte auch das eLua-Projekt erwähnen. In meinem Fall habe ich bereits eine vorgefertigte Low-Level-Software-Schicht für die Interaktion mit den Peripheriegeräten des Mikrocontrollers und anderen erforderlichen Peripheriegeräten auf der Geräteplatine. Daher habe ich dieses Projekt nicht in Betracht gezogen (da bekannt ist, dass es genau die Schichten für die Verbindung des Lua-Kerns mit den Peripheriegeräten des Mikrocontrollers bereitstellt).

Über das Projekt, in das Lua eingebettet wird


Traditionell wird mein Sandbox-Projekt als Qualität des Feldes für Experimente verwendet (Link zum Commit mit der bereits integrierten Lua mit allen unten beschriebenen notwendigen Verbesserungen).

Das Projekt basiert auf dem Mikrocontroller stm32f405rgt6 mit 1 MB nichtflüchtigem Material und 192 KB RAM (die älteren 2 Blöcke mit einer Gesamtkapazität von 128 KB werden derzeit verwendet).

Das Projekt verfügt über ein FreeRTOS-Echtzeitbetriebssystem zur Unterstützung der Hardware-Peripherie-Infrastruktur. Der gesamte Speicher für Aufgaben, Semaphoren und andere FreeRTOS-Objekte wird in der Verknüpfungsphase (im .bss-Bereich des RAM) statisch zugewiesen. Alle FreeRTOS-Entitäten (Semaphoren, Warteschlangen, Aufgabenstapel usw.) sind Teile globaler Objekte in den privaten Bereichen ihrer Klassen. Der FreeRTOS-Heap wird jedoch weiterhin zugewiesen, um die malloc- freien Calloc- Funktionen (erforderlich für Funktionen wie printf ) zu unterstützen, die für die Arbeit mit ihm neu definiert wurden. Es gibt eine erhöhte API für die Arbeit mit MicroSD (FatFS) -Karten sowie für das Debuggen von UART (115200, 8N1).

Über die Logik, Lua als Teil eines Projekts zu verwenden


Zum Debuggen der Geschäftslogik wird angenommen, dass Befehle über UART gesendet, (als separates Objekt) in fertige Zeilen gepackt (mit dem Zeichen "\ n" + 0-Terminator endend) und an die Lua-Maschine gesendet werden. Bei erfolgloser Ausführung erfolgt die Ausgabe über printf (da es zuvor am Projekt beteiligt war). Wenn die Logik debuggt ist, kann die endgültige Geschäftslogikdatei aus der Datei von der microSD-Karte heruntergeladen werden (nicht im Material dieses Artikels enthalten). Zum Debuggen von Lua wird der Computer außerdem in einem separaten FreeRTOS-Thread ausgeführt (in Zukunft wird jedem debuggten Geschäftslogikskript, in dem er mit seiner Umgebung ausgeführt wird, ein separater Thread zugewiesen).

Einbeziehung des Lua-Submoduls in das Projekt


Der offizielle Spiegel des Projekts auf Github wird als Quelle für die Lua-Bibliothek verwendet (da mein Projekt auch dort veröffentlicht ist. Sie können die Quellen direkt von der offiziellen Website aus verwenden ). Da das Projekt über ein etabliertes System zum Zusammenstellen von Submodulen als Teil des Projekts verfügt, einzelne CMakeLists für jedes Submodul, habe ich ein separates Submodul erstellt, in das ich diesen Fork und CMakeLists aufgenommen habe, um einen einzelnen Build-Stil beizubehalten.

CMakeLists erstellt die Quellen des Lua-Repositorys als statische Bibliothek mit den folgenden Submodul-Kompilierungsflags (aus der Submodul-Konfigurationsdatei im Hauptprojekt entnommen):

SET(C_COMPILER_FLAGS "-std=gnu99;-fshort-enums;-fno-exceptions;-Wno-type-limits;-ffunction-sections;-fdata-sections;") SET(MODULE_LUA_COMP_FLAGS "-O0;-g3;${C_COMPILER_FLAGS}" 

Und Spezifikationsflags des verwendeten Prozessors (in den Stamm-CMakeLists festgelegt ):

 SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) 

Es ist wichtig zu beachten, dass die Root-CMakeLists eine Definition angeben müssen, die es erlaubt, keine doppelten Werte zu verwenden (da der Mikrocontroller keine Hardware-Unterstützung für double bietet. Nur float):

 add_definitions(-DLUA_32BITS) 

Nun, es bleibt nur, den Linker über die Notwendigkeit zu informieren, diese Bibliothek zusammenzustellen und das Ergebnis in das Layout des endgültigen Projekts aufzunehmen:

CMakeLists-Plot zum Verknüpfen eines Projekts mit der Lua-Bibliothek
 add_subdirectory(${CMAKE_SOURCE_DIR}/bsp/submodules/module_lua) ... target_link_libraries(${PROJECT_NAME}.elf PUBLIC # -Wl,--start-group       #      . #  Lua    ,      #  . "-Wl,--start-group" ..._... MODULE_LUA ..._... "-Wl,--end-group") 

Funktionen zum Arbeiten mit Speicher definieren


Da sich Lua selbst nicht mit dem Speicher befasst, geht diese Verantwortung auf den Benutzer über. Bei Verwendung der gebündelten Lauxlib- Bibliothek und der daraus resultierenden Funktion luaL_newstate ist die Funktion l_alloc jedoch als Speichersystem gebunden . Es ist wie folgt definiert:

 static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) { (void)ud; (void)osize; /* not used */ if (nsize == 0) { free(ptr); return NULL; } else return realloc(ptr, nsize); } 

Wie am Anfang des Artikels erwähnt, hat das Projekt bereits malloc- und freie Funktionen überschrieben, es gibt jedoch keine Realloc- Funktion. Wir müssen es reparieren.

Im Standardmechanismus für die Arbeit mit dem FreeRTOS-Heap verfügt die im Projekt verwendete Datei heap_4.c nicht über eine Funktion zum Ändern der Größe eines zuvor zugewiesenen Speicherblocks. In dieser Hinsicht ist es notwendig, seine Umsetzung auf der Grundlage von malloc und frei zu machen .

Da es in Zukunft möglich ist, das Speicherzuordnungsschema (unter Verwendung einer anderen Datei heap_x.c) zu ändern, wurde beschlossen, nicht die Innenräume des aktuellen Schemas (heap_4.c) zu verwenden, sondern ein Add-In auf höherer Ebene zu erstellen. Obwohl weniger effektiv.

Es ist wichtig zu beachten, dass die Realloc- Methode nicht nur den alten Block (falls vorhanden) löscht und einen neuen erstellt, sondern auch Daten vom alten zum neuen Block verschiebt. Wenn der alte Block mehr Daten als der neue hatte, wird der neue bis zum Limit mit den alten gefüllt und die verbleibenden Daten werden verworfen.

Wenn diese Tatsache nicht berücksichtigt wird, kann Ihr Computer ein solches Skript dreimal ab der Zeile " a = 3 \ n " ausführen, wonach es in einen schweren Fehler gerät. Das Problem kann gelöst werden, nachdem das Restbild der Register im Hard-Error-Handler untersucht wurde, anhand dessen festgestellt werden kann, dass der Absturz aufgetreten ist, nachdem versucht wurde, die Tabelle im Darm des Interpreter-Codes und seiner Bibliotheken zu erweitern. Wenn Sie ein Skript wie " print 'test' " aufrufen, ändert sich das Verhalten abhängig davon, wie die Firmware-Datei zusammengestellt wird (mit anderen Worten, das Verhalten ist undefiniert).

Um Daten vom alten Block in den neuen zu kopieren, müssen wir die Größe des alten Blocks ermitteln. FreeRTOS heap_4.c (wie andere Dateien, die Heap-Behandlungsmethoden bereitstellen) bietet hierfür keine API. Deshalb musst du deine beenden. Als Basis habe ich die vPortFree- Funktion genommen und ihre Funktionalität in die folgende Form geschnitten:

VPortGetSizeBlock-Funktionscode
 int vPortGetSizeBlock (void *pv) { uint8_t *puc = (uint8_t *)pv; BlockLink_t *pxLink; if (pv != NULL) { puc -= xHeapStructSize; pxLink = (BlockLink_t *)puc; configASSERT((pxLink->xBlockSize & xBlockAllocatedBit) != 0); configASSERT(pxLink->pxNextFreeBlock == NULL); return pxLink->xBlockSize & ~xBlockAllocatedBit; } return 0; } 

Jetzt ist es klein, schreibe Realloc basierend auf malloc , free und vPortGetSizeBlock :

Realloc-Implementierungscode basierend auf malloc, free und vPortGetSizeBlock
 void *realloc (void *ptr, size_t new_size) { if (ptr == nullptr) { return malloc(new_size); } void* p = malloc(new_size); if (p == nullptr) { return p; } size_t old_size = vPortGetSizeBlock(ptr); size_t cpy_len = (new_size < old_size)?new_size:old_size; memcpy(p, ptr, cpy_len); free(ptr); return p; } 

Unterstützung für die Arbeit mit stdout hinzufügen


Wie aus der offiziellen Beschreibung hervorgeht, kann der Lua-Interpreter selbst nicht mit I / O arbeiten. Zu diesem Zweck ist eine der Standardbibliotheken angeschlossen. Für die Ausgabe wird der stdout- Stream verwendet. Die Funktion luaopen_io aus der Standardbibliothek ist für die Verbindung zum Stream verantwortlich. Um die Arbeit mit stdout zu unterstützen (im Gegensatz zu printf ), müssen Sie die Funktion fwrite überschreiben. Ich habe es basierend auf den im vorherigen Artikel beschriebenen Funktionen neu definiert.

Fwrite-Funktion
 size_t fwrite(const void *buf, size_t size, size_t count, FILE *stream) { stream = stream; size_t len = size * count; const char *s = reinterpret_cast<const char*>(buf); for (size_t i = 0; i < len; i++) { if (_write_char((s[i])) != 0) { return -1; } } return len; } 

Ohne ihre Definition wird die Druckfunktion in lua erfolgreich ausgeführt, es erfolgt jedoch keine Ausgabe. Darüber hinaus gibt es keine Fehler auf dem Lua-Stapel der Maschine (da die Funktion formal erfolgreich ausgeführt wurde).

Zusätzlich zu dieser Funktion benötigen wir die Funktion fflush (damit der interaktive Modus funktioniert, der später erläutert wird ). Da diese Funktion nicht überschrieben werden kann, müssen Sie sie etwas anders benennen. Die Funktion ist eine abgespeckte Version der fwrite- Funktion und soll das, was sich jetzt im Puffer befindet, mit der anschließenden Reinigung (ohne zusätzliche Wagenübertragung) senden.

Mc_fflush-Funktion
 int mc_fflush () { uint32_t len = buf_p; buf_p = 0; if (uart_1.tx(tx_buf, len, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } return 0; } 


Abrufen von Zeichenfolgen von einer seriellen Schnittstelle


Um Strings für eine Lua-Maschine zu bekommen, habe ich beschlossen, eine einfache Uart-Terminal-Klasse zu schreiben, die:

  • empfängt Daten auf einer seriellen Schnittstelle Byte für Byte (in Interrupt);
  • fügt das empfangene Byte der Warteschlange hinzu, von der der Stream es empfängt;
  • in einem Strom von Bytes, wenn dies kein Zeilenvorschub ist, der in der Form zurückgesendet wird, in der er angekommen ist;
  • Wenn ein Zeilenvorschub angekommen ist (' \ r '), werden 2 Bytes Terminal-Wagenrücklauf gesendet (" \ n \ r ").
  • Nach dem Senden der Antwort wird der Handler des angekommenen Bytes (Linienlayoutobjekt) aufgerufen.
  • steuert das Drücken der Löschzeichentaste (um das Löschen von Dienstzeichen aus dem Terminalfenster zu vermeiden);

Links zu Quellen:

  • Die UART-Klassenschnittstelle ist hier ;
  • Die UART-Basisklasse ist hier und hier ;
  • Klasse uart_terminal hier und hier ;
  • Erstellen eines Klassenobjekts als Teil des Projekts hier .

Außerdem stelle ich fest, dass Sie der UART-Unterbrechung im zulässigen Bereich für die Arbeit mit FreeRTOS-Funktionen aus dem Interrupt eine Priorität zuweisen müssen, damit dieses Objekt ordnungsgemäß funktioniert. Andernfalls können interessante, schwer zu debuggende Fehler auftreten. Im aktuellen Beispiel sind die folgenden Optionen für Interrupts in der Datei FreeRTOSConfig.h festgelegt .

Einstellungen in FreeRTOSConfig.h
 #define configPRIO_BITS 4 #define configKERNEL_INTERRUPT_PRIORITY 0XF0 //   FreeRTOS API   //   0x8 - 0xF. #define configMAX_SYSCALL_INTERRUPT_PRIORITY 0x80 

Im Projekt selbst legt ein Objekt der Klasse nvic die Priorität des Interrupts 0x9 fest, der im gültigen Bereich enthalten ist (die Klasse nvic wird hier und hier beschrieben ).

Saitenbildung für eine Lua-Maschine


Vom uart_terminal-Objekt empfangene Bytes werden an eine Instanz einer einfachen Klasse serial_cli übertragen, die eine minimale Schnittstelle zum Bearbeiten der Zeichenfolge und zum direkten Übertragen an den Thread bietet, in dem die Lua-Maschine ausgeführt wird (durch Aufrufen der Rückruffunktion). Beim Akzeptieren des Zeichens '\ r' wird eine Rückruffunktion aufgerufen. Diese Funktion sollte eine Zeile in sich selbst kopieren und die Steuerung „freigeben“ (da der Empfang neuer Bytes während eines Aufrufs blockiert wird. Dies ist kein Problem bei korrekt priorisierten Streams und einer ausreichend niedrigen UART-Geschwindigkeit).

Links zu Quellen:

  • serial_cli Beschreibungsdateien hier und hier ;
  • Erstellen eines Klassenobjekts als Teil des Projekts hier .

Es ist wichtig zu beachten, dass diese Klasse eine Zeichenfolge mit mehr als 255 Zeichen für ungültig hält und sie verwirft. Dies ist beabsichtigt, da Sie mit dem Lua-Interpreter zeilenweise Konstrukte eingeben können, die auf das Ende des Blocks warten.

Übergabe einer Zeichenfolge an den Lua-Interpreter und dessen Ausführung


Der Lua-Interpreter selbst weiß nicht, wie er Blockcode zeilenweise akzeptiert und dann den gesamten Block selbst ausführt. Wenn Sie jedoch Lua auf einem Computer installieren und den Interpreter im interaktiven Modus ausführen, können Sie feststellen, dass die Ausführung zeilenweise mit der entsprechenden Notation während der Eingabe erfolgt und der Block noch nicht vollständig ist. Da der interaktive Modus im Standardpaket enthalten ist, können wir dessen Code sehen. Es befindet sich in der Datei lua.c. Wir interessieren uns für die doREPL- Funktion und alles, was sie verwendet. Um kein Fahrrad zu entwickeln und die Funktionen des interaktiven Modus im Projekt abzurufen , habe ich einen Port dieses Codes in einer separaten Klasse erstellt, die ich lua_repl mit dem Namen der ursprünglichen Funktion benannt habe. Diese verwendet printf, um Informationen an die Konsole auszugeben, und verfügt über eine öffentliche Methode add_lua_string , um eine vom Klassenobjekt empfangene Zeile hinzuzufügen serial_cli wie oben beschrieben.

Referenzen:


Die Klasse wird nach dem Singleton-Myers-Muster erstellt, da nicht mehrere interaktive Modi innerhalb desselben Geräts angegeben werden müssen. Ein Objekt der Klasse lua_repl empfängt hier Daten von einem Objekt der Klasse serial_cli.

Da das Projekt bereits über ein einheitliches System zum Initialisieren und Verwalten globaler Objekte verfügt, wird der Zeiger auf das Objekt der Klasse lua_repl hier an das Objekt der globalen Klasse player :: base übergeben . In der Startmethode eines Objekts der Klasse player :: base ( hier deklariert. Es wird auch von main aufgerufen) wird die init- Methode des Objekts der Klasse lua_repl mit der Priorität der FreeRTOS 3-Task aufgerufen (im Projekt können Sie die Priorität der Task von 1 bis 4 zuweisen. Dabei 1 Ist die niedrigste Priorität und 4 ist die höchste). Nach erfolgreicher Initialisierung startet die globale Klasse den FreeRTOS-Scheduler und der interaktive Modus startet seine Arbeit.

Portierungsprobleme


Unten finden Sie eine Liste der Probleme, die beim Lua-Port des Computers aufgetreten sind.

Es werden 2-3 einzeilige Skripte der Variablenzuweisung ausgeführt, dann fällt alles in einen schweren Fehler


Das Problem war mit der Realloc-Methode. Es ist nicht nur erforderlich, den Block erneut auszuwählen, sondern auch den Inhalt des alten zu kopieren (wie oben beschrieben).

Beim Versuch, einen Wert zu drucken, gerät der Interpreter in einen schweren Fehler


Es war bereits schwieriger, das Problem zu erkennen, aber am Ende konnte ich herausfinden, dass snprintf zum Drucken verwendet wurde. Da lua Werte doppelt (oder in unserem Fall float) speichert, ist printf (und seine Ableitungen) mit Gleitkommaunterstützung erforderlich (ich habe hier über die Feinheiten von printf geschrieben).

Anforderungen an den nichtflüchtigen (Flash-) Speicher


Hier sind einige Messungen, die ich durchgeführt habe, um zu beurteilen, wie viel nichtflüchtiger (Flash-) Speicher zugewiesen werden muss, um die Lua-Maschine in das Projekt zu integrieren. Die Kompilierung wurde unter Verwendung von gcc-arm-none-eabi-8-2018-q4-major durchgeführt. Die Version von Lua 5.4 wurde verwendet. In den Messungen bedeutet der Ausdruck „ohne Lua“, dass der Interpreter und die Interaktionsmethoden mit ihm und seinen Bibliotheken sowie ein Objekt der Klasse lua_repl nicht in das Projekt einbezogen werden. Alle Entitäten auf niedriger Ebene (einschließlich Überschreibungen für die Funktionen printf und fwrite ) verbleiben im Projekt. Die FreeRTOS-Heap-Größe beträgt 1024 * 25 Byte. Der Rest wird von globalen Projekteinheiten besetzt.

Die zusammenfassende Ergebnistabelle lautet wie folgt (alle Größen in Bytes):
Optionen erstellenOhne LuaNur KernLua mit BasisbibliothekLua mit Bibliotheksbasis, Coroutine, Tabelle, StringluaL_openlibs
-O0 -g3103028220924236124262652308372
-O1 -g374940144732156916174452213068
-Os -g071172134228145756161428198400

RAM-Anforderungen


Da der RAM-Verbrauch vollständig von der Aufgabe abhängt, werde ich unmittelbar nach dem Einschalten des Computers mit einem anderen Satz von Bibliotheken eine Übersichtstabelle des verbrauchten Speichers geben (diese wird vom Befehl print (collectgarbage ("count") * 1024 ) angezeigt.
ZusammensetzungRAM verwendet
Lua mit Basisbibliothek4809
Lua mit Bibliotheksbasis, Coroutine, Tabelle, String6407
luaL_openlibs12769

Bei Verwendung aller Bibliotheken wird die Größe des erforderlichen RAM im Vergleich zu den vorherigen Sätzen erheblich erhöht. Die Verwendung in einem erheblichen Teil der Anwendungen ist jedoch nicht erforderlich.

Zusätzlich werden dem Taskstack, in dem die Lua-Maschine ausgeführt wird, 4 kb zugewiesen.

Weitere Verwendung


Um die Maschine im Projekt vollständig nutzen zu können, müssen Sie alle Schnittstellen, die der Geschäftslogikcode für die Hardware- oder Serviceobjekte des Projekts benötigt, näher beschreiben. Dies ist jedoch das Thema eines separaten Artikels.

Zusammenfassung


In diesem Artikel wurde beschrieben, wie Sie eine Lua-Maschine mit einem Projekt für einen Mikrocontroller verbinden und einen vollwertigen interaktiven Interpreter starten, mit dem Sie direkt über die Befehlszeile des Terminals mit Geschäftslogik experimentieren können. Zusätzlich wurden die Anforderungen an die Hardware des Mikrocontrollers für verschiedene Konfigurationen der Lua-Maschine berücksichtigt.

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


All Articles