Laboratoire 4 : Programmation d'un pilote de périphérique en mode noyau

Systèmes embarqués temps réel (GIF-3004)

Objectifs

Ce travail pratique vise les objectifs suivants :

  1. Apprendre le fonctionnement d'un pilote dans Linux;
  2. S'initier à la programmation en mode noyau (kernel);
  3. Acquérir les connaissances nécessaires à l'interfaçage d'un périphérique simple;
  4. Se familiariser avec les classes de dispositifs de caractères et à l'utilisation du sous-système de fichier /dev;
  5. Apprendre à utiliser judicieusement les interruptions dans un système embarqué.

Préparation et outils nécessaires (matériel)

Ce laboratoire est quelque peu différent des précédents, puisque vous devrez utiliser du matériel supplémentaire, en l'occurrence le petit clavier externe à 16 touches que vous avez déjà utilisé dans le cours Systèmes microprocesseur et interfaces. Ce clavier est très rudimentaire et vous devrez concevoir la logique nécessaire à sa lecture. Il possède huit fils d'entrée/sortie : 4 connexions pour les lignes et 4 pour les colonnes. Ces sorties seront connectées aux GPIO (General Purpose Input-Output) de votre Raspberry Pi 3. Ce dernier possède 40 points (pins) de connexion, agencés selon le schéma suivant (source : element14) :

Comme vous pouvez le constater, toutes les broches ne sont pas équivalentes. Certaines peuvent cumuler plusieurs fonctions alors que d'autres sont simplement des références pour la masse (Ground) ou des sources de tension. Dans le cadre de ce laboratoire, nous vous suggérons de brancher les lignes du clavier (qui seront écrites par le Raspberry Pi) sur les broches 29, 31, 33 et 35. De même, les colonnes du clavier (qui seront lues) devraient être branchées aux broches 32, 36, 38 et 40. D'autres configurations peuvent fonctionner et vous êtes libre de les utiliser, mais celle que nous vous proposons fonctionne à coup sûr. Si vous utilisez le clavier à 16 touches du cours de SMI, le schéma présenté ci-dessus, à droite, peut vous aider à vous repérer. Dans ce schéma, les fils sont numérotés de gauche à droite. Le fil 1 est connecté à la première ligne (la plus haute) du clavier, le 2 à la seconde, et ainsi de suite. Le fil 5 est connecté à la première colonne (celle de gauche) du clavier, le fil 6 à la seconde et ainsi de suite.

Attention lors des connexions : les broches de sortie du Raspberry Pi sont des interfaces très rudimentaires et, en particulier, elles ne possèdent pas vraiment de systèmes de protection en cas de mauvais branchement. Brancher par exemple une ligne sous tension à une broche reliée à masse (ground) court-circuitera à coup sûr votre Raspberry Pi! Assurez-vous toujours que vos branchements sont corrects et qu'il n'y a pas de faux contacts avant de mettre l'ordinateur sous tension! Par ailleurs, nous vous recommandons fortement de ne pas changer les connexions alors que le Raspberry Pi est sous tension!

Préparation et outils nécessaires (logiciels)

Les ébauches de code sont disponibles sur le dépôt Git suivant : https://bitbucket.org/GIF3004/laboratoire4/. Vous retrouverez deux dossiers, correspondant aux deux pilotes que vous devrez implémenter.

La compilation et l'édition de liens d'un module noyau constituent probablement une des tâches les plus délicates qu'un environnement de compilation croisée puisse avoir à accomplir. Étant donné la difficulté de fournir une procédure anticipant tous les environnements de développement potentiels, nous utiliserons une procédure peu orthodoxe : la création du module se fera directement sur le Raspberry Pi, mais vous pourrez tout de même tester la compilation de votre code sur votre poste de travail, avec Eclipse comme pour les laboratoires précédents, afin de faciliter le développement.

La première étape constitue à récupérer l'archive nommée dependancesLab4.tar.gz dans le dépôt Git. Cette archive contient tous les fichiers nécessaires à la production du module noyau. Vous devez décompresser cette archive :

  • 1) Dans le dossier /home/pi de votre Raspberry Pi, c'est-à-dire que le dossier /home/pi/dependancesLab4 devrait exister et contenir les fichiers de l'archive.
  • 2) Dans un dossier de votre choix sur votre station de développement et de compilation croisée. Ici, la position précise dans le système de fichier importe peu, mais rappelez-vous-en, nous y ferons référence plus loin.

Sur Eclipse, créez alors un nouveau projet (langage C seulement) utilisant l'environnement de compilation croisée déjà paramétré dans les laboratoires précédents. Créez un projet vide (sans "helloworld.c" ou autre fichier C déjà présent). Ajoutez-y un des deux fichiers de code fournis (il faudra créer un autre projet et répéter ces manipulations pour le second fichier).

Par la suite, nous devons configurer Eclipse pour qu'il trouve les différents en-têtes nécessaires à la compilation. Il y a quatre changements importants à apporter, et tous sont cruciaux. Prenez le temps de les faire en suivant attentivement les instructions suivantes.

Configuration des chemins des en-têtes

Premièrement, allez dans les propriétés du projet, puis sélectionnez, dans la section C/C++ General, l'item Paths and Symbols. Dans le premier onglet (Includes), ajoutez les répertoires suivants :

Important : modifiez le préfixe de ces chemins pour correspondre à l'endroit où vous avez décompressé l'archive. Par exemple, si vous avez décompressé l'archive dans "/home/richardstallman/setr", alors le premier chemin ("/home/gtm/buildTp4/include") deviendra /home/richardstallman/setr/dependancesLab4/include. Si vous utilisez Windows et que vous avez plutôt décompressé l'archive dans "C:\Users\BillGates\Mes Documents\setr", alors le chemin deviendra C:\Users\BillGates\Mes Documents\setr\dependancesLab4­\include.

Important (2) : assurez-vous que "GNU C" soit sélectionné, et NON Assembly!

Configuration des symboles

Tout en restant sur le même item dans le menu de gauche, déplacez-vous dans l'onglet Symbols et définissez les symboles présentés dans la figure suivante :

Notez que seul LINUX_ARM_ARCH possède une valeur, vous pouvez laisser ce champ vide pour les deux autres.

Configuration de l'éditeur de liens (linker)

Déplacez-vous maintenant dans le menu de gauche, et sélectionnez C/C++ Build -> Settings. Sélectionnez Cross GCC Linker, puis, dans le champ Command line pattern, remplacez la valeur courante par "${COMMAND} -v", comme dans la figure suivante :

Configuration des drapeaux du compilateur

Dans l'item Miscellaneous de Cross GCC compiler, repérez le champ Other flags et remplacez sa valeur par la suivante :

-c -fmessage-length=0 -include /home/gtm/buildTp4/include/linux/kconfig.h   -Wa,-mimplicit-it=thumb -D"KBUILD_STR(s)=#s" -D"KBUILD_BASENAME=KBUILD_STR(setr_driver)"  -D"KBUILD_MODNAME=KBUILD_STR(setr_driver)"

En remplaçant encore une fois /home/gtm/buildTp4 par le chemin de l'archive décompressée.

Vérification

Au final, une fois toutes ces étapes de configuration effectuées, le champ All options de l'élément Cross GCC compiler devrait ressembler à ceci :

Procédure de compilation

Une fois ces étapes effectuées, vous pourrez compiler votre module dans Eclipse comme à l'habitude. Toutefois, le binaire produit n'est pas encore tout à fait prêt à être inséré dans le noyau du système du Raspberry Pi. Pour ce faire, copiez le fichier C sur le Raspberry Pi (rappelez-vous que vous pouvez utiliser le Remote Systems Explorer pour le faire directement dans Eclipse, ou alors utiliser un autre protocole tel que SSH), dans un répertoire contenant le Makefile associé à ce module, disponible sur le dépôt Git.

Par la suite, ouvrez un terminal SSH sur le Raspberry Pi et allez dans le répertoire contenant le fichier C et le Makefile et exécutez la commande make. Si tout se passe bien, plusieurs fichiers devraient apparaître dans le répertoire, donc un fichier .ko. Si ce n'est pas le cas, vérifiez les erreurs rapportées dans la console et tentez de les corriger.

Si vous avez produit avec succès un fichier .ko, vous pouvez maintenant tenter de l'insérer dans le noyau en utilisant sudo insmod nom_du_fichier.ko. Si tout se passe bien, la commande retournera sans erreur et un lsmod confirmera la présence de votre module. Vous pouvez alors le tester!

Notez que comme l'exécution d'un module noyau se fait logiquement en mode privilégié, il est impossible de le lancer en utiliser gdb. Cela vous empêche donc d'utiliser les outils de débogage d'Eclipse. Vous pouvez utiliser printk et autres fonctions pour rapporter de l'information de débogage.

Énoncé

Le code de base et les fichiers Makefile nécessaires à la compilation des modules sont disponibles sur le dépôt Git suivant : https://bitbucket.org/GIF3004/laboratoire4/.

Méthode de lecture du clavier

Il existe sur le marché plusieurs types de claviers. Les plus évolués, tel votre clavier d'ordinateur, possèdent un microcontrôleur leur permettant de communiquer avec l'ordinateur en utilisant un protocole haut niveau (par exemple l'USB ou le Bluetooth). Toutefois, certains claviers sont conceptuellement beaucoup plus simples et nécessitent plus de travail de la part de l'ordinateur. C'est le cas du petit clavier que vous avez en votre possession. Ce clavier possède seulement 8 fils pour 16 touches, il est donc aisé de constater qu'une association 1:1 entre les fils et les touches est impossible.

En fait, chaque fil est relié à une ligne ou à une colonne du clavier. Chaque touche permet quant à elle de mettre en contact une ligne et une colonne. Si on applique par exemple une tension sur la première ligne, alors une pression de la touche A, mettant en contact la première ligne et la quatrième colonne, va faire apparaître cette tension sur la quatrième colonne. Observer cette tension nous permet donc de dire si la touche A est pressée ou non. De même, si une tension est présente sur la seconde ligne et la touche 5 enfoncée, alors cette tension apparaîtra sur la seconde colonne.

On remarque immédiatement un problème potentiel : si on applique à la fois une tension sur la première et la seconde ligne, il devient impossible de différencier une pression de la touche A et de la touche B, puisque ces deux actions produisent le même résultat (une tension sur la dernière colonne). Par conséquent, notre pilote va devoir scanner les lignes, une par une, afin de déterminer précisément quelle touche est enfoncée. En d'autres termes, l'algorithme de lecture sera le suivant :

  1. On applique une tension sur la première ligne.
  2. On vérifie si une tension est apparue sur une des colonnes. Si oui, nous savons qu'une touche est pressée, à l'intersection de la ligne courante et de la colonne sur laquelle une tension a été détectée.
  3. On passe à la seconde ligne et vérifie à nouveau si une tension apparaît sur une des colonnes.
  4. Ainsi de suite, jusqu'à la dernière ligne, après quoi le cycle recommence.

Écriture d'un module : 1) Initialisation

La première tâche d'un module est de s'initialiser en créant les structures de données dont il a besoin. En particulier, n'oubliez pas qu'un module peut être appelé plusieurs fois simultanément, vous devez donc créer et initialiser des primitives de synchronisation tels des mutex pour assurer que ces traitements parallèles s'effectuent sans encombre.

Écriture d'un module : 2) Création de noeuds dans le système de fichiers

La plupart des modules noyau s'interfacent avec le reste du système en utilisant l'abstraction du système de fichiers (rappelez-vous : sous Unix, tout est un fichier!). Les modules créent ainsi un certain nombre de pseudo-fichiers qui peuvent être utilisés pour communiquer avec eux. Dans le cadre de ce laboratoire, nous vous demandons de créer un pseudo-fichier :

  • /dev/claviersetr, un périphérique accessible en lecture seulement en mode caractère. Lorsqu'ouvert en lecture, ce fichier retourne les caractères saisis sur le clavier externe. Lorsqu'aucun caractère n'est disponible, il retourne simplement 0. Vous devez vous assurer de conserver tous les caractères qui n'ont pas encore été lus via ce fichier, même si ce fichier n'est pas lu pendant une longue période, dans la limite de la taille du tampon de votre module!

Écriture d'un module : 3) Accès aux GPIO

L'accès aux GPIO peut être un casse-tête sur des systèmes complexes tels que le Raspberry Pi 3. Heureusement, le noyau Linux fournit une couche d'abstraction pour leur utilisation, dont vous pouvez retrouver la documentation ici. Nous allons donc utiliser ces fonctions au lieu d'interagir directement avec le matériel. Notez que cette API est maintenant dépréciée au profit d'une nouvelle API plus moderne. Toutefois, dans le cadre du laboratoire, nous nous contenterons de l'API "historique" (legacy), qui est beaucoup plus simple à utiliser.

Écriture d'un module : 4) Lecture du clavier par "polling"

Comme première tâche, vous devrez compléter et tester le fichier setr_driver.c. Ce pilote fonctionne sur le principe du polling : un thread noyau est lancé et balaye constamment les lignes du clavier afin de déterminer si une nouvelle touche a été enfoncée. À la fin de chaque balayage, il se met en pause pour une courte période de temps afin d'éviter de monopoliser un processeur.

Complétez ce fichier et vérifiez son bon fonctionnement. En particulier, vérifiez si 1) votre système prend en compte l'appui d'une touche et 2) ne la prend en compte qu'une seule fois lorsqu'elle n'est pas relâchée. Prenez le temps de lire tous les commentaires contenus dans le fichier, ils contiennent des informations importantes qui pourront vous être très utile. Observez également comment la charge processeur varie selon la durée de temps de repos que le thread requiert après chaque itération. Que se passe-t-il si on supprime carrément cette pause?

Écriture d'un module : 5) Lecture du clavier par interruption

Le système de lecture conçu jusqu'à maintenant est relativement peu efficace, puisqu'il oblige le Raspberry Pi à lire systématiquement l'état du clavier, même si rien n'a changé. Une méthode plus efficace à cet égard serait de ne vérifier l'état du clavier que lorsqu'un événement s'est produit. Pour ce faire, nous allons créant un second module utilisant les interruptions. L'idée générale est la suivante :

  1. Lorsqu'aucune touche n'est pressée, les quatre lignes sont mises sous tension. On comprend donc aisément qu'une pression sur une touche, quelle qu'elle soit, fera apparaître une tension sur une des colonnes.
  2. Des interruptions sont liées à un changement de valeur sur les colonnes. Cette mise sous tension sera donc remarquée par le Raspberry Pi qui appellera notre fonction liée à l'interruption.
  3. À ce moment, nous savons seulement qu'une touche a été pressée, mais nous ne savons pas laquelle. Pour la retrouver, il suffit d'utiliser la même approche que celle présentée plus haut.
  4. Tant qu'au moins une touche est enfoncée, nous désactivons les interruptions et continuons à scanner le clavier.

Comme on peut le constater, l'algorithme de lecture reste le même. La différence majeure est plutôt que, cette fois, le système n'a pas besoin d'activement vérifier la pression d'une touche, mais compte sur une interruption pour le lui signaler, ce qui est bien plus efficace.

Pour implémenter cet algorithme, basez-vous sur le fichier setr_driver_irq.c. Ce pilote est très similaire au précédent, mais alloue aussi des interruptions sur les broches de lecture. Par ailleurs, il n'y a plus de thread noyau : à sa place, un tasklet doit être appelé après chaque interruption, interruption dont la durée doit être la plus courte possible. Ce tasklet doit effectuer la tâche qui était précédemment dévolue au thread noyau, à savoir balayer les lignes pour déterminer quelle touche a été pressée. Comme pour la tâche précédente, voyez le fichier en question et en particulier ses commentaires pour plus de détails.

Gestion des appuis multiples

Vous aurez remarqué que l'algorithme suggéré n'est pas exempt de problèmes. En particulier, si plusieurs touches sont pressées simultanément, il se peut que notre pilote "manque" des touches, ou voit au contraire des pressions "fantômes", qui n'existent pas réellement. Il existe plusieurs solutions, matérielles ou logicielles, pour régler ce problème, jusqu'à un certain point. Implémentez-en une qui supporte la pression simultanée d'au moins deux (2) touches et démontrez son efficacité. Notez bien que vous n'avez évidemment pas le droit de changer de clavier... Pour ceux qui veulent aller plus loin, serait-il possible de gérer la pression simultanée de trois touches?

Débogage et tests

Il est peu probable que vos modules fonctionnent parfaitement du premier coup, et ce même s'ils compilent sans erreur. Malheureusement, il existe peu de procédures de débogage simples dans le cadre d'un développement en espace noyau. Le plus simple reste encore d'utiliser des printk à intervalle régulier : vous pourrez ensuite les visualiser en temps réel en utilisant la commande dmesg -Hw. Faites attention de ne pas trop imprimer de messages de débogage cependant; saturer le noyau de chaînes de caractères risque fort de poser problème!

Il peut arriver que votre module soit entré dans un état invalide et qu'il ne soit plus possible de le retirer, même en utilisant rmmod ou modprobe -r. Dans ces situations, la seule méthode permettant de revenir à un environnement correct est malheureusement de redémarrer le Raspberry Pi. Il en va de même pour les corruptions mémoire que votre module pourrait causer : n'oubliez pas qu'en espace noyau, les erreurs de segmentation n'existent pas et que ce n'est pas une bonne nouvelle pour vous! Écrire à des emplacements mémoires qui ne vous appartiennent pas peut résulter en toutes sortes de conséquences sur le reste du système...

Pour tester la sortie de votre périphérique (autrement dit, s'il renvoie bien les touches pressées, dans le bon ordre), vous pouvez utiliser la commande suivante :

sudo tail -f /dev/claviersetr ---disable-inotify

Cette commande lit votre pseudo-fichier à intervalle régulier (à chaque seconde par défaut) et afficher les nouveaux caractères au fur et à mesure. Notez que le paramètre disable-inotify doit bel et bien être précédé de trois tirets!

Modalités d'évaluation

Le laboratoire comporte deux livrables :

  1. Module du pilote effectuant une lecture du clavier par "pooling" (fichier setr_driver.c);
  2. Module du pilote effectuant une lecture du clavier par interruption (fichier setr_driver_irq.c).

Ce travail doit être réalisé en équipe de deux, la charge de travail étant à répartir équitablement entre les deux membres de l'équipe. Aucun rapport n'est à remettre, mais les deux équipiers doivent être en mesure d'expliquer leur approche et de démontrer le bon fonctionnement de votre pilote. Cette évaluation sera faite lors de la séance de laboratoire du 7 avril 2017, et les deux équipiers doivent y être présents. Ce travail compte pour 10% de la note totale du cours.

Le barême d'évaluation détaillé sera le suivant (laboratoire noté sur 20 points) :

  • (2 pts) Le module noyau se charge sans erreur et s'initialise correctement.
  • (5 pts) Pour le premier module, le clavier est lu par polling correctement (les valeurs retournées sont les bonnes, dans le bon ordre).
  • (5 pts) Pour le second module, les interruptions sont bien gérées et le clavier est lu sans nécessiter un polling continuel lorsqu'aucune touche n'est enfoncée.
  • (3 pts) Le fichier /dev/claviersetr est bien créé et fonctionne comme demandé.
  • (2 pts) La synchronisation entre le thread d'écriture et la fonction de lecture est adéquate, de même que la gestion du tampon circulaire.
  • (1 pts) Le pilote gère la pression simultanée de plusieurs touches (au moins 2).
  • (2 pts) Les étudiants sont en mesure d'expliquer l'approche utilisée et de répondre aux questions concernant leur code.

Ressources et lectures connexes