MicroPython - Polices Truetype

pour afficheurs Oled, LCD et TFT

Hervé Troadec

MicroPython implémente la classe FrameBuffer pour gérer l’affichage sur les écrans matriciels tels que les écrans OLED, LCD ou TFT. Cette classe fournit, entre autres, une fonction destinée à afficher du texte sur ces écrans.

Pour afficher du texte, cette fonction doit disposer d’une fonte au format bitmap. FrameBuffer fournit donc une fonte de ce type. Cette fonte est à chasse fixe et chaque caractère est dessiné dans une matrice de 8×8 pixels. Si, compte tenu des dimensions courantes des écrans, elle permet un remplissage optimal de la zone d’affichage, elle présente malgré tout trois inconvénients majeurs :

- sa taille est insuffisante pour assurer un confort de lecture correct ;

- elle n’est actuellement pas modifiable, et rien ne laisse penser qu’elle le sera dans un avenir proche ;

- elle ne dispose que du sous-ensemble imprimable des caractères ASCII, donc codés sur 7 bits, s’étendant de 32 à 126, ce qui interdit notamment l’affichage des signes diacritiques utilisés dans la langue française.

Dans ce tutoriel, nous allons voir comment générer des fontes bitmap à partir des fontes Truetype vectorielles et comment les utiliser pour réaliser un affichage digne de ce nom, dans les limites, bien sûr, des contraintes liées au matériel.

Nous verrons également comment optimiser l’utilisation de la RAM en pré-compilant les fichiers.

Commentez

- Titre : MicroPython - Polices Truetype

- Auteur : Hervé Troadec

- Parution : 20 juillet 2026

- Public visé :

- Licence : Le contenu de cet article est rédigé par naute et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Avant-propos

- II. Prérequis

- III. Introduction

- IV. Installations

- IV-A. mpy-cross

- IV-B. font_to_py.py

- IV-C. writer.py

- V. font_to_py.py

- V-A. Choix des polices

- V-B. Commande

- V-C. Options

- V-D. Génération des fichiers

- VI. writer.py

- VI-A. Prérequis

- VI-B. Constructeur

- VI-C. set_textpos()

- VI-D. printstring()

- VI-E. height()

- VI-F. stringlen()

- VI-G. set_clip()

- VI-H. tabsize()

- VI-I. Exemple

- VII. mpy-cross

- VII-A. Généralités

- VII-B. Utilisation

- VIII. Application

- IX. Conclusion

- X. Remerciements

I - Avant-propos

Puisque ce tutoriel est essentiellement dédié à la manipulation des polices de caractères, il peut être utile de se mettre d’accord sur le vocabulaire.

On a souvent tendance à confondre police (sous entendu « de caractères ») avec fonte, probablement en raison du fait que la traduction du mot anglais font est le mot français police (toujours sous entendu « de caractères »). Or, en informatique, où l’anglais est omniprésent et où l’on manipule couramment les « polices », on a vite emprunté le raccourci. Eh bien… On n’a pas tout à fait tort.

Le mot police désigne globalement un ensemble de caractères cohérent au niveau du graphisme. Cette police peut posséder plusieurs variantes, en général nommées en anglais, comme regular, italic, bold, mono (chasse fixe), condensed, serif, sans serif, etc. Ces variantes constituent ce que l’on appelle des fontes de cette police, le mot « fonte » découlant directement du procédé de fabrication des caractères typographiques, imprimables ou non, en alliage de plomb.

La taille, exprimée en « points », entrait également en ligne de compte dans la définition d’une fonte, car elle permettait de discriminer deux ensembles de caractères par ailleurs identiques au niveau du graphisme. Techniquement, ce critère n’a aucune signification pour une police vectorielle, police pour laquelle la taille est déterminée par un traitement informatique appliqué au graphisme de base, c’est-à-dire à la fonte.

Donc, sauf erreur, à part pour les polices qui ne possèdent aucune variante, on utilise toujours une fonte particulière d’une police donnée. C’est pourquoi j’utiliserai généralement le mot « fonte », même si dans certains cas, ce mot pourrait sembler inapproprié. Que les puristes me pardonnent.

De plus, ce choix en matière de terminologie me permet d’éviter de parler de « Police python », ce qui, vous en conviendrez, serait hors sujet. Bon ! Ça c’est fait , on n’en parle plus…

II - Prérequis

Ce tutoriel n’est pas un mode d’emploi des microcontrôleurs ni des écrans matriciels, ni, bien sûr, un mode d’emploi de MicroPython. Il est là pour présenter des outils destinés à améliorer les possibilités d’affichage du texte sur les petits écrans, ce qui sous-entend que vous savez déjà afficher du texte. Je pars donc du principe que vous disposez du matériel adéquat, que ce matériel est configuré, notamment que MicroPython est installé, que vous disposez des outils nécessaires au téléversement des fichiers sur votre microcontrôleur et que vous savez vous en servir. Pour les exemples de téléversement, je me servirai de l’outil MicroPython en ligne de commande mpremote.

Comme support matériel, j’utiliserai un NodeMCU (ESP8266EX) et un ESP-WROOM-32 (ESP32), mais tous les microcontrôleurs pour lesquels existe une version de MicroPython et proposant une interface SPI ou I2C sont concernés. Pour ce qui touche à l’affichage, le microcontrôleur ESP8266EX est un des plus mauvais choix, car sa faible capacité en mémoire vive (RAM) le limite drastiquement. Nous verrons donc comment contourner ce problème de mémoire, dans une certaine mesure bien sûr, et c’est la raison de ce choix. Un autre intérêt réside dans le fait que si ça fonctionne avec ce microcontrôleur, ça fonctionnera forcément avec les autres.

L’affichage sera confié à un petit écran OLED monochrome de 128×64 pixels dans sa version I2C, assez couramment utilisé pour le prototypage eu égard à son faible coût et à sa facilité de mise en œuvre. Pour pallier la carence en mémoire vive de l’ESP8266EX, le pilote ssd1306.py, nécessaire à l’interfaçage avec cet écran, est inclus dans la version de MicroPython spécifique à ce microcontrôleur, et vous pourrez donc l’importer directement, mais si vous utilisez un autre microcontrôleur, vous devrez le téléverser préalablement dans son système de fichiers. Ce pilote est fourni dans l’archive accompagnant le tutoriel. Ce tutoriel cible tous les écrans dont les pilotes dérivent la classe FrameBuffer de MicroPython.

Vous trouverez, sauf omission, les adresses officielles de téléchargement des fichiers au fur et à mesure de leur utilisation dans ce tutoriel, mais vous n’avez pas besoin de les télécharger un par un : je regroupe tous les fichiers nécessaires dans une archive à télécharger à la fin du chapitre VIII. Application.

Voici l’écran en question :

Écran OLED 0,96 " 128×64 px version I2C

Du très classique !

Une petite remarque au sujet de cet afficheur. Après avoir laissé tourner en continu l’application fournie en fin de tutoriel pendant quelques mois, j’ai noté une dégradation de l’affichage se manifestant par une diminution irrégulière du contraste entre les pixels allumés et les pixels éteints (voir les photos en fin de chapitre VIII. Application).

J’ignore si ce défaut est récurent sur ce modèle d’afficheur ou s’il n’affecte que l’exemplaire utilisé. Ce tutoriel ne ciblant pas spécifiquement cet afficheur, bien d’autres étant utilisables, je n’ai pas poussé les tests plus loin avec ce qui n’est, somme toute, qu’un support expérimental. Pour une utilisation intermittente ou pour du prototypage, il n’y a pas de problème, mais si l’on souhaite un affichage permanent sur un dispositif utilisé en production, c’est un point à vérifier.

Enfin, Python3 et pip doivent bien entendu être installés sur votre système. En ce qui concerne Python3, c’est très probablement le cas. Quant à pip, au cas où il ne serait pas installé, vous pouvez le faire par exemple comme suit :

user

sudo apt update && sudo apt install python3-pip

Cette ligne de commande assume que vous travaillez sur un système GNU-Linux Debian ou dérivé. Vous devrez l’adapter si vous êtes sur un autre système.

III - Introduction

Nous allons utiliser trois outils :

- font_to_py.py © Peter Hinch : cet outil est chargé de générer, à partir d’une fonte vectorielle Truetype ou Opentype, une fonte bitmap codée dans un fichier Python qui sera utilisé par writer.py pour afficher du texte ;

- writer.py © Peter Hinch : writer est une classe dérivée de la classe FrameBuffer de MicroPython qui implémente les primitives destinées à l’affichage de texte en exploitant les fontes créées avec font_to_py.py ;

- mpy-cross © Damien P. George : outil à utiliser, si besoin, pour pré-compiler les fichiers MicroPython sous forme de bytecode.

Ces trois outils sont fournis gratuitement sous licence MIT et aucun contrat, de quelque nature qu’il soit, ne lie leurs auteurs respectifs à l’utilisateur final. Je propose ci‑dessous, pour fixer les idées, une traduction non certifiée de la licence MIT, mais seule la version officielle en anglais, dont le lien est donné ci-dessus, est opposable :

« Copyright <détenteurs des droits d’auteur>

L’autorisation est accordée par la présente, gratuitement, à toute personne obtenant une copie de ce logiciel et des fichiers de documentation associés (le « Logiciel »), de traiter le Logiciel sans restriction, y compris sans limitation des droits d’utilisation, de copie, de modification, de fusion, de publication, de distribution, de sous-licence, ou de vente de copies du Logiciel, et d’autoriser les personnes à qui le Logiciel est fourni à faire de même, sous réserve des conditions suivantes :

- l’avis de droit d’auteur ci-dessus et cet avis d’autorisation doivent être inclus dans toutes les copies ou parties substantielles du logiciel ;

- le logiciel est fourni « tel quel », sans garantie d’aucune sorte, expresse ou implicite, y compris, mais sans s’y limiter, les garanties de qualité marchande, d’adéquation à un usage particulier et d’absence de contrefaçon. En aucun cas, les auteurs ou les détenteurs des droits d’auteur ne peuvent être tenus responsables d’une quelconque réclamation, d’un quelconque dommage ou d’une quelconque responsabilité, que ce soit dans le cadre d’une action contractuelle, délictuelle ou autre, découlant du logiciel, de son utilisation ou de toute autre opération liée au logiciel. »

une licence peu contraignante donc pour les applications déployées, commerciales ou non, et totalement permissive pour les applications privées.

Ce tutoriel est utilisable tel quel pour toute distribution GNU-Linux basée sur Debian, environnement sous lequel il est écrit. Pour les autres distributions GNU-Linux ou les autres systèmes d’exploitation, il y aura un petit effort d’adaptation à fournir, essentiellement au niveau de la syntaxe de la ligne de commande, mais ça ne devrait pas être insurmontable, Python étant multi-plateforme.

IV - Installations

Je regroupe dans ce chapitre les trois outils vus ci-dessus, mais seul mpy-cross nécessite réellement une installation. Pour writer.py et font_to_py.py, il ne s’agira que d’un simple téléchargement de fichier. À noter toutefois que font_to_py.py nécessite la bibliothèque freetype-py pour fonctionner et qu’il faudra installer celle-ci si ce n’est déjà fait.

Pour les installations qui vont suivre, seules celles réalisées avec apt nécessitent le mode root. Celles réalisées avec pip, et donc pipx qui est basé sur pip, seront effectuées en mode user. Cette pratique est rappelée dans l’entête des zones de code.

Si vous utilisez sudo avant d’invoquer apt, vous devez évidemment rester en mode user.

IV-A - mpy-cross

L’utilisation de pip au lieu de apt sur une distribution Debian est susceptible de provoquer des problèmes, notamment en raison d’une gestion différente des dépendances. Depuis Debian 12, l’installation d’une application Python doit donc se faire au sein d’un environnement virtuel. Si vous avez déjà utilisé un tel environnement, vous connaissez la procédure et vous installerez mpy-cross dans l’environnement virtuel que vous aurez créé pour, après l’avoir activé, en exécutant la ligne :

user

pip install mpy-cross

Si vous n’avez pas configuré d’environnement virtuel, l’installation n’aboutira pas. Pour faire simple, je vous conseille, dans ce cas, d’installer d’abord l’utilitaire pipx en exécutant :

user

sudo apt update && sudo apt install pipx
pipx ensurepath

Cet utilitaire se sert de pip pour l’installation proprement dite, mais crée au préalable un environnement virtuel spécifique à l’application qu’il va installer et à ses dépendances. Il s’utilise exactement comme pip:

user

pipx install mpy-cross

mpy-cross est installé dans le dossier /home/user/.local/bin. Si ce dossier n’existe pas, il sera créé et ajouté au path pour donner l’accès à l’utilitaire depuis un dossier quelconque de votre PC. Pour faire simple, ce dossier nous servira également à stocker font_to_py.py.

IV-B - font_to_py.py

Ce fichier, comme tous les fichiers utilisés dans ce tutoriel, est contenu dans l’archive fournie à la fin du chapitre VIII. Application. Pour avoir une version plus récente, si elle existe, rendez-vous sur cette page puis cliquez sur Download raw file :

Dans ce cas, il sera peut-être nécessaire de s’adapter aux modifications apportées à cette nouvelle version.

Le fichier font_to_py.py est téléchargé dans un dossier qui dépend du paramétrage de votre navigateur, généralement le dossier Téléchargements. Déplacez-le dans le dossier /home/user/.local/bin que nous venons de voir pour le rendre accessible depuis n’importe quel emplacement de votre PC. Vous pouvez également, si vous préférez, créer un dossier spécifique et l’incorporer au path.

Comme vu plus haut, font_to_py.py requiert la bibliothèque freetype-py pour fonctionner. Cette bibliothèque se trouve dans les dépôts Debian et peut être déjà installée sur votre PC, mais ce n’est pas obligatoire. Avant de l’installer, on va vérifier cela. Exécutez l’une des deux lignes suivantes :

user

apt show python3-freetype

ou

user

pip show freetype-py

Si le retour indique qu’aucun paquet n’a été trouvé, il va falloir installer freetype-py. Vous avez deux possibilités :

root

apt install python3-freetype

ou

user

pip install freetype-py

Dans les deux cas, il s’agit bien de la même bibliothèque, à savoir freetype-py, mais installée par deux méthodes différentes.

IV-C - writer.py

Ce fichier, comme tous les fichiers utilisés dans ce tutoriel, est contenu dans l’archive fournie à la fin du chapitre VIII. Application. Pour avoir une version plus récente, s’il y en a une, rendez-vous sur cette page puis cliquez sur Download raw file :

Le fichier writer.py est téléchargé dans un dossier qui dépend du paramétrage de votre navigateur, généralement le dossier Téléchargements. Déplacez-le dans le dossier de votre choix.

Pour utiliser cette bibliothèque, il vous suffit de la téléverser telle quelle dans la mémoire flash du microcontrôleur puis de l’importer dans le script qui doit l’utiliser. Comme il s’agit d’une bibliothèque que vous n’avez pas à modifier, vous pouvez également la compiler avec mpy-cross (voir le chapitre VII. mpy-cross) avant de la téléverser avec les avantages suivants :

- fichier résultant plus petit, donc téléversement plus rapide et occupation mémoire flash plus faible ;

- importation plus rapide et RAM moins impactée.

V - font_to_py.py

font_to_py.py a pour but de générer, à partir d’un fichier TrueType ou OpenType, un fichier Python destiné à être importé dans du code MicroPython pour être utilisé par writer.py, ce qui donne potentiellement accès à toutes les polices TrueType ou OpenType existantes. L’ensemble des caractères Unicode est théoriquement accessible, dans la limite des caractères fournis par la police évidemment. De toute manière, pour tous ceux pour qui, comme pour moi, le chinois c'est de l’hébreu (et réciproquement 😄️), l’utilisation des caractères ASCII étendu imprimables, c’est-à-dire les caractères codés sur 8 bits dont le code décimal appartient à l’intervalle [32..255], devrait suffire. Vous pouvez consulter la page de présentation pour de plus amples renseignements.

V-A - Choix des polices

Pour illustrer cet article et lui donner un fil directeur, on va partir sur un très classique affichage de mesures fournies par un capteur (voir chapitre VIII. Application ). Le choix des polices dépend à la fois du type d’application visé, de la place dont on dispose et des préférences esthétiques du concepteur. C’est toujours un compromis. Dans le contexte que je viens de fixer, par exemple, le choix d’une police cursive ou gothique n’a que peu d’intérêt. On a besoin d’une police plutôt technique avec un graphisme simple facile à lire : n’oublions pas que cette police va être affichée sous forme de bitmap sans antialiasing. Personnellement, j’utilise une police standard sans serif à chasse fixe.

En ce qui concerne la taille, ça dépend de ce que l’on doit afficher. Avec un afficheur à LED, l’affichage classique d’une mesure comprend :

- l’intitulé qui indique le type de mesure effectuée. Exemple : Température. Cette information est généralement sérigraphiée sur le boîtier qui supporte l’afficheur ;

- la valeur numérique de la mesure. Exemple : 18,5 ;

- l’unité dans laquelle la mesure est exprimée. Exemple : °C. Cette information est généralement sérigraphiée sur le boîtier qui supporte l’afficheur.

Ce type de présentation, efficace au demeurant, ne se prête pas à l’évolution. Les sérigraphies sont difficiles à modifier et l’afficheur en lui-même est limité à un certain nombre de digits.

Avec un afficheur matriciel, on peut aller plus loin :

- si on décide, pour des raisons de place et de lisibilité, de n’afficher que la donnée numérique, on ne se pose pas de question : on utilise une seule fonte dans la plus grande taille possible ;

- sinon, on peut bien entendu n’utiliser également qu’une seule fonte pour afficher le tout, mais dans ce cas, on sera obligé d’opter pour une taille relativement faible afin que tout tienne dans l’écran, au détriment de la lisibilité ;

- comme l’important reste malgré tout la valeur numérique affichée, on peut envisager une grande taille pour l’affichage de la mesure elle-même et une taille plus modeste pour l’intitulé et l’unité de mesure ;

- on peut également envisager de n’afficher que l’intitulé ou que l’unité, dans la mesure où il y a redondance d’information ;

- on peut également alterner l’affichage des différentes informations.

En tout état de cause, ça reste une affaire de choix, et chacun fera le sien. Pour le tutoriel, on va tout afficher, mais en privilégiant l’affichage de la mesure en elle-même.

Partant de là, il nous faut au moins une police dans au moins deux fontes de tailles différentes. Le plus simple consiste à utiliser l’une des polices installées sur votre ordinateur. Sous Debian, par exemple, elles se trouvent dans les dossiers /usr/share/fonts/truetype/ et /usr/share/fonts/opentype/. Elles sont toutes libres d’usage dans le cadre privé, mais en dehors de ce cadre, il est préférable de vérifier les licences qui les accompagnent. Vous pouvez également en télécharger depuis des sites web. Une fonte comme LiberationMono-Regular fera parfaitement l’affaire. Elle est mise à disposition sous licence GNU-GPLv2 modifiée pour les polices de caractères par cette exception. LiberationMono-Regular est installée d’origine sur Debian, mais comme ce n’est pas forcément le cas pour toutes les distributions, je vous ai mis un lien à tout hasard si vous voulez la télécharger. Elle est également fournie dans l’archive téléchargeable en fin de tutoriel. J’ai fait le choix d’une fonte à chasse fixe car cela permet de déterminer simplement la longueur d’un mot et donc, entre autres, de savoir au moment du codage s’il tiendra dans l’espace qui lui est attribué. Ça limite les modifications à posteriori.

Bien que cette police suffise à nos besoins, j’aime bien, pour la présentation, afficher les valeurs numériques avec une police de type 7 segments. Ça rappelle les afficheurs à LED qui ont eu leur heure de gloire, qui sont d’ailleurs toujours utilisés en raison de leur facilité de mise en œuvre et de leur lisibilité, mais qui n’offrent pas, bien entendu, la même souplesse d’affichage que les écrans matriciels comme je l’ai souligné plus haut.

Je n’ai pas trouvé, ce qui ne veut pas dire qu’il n’y en a pas, de police de ce type vraiment libre de droits : le plus simple a été d’en bricoler une que j’ai appelée led_7_digits.ttf. Vous la trouverez dans l’archive téléchargeable en fin de tutoriel. C’est une police TrueType sans baseline à chasse presque fixe (voir note ci-dessous) qui ne propose qu’une seule fonte (regular) et qui ne contient que les caractères nécessaires à l’affichage de données numériques. Si ce minimum syndical vous convient et que vous en avez l’utilité, vous pouvez bien entendu vous en servir gratuitement sans aucune restriction ni obligation dans vos applications, que ce soit sous sa forme vectorielle ou sous sa forme matricielle, sans garantie d’aucune sorte, bien entendu, comme d’habitude.

Puisqu’en France on ne peut rien placer de son propre chef dans le domaine public, j’ai opté pour ©naute 2024 sous licence CC0-1.0, assez proche de la licence GNU-GPLv3 modifiée et qui permet en gros de faire tout ce que l’on veut avec cette police sauf de la rendre non libre (c’est le but de la protection). À noter que le copyright n’a aucune valeur juridique en France où l’on utilise plutôt les notions de droit d’auteur ou de propriété intellectuelle. Voici un aperçu de son rendu :

Chasse presque fixe ne veut rien dire, bien sûr, mais c’est seulement pour signaler que, si tous les chiffres ont la même largeur, j’ai affecté une largeur plus petite aux caractères « . » et « : » à la fois dans un but esthétique et de gain de place. Cela complique un peu le calcul de la taille de la fonte à afficher, mais permet de l’optimiser pour qu’elle soit la plus grande possible en fonction de l’espace disponible.

De plus, j’ai opté pour le point (.) comme séparateur décimal en lieu et place de la virgule, car ma police sans « base line » ne peut rien afficher en dessous de la ligne de base des caractères (voir la définition de « baseline » dans le chapitre suivant).

L’aperçu ci-dessus n’est donné qu’à titre indicatif et résulte d’une capture d’écran réalisée sur mon système. Comme cette police n’est, à l’évidence, pas (encore) installée sur votre propre système, je n’ai pas la possibilité de vous en offrir une représentation vraiment fidèle à l’original.

V-B - Commande

font_to_py.py n’implémente qu’une fonction, qui accepte toutefois plusieurs paramètres. La ligne de commande minimale pour générer une fonte nécessite trois arguments : le fichier d’entrée, la hauteur souhaitée pour les caractères, en pixels, et le fichier de sortie. Elle est de la forme :

user

font_to_py.py fonte_1.ttf hauteur fonte_2.py

ou

user

font_to_py.py fonte_1.otf hauteur fonte_2.py

Cette ligne va générer le fichier fonte_2.py, et un résumé de l’opération va s’afficher. fonte_1 et fonte_2 représentent symboliquement le chemin vers le fichier orignal et le fichier généré et peuvent être identiques, mais les extensions doivent être respectées.

La fonte générée n’est plus vectorielle, mais matricielle, et l’utilisateur décide de la hauteur en pixels de la fonte qu’il veut créer. Un algorithme détermine alors, en plusieurs passes s’il le faut, la largeur permettant de respecter au mieux les proportions des caractères originaux et peut être amené, dans ce but, à modifier légèrement la hauteur demandée. D’après mes essais, cette modification semble ne pas excéder deux pixels. Si cette modification, occasionnelle, je le souligne, ne convient pas à votre projet, vous devrez répéter l’opération en modifiant les paramètres de votre demande, jusqu’à obtenir le résultat escompté, ou tout au moins un résultat satisfaisant.

Par exemple, la commande suivante va créer le fichier LiberationMono-Regular.py dans le répertoire courant à partir du fichier LiberationMono-Regular.ttf présent dans le même répertoire. L’afficheur utilisé ayant une définition verticale de 64 pixels, paramétrer 16 pixels pour la hauteur des caractères assurera l’affichage de 4 lignes avec une lisibilité convenable et un remplissage optimal :

user

font_to_py.py LiberationMono-Regular.ttf 16 LiberationMono-Regular.py

et en image :

Le résumé indique que tout s’est bien passé et que la fonte tient dans une matrice de 16✕10 px (la hauteur demandée a été respectée). Étant donné les 128 px de large de l’afficheur, on peut « caser » 12 caractères, ce qui autorise potentiellement l’affichage de 48 caractères. Si c’est insuffisant pour une liseuse, ça permet malgré tout de travailler. Dans l’histoire, 512 px seront « gâchés », soit 6,25 % de la surface d’affichage. Même si ce n’est pas parfait, cela reste acceptable.

Dans l’exemple ci-dessus, nous assumons le fait que font_to_py.py est dans le path ou dans le répertoire courant, que LiberationMono_Regular.ttf est dans le répertoire courant et que LiberationMono_Regular.py sera généré dans le répertoire courant. Il faudra bien entendu adapter cette ligne de commande au contexte.

En plus de la génération d’une fonte bitmap à partir d’une fonte vectorielle, font_to_py.py implémente neuf fonctions qui pourront être appelées lors de l’utilisation de writer.py. Parmi ces fonctions, cinq seront particulièrement utiles (voir la page de référence pour de plus amples informations) :

1. height() : retourne la hauteur des caractères en pixels. Quand on parle de la hauteur du caractère, on parle de la hauteur de la matrice qui contient le caractère. Cette hauteur est plus grande que la hauteur réelle du plus grand caractère pour éviter que les caractères de deux lignes successives ne se touchent ;

2. max_width() : retourne la largeur en pixels du caractère le plus large. Il s’agit ici aussi de la largeur de la matrice qui est plus grande que la largeur réelle du plus grand caractère pour éviter que deux caractères successifs ne se touchent. Dans le cas de chasse fixe, tous les caractères ont la même largeur, naturellement ;

3. baseline() : retourne la hauteur du caractère depuis sa ligne de base (baseline). Cette donnée est utile quand on veut aligner deux caractères de tailles différentes. Un petit dessin vaut mieux qu’un long discours :

4. monospaced() : retourne True si la police est à pas fixe. Utile pour la mise en page ;

5. get_ch(arg) : retourne un tupple de trois items dont width (largeur du caractère passé en argument). Utile pour la mise en page quand monospaced() retourne False.

Sur l’illustration, pour des raisons de cohérence, baseline représente la hauteur renvoyée par la fonction éponyme. Techniquement, baseline est le nom de la ligne horizontale rouge.

L’utilisation de la ligne de commande minimale fonctionne parfaitement mais présente deux inconvénients :

- la police générée ne contient par défaut que les caractères dont les codes ASCII sont compris entre 32 et 126 inclus, codés sur 7 bits. Si c’est suffisant pour un anglo-saxon, c’est un peu juste pour nous autres Français qui utilisons des diacritiques ;

- pour les fontes dont on sait qu’on n’utilisera qu’un sous-ensemble réduit de caractères, il est inutile de s’encombrer de l’ensemble complet : n’oublions pas que la RAM est comptée sur les microcontrôleurs et qu’il est préférable de l’économiser.

Pas d’inquiétude : c’est prévu !

V-C - Options

Outre les arguments obligatoires que nous venons de voir, la ligne de commande accepte des arguments optionnels qui vont permettre d’adapter au mieux le fichier de sortie à nos besoins. Tous les arguments optionnels ne seront pas traités ici, mais vous pouvez vous rendre sur la page de référence pour de plus amples informations.

1. -f ou --fixed : permet de générer une police à pas fixe à partir d’une police à pas variable. Si c’est utile quand la police à pas fixe n’existe pas, cette fonction donne malgré tout de moins bons résultats que quand on utilise directement une police à pas fixe. Il est donc préférable de ne l’utiliser que lorsque la police à pas fixe n’est pas disponible ;

2. -s ou --smallest : permet de fixer la valeur ordinale inférieure à partir de laquelle commencera la génération. Cette valeur, appartenant obligatoirement à l’intervalle [0..1114111] représentant l’ensemble des caractères Unicode codés sur 4, 5 ou 6 octets (codage UTF-8, intervalle [0000h..10FFFFh]), est fixée à 32 par défaut. Combinée à la valeur supérieure, elle doit déterminer un ensemble non vide de caractères : s ≤ l ;

3. -l ou --largest : permet de fixer la valeur ordinale supérieure à laquelle s’arrêtera la génération. Cette valeur, appartenant obligatoirement au même intervalle que ci-dessus, est fixée par défaut à 126. Combinée à la valeur inférieure, elle doit déterminer un ensemble non vide de caractères : l ≥ s ;

4. -e ou --errchar : fixe le caractère générique qui sera affiché en cas d’appel d’un caractère qui n’est pas contenu dans le set en cours. Le caractère générique par défaut est ? (point d’interrogation). Il faut bien entendu que le caractère générique choisi fasse partie du set en cours. Si, par exemple, vous utilisez le set ainsi que le caractère générique par défaut et que vous voulez afficher le mot « Température », le mot qui s'affichera sera Temp?rature ;

5. -c ou --charset : permet de limiter la génération de caractères à un ensemble saisi explicitement dans la ligne de commande. Si, par exemple, vous utilisez une fonte 7 segments pour n’afficher que des nombres, vous n’aurez besoin que des chiffres de 0 à 9 et, éventuellement, des caractères : ou . comme séparateurs horaire ou décimal. Toujours dans l’optique d’économiser la mémoire, il est inutile de générer tous les caractères ;

6. -k ou --carset_file : permet de fournir l’ensemble des caractères à générer sous la forme d’un fichier texte. Cet argument permet de faire la même chose que l’argument -c mais évite d’avoir à saisir à chaque fois la même séquence de caractères à générer. Si ce n’est pas gênant pour une dizaine de caractères, ça devient un problème s’il y en a une centaine, voire plus. On sauvegarde donc une fois pour toutes cette séquence dans un fichier et on donne ce fichier comme argument. On peut ainsi créer autant de fichiers que l’on veut pour couvrir des besoins différents.

V-D - Génération des fichiers

Pour l’application proposée au chapitre VIII. Application, il va nous falloir trois fontes py : une basée sur la fonte led_7_digits.ttf et deux basées sur la fonte LiberationMono-Regular.ttf.

Pour la fonte led_7_digits.ttf, nous n’utiliserons que 12 caractères (de toute manière, elle n’en propose pas d’autres). L’option -c est suffisante dans ce cas, car la saisie explicite de ces 12 caractères est rapide et il y a peu de chance d’en oublier un. Voici la commande correspondante :

font_to_py.py led_7_digits.ttf 35 digits_35.py -c 0123456789.:

- le choix de la hauteur de la fonte créée, 35 ici, dépend uniquement du résultat que l’on veut obtenir au niveau de l’affichage et nécessite quelques essais pour arriver au bon compromis ;

- le nom de la fonte créée est libre, mais à l’usage, on s’aperçoit rapidement que le rappel de la fonte d’origine et de la taille choisie sont des renseignements utiles, d’où le choix digits_35.py ;

- l’option -c 0123456789.: indique explicitement les caractères qui vont être générés.

À partir de la fonte LiberationMono-Regular.ttf, nous allons créer deux fontes py : l’une dédiée à l’affichage du libellé du type de mesure effectué, l’autre dédiée à l’affichage de l’unité de mesure correspondante. Elles différeront par le set de caractères utilisé et la taille choisie pour ces caractères.

Contrairement au cas précédent, la saisie explicite des caractères n’est pas pratique : d’une part, elle prend du temps et expose à des oublis, et d’autre part elle n’est pas réutilisable. Si, par exemple, on veut créer plusieurs fontes avec le même set de caractères, il faudra tout ressaisir à chaque fois. On peut bien entendu faire un copier-coller, mais il y a une solution plus pérenne : elle consiste à créer deux fichiers « texte » contenant les caractères voulus.

Pour le libellé, il nous faut tous les caractères alphanumériques (pour être sûr) et le caractère – (tiret) pour les mots composés. Ces caractères seront regroupés dans le fichier alphanum.

alphanum

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzàâäçéèêëîïôöùûüÿæœÀÂÄÇÉÈÊËÎÏÔÖÙÛÜŸÆŒ-

Pour les unités de mesure, j’ai regroupé dans le fichier usysint (UnitésSYStèmeINTernational) les caractères utilisés dans le système international d’unités en vigueur sur (presque*) toute la planète (*exception faite des États-Unis, du Liberia et de la Birmanie : voir ici), en espérant ne pas en avoir oublié.

usysint

abcdfghklmnopqrstvxyz ABCFGHJKMNPSTVW/°Ωµ%

Nous pouvons maintenant créer nos deux polices :

liberation_alphanum_16.py

font_to_py.py LiberationMono-Regular.ttf 16 liberation_alphanum_16.py -k alphanum

et

liberation_usysint_16.py

font_to_py.py LiberationMono-Regular.ttf 20 liberation_usysint_20.py -k usysint

Pour les raisons évoquées plus haut, le nom du fichier généré comporte le rappel du set de caractères utilisé pour sa création et la hauteur de caractère demandée.

En cas de manque drastique de RAM, vous pouvez limiter la génération des fontes aux caractères réellement utilisés, par exemple ici :

- %°C pour l’affichage des unités de mesure ;

- aeégHimoprtTuy pour le libellé du type de mesure affiché (« Température » et « Hygrométrie »).

VI - writer.py

writer.py implémente deux classes :

- Writer qui prend en charge le rendu des polices monochromes bitmap créées par font_to_py.py ;

- Cwriter qui étend Writer pour la prise en charge du rendu des couleurs.

Ne disposant pas d’afficheur couleur pour effectuer les tests, je ne commente que la classe Writer. Rendez-vous sur cette page pour en savoir plus sur les spécificités de la classe Cwriter.

Caractéristiques principales de Writer :

- fonctionne avec tous les pilotes utilisant la classe framebuffer de MicroPython ;

- fonctionne avec plusieurs instances d’écrans ;

- affiche le texte avec des polices à chasse fixe ou variable avec défilement vertical automatique ;

- gestion des débordements par écrêtage ou retour à la ligne au caractère/mot paramétrable ;

- support (minimaliste) de la tabulation ;

- mesure de la longueur des chaînes pour faciliter la justification ou le centrage ;

- inversion des couleurs d’affichage fond/texte.

VI-A - Prérequis

Pour utiliser writer, vous devez l’importer dans votre script. Le fichier writer.py doit donc être présent dans le système de fichiers de votre microcontrôleur. Pour le téléverser, exécutez la ligne suivante (ou l’équivalent avec votre utilitaire de téléversement) en adaptant les chemins si besoin :

mpremote u0 cp writer.py :

ce qui nous donne :

Cette manipulation sera à effectuer pour tous les fichiers nécessaires, à savoir les pilotes et les fontes : comme la seule différence sera le nom du fichier, je ne détaillerai pas les autres téléversements.

Vous devez ensuite créer une instance de writer et, pour cela, vous devez fournir :

- une instance de l’afficheur utilisé : dans notre cas, le pilote ssd1306.py doit donc soit être présent dans votre version de MicroPython, soit avoir été préalablement téléversé dans le système de fichiers de votre microcontrôleur, pour pouvoir, après importation, créer cette instance :

from machine import Pin, SoftI2C
from ssd1306 import SSD1306_I2C

# Initialisation du bus I2C pour un ESP32
i2c = SoftI2C(scl=Pin(22), sda=Pin(21))

# Dimensions de l’afficheur stockées en mémoire flash
WIDTH = const(128)
HEIGHT = const(64)

# Création d’une instance de l’afficheur
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)

- une police bitmap, par exemple LiberationMono_Regular.py que nous avons créée plus haut avec l’outil font_to_py.py, et qui aura été téléversée dans le système de fichiers de votre microcontrôleur.

VI-B - Constructeur

À une exception près, les méthodes fournies par writer nécessitent une instance de classe et nous avons donc besoin d’un constructeur. Il prend les arguments suivants :

- device : une instance de l'afficheur utilisé ;

- font : la police qui va être utilisée pour la prochaine écriture. Vous pouvez définir autant d’instances de writer que nécessaire avec des polices différentes pour les besoins de l'affichage, dans la limite de la mémoire disponible ;

- verbose : True par défaut. Affiche dans la console un résumé des arguments déjà définis.

Par exemple, maintenant que nous avons tous les éléments, à savoir un afficheur (oled) et une police (liberationMono_Regular.py), nous pouvons créer une instance de Writer :

import LiberationMono_Regular as lib
from writer import Writer

# Création d’une instance de "Writer"
w_lib = Writer(oled, lib, False)

VI-C - set_textpos()

Cette méthode est l’exception à laquelle j’ai fait allusion ci-dessus. Elle est statique et n’est donc pas liée à une instance de classe. Il faut par conséquent lui fournir en argument l’instance sur laquelle elle doit agir.

set_textpos(device, row=None, col=None) fixe le point d’insertion du prochain texte affiché quelle que soit l’instance utilisée, et donc, quelle que soit la police. Elle reçoit trois arguments :

- device : l’instance de l’afficheur utilisé ;

- row : position verticale en pixels, l’origine étant en haut de l’afficheur. La valeur par défaut est zéro (0). Si None est passé en argument, la valeur précédemment assignée reste inchangée ;

- col : position horizontale en pixels, l’origine étant à gauche de l’afficheur. La valeur par défaut est zéro (0). Si None est passé en argument, la valeur précédemment assignée reste inchangée.

Exemple :

# Affichage du prochain caractère aux coordonnées "(10,20)"
set_textpos(oled, 10, 20)

Quelques précisions :

- la méthode attend les paramètres de positionnement row et col dans cet ordre précis, ce qui revient à fournir les coordonnées (-y,x). Ceci peut sembler contre-intuitif dans un contexte orthonormé si on considère la matrice d’affichage de l’écran comme tel, mais ça devient cohérent si on considère l’affichage de caractères comme dans un traitement de texte. writer étant dédié à l’affichage de texte, ce choix est logique ;

- le point de référence pour le positionnement du caractère est le pixel supérieur gauche représenté en rouge dans le schéma suivant :

- une fois le caractère dessiné, le point d’insertion du caractère suivant est automatiquement réévalué en fonction de la place disponible sur la même ligne pour le nouveau caractère et des paramètres fournis à set_clip(). Avec les paramètres par défaut, si la place est suffisante, le nouveau point d’insertion se trouve sur la même ligne que l’ancien et décalé de la largeur du caractère précédent, ce qui fait que le nouveau caractère est dessiné à la suite du premier. S’il n’y a pas assez de place pour dessiner le nouveau caractère, le nouveau point d’insertion sera positionné en début de ligne (x=0 ou col=0), mais sera décalé de la hauteur du caractère précédent. Attention toutefois : si vous changez de police entre deux caractères, le résultat obtenu risque fort de ne pas vous convenir. Vous devrez alors soit gérer les alignements dans le code, en utilisant notamment height() et baseline(), si l’affichage est dynamique, soit déterminer « à la main » les valeurs à coder en dur si l’affichage est statique.

VI-D - printstring()

printstring(string, invert=False) affiche la chaîne fournie en argument à la position définie par set_textpos() :

- string : chaîne à afficher ;

- invert : False par défaut affiche les caractères en blanc sur fond noir. Si True, les caractères sont affichés en noir sur fond blanc.

Exemple :

# Affiche "Bonjour !" en blanc sur fond noir
w_lib.printstring('Bonjour !')

# Affiche "Bonjour !" en noir sur fond blanc
w_lib.printstring('Bonjour !', True)

VI-E - height()

height() retourne la hauteur des caractères de la police en cours. Peut servir à calculer une position verticale pour des besoins de mise en page.

Exemple :

# Dans notre contexte, l’instruction suivante stockera "16" dans la variable "hauteur_car"
hauteur_car = w_lib.height()

VI-F - stringlen()

stringlen(string, oh=False) retourne la longueur de la chaîne en pixels si oh = False (valeur par défaut). À utiliser notamment pour centrer ou justifier du texte :

- string : chaîne dont on veut connaître la longueur ;

- oh : argument à usage interne. Quand oh = True, la méthode renvoie un booléen :

- True dans le cas où la chaîne dépasserait la limite de l’afficheur si elle était positionnée au point d’insertion en cours,

- False dans le cas contraire.

Exemple :

# Dans notre contexte, après exécution de l’instruction suivante,   vaudra .
longueur = w_lib.stringlen('Bonjour !')

VI-G - set_clip()

set_clip(row_clip=None, col_clip=None, wrap=None) gère les débordements :

- row_clip (False par défaut) :

- True : si un caractère empiète sur la limite inférieure de l’afficheur, il ne sera pas affiché,

- False : si un caractère empiète sur la limite inférieure de l’afficheur, le texte déjà affiché défilera vers le haut du nombre de pixels nécessaires pour que ce caractère soit affiché entièrement,

- None : pas de modification de la valeur précédemment attribuée ;

- col_clip (False par défaut) :

- True : si un caractère empiète sur la limite droite de l’afficheur, il ne sera pas affiché,

- False : si un caractère empiète sur la limite droite de l’afficheur, il sera affiché sur la ligne suivante,

- None : pas de modification de la valeur précédemment attribuée ;

- wrap (True par défaut):

- True : si un mot empiète sur la limite droite de l’afficheur, il sera affiché sur la ligne suivante,

- False : si un mot empiète sur la limite droite de l’afficheur, il sera tronqué,

- None : pas de modification de la valeur précédemment attribuée.

Exemple :

# L’instruction suivante active le retour à la ligne et le défilement vertical
w_lib.set_clip(False, False, True)

# L’instruction suivante affecte les paramètres actuels aux variables "a", "b" et "c" (tupple)
a, b, c = w_lib.set_clip()

VI-H - tabsize()

tabsize(value=None) fixe la largeur de la tabulation en nombre d’espaces dans la police en cours. Si le paramètre saisi est un nombre entier, celui-ci représente le nombre d’espaces (4 par défaut). Si None ou pas d’argument, la méthode renvoie la valeur actuelle de la tabulation.

Exemple :

# La ligne suivante fixe la tabulation à deux espaces
w_lib.tabsize(2)

# Après exécution d’une des instructions suivantes, "tab" vaudra "2"
tab = w_lib.tabsize()
# ou
tab = w_lib.tabsize(None)

Cette fonction, bien qu’à priori de peu d’intérêt pratique sur notre petit écran, peut trouver à s’exprimer sur des écrans plus grands, notamment pour la réalisation de menus.

VI-I - Exemple

Je vous propose le code ci-dessous afin de vérifier que vos installations se sont bien passées.

Ce code fonctionnera indifféremment avec les microcontrôleurs ESP8266EX ou ESP32 en optant pour l’instanciation correspondant à votre matériel, c’est-à-dire en commentant la ligne inutile entre les deux lignes suivantes :

i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000) #ESP32
i2c = I2C(scl=Pin(5), sda=Pin(4)) # ESP8266EX

Le choix entre ces deux polices se fera en suivant la même méthode que ci-dessus, à savoir commenter l’une des deux lignes suivantes :

import digits_35 as dig35_50
import digits_50 as dig35_50

Les deux polices étant importées sous le même nom, dig35_50, il n’y aura pas d’autre modification à effectuer dans le code.

L’importation des polices sous le même nom permet, en basculant simplement de l’une à l’autre, de mettre en évidence l’intérêt de l’utilisation des fonctions max_width() et height() fournies par la police bitmap plutôt que de coder les valeurs correspondantes en dur. Quelle que soit la police utilisée, dans les limites permises par le matériel, l’affichage sera centré correctement sans qu’il y ait de modification à effectuer dans le code.

exemple.py

1. # Ce code est utilisable sur les microcontrôleurs de type ESP8266 et ESP32.

2. # Seules les entrées/sorties GPIO diffèrent, et il vous suffira de commenter

3. # la ligne inutile. Vous pourrez également choisir la police à utiliser en

4. # commentant la ligne que vous n'utiliserez pas.

6. from machine import Pin, I2C

7. import time

8. from ssd1306 import SSD1306_I2C

9. from writer import Writer

10. #import digits_35 as dig35_50

11. import digits_50 as dig35_50

13. #i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)

14. i2c = I2C(scl=Pin(5), sda=Pin(4))

16. WIDTH = const(128)

17. HEIGHT = const(64)

18. oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)

20. w = Writer(oled, dig35_50)

22. largeur = dig35_50.max_width()  # Utilisation de deux des fonctions fournies par

23. hauteur = dig35_50.height()     # la police bitmap

24. ligne = (HEIGHT - hauteur) // 2

25. colonne = (WIDTH - largeur) // 2

27. for i in range(9, -1, -1):

28. oled.fill(0)

29. Writer.set_textpos(oled, ligne, colonne)

30. w.printstring(str(i))

31. oled.show()

32. time.sleep(0.5)

34. while True:  # Clignotement

35. oled.contrast(0)

36. time.sleep(0.5)

37. oled.contrast(255)

38. time.sleep(0.5)

Le script ci-dessus fonctionnera aussi bien sur un ESP8266EX que sur un ESP32. Par contre, pour l’utiliser sur un ESP8266EX, vous devrez obligatoirement importer une version précompilée (mpy) de writer : le peu de mémoire vive de ce microcontrôleur ne permet même pas la simple importation de writer et ne laisse donc pas le choix.

L’exécution de la ligne :

from writer import Writer

déclenche l’exception suivante :

pyboard.PyboardError: exception
In file "", line 1, \r\nMemoryError: memory a\r\n')

Le chapitre suivant détaille la procédure à suivre pour résoudre simplement ce problème.

Le fichier exemple.py ainsi que les polices digits_35 et digits_50 se trouvent dans l’archive proposée au téléchargement dans le chapitre VIII. Application.

VII - mpy-cross

VII-A - Généralités

mpy-cross est un utilitaire destiné à pré-compiler un fichier MicroPython avant de l’utiliser sur le microcontrôleur (ou ailleurs). L’intérêt de cette pratique est essentiellement de diminuer la charge de travail de l’interpréteur MicroPython, et, du même coup, d’économiser la RAM, limitée sur certains matériels. Si ce n’est pas crucial avec un ESP32, par exemple, ça l’est pour un ESP8266. Donc, en fonction du microcontrôleur que vous utilisez, et de votre application bien entendu, vous pourrez ou non vous passer de cet outil.

Un autre avantage du fichier mpy réside dans le fait qu’il est nettement plus petit que le fichier py dont il est issu. Compte tenu du type d’application développé pour les microcontrôleurs ciblés, la mémoire flash qui équipe ce type de matériel est généralement largement suffisante pour les scripts MicroPython, mais pour des applications susceptibles d’utiliser des images, par exemple, comme des serveurs web, ça peut être un plus. En outre, cette pratique permet de gagner du temps lors du téléversement. mpy-cross fonctionne à partir de Python v3.8 jusqu’à Python v4.0 (ce dernier n’étant pas inclus).

Les fichiers mpy n’ont malheureusement pas que des avantages et présentent deux inconvénients qu’il faut considérer :

1. Ce sont des fichiers binaires : en tant que tels, ils ne sont pas éditables ;

2. Ils doivent être compatibles avec la révision de Micropython installée sur le microcontrôleur, ce qui fait que je n’en fournis pas dans l’archive : il y a très peu de chance pour qu’il convienne. Ils dépendent de votre environnement logiciel.

Le premier point est un frein à leur utilisation pendant la phase de développement. En effet, pendant cette phase, les fichiers sont susceptibles d’être modifiés fréquemment, ce qui impose des manipulations répétitives, simples mais chronophages, avec les fichiers mpy. Ce format est donc à éviter pendant cette étape, sauf, bien sûr, en cas de manque drastique de RAM. Par contre, les fichiers dont on est sûr qu’ils ne seront pas modifiés, comme des bibliothèques ou des fontes, peuvent sans inconvénient être utilisés sous leur forme compilée.

Le second point impose de connaître la révision de MicroPython installée dans le microcontrôleur que l’on va utiliser. Ça paraît trivial, mais quand on utilise plusieurs microcontrôleurs flashés à des périodes différentes avec des firmwares différents, on peut avoir des doutes. Cela dit, un petit script permettant d’afficher la version requise du mpy dans la console va nous permettre de lever cette incertitude. Les critères de compatibilité entre la version du fichier mpy utilisé et la révision de MicroPython qui va l’utiliser sont résumés dans le tableau suivant (voir Documentation MicroPython) :

Révision de MicroPython

Version mpy

v1.23.0 et au-dessus

6.3

v1.22.x

6.2

v1.20 à v1.21.0

6.1

v1.19.x

6

v1.12 à v1.18

5

v1.11

4

v1.9.3 à v1.10

3

v1.9 à v1.9.2

2

v1.5.1 à v1.8.7

0

Attention ici à ne pas confondre version et révision. MicroPython fonctionne sur de nombreuses familles de microcontrôleurs, et pour chacune d’entre elles, il peut exister plusieurs déclinaisons de MicroPython ayant des spécifications différentes. Par exemple, pour l’ESP8266, il existe à ce jour cinq déclinaisons différentes :

- 512 Kio de mémoire flash ;

- 1 Miode mémoire flash ;

- 2 Mio de mémoire flash avec ROM file system ;

- 4 Mio ou plus de mémoire flash ;

- OTA.

Il existe donc une version de MicroPython pour chaque déclinaison dans chacune des familles de microcontrôleur. Cela fait beaucoup de versions.

Toutes ces versions et déclinaisons de MicroPython possèdent malgré tout un noyau commun, indépendant de la plateforme, qui évolue au fil du temps. La révision ne concerne que (si je puis dire) l’évolution de ce noyau. Normalement, le script est d’abord compilé sous forme de bytecode avant d’être interprété. Or, ce compilateur évolue avec le noyau. Si on fournit directement le bytecode, il est donc indispensable que celui-ci ait été généré par le compilateur correspondant au noyau utilisé. C’est ce qui explique que la version du fichier mpy est liée à la révision de MicroPython et qu’une version donnée de mpy ne fonctionnera que sur les plateformes équipées de la même révision de MicroPython, quelle que soit la version.

En résumé, ce qui est appelé dans le tableau « Révision de MicroPython » est en fait la version de son noyau. On peut faire un parallèle avec la différence entre la version d’une distribution GNU-Linux et la version du noyau Linux associé. Une distribution peut être fournie avec différentes versions du noyau Linux, de même qu’une version du noyau Linux peut équiper différentes distributions.

VII-B - Utilisation

mpy-cross est un utilitaire en ligne de commande. Vous devez donc ouvrir une console. Pour faire simple, ouvrez cette console dans le dossier contenant le fichier à compiler : ça évite d’avoir à saisir le chemin. Comme mpy-cross est dans le path, vous pouvez l’invoquer directement en exécutant :

user

mpy-cross

Évidemment, sans argument, on se doute qu’il ne va pas se passer grand-chose. L’utilitaire va malgré tout nous retourner no input file pour nous dire qu’il manque quelque chose, à savoir un fichier.

Qu’à cela ne tienne, on va lui fournir un fichier (MicroPython de préférence) :

user

mpy-cross script.py

mpy-cross ne se fie pas à l’extension .py du fichier pour commencer la compilation. Il analyse d’abord celui-ci et retourne un message d’erreur s’il considère qu’il ne s’agit pas d’un fichier (Micro)Python, sinon il le compile, quelle que soit son extension. Toutefois, si l’extension est différente de .py, il ajoute la première lettre de l’extension du fichier original au début de l’extension du fichier compilé.

Exemple :

- toto.py donnera toto.mpy après compilation ;

- toto.xyz donnera toto.xmpy après compilation.

De cette manière, vous ne pouvez pas oublier que le fichier d’origine n’est pas « nativement » un fichier .py.

Si tout se passe bien, le fichier script.mpy est généré et aucun message n’est retourné dans la console. Donc tout est pour le mieux dans le meilleur des mondes, à une petite exception près : qu’en est-il de la version du fichier mpy généré. Souvenez-vous qu’il doit y avoir adéquation entre la version du fichier mpy et la révision de MicroPython installée sur le microcontrôleur ciblé.

Sans autre argument, cela dépend de la version de mpy-cross installée. Jusqu’au 22 juin 2024, quand vous installiez la dernière version de mpy-cross, celui-ci n’était compatible qu’avec la dernière révision de MicroPython. Si vous vouliez compiler des fichiers pour une autre révision de MicroPython, il fallait installer la version adéquate de mpy-cross. À présent, à partir de la version mpy-cross 1.23.0.post2, on peut paramétrer mpy-cross pour qu’il génère la version voulue de fichier mpy. C’est nettement plus souple et si vous possédez une ou plusieurs des anciennes versions (comme moi), vous avez tout à gagner à effectuer une mise à jour vers cette nouvelle version.

Reste à déterminer la version de MicroPython embarquée sur le microcontrôleur, du moins si vous avez un doute. Je vous propose le script suivant :

sys_mpy.py

1. import sys

2. sys_mpy = sys.implementation._mpy

3. arch = [None, 'x86', 'x64',

4. 'armv6', 'armv6m', 'armv7m', 'armv7em', 'armv7emsp', 'armv7emdp',

5. 'xtensa', 'xtensawin', 'rv32imc'][sys_mpy >> 10]

6. print('mpy version:', sys_mpy & 0xff)

7. print('mpy sub-version:', sys_mpy >> 8 & 3)

8. print('mpy flags:', end='')

9. if arch:

10. print(' -march=' + arch, end='')

11. print()

12. a = sys.implementation[1]

13. print("MicroPython release " + str(a[0]) + "." + str(a[1]) +"." + str(a[2]))

issu de la documentation de MicroPython, qui donne la version et la sous-version des mpy compatibles ainsi que le type de processeur utilisé, auquel j’ai ajouté les deux dernières lignes pour afficher également la révision de MicroPython.

À noter que depuis la version 1.25de MicroPython, sys.implementation renvoie un tuple de 5 éléments contre 4 précédemment.

Sauvegardez le script ci-dessus dans un fichier que vous nommerez par exemple sys_mpy.py (ou utilisez directement le fichier fourni dans l’archive) puis faites exécuter ce fichier par votre microcontrôleur. Avec mpremote, la ligne de commande sera :

user

mpremote u0 run sys_mpy.py

Voici, par exemple, les résultats avec trois versions différentes de MicroPython :

- version 1.22.2 :

- version 1.23 :

- version 1.25 :

En se référant au tableau, on vérifie que la version mpy 6.2 correspond bien à la révision 1.22.x de MicroPython et que la version mpy 6.3 correspond bien aux révisions 1.23 et 1.25 de MicroPython.

mpy-cross offre deux possibilités pour générer le bytecode, à savoir :

- en fonction de la révision de MicroPython :

user

mpy-cross --compat 1.22.2 script.py

- en fonction de la version du fichier mpy à générer :

user

mpy-cross --bytecode 6.2 script.py

VIII - Application

Comme stipulé au chapitre V-A. Choix des polices, « Pour illustrer cet article et lui donner un fil directeur, on va partir sur un très classique affichage de mesures fournies par un capteur » : on va donc prendre le « très classique » capteur de température et d’humidité DHT22 (AM2302) qui possède le double avantage d’être peu onéreux (aux alentours d’un euro) et d’être présent dans mon stock (si on peut dire) de composants. Pour ceux qui ne connaissent pas encore ce composant, voici les deux conditionnements sous lesquels on le trouve le plus fréquemment :

Le capteur seul nécessite l’adjonction d’une résistance de tirage (ou pull-up). Dans le module, cette résistance est déjà câblée et un condensateur de découplage est également incorporé. Pour le prototypage, le module est plus pratique, mais pour l’intégration dans un circuit, l’utilisation du capteur seul sera souvent plus judicieux, sauf si on anticipe un éventuel remplacement.

CARACTÉRISTIQUES

Alimentation

Minimum

Maximum

3 V

5,5 V

Température

Humidité

Type de capteur

Thermistance

Capacitif

Précision

±0,5 °C

±2%RH à 25 °C

Résolution

0,01 °C

0,024%RH

Dérive annuelle

±0,3 °C

< 1%RH

Hystérésis

±0,1 °C

±1%RH

Durée d’échantillonnage

2 s

Bus

Protocole série propriétaire

Remplacement

Intégralement interchangeable

Plage de mesure

Minimum

Maximum

Minimum

Maximum

−40,0 °C

+80,0 °C

0,0%RH

99,9%RH

Les chiffres diffèrent quelque peu selon les sources. Malgré mes recherches, je n’ai pas trouvé de datasheet officielle, et ces valeurs sont donc données à titre indicatif.

Comme exemple d’application, je vous propose le script suivant qui utilise notre petit écran OLED pour afficher les mesures de température et d’hygrométrie issues d’un capteur DHT22. Ce script destiné aux ESP32, ainsi que le script équivalent destiné aux ESP8266 est fourni dans l’archive accompagnant le tutoriel. Je ne présente ici qu’un seul de ces scripts.

main_esp32.py

1. # Affichage alterné de la température et de l'hygrométrie, fournies

2. # par un capteur de type DHT22, sur un écran oled monochrome

3. # fonctionnant avec le pilote SSD1306.

4. # Ce script est adapté aux modules à base d'ESP32 et doit être

5. # téléversé sur le module sous "main.py". Il nécessite pour fonctionner

6. # la présence des bibliothèques "writer.py", "ssd1306.py" et des

7. # fontes "liberation_alphanum_16.py", "liberation_usysint_20.py"

8. # et "digits_35.py" que vous devrez donc téléverser également.

10. # Bibliothèques "MicroPython"

11. import gc

12. from machine import Pin, SoftI2C

13. from time import sleep

14. from ssd1306 import SSD1306_I2C

15. from dht import DHT22

16. from writer import Writer

18. # Polices "py"

19. import liberation_alphanum_16 as lib16

20. import liberation_usysint_20 as lib20

21. import digits_35 as dig35

23. gc.collect()

25. # Dimensions de l'afficheur stockées en mémoire flash

26. WIDTH = const(128)

27. HEIGHT = const(64)

29. # Intitulés stockés en mémoire flash

30. TITRE_1 = const('Température')

31. TITRE_2 = const('Hygrométrie')

33. # Drapeau de basculement de l'affichage température/hygrométrie

34. switch = True

36. # Instanciation d'une liaison SoftI2C

37. i2c = SoftI2C(scl=Pin(22), sda=Pin(21))

39. # Instanciation d'un afficheur (objet "SSD1306_I2C")

40. oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)

42. # Instanciation du capteur DHT22

43. capteur = DHT22(Pin(14))

45. # Création des deux instances de "writer" que l'on va utiliser pour les mesures

46. w_dig35 = Writer(oled, dig35, False)

47. w_lib20 = Writer(oled, lib20, False)

49. # Routine de centrage de texte pour les intitulés

50. def centrer(afficheur, largeur, fonte, texte, inverser = False):

51. w = Writer(afficheur, fonte, False)

52. if w.stringlen(texte) < largeur:

53. Marge = (largeur - w.stringlen(texte)) // 2

54. else:

55. Marge = 0

56. Writer.set_textpos(afficheur, 0, Marge)

57. w.printstring(texte, inverser)

59. # Affiche en alternance la température et l'hygrométrie à la fréquence de 0.5 Hz

60. while True:

61. oled.fill(0)

62. oled.fill_rect(0, 0, 128, 16, 1)

63. sleep(2)

64. capteur.measure()

65. if switch == True:

66. # Température

67. centrer(oled, WIDTH, lib16, TITRE_1, True) # Affiche le titre centré en couleurs inversées

68. temp = capteur.temperature()

69. Writer.set_textpos(oled, 28, 6)

70. w_dig35.printstring(str(temp))             # Affiche la mesure

71. Writer.set_textpos(oled, 43, 89)

72. w_lib20.printstring('°C')                  # en degrés Celsius

73. elif switch == False:

74. # Hygrométrie

75. centrer(oled, WIDTH, lib16, TITRE_2, True) # Affiche le titre centré en couleurs inversées

76. hygro = capteur.humidity()

77. Writer.set_textpos(oled, 28, 6)

78. w_dig35.printstring(str(hygro))            # Affiche l'hygrométrie

79. Writer.set_textpos(oled, 43, 89)

80. w_lib20.printstring(' %')                  # normalement en %RH mais on manque de place

81. oled.show()

82. switch = not switch

83. gc.collect()

85. # La position de la mesure a été déterminée empiriquement pour obtenir un visuel correct

En fonction du type de microcontrôleur que vous utilisez, certains pilotes peuvent être déjà inclus dans le firmware et vous n’avez donc pas besoin de les téléverser (cela dit, si vous le faites, ça fonctionnera quand même, mais ça occupera inutilement de la mémoire flash). Par exemple, sur l’ESP8266, les deux pilotes sont présents dans le firmware alors que sur un ESP32 ou sur un ITSYBITSY M4, seul le pilote dht est présent. Sur un pyboard ou sur un RP2, aucun de ces pilotes n’est présent dans le firmware MicroPython et vous devrez téléverser les deux. Ce qui suit est donc à adapter à votre matériel.

Fichiers à téléverser dans la racine du microcontrôleur :

- ssd1306.py : sauf pour l’ESP8266EX ;

- dht.py : dépend du microcontrôleur ;

- writer.py ;

- liberation_alphanum_16.py ;

- liberation_usysint_20.py ;

- digits_35.py ;

- main.py adapté au microcontrôleur.

Attention : si un fichier nommé main.py est déjà présent sur votre microcontrôleur, il sera écrasé. Sauvegardez-le si besoin.

Les deux scripts fonctionnent parfaitement, respectivement sur l’ESP-WROOM-32 et sur le NodeMCU, et j’imagine qu’il en sera de même sur la plupart des autres microcontrôleurs en adaptant notamment les numéros des broches à utiliser.

Pour le NodeMCU, comme vu plus haut, il sera indispensable d’utiliser une version précompilée de writer, mais également des fontes, et si vous voulez ajouter des fonctionnalités au script, vous déclencherez assez rapidement une erreur d’allocation mémoire. À titre indicatif, voici un comparatif de la capacité en mémoire RAM de ces deux microcontrôleurs :

ESP8266EX

ESP32

DRAM

96 Kio

320 Kio

IRAM

64 Kio

200 Kio

Total

160 Kio

520 Kio

Disponible

34 Kio

110 Kio

Dans ce cas, une des possibilités consiste à diminuer un peu la taille des fontes en les restreignant strictement aux caractères dont vous avez besoin pour l’affichage (voir l’encadré en fin du chapitre V-D. Génération des fichiers). Si ça ne suffit pas, vous n’aurez d’autre choix que de passer à la taille au-dessus, ESP32 ou autre.

Vous trouverez dans cette archive les fichiers utilisés dans le tutoriel.

L’archive ne contient pas de fichiers mpy, car ils dépendent de la version de MicroPython que vous utilisez. Le chapitre consacré à mpy-cross vous fournit tous les ingrédients pour générer ces fichiers à partir des fichiers py fournis.

Voici le résultat que l’on est en droit d’espérer :

Comme vous pouvez le constater, ces deux clichés comportent deux zones de contraste différent. Ce phénomène est dû au rafraîchissement de l’affichage qui est objectivé par la vitesse de prise de vue. Il n’est pas perceptible à l’usage en raison de la persistance rétinienne.

Lors du déroulement de l’application, ces deux affichages alternent à la fréquence de 0,5 Hz, soit toutes les deux secondes.

Ainsi que précisé en début d’article, l’affichage semble avoir tendance à se dégrader dans le temps lors d’une utilisation en continu. Les deux clichés ci-dessus ont été effectués avec un afficheur neuf. Les deux clichés ci-dessous ont été pris avec un afficheur ayant été utilisé pendant quelques mois en continu pour faire tourner l’application fournie. Je n’ai pas fait de test en continu sur cette durée avec un autre afficheur et ne peux donc dire si le résultat aurait été le même.

Vous remarquerez que le contraste général est beaucoup plus faible qu’avec l’afficheur neuf, et il est de plus irrégulier comme on peut le constater en examinant notamment les lettres des titres.

IX - Conclusion

Vous venez de voir comment, avec quelques outils bien conçus, il est facile d’utiliser les polices TrueType et OpenType dans des applications mettant en œuvre un petit afficheur oled géré par le pilote ssd1306.py. Cet aperçu n’est qu’une entrée en matière dans le monde de l’affichage matriciel.

Cet écran et ce pilote ne sont pas, loin de là, les seuls à être exploitables avec ces outils : vous trouverez dans cette page une liste non exhaustive des produits ciblés, OLED, LCD ou TFT, tactiles ou pas, monochromes ou couleurs, interface SPI ou I2C, dans des dimensions allant de 94 ✕ 64 à 480 ✕ 320 ce qui permet de couvrir, vous en conviendrez, un nombre conséquent de domaines d’applications.

Pour aller plus loin, vous pouvez consulter les liens suivants :

- https://github.com/peterhinch/micropython-nano-gui ;

- https://github.com/peterhinch/micropython-micro-gui.

qui vous donneront accès à des bibliothèques dédiées à l’affichage sur écrans matriciels, simplifiant considérablement la conception des menus et la représentation graphique de données de tout type.

À vous de jouer !

X - Remerciements

Je remercie f-leb pour ses relectures technique et orthographique et ses conseils avisés.