Mini-HOWTO programmation des ports d'E/S sous Linux
(c) 1995 Riku Saikkonen
[email protected]
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
(
[email protected]).
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
<<aassmm//iioo..hh>> ; 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 <<aassmm//iioo..hh>>.
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