
Dans des applications assez importantes, une partie importante du projet est la logique métier. Il est pratique de déboguer cette partie du programme sur l'ordinateur, puis de l'intégrer dans le projet du microcontrôleur, en attendant que cette partie soit exécutée exactement comme prévu sans aucun débogage (cas idéal).
Étant donné que la plupart des programmes pour microcontrôleurs sont écrits en C / C ++, à ces fins, ils utilisent généralement des classes abstraites qui fournissent des interfaces aux entités de bas niveau (si un projet est écrit uniquement en C, les structures de pointeurs de fonction sont souvent utilisées). Cette approche fournit le niveau d'abstraction requis sur le fer, mais elle est lourde de nécessité de recompilation constante du projet avec la programmation ultérieure de la mémoire non volatile du microcontrôleur avec un
gros fichier de
micrologiciel binaire.
Cependant, il existe un autre moyen: utiliser un langage de script qui vous permet de déboguer la logique métier en temps réel sur l'appareil lui-même ou de charger des scripts de travail directement à partir de la mémoire externe, sans inclure ce code dans le micrologiciel du microcontrôleur.
J'ai choisi Lua comme langage de script.
Pourquoi Lua?
Il existe plusieurs langages de script que vous pouvez intégrer dans un projet de microcontrôleur. Quelques simples BASIC-like, PyMite, Pawn ... Chacun a ses avantages et ses inconvénients, dont une discussion n'est pas incluse dans la liste des problèmes abordés dans cet article.
Brièvement sur ce qui est bon spécifiquement lua - peut être trouvé dans l'article
"Lua en 60 minutes .
" Cet article m'a beaucoup inspiré et, pour une étude plus approfondie de la question, j'ai lu le guide officiel de l'auteur de la langue Robert Jeruzalimsky "
Programming in Lua " (disponible dans la traduction officielle russe).
Je voudrais également mentionner le projet eLua. Dans mon cas, j'ai déjà une couche logicielle de bas niveau prête à l'emploi pour l'interaction avec les périphériques du microcontrôleur et les autres périphériques requis situés sur la carte de l'appareil. Par conséquent, je n'ai pas considéré ce projet (car il est reconnu de fournir les couches mêmes pour connecter le noyau Lua avec les périphériques du microcontrôleur).
À propos du projet dans lequel Lua sera intégré
Par tradition , mon
projet sandbox sera utilisé comme la qualité du terrain d'expérimentation (lien vers le commit avec la lua déjà intégrée avec toutes les améliorations nécessaires décrites ci-dessous).
Le projet est basé sur le microcontrôleur stm32f405rgt6 avec 1 Mo non volatile et 192 Ko de RAM (les 2 anciens blocs d'une capacité totale de 128 Ko sont actuellement utilisés).
Le projet dispose d'un système d'exploitation en temps réel FreeRTOS pour prendre en charge l'infrastructure matérielle périphérique. Toute la mémoire pour les tâches, les sémaphores et autres objets FreeRTOS est allouée statiquement à l'étape de liaison (située dans la zone .bss de la RAM). Toutes les entités FreeRTOS (sémaphores, files d'attente, piles de tâches, etc.) font partie d'objets globaux dans les zones privées de leurs classes. Cependant, le tas FreeRTOS est toujours alloué pour prendre en charge les fonctions
malloc ,
free ,
calloc (requises pour des fonctions telles que
printf ) qui sont redéfinies pour fonctionner avec. Il existe une API améliorée pour travailler avec les cartes MicroSD (FatFS), ainsi que pour le débogage de l'UART (115200, 8N1).
Ă€ propos de la logique d'utilisation de Lua dans le cadre d'un projet
Dans le but de déboguer la logique métier, il est supposé que les commandes seront envoyées par UART, empaquetées (comme un objet séparé) dans des lignes finies (se terminant par le caractère "\ n" + 0-terminateur) et envoyées à la machine lua. En cas d'échec de l'exécution, sortie par printf (puisqu'il était
auparavant impliqué dans le projet). Lorsque la logique est déboguée, il sera possible de télécharger le fichier logique métier final à partir du fichier de la carte microSD (non inclus dans le matériel de cet article). De plus, dans le but de déboguer Lua, la machine sera exécutée à l'intérieur d'un thread FreeRTOS distinct (à l'avenir, un thread séparé sera alloué pour chaque script de logique métier débogué dans lequel il sera exécuté avec son environnement).
Inclusion du sous-module lua dans le projet
Le
miroir officiel du projet sur github sera utilisé comme source de la bibliothèque lua (puisque mon projet y est également affiché. Vous pouvez utiliser les sources directement depuis le
site officiel ). Étant donné que le projet dispose d'un système établi d'assemblage de sous-modules dans le cadre du projet, avec des CMakeLists individuelles pour chaque sous-module, j'ai créé un
sous-module distinct dans lequel j'ai inclus cette fourche et CMakeLists pour conserver un style d'assemblage unique.
CMakeLists construit les sources du référentiel lua comme une bibliothèque statique avec les drapeaux de compilation de sous-module suivants (extraits du
fichier de configuration du sous-module dans le projet principal):
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}"
Et drapeaux de spécification du processeur utilisé (défini dans la
racine CMakeLists ):
SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;)
Il est important de noter la nécessité pour la racine CMakeLists de spécifier une définition qui permet de ne pas utiliser de valeurs doubles (puisque le microcontrôleur n'a pas de support matériel pour double. Flottant uniquement):
add_definitions(-DLUA_32BITS)
Eh bien, il ne reste plus qu'à informer l'éditeur de liens de la nécessité d'assembler cette bibliothèque et d'inclure le résultat dans la mise en page du projet final:
Tracé de CMakeLists pour lier un projet à la bibliothèque lua add_subdirectory(${CMAKE_SOURCE_DIR}/bsp/submodules/module_lua) ... target_link_libraries(${PROJECT_NAME}.elf PUBLIC
Définir des fonctions pour travailler avec la mémoire
Puisque Lua lui-même ne s'occupe pas de la mémoire, cette responsabilité est transférée à l'utilisateur. Cependant, lors de l'utilisation de la bibliothèque
lauxlib intégrée et de la fonction
luaL_newstate Ă partir d'elle, la fonction
l_alloc est
liée en tant que système de mémoire. Il est défini comme suit:
static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) { (void)ud; (void)osize; if (nsize == 0) { free(ptr); return NULL; } else return realloc(ptr, nsize); }
Comme mentionné au début de l'article, le projet a déjà remplacé
les fonctions
malloc et
free , mais il n'y a pas de fonction
realloc . Nous devons y remédier.
Dans le mécanisme standard pour travailler avec le tas FreeRTOS, le fichier heap_4.c utilisé dans le projet n'a pas de fonction pour redimensionner un bloc de mémoire précédemment alloué. À cet égard, il est nécessaire de faire sa mise en œuvre sur la base de
malloc et
gratuit .
Puisqu'à l'avenir, il est possible de modifier le schéma d'allocation de mémoire (en utilisant un autre fichier heap_x.c), il a été décidé de ne pas utiliser les intérieurs du schéma actuel (heap_4.c), mais de créer un complément de niveau supérieur. Bien que moins efficace.
Il est important de noter que la méthode
realloc supprime non seulement l'ancien bloc (s'il en existait un) et en crée un nouveau, mais déplace également les données de l'ancien bloc vers le nouveau. De plus, si l'ancien bloc contenait plus de données que le nouveau, le nouveau est rempli à la limite des anciens et les données restantes sont supprimées.
Si ce fait n'est pas pris en compte, votre machine pourra exécuter un tel script trois fois à partir de la ligne "
a = 3 \ n ", après quoi il tombera dans une faute matérielle. Le problème peut être résolu après avoir étudié l'image résiduelle des registres dans le gestionnaire de pannes matérielles, à partir de laquelle il sera possible de découvrir que le crash s'est produit après avoir tenté d'étendre la table dans les entrailles du code interprète et de ses bibliothèques. Si vous appelez un script comme "
print 'test' ", le comportement changera en fonction de la façon dont le fichier du firmware est assemblé (en d'autres termes, le comportement n'est pas défini).
Afin de copier les données de l'ancien bloc vers le nouveau, nous devons connaître la taille de l'ancien bloc. FreeRTOS heap_4.c (comme les autres fichiers qui fournissent des méthodes de gestion de tas) ne fournit pas d'API pour cela. Par conséquent, vous devez terminer le vôtre. Comme base, j'ai pris la fonction
vPortFree et réduit sa fonctionnalité à la forme suivante:
Code de fonction VPortGetSizeBlock 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; }
Maintenant, c'est petit, écrivez
realloc basé sur
malloc ,
free et
vPortGetSizeBlock :
Code d'implémentation Realloc basé sur malloc, free et 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; }
Ajouter un support pour travailler avec stdout
Comme il ressort de la description officielle, l'interpréteur lua lui-même n'est pas en mesure de travailler avec les E / S. À ces fins, l'une des bibliothèques standard est connectée. Pour la sortie, il utilise le flux
stdout . La fonction
luaopen_io de la bibliothèque standard est responsable de la connexion au flux. Pour prendre en charge l'utilisation de
stdout (contrairement Ă
printf ), vous devrez remplacer la fonction
fwrite . Je l'ai redéfini en fonction des fonctions décrites dans l'
article précédent .
Fonction d'écriture 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; }
Sans sa définition, la fonction d'
impression dans lua s'exécutera avec succès, mais il n'y aura pas de sortie. De plus, il n'y aura aucune erreur sur la pile Lua de la machine (puisque la fonction a été exécutée avec succès).
En plus de cette fonction, nous aurons besoin de la fonction
fflush (pour que le mode interactif fonctionne, qui sera
discuté plus tard). Comme cette fonction ne peut pas être remplacée, vous devrez la nommer un peu différemment. La fonction est une version allégée de la fonction
fwrite et est destinée à envoyer ce qui est maintenant dans le tampon avec son nettoyage ultérieur (sans transfert de chariot supplémentaire).
Fonction Mc_fflush 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; }
Récupération de chaînes à partir d'un port série
Pour obtenir des chaînes pour une machine lua, j'ai décidé d'écrire une classe uart-terminal simple, qui:
- reçoit des données sur un port série octet par octet (en interruption);
- ajoute l'octet reçu à la file d'attente, d'où le flux le reçoit;
- dans un flux d'octets, s'il ne s'agit pas d'un saut de ligne, renvoyé sous la forme dans laquelle il est arrivé;
- si un saut de ligne est arrivé (' \ r '), alors 2 octets de retour de chariot terminal sont envoyés (" \ n \ r ");
- après l'envoi de la réponse, le gestionnaire de l'octet arrivé (objet de disposition de ligne) est appelé;
- contrôle la pression de la touche de suppression de caractère (pour éviter de supprimer des caractères de service de la fenêtre du terminal);
Liens vers les sources:
- L'interface de classe UART est ici ;
- La classe de base UART est ici et ici ;
- classe uart_terminal ici et ici ;
- créer un objet de classe dans le cadre du projet ici .
De plus, je note que pour que cet objet fonctionne correctement, vous devez attribuer une priorité à l'interruption UART dans la plage autorisée pour travailler avec les fonctions FreeRTOS à partir de l'interruption. Sinon, vous pouvez obtenir des erreurs difficiles à déboguer. Dans l'exemple actuel, les options suivantes pour les interruptions sont définies dans le fichier
FreeRTOSConfig.h .
Paramètres dans FreeRTOSConfig.h #define configPRIO_BITS 4 #define configKERNEL_INTERRUPT_PRIORITY 0XF0
Dans le projet lui-mĂŞme, un objet de classe
nvic définit la priorité de l'interruption 0x9, qui est incluse dans la plage valide (la classe nvic est décrite
ici et
ici ).
Formation de cordes pour une machine Lua
Les octets reçus de l'objet uart_terminal sont transférés vers une instance d'une classe simple serial_cli, qui fournit une interface minimale pour éditer la chaîne et la transférer directement vers le thread dans lequel la lua-machine est exécutée (en appelant la fonction de rappel). Lors de l'acceptation du caractère '\ r', une fonction de rappel est appelée. Cette fonction doit copier une ligne sur elle-même et «libérer» le contrôle (puisque la réception de nouveaux octets est bloquée pendant un appel. Ce n'est pas un problème avec des flux correctement priorisés et une vitesse UART suffisamment basse).
Liens vers les sources:
- fichiers de description serial_cli ici et ici ;
- créer un objet de classe dans le cadre du projet ici .
Il est important de noter que cette classe considère qu'une chaîne de plus de 255 caractères n'est pas valide et la rejette. C'est intentionnel, car l'interpréteur lua vous permet d'entrer des constructions ligne par ligne, en attendant la fin du bloc.
Passer une chaîne à l'interpréteur Lua et son exécution
L'interpréteur Lua lui-même ne sait pas accepter le code de bloc ligne par ligne, puis exécuter lui-même le bloc entier. Cependant, si vous installez Lua sur un ordinateur et exécutez l'interpréteur en mode interactif, nous pouvons voir que l'exécution se fait ligne par ligne avec la notation correspondante lors de la frappe, que le bloc n'est pas encore terminé. Puisque le mode interactif est ce qui est fourni dans le package standard, nous pouvons voir son code. Il se trouve dans le fichier
lua.c. Nous nous intéressons à la fonction
doREPL et à tout ce qu'elle utilise. Afin de ne pas proposer de vélo, pour obtenir les fonctions du mode interactif dans le projet, j'ai créé un portage de ce code dans une classe distincte, que j'ai nommée
lua_repl par le nom de la fonction d'origine, qui utilise printf pour sortir des informations sur la console et a une méthode publique
add_lua_string pour ajouter une ligne reçue de l'objet classe serial_cli décrit ci-dessus.
Références:
- description de la classe lua_repl ici ;
- code ici , ici et ici ;
La classe est faite selon le modèle Myleton singleton, car il n'est pas nécessaire de donner plusieurs modes interactifs au sein du même appareil. Un objet de classe lua_repl reçoit ici les données d'un objet de classe serial_cli.
Étant donné que le projet dispose déjà d'un système unifié pour l'initialisation et la maintenance des objets globaux, le pointeur vers l'objet de la classe lua_repl est passé à l'objet de la classe globale
player :: base ici . Dans la méthode de
démarrage d'un objet de la classe
player :: base (déclarée
ici . Elle est également appelée depuis main), la méthode
init de l'objet de la classe lua_repl est appelée avec la priorité de la tâche FreeRTOS 3 (dans le projet, vous pouvez affecter la priorité de la tâche de 1 à 4. Où 1 Est la priorité la plus faible et 4 est la plus élevée). Après une initialisation réussie, la classe globale démarre le planificateur FreeRTOS et le mode interactif commence son travail.
Problèmes de portage
Vous trouverez ci-dessous une liste des problèmes que j'ai rencontrés lors du port Lua de la machine.
2-3 scripts d'une seule ligne d'affectation de variables sont exécutés, puis tout tombe en faute
Le problème venait de la méthode de réallocation. Il est nécessaire non seulement de resélectionner le bloc, mais également de copier le contenu de l'ancien (comme décrit ci-dessus).
Lorsque vous essayez d'imprimer une valeur, l'interprète tombe en faute
Il était déjà plus difficile de détecter le problème, mais à la fin j'ai réussi à découvrir que snprintf était utilisé pour l'impression. Puisque lua stocke les valeurs en double (ou float dans notre cas), printf (et ses dérivés) avec support en virgule flottante est requis (j'ai écrit sur les subtilités de printf
ici ).
Configuration requise pour la mémoire non volatile (flash)
Voici quelques mesures que j'ai prises pour juger de la quantité de mémoire non volatile (flash) qui doit être allouée pour intégrer la machine Lua dans le projet. La compilation a été effectuée en utilisant gcc-arm-none-eabi-8-2018-q4-major. La version de Lua 5.4 a été utilisée. Ci-dessous dans les mesures, l'expression «sans Lua» signifie la non-inclusion de l'interpréteur et des méthodes d'interaction avec lui et ses bibliothèques, ainsi qu'un objet de la classe lua_repl dans le projet. Toutes les entités de bas niveau (y compris les remplacements pour les fonctions
printf et
fwrite ) restent dans le projet. La taille du segment FreeRTOS est de 1024 * 25 octets. Le reste est occupé par des entités de projet mondiales.
Le tableau récapitulatif des résultats est le suivant (toutes les tailles en octets):
Options de construction | Sans lua | Noyau uniquement | Lua avec bibliothèque de base | Lua avec base de bibliothèques, coroutine, table, chaîne | luaL_openlibs |
---|
-O0 -g3 | 103028 | 220924 | 236124 | 262652 | 308372 |
-O1 -g3 | 74940 | 144732 | 156916 | 174452 | 213068 |
-Os -g0 | 71172 | 134228 | 145756 | 161428 | 198400 |
Exigences de RAM
Étant donné que la consommation de RAM dépend entièrement de la tâche, je donnerai un tableau récapitulatif de la mémoire consommée immédiatement après avoir
allumé la machine avec un ensemble de bibliothèques différent (il est affiché par l'
impression (commande collectgarbage ("count") * 1024 )).
La composition | RAM utilisée |
Lua avec bibliothèque de base | 4809 |
Lua avec base de bibliothèques, coroutine, table, chaîne | 6407 |
luaL_openlibs | 12769 |
Dans le cas de l'utilisation de toutes les bibliothèques, la taille de la RAM requise est considérablement augmentée par rapport aux ensembles précédents. Cependant, son utilisation dans une partie considérable des applications n'est pas nécessaire.
De plus, 4 ko sont également alloués à la pile de tâches, dans laquelle la machine Lua est exécutée.
Utilisation ultérieure
Pour une utilisation complète de la machine dans le projet, vous devrez décrire plus en détail toutes les interfaces requises par le code logique métier pour les objets matériels ou de service du projet. Cependant, c'est le sujet d'un article séparé.
Résumé
Cet article décrit comment connecter une machine Lua à un projet de microcontrôleur, ainsi que lancer un interpréteur interactif à part entière qui vous permet d'expérimenter la logique métier directement à partir de la ligne de commande du terminal. De plus, les exigences pour le matériel du microcontrôleur ont été prises en compte pour différentes configurations de la machine Lua.