Linux I/O ポートプログラミング mini-HOWTO Riku Saikkonen
Riku.Saikkonen@hut.fi
JF Project 日本語訳
JF@linux.or.jp
v3.0, 2000-12-13 この ハウツーでは、インテルの x86 プロセッサ上で走るプログラム上の、 ハードウェア I/O ポートのプログラミングと、Linux のユーザモードで短い時間 待ちをおこなうプログラム、などについて述べます。
Introduction この ハウツーでは、インテルの x86 プロセッサ上で走るプログラム上の、 ハードウェア I/O ポートのプログラミングと、Linux のユーザモードで短い時間 待ちをおこなうプログラム、などについて述べます。 私は前にも同じような(とても小さな) IO-Port-mini-HOWTO を書きましたが、 この文書はその続編です。 このドキュメントの著作権は 1995-2000 Riku Saikkonsen に属します。 詳細に関しては通常の Linux の HOWTO COPYRIGHT をご覧下さい。 もし、なにか間違いがあったり、付け加えることなどありましたらぜひ 私にメイルしてください。(Riku.Saikkonen@hut.fi) 日本語訳版についてのご指摘、ご意見などございましたら JF Projectまでお願いします。<JF@linux.or.jp> C プログラムから I/O ポートを使う 普通の方法 I/O ポートをアクセスするためのルーチンは /usr/include/asm/io.h (またはカーネルのソースパッケージの中の linux/include/asm-i386/io.h) に定義されています。必要なルーチンは、 この中でインラインマクロとして定義されていま すので、#include <asm/io.h> とするだけで十分でしょう。特別なライブラリなどは不要です。 gcc (私の知る限り全てのバージョン、egcs も含む) からの制限のため、 これらのルーチンを使うソースをコンパイルするにあたり、最適化をオン にする( gcc またはそれ以上)、あるい は #include <asm/io.h> の前に #define extern static という宣言を おこなっておく必要があります。 (あとで #undef extern しておくのを 忘れないように。) デバッグのために、(少なくとも最近の gcc では) gcc を使うことができますが、 最適化されたコードではデ バッガが変な挙動を示すこともあります。このような場合には、I/O ポート アクセスを含んだコードを独立したファイルにしておいて、そのファイルの コンパイルの時にだけ最適化をオンにするという方法を使うこともできます。 パーミッション ポートをアクセスする前に、そのポートをアクセスする許可をプログラムに与え なければなりません。これには、ioperm() 関数 (unistd.h で宣言され、カーネルの中で 定義されている)を呼び出します。この関数はプログラムの最初の方で( I/O ポート アクセスをする前に)呼び出す必要があります。 この関数の書式は ioperm(from, num, turn_on) です。 fromはアクセスを許すポートの最初のアドレス、 numfromから いくつの連続アドレスのアクセスを許すか、を指定します。 例えば、ioperm(0x300, 5, 1) は、0x300 から 0x304 (合計で 5 つのポート)に対する許可を指定し、最後のパラメータでは、 そのポートに対するアクセス許可を与えるのか禁止するのかを論理値(true : 1 = アクセスを許可する, false : 0 = アクセスを禁止する)で指定します。 連続していないポートの場合には、複数回ioperm() を呼び出して、適切な許可を指定します。 文法の詳細に関しては ioperm(2) のマニュアル ページを参照してください。 ioperm() を呼び出すには、そのプログラムが root 特権で走っていることが必要です。 つまり、そのプログラムをルートユーザで実行するか、setuid root (訳注: 実 行ファイルのオーナーを root にしておき、chmod 4755 hogehoge というやつ です。)しておく必要があります。 必要なポートに対する許可を ioperm() で与え たあとに、root 特権を落すこともできます。 プログラムの最後で、明示的に ioperm(..., 0) を呼び出して許可を落す必要はありません。 これはプログラムの終了時に自動的に行なわれますから。 root 以外のユーザに setuid() することで ioperm() の与えたポートアクセス許可が なくなることはありませんが、一方で fork() すると許可がなくなります。 (子プロセスはアクセス許可を持っていませんが、親プロセスは持っています。) ioperm() では、0x000 から 0x3ff までの ポートに対するアクセス許可しか与えることはできません。 これよりも上のポートに対しては、iopl() を使う必要があります。 (iopl() を使うと一度にすべてのポートに対 するアクセス許可を与えることになります。) すべてのポートに対してアクセス許可を与えるには、 レベルパラメータとして「3」を使います。つまり、 iopl(3) という関数呼び出しをします。 (間違ったポートにアクセスするととんでもないことが起きるかも知れません から、使うときには注意してください。) もちろん、iopl() を呼び出す時には root 特権が必要です。 詳細に関しては iopl(2) のマニュアルページを 参照してください。 ポートのアクセス ポートからバイトデータ(8bit)を入力する場合には inb(port) を呼び出します。 この関数はバイトデータを返します。 ポートにバイトデータを出力するには、outb(value, port) を呼び出します。 (パラメータの順番に注意して下さい。) (訳注:アセンブラや、MS-DOS のコンパイラライブラリなどの場合とは逆です。) ポートアドレス xx+1 からワードデータを入力する場合には inw(x) を呼び出します。(アセンブラのinw とまったく同じようにそれぞれのポートから 1 バイトずつ読んできて、ワード データを構成するわけですが。) ふたつのポートにワード( 2 バイト)を出力するには outw(value, x) を使います。 どちらの命令(バイトデータかワードデータか)を使えばいいのか分からないときは、 多分、inb()outb() を使えばいいでしょう。 大抵のデバイスはバイト単位のポートアクセスをするよう設計されています。 ポートアクセスの命令はすべて実行に少なくとも約 1 マイクロ秒かかるので 注意してください。 inb_p(), outb_p(), inw_p(), outw_p() といったマクロは、上に述べたのと 同じ動作をしますが、ポートにアクセスした後に少しだけ(約 1 マイクロ秒) ウエイトします。 #include <asm/io.h> の前に #define REALLY_SLOW_IO を 付け加えるとこのウエイト時間を 4 マイクロ秒に変えることができます。 これらのマクロは通常、0x80 番地の I/O ポートに出力することでウエイトを 実現しています。 (#define SLOW_IO_BY_JUMPING としている場合には別の方法が使われますが、これはあまり正確な遅延 をおこなうことができないはずです。) このような理由から、0x80 番地のポートに対する ioperm() を先に実行しておく必要が あります。(0x80 番地のポートに対する出力はシステムに対してなんら影響を 与えることはないはずです。) さらにさまざまな遅延を行なう方法については、以下を読み進めて下さい。 そこそこ最近のリリースの Linux マニュアルページには、 ioperm(2)iopl(2) 、上述のマクロに関する説明があります。 I/O ポートアクセスをする別の方法:<literal>/dev/port</literal> I/O ポートにアクセスするもう一つの方法は、open() を使って /dev/port (キャラクタデ バイス、メジャー番号 1、マイナー番号 4)をオープンして、read/write をお こなうという方法です。 (stdio の f*() 関数(訳注: fwrite() とか fread() とか...)は内 部でバッファリングをしているので、使えませんね。) オープンした後に、入出力したいポートのアドレスまで lseek() します。 (file position 0 が ポート 0x00 に、file position 1 が ポート 0x01 ... といった具合に相当します。) その後に read(), write() を使ってバイト/ワードを読み書きします。 この方法を使うには、もちろん そのプログラムが /dev/port に対する read/write 許可を持っていなければなりません。 この方法は上に述べた通常の方法よりもおそらく遅いでしょうが、コンパイル時の 最適化や ioperm() が不要であるという利点があり ます。 /dev/port に対してルート以外のユーザやグループの アクセス許可を与えれば root 特権も必要ありません。 でもこれはシステムセキュリティという意味ではとても良くないことです。 というのは、/dev/port を使ってハードディスク、 ネットワークカード、その他に直接アクセスすることで、システムに障害を与えたり、 さらには root 特権も与えてしまう可能性があるからです。 /dev/port からの読み込みにおいて select(2)poll(2) を使うことはできません。 なぜなら入力ポートの値が変化したときに、ハードウェアにはそれをCPUに 伝える機能がないからです。 割り込み (IRQ) と DMA アクセス ユーザモードのプログラムから直接に割り込みや DMA を使うことはできません。 これには、カーネルドライバを作成する必要があります。その詳細については 「Linux カーネルハッカーズ ガイド」を、またその例としては、カーネルの ソースコードを読んで下さい。 ユーザモードのプログラムから割り込みを禁止することは、危険を伴いますが、 可能です。(カーネルドライバですらそれを実行するときはできる限り短い時間 でするものです。) iopl(3) を呼び出したあと、 asm("cli"); を呼び出すだけで割り込みを禁止することができます。 再び割り込み可能にするには asm("sti") を 呼びます。 高い精度のタイミング制御 ディレイ まず最初に断っておきますが、ユーザプログラムにおいて正確なタイミング保証 をすることはできません。これは、Linux がマルチタスク・プリエンプティブ なシステムだからです。あなたのプロセスが、約 10 ミリ秒から数秒(非常に負荷の 高いシステムなどで)に渡ってなんらかの理由でスケジューリング対象から外され ることは、いつでもあり得ます。 しかし、I/O ポートを使うほとんどのアプリケーションでは、これは実際には問題 にはならないでしょう。 この時間をできるだけ小さくするためには、nice (マニュアルページ nice(2)を 参照のこと) を使ってプロセスの優先順位を高くしたり、またはリアルタイムス ケジュールのシステム(下記を参照のこと)を使うということもできます。 (訳注:nice を有効に使うことで、プロセスの持ち時間の再計算を頻繁に行い、 それによりある程度、優先的にスケジュールされるという効果があります。) 通常のユーザモードプロセスで扱える範囲よりもっと精細なタイミング制御を 求めるのなら、ユーザモードで「リアルタイム」をサポートするための対策も あります。 Linux 2.x カーネルはソフトリアルタイムをサポートしています。 詳細は sched_setscheduler(2) の マニュアルページを参照してください。 ハードリアルタイムをサポートする特別なカーネルもあります。 これについてのもっと詳しい情報は http://luz.cs.nmt.edu/ をご覧ください。 (訳注:上記以外にもハードリアルタイムをサポートする実装が存在します。 詳細は http://www.linux.or.jp/link/kernel.html#RTを参照してください。) スリープ:<function>sleep()</function> と <function>usleep()</function> さて、もっと簡単なタイミングについてお話しましょう。数秒のディレイが 欲しい場合には、おそらく sleep() がいい でしょう。 数十ミリ秒以上のディレイ(最小のディレイは約 10 ミリ秒のようです。)の 場合には usleep() がお薦めです。 これらの関数を呼び出すと、CPU は他のプロセスに割り当てられます(つまり プロセスが「眠る」)ので、CPU タイムが無駄になることはありません。 詳細については sleep(3)usleep(3) のマニュアルページをご覧くだ さい。 約 50 ミリ秒よりも小さいディレイに関しては(おそらくプロセッサやマシンの 速度、システムの負荷にも依存しますが)、上に述べたような CPU を放してしまう というアプローチではうまくいきません。通常、Linux のスケジューラがあなたの プロセスに対して制御を戻す前に(x86 アーキテクチャでは)少なくとも 10-30 ミ リ秒はかかるからです。 そのようなわけで、usleep(3) に小さいディレイ を指定すると通常は指定した値をちょっとだけ越えるディレイが発生します。 また最小の値は約 10 ミリ秒ということになります。 <function>nanosleep()</function> 2.0.x シリーズの Linux カーネルには nanosleep() という新たなシステムコールが付け加わりました。(詳細は nanosleep(2) のマニュアルページを参照してください。) このシステムコールを使えば(数マイクロ秒といった)短い時間のスリープまたは ディレイが可能となります。 プロセスが (sched_setscheduler()を 用いて)ソフトリアルタイムスケジューリングされている場合、2 ミリ秒以下の ディレイに対しては、nanosleep() はビジール ープを使います。 それ以上のディレイに対しては usleep() と同様 スリープします。 (訳注:ソフトリアルタイムでなければ、usleep() と同程度の時間分解能になるようです。) このビジーループは udelay() (多くのカーネル ドライバが使うカーネルの内部ファンクション)を使っていて、ループの長さは BogoMips 値を使って計算されます。 (この手のビジーループの速さなどは BogoMips 値が正確に反映されるものの例です。) どのように動作するかについて詳細は /usr/include/asm/delay.h を参照してください。 ポートI/Oを使ったディレイ ポート I/O を使えば数マイクロ秒のディレイがまた違った方法で作れます。 ポート 0x80 に何かバイトデータを出力または入力すると、プロセッサの種類や 速度に関係なく、ほぼ正確に 1 マイクロ秒待つことができます。 (入出力の方法については、前の方を読んで下さい。) これを必要な回数繰り返すことで数マイクロ秒待つこともできます。 標準的なマシンでは、このポート出力によってなにか変な副作用があったりし ないはずです。 (カーネルドライバでこれを使っているものもありますから。) この方法は {in|out}[bw]_p() でもディレイするために通常に使われている方法です。 (asm/io.h を見て下さい。) ポートアドレス 0-0x3ff の範囲にある大抵のポートへの I/O 命令は実際のところ ほぼ正確に 1 マイクロ秒かかります。 だから例えばパラレルポートを直接扱っている場合なら、ディレイを作るためには そのポートに対して inb() をいくつか追加するだけ でいいわけです。 アセンブラ命令によるディレイ そのプログラムが走るマシンのプロセッサの種類とクロック速度がわかっている 場合には、特定のアセンブラ命令をハードコードすることで、もっと短いディレイ を実現することもできます。 (しかし、プロセスがスケジューリングから外され、ディレイが長くなってしまう ことがあり得ることを忘れないで下さい。) 以下の表では、それぞれの命令で何クロック(内部クロック)消費されるかを示して います。 これによってどれくらいの時間を消費するかを知ることができます。 例えば 50MHz のプロセッサ (486DX-50 とか、486DX2-50) の場合には 1 クロックは 1/50000000 秒(= 200 ナノ秒)です。 Instruction i386 clock cycles i486 clock cycles xchg %bx,%bx 3 3 nop 3 1 or %ax,%ax 2 1 mov %ax,%ax 2 1 add %ax,0 2 1 Pentium のクロック数は i486 と同じになるはずです。 ただし Pentium Pro/II では違います。 また add %ax, 0 は半クロックしか消費 しません。 この命令は時々他の命令とペアで実行されるからです。 (out-of-order 実行のため、相方は命令実行ストリームのすぐお隣の命令である 必要はないのです。) 上の表で、nopxchg は何も副作用はないはずです。 その他の命令はフラグレジスタを変更する可能性があります。 でも、gcc はそれを検出してくれますから、問題にはならないはずです。 xchg %bx, %bx がディレイ用の 命令としては無難な選択と言えるでしょう。 これらを使うには、プログラムの中で asm("命令") という書式を使って、上の表の命令を埋め込みます。 ひとつの asm() 文で複数の命令を埋め込む にはセミコロンで各命令をつなげます。 例えば asm("nop ; nop ; nop ; nop")nop 命令を4回実行して、i486 または Pentium プロセッサでは 4 クロックのディレイ(i386 では 12 クロック)になります。 asm() 文は、gcc によって、インラインアセ ンブラコードに変換されるので関数呼び出しのオーバーヘッドはありません。 Intel x86 アーキテクチャでは 1 クロックより短いディレイを作ることは 不可能です。 Pentiumの<literal>rdtsc</literal>について Pentium プロセッサでは、リブートから現在までの経過クロックサイクル数 を知ることができます。 以下のコードです(このコードは RDTSC という CPU 命令を実行しています。): extern __inline__ unsigned long long int rdtsc() { unsigned long long int x; __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x)); return x; } この値がディレイとして必要な数だけ変化するのをビジーループ中で 監視すればよいでしょう。 時間の測定 1 秒くらいの精度の時間ならば、time() を使うのが一番簡単でしょう。 もっと正確な時間が必要な場合には、gettimeofday() を使うと大体、マイクロ秒の精度があります。 (でも、前の方で述べた、スケジューリングのことは忘れないでください。) Pentium の場合には、上のコードを使うと 1 クロックサイクルの精度が出ます。 プロセスが、ある時間経過した後にシグナルを受け取るようにしたい場合には、 setitimer()alarm() を使います。 この関数の詳細についてはマニュアルページをご覧ください。 その他のプログラミング言語について 以上の説明では C 言語に特化して述べましたが、C++ や Objective C でも 同じようにできるはずです。 アセンブラの場合、C の場合と同様に ioperm()iopl() を呼び出さなければなりませんが、 その後には直接 I/O ポートに対するリード/ライト命令(訳注: in, out 命令) を使うことができます。 その他の言語で、もしインラインアセンブラや C のコードを埋め込むことが できない、または前に述べたシステムコールが使えない場合にもっとも簡単な 方法は、必要な I/O ポートアクセスのためのソースコードを C で記述してコ ンパイルし、プログラムの他の部分とリンクしてしまうという方法でしょう。 あるいは前で触れた /dev/port を使うとよいで しょう。 よく使われるポート 以下は、よく使われる TTL (または CMOS)の汎用論理 I/O ポートについての プログラミング情報です。 もし以下に述べるポートやその他の標準的なポートで本来の使い方(例えば 普通のプリンタやモデムを制御するといったような)をしたいのなら、この HOWTO で述べるようにポートを直接操作するよりも、既存のドライバ(大抵は カーネルにくっついてきます。)を使った方がいいでしょう。 このセクションは PC の標準ポートにLCDディスプレイ(訳注:と言っても 近頃はやりの 14 インチとかのディスプレイのことじゃないですよ。)とか ステッピングモーターとか特製の電子機器をつなぎたいという人を意図して います。 もしスキャナとかいったお店に並べて売ってあるような機器(しかもお店に 出回り始めてからをちょっと経ったようなもの)を制御したいのなら、既に ある Linux ドライバを探してみましょう。 Hardware-HOWTO (訳注:日本語訳は Hardware-HOWTO(日本語訳)です。) などが最初の手がかりとしてはいいと思います。 コンピュータに接続する機器(とか、他にも一般の電子機器)についての より詳しい情報は http://www.hut.fi/Misc/Electronics/がお薦めです。 パラレルポート パラレルポートのベースアドレス(以下では ``BASE'' と呼ぶことにします。)は /dev/lp0 では 0x3bc、 /dev/lp1 では 0x378、 /dev/lp2 では 0x278 になります。 (訳注:Linux カーネル2.2.x以降の parport ドライバをサポートしたシステムでは lp デバイスファイルとベースアドレスが必ずしも対応しているわけではありません。 詳しくはカーネル付属文書 parport.txt を参照してください。) 通常のプリンタポートとして動作させたいだけでしたら、 Printing-HOWTO(訳注:日本語訳は Printing-HOWTO(日本語訳)です。)を 読むのがいいでしょう。 ほとんどのパラレルポートには以下に述べる標準的な出力専用モードに加えて、 「拡張」双方向モードがあります。これに関すること、また最近の ECP/EPP モード(さらには汎用の IEEE1284 規格)については、 http://www.fapo.com/ http://www.senet.com.au/~cpeacock/parallel.htmlを参照してください。 ユーザモードのプログラムでは IRQ や DMA が使えませんので、ECP/EPP を使う ためにはおそらくカーネルドライバを書かなければならないでしょう。 誰かがそういったドライバを書いているだろうとは思いますが、私は詳しいこと は知りません。 BASE+0のポート(データポート) は、ポートの データ信号を制御します。 (D0 〜 D7 はビット 0 〜 7 に対応します。状態 : 0 = low (0V)、1 = high (5V))。 このポートに対するライトはデータをラッチして外部のピンに出力します。 標準または拡張ライトモードの場合には、リードすると、最後にライトされた データが読み出されます。 拡張リードモードの場合には、外部デバイスからのデータが読み出されます。 BASE+1のポート(ステータスポート) は リードオンリーで、以下のような入力信号のステータスを返します: Bit 0 と 1 は予約済 Bit 2 IRQ ステータス (外部ピンの値ではない。どういうふうに動作するか知らない) Bit 3 ERROR (1=high) Bit 4 SLCT (1=high) Bit 5 PE (1=high) Bit 6 ACK (1=high) Bit 7 -BUSY (0=high) BASE+2のポート(制御ポート)はライトオンリー (リードした場合、最後にライトしたデータを返す。)で、 以下のステータス信号を制御します: Bit 0 -STROBE (0=high) Bit 1 -AUTO_FD_XT (0=high) Bit 2 INIT (1=high) Bit 3 -SLCT_IN (0=high) Bit 4 '1' にすると、パラレルポートの割り込みを許可する(ACK の low -> high の遷移で割り込み発生。) Bit 5 拡張モードでの方向(0 = 出力, 1 = 入力) を制御する。このビットは 完全にライトオンリーで、リード時には不定の値が返る。 Bit 6 と 7 は予約済 ピン接続 (25 ピンの D-sub メスコネクタ) (i=input, o=output): 1io -STROBE, 2io D0, 3io D1, 4io D2, 5io D3, 6io D4, 7io D5, 8io D6, 9io D7, 10i ACK, 11i -BUSY, 12i PE, 13i SLCT, 14o -AUTO_FD_XT, 15i ERROR, 16o INIT, 17o -SLCT_IN, 18-25 Ground IBM の規格によれば、1, 14, 16, 17 番ピン(制御出力)は、4.7kオームで 5V に プルアップされたオープンコレクタドライバ (シンク(吸い込み) 20mA, ソース (吐き出し) 0.55mA, ただし、ハイレベルの出力は プルアップの 5V) その他のピンについては、シンク(吸い込み) 24mA, ソース(吐き出し) 15mA, ハイレベル出力は最小 2.4V, ローレベルの出力は最大 0.5V。 IBM 以外のパラレルポートではおそらくこの標準とは少々違う場合もあるでしょう。 これに関するもっと詳しい情報は http://www.hut.fi/Misc/Electronics/circuits/lptpower.html をご覧ください。 最後に注意: グランド(接地)には気をつけましょう。私はコンピュータが稼働中に コネクタを接続したせいで、パラレルポートを何個も壊してしまった経験があります。 これを考えると、マザーボードに乗っているのではない(拡張カード上の)パラレル ポートを使うのがいいかもしれませんね。 (標準的な安い「マルチI/O」カードを使えばあなたのコンピュータに二つめの パラレルポートが作れます。 必要のない(パラレルポート以外の:訳注)ポートは disable してしまえばいいです。 拡張カードのパラレルポートのI/Oアドレスを空いているところに設定しましょう。 IRQ は使う予定がなければ気にする必要はありません。) (訳注: 各機器のフレームグランドは、ちゃんととりましょうね。) ゲーム(ジョイスティック)ポート ゲームポートはポートアドレス 0x200-0x207 です。 普通のジョイスティックを制御するなら、多分、Linux カーネルにくっついてくる ドライバを使った方がいいですよ。 ピン接続 (15 ピンの D-sub メスコネクタ): 1,8,9,15: +5 V (power) 4,5,12: Ground 2,7,10,14: デジタル入力 (順番に) BA1, BA2, BB1, BB2 3,6,11,13: アナログ入力 (順番に) AX, AY, BX, BY +5 V ピンはマザーボードの電源に直接接続されているようですので、 (マザーボードの作りや、電源、ゲームポートにもよりますが)非常に 大きな電力を供給することができるはずです。 このポートには二つのジョイスティックを接続できますが、デジタル入力は、 これらのボタンからの入力です。 (ジョイスティック A とジョイスティック B それぞれに二つずつボタンがあ ります。) これらは通常の TTL レベルの入力で、以下で述べるステータスポート(以下 に記述)から現在の状態を読み出すことができます。 実際のジョイスティックではボタンが押されていると low (0V) を、そうで なければ high (1K オームの抵抗を通じて 5V 電源にプルアップ) が読み出 されます。 いわゆるアナログ入力というのは、実際には抵抗値を計っているのです。 以下でその仕組みを述べます。 ゲームポートには4本の入力それぞれにワンショットマルチバイブレータ が接続されています。(全体(4本)で 558 チップ一つ) それぞれの入力について、入力ピンとマルチバイブレータの出力の間に 2.2K オームの抵抗が、マルチバイブレータの出力とグランドの間に 0.01uF のタイミング用コンデンサが接続されています。 実際のジョイスティックには、X と Yの両方の軸にそれぞれポテンショメー タがあり、+5V とそれぞれの入力ピンに接続されています。 (AX, AY がジョイスティック A に、BX, BY がジョイスティック B に対応し ます。) (訳注: 回路図はこんな感じかな: +5V | ^ | / 2.2Kオーム +---/\/\/\--IN----/\/\/\------+ / | ポテンショメータ | +---------+ | | Multi | | | vib. Q|--+ | | | | | | | | | | | === 0.01uF | | | +---------+ | | GND ) マルチバイブレータが作動すると、その出力を high (5V) にドライブし、 それぞれに接続されているタイミングコンデンサの電位が 3.3V になるまで 待ち、その後に出力を low にします。 この結果、マルチバイブレータの出力が high の時間はジョイスティック中 のポテンショメータの抵抗値に比例します。 (つまり、それぞれの軸のジョイスティックの位置を表します。) 抵抗値は以下の式のとおりです: R = (t - 24.2) / 0.011, ここで、R は ポテンショメータの抵抗値(オーム)、t は high の時間(マイ クロ秒)です。 とまあ、こういうわけですので、アナログ入力を読み出すためには、以下で 述べるようにまずポートにライトして、マルチバイブレータを作動させる必要が あります。 その後に4つの軸の状態をポーリング(ポートを繰り返し読み出す)し、それぞれ のポートが high から low になるまでの時間を測定します。 このポーリングには非常に CPU タイムを消費しますし、(通常のユーザモードの) Linux のような非リアルタイム・マルチタスクシステムではその結果はあまり正 確ではありません。 ポートを絶え間なく読み出すことはできないからです。 (カーネルレベルのドライバを使うか、ポーリングの間、割り込みを禁止すれば いいのですが、これは更に多くの CPU タイムを食います。) 信号が low になるまで長い時間(数十ミリ秒)かかるのが分かっているなら、ポー リング前に CPU タイムを他のプロセスに明け渡すよう usleep() を呼ぶこともできます。 さて、アクセスする必要のあるポートはただ一つ、0x201 (他のポートアドレスはこの ポートと同じようにふるまうか、なにもしないか、のどちらか)です。 このポートに対するライト(書き込むデータはなんでも良いです。)によって マルチバイブレータが作動します。リードすると入力信号の状態が返ります: Bit 0: AX (マルチバイブレータ出力の状態 (1=high)) Bit 1: AY (マルチバイブレータ出力の状態 (1=high)) Bit 2: BX (マルチバイブレータ出力の状態 (1=high)) Bit 3: BY (マルチバイブレータ出力の状態 (1=high)) Bit 4: BA1 (デジタル 入力, 1=high) Bit 5: BA2 (デジタル 入力, 1=high) Bit 6: BB1 (デジタル 入力, 1=high) Bit 7: BB2 (デジタル 入力, 1=high) シリアルポート コンピュータと接続させたい機器が RS-232 みたいな仕組みを持っているなら、 その機器との接続にシリアルポートを使うといいでしょう。 Linux のシリアルドライバはほとんどありとあらゆる応用が効きます。 (シリアルポートを直接操作するなんてしない方がいいです。そんなことするなら 多分カーネルドライバを書かなければならないハメになりますよ。) このドライバとっても汎用性が高いので標準でない bps レートを使うといった こととかも問題ではありません。 Unix システムでシリアルポートを使うプログラムのより詳しい情報については termios(3) マニュアルページやシリアルドライバの ソースコード(linux/drivers/char/serial.c) 、また http://www.easysw.com/~mike/serial/を参考にしてください。 Hints まともなアナログ I/O が必要な場合には、ADC (アナログ -> デジタル コンバータ)や DAC (デジタル -> アナログコンバータ)チップをパラレ ルポートに接続するという方法もあります。 (ヒント: 電源には気をつけて下さい。ゲームポートからの電源を使うか、 ディスクドライブ用の余っている電源コネクタをコンピュータケースから 引っ張り出すなどがいいでしょう。パラレルポートから電源をとるならば、 低消費電力のデバイスを使う必要があります。) (訳注: いずれにせよ、これらはデジタル系の電源ですから、アナログ系に 使う場合にはなんらかの方法でデカップリングをお忘れなく。) または、AD/DA カードを購入するという手もあります。 (昔のトロいものなら大抵 I/O ポートで制御できます。) もし、1〜2 チャネルで十分であり、あまり正確でなくてもよく、(おそらく) 不正確な中点補正(zeroing)でもよければ、Linux のサウンドドライバでサ ポートされている安いサウンドカードを買ってきて使う、という方法もあります。 (それに、結構高速ですよね。) 精密なアナログ機器では不適当なグランド設定によりアナログ入出力の誤差が 生ずることになります。 何か変だなぁ、と思ったら、その機器をフォトカップラを用いてコンピュータから 電気的に(コンピュータと機器の間のすべての信号線を) アイソレートしてみるといいでしょう。 より良いアイソレーションを得るためにフォトカップラの電源をコンピュータから とるようにしてみましょう。 (ポートの予備の信号線から十分なくらいの電源がとれます。) Linux で使えるプリント基板設計のソフトウェアをお探しでしたら、 Pcb という名前のフリーな X11 アプリケーションがあります。 すごく難しいことをしようというのでない限り、これで十分役にたちます。 多くの Linux ディストリビューションに収録されていますが、 ftp://sunsite.unc.edu/pub/Linux/apps/circuits/ (ファイル名 pcb-* ) からも手にいれ ることができます。 トラブルシュート Q1. ポートをアクセスした時に segmentation faults が起きてしまいます。 A1. プログラムが root 特権を持っていないか、なんらかの理由で ioperm() を呼び出した時に失敗しているので しょう。 ioperm() の返り値を調べてみてください。 それと本当に ioperm() で許可を得たポートに アクセスしているかどうかも調べてみてください。 (Q3も参考にしてください。) ディレイ付きマクロ (inb_p(), outb_p() などなど)を使っているなら、 ioperm() でポート 0x80 のアクセス許可も忘れ ずに取得してください。 Q2. in*() とか out*() とかの関数がどこにもみつかりません。gcc が undefined references とか文句 を言ってきます。 A2. コンパイルの時に最適化をオン () にしなかったのでは? その結果、gcc は asm/io.h の中にあるマクロ を解決することができなかったのです。 それとも、#include <asm/io.h> を忘れてませんか? Q3. out*() を実行してもなにもおこらない、 またはなんか変なのですが。 A3. パラメータの順番をチェックして下さい。outb(value, port) という順番です。 MS-DOS でおなじみの outportb(port,value) とは逆です。 Q4. 標準の RS-232 機器、パラレルプリンター、ジョイスティック といったものを制御したいのですが... A4. たぶん既にあるドライバ( Linux のカーネルとか X サーバとかそういったものに くっついてくるやつのことです。)を使った方がいいです。 そういうドライバはすごく汎用性が高くて、ちょっと規格外の機器なんかも大 抵動きます。 そこらへんの文書へのポインターとして、上で書いた標準ポートの情報を参考 にしてください。 プログラムの例 これは、I/O ポートアクセスの単純なコーディング例です: /* * example.c: とっても簡単なポートI/Oの例 * * なにも役にたつことはしていません。ポートに書き込み、一時停止して、 * ポートから読み出すだけです。「gcc -O2 -o example example.c」で * コンパイルして、ルートユーザになって「./example」で実行してください。 */ #include <stdio.h> #include <unistd.h> #include <asm/io.h> #define BASEPORT 0x378 /* lp1 */ int main() { /* ポートへのアクセス許可を得る */ if (ioperm(BASEPORT, 3, 1)) {perror("ioperm"); exit(1);} /* ポートのデータ (D0-D7)を全て low (0) にする */ outb(0, BASEPORT); /* しばらくの間 (100 ms) スリープ */ usleep(100000); /* ステータスポート (BASE+1) から読み出して、表示する */ printf("status: %d\n", inb(BASEPORT + 1)); /* もうポートを使わないので後始末 */ if (ioperm(BASEPORT, 3, 0)) {perror("ioperm"); exit(1);} exit(0); } /* example.c おわり */ 謝辞 とてもたくさんの方から助け (contribute) を受けましたので、ここには記しきれ ません。皆さん、とてもありがとう。 すべての方にリプライすることはできませんでしたがそういう方々には ごめんなさい、そしてもう一度、助けてくれてありがとう。 日本語訳について 訳者 : 川島 浩 <kei@sm.sony.co.jp> 訳の吟味/校正 : 山崎 康弘さん <hiro@koneeko.linux.or.jp> (1996/12/26) 更新:幸田あきひろ <coda@post.kek.jp> (2001/03/24)