Tixlegeek's DevBlog Code, Gringo, Silicium

qrcode

[T-Watch2020] Framework ESP-IDF Partie II: Plan, convention, et interface.

Partie II:

Salutations!

Aujourd'hui, on va commencer à dresser un plan, et choisir des conventions. Ça veut dire qu'on va définir les interfaces qui nous permettront d'utiliser les composants de la T-Watch depuis le code de manière unifiée et efficace.

Si je fait ça maintenant, c'est parce qu'avant de faire n'importe quoi, il faut absolument se faire un chemin. Si on ne sait pas ou on va, on à tendance à tourner en rond et c'est méga chiant. Du coup, pour que vous me suiviez, j'ai fait un schéma:

l'idée derrière tout ça c'est de rendre les données accessibles, le tout de manière homogène. On abstrait la gestion des périphériques, et on donne accès à des fonctions spécifiques via une structure organisée

En faisant comme ça, on s'offre la possibilité de modifier les structures indépendamment des autres. Par exemple, si on décide de changer la partie qui gère l'affichage, on aura qu'a brancher la structure sur un autre driver, et tout le code qui utilisait déjà l'affichage n'y verra que du feu.

Pour mettre en place le plan, on va utiliser les types, et les structures. On va donc commencer par le haut du schéma, et coder les fonctions utiles à chaque composant. Une fois que c'est fait, on les intègre à une structure qui va nous permettre d'accéder à ces fonctions via l'application finale.

Driver

Dans l'exemple du AXP202, le "driver" à proprement parler, ça sera les fonctions spécifiques au composant, et les fonctions read/write. Les fonctions spécifiques dont je parle, ce sont celles décrite dans la documentation du composant. On aura pas besoin de tout coder, parce qu'on aura pas forcément besoin de toutes les fonctions sur tous les composants. On essaye de faire quelque-chose de léger, et extensible, alors on va se focaliser sur l'essentiel.

Objet

L'objet, toujours pour notre AXP202, sera une structure permettant de donner accès à chacune des fonctions du driver. En C, ça ressemblera à ça:


typedef struct {
  // variables utiles au driver
  esp_err_t init;
  esp_err_t status;

  // Fonctions génériques
  esp_err_t (*writeCb)(uint8_t reg, uint8_t nbytes, uint8_t *data);
  esp_err_t (*readCb)(uint8_t reg, uint8_t nbytes, uint8_t *data);
  // Fonctions spécifiques
  esp_err_t (*setPowerOutPut) (uint8_t ch, bool en);
  esp_err_t (*getChargeControlCur)( uint8_t *charge);
  esp_err_t (*setChargeControlCur)( uint16_t ma);
} power_t

Ça c'est juste un exemple. On verra comment mettre ça en forme dans la partie "Conventions" qui suit.

Contexte

Le contexte, c'est la structure qui va contenir toutes les informations du système. En faisant de gros raccourcis, on peut dire que ça sera le /dev d'un GNU/Linux, ou, la base de registre d'un Windows.
En C, ça va se traduire par une structure globale qui tiendra les comptes, et répertoriera tous les drivers/variables, etc. On pourrait représenter notre contexte à nous comme ça:
// Exemple de contexte
typedef struct {
        esp_err_t init;
        esp_err_t status;
        display_t *Display;
        power_t *Power;
       /*
        [...]
       */
} twatch_t;

Si on fait tout ça, c'est pour rendre les données et E/S accessibles. Si vous êtes entrain de vous dire qu'on pourrait parfaitement se passer du contexte, vous avez en partie raison, mais, dans ce cas précis, ça va nous intéresser parce que les applications qu'on va vouloir développer sur la T-Watch vont nécessiter une certaine organisation. Il est bien plus facile de maîtriser son code quand on peut contrôler les données, et quand elles sont bien organisées.

Convention

Ahah! j'en vois qui commencent à flipper.
La convention, c'est les règles qu'on va respecter pour nommer nos variables, nos fonctions, et pour coder, de manière générale. Je vous trolle un peu depuis tout à l'heure avec le Spectre de la convention, mais, en fait, tout le monde n'en fait qu'a sa tête.

Pour ma part, pour le nommage, j'aime bien utiliser le CamelCase, et le snake_case conjointement, mais, ça on s'en fou un peu, c'est de la rhétorique. Ce qui va surtout falloir décider, c'est comment les nommer fonctionnellement. Comme je suis entrain de faire un truc spécifique à la T-Watch, je vais nommer mon contexte TWatch et, chacun de ses périphérique aura un nom évocateur. Par exemple, l'écran et tout ce qui s'y rapporte sera Display.
Le contexte peut être global, et accessible de partout, ou stocké dans un pointeur. Dans mon cas, je vais le stocker globalement, parce que jusqu’à preuve du contraire, on a qu'une montre dans une montre.
Je prend donc la liste des périphériques, et dresse les structures nécessaires.
// Exemple de contexte
typedef struct {
        esp_err_t init;
        esp_err_t status;
        display_t *Display;
        power_t *Power;
        acc_t *Acc;
        clock_t *Clock;
        vibrator_t *Vibrator;
        speaker_t *Speaker;
        ir_t *IR;
} twatch_t;

Interface

Pour stocker les données, on a deux solutions:

1. Créer une structure statique.

Une structure statique, c'est une structure définie et allouée dès sa création. Basiquement, c'est un arbre avec suffisamment de place sur chacune de ses branches, qu'on rempli petit à petit. Ça a certains avantages, surtout que les périphériques disponibles sur la T-Watch sont fixes, mais, l'emprunte mémoire sera elle-aussi fixe, et si on souhaite libérer l'utilisation d'un périphérique, on aura quand même alloué l'espace nécessaire à son fonctionnement.

2. Créer une structure dynamique.

Une structure dynamique, c'est une structure composée essentiellement de pointeurs. C'est comme une plateforme vide à laquelle on vient connecter des ponts (ou pas) vers d'autre plateforme, si on le désir.
C'est très avantageux parce qu'on aura pas à allouer quoi que ce soit. En fait, on aura une structure vide, qu'on remplira petit à petit avec des objets qu'on allouera uniquement à l'initialisation. On pourra très bien désallouer l'écran, pour réserver la mémoire à une utilisation ponctuelle, puis le ré-allouer dans la foulée.
Vous l'aurez compris, c'est l'option 2 qu'on va choisir! On va simplement coder ça comme ça:

twatch_t TWatch = {null};

power_t *powerInit( void ){
  power_t *object = heap_caps_malloc(sizeof(power_t), MALLOC_CAP_8BIT);
  return object;
}
power_t *power = powerInit();
...
TWatch.Power = power;
...
free(TWatch.power);

Ça m'a l'aire pas mal pour le moment. Si vous avez des interrogations, des critiques, ou des coquilles à remonter, n'hésitez pas à me pinger sur twitter!

La prochaine fois, je vous en dirai plus, et on commencera à faire les choses correctement. Tout ce qui nous reste à faire, c'est lire plus de doc, lire et écrire plus de doc, et tout ranger. La suite au prochain article

HppHckng!