Mini-HOWTO programmation des ports d'E/S sous Linux <author>(c) 1995 Riku Saikkonen <tt/rjs@spider.compart.fi/ <date>26 Dec 1995 <abstract> Ce HOWTO traite de l'utilisation des ports d'E/S ainsi que de la programmation de mini-temporisations (de quelques microsecondes à quelques millisecondes) en C sous Linux (mode utilisateur) sur processeur Intel x86. Ce document est issu du minuscule IO-Port mini-HOWTO du même auteur. Si vous avez des modifications à apporter ou des compléments à ajouter, n'hésitez pas à m'envoyer un message (<tt/rjs@spider.compart.fi/)... Innombrables modifications depuis la précédente version (16 Nov 1995) dont l'ajout des spécifications du port parallèle. Adaptation française réalisée par Nicolas Lejeune (<tt/nl@freenix.fr/). </abstract> <!-- Ce document est (c) 1995 Riku Saikkonen. Consultez le COPYRIGHT habituel des Linux HOWTOs pour les détails. --> <toc> <sect>Utilisation des ports d'E/S dans les programmes C <p> <sect1>Méthode classique <p> Les routines permettant l'accès aux ports d'E/S sont définies dans <bf>/usr/include/asm/io.h</bf> (ou <bf>linux/include/asm-i386/io.h</bf> dans les sources du noyau). Ce sont des macros <sq>inline</sq>, il suffit donc de #inclure <bf><asm/io.h></bf> ; Aucune autre bibliothèque (<em>library</em>, NDT) n'est requise. Du fait d'une limitation de <bf>gcc</bf> (au moins jusqu'à la version 2.7.0 comprise), vous <bf>devez</bf> compiler tout code source utilisant ces routines avec les options d'optimisation (i.e. <em>gcc -O</em>). Une autre limitation de <bf>gcc</bf> empêche de compiler à la fois avec les options d'optimisation et de mise au point (<em>-g</em>). Cela signifie que si vous désirez utiliser <bf>gdb</bf> sur un programme manipulant les ports d'E/S, il est judicieux de mettre les routines utilisant les ports d'E/S dans un fichier source séparé, puis, lors de la mise au point, de compiler ce fichier source avec l'option d'optimisation, le reste avec l'option de mise au point. Avant d'utiliser un port, il faut donner à votre programme la permission de le faire. Il suffit pour cela d'appeler la fonction <bf>ioperm(2)</bf> (déclarée dans <bf>unistd.h</bf> et définie dans le noyau) quelque part au début de votre application (avant tout accès à un port d'E/S). La syntaxe est <bf>ioperm(from,num,turn_on)</bf>, où <bf>from</bf> représente le premier numéro de port et <bf>num</bf> le nombre de ports consécutifs à rendre accessibles. Par exemple, <bf>ioperm(0x300,5,1);</bf> autoriserait l'accès aux ports 0x300 à 0x304 (5 ports au total). Le dernier argument est un booléen précisant si l'on désire donner (vrai (1)) ou retirer (faux (0)) l'accès au port. Pour autoriser plusieurs ports non consécutifs, on peut appeler <bf>ioperm()</bf> autant que nécessaire. Consultez la page de manuel de <bf>ioperm(2)</bf> pour avoir des précisions sur la syntaxe. Votre programme ne peut appeler <bf>ioperm()</bf> que s'il possède les privilèges de root ; pour cela, vous devez soit le lancer comme utilisateur root, soit le rendre suid root. Il devrait être possible (Je n'ai pas essayé ; SVP, envoyez-moi un message si vous l'avez fait) d'abandonner les privilèges de root une fois l'accès aux ports obtenu par <bf>ioperm()</bf>. Il n'est pas nécessaire d'appeler <bf>ioperm(...,0)</bf> à la fin du programme pour abandonner explicitement les droits, cette procédure étant automatique. Les privilèges accordés par <bf>ioperm()</bf> demeurent lors d'un <bf>fork()</bf>, <bf>exec()</bf> ou <bf>setuid()</bf> en un utilisateur autre que root. <bf>ioperm()</bf> ne permet l'accès qu'aux ports 0x000 à 0x3ff ; pour les ports supérieurs, il faut utiliser <bf>iopl(2)</bf> (qui donne des droits sur tous les ports d'un coup) ; je ne l'ai jamais fait, regardez le manuel pour en savoir plus. Je suppose que l'argument <bf>level</bf> doit valoir 3 pour autoriser l'accès. SVP, envoyez-moi un message si vous avez des précisions à ce sujet. Maintenant, l'utilisation proprement dite... Pour lire un octet sur un port, appelez <bf>inb(port);</bf> qui retourne l'octet correspondant. Pour écrire un octet, appelez <bf>outb(value, port);</bf> (attention à l'ordre des paramètres). Pour lire un mot sur les ports x et x+1 (mot formé par un octet de chaque port, comme l'instruction INW en assembleur), appelez <bf>inw(x);</bf>. Pour écrire un mot vers deux ports, <bf>outw(value,x);</bf>. Les macros <bf/inb_p()/, <bf/outb_p()/, <bf/inw_p()/ et <bf/outw_p()/ fonctionnent de la même façon que celles précédemment évoquées, mais elles respectent, en plus, une courte attente (environ une microseconde) après l'accès au port; vous pouvez passer l'attente à quatre microsecondes en #définissant <bf/REALLY_SLOW_IO/ avant d'inclure <bf>asm/io.h</bf>. Ces macros créent cette temporisation en écrivant (à moins que vous ne #définissiez <bf/SLOW_IO_BY_JUMPING/, moins précis certainement) dans le port 0x80, vous devez donc préalablement autoriser l'accès à ce port 0x80 avec <bf/ioperm()/ (les écriture vers le port 0x80 ne devraient pas affecter le fonctionnement du système par ailleurs). Pour des méthodes de temporisations plus souples, lisez plus loin. Les pages de manuels associées à ces macros paraitront dans une version future des pages de manuels de Linux. <sect1>Problèmes <p> <sect2>Je récolte des segmentation faults lorsque j'accède aux ports ! <p> Soit votre programme n'a pas les privilèges de root, soit l'appel à <bf/ioperm()/ a échoué pour quelqu'autre raison. Vérifiez la valeur de retour de <bf/ioperm()/. <sect2>Je ne trouve pas les définitions des fonctions in*(), out*(), gcc se plaint de références inconnues ! <p> Vous n'avez pas compilé avec l'option d'optimisation (<em/-O/), et donc gcc n'a pas pu définir les macros dans <bf>asm/io.h</bf>. Ou alors vous n'avez pas #inclus <bf><asm/io.h></bf>. <sect1>Une autre méthode <p> Une autre méthode consiste à ouvrir <bf>/dev/port</bf> (un périphérique caractère, major number 1, minor number 4) en lecture et/ou écriture (en utilisant les fonctions habituelles d'accès aux fichiers, <bf>open()</bf> etc. - les fonctions <bf>f*()</bf> de stdio utilisent des tampons internes, évitez-les). Puis positionnez-vous (<em>seek</em>, NDT) au niveau de l'octet approprié dans le fichier (position 0 dans le fichier = port 0, position 1 = port 1, etc.), lisez-y ou écrivez-y ensuite un octet ou un mot. Je n'ai pas vraiment essayé et je ne suis pas absolument certain que cela marche ainsi ; envoyez-moi un message si vous avez des détails. Bien évidemment, votre programme doit posséder les bons droits d'accès en lecture/écriture sur <bf>/dev/port</bf>. Cette méthode est probablement plus lente que la méthode traditionnelle évoquée auparavant. <sect1>Interruptions (IRQs) et DMA <p> Pour autant que je sache, il n'est pas possible d'utiliser les IRQs ou DMA directement dans un programme en mode utilisateur. Vous devez écrire un pilote dans le noyau  voyez le Linux Kernel Hacker's Guide (khg-x.yy) pour les détails et les sources du noyau pour des exemples. <sect>Réglages de haute précision <p> <sect1>Temporisations <p> Tout d'abord, je dois préciser que, du fait de la nature multi-tâches préemptive de Linux, on ne peut pas garantir à un programme en mode utilisateur un contrôle exact du temps. Votre processus peut perdre l'usage du processeur à n'importe quel instant pour une période allant d'environ 20 millisecondes à quelques secondes (sur un système lourdement chargé). Néanmoins, pour la plupart des applications utilisant les ports d'E/S, cela ne pose pas de problèmes. Pour minimiser cet inconvénient, vous pouvez augmenter la priorité (avec <bf>nice</bf>) de votre programme. Il y a eu des discussions sur des projets de noyaux Linux temps-réel prenant ce phénomène en compte dans <em>comp.os.linux.development.system</em>, mais j'ignore leur avancement ; renseignez-vous dans ce groupe de discussion. Si vous en savez davantage, envoyez-moi un message... Maintenant, commençons par le plus facile. Pour des délais de plusieurs secondes, la meilleure fonction reste probablement <bf>sleep(3)</bf>. Pour des attentes de quelques dixièmes de secondes (20 ms semble un minimum), <bf>usleep(3)</bf> devrait convenir. Ces fonctions rendent le processeur aux autres processus, ce qui ne gâche pas de temps machine. Consultez les pages des manuels pour les détails. Pour des temporisations inférieures à 20 millisecondes environ (suivant la vitesse de votre processeur et de votre machine, ainsi que la charge du système), il faut proscrire l'abandon du processeur car l'ordonnanceur de Linux ne rendrait le contrôle à votre processus qu'après 20 millisecondes minimum (en général). De ce fait, pour des temporisations courtes, <bf>usleep(3)</bf> attendra souvent sensiblement plus longtemps que ce que vous avez spécifié, au moins 20 ms. Pour les délais courts (de quelques dizaines de microsecondes à quelques millisecondes), la méthode la plus simple consiste à utiliser <bf>udelay()</bf>, définie dans <bf>/usr/include/asm/delay.h</bf> (<bf>linux/include/asm-i386/delay.h</bf>). <bf>udelay()</bf> prend comme unique argument le nombre de microsecondes à attendre (unsigned long) et ne renvoie rien. L'attente dure quelques microsecondes de plus que le paramètre spécifié à cause du temps de calcul de la durée d'attente (voyez <bf>delay.h</bf> pour les détails). Pour utiliser <bf>udelay()</bf> en dehors du noyau, la variable (unsigned long) <bf>loops_per_sec</bf> doit être être définie avec la bonne valeur. Autant que je sache, la seule façon de récupérer cette valeur depuis le noyau consiste à lire le nombre de BogoMips dans <bf>/proc/cpuinfo</bf> puis à le multiplier par 500000. On obtient ainsi une évaluation (imprécise) de <bf>loops_per_sec</bf>. Pour les temporisations encore plus courtes, il existe plusieurs solutions. Ecrire n'importe quel octet sur le port 0x80 (voyez plus haut la manière de procéder) doit provoquer une attente d'exactement 1 microseconde, quelque soit le type et la vitesse de votre processeur. Cette écriture ne devrait pas avoir d'effets secondaires sur une machine standard (et certains pilotes de périphériques du noyau l'utilisent). C'est ainsi que <bf>{in|out}{b|w}_p()</bf> réalise normalement sa temporisation (voyez <bf>asm/io.h</bf>). Si vous connaissez le type de processeur et la vitesse de l'horloge de la machine sur laquelle votre programme tournera, vous pouvez coder des délais plus courts <sq>en dur</sq> en exécutant certaines instructions d'assembleur (mais souvenez-vous que votre processus peut perdre le processeur à tout instant, et, par conséquent, que l'attente peut, de temps à autres, s'avérer beaucoup plus importante). Dans la table suivante, la durée d'un cycle d'horloge est déterminée par la vitesse interne du processeur ; par exemple, pour un processeur à 50MHz (486DX-50 ou 486DX2-50), un cycle prend 1/50000000 seconde. <tscreen><verb> Instruction cycles sur i386 cycles sur i486 nop 3 1 xchg %ax,%ax 3 3 or %ax,%ax 2 1 mov %ax,%ax 2 1 add %ax,0 2 1 {source : Borland Turbo Assembler 3.0 Quick Reference} </verb></tscreen> (désolé, je n'ai pas de valeurs pour les Pentiums  ce sont probablement les mêmes que pour i486) (Je ne connais pas d'instruction qui n'utilise qu'un seul cycle sur i386) <p> Les instructions <bf>nop</bf> et <bf>xchg</bf> du tableau n'ont pas d'effets de bord. Les autres peuvent modifier le registre des indicateurs, mais cela ne devrait pas avoir de conséquences puisque <bf>gcc</bf> est sensé le détecter. Pour vous servir de cette astuce, appelez <bf>asm("intruction");</bf> dans votre programme. Pour "instruction", utilisez la même syntaxe que dans la table précédente ; pour avoir plusieurs instructions dans un même <bf>asm()</bf>, faites <bf>asm("instruction; instruction; instruction");</bf>. Comme <bf>asm()</bf> est traduit en langage d'assemblage <sq>inline</sq> par gcc, il n'y a pas de perte de temps consécutive à un éventuel appel de fonction. L'architecture des Intel x86 n'autorise pas de temporisations inférieures à un cycle d'horloge. <sect1>Chronométrages <p> Pour des chronométrages à la seconde près, le plus simple consiste probablement à utiliser <bf>time(2)</bf>. Pour des temps plus fins, <bf>gettimeofday(2)</bf> fournit une précision d'une microseconde (voyez toutefois, plus haut, les remarques concernant l'ordonnancement). Si vous désirez que votre processus reçoive un signal après un certain laps de temps, utilisez <bf>setitimer(2)</bf>. Consultez les pages des manuels des différentes fonctions pour les détails. <sect>Quelques ports utiles <p> Voici quelques informations concernant la programmation des ports les plus courants, pouvant servir, à des fins diverses, d'E/S TTL. <sect1>Le port parallèle <p> Le port parallèle (BASE = 0x3bc pour /dev/lp0, 0x378 pour /dev/lp1 et 0x278 pour /dev/lp2) : {source : <em>IBM PS/2 model 50/60 Technical Reference</em>, et quelques expériences} En plus du mode standard, monodirectionnel en sortie, il existe, pour la plupart des ports parallèles, un mode <sq>étendu</sq> bidirectionnel. Ce mode possède un bit de sens qui peut être positionné en lecture ou écriture. Malheurement, j'ignore comment sélectionner ce mode étendu (il ne l'est pas par défaut)... Le port BASE+0 (port de données) contrôle les signaux de données du port (D0 à D7 pour les bits 0 à 7, respectivement ; états : 0 = bas (0V), 1 = haut (5V)). Une écriture sur ce port recopie (<em>latches</em>, NDT) les données sur les broches. En mode d'écriture standard ou étendu, une lecture renvoie les dernières données écrites. En mode de lecture étendu, une lecture renvoie les données présentes sur les broches du périphérique connecté. Le port BASE+1 (port d'état), en lecture seule, renvoie l'état des signaux d'entrée suivants : <descrip> <tag/Bits 0 et 1/ réservés. <tag/Bit 2/ IRQ status (ne correspond à aucune broche, j'ignore comment il se comporte) <tag/Bit 3/ -ERROR (0=haut) <tag/Bit 4/ SLCT (1=haut) <tag/Bit 5/ PE (1=haut) <tag/Bit 6/ -ACK (0=haut) <tag/Bit 7/ -BUSY (0=haut) </descrip> (Je ne suis pas certain des états hauts et bas.) Le port BASE+2 (port de contrôle), en écriture seule (une lecture renvoie la dernière donnée écrite), contrôle les signaux d'états suivants : <descrip> <tag/Bit 0/ -STROBE (0=haut) <tag/Bit 1/ AUTO_FD_XT (1=haut) <tag/Bit 2/ -INIT (0=haut) <tag/Bit 3/ SLCT_IN (1=haut) <tag/Bit 4/ si positionné à 1, autorise l'IRQ associée au port parallèle (qui intervient lors de la transition de -ACK de bas à haut). <tag/Bit 5/ commande le sens du mode étendu (0 = écriture, 1 = lecture), en écriture seule (une lecture ne renvoie rien d'utile sur ce bit). <tag/Bits 6 et 7/ réservés. </descrip> (Là non plus, je ne suis pas certain des états hauts et bas.) Brochage (un connecteur 25 broches femelle sur le port) (<em/e/=entrée, <em/s/=sortie) : <bf/1/<em/es/ -STROBE, <bf/2/<em/es/ D0, <bf/3/<em/es/ D1, <bf/4/<em/es/ D2, <bf/5/<em/es/ D3, <bf/6/<em/es/ D4, <bf/7/<em/es/ D5, <bf/8/<em/es/ D6, <bf/9/<em/es/ D7, <bf/10/<em/e/ -ACK, <bf/11/<em/e/ -BUSY, <bf/12/<em/e/ PE, <bf/13/<em/e/ SLCT, <bf/14/<em/s/ AUTO_FD_XT, <bf/15/<em/e/ -ERROR, <bf/16/<em/s/ -INIT, <bf/17/<em/s/ SLCT_IN, <bf/18-25/ Masse. Les spécifications d'IBM précisent que les broches 1, 14, 16 et 17 (les sorties de contrôle) sont à collecteurs ouverts, connectées au 5V à travers des résistances de 4,7kiloohms (puits 20mA, source 0,55mA, niveau de sortie haut 5V moins la tension aux bornes de la résistance). Les autres broches ont un courant de puits de 24mA, de source de 15mA et leur niveau de sortie haut est supérieur à 2,4V. L'état bas dans les deux cas est inférieur à 0,5V. Il est probable que les ports parallèles des clones s'écartent de cette norme. Enfin, un avertissement : attention à la mise à la masse. J'ai endommagé plusieurs ports parallèles en les connectant alors que la machine fonctionnait. Il est conseillé d'utiliser un port parallèle non intégré à la carte mère pour faire des choses pareilles. <sect1>Le port jeu <p> Le port jeu (ports 0x200-0x207) : je n'ai pas de spécifications là-dessus, mais je pense qu'il doit y avoir au moins quelques entrées TTL et un peu de puissance en sortie. Si quelqu'un possède plus d'informations, qu'il me le fasse savoir... <sect1>E/S analogiques <p> Si vous voulez des E/S analogiques, vous pouvez connecter des circuits convertisseurs analogiques-numériques (ADC) et/ou numériques-analogiques (DAC) sur ces ports (astuce : pour l'alimentation, utilisez un connecteur d'alimentation (de lecteur) inutilisé que vous sortirez du boitier, à moins que votre composant ne consomme très peu, auquel cas le port lui-même peut fournir la puissance). Sinon, achetez une carte AD/DA (la plupart sont contrôlées par les ports d'E/S). Ou, si vous pouvez vous contenter de 1 ou 2 voies, peu précises, et (probablement) mal réglées en zéro, une carte son à bas prix, supportée par le pilote sonore de Linux, devrait faire l'affaire (et se montrera plutôt rapide). <sect>Ce qu'il reste à faire <p> <itemize> <item>vérifier ce dont je n'étais pas sûr <item>donner des exemples simples d'utilisation des fonctions décrites </itemize> Merci pour les nombreuses corrections et additions utiles que j'ai reçues. Fin du mini-HOWTO programmation des ports d'E/S sous Linux </article>