Pilotes de périphériques PCI

Pierre Ficheux (pierre.ficheux@openwide.fr)

Juin 2002


Résumé

Cet article est un introduction à la gestion du bus PCI sous LINUX ainsi qu'à l'écriture de pilotes dédiés aux cartes PCI. C'est également une suite et une mise à jour de l'article Introduction à l'écriture de pilotes de périphériques LINUX paru en mai 2000 dans ce même journal. Même si certains concepts généraux liés aux pilotes et aux modules du noyau sont rappelés dans cet article, sa compréhension nécessite quelques connaissances préalables ou bien la lecture de documents cités dans la bibliographie en fin d'article. Les exemples décrits sont disponibles en téléchargement sur http://www.ficheux.com/articles/lmf/pci/exemples_pci.tgz. Ce code source fut initialement écrit par Julien Gaulmin (voir bibliographie) et a été modifié par mes soins pour les besoins de l'article.

Généralités sur le bus PCI

Le bus PCI (Peripheral Component Interconnect) est apparu en 1992 en "remplacement" du bus ISA (Industry Standard Architecture) introduit dans l'architecture PC/XT en 1981 et de son extension VLB (VESA Local Bus).

Il était destiné à fournir un bus plus performant que l'ISA et facilitant la configuration automatique des périphériques au démarrage (adresses, niveaux d'interruption). Au démarrage du système, un BIOS PCI se charge de dialoguer avec les périphériques afin de leur affecter des valeurs de paramètres cohérentes. Concernant les interruptions, le bus PCI utilise la notion d'interruptions partagées entre plusieurs périphérique. De part ses performances et ses fonctionnalités très innovantes, le bus PCI s'est aujourd'hui imposé dans la majorité des architectures matérielles y compris celles non basées sur l'architecture PC/Intel.

La version classique du bus fonctionne à 33 Mhz, dispose d'une interface 32 bits et fournit un taux de transfert maximale de 132 Mo/s. Les versions les plus performantes permettent d'utiliser un bus 64 bits à 133 Mhz pour obtenir un taux de transfert maximal de 1066 Mo/s (1 Go/s !). Ces cartes utilisent un connecteur plus long qui étend le connecteur 32 bits classique. Les performances sont à comparer au vieux bus ISA dont le taux de transfert maximal plafonne à 8 Mo/s dans la version "moderne" de l'ISA soit un bus 16 bits (datant de 1984). La tableau ci-dessous donne un comparatif des caractéristiques des différents bus les plus utilisés. Nous pouvons noter l'existence du bus compactPCI similaire au bus PCI mais utilisant une connectique de taille réduite dédiée aux applications embarquées.

Type de bus

Fréquence d'horloge

Largeur du bus
Taux de transfert maxi
ISA 8 MHz
16-bit
8 Mbytes/sec
PCI 133 MHz
64-bit
1 Gbytes/sec
CompactPCI 33 MHz
64-bit
132 Mbytes/sec
PCMCIA 10 MHz
16-bit
20 Mbytes/sec
USB 1.1 n/a
n/a
1.5 Mbytes/sec
IEEE 1394a (FireWire) n/a
n/a
50 Mbytes/sec

Chaque périphérique du bus PCI est identifié par trois éléments:

Le spécifications PCI autorise jusqu'à 256 bus, chaque bus pouvant accueillir 32 devices. Chaque device (carte) peut cumuler jusqu'à 8 fonctions PCI. Les systèmes modernes incluent au moins deux bus PCI.

Adressage du bus PCI

L'espace d'adressage du bus PCI est sur 32 ou 64 bits suivant l'architecture, le cas 64 bits étant bien entendu réservé aux système haut de gamme (SPARC64, IA64). Le bus offre également un espace d'entrées/sorties (I/O ports) sur 32 bits ce qui correspond à 4 Go.
L'espace mémoire de configuration d'un périphérique PCI (256 octets) est organisé de manière standard pour les 64 premiers octets. La figure ci-dessous décrit la répartition des registres:

Les deux premiers registres identifient de manière unique le périphérique de part son Vendor ID (numéro de constructeur) et son Device ID (numéro de périphérique). Le Class Code identifie le type de périphérique. Les différentes zones mémoire Base Address 0 (BAR 0) à Base Address 5 (BAR 5) constituent un espace d'accès aux fonctions du périphérique.

LINUX et le bus PCI

De part ses performances et sa large diffusion, le bus PCI est donc un choix de prédilection pour les cartes d'extension et le pilotage de ces extension est de ce fait un sujet très important dans le monde LINUX.

Manipulation par l'utilisateur

Du coté de l'utilisateur, la configuration PCI du système est visible à travers la structure /proc et la commande lspci.
# lspci
00:00.0 Host bridge: Intel Corporation: Unknown device 1130 (rev 02)
00:02.0 VGA compatible controller: Intel Corporation: Unknown device 1132 (rev 02)
00:1e.0 PCI bridge: Intel Corporation: Unknown device 244e (rev 01)
00:1f.0 ISA bridge: Intel Corporation: Unknown device 2440 (rev 01)
00:1f.1 IDE interface: Intel Corporation: Unknown device 244b (rev 01)
00:1f.3 SMBus: Intel Corporation: Unknown device 2443 (rev 01)
01:0a.0 Ethernet controller: 3Com Corporation 3c905 100BaseTX [Boomerang]
01:0b.0 Ethernet controller: 3Com Corporation 3c905 100BaseTX [Boomerang]
01:0c.0 SCSI storage controller: Adaptec AIC-7881U
01:0d.0 Multimedia audio controller: Creative Labs SB Live! EMU10000 (rev 06)
01:0d.1 Input device controller: Creative Labs SB Live! (rev 06)
01:0e.0 Multimedia video controller: Brooktree Corporation Bt878 (rev 11)
01:0e.1 Multimedia controller: Brooktree Corporation Bt878 (rev 11)
Les trois premières colonnes correspondent respectivement au numéro de bus, numéro de device sur le bus et numéro de fonction. On peut obtenir plus d'information en utilisant l'option -v, comme décrit dans l'exemple ci-dessous concernant une carte réseau 3c905.
01:0a.0 Ethernet controller: 3Com Corporation 3c905 100BaseTX [Boomerang]
        Flags: bus master, medium devsel, latency 64, IRQ 10
        I/O ports at d800
La même commande avec l'option -n donne les valeurs numériques des registres.
01:0a.0 Class 0200: 10b7:9050
        Flags: bus master, medium devsel, latency 64, IRQ 10
        I/O ports at d800
Le même type d'information est obtenu en consultant l'entrée /proc/pci.
  Bus  1, device  10, function  0:
    Ethernet controller: 3Com 3C905 100bTX (rev 0).
      Medium devsel.  IRQ 10.  Master Capable.  Latency=64.  Min Gnt=3.Max Lat=8.
      I/O at 0xd800 [0xd801].
Les informations binaires concernant les périphériques sont également disponibles via l'entrée /proc/bus/pci.
# od -x /proc/bus/pci/01/0a.0 
0000000 10b7 9050 0007 0200 0000 0200 4000 0000
0000020 d801 0000 0000 0000 0000 0000 0000 0000
0000040 0000 0000 0000 0000 0000 0000 0000 0000
0000060 0000 0000 0000 0000 0000 0000 010a 0803
0000100 02d8 0163 0000 0000 e040 0000 ffff ffff
0000120 0000 0000 0000 0000 0000 0000 0000 0000
*
0000400

Quelques rappels sur les pilotes de périphériques

Une introduction aux pilotes de périphériques LINUX fit l'objet d'un article en mai 2000. A l'époque, le document s'était attaché à décrire les concepts liés aux versions 2.0 et 2.2 du noyau LINUX. Les concepts décrits actuellement seront liés à la version 2.4 du noyau même si la majorité d'entre eux sont toujours valables pour le noyau 2.2.

La notion de pilote de périphérique est liée à celle de module dynamique du noyau, chargé pendant l'exécution de celui-ci. Il existe bien entendu des versions statiques de certains pilotes mais ceux-ci sont en général réservés aux pilotes intégrés à l'arborescence officielle du noyau. Nous ne traiterons pas ce cas dans cet article. Au niveau de l'espace utilisateur, les modules sont manipulables par un jeu de commandes dédié:

Ces programmes font partie du package modutils. Lorsqu'un module est au point, il est en général chargé automatiquement par kmod, un thread du noyau qui a remplacé le démon kerneld qui effectuait la même tâche dans l'espace utilisateur. Si un pilote est presque toujours un module, la réciproque n'est pas vraie, et l'on peut imaginer un module très simple qui n'assure aucune fonction d'interfaçage avec un composant matériel ou logiciel. La structure d'un tel module minimal est la suivante:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

MODULE_DESCRIPTION("Module Hello World");
MODULE_AUTHOR("Moi");

static int __init hello_init(void)
{
	printk(KERN_INFO "Hello World\n");
	return 0;
}

static void __exit hello_exit(void)
{
	printk(KERN_INFO "Goodbye cruel world\n");
}

/* Points d'entrée */
module_init(hello_init);
module_exit(hello_exit);

Notez que la structure est un peu différente de celle du module décrit dans l'article de mai 2000 et dédié au noyau 2.2. En effet, le noyau 2.4 utilise à présent des optimisations du compilateur gcc:

D'autres optimisations de ce type seront utilisées dans la suite de l'article.

Les macros (MODULE_*) en tête du fichier permettent d'indiquer les paramètres utilisables par la commande modinfo.

# modinfo hello.o
filename:    hello.o
description: "Module Hello World"
author:      "Moi"
license:     <none>
Ces macros sont définies dans le fichier <linux/modules.h> et permettent de définir entre autres les paramètres que l'on pourrait passer au module lors de son chargement.
MODULE_PARM(un_paramètre_entier, "i");
MODULE_PARM_DESC(un_paramètre_entier, "Exemple de paramètre entier");
Si le module en question est bien un pilote de périphérique, il devra en plus de cela inclure les points d'entrée (méthodes) d'accès à ce périphérique, dont la liste non exhaustive est donnée ci-dessous.

Au niveau du code source du pilote, les méthodes en question seront déclarées d'une manière très similaire à celle du noyau 2.2.
static struct file_operations mondriver_fops = {
owner: THIS_MODULE, /* 2.4 uniquement */
read: genpci_read,
write: genpci_write,
ioctl: genpci_ioctl,
open: genpci_open,
release: genpci_release,
};
Le point d'accès au pilote est matérialisé dans le système de fichier LINUX par une entrée dans le répertoire /dev créée comme suit.
   mknod /dev/genpci c majeur mineur
Nous rappelons que la lettre c indique un périphérique en mode caractère, que l'on peut donc accéder en mode non bufferisé. Les autres périphériques de type bloc (block device, en général les périphériques de stockage, bufferisés) et réseau (network devices) ne seront pas traités ici.

Le bon fonctionnement du pilote nécessite l'allocation de la même valeur de majeur dans le code source. On utilise pour cela les fonctions suivantes:

int register_chrdev (unsigned int major, const char *name, struct file_operations *fops); 
int unregister_chrdev (unsigned int major, const char *name); 
int devfs_register_chrdev (unsigned int major, const char *name, struct file_operations *fops); 
int devfs_unregister_chrdev (unsigned int major, const char *name); 
La liste des majeurs utilisée est fournie dans le fichier devices.txt du répertoire de documentation des sources du noyau LINUX. Dans le cas du développement d'un pilote non intégré dans le noyau officiel, il est préférable d'utiliser un majeur alloué dynamiquement de manière à ne pas entrer en conflit avec des valeurs déja utilisées. Pour cela il suffit de forcer le paramètre major à 0, la valeur retournée par [devfs_]register_chrdev() constituera la valeur dynamique du majeur. On peut retrouver aisément cette valeur en fonction du nom du module via le fichier virtuel /proc/devices. L'appel à [devfs_]register_chrdev() et [devfs_]unregister_chrdev() se fera respectivement dans les points d'entrée module_init() et module_exit() du noyau. Le nouveau système de fichier devfs apparu dans le noyau 2.4 (en fait 2.3.46) permet d'enregistrer les pilotes de périphériques par nom au lieu de les enregistrer par majeur et mineur. Cette fonctionnalité est cependant optionnelle et n'est pas validée par défaut dans le noyau 2.4 (CONFIG_DEVFS_FS=n). Il est cependant prudent de faire en sorte que le pilote la prenne en compte.

Structure d'un pilote PCI

Les pilotes PCI suivent les règles expliquées précédemment mais l'API du noyau LINUX inclut de nombreuses fonctions dédiées facilitant fortement la gestion de ce type de périphérique. Les prototypes de ces fonctions sont définis dans le fichier <linux/pci.h>.

Ajout d'un nouveau pilote

Au niveau du code source, un pilote PCI sera chaîné à la liste des autres pilotes PCI par la structure suivante:
static struct pci_driver genpci_pci_driver = {
	name:		"genpci",
	id_table:	genpci_id_table, /* Liste des devices supportés */
	probe:		genpci_probe,    /* Détection device */
	remove:		genpci_remove,   /* Libération device */
};
Comme nous l'avons dit au début, le BIOS PCI se charge de l'affectation des paramètres dynamiques au démarrage du système. Cependant, le BIOS n'effectue pas le démarrage du périphérique et la méthode genpci_probe() devra se charger de cette initialisation. De la même manière, la méthode genpci_remove() devra se charger des libérations des ressources et autres arrêts matériels.

La liste des devices supportés est constituée d'un tableau de type pci_device_id définissant un périphérique par ligne. Dans notre cas, le pilote supporte un seul type de périphérique défini logiquement par son VENDOR_ID et son DEVICE_ID.

typedef enum {
	MY_BOARD = 0,
} board_t;

static struct {
	const char *name;
} board_info[] __devinitdata = {
  {"Mon périphérique PCI"}
};

static struct pci_device_id genpci_id_table[] __devinitdata = {
	{VENDOR_ID, DEVICE_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, MY_BOARD},
	{0,}	/* 0 terminated list */
};
MODULE_DEVICE_TABLE(pci, genpci_id_table);
L'enregistrement et la suppression du pilote se font respectivement dans les fonction module_init() et module_exit() en utilisant les primitives
int pci_register_driver(struct pci_driver *drv); 
int pci_module_init(struct pci_driver *drv); 
pour l'enregistrement et la primitive
void pci_unregister_driver(struct pci_driver *drv); 
pour la suppression.

Détection et initialisation du périphérique: méthode probe

La méthode probe (dans notre cas genpci_probe()), effectue l'initialisation complète du périphérique sur le bus PCI. Le prototype de la fonction est du type:
int genpci_probe (struct pci_dev *dev, const struct pci_device_id *id); 
Les variables propres au périphérique sont en général stockées dans une structure privée qui sera disponible sous forme d'un pointeur calculé à partir du paramètre dev de type pci_dev.
struct struct_private {
        /* Champs généraux */
	struct list_head link;                   /* Double linked list */
	struct pci_dev	*dev;                    /* PCI device */
	board_t		type;                    /* Board type */
	int		minor;                   /* Minor number */
	devfs_handle_t	devfs_handle             /* Devfs file handle */
	void *mmio[DEVICE_COUNT_RESOURCE];       /* Remaped I/O memory */
	u32 mmio_len[DEVICE_COUNT_RESOURCE];     /* Size of remaped I/O memory */

	/* Champs spécifiques */
        void            *bar0;                   /* BAR0 */
        u32             bar0_len;		 
};
La liste des champs spécifiques peut être enrichie suivant le type de périphérique contrôlé de manière à accélérer l'accès à des données. Dans notre cas, nous avons simplement défini un pointeur sur la zone BAR0 ainsi que la longueur de cette zone. L'initialisation de la structure est effectué de la manière suivante:
  1. On alloue la structure à l'aide de la primitive kmalloc().
    	struct struct_private *genpci_private;
            /* ... */
    	genpci_private = (struct struct_private *)kmalloc(sizeof(struct struct_private), GFP_KERNEL);
    	if (genpci_private == NULL) {
    	   printk(KERN_WARNING "genpci: unable to allocate private structure\n");
               ret = -ENOMEM;
    	   goto cleanup_kmalloc;
    	}
    
  2. On lie le pointeur à la structure du périphérique puis on initialise quelques valeurs.
    	pci_set_drvdata(dev, genpci_private);
    	genpci_private->dev = dev;
    	genpci_private->minor = genpci_private->type = id->driver_data;
    
    L'étape suivante consiste à initialiser le périphérique au niveau matériel en effectuant l'appel qui suit:
    	ret = pci_enable_device(dev);
    	if (ret < 0) {
              printk(KERN_WARNING "genpci: unable to initialize PCI device\n");
    	  goto cleanup_pci_enable;
    	}
    
    La suite de la méthode effectue la réservation de l'espace mémoire du périphérique sur le bus PCI. Comme pour d'autres types de bus, cet espace est divisé en deux catégories:

    1. les ports d'entrée/sortie (I/O ports, type IORESOURCE_IO)
    2. les plages mémoire (I/O memory, type IORESOURCE_MEM)

    Les types IORESOURCE_IO et IORESOURCE_MEM sont définis dans le fichier <linux/ioport.h>.

    L'accès à un périphérique matériel se fait en général à travers des registres. Dans le cas du bus ISA, la plupart des registres de configuration étaient associés à des ports d'entrée sortie. Dans l'exemple de mai 2000, notre petit pilote accédait à un registre ISA du PC pour configurer l'état d'un diode électro-luminescente (LED) et ce en utilisant les fonctions inb() et outb().

    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;
    }
    
    Contrairement au bus ISA, le bus PCI utilise assez peu la notion de port d'entrée sortie et la plupart des périphériques effectuent un mapping des registres sur des plages mémoire (I/O memory). Cette approche a l'avantage d'être plus générale et d'éviter les effets de bords propres aux ports d'entrée/sortie au niveau des caches et des optimisations su compilateur. Une plage mémoire est en fait une espace similaire à de la RAM que le périphérique met à disposition sur le bus. La méthode d'accès à ces plages peut cependant être différente suivant le type d'architecture et le noyau devra alors rendre la plage accessible en utilisant la fonction ioremap().

    Dans le cas de notre pilote de périphérique, la configuration de l'espace mémoire se fera donc en deux temps.

    1. On réserve les plages et ports d'entrée/sortie liées au périphérique
      	/* Reserve PCI I/O and memory resources */
      	ret = pci_request_regions(dev, "genpci");
      	if (ret < 0) {
      		printk(KERN_WARNING "genpci: unable to reserve PCI resources\n");
      		goto cleanup_regions;
      	}
      

    2. On remplit la structure privée avec les valeurs d'adresses accessibles depuis le pilote. Dans le cas de plages mémoire, on utilise la fonction ioremap(). Les caractéristiques d'un registre (début et longueur) sont lue grâce aux fonctions pci_resource_start() et pci_resource_len().
              for (i=0; i < DEVICE_COUNT_RESOURCE; i++) {
      		if (pci_resource_flags(dev, i) & IORESOURCE_MEM) {
      			genpci_private->mmio[i] = ioremap(pci_resource_start(dev, i), pci_resource_len(dev, i));
      			if (genpci_private->mmio[i] == NULL) {
      				printk(KERN_WARNING "genpci: unable to remap I/O memory\n");
      				ret = -ENOMEM;
      				goto cleanup_ioremap;
      			}
      
      			genpci_private->mmio_len[i] = pci_resource_len(dev, i);
      		} else {
      			genpci_private->mmio[i] = NULL;
      		}
      	}
      
      
    3. A partir de la, on peut manipuler les registres.
      	genpci_private->bar0 = genpci_private->mmio[0];
      	genpci_private->bar0_len = genpci_private->mmio_len[0];
      

    L'accès aux registres PCI s'effectue par le jeu de fonctions suivant:

    int pci_read_config_byte(struct pci_dev *dev, int addr, u8 *val);
    int pci_read_config_word(struct pci_dev *dev, int addr, u16 *val)
    int pci_read_config_dword(struct pci_dev *dev, int addr, u32 *val);
    
    Dans notre cas, nous pouvons lire les valeurs du Vendor ID et Device ID.
    	pci_read_config_word(dev, PCI_VENDOR_ID, &vendor);
    	pci_read_config_word(dev, PCI_DEVICE_ID, &device);
    
    La suite de la méthode effectue l'initialisation des interruptions pour le périphérique PCI. La gestion des interruptions en PCI n'a rien de particulier par rapports aux autres pilotes si ce n'est que la norme PCI impose la possibilité de gestion d'interruptions partagées par plusieurs périphériques. Un connecteur PCI dispose de 4 lignes d'interruption et chaque périphérique peut utiliser les 4. Un registre PCI permet de savoir si le périphérique gère des interruptions. Il est donc conseillé de tester ce registre afin de savoir si une fonction de gestion d'interruption (IRQ handler) doit être installée.
    pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &mypin);
    
    Si le périphérique ne gère pas d'interrution, la valeur mypin sera égale à zéro. Le niveau d'interruption alloué par le BIOS est disponible dans le registre PCI_INTERRUPT_LINE.
    pci_read_config_byte(dev, PCI_INTERRUPT_LINE, &myirq);
    
    Une fonction d'interruption doit par définition être courte afin d'y passer le moins de temps possible. Sous LINUX, la gestion d'une interruption est divisée en deux phase:

    1. La partie top-half qui doit être très concise car ininterruptible. Dans cette partie, on pourra effectuer la lecture d'un registre spécifique afin d'acquitter l'interruption et récupérer son origine.

    2. La partie bottom-half dont l'exécution est placée dans une file de tâches. Durant cette phase, on peut recevoir de nouvelles interruptions. Cette phase est optionnelle et l'on peut concevoir une fonction de gestion d'interruption ne comportant que la partie top-half.
    L'installation de la fonction d'interruption pour notre périphérique s'effectue comme suit.
    	pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &mypin);
    
    	if (mypin) {
    	  /* Initialize intr queue */
    	  INIT_TQUEUE(&genpci_bh_task, NULL, NULL);
    
    	  ret = request_irq(dev->irq, genpci_irq_handler, SA_SHIRQ, "genpci", genpci_private);
    	  if (ret < 0) {
    	    printk(KERN_WARNING "genpci: unable to register irq handler\n");
    
    	    goto cleanup_irq;
    	  }
    	}
    
    
    La valeur SA_SHIRQ indique une interruption partagée. La fonction de gestion elle-même a la structure suivante pour la partie top-half.
        void genpci_irq_handler(int irq, void *dev_id, struct pt_regs *regs)
        {
            struct struct_private *genpci_private = (struct struct_private *)dev_id;
    
            /* ... */
    
            PREPARE_TQUEUE(&genpci_bh_task, genpci_bh_handler, dev_id);
            queue_task(&genpci_bh_task, &tq_immediate);
            mark_bh(IMMEDIATE_BH);
        }
    
    La fonction bottom-half a la structure suivante.
        static void genpci_bh_handler(void *dev_id)
        {
            struct struct_private *genpci_private = (struct struct_private *)dev_id;
    
            /* ... */
        }
    
    Le champ dev_id permet d'identifier le périphérique en mode partage d'interruption.

    La fin de la méthode effectue les opérations suivantes:

    1. Validation du contrôle de bus (bus mastering) pour le périphérique. Cette fonction permet aux divers périphériques du bus PCI de dialoguer sans passer par le CPU.
      	pci_set_master(dev);
      
    2. Création du point d'entrée pour le système de fichier devfs et lien de la structure privée aux autres structures de ce type.
      	list_add_tail(&genpci_private->link, &privates);
      
      	return 0;
      
      	sprintf(devfs_name, "genpci/device_%d", ent->driver_data);
      	genpci_private->devfs_handle = devfs_register(NULL, devfs_name, \
                 DEVFS_FL_DEFAULT, major, id->driver_data, S_IFCHR | S_IRUGO | \
                 S_IWUSR, &genpci_fops, dev);
      
    3. La dernière partie constitue les points de sortie en cas d'erreur.
      cleanup_irq:
      	for (i--; i >= 0; i--) {
      		if (genpci_private->mmio[i] != NULL)
      			iounmap(genpci_private->mmio[i]);
      	}
      cleanup_ioremap:
      	pci_release_regions(dev);
      cleanup_regions:
      	pci_disable_device(dev);
      cleanup_pci_enable:
      	kfree(genpci_private);
      cleanup_kmalloc:
      	return ret;
      

    Libération du périphérique: méthode remove

    Comme il est souvent plus compliqué de faire que de défaire, la méthode remove est largement plus simple que la méthode probe !
    static void __devexit genpci_remove(struct pci_dev *dev)
    {
    	int i;
    	struct struct_private *genpci_private = pci_get_drvdata(dev);
    
            for (i=0; i < DEVICE_COUNT_RESOURCE; i++) {
    		if (genpci_private->mmio[i] != NULL)
    			iounmap(genpci_private->mmio[i]);
    	}
    	pci_release_regions(dev);
    	pci_disable_device(dev);
    	kfree(genpci_private);
    	free_irq(dev->irq, "genpci");
    	devfs_unregister(genpci_private->devfs_handle);
    	list_del(&genpci_private->link);
    }
    

    Ouverture du périphérique: méthode open

    La méthode open se résume à la recherche du pointeur de la structure privée dans la liste des structures.
    static int genpci_open(struct inode *inode, struct file *file)
    {
    	int minor = MINOR(inode->i_rdev);
    	struct list_head *cur;
    	struct struct_private *genpci_private;
    
    	printk(KERN_DEBUG "genpci_open()\n");
    
    	list_for_each(cur, &privates) {
    		genpci_private = list_entry(cur, struct struct_private, link);
    
    		if (genpci_private->minor == minor) {
    			file->private_data = genpci_private;
    			return 0;
    		}
    	}
    
    	return -ENODEV;
    }
    

    Fermeture du périphérique: méthode release

    La méthode se résume a affecter le pointeur de structure privée au pointeur NULL.
    static int genpci_release(struct inode *inode, struct file *file)
    {
    	file->private_data = NULL;
    	return 0;
    }
    

    Lecture sur le périphérique: méthode read

    Cette méthode est également assez simple dans sa structure basique. Le seul point important est la nécessité d'utiliser la fonction copy_to_user() afin de transmettre les données lues sur le périphérique vers l'espace utilisateur. L'exemple ci-dessus effectue une lecture d'un nombre donné d'octets sur la zone BAR0.
    static ssize_t genpci_read(struct file *file, char *buf, size_t count, loff_t *ppos)
    {
            int i, real, bank = DEVICE_COUNT_RESOURCE;
             struct struct_private *genpci_private = file->private_data;
    
    	/* Check for overflow */
    	if (count <= genpci_private->bar0_len - (int)*ppos)
    		real = count;
    	else
    		real = genpci_private->bar0_len - (int)*ppos;
    
    	if (real)
    		copy_to_user(buf, (char*)genpci_private->bar0 + (int)*ppos, real);
    
    	*ppos += real;
    
    	return real;
    }
    
    

    Ecriture sur le périphérique: méthode write

    La méthode est très proche sauf que l'on utilise la fonction copy_from_user() pour faire passer les données de l'espace utilisateur vers l'espace noyau.
    	if (real)
    		copy_from_user((char*)genpci_private->bar0 + (int)*ppos, buf, real);
    

    Configuration du périphérique: méthode ioctl

    La méthode ioctl permet d'effectuer une opération sur un périphérique, celle-ci n'étant pas réalisable par une lecture ou une écriture. Du coté du programme utilisateur, l'appel système ioctl sera utilisé de la manière suivante:
      result = ioctl (fd, TIOCGETP, &term);
    
    Le premier paramètre représente le descripteur de fichier associé au périphérique (obtenu par un appel système open), le deuxième correspond à la commande et le troisième à un paramètre permettant de d'envoyer ou de recevoir une valeur vers ou bien en provenance périphérique. On peut également obtenir une valeur venant par le code de retour de la fonction ioctl.

    Du coté du pilote, la méthode a l'allure suivante. Le paramètre arg correspond à un pointeur de transfert entre l'espace utilisateur et l'espace du noyau. On devra de nouveau utiliser les fonction copy_from_user() et copy_to_user().

    static int genpci_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
    {
      switch (cmd) {
    
      case GENPCI_CMD1:
        printk(KERN_INFO "genpci: ioctl cmd 0x%x\n", cmd);
        break;
    
      case GENPCI_CMD2:
        printk(KERN_INFO "genpci: ioctl cmd 0x%x\n", cmd);
    
        break;
    
      default:
        printk(KERN_WARNING "genpci: 0x%x unsupported ioctl command\n", cmd);
        return -EINVAL;
      }
    
      return 0;
    }
    

    Compilation du pilote

    Pour compiler le pilote on utilise un fichier Makefile classique sur lequel le chemin d'accès des fichier d'en-tête (includes) sera calculé dynamiquement en fonction du noyau en cours d'exécution.
    CC= gcc
    
    CCOPTS= -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer \
    	-fno-strict-aliasing -pipe -fno-strength-reduce -m486 \
    	-malign-loops=2 -malign-jumps=2 -malign-functions=2
    
    KERNELSRC= /lib/modules/$(shell uname -r)/build
    
    MODVERSIONS= -DMODVERSIONS -include $(KERNELSRC)/include/linux/modversions.h
    
    CFLAGS= -I$(KERNELSRC)/include $(CCOPTS) -DMODULE $(MODVERSIONS) -D__KERNEL__
    
    all: hello.o genpci.o
    
    clean:
    	rm -f *.o *~
    
    %.o: %.c ex.h
    	$(CC) $(CFLAGS) -c $< -o $@
    

    Test du pilote

    Après compilation, on obtient le fichier genpci.o que l'on peut charger par la commande:
    # insmod genpci.o
    
    On obtient alors la trace suivante dans le fichier /var/log/messages:
    genpci: found Philips Semiconductors SAA7146
    genpci: using major 254 and minor 0 for this device
    PCI: Found IRQ 5 for device 00:0f.0
    PCI: Sharing IRQ 5 with 00:02.2
    genpci: BAR 0 (0xcf7ede00-0xcf7edfff), len=512, flags=0x000200
    genpci: I/O memory has been remaped at 0xc8879e00
    genpci: bar0=  0xc8879e00 len= 512
    genpci: don't set IRQ!
    genpci: device removed
    genpci: found Philips Semiconductors SAA7146
    genpci: using major 254 and minor 0 for this device
    PCI: Found IRQ 5 for device 00:0f.0
    PCI: Sharing IRQ 5 with 00:02.2
    genpci: BAR 0 (0xcf7ede00-0xcf7edfff), len=512, flags=0x000200
    genpci: I/O memory has been remaped at 0xc8879e00
    genpci: bar0=  0xc8879e00 len= 512
    
    Le majeur est alloué dynamiquement et on peut obtenir sa valeur grâce à /proc/devices et donc créer le point d'entrée dans /dev.
    # grep genpci /proc/devices 
    254 genpci
    # mknod /dev/genpci c 254 0
    
    On peut alors effectuer quelques tests de lecture, écriture et configuration par ioctl. On commence tout d'abord par écrire une chaîne de test dans la zone BAR0 du périphérique, puis on relit la valeur.
    # echo -n "test genpci" > /dev/genpci 
    # dd bs=11 count=1 < /dev/genpci             
    test genpci1+0 enregistrements lus.
    1+0 enregistrements écrits.
    
    Le programme ioctl permet d'envoyer des valeurs générique par ioctl à un device donnée. Le source est fourni dans l'archive des exemples.
    # ./ioctl /dev/genpci 1 0
    Command 'ioctl /dev/genpci 0x1 0' ==> OK
    # ./ioctl /dev/genpci 2 0
    Command 'ioctl /dev/genpci 0x2 0' ==> OK
    # ./ioctl /dev/genpci 3 0
    Error doing 'ioctl /dev/genpci 0x3 0'
    ioctl(): Invalid argument
    

    Conclusion

    La réalisation d'un pilote de périphérique n'est jamais aisée mais force est de constater que l'API du noyau LINUX facilite grandement le travail du développeur on fournissant une couche d'abstraction assez simple et quasiment indépendante de l'architecture. L'exemple de pilote générique présenté ici inclut les principaux modules d'un pilote PCI et pourra donc servir de base à des développements réels même si certains domaines comme le DMA ne sont pas abordés ici.

    Bibliographie