Aller au contenu | Aller au menu | Aller à la recherche

Dans les entrailles du Libre

mar., 10/10/2017

Une erreur… bien cachée

J'avais dit que je parlerais un peu des systèmes embarqués et de mes aventures dans le milieu. Aujourd'hui je voudrais relater un problème que j'avais dû résoudre au travail et qui n'était pas évident. Le prochain article sera plus générique (et celui d'après à propos d'un autre problème au boulot). Je vais essayer de varier un peu. ;-)

Ce sera sûrement un peu long, et je vais donner mon cheminement intellectuel autour de ce problème. Je respecte la chronologie des éléments dont j'avais connaissance à chaque étape.

Présentation du matériel et contexte

Pour situer un peu, je travaillais sur une plateforme Texas Instrument 8148 qui est un processeur ARM Cortex-A8 couplé avec un processeur M3 et des accélérateurs vidéos / DSP en son sein. Cette plateforme exploitait un noyau Linux 2.6.37 patchée à mort par Texas Instrument pour son processeur.

Le but du jour était d'exploiter le composant TVP5158 de… Texas Instrument encore (ce module a été conçu pour ce processeur en même temps). Pour décrire rapidement, ce composant permet de fusionner jusqu'à 4 flux vidéo analogiques (PAL / NTSC principalement) dans un seul numérisé que le processeur peut exploiter.

En soit le composant avait déjà un pilote fonctionnel pour Linux. Mais cela ne nous convenait pas. En effet, nous utilisons le TVP dans une chaîne de traitement vidéo où on faisait des décodages, redimensionnements, des remplacements d'images ou encore des encodages. Pour faire cela, non seulement il faut de la performance mais il faut aussi utiliser une API plutôt unifiée.

La performance est offerte par les accélérateurs vidéos qui sont dans les co-processeurs du système. On peut les exploiter via un firmware (dont le code source est confidentiel, désolé) et on peut communiquer avec ces accélérateurs via le noyau Linux et l'API OpenMax (OMX). Cette API est standard, éditée par le même groupe et dans les mêmes conditions qu'OpenGL, mais plutôt que de s'occuper de la 3D, il s'occupe des codecs vidéos. C'est souvent la référence libre dans ce domaine, avec GStreamer couplé avec libav.

Cette API est disponible dans le firmware de TI, cela tombe bien. Mais là où cela ce corse c'est pour récupérer des informations. En effet, il faut que durant notre traitement vidéo que nous sachions la résolution détectée par TVP du flux vidéo, et s'il existe (sur les 4 flux, des caméras peuvent être en pannes ou absentes pour diverses raisons).

Et TVP se configure via un bus assez standard et typique I2C qui est assez simple à mettre en place et largement suffisant pour lire / écrire des registres sur un composant. Il est par exemple souvent utilisé dans les cartes mères / graphiques de nos ordinateurs pour les capteurs de températures. Le soucis est que ce bus ne peut être géré par le firmware de TI (pour configurer le flux) et par le noyau Linux (pour récupérer les infos sur le flux pour l'application). Si on fait cela, il y aura conflit et le bus sera inutilisable. Modifier le firmware pour renvoyer les informations à l'espace utilisateur ou que le noyau gère la configuration du flux vidéo est plutôt complexe. Le mieux est d'utiliser un canal de communication existant entre le noyau et le firmware pour faire ceci, le firmware a donc la main sur le bus I2C et le noyau fera ses requêtes a travers lui.

On code

Partie intéressant du travail, coder. Et réfléchir à comment faire aller / revenir les informations voulues. Le noyau et le firmware, comme dans la quasi-totalité des systèmes à processeurs asymétriques, communiquent entre eux par mémoire partagée. Une partie de la RAM est allouée pour les messages, une autre pour les buffers vidéos, etc. Ceci est configurable (avec des limites) dans le code des deux côtés. Bien évidemment, les deux doivent être d'accord sur les adresses de base de chaque fonction, sinon ils ne retrouveront plus leurs petits. Cela est plutôt bien pris en charge par l'environnement de compilation fourni par TI. Vous pouvez consulter l'adressage mémoire entre les deux ici.

Dm8148-ezsdk-sw-arch.png

Bon, et le code alors ? La communication entre ces deux modules se faisant par mémoire partagée, il y a un certain protocole qui a été conçu par TI et qui s'exploite à travers une API maison nommée FVID2. Elle est déjà partiellement implémentée mais pas celle concernant le fameux TVP (qui est pourtant décrite dans l'API en question). J'ai donc écrit un pilote Linux pour combler cela. Aspect amusant, TI à la pointe de la technologie fournissait la doc de cette API dans un document .chm, un vieux format propriétaire dont le seul lecteur sous Linux potable est une application de l'ère de KDE3 : Kchmviewer. Quand je vous dis que l'embarqué c'est moderne !

Mais cela reste un peu moche, l'application en espace utilisateur demande au firmware HDVPSS de configurer le TVP. Mais, pour faire cela, il instancie le pilote interne de TVP du firmware qui ne doit pas être instancié deux fois et dont on ne peut récupérer la référence pour notre API FVID2… J'utilise donc une référence d'un autre composant dont le noyau a la référence (car il l'instancie) et j'envoie mes messages avec, le firmware a été modifié pour comprendre la situation et rediriger le message ensuite à bon port. Je n'avais pas trouvé mieux dans le délai imparti.

Et le bogue arrive… parfois

Et après le code, on teste. Après plusieurs essais difficiles, cela fini par passer. Youhou. Champomy dans les bureaux.

Mais, quand mes collègues vont tester leur application avec mon travail, cela ne fonctionne pas toujours. Le module noyau qui fait les échanges avec le coprocesseur nous signifie que certaines requêtes, en quantité variables, mettent trop de temps à revenir. On était à une moyenne de 1 à 10 échecs par minute (pour 24 requêtes). Mais il arrivait malgré tout que sur 30 minutes / 1 heure cela n'arrive pas, avant de revenir. C'est beaucoup trop problématique, et ce qui est étonnant c'est que mes tests ne présentaient aucune erreur.

Du coup, comme pour toute chaine de communication, on va déboguer étape par étape pour identifier où cela coince. Je précise que la seule section dont je pouvais difficilement déboguer c'est l'échange des messages entre Linux et le firmware qui est assez mal documenté et le code assez obscur en plus d'être gros.

Matériel

Le plus simple à tester, c'est le matériel (recompiler le firmware vidéo pour y ajouter du débogue c'est 40 minutes de compilation, c'est pénible et long, on évite au maximum de le faire). Je vérifie donc que le TVP reçoit les bonnes requêtes et y répond. Le bus I2C étant très simple, un petit oscilloscope sur un fil et on peut rapidement conclure que les signaux sont bons dans les deux sens que la requête échoue ou non à la fin.

Mais je me dis que le temps d'attente côté Linux pour recevoir ce message est trop court, je l'allonge volontairement à un délai absurde comme 30 secondes, cela ne change rien. Soit ça réussi vite, soit au bout des 30 secondes j'ai l'erreur. Pas d'entre deux, pas de hausse ou baisse de ces erreurs.

Du coup on sait que la chaîne d'envoi est bonne, et le matériel aussi, le soucis se situe bien au retour.

Firmware vidéo

Donc forcément je remonte un peu la chaîne côté firmware vidéo et à chaque étape en son sein, l'information est correcte à tous les coups. Comme le soucis n'est pas côté Linux après l'API FVID2, le soucis se situe forcément dans le transfert des messages entre les deux mondes comme on dit. Côté retour uniquement.

Plongeons au cœur de la mémoire

À ce stade là, cela devient assez étrange. Comment cela peut planter de cette manière là ? J’émets quelques hypothèses, manque de place pour l'échange de messages (il y a d'autres messages que ceux du TVP là dedans) ou éventuellement un conflit de lecture / écriture simultanée par les deux sur un même message.

Pendant que je cherchais comment l'ensemble fonctionnait, des collègues m'ont rapporté des informations pertinentes (bah oui, ils continuent de testeur leur travail de leur côté et disent ce qui est pertinent quand ils constatent le problème). Il y a une corrélation entre le nombre de caméras branchées au TVP (et exploitées par notre programme) et la fréquence du bogue. Plus il y a avait de caméras, moins cela plantait. Cela paraît contre intuitif.

J'ai maintenant une idée plus claire de ce qui semble être le cas, mais je dois vérifier. J'essaye de voir donc comment l'échange de message fonctionne, et la fonction que j'appelle a quelques lignes intrigantes dont je copie la boucle intéressante :

while (fctrl->fctrlprms->returnvalue == VPS_FVID2_M3_INIT_VALUE) {
       usleep_range(100, 300);
       if (vps_timeout) {
              do_gettimeofday(&etime);
              td = time_diff(stime, etime);
              if (vps_timeout < td) {
                    VPSSERR("contrl event 0x%x timeout\n",  cmd);
                     return -ETIMEDOUT;
               }
        }
}

En gros, Linux initialise une valeur précise à une adresse précise dans la RAM. Quand le firmware a fini son boulot et renvoie les infos demandés, il signifie au noyau qu'il a fini en changeant la valeur initialisée précédemment. Le noyau regarde sa valeur régulièrement par tranches de 100 à 300 microsecondes pendant 2 secondes maximum.

Et comme par hasard, si je mets dans la boucle un printk de débogue (l'équivalent de printf pour le noyau, pour afficher des chaînes de caractères visibles en espace utilisateur, cette fonction est plutôt grosse), le bogue disparaît. Quelques soient les conditions.

Cela me renforce dans mon hypothèse : nous sommes face à un soucis d'accès à la valeur de cette variable. Le noyau Linux relit la variable depuis le cache ou le registre du processeur qui bien sûr ne change pas (car le processeur n'a pas changé cette valeur, c'est au coprocesseur de le faire !), du coup il voit éternellement la variable comme au départ et il croit que le firmware vidéo ne fout rien. Le printk ou l'activité du système (plus il y a de caméras, moins cela arrive, n'oublions pas) permettant à Linux de bien voir la véritable valeur en la rechargeant depuis la RAM.

Le problème vient du fait que ce genre de vérification ne doit pas se faire directement, surtout que la zone mémoire concernée a été allouée avec "ioremap()".

Il suffit donc de remplacer la ligne

 while (fctrl->fctrlprms->returnvalue == VPS_FVID2_M3_INIT_VALUE) {

Par

 while (ioread32(&fctrl->fctrlprms->returnvalue) == VPS_FVID2_M3_INIT_VALUE) {

Les accès par "ioread*()" mettent des barrières mémoires et indiquent que la variable peut être modifiée de l'extérieur. Obligeant donc une nouvelle lecture de la valeur en toute circonstance.

Conclusion

Je suis tombé sur ce bogue après quelques mois dans la vie active seulement. C'était un défi intéressant, je n'avais pas trouvé cela évident. C'est vraiment le genre de choses que l'on a tendance à oublier, on accorde trop de confiances aux couches d'en dessous (matériel / noyau / compilateur / bibliothèque / langage) qu'on en oublie qu'ils peuvent présenter des problèmes et qu'on doit faire attention en les utilisant.

Après, cela met en évidence un énième bogue / oubli stupide de la part de Texas Instrument (ils en font du code horrible, je vous le dis) qui aurait pu être évité s'ils travaillaient un peu plus avec le noyau officiel. Nul doute qu'avec plus de relecteurs de leur code, quelqu'un l'aurait vu. Mais bon, tant pis, je me suis bien amusé. :-)

ven., 07/04/2017

Les systèmes embarqués

Cela faisait un moment que je réfléchissais à parler des systèmes embarqués, en particulier autour de Linux. Cela fait quelques temps que je suis dedans, et les ressources francophones étant plutôt rares dans le secteur, j'aimerais partager mon expérience, mes pensées, mes trucs et astuces potentiels.

Bien sûr cela ne remet nullement en cause la communication de l'actualité autour de Fedora. :-)

Pour commencer cette catégorie, je pense qu'il est bon de définir ce que j'entends ici par systèmes embarqués afin que ce soit clair et raconter un peu ma vie sur pourquoi je travaille dedans.

Mon parcours

Mon intérêt pour les systèmes embarqués vient de loin, mais reste flou. Globalement j'ai toujours apprécié manipuler un produit, un objet. Cela s'est confirmé durant mon cursus dans le supérieur, je n'éprouve pas une grande satisfaction à faire tourner mon bout de programme sur un serveur dont j'ignore même sa localisation. Ce besoin de concret est très important pour moi. J'ai toujours été très excité de lancer le résultat de mon travail préliminaire sur une nouvelle carte électronique qui vient de débarquer. Et constater que cela ne fonctionne pas du premier coup bien entendu.

Ensuite, il y a également l'intérêt d'apprendre le fonctionnement de nos ordinateurs. J'ai toujours été fasciné par le fonctionnement des processeurs, de la RAM ou des systèmes d'exploitation. Sans doute l'émerveillement devant leur complexité et l'ingéniosité de leur conception (si on met sous le tapis les horreurs également présentes). Dans ce domaine, il faut connaître les caractéristiques du circuit, du processeur, lire la documentation associée, comprendre l'état de la mémoire, comment l'OS va gérer les entrées/sorties et les processus pour donner accès à la fonction voulue. Impossible de se reposer sur une machine virtuelle ou un interpréteur qui nous cache ces détails (ce qu'ils font à raison, mais cela ne m'intéresse pas).

Enfin, je trouve que c'est un domaine omniprésent. Cela permet de côtoyer de nombreux secteurs d'activités différents comme: télécommunication, aéronautique ou spatial, automobile, agriculture, sécurité bancaire, multimédia, l'électronique, etc. Et comme le produit doit répondre aux contraintes de chaque secteur, il faut étudier, comprendre et analyser les caractéristiques du secteur considéré. Cela permet de varier l'environnement d'étude et donc les choses à en apprendre. Or j'adore apprendre.

Qu'est-ce qu'un système embarqué ?

Cela peut paraître absurde, mais il y a probablement autant de définitions que d'acteurs dans le domaine. D'autant plus qu'avec le temps et l'évolution technique, la définition de ce qui entre ou pas dans ce cadre peut varier. Typiquement, un téléphone aujourd'hui est radicalement différent par rapport aux débuts des années 2000. Pour certains un smartphone n'en est plus un, pour d'autres, cela continue.

Du coup on va dégager les tendances ou les points communs pour le définir. Globalement nous pouvons résumer cela en tout système programmé qui n'est pas d'architecture x86 ou qui présente une contrainte spécifique.

La puissance disponible

La première chose qui vient à l'esprit, ce sont les performances matérielles. Un processeur pas très puissant, voire ancien, peu de RAM ou de stockage et une connectique souvent un peu spéciale pour en tirer parti. Grâce aux progrès de la miniaturisation et de l'économie d'énergie, c'est de moins en moins vrai. Des processeurs tels que les nVidia Tegra TK1/TX1 ne consomment pas grand chose. Pour moins d'une dizaine de watts, et une surface raisonnable, nous avons un couple GPU/CPU pouvant atteindre le TFLOPS en puissance de calcul brut. Qu'on peut facilement épauler de quelques Gio de RAM et de mémoire de stockage.

À titre de comparaison, 1 TFLOPS (en admettant cette unité de mesure comme pertinente, ce qui est discutable) c'est l'ordinateur le plus puissant du monde déclaré (donc pas militaire ou top secret) en 1997. 20 ans après un téléphone ou une voiture en ont un voire plusieurs sur quelques millimètres carré. Nous sommes très loin d'un simple microcontrôleur ou d'un processeur peinant à gérer plusieurs processus à la fois. Cela est également proche de ce qu'on peut retrouver dans un ordinateur personnel normal.

Donc si la puissance disponible est un critère souvent retenu, programmer un microcontrôleur est de fait un système embarqué, il est possible aujourd'hui de faire des systèmes embarqués avec une puissance de calcul très élevée.

Les contraintes d'un système peu puissant c'est qu'il demandera sans doute une économie des ressources, la renonciation à certaines technologies et à certaines fonctionnalités. Ce n'est pas pour rien que le C et le C++ règnent encore en maître dans le domaine, même si Python ou Java prennent de plus en plus de place, grâce justement à l'amélioration des performances.

L'énergie

Pour enchaîner avec le point précédent, nous pouvons relever la consommation énergétique. Tous les systèmes embarqués n'ont pas cette contrainte, mais cela reste assez commun. En effet, un système sur batterie devra faire attention à la consommation énergétique pour limiter l'apport d'énergie (en fréquence comme en puissance) par exemple pour augmenter son autonomie.

Le lien avec la puissance est évidente, malgré les progrès, plus de puissance de calcul demandera plus de ressources électriques. C'est ce qui a limité pendant longtemps la possibilité de faire des systèmes embarqués avec des performances élevées. La mise à disposition de processeurs modernes avec une consommation faible permet le contournement de ce problème mais pas complètement. Cela se vérifie avec l'Internet des Objets qui remet au centre du secteur des processeurs très minimalistes et très peu gourmands pour exécuter uniquement la fonction demandée.

L'énergie va avoir plusieurs contraintes sur le système. Il peut en effet limiter la puissance disponible. Il peut aussi être une contrainte car l'énergie fournie est non fiable. Nous pouvons penser à des systèmes en aéronautique ou aérospatiale dont certains composants ont une puissance électrique allouée maximale et que dans certains cas l'engin peut couper l'alimentation de composants secondaire (par exemple le multimédia à bord des avions).

Il faut donc tenir compte de cela, par exemple certaines communications peuvent être coupées brutalement, le système aussi. Cela a un impact fort sur le design électronique et logiciel. Il faut réfléchir à comment gérer le cas d'une coupure de courant lors d'une mise à jour du système (dans quel état le système va être retrouvé ?). Les fonctions qui doivent être maintenues en vie et d'autres pas. Si ces questions existent pour l'ordinateur personnel, cela reste moins problématique et beaucoup plus rare que dans le cas des systèmes embarqués. On peut en effet présumer que dans les pays développés le réseau électrique est plutôt fiable (ce qui n'est pas sans poser de soucis pour ceux ne vivant pas dans de tels pays mais passons).

L'autonomie

Ici je ne parlerai pas d'autonomie électrique, bien que ce soit liée, mais plutôt d'autonomie en terme d'isolement du système. Un système où l'accès au réseau n'existe pas forcément, où il n'y a pas un technicien disponible pour le réparer en cas de besoin rapidement.

Cela a un impact bien entendu très fort sur le système. Cela signifie que le système doit être robuste aux pannes ou bogues divers. Certains processus devront être très fiables, comme la mise à jour. Si on envoie un nouveau logiciel à un satellite dans l'espace et que cela échoue, le satellite doit rester opérationnel dans tous les cas. On ne va pas envoyer une fusée pour réparer ce genre de choses.

En terme d'architecture du système, il va falloir penser à beaucoup de choses pour minimiser l'intervention humaine. Il faut forcément automatiser le lancement pour que l'application métier soit lancée toute seule. Mais aussi programmer des tâches récurrentes, prévoir des solutions en cas de coupure du réseau ou de l'électricité.

Par exemple, pour que le système soit auto-entretenu dans un tel contexte, il est courant de recourir à un système sans état. Ce type de système fait qu'en cas de redémarrage, la fonction recommence comme si de rien n'était, le système pouvant être en lecture seule. On couple cela avec un watchdog pour que le système redémarre matériellement tout seul si jamais l'application ou le noyau ont planté.

Les entrées/sorties limitées ou spéciales

De part les contraintes énoncées plus haut, il en ressort qu'on n'interagit pas avec un système embarqué comme avec son ordinateur personnel. Il n'y a pas forcément d'écran, encore moins souvent de clavier complet ou de souris. Si écran il y a, c'est souvent du mono applicatif donc sans bureau. Le système doit être minimal, pour économiser en coût de composants, en risque de problèmes matériels ou logiciels et simple d'utilisation pour un utilisateur non formé. En effet il serait malvenue que le distributeur de billet de votre quartier soit complexe à prendre en main ou qu'il tombe en panne régulièrement.

Cela va induire par effet de bords des choix en terme d'interface utilisateur pour présenter le moins de choix possibles d'un coup, en restant clair et complet. Et que tout ceci soit sélectionnable sans difficulté ou ambiguïté pour tous les utilisateurs (comme les personnes âgées ou les handicapées). Ceci explique souvent les interfaces un peu vieillottes avec de gros boutons que ce soit bien visible et clair. Les interfaces riches peuvent être sources de confusion dans ce genre de situations en plus de demander plus de ressources matérielles et de temps de développement.

En terme d'entrées/sorties spécifiques, nous avons ceux du développement et en environnement de production. Pour le développement, comme nous utilisons notre ordinateur, nous devons communiquer avec la bête. Très souvent cela se fait par le réseau (par SSH et NFS), par port série voire USB pour l'accès à la console (quand le SSH n'est pas possible) et JTAG pour déboguer en stoppant le processeur par exemple pour analyser la situation à n'importe quel moment. Parfois pour flasher la JTAG sert encore mais cela est de moins en moins le cas. Il faut souvent jongler avec tout ceci ce qui encombre pas mal le poste de travail.

Les normes et l'environnement

Encore un sujet assez vaste et pourtant au combien important. Les systèmes embarqués sont très souvent employés dans des secteurs industriels où les normes sont omniprésentes pour des raisons de sécurité, de compatibilité, de respect de l'environnement, etc. Et cela n'est pas sans incidence sur le développement.

Des secteurs sont par exemple très très réglementés comme l'aéronautique, le médical, le spatial, l'automobile ou le ferroviaire. Ce sont des secteurs où des défauts peuvent engendrés la perte de vies humaines ou avoir des coûts matériels très important en cas de défaut. Il est impératif de respecter des normes afin de limiter au maximum les risques.

Je vais parler de l'aéronautique, secteur que j'ai pu côtoyer durant deux ans. Tout logiciel (et matériel) embarquant dans un avion doit être certifié conformément aux directives des agences continentales et internationales. Pour être certifié, l'écriture du logiciel doit suivre une procédure précise qui dépend bien sûr de la criticité du système.

En effet, tout système se voit attribué un DAL qui est un niveau d'exigence noté de A à F, plus la note est élevée, plus l'exigence et les limitations seront fortes. Dans tous les cas il faudra documenter le design, rédiger des cahiers de tests et communiquer les résultats. Il faut également facilement faire le lien entre le code / commit et un test ou une exigence du design.

Et là où cela se corse, c'est par exemple des logiciels pour les DAL A ou B, il faut impérativement que tout soit contrôlé. Tout le chemin d'exécution du code, de même que le déterminisme du processeur en dessous. C'est pourquoi il est fréquent d'utiliser des code très léger sur microcontrôleur dans ce cas, on ne peut se fier à la complexité d'un noyau Linux complet sur un processeur x86. La communication avec les autres composants doit être réduite au strict nécessaire, la consommation électrique est plafonnée. Il y a aussi des interdictions plus concrètes comme le respect d'un code couleur pour les interfaces (le rouge est par exemple réservé aux erreurs sévères et strictement définies).

Concernant les normes, il y a aussi les standards comme le bus CAN pour l'automobile ou l'A429 pour l’aéronautique qui sont incontournables pour interagir avec le reste de l'appareil sans réinventer la roue systématiquement. D'ailleurs l'analyse de ces normes mettent en évidence les contraintes de l'environnement pour expliquer ces choix de design et le maintient de ces normes anciennes sur des systèmes modernes. Cela fera l'objet d'un autre article.

En somme, il faut respecter les contraintes du secteur d'activités qui peuvent être importantes car il y a de fortes responsabilités derrière. Et cela influe forcément le design de l'application et du matériel et de fait du produit qui sera réalisé.

Conclusion

C'est un article un peu long qui se veut être une introduction générale. Par la suite je compte présenter quelques petites choses :

  • Étude d'une norme comme le bus CAN ou A429, qui sont assez simples, et comprendre leur intérêt dans les systèmes actuels et les raisons d'une telle conception ;
  • Présenter des projets liés aux systèmes embarqués comme Yocto, buildroot, U-boot ou le noyau Linux et expliquer le boulot d'un développeur sur ces projets ;
  • Expliquer certaines pratiques industrielles ou technologies dans le secteur, pour comprendre certaines décisions qui peuvent paraître aberrantes mais qui finalement le sont moins. Typiquement pourquoi il est rare qu'Android soit mis à jour décemment sur la plupart des modèles vendus ? Pourquoi il est difficile de faire un OS compatible avec tous les systèmes ARM ?
  • Présenter certains bogues ou designs que j'ai rencontré dans le cadre de mon travail.

Cela s'étendra dans le temps, je ne compte pas tout rédiger maintenant. Les humeurs, les envies, les idées, tout ça.

N'hésitez pas si vous avez des suggestions, des corrections, des questions ou de signifier si cela vous plaît / déplaît. Merci.