Du schéma à l’application: bring-up logiciel express

Je continue sur ma lancée d’expérimenter différents scénarios d’utilisation de Zephyr RTOS pour combiner entre eux l’électronique et le logiciel applicatif.

Dans cette optique, j’ai sorti de mon tiroir une carte d’évaluation obsolète de chez ST, un ST25DV-DISCOVERY datant de 2016, que j’avais récupérée d’une opération rangement d’un labo électronique. Cette carte comprend un écran et un bouton à 4 directions façon joystick, j’avais l’idée d’en faire un mini-jeu façon arcade, sans écrire de code, espérant trouver un Pong ou un Tetris à réutiliser quelque part.

Par chance, ma carte a le bon goût d’avoir un STM32 et tous ses périphériques sont déjà supportés par Zephyr, ce qui me permet de me concentrer sur la partie couture.

Quelques liens utiles avant de commencer:

C’est parti !

Écran LCD

Erreur 404 : datasheet inexistante

Faire fonctionner cet écran est le plus gros morceau du projet, d’autant que je ne m’y connais pas beaucoup en écrans LCD. Je trouve beaucoup de pièces qui ne sont pas vendues sur étagère, et les datasheets sont difficiles à se procurer sans passer par le distributeur ou le fabricant.

Les écrans sont généralement vendus sous forme de modules, avec un contrôleur intégré permettant de leur parler par des bus série comme I2C ou SPI, ou parallèles comme le MIPI DSI. La plupart des références possèdent des drivers compatibles, peut-être parce-qu’elles utilisent les mêmes références de contrôleurs, mais comment savoir quel driver compatible utiliser sans démonter le module d’écran ? Pour ce projet, j’ai le code source original du kit, je peux simplement aller farfouiller dans le code, et je finis par trouver le nom d’un équivalent bien connu, le Ilitek ILI9341.

Cet équivalent est proposé dans les catalogues Adafruit /Sparkfun et il est bien connu et documenté.

Autre découverte du monde des écrans, le pilote Zephyr d’écran LCD SPI a été récemment renommé « MIPI DBI ». Un standard inconnu au bataillon en ce qui me concerne, mais de ce que j’en comprends, c’est un standard de commande d’écran LCD basé sur un bus SPI, plus deux pins supplémentaires : un reset et une pin de sélection entre commande et data. Il a l’air d’être standard de facto, mais n’est mentionné nulle part, il y a sans doute une histoire technologique fascinante entremélée de stratégie géopolitique, mais je décide d’arrêter de me poser des questions car j’ai un Pong à réaliser.

DC, WR, DCX, TE, SDA, WTF

Comment remplir notre device tree ? Notre driver prend comme paramètres un bus SPI, ainsi que deux GPIOs, une « reset » et une « dc », dc comme « data / commande ».

Schéma côté écran
Top level du schéma
Schéma côté STM32
        mipi_dbi {
                compatible = "zephyr,mipi-dbi-spi";
                reset-gpios = <&gpioc 1 GPIO_ACTIVE_HIGH>;
                dc-gpios = <&gpioc 0 GPIO_ACTIVE_HIGH>;
                spi-dev = <&spi2>;
                [...]
        };

boards/arm/st25dv_mb1283_disco/st25dv_mb1283_disco.dts

Il s’agit donc du bus SPI 2, mais le MISO n’est pas connecté. MOSI est câblé, mais il est renommé en « SDA » au top level… et en fait ce changement n’est pas juste un hasard de copier-coller, il indique que notre câble peut être utilisé de manière bidirectionnelle, et oui, il existe un SPI bidirectionnel, half-duplex, où le câble data est censé s’appeller SISO ou MOMI.

Mauvaise nouvelle, mes GPIOs s’appellent WR et TE, je ne sais pas qui est qui. Après lecture de la datasheet de l’équivalence ILI9341, WR ressemble à WRX, Write-Read, mais ça correspond à un autre mode d’utilisation de l’écran. C’est une pin également utilisée comme D/CX, donc je retrouve le DC que je cherchais, et c’est la GPIO PC0.

WRX (DCX)

– 8080-I /8080-II system (WRX): Serves as a write signal and writes data at the rising edge.

– 4-line system (D/CX): Serves as command or parameter select.

TE signifie Tearing Effect, et sert à synchroniser l’écriture de l’écran avec son rafraîchissement. Ce n’est pas le reset, mais j’ai envie de contrôler cette pin avec le driver, donc pourquoi pas la câbler au reset pour qu’elle reste à son niveau bas tout du long, d’où la GPIO PC1.

Paramétrage du driver

Une fois notre bus MIPI DBI virtuel créé, nous ajoutons le driver de notre écran, avec ses paramètres.

#include <zephyr/dt-bindings/display/ili9xxx.h>
[...]
        mipi_dbi {
                [...]
                write-only;

                ili9341: ili9341@0 {
                        compatible = "ilitek,ili9341";
                        mipi-max-frequency = <20000000>;
                        reg = <0>;
                        pixel-format = <ILI9XXX_PIXEL_FORMAT_RGB888>; // 1
                        rotation = <0>;
                        width = <240>;
                        height = <320>;
                        duplex = <0x800>;
            };
        };

boards/arm/st25dv_mb1283_disco/st25dv_mb1283_disco.dts

« duplex = <0x800> » permet de paramétrer le bus en half-duplex, ce qui est nécessaire dans notre cas puisque MISO n’est pas là.

« write-only » permet d’éviter les lectures au maximum, ce qui est pertinent dans notre cas pour éviter les embouteillages sur le bus.

pixel-format, width, et height, ont fait l’objet d’essais et erreurs. Au départ, mon format de pixel était à 0, et j’avais des couleurs toutes bizarres, problème rapidement résolu après avoir changé cette valeur.

mipi-max-frequency permet de sélectionner la fréquence du bus SPI. Ici la valeur est maximale, il est intéressant de la diminuer pour jouer avec son analyseur logique préféré.

Une fois le tout paramétré, il faut signifier à Zephyr qui est l’écran.

zephyr,display = &ili9341;

boards/arm/st25dv_mb1283_disco/st25dv_mb1283_disco.dts

Sans oublier de demander à cmake de compiler les fichiers sources des drivers, dans le prj.conf :

CONFIG_DISPLAY=y

game/prj.conf

Un coup d’oeil au protocole

Voici un exemple de transmission d’image en SPI. Ici on envoie un bitmap dans un rectangle aux coordonnées (X1, Y1, X2, Y1) :

  • CD passe à 0, pour envoyer une commande
  • La commande 0x2A (Column Address Set, CASET) est envoyée sur le bus SPI
  • CD passe à 1 pour envoyer une donnée
  • Les coordonnées X1 et X2 sont envoyées sur le bus SPI
  • De la même manière, 0x2B (Page Address Set, PASET) est envoyé, suivi des coordonnées Y1 et Y2
  • La commande 0x2C est ensuite transmise, suivie de tous les octets du bitmap

Le protocole complet est décrit dans la datasheet ILI9341.

L’exemple « display »

Une fois l’écran correctement configuré, il est possible de le tester à l’aide d’un petit exemple tout à fait utile présent dans les dossiers d’exemples Zephyr, j’ai nommé : samples/drivers/display.

Pavé tactile résistif

L’écran n’est pas seulement un écran, il dispose également d’un pavé tactile, mais un pavé tactile résistif. Les smartphones nous ont habitués à la technologie capacitive, celle à base d’indium (ITO), et j’avais oublié cette technologie plus ancienne et moins agréable à utiliser, mais qui a le bon goût de ne pas contenir de métaux ultra-rares. Même si la présence de boutons physiques m’enthousiasme bien plus, je rajoute mon pavé tactile au device tree.

Schéma côté composant
Schéma côté STM32
Top level du schéma
&i2c1 {
        pinctrl-0 = <&i2c1_scl_pb6 &i2c1_sda_pb7>;
        pinctrl-names = "default";
        status = "okay";
        clock-frequency = <I2C_BITRATE_FAST>;

        stmpe811: stmpe811@41 {
                compatible = "st,stmpe811";
                status = "okay";
                reg = <0x41>;
                int-gpios = <&gpiob 5 GPIO_ACTIVE_LOW>;
                [...]
        };
};

boards/arm/st25dv_mb1283_disco/st25dv_mb1283_disco.dts

Le lecteur aura repéré les correspondances entre notre SCL et la pin I2C1_SCL PB6, notre SDA et la pin I2C1_SCL PB7, et notre interruption sur PB5.

Adresse I2C

Mais comment trouver cette valeur de 0x41 ? Encore une fois elle m’a été donnée par le copier-coller, mais voici où aller la chercher dans la datasheet du STMPE811.

ADDR0 Address
00x82
10x84

Nous avons donc une pin ADDR0 qui est connectée soit à GND (valeur 0), soit à VCC (valeur 1). Il faut donc aller voir comment est configurée ADDR0 sur notre schéma. ADDR0 n’existe pas, mais j’ai une pin A0/DATA_OUT. De retour dans la datasheet, A0/DATA_OUT est décrite comme ceci:

PinNameFunction
3A0/Data OutI2C address in Reset, Data out in SPI mode (VCC domain)

A0 est décrit comme « I2C address », donc A0 et ADDR0 sont deux noms désignant la même pin.

0 ou 1 ?

R83, qui mène à VCC, est noté « NC », « Not Connected », donc à priori, il n’y a aucun composant collé ici. Je peux vérifier que mon schéma dit vrai directement sur la carte:

R86, qui mène à GND, est quant à elle une résistance 0 Ω, équivalent à un fil, donc notre ADDR0 est égal à 0.

Et notre adresse dans le DTS est 0x41, et pas 0x82. Pourquoi ? Car Zephyr RTOS, à l’instar de Linux, utilise les adresses 7 bits pour l’I2C, quand notre datasheet utilise manifestement la version 8 bits. Le LSB de la version 8 bits est toujours un Read/Write. Le driver va en fait utiliser 0x82 pour l’écriture et 0x83 pour la lecture, mais ces valeurs sont sélectionnées automatiquement à partir de la « racine » 7-bit qui est 0x41, 0x82 déplacé d’un bit vers la droite.

Bonus: shell I2C

Si vous voulez vérifier des adresses I2C rapidement, il est possible de scanner le bus avec une commande shell.

Activez le shell et ses options :

CONFIG_SHELL=y
CONFIG_I2C=y
CONFIG_I2C_SHELL=y

game/prj.conf

Une fois connecté :

uart:~$ device list
devices:
[...]
- i2c@40005400 (READY)
uart:~$ i2c scan i2c@40005400
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:             -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- 41 -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                         
1 devices found on i2c@40005400

Niveau de l’interruption

Notez bien, ici c’est « @41 » au sens de « @0x41 » et pas de 41 en décimal, même syntaxe que les device trees en Linux.

Autre détail, notre interruption sur PB5, est-elle active niveau haut ou bas ? La datasheet nous dit qu’elle est drain ouvert, sans mention de polarité.

PinNameFunction
2INTInterrupt output (VCC domain), open drain
Extrait de schéma du SMTPE811

L’électronicien n’a pas oublié d’ajouter une pull-up, par défaut cette pin sera à 1, donc il faudra que logiciellement elle soit active à l’état bas.

Si elle était active à l’état haut, côté stmpe811, vous forceriez à 1 une sortie qui était déjà à 1, et comme nous sommes à drain ouvert, le 0 logique correspondrait à la laisser vivre sa vie… donc elle serait à 1 aussi.

Niveau logique01
Niveau électrique11
Une table de vérité trop nulle

Un peu plus loin, la datasheet nous indique que la polarité est un paramètre à sélectionner dans le registre INT_CTRL (0x09) :

[2] INT_POLARITY: This bit sets the INT pin polarity

1: Active high/rising edge

0: Active low/falling edge

La valeur par défaut de ce registre est 0x00, donc le 3ème bit du registre est 0, donc il me reste à vérifier que le driver ne s’amuse pas à changer cette valeur pour que tout se passe bien.

Dans le driver du STMPE811 :

#define STMPE811_INT_CTRL_REG         0x09U
[...]
#define STMPE811_INT_CTRL_BIT_GLOBAL_INT BIT(0)
[...]
    /* Enable global interrupts */
    err = i2c_reg_write_byte_dt(&config->bus, 
                  STMPE811_INT_CTRL_REG,
                  STMPE811_INT_CTRL_BIT_GLOBAL_INT);
[...]

zephyrproject/zephyr/drivers/input/input_stmpe811.c

On écrit « BIT(0) », c’est-à-dire 1, dans le registre, et le reste des bits reste à 0, dont notre fameux 3ème bit. Le driver est d’accord pour être actif à l’état bas.

Pour demander à ce que l’on compile les drivers d’entrée :

CONFIG_INPUT=y

game/prj.conf

Bouton 4 directions + 1 clic

Il ne s’agit pas d’un joystick puisqu’il n’est pas analogique, mais d’un astucieux petit bouton poussoir avec 4 directions et un clic vertical.

Schéma côté composant
Schéma côté STM32 pas de top-level, c’est la même page
        buttons: gpio_keys {
                compatible = "gpio-keys";
                sel_button: button_2 {
                        label = "Sel";
                        gpios = <&gpioe 8 GPIO_ACTIVE_LOW>;
                };
                left_button: button_3 {
                        label = "Left";
                        gpios = <&gpioe 9 GPIO_ACTIVE_LOW>;
                };
                right_button: button_4 {
                        label = "Right";
                        gpios = <&gpioe 11 GPIO_ACTIVE_LOW>;
                };
                up_button: button_5 {
                        label = "Up";
                        gpios = <&gpioe 10 GPIO_ACTIVE_LOW>;
                };
                down_button: button_6 {
                        label = "Down";
                        gpios = <&gpioe 12 GPIO_ACTIVE_LOW>;
                };
        };

boards/st25dv_lvgl.overlay

Attendez, PE11 est JOY_UP sur le schéma et « right » dans le DTS, tandis que PE10 est JOY_RIGHT sur le schéma et « up » dans le DTS ? Non je n’ai pas fait d’erreur, l’erreur est dans la saisie de composant, dont voici la datasheet. Si on décide que la pin A est « LEFT », puisque nous sommes libres de placer ce composant comme nous voulons, alors l’ordre des pins dans le sens horaire est LEFT, UP, RIGHT, DOWN, ce qui correspond à A(1), B(4), D(6) et C(3).

Composant

Il est possible d’indiquer à Zephyr quels codes sont associés à quelles touches, avec zephyr,code.

#include <zephyr/dt-bindings/input/input-event-codes.h>
[...]
                left_button: button_3 {
                        label = "Left";
                        gpios = <&gpioe 9 GPIO_ACTIVE_LOW>;
                        zephyr,code = <INPUT_KEY_LEFT>;
                };

boards/arm/st25dv_mb1283_disco/st25dv_mb1283_disco.dts

Ces codes sont peu ou prou les mêmes que sous Linux, si vous faites un « evtest », vous aurez code 103 (KEY_UP) et code 108 (KEY_DOWN), en décimal.

Dans input-event-codes.h :

#define INPUT_KEY_LEFT 105 /**< Left Key */
#define INPUT_KEY_DOWN 108 /**< Down Key */

zephyrproject/zephyr/include/zephyr/dt-bindings/input/input-event-codes.h

Pour que les drivers adéquats soient compilés, on n’oublie pas le prj.conf :

CONFIG_GPIO=y

game/prj.conf

Bonus: shell GPIO

Le shell est d’une grande aide pour débogguer les GPIOs.

Activez le shell et ses options :

CONFIG_SHELL=y
CONFIG_GPIO=y
CONFIG_GPIO_SHELL=y

game/prj.conf

Une fois connecté :

uart:~$ gpio info 
Line         Reserved Device           Pin
                         gpio@40020000     0
                         gpio@40020000     1
                         gpio@40020000     2
                         gpio@40020000     3
[...]

Je ne connais pas les adresses de registre par coeur, mais l’information est rapide à retrouver dans la datasheet ou dans le fichier DTS pré-buildé, zephyr.dts.pre:

   gpioc: gpio@40020800 {
    compatible = "st,stm32-gpio";
    gpio-controller;
    #gpio-cells = <2>;
    reg = <0x40020800 0x400>;
    clocks = <&rcc 0x030 0x00000004>;
   };  

build/zephyr/zephyr.dts.pre

Je veux vérifier que j’ai la bonne GPIO et le bon niveau pour mon bouton bleu, à savoir :

gpios = <&gpioc 14 GPIO_ACTIVE_LOW>;

boards/arm/st25dv_mb1283_disco/st25dv_mb1283_disco.dts

Quand le bouton est relâché :

uart:~$ gpio get gpio@40020800 14
1

Vous connaissez la gymnastique pour appuyer sur le bouton et invoquer la commande shell en même temps :

uart:~$ gpio get gpio@40020800 14
0

Mon bouton est donc correctement paramétré !

Le middleware : LVGL

Plomberie et pseudo-drivers

Une fois tout mon hardware câblé et fonctionnel, j’ai envie de tester LVGL, le module graphique dont tout le monde parle.

LVGL va trouver l’écran tout seul puisque nous avons indiqué un zephyr,display, mais il reste quelques pseudo-drivers à définir pour connecter le système Zephyr au module LVGL.

#include <zephyr/dt-bindings/input/input-event-codes.h>
#include <zephyr/dt-bindings/lvgl/lvgl.h>

/ {
        pointer {
                compatible = "zephyr,lvgl-pointer-input";
                input = <&stmpe811>;
                invert-y;
        };

        keypad {
                compatible = "zephyr,lvgl-keypad-input";
                input = <&buttons>;
                input-codes = <INPUT_KEY_RIGHT INPUT_KEY_LEFT INPUT_KEY_UP INPUT_KEY_DOWN INPUT_KEY_ENTER>;
                lvgl-codes =  <LV_KEY_DOWN LV_KEY_UP LV_KEY_RIGHT LV_KEY_LEFT LV_KEY_ENTER>;
        };
};

boards/st25dv_lvgl.overlay

Remarquez les petites adaptations réalisées : invert-y permet de remettre le pavé tactile dans le même sens que l’écran. Une rotation de 90° dans l’affectation des boutons est également réalisée en remplaçant leurs noms.

En plus du très attendu:

CONFIG_LVGL=y

game/prj.conf

Il y a tout un tas de paramètres à ajouter au prj.conf pour le bon fonctionnement du logiciel, à aller chercher dans la documentation et dans les exemples.

Drivers d’entrées dans LVGL

Par défaut, LVGL sélectionne comme périphérique d’entrée les touchpads et écrans tactiles, contrôlés par le pseudo-driver « zephyr,lvgl-pointer-input ». Les autres périphériques d’entrées doivent être sélectionnés en plus, dans mon cas, mon bouton pseudo-joystick est géré par le driver de claviers, à activer avec :

CONFIG_LV_Z_KEYPAD_INPUT=y

game/prj.conf

Pour tester le tout, un petit exemple capable d’afficher des chiffres et de se déplacer avec le bouton directionnel : zephyrproject/zephyr/samples/subsys/display/lvgl

Et maintenant, une application

Je suis dorénavant libre d’aller chercher ce que je veux dans toutes les applications LVGL, je repère un projet Github LVGL avec un 2048 dedans.

Quelques copier-collers et adaptations mineures plus tard, le 2048 s’affiche sur mon écran, mais je n’ai qu’un contrôle tactile, et le swipe sur un écran tactile résistif… c’est l’inverse du fun.

Les interruptions des boutons étaient bien lues dans mon système, mais impossible de jumper dans « lv_100ask_2048_event », la gestion des évènements de mon application LVGL. Pourtant la fonction est prête à gérer l’apparition d’un évènement de type appui bouton :

  else if(code == LV_EVENT_KEY)
    {
        game_2048->game_over = game_over(game_2048->matrix);
        if (!game_2048->game_over)
        {
            switch(*((uint8_t *)lv_event_get_param(e)))
            {
                case LV_KEY_UP:
                    success = move_left(&(game_2048->score),           game_2048->matrix);
                    break;

game/src/lv_100ask_2048.c

Il se trouve qu’il manquait à mon code quelques lignes :

    lv_group_t *btn_matrix_group;
    btn_matrix_group = lv_group_create();
    lv_group_add_obj(btn_matrix_group, game_2048->btnm);
    lv_indev_set_group(lvgl_input_get_indev(lvgl_keypad), btn_matrix_group);

game/src/lv_100ask_2048.c

Ces lignes créent un groupe de widgets, y ajoutent le widget principal du 2048 (game_2048->btnm), et lient le groupe ainsi créé avec mon périphérique d’entrée, lvgl_keypad. Et là ça marche bien mieux, puisque l’évènement de clic bouton est transmis par le groupe au gestionnaire d’évènements.

Conclusion

J’aurais préféré un Tetris…. mais j’ai appris de nouvelles technologies, j’ai un 2048 portatif, j’ai eu l’occasion d’aller harceler la communauté Zephyr avec mes problèmes improbables (merci à eux !), j’ai gagné mon badge de contributeur•ice, et j’ai une justification pour garder des électroniques obsolètes dans mon tiroir : c’était un beau projet.

À retrouver sur https://github.com/everedero/zephyr_lvgl_2048_st25dv

Mise à jour : la carte a été mergée dans le repo principal, vous pouvez dorénavant utiliser « st25dv_mb1283_disco » !