Mini-HOWTO programmation des ports d'E/S sous Linux (c) 1995 Riku Saikkonen rjs@spider.compart.fi 26 Dec 1995 Ce HOWTO traite de l'utilisation des ports d'E/S ainsi que de la pro- grammation de mini-temporisations (de quelques microsecondes a quelques millisecondes) en C sous Linux (mode utilisateur) sur pro- cesseur Intel x86. Ce document est issu du minuscule IO-Port mini- HOWTO du meme auteur. Si vous avez des modifications a apporter ou des complements a ajouter, n'hesitez pas a m'envoyer un message (rjs@spi- der.compart.fi)... Innombrables modifications depuis la precedente version (16 Nov 1995) dont l'ajout des specifications du port paral- lele. Adaptation francaise realisee par Nicolas Lejeune (nl@freenix.fr). 11.. UUttiilliissaattiioonn ddeess ppoorrttss dd''EE//SS ddaannss lleess pprrooggrraammmmeess CC 11..11.. MMeetthhooddee ccllaassssiiqquuee Les routines permettant l'acces aux ports d'E/S sont definies dans //uussrr//iinncclluuddee//aassmm//iioo..hh (ou lliinnuuxx//iinncclluuddee//aassmm--ii338866//iioo..hh dans les sources du noyau). Ce sont des macros "inline", il suffit donc de #inclure <> ; Aucune autre bibliotheque (_l_i_b_r_a_r_y, NDT) n'est requise. Du fait d'une limitation de ggcccc (au moins jusqu'a la version 2.7.0 comprise), vous ddeevveezz compiler tout code source utilisant ces routines avec les options d'optimisation (i.e. _g_c_c _-_O). Une autre limitation de ggcccc empeche de compiler a la fois avec les options d'optimisation et de mise au point (_-_g). Cela signifie que si vous desirez utiliser ggddbb 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 separe, 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 a votre programme la permission de le faire. Il suffit pour cela d'appeler la fonction iiooppeerrmm((22)) (declaree dans uunniissttdd..hh et definie dans le noyau) quelque part au debut de votre application (avant tout acces a un port d'E/S). La syntaxe est iiooppeerrmm((ffrroomm,,nnuumm,,ttuurrnn__oonn)), ou ffrroomm represente le premier numero de port et nnuumm le nombre de ports consecutifs a rendre accessibles. Par exemple, iiooppeerrmm((00xx330000,,55,,11));; autoriserait l'acces aux ports 0x300 a 0x304 (5 ports au total). Le dernier argument est un booleen precisant si l'on desire donner (vrai (1)) ou retirer (faux (0)) l'acces au port. Pour autoriser plusieurs ports non consecutifs, on peut appeler iiooppeerrmm(()) autant que necessaire. Consultez la page de manuel de iiooppeerrmm((22)) pour avoir des precisions sur la syntaxe. Votre programme ne peut appeler iiooppeerrmm(()) que s'il possede les privileges de root ; pour cela, vous devez soit le lancer comme utilisateur root, soit le rendre suid root. Il devrait etre possible (Je n'ai pas essaye ; SVP, envoyez-moi un message si vous l'avez fait) d'abandonner les privileges de root une fois l'acces aux ports obtenu par iiooppeerrmm(()). Il n'est pas necessaire d'appeler iiooppeerrmm((......,,00)) a la fin du programme pour abandonner explicitement les droits, cette procedure etant automatique. Les privileges accordes par iiooppeerrmm(()) demeurent lors d'un ffoorrkk(()), eexxeecc(()) ou sseettuuiidd(()) en un utilisateur autre que root. iiooppeerrmm(()) ne permet l'acces qu'aux ports 0x000 a 0x3ff ; pour les ports superieurs, il faut utiliser iiooppll((22)) (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 lleevveell doit valoir 3 pour autoriser l'acces. SVP, envoyez-moi un message si vous avez des precisions a ce sujet. Maintenant, l'utilisation proprement dite... Pour lire un octet sur un port, appelez iinnbb((ppoorrtt));; qui retourne l'octet correspondant. Pour ecrire un octet, appelez oouuttbb((vvaalluuee,, ppoorrtt));; (attention a l'ordre des parametres). Pour lire un mot sur les ports x et x+1 (mot forme par un octet de chaque port, comme l'instruction INW en assembleur), appelez iinnww((xx));;. Pour ecrire un mot vers deux ports, oouuttww((vvaalluuee,,xx));;. Les macros iinnbb__pp(()), oouuttbb__pp(()), iinnww__pp(()) et oouuttww__pp(()) fonctionnent de la meme facon que celles precedemment evoquees, mais elles respectent, en plus, une courte attente (environ une microseconde) apres l'acces au port; vous pouvez passer l'attente a quatre microsecondes en #definissant RREEAALLLLYY__SSLLOOWW__IIOO avant d'inclure aassmm//iioo..hh. Ces macros creent cette temporisation en ecrivant (a moins que vous ne #definissiez SSLLOOWW__IIOO__BBYY__JJUUMMPPIINNGG, moins precis certainement) dans le port 0x80, vous devez donc prealablement autoriser l'acces a ce port 0x80 avec iiooppeerrmm(()) (les ecriture vers le port 0x80 ne devraient pas affecter le fonctionnement du systeme par ailleurs). Pour des methodes de temporisations plus souples, lisez plus loin. Les pages de manuels associees a ces macros paraitront dans une version future des pages de manuels de Linux. 11..22.. PPrroobblleemmeess 11..22..11.. ppoorrttss !! JJee rreeccoollttee ddeess sseeggmmeennttaattiioonn ffaauullttss lloorrssqquuee jj''aacccceeddee aauuxx Soit votre programme n'a pas les privileges de root, soit l'appel a iiooppeerrmm(()) a echoue pour quelqu'autre raison. Verifiez la valeur de retour de iiooppeerrmm(()). 11..22..22.. ggcccc ssee ppllaaiinntt ddee rreeffeerreenncceess iinnccoonnnnuueess !! JJee nnee ttrroouuvvee ppaass lleess ddeeffiinniittiioonnss ddeess ffoonnccttiioonnss iinn**(()),, oouutt**(()),, Vous n'avez pas compile avec l'option d'optimisation (_-_O), et donc gcc n'a pas pu definir les macros dans aassmm//iioo..hh. Ou alors vous n'avez pas #inclus <>. 11..33.. UUnnee aauuttrree mmeetthhooddee Une autre methode consiste a ouvrir //ddeevv//ppoorrtt (un peripherique caractere, major number 1, minor number 4) en lecture et/ou ecriture (en utilisant les fonctions habituelles d'acces aux fichiers, ooppeenn(()) etc. - les fonctions ff**(()) de stdio utilisent des tampons internes, evitez-les). Puis positionnez-vous (_s_e_e_k, NDT) au niveau de l'octet approprie dans le fichier (position 0 dans le fichier = port 0, position 1 = port 1, etc.), lisez-y ou ecrivez-y ensuite un octet ou un mot. Je n'ai pas vraiment essaye et je ne suis pas absolument certain que cela marche ainsi ; envoyez-moi un message si vous avez des details. Bien evidemment, votre programme doit posseder les bons droits d'acces en lecture/ecriture sur //ddeevv//ppoorrtt. Cette methode est probablement plus lente que la methode traditionnelle evoquee auparavant. 11..44.. IInntteerrrruuppttiioonnss ((IIRRQQss)) eett DDMMAA 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 ecrire un pilote dans le noyau voyez le Linux Kernel Hacker's Guide (khg-x.yy) pour les details et les sources du noyau pour des exemples. 22.. RReeggllaaggeess ddee hhaauuttee pprreecciissiioonn 22..11.. TTeemmppoorriissaattiioonnss Tout d'abord, je dois preciser que, du fait de la nature multi-taches preemptive de Linux, on ne peut pas garantir a un programme en mode utilisateur un controle exact du temps. Votre processus peut perdre l'usage du processeur a n'importe quel instant pour une periode allant d'environ 20 millisecondes a quelques secondes (sur un systeme lourdement charge). Neanmoins, pour la plupart des applications utilisant les ports d'E/S, cela ne pose pas de problemes. Pour minimiser cet inconvenient, vous pouvez augmenter la priorite (avec nniiccee) de votre programme. Il y a eu des discussions sur des projets de noyaux Linux temps-reel prenant ce phenomene en compte dans _c_o_m_p_._o_s_._l_i_n_u_x_._d_e_v_e_l_o_p_m_e_n_t_._s_y_s_t_e_m, mais j'ignore leur avancement ; renseignez-vous dans ce groupe de discussion. Si vous en savez davantage, envoyez-moi un message... Maintenant, commencons par le plus facile. Pour des delais de plusieurs secondes, la meilleure fonction reste probablement sslleeeepp((33)). Pour des attentes de quelques dixiemes de secondes (20 ms semble un minimum), uusslleeeepp((33)) devrait convenir. Ces fonctions rendent le processeur aux autres processus, ce qui ne gache pas de temps machine. Consultez les pages des manuels pour les details. Pour des temporisations inferieures a 20 millisecondes environ (suivant la vitesse de votre processeur et de votre machine, ainsi que la charge du systeme), il faut proscrire l'abandon du processeur car l'ordonnanceur de Linux ne rendrait le controle a votre processus qu'apres 20 millisecondes minimum (en general). De ce fait, pour des temporisations courtes, uusslleeeepp((33)) attendra souvent sensiblement plus longtemps que ce que vous avez specifie, au moins 20 ms. Pour les delais courts (de quelques dizaines de microsecondes a quelques millisecondes), la methode la plus simple consiste a utiliser uuddeellaayy(()), definie dans //uussrr//iinncclluuddee//aassmm//ddeellaayy..hh (lliinnuuxx//iinncclluuddee//aassmm-- ii338866//ddeellaayy..hh). uuddeellaayy(()) prend comme unique argument le nombre de microsecondes a attendre (unsigned long) et ne renvoie rien. L'attente dure quelques microsecondes de plus que le parametre specifie a cause du temps de calcul de la duree d'attente (voyez ddeellaayy..hh pour les details). Pour utiliser uuddeellaayy(()) en dehors du noyau, la variable (unsigned long) llooooppss__ppeerr__sseecc doit etre etre definie avec la bonne valeur. Autant que je sache, la seule facon de recuperer cette valeur depuis le noyau consiste a lire le nombre de BogoMips dans //pprroocc//ccppuuiinnffoo puis a le multiplier par 500000. On obtient ainsi une evaluation (imprecise) de llooooppss__ppeerr__sseecc. Pour les temporisations encore plus courtes, il existe plusieurs solutions. Ecrire n'importe quel octet sur le port 0x80 (voyez plus haut la maniere de proceder) doit provoquer une attente d'exactement 1 microseconde, quelque soit le type et la vitesse de votre processeur. Cette ecriture ne devrait pas avoir d'effets secondaires sur une machine standard (et certains pilotes de peripheriques du noyau l'utilisent). C'est ainsi que {{iinn||oouutt}}{{bb||ww}}__pp(()) realise normalement sa temporisation (voyez aassmm//iioo..hh). 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 delais plus courts "en dur" en executant certaines instructions d'assembleur (mais souvenez-vous que votre processus peut perdre le processeur a tout instant, et, par consequent, que l'attente peut, de temps a autres, s'averer beaucoup plus importante). Dans la table suivante, la duree d'un cycle d'horloge est determinee par la vitesse interne du processeur ; par exemple, pour un processeur a 50MHz (486DX-50 ou 486DX2-50), un cycle prend 1/50000000 seconde. 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} (desole, je n'ai pas de valeurs pour les Pentiums ce sont probablement les memes que pour i486) (Je ne connais pas d'instruction qui n'utilise qu'un seul cycle sur i386) Les instructions nnoopp et xxcchhgg du tableau n'ont pas d'effets de bord. Les autres peuvent modifier le registre des indicateurs, mais cela ne devrait pas avoir de consequences puisque ggcccc est sense le detecter. Pour vous servir de cette astuce, appelez aassmm((""iinnttrruuccttiioonn""));; dans votre programme. Pour "instruction", utilisez la meme syntaxe que dans la table precedente ; pour avoir plusieurs instructions dans un meme aassmm(()), faites aassmm((""iinnssttrruuccttiioonn;; iinnssttrruuccttiioonn;; iinnssttrruuccttiioonn""));;. Comme aassmm(()) est traduit en langage d'assemblage "inline" par gcc, il n'y a pas de perte de temps consecutive a un eventuel appel de fonction. L'architecture des Intel x86 n'autorise pas de temporisations inferieures a un cycle d'horloge. 22..22.. CChhrroonnoommeettrraaggeess Pour des chronometrages a la seconde pres, le plus simple consiste probablement a utiliser ttiimmee((22)). Pour des temps plus fins, ggeettttiimmeeooffddaayy((22)) fournit une precision d'une microseconde (voyez toutefois, plus haut, les remarques concernant l'ordonnancement). Si vous desirez que votre processus recoive un signal apres un certain laps de temps, utilisez sseettiittiimmeerr((22)). Consultez les pages des manuels des differentes fonctions pour les details. 33.. QQuueellqquueess ppoorrttss uuttiilleess Voici quelques informations concernant la programmation des ports les plus courants, pouvant servir, a des fins diverses, d'E/S TTL. 33..11.. LLee ppoorrtt ppaarraalllleellee Le port parallele (BASE = 0x3bc pour /dev/lp0, 0x378 pour /dev/lp1 et 0x278 pour /dev/lp2) : {source : _I_B_M _P_S_/_2 _m_o_d_e_l _5_0_/_6_0 _T_e_c_h_n_i_c_a_l _R_e_f_e_r_e_n_c_e, et quelques experiences} En plus du mode standard, monodirectionnel en sortie, il existe, pour la plupart des ports paralleles, un mode "etendu" bidirectionnel. Ce mode possede un bit de sens qui peut etre positionne en lecture ou ecriture. Malheurement, j'ignore comment selectionner ce mode etendu (il ne l'est pas par defaut)... Le port BASE+0 (port de donnees) controle les signaux de donnees du port (D0 a D7 pour les bits 0 a 7, respectivement ; etats : 0 = bas (0V), 1 = haut (5V)). Une ecriture sur ce port recopie (_l_a_t_c_h_e_s, NDT) les donnees sur les broches. En mode d'ecriture standard ou etendu, une lecture renvoie les dernieres donnees ecrites. En mode de lecture etendu, une lecture renvoie les donnees presentes sur les broches du peripherique connecte. Le port BASE+1 (port d'etat), en lecture seule, renvoie l'etat des signaux d'entree suivants : BBiittss 00 eett 11 reserves. BBiitt 22 IRQ status (ne correspond a aucune broche, j'ignore comment il se comporte) BBiitt 33 -ERROR (0=haut) BBiitt 44 SLCT (1=haut) BBiitt 55 PE (1=haut) BBiitt 66 -ACK (0=haut) BBiitt 77 -BUSY (0=haut) (Je ne suis pas certain des etats hauts et bas.) Le port BASE+2 (port de controle), en ecriture seule (une lecture renvoie la derniere donnee ecrite), controle les signaux d'etats suivants : BBiitt 00 -STROBE (0=haut) BBiitt 11 AUTO_FD_XT (1=haut) BBiitt 22 -INIT (0=haut) BBiitt 33 SLCT_IN (1=haut) BBiitt 44 si positionne a 1, autorise l'IRQ associee au port parallele (qui intervient lors de la transition de -ACK de bas a haut). BBiitt 55 commande le sens du mode etendu (0 = ecriture, 1 = lecture), en ecriture seule (une lecture ne renvoie rien d'utile sur ce bit). BBiittss 66 eett 77 reserves. (La non plus, je ne suis pas certain des etats hauts et bas.) Brochage (un connecteur 25 broches femelle sur le port) (_e=entree, _s=sortie) : 11_e_s -STROBE, 22_e_s D0, 33_e_s D1, 44_e_s D2, 55_e_s D3, 66_e_s D4, 77_e_s D5, 88_e_s D6, 99_e_s D7, 1100_e -ACK, 1111_e -BUSY, 1122_e PE, 1133_e SLCT, 1144_s AUTO_FD_XT, 1155_e -ERROR, 1166_s -INIT, 1177_s SLCT_IN, 1188--2255 Masse. Les specifications d'IBM precisent que les broches 1, 14, 16 et 17 (les sorties de controle) sont a collecteurs ouverts, connectees au 5V a travers des resistances de 4,7kiloohms (puits 20mA, source 0,55mA, niveau de sortie haut 5V moins la tension aux bornes de la resistance). Les autres broches ont un courant de puits de 24mA, de source de 15mA et leur niveau de sortie haut est superieur a 2,4V. L'etat bas dans les deux cas est inferieur a 0,5V. Il est probable que les ports paralleles des clones s'ecartent de cette norme. Enfin, un avertissement : attention a la mise a la masse. J'ai endommage plusieurs ports paralleles en les connectant alors que la machine fonctionnait. Il est conseille d'utiliser un port parallele non integre a la carte mere pour faire des choses pareilles. 33..22.. LLee ppoorrtt jjeeuu Le port jeu (ports 0x200-0x207) : je n'ai pas de specifications la- dessus, mais je pense qu'il doit y avoir au moins quelques entrees TTL et un peu de puissance en sortie. Si quelqu'un possede plus d'informations, qu'il me le fasse savoir... 33..33.. EE//SS aannaallooggiiqquueess Si vous voulez des E/S analogiques, vous pouvez connecter des circuits convertisseurs analogiques-numeriques (ADC) et/ou numeriques- analogiques (DAC) sur ces ports (astuce : pour l'alimentation, utilisez un connecteur d'alimentation (de lecteur) inutilise que vous sortirez du boitier, a moins que votre composant ne consomme tres peu, auquel cas le port lui-meme peut fournir la puissance). Sinon, achetez une carte AD/DA (la plupart sont controlees par les ports d'E/S). Ou, si vous pouvez vous contenter de 1 ou 2 voies, peu precises, et (probablement) mal reglees en zero, une carte son a bas prix, supportee par le pilote sonore de Linux, devrait faire l'affaire (et se montrera plutot rapide). 44.. CCee qquu''iill rreessttee aa ffaaiirree +o verifier ce dont je n'etais pas sur +o donner des exemples simples d'utilisation des fonctions decrites Merci pour les nombreuses corrections et additions utiles que j'ai recues. Fin du mini-HOWTO programmation des ports d'E/S sous Linux .