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