Une conséquence de l’adoption massive des technologies de l’Internet des objets dans tous les secteurs est un besoin croissant de compétences de développement intégrées. Pourtant, le développement embarqué a toujours été un domaine assez complexe, et pas quelque chose que l’on peut ajouter à ses compétences du jour au lendemain.
Heureusement, au cours de la dernière décennie, les fournisseurs de silicium ont déployé beaucoup d’efforts pour simplifier le développement embarqué, en particulier pour les personnes ayant peu ou pas d’expérience dans le domaine. Des communautés telles qu’Arduino et PlatformIO ont également énormément contribué à fournir des outils faciles à utiliser et des bibliothèques de haut niveau qui peuvent cacher la plupart des détails effrayants – oui, l’assemblage, je vous regarde ! – de la programmation embarquée tout en permettant pour la rédaction d’applications professionnelles.
D’après mon expérience, il y a au moins un domaine où les choses restent trop lourdes : le développement de l’interface utilisateur graphique (GUI). De nombreuses applications nécessitent au moins certains sorte d’interface utilisateur graphique : l’écran est peut-être petit et monochrome, avec pratiquement aucun bouton sur lequel l’utilisateur peut appuyer, mais c’est toujours une interface utilisateur, n’est-ce pas ?
Je suis sûr que beaucoup d’entre vous le comprendront : le développement d’interfaces graphiques peut être très amusant… jusqu’à ce qu’il ne le soit pas !
Dans cet article, j’ai compilé 5 raisons pour lesquelles j’ai tendance à ne plus aimer écrire du code GUI intégré. Et puisque vous n’êtes peut-être pas intéressé par la simple lecture d’une diatribe, je partage également quelques conseils et certains des outils que j’utilise pour aider à garder le développement de l’interface graphique agréable.
Intégration et portabilité matérielles
La plupart des périphériques d’affichage sont fournis avec des exemples de codes et de pilotes qui vous donneront une longueur d’avance pour pouvoir au moins afficher quelque chose.
Mais une interface graphique ne se limite pas à un écran, car une interface est également composée d’entrées, n’est-ce pas ? Qu’en est-il de ces boutons-poussoirs, entrées d’écran tactile et autres capteurs de votre système qui peuvent tous participer à vos interactions ?
Une grande partie du développement embarqué se fait en C, donc, à l’exception du code d’amorçage de bas niveau, le code embarqué peut en théorie être assez portable. Cependant, écrire du code GUI portable est une toute autre histoire et à moins que vous ne construisiez sur un framework existant tel que LVGL ou Azure RTOS GUIX, il faut beaucoup d’efforts pour faire abstraction de toutes les dépendances matérielles, d’autant plus lorsque vous essayez de garder optimale.
Bien sûr, il n’est pas toujours nécessaire (ou possible) d’avoir un code GUI 100% portable. Cependant, en ces temps de pénurie mondiale de puces, il peut s’avérer très utile de ne pas dépendre fortement d’un type spécifique de microcontrôleur ou d’écran LCD.
Gestion de la mémoire
Comme je l’ai mentionné dans l’introduction, la manipulation des pixels peut être très amusante, c’est vraiment le cas ! Cependant, les systèmes contraints ont des quantités de mémoire limitées, et les pixels que vous manipulez dans votre code peuvent rapidement ajouter jusqu’à des milliers d’octets de mémoire RAM et Flash précieux.
Prenons l’exemple d’un tout petit écran monochrome de 128×64 pixels. Comme l’écran ne prend en charge que le noir et blanc, chaque pixel peut être représenté en mémoire à l’aide d’un seul bit, ce qui signifie qu’un octet peut contenir jusqu’à 8 pixels. Mais si vous faites le calcul :
C’est déjà 1 Ko de RAM, ce qui est assez important si votre MCU n’a, disons, que 16 Ko au total. Intéressé à afficher une poignée d’icônes 32×32px ? Ce sera 128 octets supplémentaires pour chacune de ces petites icônes !
En bref : votre interface utilisateur graphique consommera probablement une grande partie de votre mémoire, et vous devez être très intelligent pour laisser suffisamment de place à votre application réelle. Par exemple, un moyen rapide d’économiser sur la mémoire graphique est de vérifier si certains de vos graphiques raster (par exemple les icônes) peuvent être remplacés par un équivalent vectoriel : il faut sûrement beaucoup moins de code et de RAM pour dessiner directement un simple carré rouge 32x32px sur l’écran, au lieu de l’avoir stocké sous forme de bitmap en mémoire.
La gestion des ressources
Il peut être difficile de gérer correctement les différentes ressources qui composent un projet d’interface graphique.
Plus précisément, et que vous ayez la chance de travailler avec un graphiste ou non, vos maquettes d’interface graphique seront probablement composées d’une variété de fichiers image, d’icônes, de polices, etc. Cependant, dans un contexte intégré, vous ne pouvez généralement pas vous attendre pour pouvoir manipuler directement ce joli fichier PNG transparent ou cette police TrueType dans votre code ! Il doit d’abord être converti dans un format qui permet de le manipuler dans votre code embarqué.
Si vous êtes un développeur embarqué chevronné, je suis sûr que vous (ou quelqu’un de votre entreprise) avez probablement développé vos propres macros et outils pour vous aider à rationaliser la conversion/de vos actifs binaires, mais d’après mon expérience, cela a toujours été un beaucoup de bricolage et de scripts de conversion ponctuels, rapides et sales, qui ont un impact sur la maintenabilité à long terme. Ajoutez le contrôle de version au mix, et il devient assez difficile de garder vos ressources en ordre à tout moment !
Gestion des événements et performances
La programmation graphique est par nature très événementielle. Il est donc tout à fait naturel de s’attendre à ce que le code de l’interface graphique embarquée se présente comme suit (pseudo-code) :
Button btnOK;
btnOK.onClick = btnOK_click;
btnOK_click = function() {
// handle click on button btnOK
// ...
}
Comme vous pouvez l’imaginer, les choses ne sont pas toujours aussi simples !
Premièrement, C, qui reste le langage de programmation embarqué le plus utilisé, n’est pas exactement orienté objet. En conséquence, même s’il est parfaitement possible de viser une API de haut niveau qui ressemble à ce qui précède, il y a de fortes chances que vous vous retrouviez à jongler avec des pointeurs de fonction sujets aux erreurs lors de l’ajout/la mise à jour d’un gestionnaire d’événement à l’un de vos Éléments de l’interface utilisateur.
En supposant que vous ayez effectivement trouvé un moyen élégant d’associer des gestionnaires d’événements aux différentes parties de votre interface utilisateur, vous devez toujours implémenter une sorte de boucle d’événements. En effet, vous devez régulièrement traiter les événements qui se produisent dans votre système (« bouton A enfoncé », etc.) et les envoyer aux gestionnaires d’événements appropriés. Un modèle courant en programmation embarquée consiste à le faire via une super boucle : le programme exécute une boucle infinie qui invoque chaque tâche que le système doit effectuer, à l’infini.
int main() {
setup();
while (1) {
read_sensors();
refresh_ui();
// etc.
}
/* program's execution will never reach here */
return 0;
}
Un avantage de cette approche est que le flux d’exécution reste assez lisible et simple, et cela évite également certains maux de tête potentiels qui peuvent être induits par un multi-threading complexe ou une gestion des interruptions. Cependant, tout gestionnaire d’événements fonctionnant trop longtemps (ou plantant !) peut compromettre les performances et la stabilité de votre application principale.
Alors que les systèmes d’exploitation en temps réel intégrés tels que FreeRTOS ou Azure RTOS ThreadX deviennent de plus en plus populaires, une approche plus moderne consiste à exécuter la boucle d’événements de l’interface utilisateur dans une tâche d’arrière-plan dédiée. Le système d’exploitation peut donc s’assurer que cette tâche, compte tenu de sa priorité inférieure, ne compromettra pas les performances de votre application principale.
Une interface graphique embarquée n’a pas toujours besoin d’être performante comme dans vite et sensible. Cependant, il est considéré comme une bonne pratique d’utiliser les ressources intégrées aussi efficacement que possible. S’assurer que votre interface graphique et votre code d’application sont aussi performants que raisonnablement possible peut potentiellement vous faire économiser beaucoup d’argent, car cela signifie que vous pouvez vous en tenir à l’utilisation du plus petit MCU possible pour la tâche.
Outillage
Dernier point mais non le moindre : l’outillage. Pour être honnête, je n’ai jamais été un grand fan de la conception d’interfaces utilisateur graphiques en utilisant une approche WYSIWYG (What You See Is What You Get). Cela étant dit, le codage d’une interface utilisateur graphique comportant plus que quelques écrans nécessite au moins certains prise en charge de l’outillage, car la plupart des codes de colle « ennuyeux » peuvent souvent être générés automatiquement.
De plus, tester une interface graphique intégrée peut rapidement devenir pénible, car la recompilation de votre application et son téléchargement sur votre cible peuvent prendre un certain temps. Cela peut être très frustrant lorsque vous devez attendre quelques minutes pour, par exemple, tester à quoi ressemble cette nouvelle animation fantaisiste que vous avez codée.
Au cours des derniers mois, j’ai commencé à utiliser Renode plus souvent. Il s’agit d’une suite d’outils open source assez complète pour émuler du matériel embarqué, y compris leur affichage ! Dans un prochain article, je prévois de partager davantage sur la façon dont j’ai commencé à utiliser Renode pour raccourcir considérablement la « boucle de développement intégrée interne », c’est-à-dire le temps entre la modification du code et la possibilité de le tester en direct sur votre appareil – émulé ! .
Je serais curieux de connaître votre expérience et vos points faibles lorsque vous travaillez avec des interfaces graphiques intégrées. Faites-le moi savoir dans les commentaires ci-dessous!
Comme je l’ai déjà mentionné, restez à l’écoute pour les prochains articles où je couvrirai certains des outils et cadres que j’ai commencé à utiliser (et à aimer) et qui font ma vie DONC beaucoup plus facile quand il s’agit de développement d’interface graphique !