Pierre Ficheux (pierre@alienor.fr)
Mars 2000
A de rares exceptions près, ce type de ressource est accessible exclusivement au noyau (kernel space) et non aux programmes utilisateurs (user space). Il existe cependant des exceptions célèbres comme le serveur XFree86 ou bien la svgalib qui effectuent des accès directs à des périphériques.
Il existe différents types de pilotes suivant le périphérique à controler:
Dans la suite de l'article, nous nous intéresserons exclusivement aux pilotes en mode caractère.
L'exemple ci-dessous donne la liste des fichiers spéciaux associés aux pilotes des ports séries:
[pierre@ca-ol-bordeaux-7-146 pierre]$ ls -l /dev/ttyS* crw------- 1 root tty 4, 64 mai 5 1998 /dev/ttyS0 crw------- 1 root tty 4, 65 mai 5 1998 /dev/ttyS1 crw------- 1 root tty 4, 66 mai 5 1998 /dev/ttyS2 crw------- 1 root tty 4, 67 mai 5 1998 /dev/ttyS3Dans ce cas la, le majeur vaut 4 et les mineurs vont de 64 à 67.
Pour créer une nouvelle entrée dans le répertoire /dev, on utilisera la commande mknod:
mknod /dev/monpilote c majeur mineurLe c indique que l'on crée un noeud pour un pilote en mode caractère. Si l'on désirait créer une entrée pour un pilote en mode bloc, on utiliserait la même commande avec un b à la place du c. La liste des majeurs utilisés par le noyau LINUX est disponible dans le fichier Documentation/devices.txt de l'arborescence des sources du noyau.
A partir du moment ou le noeud est créé et le pilote installé, on peut accèder au périphérique en utilisant les commandes standards de LINUX comme:
echo "une commande" > /dev/monpilotepour écrire sur le périphérique.
dd bs=1 count=10 < /dev/monpilotepour lire 10 caractères du péripéhrique.
On peut bien entendu accèder au périphérique en utilisant des langages évolués comme le C, exemple:
int fd = open ("/dev/monpilote", O_RDWR);pour ouvrir un descripteur de fichier associé au périphérique.
char *s = "une commande"; write (fd, s, strlen(s));pour écrire une chaîne sur le périphérique.
char buf[256]; read (fd, buf, n);pour lire n caractères du périphérique.
close (fd);pour terminer l'accès au périphérique.
L'utilisation des modules chargeables permet aussi de limiter la mémoire utilisée par les pilotes car ceux-ci sont chargés uniquement lorsqu'un programme utilisateur ou un autre module en fait la demande. Les programmes kerneld ou kmod sont destinés à gèrer le chargement/déchargement automatique des modules chargeables.
Il est relativement simple de construire un module chargeable minimal. Considérons le code source suivant:
#include <linux/module.h> int init_module(void) { printk("<1>Hello, world\n"); return 0; } void cleanup_module(void) { printk("<1>Goodbye cruel world\n"); }
Ce code constitue la base d'un module chargeable. Si on le compile par:
cc -O -DMODULE -D__KERNEL__ -c hello.con pourra insérer dynamiquement le module par la commande insmod:
insmod hello.oCe qui provoque le chargement du module (voir lsmod) et l'apparition du message dans les traces du noyau:
[root@ca-ol-bordeaux-7-146 exemple]# lsmod Module Size Used by hello 184 0 (unused) 3c509 5972 1 (autoclean) vfat 9116 0 (unused) fat 30048 0 [vfat] [root@ca-ol-bordeaux-7-146 exemple]# dmesg | tail -1 Hello, worldDe même si on décharge le module par rmmod on obtient:
[root@ca-ol-bordeaux-7-146 exemple]# rmmod hello [root@ca-ol-bordeaux-7-146 exemple]# dmesg | tail -1 Goodbye cruel world
Notre LED n'utilisera qu'un octet sur un port dont l'adresse est calculée lors de l'ouverture du pilote. Le code est fourni pour les version 2.0 et 2.2 du noyau LINUX.
Les fonctionnalités du pilote sont les suivantes:
Dans le cas de cet petit module de test, on peut choisir le majeur comme étant la valeur 10. En effet d'après le fichier devices.txt, le majeur 10 est entre-autres réservé à des misc features (usages divers):
10 char Non-serial mice, misc features 0 = /dev/logibm Logitech bus mouse 1 = /dev/psaux PS/2-style mouse port 2 = /dev/inportbm Microsoft Inport bus mouse 3 = /dev/atibm ATI XL bus mouse 4 = /dev/jbm J-mouse 4 = /dev/amigamouse Amiga mouse (68k/Amiga) 5 = /dev/atarimouse Atari mouse 6 = /dev/sunmouse Sun mouse 7 = /dev/amigamouse1 Second Amiga mouse 8 = /dev/smouse Simple serial mouse driver 9 = /dev/pc110pad IBM PC-110 digitizer pad 10 = /dev/adbmouse Apple Desktop Bus mouse 11 = /dev/vrtpanel Vr41xx embedded touch panel 128 = /dev/beep Fancy beep device ...On peut connaitre la liste des modules chargés sur le majeur 10 en faisant:
[root@ca-ol-bordeaux-7-146 exemple]# cat /proc/misc 1 psauxLa valeur du mineur sera allouée dynamiquement au chargement du module.
La première partie du code est constituée des en-têtes et de la fonction permettant de récupérer l'adresse du port controlant la LED:
#include <linux/module.h> #include <linux/sched.h> #include <linux/fs.h> #include <linux/miscdevice.h> #include <asm/io.h> #include <asm/segment.h> static int base_addr = 0; static struct miscdevice ledctl_dev; #define LED_RED 0xFD #define LED_ORANGE 0xFB #define LED_OFF 0xFF /* Lecture adresse de base */ static int read_base_addr (void) { int addr; unsigned int msb, lsb; outb (0x07, 0x2E); outb (0x07, 0x2F); outb (0x60, 0x2E); msb = inb (0x2F); outb (0x61, 0x2E); lsb = inb (0x2F); addr = (msb << 8) + lsb; outb (0x0f, addr+1); return addr; }Dans le cas du noyau 2.2, on devra ajouter la ligne:
#include <asm/uaccess.h>qui inclut les macros de transfert entre l'espace utilisateur et l'espace noyau.
Le suite contient les fonctions d'ouverture et de fermeture du device:
static int ledctl_open (struct inode *inode, struct file *filp) { printk("<1>ledctl: open\n"); /* Comptage du nombre de chargements */ MOD_INC_USE_COUNT; /* Adresse de base du hardware */ base_addr = read_base_addr (); return 0; } static int ledctl_close (struct inode *inode, struct file *filp) { /* Dechargement */ MOD_DEC_USE_COUNT; }
La fonction ledctl_read retourne à l'espace utilisateur la valeur contenue dans l'adresse de base, soit l'état de la LED. Le lecture sur le port se fait par la macro inb.
static int ledctl_read(struct inode *inode, struct file *filp, char *buf, int count) { unsigned char led_value = inb (base_addr), led_color; switch (led_value) { case LED_RED: led_color = 'R'; break; case LED_ORANGE: led_color = 'O'; break; case LED_OFF: led_color = 'E'; break; default: led_color = '?'; break; } put_user (led_color, buf); return count; }Dans le cas du noyau 2.2, les paramètres de la fonction sont un peu différents:
static ssize_t ledctl_read (struct file *file, char *buf, size_t count, loff_t *ppos)
La fonction ledctl_write est similaire: on lit le caractère passé par le programe utilisateur et on l'écrit dans le port par un outb:
int ledctl_write(struct inode *inode, struct file *filp, const char *buf, int count) { unsigned char cmd; cmd = get_user(buf++); switch (cmd) { case 'R': outb (LED_RED, base_addr); break; case 'O': outb (LED_ORANGE, base_addr); break; case 'E': outb (LED_OFF, base_addr); break; default: break; } return count; }Dans le cas du noyau 2.2, les paramètres de la fonction sont différents:
static ssize_t ledctl_write (struct file *file, const char *buf, size_t count, loff_t *ppos)L'appel à get_user permet de tester une erreur éventuelle. La fonction prend donc maintenant deux paramètres:
if (get_user (cmd, buf++)) return -EFAULT;
La suite contient la structure décrivant la liste des méthodes du pilote. Les entrées contenant NULL indiquent que la méthode n'est pas utilisée pour ce pilote:
struct file_operations ledctl_fops = { NULL, ledctl_read, ledctl_write, NULL, /* ledctl_readdir */ NULL, /* ledctl_select */ NULL, /* ledctl_ioctl */ NULL, /* ledctl_mmap */ ledctl_open, ledctl_close, NULL, /* ledctl_fsync */ NULL, /* ledctl_fasync */ };Pour le noyau 2.2, on a quelques différences, en particulier le remplacement de la méthode select par poll et l'apparition de la méthode flush.
struct file_operations ledctl_fops = { NULL, ledctl_read, ledctl_write, NULL, /* ledctl_readdir */ NULL, /* ledctl_poll */ NULL, /* ledctl_ioctl */ NULL, /* ledctl_mmap */ ledctl_open, NULL, /* ledctl_flush */ ledctl_close, NULL, /* ledctl_fsync */ NULL, /* ledctl_fasync */ };
On termine par les fonctions de chargement/déchargement du module:
int init_module(void) { int retval; printk("<1>ledctl: init_module\n"); /* Calcul dynamique du mineur */ ledctl_dev.minor = MISC_DYNAMIC_MINOR; ledctl_dev.name = "ledctl"; ledctl_dev.fops = &ledctl_fops; /* Enregistrement du module */ retval = misc_register(&ledctl_dev); if (retval) return retval; return 0; } void cleanup_module(void) { printk("<1>ledctl: cleanup_module\n"); /* Suppression du module */ misc_deregister(&ledctl_dev); }
On peut compiler ce module par:
cc -O -DMODULE -D__KERNEL__ -c ledctl.cpuis l'insérer par:
insmod ledctl.oOn obtient alors le mineur, qui permet de créer le point d'entrée dans le répertoire /dev. Dans un cas réel, on pourra facilement automatiser cette procédure en créant/supprimant dynamiquement le point d'entrée à chaque chargement/déchargement du module.
root@ca-ol-bordeaux-7-146 2.2]# cat /proc/misc 63 ledctl 1 psaux [root@ca-ol-bordeaux-7-146 2.2]# mknod /dev/ledctl c 10 63
On peut alors utiliser le module, en particulier pour passer la LED au rouge on fera:
echo R > /dev/ledctlet pour lire la valeur:
dd bs=1 count=1 < /dev/ledctl