Rapport de mini-projet Linux embarqué
Transcription
Rapport de mini-projet Linux embarqué
LECANU Sandra NURY Sylvain 3A ESE Rapport de mini-projet Linux embarqué M. Corneloup M. Pépin 2009/2010 Sommaire I : Cahier des charges II : Description du matériel mis à disposition III : Cross développement IV : Compilation du noyau linux embarqué V : Mise en place du noyau linux créé sur la carte VI : Implantation d’un programme sur la cible VII : Driver Les drivers caractères Les drivers blocs LECANU Sandra NURY Sylvain Page 2 I : Cahier des charges La finalité de notre mini projet est de porter le protocole CAN sur le bus spi de la carte, le tout étant géré par un driver sous linux embarqué. Dans un premier temps, nous devrons nous familiariser avec l’environnement linux. Puis, nous devrons configurer et générer le noyau linux pour l’implanter sur la carte. Dans un troisième temps, nous devrons créer et insérer des modules dans le noyau. Ensuite, nous devrons nous familiariser avec les drivers linux. Enfin, nous devrons utiliser le bus spi afin d’y porter le protocole CAN. II : Description du matériel mis à disposition Un ordinateur sous linux Un ordinateur sous Windows avec port série et HyperTerminal Un kit de développement sk27 basé sur le processeur arm9, possédant entre autre des ports série, Ethernet et Spi Cette liste de matériels n’est pas optimale en effet, la configuration minimale est un pc sous linux possédant un port série, un port Ethernet et l’application Minicom afin de développer nos programmes et la carte cible sk27 pour laquelle on développe. Si nous avons opté pour ce choix, c’est parce que le pc mis à notre disposition n’avait pas de port série. Afin de contrecarrer ce manque, nous avons essayé d’utiliser un câble série USB malheureusement, le port Minicom ne le reconnaissait pas. C’est pourquoi, nous avons connecté la cible à un ordinateur possédant un port série. Ce dernier fonctionne sous Windows NT afin d’avoir accès à l’application HyperTerminal. Pour le choix de la version de Microsoft Windows NT, cette option s’est quelque peu imposée à nous à cause de la faible puissance de calcul de l’ordinateur mis à notre disposition. Mais tout système d’exploitation comportant une application pour communiquer via le port série. Schéma du dispositif Alimentation 0-5V Switch PC sous linux Légende : Carte sk27 PC sous Windows NT Liaison Ethernet Liaison série Câble d’alimentation LECANU Sandra NURY Sylvain Page 3 III : Cross développement On parle de cross développement ou développement croisé lorsque l’on écrit un programme sur un ordinateur mais que ce dernier est compiler pour une autre machine. Ainsi le développement et l’exécution du programme ne se font pas sur le même microprocesseur. 3.1 : Nécessité du cross développement Dans le cadre de notre mini projet, nous devons porter l’Operating System Linux embarqué sur le kit sk27. Nous ne pouvons utiliser que le kit sk27 car « à vide », celui-ci ne comporte que le bootloader qui sert à connaître le début du code que l’on veut exécuter. De plus, il n’y a aucune interface pour le développement. Nous ne pouvons donc pas travailler directement sur la carte sk27 c’est pourquoi, le cross développement s’est imposé à nous. 3.2 : Fonctionnement du cross développement Pour notre travail, nous avons utilisé le développement croisé à deux niveaux avec deux procédés différents. En effet, nous avons mis en œuvre cet outil pour la génération et l’implantation du noyau puis pour tout ce que nous avons rajouté au noyau. Ainsi, pour la première étape, c’est la cible à l’aide de son U-boot qui va chercher le développement fait sur le PC hôte via le réseau Ethernet et la protocole tftp. Alors que pour la seconde, c’est le PC qui envoie les informations à la cible grâce au protocole gftp. Dans ce procédé, la cible n’est plus l’acteur des transferts ; le rôle de l’ordinateur hôte et celui de la cible sont donc inter changés par rapport au premier procédé où l’acteur était la cible et le PC hôte vu juste comme une adresse IP. IV : Compilation du noyau linux embarqué Avant de compiler le noyau linux, on a besoin de mettre en place la chaîne de cross compilation car il n’est pas possible d’utiliser le compilateur gcc. En effet, on doit utiliser un compilateur compatible avec la carte de développement or gcc ne l’ai pas. Il faut donc dans un premier temps installer, dans le répertoire local de linux du PC hôte, la toolchain , chaîne d’outil spécifique pour les plates-formes arm. Cette dernière est présente sous la forme de fichiers compressés sur le CD d’installation de la carte. Ainsi, après avoir copié l’intégralité du CD d’installation sur l’ordinateur sous linux, il faut décompresser dans le répertoire de linux /usr/local, le fichier arm-ssv1-linus présent dans l’arborescence suivante : CD/ANP9200/toolchain/arm-ssv1-linux.tgz. Pour cela, on passe en ligne de commandes et on tape les lignes suivantes. cd /usr/local tar –xzf /home/ese/CD1/ADNP9200/toolchain/arm-ssv1-linux.tgz cd : Navigation dans le répertoire /usr/local tar –xzf : Décompression de l’archive .tgz. Après avoir installé la chaîne de compilation, on peut dès lors compiler le noyau linux que l’on veut exécuter sur la carte. Pour cela, toujours en ligne de commandes, on se place LECANU Sandra NURY Sylvain Page 4 dans le noyau linux présent dans le répertoire linux 2.6.16.20atssv2 du CD d’installation. Puis, il suffit d’exécuter successivement les commandes suivantes. cd/MiniprojetLN/noyauLinux/linux-2.6.16.20-at91-ssv2 make menuconfig make Image make modules make modules-install make menuconfig : Cette commande ouvre un gestionnaire graphique qui nous permet de configurer notre noyau. Ainsi, nous pouvons choisir pour quel type de processeur on veut compiler soit dans notre cas le processeur atmel ADNP9200. De plus, c’est dans ce gestionnaire que nous pouvons ajouter des modules au démarrage du noyau comme le driver spi que nous utiliserons. Aperçu du gestionnaire menuconfig Le changement de configuration est alors prise en compte dans les fichiers de configurations .config présents à tous les étages de l’arborescence linux. Vous trouverez cidessous un extrait du .config au plus haut de l’arborescence de notre noyau linux. LECANU Sandra NURY Sylvain Page 5 Exemple de fichier de configuration Nous pouvons observer le support SPI. Ce dernier nous confirme que nous avons bien configuré le bus SPI puisque CONFIG_SPI, CONFIG_SPI_DEBUG et CONFIG_SPI_MASTER sont suivis d’un « y » pour yes. De même, CONFIG_SPI_BITBANG a bien été installé. Cependant, nous pouvons aussi observer qu’il n’y a pas de protocoles pour les masters. make Image : Cette commande permet de générer l’image du noyau à partir des fichiers de configuration précédemment définis. make modules : Cette commande permet de générer les modules qui ont été crées et configurés pour être lancés au démarrage du noyau. Dans notre cas, nous n’avons créé aucun module de ce type. Cependant, nous exécutons tout de même cette commande pour générer tous les modules initialement créés. En effet, nous n’avons pas minimisé le noyau linux c’est pourquoi par précaution il vaut mieux l’exécuter. make modules-install : Cette commande permet d’installer les modules précédemment générés. Ainsi, ils sont intégrés à l’image du noyau précédemment générée. LECANU Sandra NURY Sylvain Page 6 Après avoir exécuté les commandes précédentes, l’image du noyau linux a bien été construite dans le répertoire mkimage du noyau linux. Elle porte le nom suivant : imgdnp9200. Nous allons donc maintenant voir comment mettre cette image du noyau linux dans la mémoire de la carte afin que celle-ci l’exécute et boot alors sur celle-ci et non plus sur le linux initialement installé dans la mémoire flash de cette dernière. V : Mise en place du noyau linux créé sur la carte Afin de mettre en place le noyau linux sur la carte, il faut transférer celui-ci par le serveur tftp. En effet la carte sk27 est configurée pour utiliser ce type de serveur afin de télécharger un noyau linux. Toutefois, par défaut elle va chercher le serveur tftp à l’adresse ip 192.168.0.1. Bien que cela soit possible nous avons décidé de ne pas la changer. Il faut donc modifier l’adresse ip de l’ordinateur hôte. Cependant le réseau de l’ENSEA comportant un nombre conséquent d’adresses ip déjà attribuées, nous avons donc déconnecté le PC du réseau afin de ne pas provoquer de conflits d’adresses. Pour cela, nous avons déconnecté le switch auquel est relié le PC hôte et la carte du réseau local. Puis nous avons changer l’adresse ip de l’ordinateur par 192.168.0.1 ainsi que son masque réseau en 255.255.255.0. Pour faire cela, il faut aller dans le menu Configurer votre ordinateur de linux puis dans Réseau et internet et enfin dans Centre réseau. Après avoir exécuté cette manipulation, il ne faut oublier de se déconnecter si cela n’avait pas été fait puis de se reconnecter au réseau. En effet, sans cela, les modifications peuvent ne pas être prises en compte et la carte ne pourra trouver son serveur tftp par défaut. Ensuite, il faut vérifier qu’un serveur tftp est bien installé sur l’ordinateur. Pour cela, il suffit de regarder les différents modules installés dans linux et de vérifier s’il y en a un. Si ce n’est pas le cas, il faut alors en télécharger un sur internet puis l’installer. Mais attention l’installation ne peut se faire que si nous avons les droits administrateurs, si ce n’est pas le cas, il faut le faire en super utilisateur. Néanmoins, il se peut que le serveur tftp soit correctement installé et fonctionne normalement sans pour autant apparaître dans les modules linux. Mais nous pouvons vite nous en rendre compte en essayant en ligne de commandes de le réinstaller car dans ce cas, un message s’affichera pour nous l’indiquer. Dès que le serveur tftp a été installé, il faut copier l’image du noyau linux généré qui se trouve dans le répertoire mkimage de ce noyau, dans le répertoire var/lib/tftpboot du linux tournant sur l’ordinateur hôte. Attention tout comme précédemment, il faut avoir les droits administrateurs pour exécuter ceci car le répertoire de destination est protégé en écriture. Le serveur est alors prêt à être utilisé par la carte sk27. Il ne reste plus qu’à ce que la cible se connecte à ce serveur tftp fraîchement créé. Pour cela, il faut utiliser l’ordinateur sous Windows NT. En effet, c’est via HyperTerminal que l’on va pouvoir connecter la carte au serveur. Dans un premier temps, il faut se mettre en Uboot sur la carte. Il faut donc reseter la carte puis arrêter l’autoboot en appuyant sur n’importe quelle touche. Ensuite, il faut taper les commandes suivantes toujours dans HyperTerminal. LECANU Sandra NURY Sylvain Page 7 tftpboot bootm tftpboot : Cette commande permet de se connecter au serveur tftp qui par défaut est à l’adresse 192.168.0.1. La cible va alors aller chercher l’image du noyau linux que l’on a mise dans le répertoire var/lib/tftpboot et la mettre dans sa mémoire. Cependant, cette commande ne permet pas de conserver l’image après un reset de la carte. bootm : Cette commande permet d’exécuter ce nouveau noyau linux. La carte reboot alors sur celui-ci. VI : Implantation d’un programme sur la cible Pour implanter du code sur le noyau linux embarqué, il y a des procédés possibles. Dans les deux cas, le début est le même. Ainsi, il faut écrire le programme sur l’ordinateur hôte et l’inclure dans un module. Ce qui diffère est ce qu’on fait du module. En effet, le premier procédé consiste à inclure le module créé dans le système de fichiers du noyau pour qu’à la commande make modules (qui serait alors dans un fichier de configuration), celui-ci s’inclut au démarrage du noyau. L’inconvénient est qu’il faut recharger le noyau linux sur la carte à chaque modification du module ou à chaque création de nouveau module. Le deuxième procédé consiste à utiliser un serveur gftp, un protocole qui est supporté par linux embarqué, afin de l’inclure directement sur la carte. C’est cette méthode que nous avons choisie d’utiliser. Ci-dessous, vous pourrez trouver un imprime écran du serveur gftp. Pour le faire fonctionner, il suffit de rentrer l’adresse ip de la carte pour l’hôte. Pour la connaître, il faut taper la commande ifconfig dans HyperTerminal et il ne reste plus qu’à la lire. Ensuite, il faut rentrer le nom de l’utilisateur soit root ainsi que le mot de passe sachant que par défaut, il n’y en a pas. Ensuite, une petite présentation de l’application s’impose. Dans la fenêtre de gauche se trouve l’arborescence de linux présent sur le PC hôte. Quant à la fenêtre droite, dès lors qu’on se connecte à la carte, on peut lire l’arborescence du linux embarqué. Il ne reste alors plus qu’à copier grâce à la flèche le module que l’on a créé dans un répertoire du linux embarqué. On peut choisir le répertoire tmp pour temporaire car il s’y prête très bien. Après toutes ces étapes, le module est téléchargé sur la carte, il n’y aura alors plus qu’à l’exécuter directement à partir du root de la cible. LECANU Sandra NURY Sylvain Page 8 On peut alors créer des modules que l’on peut implanter sur la cible et « exécuter » via hyperterminal ou telnet. Afin de construire un module, il faut le créer et le compiler : -création du module : Un module basique (mon_module.c) a la structure suivante : /* mon_module.c */ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> static int mon_module_init (void) { printk(KERN_ALERT "Initialisation du module\n"); return 0; } static void mon_module_exit (void) { printk(KERN_ALERT "Sortie du module\n"); } LECANU Sandra NURY Sylvain Page 9 module_init(hello_init); module_exit(hello_exit); /*macros définissant les fonctions d‟entrées/de sorties du module*/ MODULE_LICENSE ("GPL"); -compilation du module : $(MAKE) ARCH=arm CROSS_COMPILE=/usr/local/arm-ssv1linux/bin/arm-ssv1-linux- -C $(KDIR) SUBDIRS=$(PWD) modules Une fois ces étapes effectuées, on télécharge le fichier .ko généré via la connexion gftp puis on peut éxécuter les commandes suivantes dans la console en telnet (ce qui nécessite de connaitre l’adresse ip de la carte que l’on obtient par la commande ifconfig dans l’hyperterminal) : insmod module.ko : cette commande permet de lancer le module, ie d’éxécuter la fonction init() du module rmmod module.ko : cette commande permet d’arrêter le module, ie d’éxécuter la fonction exit() du module Si l’on effectue des modifications dans ce module et qu’on veut le recharger sur la carte, il faut préalablement supprimer l’ancien module via gftp et recharger le nouveau fichier module.ko VII : Driver Les drivers caractères Pour développer un driver, il faut bien prendre conscience des trois espaces de travails suivants : l’espace matériel (hardware space) ; l’espace noyau (kernel space) ; l’espace utilisateur (user space). L’espace matériel (hardware space) correspond simplement aux périphériques L’espace noyau (kernel space) est Linux, où se situent les drivers. L’espace utilisateur (user space) correspond à l’espace des applications Nous allons maintenant voir comment développer un driver caractère. A/ Principales structures du noyau à utiliser FILE OPERATION (dans linux/fs.h) LECANU Sandra NURY Sylvain Page 10 Il s’agit d’un ensemble de pointeurs de fonctions qui correspond à une liste d’opérations qu’une application peut invoquer sur un périphérique. La structure est représentée ci-dessous et ne contient que les fonctions principales devant être implémentées par le programmeur. Cette structure doit être définie en tête de votre fichier principal. struct file_operations mon_module_fops = { .owner = THIS_MODULE, .read = mon_module_read, .write = mon_module_write, .ioctl = mon_module_ioctl, .open = mon_module_open, .release = mon_module_release, ... }; FILE STRUCTURE : se situe dans <linux/fs.h> Cette structure est créée par le noyau à chaque fois que le fichier spécial (special file, voir plus loin) correspondant est ouvert et est transmis à chaque fonction qui agit sur le fichier. Le programmeur ne doit pas créer cette structure, mais juste utiliser certains éléments en fonction de ses nécessités. struct file { const struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; void *private_data; ... }; CDEV STRUCTURE : se situe dans <linux/cdev.h> Le noyau représente un périphérique caractère (character device) par la structure cdev. struct cdev { struct kobject kobj; struct module *owner; struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; }; Les fonctions suivantes devront être utilisées dans la fonction d’initialisation et de sortie du module. Dans la fonction d’initialisation : void cdev_init(struct cdev *cdev, struct file_operations *fops); int cdev_add(struct cdev *cdev, dev_t num, unsigned int count); LECANU Sandra NURY Sylvain Page 11 Dans la fonction de sortie : void cdev_del(struct cdev *cdev); mon_module_dev STRUCTURE : que vous créer dans votre fichier " .h " Chaque périphérique (device) est représenté par une structure : struct mon_module_dev { struct cdev cdev; /*Char device structure*/ ...... } Cette structure doit être allouée dynamiquement et sera désallouée à la fin de l’utilisation du périphérique. On intègre à notre propre structure, la structure cdev. B/ Quelques outils pour l’implémentation de driver Allocation dynamique de variables et de structures La fonction utilisée pour l’allocation dynamique de structures dans le noyau s’apelle kmalloc. Celle pour libérer l’espace alloué est kfree(). (les prototypes sont dans <linux/slab.h>) void *kmalloc(size_t size, int priority) size : taille en octet de l’espace mémoire à allouer ; priority : GFP_KERNEL : allocation mémoire " normale du noyau P_USER : allocation mémoire pour l’espace utilisateur … retourne un pointeur de la mémoire allouée ou NULL si non alloué. void kfree(const void *ptr) ptr : pointeur retourné par la fonction kmalloc() Méthodes de débogage Lecture directe des messages du noyau Pour ce faire, on peut intégrer dans les fonctions le printk() (fonction propre au noyau). Ensuite, pour voir les messages, on peut ouvrir une console et taper tail -f /var/log/messages afin de voir les messages. Plus simplement, on peut le voir dans l’hyperterminal ou le telnet qui affichent tous les messages présents dans ce répertoire. La spécification des flags du noyau dans printk() peut être: KERN_ALERT : action devant être effectuée immédiatement ; KERN_INFO : information ; KERN_DEBUG : messages de débogage. … LECANU Sandra NURY Sylvain Page 12 Exemple d’utilisation : printk(KERN_INFO ‘Ceci est mon module !!\n’’) C/ Étapes de codage Paramètres du module La commande lsmod permet d’afficher la liste des modules insérés dans le noyau. La commande modinfo nom_du_module permet de voir les informations de base du module Lors de la création du module, on peut écrire les informations du programme que l’on développe par : MODULE_AUTHOR("Mon nom"); MODULE_DESCRIPTION("Device driver"); MODULE_SUPPORTED_DEVICE("Reference(s) of peripheral(s)"); MODULE_LICENSE(“GPL/BSD”); Paramètres d’enregistrement du module au sein du noyau Chaque pilote est enregistré au sein du noyau à l’aide de deux numéros. le " major number " le " minor number " Le " major number " permet au système de fichier virtuel (Virtual File System – VFS) d’identifier le pilote. Le " minor number " permet au pilote d’identifier le périphérique utilisé. On décide d’allouer statiquement ou dynamiquement le " major number " pour le module. Le plus simple est de l’allouer dynamiquement : on laisse le noyau allouer le numéro. Remarque : la liste des numéros déjà alloués est visualisable dans le fichier documentation/devices. Exemple de l’allocation dynamique: #define N_DEV 1 mon_module_major = 0; mon_module_minor = 0; static dev_t devno; /*To store Major and minor numbers*/ static int __init mon_module_init(void) { int result; /*Get the device numbers*/ result = alloc_chrdev_region(&devno, mon_module_minor, N_DEV, “mon_module”); mon_module_major = MAJOR(devno); printk(KERN_INFO “MON_MODULE: MAJOR: %d\nMON_MODULE: MINOR:%d\n”, mon_module_major, mon_module _minor); if(result < 0) { printk(KERN_WARNING “MON_MODULE: can‟t get major %d\n”, mon_module_major); return result; } ... } LECANU Sandra NURY Sylvain Page 13 Prototypes : /*Allocation statique (non utilisé ici)*/ int register_chrdev_region(dev_t devno, unsigned int count, const char *name) /*Allocation dynamique*/ int alloc_chrdev_region(dev_t *devno, unsigned int baseminor, unsigned int count, const char *name) devno : variable noyau de type dev_t contenant le " major number ", ainsi que le " minor number " baseminor : la première valeur que l’on veut attribuer au périphérique. En général " 0 " count : le nombre de périphériques utilisant le driver name : le nom de votre périphérique ; return 0 si succès, valeur négative si échec. Remarque : MKDEV (associe le " major " et " minor " dans une seule variable donnée en paramètre) et MAJOR (extrait le " major " de la variable dev_t donnée en paramètre) sont des macros. Ajout du pilote au sein du noyau Il s’agit du point d’entrée du module. Le programmeur implémente la fonction mon_module_init() et précise, à la fin du programme, que cette fonction correspondra au point d’entrée du module par l’intermédiaire de la macro module_init(). L’argument étant le nom de la fonction. Exemple : module_init(mon_module_init); L’exemple suivant correspond à l’initialisation et l’ajout de la structure cdev pour le noyau. Exemple : static int __init mon_module_init(void) { struct mon_module_dev *sdev = NULL; /*Allocate a private structure and reference it as driver‟s data*/ sdev = (struct mon_module_dev *)kmalloc(sizeof(struct mon_module_dev), GFP_KERNEL); if(sdev == NULL) { printk(KERN_WARNING "MON_MODULE: unable to allocate private structure\n"); return -ENOMEM; } /*Get the device numbers*/ ... /*Init the cdev structure*/ cdev_init(&sdev->cdev, &mon_module_fops); sdev->cdev.owner = THIS_MODULE; ... /*Add the device to the kernel*/ LECANU Sandra NURY Sylvain Page 14 cdev_add(&sdev->cdev, mon_module_minor, 1); ... return 0; } THIS_MODULE permet de dire au noyau que la structure ne sera utilisé que par notre module. Prototypes : void cdev_init(struct cdev *cdev, struct file_operations *fops) fops : correspond à mon_module_fops, il s’agit de notre file operation. int cdev_add(struct cdev *cdev, dev_t num, unsigned int N_DEV) retourne 0 si réussi, valeur négative si erreur. Suppression du pilote au sein du noyau Il s’agit du point de sortie module. Le programmeur implémente la fonction mon_module_exit() et précise, à la fin du programme, que cette fonction correspondra au point de sortie du module par l’intermédiaire de la macro module_exit(). L’argument étant le nom de la fonction mon_module_exit() qui doit désallouer tout ce qui a été alloué durant l’initialisation. Exemple : module_exit(mon_module_exit); Exemple : static void __exit mon_module_exit(void) { struct mon_module_dev *sdev; /*Delete the cdev structure*/ cdev_del(&sdev->cdev); /*Free the allocated memory*/ kfree(sdev); /*Unregister Driver from the kernel*/ unregister_chrdev_region(devno, N_DEV); } Prototypes : void unregister_chrdev_region(dev_t devno, unsigned int N_DEV) Ouverture du périphérique : fonction open() Cette fonction initialise le périphérique : elle identifie le périphérique qui doit être ouvert, initialise la structure filp->private_data, correspondant à un pointeur de structure utilisé comme " sauvegarde " de la structure principale sdev . L’argument inode est utilisé pour effectuer le lien avec la structure cdev. Cependant, puisqu’il est nécessaire d’obtenir la structure complète de votre périphérique (mon_module_dev), incluant la structure cdev, on utilise la macro container_of afin d’obtenir avec précision la structure voulue. LECANU Sandra NURY Sylvain Page 15 Exemple : static int mon_module_open(struct inode *inode, struct file *filp) { int minor; struct mon_module_dev *sdev = NULL; /*Device information*/ /*Allocate and fill any data structure to be put in filp->private_data*/ sdev = container_of(inode->i_cdev, struct mon_module_dev, cdev); filp->private_data = sdev; return 0; } Macro : container_of(pointer, container_type, container_field) : retourne la structure de type container_type dont le champ container_field contient la valeur pointer. Prototypes : int mon_module_open(struct inode *inode, struct file *filp) inode : structure interne au noyau, non utilisée par le programmeur ; filp : correspond à mon_module_fops, il s’agit de votre file operation ; Fermeture du périphérique : fonction release() Cette fonction est appelée pour la fermeture du ou des périphériques à la fin de leur utilisation et doit désallouer tout ce qui a été alloué par la méthode open. Exemple : int mon_module_release(struct inode *inode, struct file *filp) { struct mon_module_dev *sdev; filp->private_data = NULL; sdev = NULL; return 0; } Prototype : int mon_module_release(struct inode *inode, struct file *filp) inode : structure interne au noyau, non utilisé par le programmeur ; filp : correspond à mon_module_fops, il s’agit de votre file operation ; Communications entre espaces a) La liaison entre l’espace matériel et l’espace noyau s’effectue par l’intermédiaire d’un gestionnaire de mémoire (memory management). Le système d’adressage du périphérique étant différent de celui du noyau, le gestionnaire de mémoire traduit les adresses physiques du périphérique en adresses virtuelles pour l’espace noyau. LECANU Sandra NURY Sylvain Page 16 On doit savoir si notre périphérique effectue la liaison avec notre ordinateur par l’intermédiaire de ports d’entrées/sorties ou par l’allocation mémoire d’entrées/sorties. Le code sera alors différent en fonction de cette caractéristique. Ports d’entrées/sorties La fonction request_region() permet de demander au noyau que l’on souhaite l’utilisation de ports d’entrées/sorties pour votre périphérique. Exemple : static int __init mon_module_init(void) { struct mon_module_dev *sdev = NULL; int *port = NULL; int i; /*Allocate a private structure and reference it as driver‟s data*/ ... /*Get the device numbers*/ ... /*Init the cdev structure*/ ... /*Requested I/O ports*/ port = request_region(FIRST_PORT, PORT_NBER, „‟my_device‟‟); if(port == NULL) printk(KERN_ERR „‟MON_MODULE: Error port attribution\n‟‟); /*Add the device to the kernel*/ ... return 0; } Une ressource étant allouée lors de l’initialisation, il faut donc la désallouer lors de la fin d’utilisation. Exemple : static void __exit mon_module_exit(void) { struct mon_module_dev *sdev; int i; /*Delete the cdev structure*/ ... /*Free the allocated memory*/ ... /*Release I/O ports*/ release_region(FIRST_PORT, PORT_NBER); /*Unregister Driver from the kernel*/ ... } Vous pouvez utiliser les commandes cat, more ou encore less pour obtenir les informations concernant l’adressage virtuel de votre système dans les fichiers /proc/ioports s’il s’agit de ports d’entrées/sorties ou /proc/iomem s’il s’agit de plages mémoire utilisées. Variables : LECANU Sandra NURY Sylvain Page 17 FIRST_PORT : numéro de port que vous aurez attribué dans un fichier .h en hexadécimal ; PORT_NBER : nombre de ports que vous aurez attribués dans un fichier .h en hexadécimal. Prototypes : <linux/ioport.h> struct ressource *request_region(unsigned long first, unsigned long n, const char *name) return NULL si échec. viod release_region(unsigned long start, unsigned long n) Mémoires d’entrées/sorties La fonction request_mem_region() permet de demander au noyau une attribution de plage mémoire pour votre périphérique. Exemple : static int __init mon_module_init(void) { struct mon_module_dev *sdev = NULL; int *mem = NULL; int i; /*Allocate a private structure and reference it as driver‟s data*/ ... /*Get the device numbers*/ ... /*Init the cdev structure*/ ... /*Requested I/O memories*/ mem = request_mem_region(BaseAddr, LenAddr, „‟my_device‟‟); if(mem == NULL) printk(KERN_ERR „‟MON_MODULE: Error memory allocation\n‟‟); /*Add the device to the kernel*/ ... return 0; } De même, la ressource allouée doit pouvoir être désallouée. Exemple : static void __exit mon_module_exit(void) { struct mon_module_dev *sdev; int i; /*Delete the cdev structure*/ ... /*Free the allocated memory*/ ... /*Release I/O memories*/ release_mem_region(BaseAddr, LenAddr); /*Unregister Driver from the kernel*/ ... LECANU Sandra NURY Sylvain Page 18 } Variables : BaseAddr : adresse de base en hexadécimal ; LenAddr : longueur de la plage d’adresse souhaitée en hexadécimal. Prototype : <linux/ioport.h> struct ressource *request_mem_region(unsigned long start, unsigned long length, char *name) b) La liaison entre l’espace noyau et l’espace utilisateur s’effectue par l’intermédiaire d’un fichier spécial (special file ou inode). Les fichiers spéciaux sont visualisables dans le répertoire /dev/. Si l’on tape la commande ls –l, on peut voir : -La nature du périphérique : « c » pour caractère, « b » pour bloc (première lettre) -Les nombres au milieu correspondent au numéro majeur et mineur -La dernière colonne correspond au nom de l’inode rattaché Transfert de données entre espace matériel et espace noyau Afin de coder cette partie, on doit bien connaitre le périphérique (ses registres) que l’on utilise. C’est la fonction d’interruption qui gère ces transferts de données. Le périphérique génère une interruption sur le processeur de la machine lorsque le périphérique souhaite communiquer des informations à l’utilisateur ou lorsque le périphérique reçoit ou émet une information. Exemple : static int __init mon_module_init(void) { struct mon_module_dev *sdev = NULL; int req; /*Allocate a private structure and reference it as driver‟s data*/ ... /*Get the device numbers*/ ... /*Init the cdev structure*/ ... /*Requested I/O ports or I/O memories*/ ... /*Add the device to the kernel*/ ... /*Declare the interrupt line*/ req = request_irq(sdev->irq, mon_module_interrupt, IRQF_SHARED, "Device_name", sdev); if(req < 0) { printk(KERN_WARNING "MON_MODULE: Can‟t get assigned irq %d\n", sdev>irq); return 0; } LECANU Sandra NURY Sylvain Page 19 Prototype : int request_irq(unsigned int irq, mon_module_interrupt, unsigned long flags, const char *dev_name, void *dev_id) irq : numéro de la ligne d’interruption ; mon_module_interrupt : nom du prototype de la fonction d’interruption ; flags : permet de définir le type de l’interruption ; o SA_INTERRUPT : pour interruption "rapide" o SA_SHIRQ : pour ligne d’interruption partagée o (liste non exhaustive) dev_name : le nom de votre périphérique ; dev_id : numéro d’identification du périphérique (pour interruption partagée). Ce que doit faire la fonction d’interruption : Identifier la raison de l’interruption. Changer la valeur du bit signalant l’interruption afin de permettre d’autres interruptions. Appeler les fonctions du noyau pour la lecture ou l’écriture de données en fonction de la source d’interruption. Les fonctions appelées dépendent de la taille d’adressage des registres. Il peut s’agir de 8, 16 ou 32 bits. Pour écrire ou lire, on utilise des fonctions du type ioread()/readw() ou iowrite()/writew() Exemple : irqreturn_t mon_module_interrupt(int irq, void *dev_id, struct pt_regs *regs) { struct mon_module_dev *sdev = (mon_module_dev *)dev_id; /*Le code dépend de votre périphérique*/ /*Identifier la raison de l‟interruption*/ /*Changer la valeur du bit signalant l‟interruption*/ /*Appeler les fonctions du noyau pour la lecture ou l‟écriture des données*/ return IRQ_HANDLED; } Prototypes : irqreturn_t mon_module_interrupt(int irq, void *dev_id, struct pt_regs *regs) irq : le numéro de l’interruption concernée ; dev_id : utilisé pour les lignes d’interruption partagées. Identifiant complémentaire de l’interruption lorsque la ligne d’interruption est partagée ; Regs : rarement utilisé. Non utilisé dans notre cas (registres stockés sur la pile) ; return IRQ_HANDLED (si l’interruption a bien été gérée). Là où les lignes d’interruptions sont une ressource allouée, il ne faut pas oublier de la désallouer lors de l’arrêt du périphérique. Exemple : LECANU Sandra NURY Sylvain Page 20 static void __exit mon_module_exit(void) { struct mon_module_dev *sdev; /*Delete the cdev structure*/ ... /*Free the allocated memory*/ ... /*Free the interrupt line*/ free_irq(sdev->irq, sdev); /*Free the virtual memory addresses*/ ... /*Release I/O ports or I/O memories*/ ... /*Unregister Driver from the kernel*/ ... } Prototype : void free_irq(unsigned int irq, void *dev_id) irq : numéro de la ligne d’interruption ; dev_id : identifiant complémentaire de la ligne d’interruption. Transfert de données entre espace noyau et espace utilisateur On doit coder les deux fonctions mon_module_read() et mon_module_write() en y intégrant les appels aux fonctions suivantes, respectivement copy_to_user() et copy_from_user(), qui effectuent la copie des données et non le transfert. Prototypes : ssize_t mon_module_read(struct file *filp, char __user *buffer, size_t count, loff_t *offp) filp : correspond à mon_module_fops (file operation) buffer : mémoire tampon de l’espace utilisateur où seront placées les données count : taille des données à transférer offp : pointeur des données en cours de traitement retourne le nombre d’octets lus. ssize_t mon_module_write(struct file *filp, const char __user *buffer, size_t count, loff_t *offp) buffer : mémoire tampon de l’espace utilisateur où sont les données à copier dans l’espace noyau count : taille des données à transférer offp : pointeur des données en cours de traitement retourne le nombre d’octets écrits. unsigned long copy_to_user(void __user *to, const void *from, unsigned long count) to : mémoire tampon de l’espace utilisateur ; from : mémoire tampon de l’espace noyau ; count : nombre d’octets à copier en une fois, à l’appel de la fonction copy_to_user ; LECANU Sandra NURY Sylvain Page 21 return la quantité de mémoire restante à être copiée. unsigned long copy_from_user(void *to, const void __user *from, unsigned long count) to : mémoire tampon de l’espace noyau ; from : mémoire tampon de l’espace utilisateur ; count : nombre d’octets à copier en une fois, à l’appel de la fonction copy_from_user return la quantité de mémoire restante à être copiée. ; Création des fichiers spéciaux (inodes) Les fichiers spéciaux sont créés à l’aide de la commande mknod. Exemple : mknod /dev/mon_module La commande rmmod permet de supprimer le fichier spécial. Afin que la création de vos fichiers spéciaux soit automatique, on peut utiliser un script shell. (un pour la création du module et un autre pour sa suppression). Récapitulatif des " header " à inclure : #include <linux/module.h> /*Pour la création du module*/ #include <linux/init.h> /*Pour l‟utilisation du module*/ #include <linux/kernel.h> /*Pour l‟utilisation de printk()*/ #include <linux/fs.h> /*Pour la structure file_operations*/ #include <linux/types.h> /*Pour l‟utilisation de types comme size_t*/ #include <linux/cdev.h> /*Pour la structure cdev*/ #include <linux/errno.h> /*Pour la liste de erreurs*/ #include <linux/sched.h> /*Pour la déclaration de la ligne d‟interruption*/ #include <linux/interrupt.h> /*Pour la fonction d‟interruption*/ #include <linux/slab.h> /*Pour l‟allocation mémoire kmalloc()/kfree()*/ #include <asm/io.h> /*Pour les fonctions ioremap/iounmap*/ #include <asm/uaccess.h> /*Pour les fonctions copy_{from,to}_user()*/ #include <asm/ioctl.h> /*Pour la fonction ioctl()*/ Synthèse Le schéma suivant synthétise le fonctionnement global du pilote de périphérique. LECANU Sandra NURY Sylvain Page 22 Les mémoires tampons (buffers) " RX " et " TX " des espaces noyau et utilisateur doivent être créés par l’intermédiaire de structures que l’on définies au préalable dans un fichier .h. Contrôle du périphérique La fonction ioctl() sert au contrôle du périphérique. Nous n’en parlerons pas dans ce rapport, mais elle peut être utile dans certains cas. Prototype: int ioctl(struct inode *inode, struct file *fops, unsigned int cmd, unsigned long arg) inode : structure interne au noyau, non utilisée par le programmeur ; fops : correspond à mon_module_fops, il s’agit de votre file operation ; cmd : il s’agit de la variable attribuée pour chaque commande dans le fichier .h ; arg : argument optionnel de validité ; Compilation du programme (fichier Makefile) Dans le fichier Makefile, nous avons juste mis: obj-m:=mon_module.o LECANU Sandra NURY Sylvain Page 23 puis on tape la commande suivante (vue précédemment) pour compiler le module (en étant dans le répertoire de notre module): $(MAKE) ARCH=arm CROSS_COMPILE=/usr/local/arm-ssv1-linux/bin/arm-ssv1-linux- -C $(KDIR) SUBDIRS=$(PWD) modules Configuration du système pour l’insertion du module Il existe deux commandes pour insérer son module dans le noyau. La commande insmod mon_module permet uniquement d’insérer votre module sans se préoccuper des possibles dépendances avec d’autres modules du noyau. Pour retirer le module, on utilise alors la commande rmmod mon_module. La commande modprobe mon_module permet non seulement d’insérer le module, mais de satisfaire les dépendances. On retire alors le module avec la commande modprobe -r mon_module. A noter : Si on utilise un fichier script pour la création des fichiers spéciaux, celui-ci n’est pas appelé lors de l’insertion du module. Remarque: Dans le cas d’un pilote de périphérique PCI, les fonctions request_region() et request_mem_region() sont remplacées par la fonction ioremap(). De même, les fonctions release_region() et release_mem_region() seront remplacées par la fonction iounmap().<asm/io.h> LECANU Sandra NURY Sylvain Page 24 Les drivers blocs L’écriture de drivers blocs est un peu différente que les drivers caractères, du fait que les fonctions du device driver d’un driver bloc ne lisent ou n’écrivent pas directement des données du ou vers le device : elles utilisent une fonction request() utilisée à la fois pour la lecture ou pour l’écriture. Il existe des fonctions génériques block_read() et block_write() qui appelent elles-même la fonction request(). La fonction request() ne prends aucun argument et ne retourne rien. Elle regarde dans une file de requête et effectuent ces requêtes les unes après les autres. Si la requête nécessite une interruption (comme vu précédemment dans certains types de drivers), elle va laisser l’interruption suivre son cours et celle-ci devra avoir un end_request() à la fin, puis un request() pour ordonnancer et traiter la requête suivante si il y en a une. Si la requête ne nécessite pas d’interruption, celle-ci doit tout-de-même appelé la fonction end_request() à la fin. La fonction end_request() prend un entier en argument : 1 si le traitement a réussi, 0 si le traitement a échoué. Cela est important afin que le end_request() puisse réveiller les tâches qui étaient en attente de la requête. La fonction requête ne s’arrête pas d’elle-même : ceci est fait par la macro INIT_REQUEST qui vérifie la file d’attente des requêtes et qui fait un return si celle-ci est vide. CURRENT est définit comme étant: blk_dev[MAJOR_NR].current_request dans le fichier drivers/block/blk.h. Ceci est la requête demandée à la tête de la file si elle existe. La macro INIT_REQUEST fait des tests sur CURRENT pour savoir si il y a une autre requête. Cette structure contient toutes les informations nécessaires pour pouvoir traiter la requête : -le device -la commande (read ou write) pour savoir ce que l’on doit faire -le secteur qui va être lu/écrit -le nombre de secteurs à lire/écrire -un pointeur sur la mémoire ou l’on va stocker les données -un pointeur vers la requête suivante … Chaque device block driver possède sa propre fonction requête qui utilise des variables susceptibles de se trouver dans le fichier blk.h.. Le driver bloc que l’on code doit inclure en particulier les deux fichiers suivants: linux/major.h et linux/blkdrv.h. Ci-dessous un exmple de fonction request() implémentable: static void do_ mon_device_request(void) { repeat: INIT_REQUEST; LECANU Sandra NURY Sylvain Page 25 /* check to make sure that the request is for a valid physical device */ if (!valid_ mon_device_device(CURRENT->dev)) { end_request(0); goto repeat; } if (CURRENT->cmd == WRITE) { if (mon_device_write( CURRENT->sector, CURRENT->buffer, CURRENT->nr_sectors < 9)) { /* successful write */ end_request(1); goto repeat; } else end_request(0); goto repeat; } if (CURRENT->cmd == READ) { if (mon_device_read( CURRENT->sector, CURRENT->buffer, CURRENT->nr_sectors << 9)) { /* successful read */ end_request(1); goto repeat; } else end_request(0); goto repeat; } } } Beaucoup de macros à utiliser sont présentes dans le fichier drivers/block/blk.h. On va seulement voir les macros essentielles qu’il faut utiliser pour le bon fonctionnement du block driver. Il faut rajouter les lignes suivantes à notre driver pour pouvoir utiliser le fichier blk.h : #define MAJOR_NR MON_DEVICE_MAJOR #include "blk.h" nombre majeur de notre device Il faut alors écrire les lignes suivantes dans le fichier blk.h (à la fin): #elif (MAJOR_NR == FOO_MAJOR) #define DEVICE_NAME "mon_device" #define DEVICE_REQUEST do_mon_device_request #define DEVICE_NR(device) (MINOR(device) >> 6) #define DEVICE_ON(device) #define DEVICE_OFF(device) #endif DEVICE_NAME : nom du driver DEVICE_REQUEST : fonction request() du driver. DEVICE_NR : le nombre DEVICE_ON and DEVICE_OFF : seulement utiliser pour les devices que l’on peut arrêter/reprendre. LECANU Sandra NURY Sylvain Page 26 On doit aussi effectuer des changements/rajouts dans la fonction init() du module: register_blkdev() : register_blkdev(FOO_MAJOR,"foo", & foo_fops) blk_dev qui possède un pointeur vers la fonction request() : blk_dev[FOO_MAJOR].request_fn = DEVICE_REQUEST ; blksize_size qui est la taille des blocs que l’on demande : blksize_size[MAJOR_NR] = 1024 ; Remarque : L’appel de la fonction init() est protégé dans le fichier Kconfig dans le répertoire du device que l’on a créé par la ligne suivante : bool `Foobar disk support' CONFIG_FOO y avec les commentaires (help) que l’on veut afficher quand l’utilisateur est dans le menuconfig dont on a parler précédemment. Dans le makefile drivers/block/Makefile, on doit de plus rajouté : ifdef CONFIG_FOO OBJS := $(OBJS) foo.o SRCS := $(SRCS) foo.c Endif Enfin, dans le Kconfig drivers/Kconfig, on doit de plus rajouté : source "drivers/foo_driver/Kconfig" LECANU Sandra NURY Sylvain Page 27