Pierre Ficheux (pierre.ficheux@openwide.fr)
Juin 2002
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.
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.
# 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 d800La 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 d800Le 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
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.
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 mineurNous 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.
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.
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:
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; }
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:
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.
/* 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; }
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; } }
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:
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:
pci_set_master(dev);
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);
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;
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); }
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; }
static int genpci_release(struct inode *inode, struct file *file) { file->private_data = NULL; return 0; }
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; }
if (real) copy_from_user((char*)genpci_private->bar0 + (int)*ppos, buf, real);
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; }
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 $@
# insmod genpci.oOn 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= 512Le 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 0On 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