  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).

  11..  UUttiilliissaattiioonn ddeess ppoorrttss dd''EE//SS ddaannss lleess pprrooggrraammmmeess CC

  11..11..  MMeetthhooddee ccllaassssiiqquuee

  Les routines permettant l'acces aux ports  d'E/S  sont  definies  dans
  //uussrr//iinncclluuddee//aassmm//iioo..hh (ou lliinnuuxx//iinncclluuddee//aassmm--ii338866//iioo..hh dans les sources
  du noyau). Ce sont des macros "inline", il  suffit  donc  de  #inclure
  <<aassmm//iioo..hh>> ; Aucune autre bibliotheque (_l_i_b_r_a_r_y, NDT) n'est requise.

  Du  fait  d'une  limitation  de ggcccc (au moins jusqu'a la version 2.7.0
  comprise), vous ddeevveezz compiler tout code source utilisant ces routines
  avec les options d'optimisation (i.e. _g_c_c _-_O). Une autre limitation de
  ggcccc empeche de compiler a la fois avec les options  d'optimisation  et
  de  mise au point (_-_g). Cela signifie que si vous desirez utiliser ggddbb
  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
  iiooppeerrmm((22)) (declaree dans uunniissttdd..hh et definie dans  le  noyau)  quelque
  part au debut de votre application (avant tout acces a un port d'E/S).
  La syntaxe est iiooppeerrmm((ffrroomm,,nnuumm,,ttuurrnn__oonn)), ou ffrroomm represente le premier
  numero  de  port  et  nnuumm  le  nombre  de  ports  consecutifs a rendre
  accessibles. Par exemple, iiooppeerrmm((00xx330000,,55,,11));; 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 iiooppeerrmm(()) autant que necessaire. Consultez la  page  de
  manuel de iiooppeerrmm((22)) pour avoir des precisions sur la syntaxe.

  Votre  programme  ne  peut  appeler  iiooppeerrmm(())  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 iiooppeerrmm(()). Il n'est pas necessaire d'appeler iiooppeerrmm((......,,00)) a la fin
  du programme pour abandonner explicitement les droits, cette procedure
  etant automatique.

  Les privileges accordes  par  iiooppeerrmm(())  demeurent  lors  d'un  ffoorrkk(()),
  eexxeecc(()) ou sseettuuiidd(()) en un utilisateur autre que root.

  iiooppeerrmm(()) ne permet l'acces qu'aux ports 0x000 a 0x3ff ; pour les ports
  superieurs, il faut utiliser iiooppll((22)) (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 lleevveell  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  iinnbb((ppoorrtt));;  qui  retourne  l'octet correspondant. Pour
  ecrire un octet, appelez oouuttbb((vvaalluuee,, ppoorrtt));; (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
  iinnww((xx));;. Pour ecrire un mot vers deux ports, oouuttww((vvaalluuee,,xx));;.

  Les  macros  iinnbb__pp(()), oouuttbb__pp(()), iinnww__pp(()) et oouuttww__pp(()) 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  RREEAALLLLYY__SSLLOOWW__IIOO  avant  d'inclure  aassmm//iioo..hh.  Ces  macros
  creent  cette  temporisation  en  ecrivant  (a  moins  que   vous   ne
  #definissiez  SSLLOOWW__IIOO__BBYY__JJUUMMPPIINNGG,  moins  precis certainement) dans le
  port 0x80, vous devez donc prealablement autoriser l'acces a  ce  port
  0x80  avec  iiooppeerrmm(())  (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.

  11..22..  PPrroobblleemmeess

  11..22..11..  ppoorrttss !!  JJee rreeccoollttee ddeess sseeggmmeennttaattiioonn ffaauullttss  lloorrssqquuee  jj''aacccceeddee
  aauuxx

  Soit votre programme n'a pas les privileges de root,  soit  l'appel  a
  iiooppeerrmm(())  a  echoue  pour  quelqu'autre raison.  Verifiez la valeur de
  retour de iiooppeerrmm(()).

  11..22..22..  ggcccc ssee ppllaaiinntt ddee rreeffeerreenncceess iinnccoonnnnuueess !!  JJee nnee ttrroouuvvee ppaass  lleess
  ddeeffiinniittiioonnss ddeess ffoonnccttiioonnss iinn**(()),, oouutt**(()),,

  Vous n'avez pas compile avec l'option d'optimisation (_-_O), et donc gcc
  n'a  pas pu definir les macros dans aassmm//iioo..hh. Ou alors vous n'avez pas
  #inclus <<aassmm//iioo..hh>>.

  11..33..  UUnnee aauuttrree mmeetthhooddee

  Une  autre  methode  consiste  a  ouvrir  //ddeevv//ppoorrtt  (un  peripherique
  caractere,  major  number 1, minor number 4) en lecture et/ou ecriture
  (en utilisant les fonctions habituelles d'acces aux  fichiers,  ooppeenn(())
  etc.  -  les  fonctions  ff**(()) 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 //ddeevv//ppoorrtt. Cette methode est probablement plus
  lente que la methode traditionnelle evoquee auparavant.

  11..44..  IInntteerrrruuppttiioonnss ((IIRRQQss)) eett DDMMAA

  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.

  22..  RReeggllaaggeess ddee hhaauuttee pprreecciissiioonn

  22..11..  TTeemmppoorriissaattiioonnss

  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
  nniiccee) 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 sslleeeepp((33)).
  Pour des attentes de quelques dixiemes de secondes (20  ms  semble  un
  minimum),   uusslleeeepp((33))  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, uusslleeeepp((33)) 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
  uuddeellaayy(()),  definie  dans  //uussrr//iinncclluuddee//aassmm//ddeellaayy..hh (lliinnuuxx//iinncclluuddee//aassmm--
  ii338866//ddeellaayy..hh). uuddeellaayy(()) 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 ddeellaayy..hh pour les
  details).

  Pour utiliser uuddeellaayy(()) en dehors du noyau, la variable (unsigned long)
  llooooppss__ppeerr__sseecc 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 //pprroocc//ccppuuiinnffoo puis a le
  multiplier par 500000. On obtient ainsi une evaluation (imprecise)  de
  llooooppss__ppeerr__sseecc.

  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 {{iinn||oouutt}}{{bb||ww}}__pp(()) realise normalement sa
  temporisation (voyez aassmm//iioo..hh).

  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 nnoopp et xxcchhgg 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 ggcccc est sense le detecter.

  Pour vous servir de  cette  astuce,  appelez  aassmm((""iinnttrruuccttiioonn""));;  dans
  votre programme. Pour "instruction", utilisez la meme syntaxe que dans
  la table precedente ; pour avoir plusieurs instructions dans  un  meme
  aassmm(()),  faites  aassmm((""iinnssttrruuccttiioonn;;  iinnssttrruuccttiioonn;;  iinnssttrruuccttiioonn""));;. Comme
  aassmm(()) 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.

  22..22..  CChhrroonnoommeettrraaggeess

  Pour des chronometrages a la seconde pres,  le  plus  simple  consiste
  probablement   a   utiliser   ttiimmee((22)).   Pour  des  temps  plus  fins,
  ggeettttiimmeeooffddaayy((22))  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 sseettiittiimmeerr((22)). Consultez les pages des  manuels
  des differentes fonctions pour les details.

  33..  QQuueellqquueess ppoorrttss uuttiilleess

  Voici  quelques informations concernant la programmation des ports les
  plus courants, pouvant servir, a des fins diverses, d'E/S TTL.

  33..11..  LLee ppoorrtt ppaarraalllleellee

  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 :

     BBiittss 00 eett 11
        reserves.

     BBiitt 22
        IRQ  status  (ne correspond a aucune broche, j'ignore comment il
        se comporte)

     BBiitt 33
        -ERROR (0=haut)

     BBiitt 44
        SLCT (1=haut)

     BBiitt 55
        PE (1=haut)

     BBiitt 66
        -ACK (0=haut)

     BBiitt 77
        -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 :

     BBiitt 00
        -STROBE (0=haut)

     BBiitt 11
        AUTO_FD_XT (1=haut)

     BBiitt 22
        -INIT (0=haut)
     BBiitt 33
        SLCT_IN (1=haut)

     BBiitt 44
        si positionne a 1, autorise l'IRQ  associee  au  port  parallele
        (qui intervient lors de la transition de -ACK de bas a haut).

     BBiitt 55
        commande  le sens du mode etendu (0 = ecriture, 1 = lecture), en
        ecriture seule (une lecture ne renvoie rien d'utile sur ce bit).

     BBiittss 66 eett 77
        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) :

  11_e_s -STROBE, 22_e_s D0, 33_e_s D1, 44_e_s D2, 55_e_s D3, 66_e_s D4, 77_e_s D5,  88_e_s  D6,
  99_e_s  D7,  1100_e  -ACK,  1111_e -BUSY, 1122_e PE, 1133_e SLCT, 1144_s AUTO_FD_XT, 1155_e
  -ERROR, 1166_s -INIT, 1177_s SLCT_IN, 1188--2255 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.

  33..22..  LLee ppoorrtt jjeeuu

  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...

  33..33..  EE//SS aannaallooggiiqquueess

  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).

  44..  CCee qquu''iill rreessttee aa ffaaiirree

  +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

