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