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 ?)

Boutons BTN et RST sur le microcontrôleur SPIN

Fig. 4 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 :

>>> 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é

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.0F0.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/30

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) :

Comparaison de la division entière 1/3 et flottante 1.0/3.0

Fig. 5 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

Transformations (Clarke, Park)

Référentiels triphasés et vecteur spatial

Fig. 6 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

Structures de données triphasées et transformations

Fig. 7 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.

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);

Oscilloscope embarqué

Schéma d'utilisation du scope embarqué sur le microcontrôleur

Fig. 8 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.

Séquence temporelle d'acquisition de l'oscilloscope

Fig. 9 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).