.. _prog: ################################ Programmation des cartes OwnTech ################################ Cette page contient une documentation introductive et des astuces pour la programmation des cartes OwnTech. La documentation officielle qui fait foi (mais qui peut être incomplète : contributions are welcome) est à l'adresse https://docs.owntech.org. Recovery mode du microcontrôleur ================================ **Symptôme** : le microcontrôleur SPIN est « planté », dans le sens où il n'accepte pas de nouveau programme avec l'action “Upload →” de VSCode/PlatformIO (message d'erreur dans le terminal d'upload). **Solution** : redémarrer le microntrôleur en mode **RECOVERY** avec la procédure suivante (https://docs.owntech.org/latest/spin/1.2.0/getting_started/) 1. Appuyez simultanément sur les boutons BTN et RST 2. Relâchez le bouton RST (après un certain temps ?), tout en maintenant BTN enfoncé 3. Relâchez BTN (après un certain temps ?) .. figure:: images/SPIN_board_buttons.png :height: 8em :alt: Boutons BTN et RST sur le microcontrôleur SPIN Boutons BTN et RST sur le microcontrôleur SPIN Nombres et arithmétique en C/C++ embarqué ========================================= Calcul en float32 ----------------- Le microcontrôleur `SPIN`_ (un microcontrôleur 32-bit `ARM Cortex-M4`_ de la famille `STM32G4`_) permet de faire des calculs avec des nombres à `virgule flottante`_ (représentation approximée des nombres réels avec *mantisse* et *exposant*, semblable à la notation scientifique, mais en base 2), par opposition au calcul à virgule fixe, car il inclut un circuit de calcul dédié dit `floating-point unit (FPU)`_. Cependant, ces calculs se font en **simple précision** (32 bits), par opposition à la double précision (64 bits) plus habituelle en Python (le type ``float`` de Python) ou Matlab ou même en C non embarqué (type ``double``). Ainsi, un résultat numérique obtenu sous Python ou Matlab (filtre, contrôleur…) ne donnera pas forcément le même résultat une fois embarqué. **Syntaxe** à utiliser : * **type** : les variables réelles C/C++ pour un code microcontrôleur ARM sont déclarés avec le type ``float32_t`` * **constantes** : les constantes numériques à virgule en C/C++ (ex. : ``1.0``) sont par défaut des ``double`` (64 bits). Pour saisir des constants simple précision (32 bits), il faut les préfixer par ``f`` ou ``F``: ``1.0f`` ou ``1.0F``. Ceci dit, pour simplifier la programmation, le code est compilé avec l'option ``-fsingle-precision-constant`` qui permet d'oublier ces préfixes... **Effet sur la précision** : la longueur de la représentation des nombres change la précision des calculs : * 32 bits : précision relative de l'ordre de 1.10^-7 (jusqu'à 7 chiffres significatifs justes) * 64 bits : précision relative de l'ordre de 2.10^-16 (jusqu'à 16 chiffres significatifs justes) On peut prototyper des calculs en float32 en Python avec numpy : .. code-block:: python-console >>> import numpy as np >>> np.float32(1.0)/np.float32(3.0) np.float32(0.33333334) >>> 1 + 1e-10 # en float de Python, cad précision 64 bits 1.0000000001 # le +1e-10 est bien pris en compte >>> np.float32(1.0) + np.float32(1e-10) np.float32(1.0) # le +1e-10 a été tronqué .. _SPIN: https://docs.owntech.org/1.0.0/spin/1.2.0/getting_started/ .. _STM32G4: https://www.st.com/en/microcontrollers-microprocessors/stm32g4-series.html .. _ARM Cortex-M4: https://en.wikipedia.org/wiki/ARM_Cortex-M .. _virgule flottante: https://fr.wikipedia.org/wiki/Virgule_flottante .. _floating-point unit (FPU): https://en.wikipedia.org/wiki/Floating-point_unit Division de nombres entiers en C -------------------------------- Le C/C++, le résultat de la division deux nombres ``a/b`` dépend du *type* des deux opérandes : * ``a`` ou ``b`` à virgule flottante (au moins un des deux) : division en virgule flottante. Exemple ``1.0F/3.0F`` → ``0.33333334326...F`` * ``a`` et ``b`` entiers : division en virgule flottante : **division entière** (euclidienne), c'est-à-dire avec troncature de la partie fractionnaire, *même si le résultat est stocké dans une variable à virgule flottante*. Exemple : ``1/3`` → ``0`` Astuce : dans le cas où la division fait intervenir des constantes, la fonction `IntelliSense `_ de VSCode, avec `l'extension C/C++ `_ installée (recommandé), permet de diagnostiquer rapidement le résultat du calcul en passant le pointeur sur le code (`Quick Info `_) : .. figure:: images/Cpp_division_comparison.png :height: 8em :alt: Comparaison de la division entière 1/3 et flottante 1.0/3.0 Comparaison de la division entière ``1/3`` et flottante ``1.0/3.0`` Ressource: WikiLivres `Programmation C++/Les opérations de base `_. Bibliothèque OwnTech ==================== Fonctions mathématiques (trigonométrie) --------------------------------------- Exemples : :: float32_t angle, x; ot_sin(angle); ot_cos(angle); ot_modulo_2pi(x); // x % (2*PI) Détail de l'implémentation de cos : `ARM CMSIS arm_cos_f32.c `_ .. _prog-transform: Transformations (Clarke, Park) ------------------------------ .. figure:: images/space_vector_diag.png :height: 20em :alt: Référentiels triphasés et vecteur spatial Référentiels triphasés et vecteur spatial Les transformations, c'est-à-dire les changements de référentiel pour les grandeurs triphasées, sont implémentées avec : - 3 types de données : un par référentiel (naturel abc, Clarke alpha beta et Park dq) - les fonctions de transformation proprement dites .. figure:: images/transforms.drawio.svg :height: 20em :alt: Structures de données triphasées et transformations Structures de données triphasées et transformations Exemples : :: /* Définition de variables abc et dq */ float32_t grid_angle; // phase du réseau, estimé par une PLL three_phase_t Vi_abc, Iabc; dqo_t Vi_dq, Idq; /* Transformation */ Idq = Transform::to_dqo(Iabc, grid_angle); Vi_abc = Transform::to_threephase(Vi_dq, grid_angle); Remarque : comme les transformations se font généralement sur des mesures qui bougent au cours du temps et parce que le référentiel dq n'est pas stationnaire (il tourne), le calcul des transformations est généralement fait périodiquement dans la boucle de contrôle. .. _prog-ctrl: Régulation (Contrôleurs) ------------------------ https://docs.owntech.org/latest/controlLibrary/docs/use-pid/ Fonction à appeler à chaque période de contrôle : :: u = pi.calculateWithReturn(reference, measurement); .. _prog-scope: Oscilloscope embarqué ===================== .. figure:: images/Scope_usage.png :height: 10em :alt: Schéma d'utilisation du scope embarqué sur le microcontrôleur Schéma d'utilisation du scope embarqué sur le microcontrôleur Il est possible d'enregistrer une les variables internes du microcontrôleur (mesures, régulation...) avec la `bibliothèque Scope `_. L'oscilloscope logiciel permet d'enregistrer une **séquence temporelle** de plusieurs variables ("channels"), comme un oscilloscope réel réglé en mode "Single" (enregistrement unique). L'oscilloscope se trouve, à chaque instant, dans l'un de ses trois états de fonctionnement possibles : - **Non déclenché** (`ACQ_UNTRIG`) : l'oscilloscope enregistre des échantillons avant d'être déclenché (trigger) - **Déclenché** (`ACQ_TRIG`) : l'oscilloscope enregistre des échantillons après un événement déclencheur - **Terminé** (`ACQ_DONE`) : l'oscilloscope a terminé l'enregistrement des échantillons et les données sont prêtes à être récupérées Le fonctionnement de l'oscilloscope commence par un appel à sa méthode `start()` qui l'initialise (ou le réinitialise) à l'état **Non déclenché**. Ensuite, des appels répétés à `acquire()` permettent d'enregistrer des échantillons et d'appeler la fonction `trigger()`. Un front montant du trigger amène l'oscilloscope à l'état **Déclenché**, où il reste jusqu'à ce que la mémoire d'enregistrement soit pleine. .. figure:: images/Scope_Acquisition_timing.svg :height: 23em :alt: Séquence temporelle d'acquisition de l'oscilloscope Séquence temporelle d'acquisition de l'oscilloscope **Récupération et affichage des données** : Après acquisition, les données peuvent être récupérées via le `Serial Port Monitor `_ (aussi appelé Device Monitor), grâce à un print des valeurs enregistrées en hexadécimal. Les données transmises sont automatiquement enregistrées par VSCode/PlaformIO grâce à un `filtre `_ qui lit en permence le flux Device Monitor. Les données récupérées sont enregistrées dans un sous-dossier ``src/Data_records``, c'est-à-dire à côté du fichier source principal ``main.cpp``. Trois enregistrements sont générées : * fichier brut : ``{date}_{heure}-record.txt`` * tableau CSV : ``{date}_{heure}-record.txt`` * une colonne par variable enregistrée (colonnes séparées par des virgules) * une ligne par instant enregistrés (une ligne de tableau par ligne de texte) * avec nom des variables sur la première ligne * tracé graphique par défaut : ``{date}_{heure}-record.png`` **Interface utilisateur** : (gérée par la ``user_interface_task()`` : taper ``h`` dans la console pour la liste des actions disponibles dans l'application) * récupérer les données enregistrées : ``s`` (comme "save") * réinitialiser le scope pour faire une nouvelle acquisition : ``r`` (comme "reset") **Programmation du trigger** : L'enregistrement démarre lorsque la fonction ``scope_trigger()``, qui est appelée automatiquement à chaque acquisition, renvoie ``true`` (c'est le front montant ``false`` suivi de ``true`` qui déclenche, les valeurs suivantes n'ayant plus d'importance). Plus exactement, la fonction appelée est celle passée en argument de ``scope.set_trigger()`` au moment de la configuration du scope (voir ``setup_scope()``). Pour changer l'événement de déclenchement, il faut modifier ``scope_trigger()`` (puis recompiler et uploader le code). Exemple de fonction trigger (seule la première instruction ``return`` non commentée compte) : :: bool scope_trigger() { return true; // enregistrer immédiatement après scope.start() return pll_on; // déclenchement à l'activation de la PLL return Idq_ref.d >= 1.0; // déclenchement sur un échelon de consigne de courant return control_mode == POWER_ST; // déclenchement au démarrge du mode POWER return over_current_flag || pll_unsync_flag; // déclenchement sur une erreur de surintensité ou (||) désynchronisation PLL } Astuce programmation : si plusieurs expériences nécessitent des trigger différents, mettre toute les expressions dans ``scope_trigger()`` et les commenter/décommenter selon l'expérience (recompilation et upload restent néanmoins nécessaires).