Operating Systems Development Series
8237A ISA DMAC
by Mike, 2008, 2009

イントロダクション

ようこそ!:)

この章では、DMAC(Direct Memory Access Controller)を詳しく見ていきます。DMAC は、ソフトウェアに頼らず、デバイスから直接メモリにデータブロッ クを転送する方法を提供します。 ソフトウェアではなくハードウェアが行うので、非常に高速 なデータ転送が可能になります。

今日のリストはこちらです。

  • DMAの歴史
  • DMAハードウェア
  • DMAポート
  • DMA レジスタ
  • DMAコマンド

概要

ダイレクトメモリアクセス(DMA)は、最近のすべてのコンピュータに搭載されている機能で、デバイスがプロセッサとの相互作用なしに大きなデータブロックを移動できるようにするものです。これは、フロッピーのプログラミングの章ですでにお分かりのように、便利な機能です。デバイスがデータブロックを転送している間、プロセッサはデータがメモリや他のデバイスに転送されるのを気にすることなく、自由にソフトウェアを実行し続けることができます。 基本的な考え方は、DMAデバイスが自分自身でタスクを実行するようにスケジュールすることができるということです。クールでしょう?

バスやアーキテクチャの設計によって、ダイレクトメモリアクセスの方法は様々です。今回はISAダイレクトメモリアクセスコントローラ(DMAC)に焦点を当てますが、念のため他の方式も取り上げておくことにしました。

ISA

業界標準アーキテクチャ(ISA)は、Intel 8237マイクロコントローラをベースにしたコントローラを通じて、DMAリクエストのための中心的な場所を提供します。ATXマザーボードの設計では、コントローラは1つだけでした。しかし、1つのコントローラで8台のデバイスをサポートするには限界があるため、AT以降のアーキテクチャでは、コントローラが2つ存在します。プログラマブル割り込みコントローラ(PIC)がスレーブ化されているのと同じように、2つのコントローラはスレーブ化されているのです。両コントローラは常に4MHzで動作します。

性能とデバイス数の制限から、新しいデバイスはPIOやUDMAを使う傾向があります。 しかし、DMAはレガシーデバイスのためにISAでまだサポートされています。

これらのデバイスはすべて、コントローラ上のチャネルに接続されています。これらのチャネルとともに、各チャネルはDACK(DMA Acknowledge)ラインとDRQ(DMA Request)ラインを持っています。

以下は、両者のDMAC(Direct Memory Access Controller)の標準的な割り当てである。

  • XT
    • チャネル0:システムで使用、使用不可(DRAM Refresh、廃止予定)
    • チャネル1:利用可能、標準的なDMAの割り当てなし
    • チャネル2:フロッピーディスクコントローラ
    • チャンネル3:ハードディスクコントローラ(代わりにPIOまたはUDMAを推奨)
  • ATのみ
    • チャンネル4:XTコントローラにカスケード接続 - スレーブDMAコントローラをマスターに入力
    • チャンネル5:使用可能、標準的なDMAの割り当てなし(16ビット)
    • チャンネル6使用可能、標準DMAの割り当てなし(16ビット)
    • チャンネル7:使用可能(標準DMAは未割り当て)(16ビット

転送を開始するために、ソフトウェアは、物理メモリ内の転送を完了する場所と転送サイズをチャネルアドレスとカウントレジスタに設定します。その後、そのメモリブロックから読み出すか書き込むかを設定し、コントローラを転送完了の方向に設定します。転送が完了すると、転送を開始したデバイスが割り込み要求(IRQ)を発行し、システムソフトウェアがそれをキャッチしてさらに処理します。これは重要なことです!DMA を使用して転送を開始する場合、これらのステップを実行する必要があります。

PCI

PCIデバイスは、同じDMAコントローラを共有することも、中央のDMAコントローラを持つこともありません。その後、物理メモリの読み書き要求がノースブリッジに渡され、ノースブリッジは要求をメモリ操作に変換し、メモリコントローラに操作を送出します。

PCI転送は4GBの物理メモリに制限されています。しかし、デバイスとPCIブリッジがダブルアドレスサイクル(DAC)または同様の技術を実装している場合、PCIコントローラは4GB物理メモリを超える読み取りと書き込みの要求を開始することができます。

ISA DMAハードウェア

ダイレクトメモリアクセスコントローラ(DMAC)

ISA(業界標準アーキテクチャ)は、オリジナルのIntel 8237 DMAチップをベースにしたコントローラを使用しています。ほとんどの新しいDMACは、より多くの機能を提供しますが、8237マイクロコントローラとほぼ完全に下位互換性があります。新しいPCにはより高度なDMACが搭載されていますが、すべての始まりとなったデバイスを見ることは常に素晴らしいことです。そこで、DIP (Dual Inline Package)で配布されたオリジナルの8237Aコントローラのピン配置図を示します。

この章でプログラミングするコントローラはこれです。ピンの数は非常に多いですが、それほど複雑ではありません。それでは、重要なピンを中心に見ていきましょう。
  • Pin 1 (IOR) I/O Read
  • Pin 2 (IOW) I/O Write
  • Pin 3 (MEMR) Memory Read
  • Pin 4 (MEMW) Memory Write
  • Pin 5
  • Pin 6 (READY)
  • Pin 7 (HACK) Hold Acknowledge
  • Pin 8 (ADSTB) Address Strobe
  • Pin 9 (AEN) Address Enable
  • Pin 10 (HREQ) Hold Request
  • Pin 11 (CS) Chip Select
  • Pin 12 (CLK) Clock
  • Pin 13 (RESET) Reset
  • Pins 14-15 (DACK) DMA Ackowledge
  • Pins 16-19 (DREQ0-DREQ3) DMA Request
  • Pin 20 (GND/Vss) Ground
  • Pins 21-23 (DB0-DB3) Data Bus
  • Pins 24-25 (DACK) DMA Ackowledge
  • Pins 26-30 (DB4-DB7) Data Bus
  • Pin 31 (Vcc) +5 volt power
  • Pins 32-35 (A0-A3) Address Lines
  • Pin 36 (EOP) End Of Process
  • Pins 37-40 (A4-A7) Address Lines

こちらも悪くないですね。20番ピンをグランドに、31番ピンを電源に使っています。ピン12(CLK)は、すべてのコントローラで見られるもう一つの一般的なものです。プロセッサのCLKピンと接続し、入力クロック信号として、コントローラ内の動作タイミングを制御する。CS (Chip Select)ピンは、ほとんどのコントローラで共通に使用されているピンです。データ・バス上のI/Oデバイスとしてコントローラを選択するために使用されます。RESETは、コントローラの内部レジスタ(ステータス、リクエスト、テンポラリー、コマンド)をリセットし、内部フリップフロップをクリアし、マスクレジスタを設定します。ここまでは、特に目新しいことはありませんね。ゲルネリック・アドレスライン(A0-A7)は、システム・アドレス・バスに接続されています。入力時は、CPUはA0-A3にのみデータを書き込んで、読み出すレジスタを選択することができます。すべてのピンは出力(物理メモリアドレスへの)にも使用されますが、DMAリクエストのときのみアクティブになります。最後に、システムのデータバスに接続する一般的なD0-D7ピンがあります。

次に、より興味深いピンを紹介します。DMACがシステムアドレスとデータバスに接続されていることは、多くの読者が知っていることでしょう。しかし、DMACはCPUから直接注意を受ける必要があることはご想像いただけると思います。このため、DMACがCPUと通信できるように、またその逆もできるように、CPUに接続するいくつかのラインがあります。HACK(Hold Acknowledge)は、CPUがDMACにシステムバスの完全な制御を与えたとき、Highになります。これにより、DMACはいつメモリコントローラにデータを送信しても安全であることを知ることができる。結局のところ、DMACとプロセッサの両方が同時に同じシステムバスを使おうとすることはできないわけですね。

DMACが物理メモリに直接データを転送するのは、システムバスがプロセッサによって使用されていないときだけである。DMACは、メモリ変換や物理メモリへの読み書きのために、メモリコントローラにデータを伝送するシステムバスを必要とすることになる。

なるほど、CPUはDMACにシステムバスを引き継げることを伝える手段を持っているわけだ。でも、そもそもDMACはどうやってCPUにシステムバスが必要だと伝えるのだろう?それが、HREQ(Hold Request)ラインだ。DMAに接続されたデバイス(フロッピーディスクコントローラなど)からDMAリクエスト(DRQ)が発生し、その「チャンネル」が現在無効になっていない場合、コントローラは次のクロックサイクルでHREQをHighにして、リクエストを完了するためにシステムバスの制御が必要であることをCPUに通知するのである。

ラインDR0-DR3(DMA Request Lines)は、デバイスがDMAにリクエストを通知するために使用される。たとえば、フロッピーディスク・ドライブ・コントローラ(FDC)は、通常DR2(「チャンネル2」)を使用するように接続されている。そこで、そのチャンネルを有効にして、FDCがDMACを使うようにプログラムすると、FDCにリードまたはライト・コマンドが送られたとき、FDCはRQ2ラインをアクティブにして、DMACに注意を要することを通知する。ここから先は、そのチャンネルにプログラムしたモードに応じて、DMACを通じて読み書きが行われます。

ここでもうひとつ重要なポイントがあります。DRQラインは4本しかないのです。1つのDMACに接続できるデバイスは4つだけです。これはかなり制限されていますね。i86アーキテクチャでは、2つのDMACをくっつけることで、この問題はいくらか解決されました。そのあたりは、また後日。

今のところ、すべてうまくいっています。ソフトウエアがCPUにDMACをプログラムするように指示するのはわかったとして、CPUはどうやってDMACにレジスタの読み書きが必要であることを伝えるのでしょうか?それがIOR (I/O Read)IOW (I/O Write)ピンです。同様に、DMACは、MEMR(Memory Read)またはMEMW(Memory Write)を出力して、メモリリードまたはライト制御線を活性化することにより、メモリコントローラに読み書きを指示する。EOP(End Of Process)は、リクエストが完了したときにデバイスに信号を送るために使用されます。リクエストは、そのチャンネルのターミナルカウント(TC)に到達すると完了します。これはプログラマブルなカウンタ値です。AEN(Address Enable)は、コントローラの内部8ビット・アドレス・ラッチ・レジスタをシステム・アドレス・バスにロードすることを通知するために使用されます。ADSTB(Address Strobe)は、上位アドレスバイトを外部ラッチレジスタにストローブするために使用されます。

いろいろとあるんですね。コントローラが行う操作の詳細は、使用するモードと転送タイプに依存します。しかし、基本的な手順は同じです。デバイスがDMACに通知し、DMACがCPUにシステムバスを介して制御を通知する。DMACは制御を待ちます。DMACは制御を待つ。制御を受けると、チャネルアドレスレジスタを内部ラッチレジスタにロードする。そこから、MEMR、MEMWを設定し、必要に応じてメモリの読み書きを行う。ちょっと、何?メモリから読み出すのはわかるけど、書き込むとなるとどこに行くんだ?

もう一度、FDCの章を振り返ってみてください。DMACやCPU、メモリコントローラなどが見ているのと同じデータバスに接続するD0〜D7ピンを持っていることに気がつきましたか?つまり、メモリから書き込むときは、MEMRラインをアクティブにして、アドレスをアドレスバスにアップロードし、メモリコントローラがデータを変換してデータバスに置くだけでよいのです。FDCは書き込み要求を待っているので、読み込んだデータを取り込み、FDCに送られた書き込みコマンドで設定されたディスクに書き込む。ディスクから読み出す場合も基本的には同じですが、DMACが代わりにMEMWラインをアクティブにします。メモリコントローラは、FDCから送られてきたデータバスから書き込むデータを取得し、すべてがうまくいくと、DMACはプロセッサのHREQラインを解放し、CPUが再びバスを完全にコントロールできるようにする。

重要なことは、プロセッサはDMACの終了を待つことができないことです。CPUは、再びシステムバスへのアクセスが必要になると、HACKラインをLowにする。この間、DMACはこのラインが再びHighになるまで待って処理を続けるしかないのです。

そして、読者の皆さん、お待たせしました。i86では、DMACがもう1つ追加され、使用できるチャンネル数が8になっています。まあ、そんなところです。では、見てみましょう。

x86のDMAC

新しいPCには2つのDMACがあることを思い出してください。2つのDMACは、2つのPICが接続されているのと同じような方法で接続されています...ただ、逆になっているだけです。はぁーっ!?そうなんです、そうなんです:)見てみましょう。

DMACは、ISAバスの制御を行う際にプロセッサのHOLDと HLDA(Hold Acknowledge)ピンを使用します。DMACはHOLDでプロセッサに信号を送り、プロセッサはHDLAでこの要求を確認する。また、2番目の(スレーブ)DMACがDRQの0~3を含むのに対し、1番目のDMACはDRQの5~7を持っていることに注意してください。DRQはDMAリクエストです。これらのラインは、DMACを使用するシステム内のさまざまなデバイスに接続されています。デバイスがDMACの注意を引くよう要求するたびに、デバイスはこのラインをHighにしてDMACに信号を送ります。もう一度画像を見てください。何か面白いものが見えるかもしれません。DRQ4はどこにあるのでしょうか?

DRQ4は、どちらのデバイスにも存在しますが、それぞれのDMACを接続するものです。DRQラインはDMACのシグナルとして使用されるため、プライマリとセカンダリのDMACが互いに正しいDRQラインを上げるようシグナルすることができます。つまり、DMACをプログラミングするときは、DRQ4がプライマリとスレーブのコントローラーを接続するために使用されることを忘れてはならないのです。このため、DRQ4は使用できない。上の図を振り返ると、プライマリまたはスレーブのDMACが完了した(TC(Terminal Count)ラインを上げた)場合に真を出力するORゲートも見えます。TCラインは、DMACに送った転送要求が完了したときに上がる。

さて、ここで覚えておくべき重要なことをまとめておきますので参考にしてください。

  • DMAは常に物理メモリで動作し、仮想メモリで動作することはない
  • i86アーキテクチャでDMACを使用するために接続できるデバイスは8個だけである。
  • DRQ4 (Channel 4)はプライマリDMACとセカンダリDMACの接続に使用され、使用できない。
また、これらの構成についても興味深いことが分かるかもしれません。スレーブDMACはマスターDMACに接続する最初のDMACであり、その逆ではありません。スレーブDMACがチャンネル0-3(と、技術的にはプライマリDMACに接続するために使われる4)を担当し、プライマリDMACがチャンネル5-7を担当するのは、このためです。ちょっと変でしょう?このように、2つのPICが一緒に動作する方法とは多少異なります。また、これらのコントローラの接続方法によって、マスターDMACは16ビットDMACのように動作し、スレーブDMACは8ビットDMACのように動作します。 このため。
  • 1番目のDMACはスレーブ(8ビット)、2番目のDMACはマスター(16ビット)です

ISA DMAインタフェース

ポートマッピング

DMAコントローラが2つあるため、ポートも2セットあります。

汎用レジスタ

ISA DMAC Ports
DMAC 0 Port (Slave) DMAC 1 Port (Master) Descripton
0x08 0xD0 Status Register (Read)
0x08 0xD0 Command Register (Write)
0x09 0xD2 Request Register (Write)
0x0A 0xD4 Single Mask Register (Write)
0x0B 0xD6 Mode Register (Write)
0x0C 0xD8 Clear Byte Pointer Flip-Flop (Write)
0x0D 0xDA Intermediate Register (Read)
0x0D 0xDA Master Clear (Write)
0x0E 0xDC Clear Mask Register (Write)
0x0F 0xDE Write Mask Register (Write)

これらのレジスタは次のセクションでより詳細に説明される。これらのレジスタは両方の DMAC と相互作用するとき使用される。それらはポートマップドI/Oを通して読んだり書いたりすることができる。つまり、標準的なi86のinと out命令を使用する。

DMACは逆方向であることを覚えておくことが非常に重要である。DMAC 0はスレーブで、DMAC 1はマスターです。 また、ポートの範囲がどのように異なるか注意してください。スレーブは8ビットで、マスターは16ビットであることを思い出してください。

読みやすくするために、これらの醜い数字を列挙して抽象化してみましょう。

enum DMA0_IO { DMA0_STATUS_REG = 0x08, DMA0_COMMAND_REG = 0x08, DMA0_REQUEST_REG = 0x09, DMA0_CHANMASK_REG = 0x0a, DMA0_MODE_REG = 0x0b, DMA0_CLEARBYTE_FLIPFLOP_REG = 0x0c, DMA0_TEMP_REG = 0x0d, DMA0_MASTER_CLEAR_REG = 0x0d, DMA0_CLEAR_MASK_REG = 0x0e, DMA0_MASK_REG = 0x0f };

これらの値は、上の表と一致していることに注意してください。次にDMAC 2について...

enum DMA1_IO { DMA1_STATUS_REG = 0xd0, DMA1_COMMAND_REG = 0xd0, DMA1_REQUEST_REG = 0xd2, DMA1_CHANMASK_REG = 0xd4, DMA1_MODE_REG = 0xd6, DMA1_CLEARBYTE_FLIPFLOP_REG = 0xd8, DMA1_INTER_REG = 0xda, DMA1_UNMASK_ALL_REG = 0xdc, DMA1_MASK_REG = 0xde };

それでは、レジスタを紹介します。

チャネルレジスタ

i86では、上記のレジスタの他に、各チャンネルのアドレスやカウンタを制御するための以下のレジスタが用意されています。

ISA DMAC Channel Ports
DMAC 0 Port (Slave) DMAC 1 Port (Master) Descripton
0x0 0xC0 Channel 0 Address/Channel 4 Address
0x1 0xC2 Channel 0 Counter/Channel 4 Counter
0x2 0xC4 Channel 1 Address/Channel 5 Address
0x3 0xC6 Channel 1 Counter/Channel 5 Counter
0x4 0xC8 Channel 2 Address/Channel 6 Address
0x5 0xCA Channel 2 Counter/Channel 6 Counter
0x6 0xCC Channel 3 Address/Channel 7 Address
0x7 0xCE Channel 3 Counter/Channel 7 Counter

上の表をもう一度見てください。マスターDMACのChannel 0 Addressは・・・なんと!ポート0です。これはこのシリーズの歴史的瞬間であり、我々は入出力ポート0を見つけたのです。

また、マスターDMACが16ビットで、スレーブDMACが8ビットであることを思い出してください。これは重要な特性で、特にここでは、スレーブDMACには8ビットの値を読み書きできますが、マスターDMACには16ビットの値を読み書きできることを意味します。

とにかく、これらのレジスタの詳細に入る前に、まずレジスタを隠してしまいましょう。以下の列挙を見ると、何も派手なことはしていないことがわかります。これらのレジスタは、すべてポートマップドI/Oでアクセスされることを忘れないでください。つまり、x86マシン命令で 読み書きできるのです。

enum DMA0_CHANNEL_IO { DMA0_CHAN0_ADDR_REG = 0, //! Thats right, i/o port 0 DMA0_CHAN0_COUNT_REG = 1, DMA0_CHAN1_ADDR_REG = 2, DMA0_CHAN1_COUNT_REG = 3, DMA0_CHAN2_ADDR_REG = 4, DMA0_CHAN2_COUNT_REG = 5, DMA0_CHAN3_ADDR_REG = 6, DMA0_CHAN3_COUNT_REG = 7, };

次にDMAC 2について。

enum DMA1_CHANNEL_IO { DMA1_CHAN4_ADDR_REG = 0xc0, DMA1_CHAN4_COUNT_REG = 0xc2, DMA1_CHAN5_ADDR_REG = 0xc4, DMA1_CHAN5_COUNT_REG = 0xc6, DMA1_CHAN6_ADDR_REG = 0xc8, DMA1_CHAN6_COUNT_REG = 0xca, DMA1_CHAN7_ADDR_REG = 0xcc, DMA1_CHAN7_COUNT_REG = 0xce, };

これらのレジスタの基本的な目的は、チャンネルをどのように開始するかをDMACに伝える方法を提供することです。 各チャンネルはベースアドレスとカウンタを持ちます。ベースアドレスは読み書きを開始するメモリ上の位置で、カウンタはそのチャンネルでどれだけ転送するかをDMACに伝える。重要なのは、これらは常に物理アドレスであり、仮想ではないことです。

例を見てみましょう。チャンネルが使用するベースアドレスを設定するために必要なことは、上の表に示された正しいi/oポートに書き込むことです。DMA0_CHAN0_ADDR_REGが 0 で、DMA1_CHAN7_ADDR_REGが表の最後の値 (0xde) であると仮定すると、これは簡単なことです。すべてのサンプルコードは、この章の最後のデモにあることを忘れないでください。

void dma_set_address(uint8_t channel, uint8_t low, uint8_t high) { if ( channel > 8 ) return; unsigned short port = 0; switch ( channel ) { case 0: {port = DMA0_CHAN0_ADDR_REG; break;} case 1: {port = DMA0_CHAN1_ADDR_REG; break;} case 2: {port = DMA0_CHAN2_ADDR_REG; break;} case 3: {port = DMA0_CHAN3_ADDR_REG; break;} case 4: {port = DMA1_CHAN4_ADDR_REG; break;} case 5: {port = DMA1_CHAN5_ADDR_REG; break;} case 6: {port = DMA1_CHAN6_ADDR_REG; break;} case 7: {port = DMA1_CHAN7_ADDR_REG; break;} } outportb(port, low); outportb(port, high); }
非常によく似た方法で、その特定のチャンネルのカウントレジスタを設定するルーチンを書くことができます。
void dma_set_count(uint8_t channel, uint8_t low, uint8_t high) { if ( channel > 8 ) return; unsigned short port = 0; switch ( channel ) { case 0: {port = DMA0_CHAN0_COUNT_REG; break;} case 1: {port = DMA0_CHAN1_COUNT_REG; break;} case 2: {port = DMA0_CHAN2_COUNT_REG; break;} case 3: {port = DMA0_CHAN3_COUNT_REG; break;} case 4: {port = DMA1_CHAN4_COUNT_REG; break;} case 5: {port = DMA1_CHAN5_COUNT_REG; break;} case 6: {port = DMA1_CHAN6_COUNT_REG; break;} case 7: {port = DMA1_CHAN7_COUNT_REG; break;} } outportb(port, low); outportb(port, high); }
これらのレジスタは16ビットであることに注意することが非常に重要です。つまり、DMACは1つのチャンネルから一度に最大64kしか転送できないのです。

また、これらは物理アドレスであることに注意することも非常に重要です!システムソフトウェアがページングを有効にしている場合、チャネルが使用する場所を同じ仮想アドレスにマッピングする必要があります

というわけで、おさらいです。8つのチャネルがあることを知り、そのチャネルのデバイスがDMACを使用できるようにした後、チャネルレジスタの1つに書き込むことによって、チャネル情報(メモリの場所と、そこから読み書きする天候)を与えれば、DMACへの読み込みまたは書き込み転送を開始することができる。このデータはどこから来るのだろうかと思うかもしれません。あるいは、読み出しの場合、データはどこに行くのでしょうか。それは、そのチャンネルを制御しているデバイス次第です。たとえば、フロッピーディスクドライブの場合、フロッピーディスクドライブコントローラ(FDC)にリードコマンドを送ると、FDCはDMACに転送を開始するよう通知する。DMACはベースとなる物理アドレス、チャンネル操作(リードかライトか、この場合はリードを希望)、バッファのサイズを取得し、あとは自分で書き込みを行う。FDCはDMACにデータを転送し続け、DMACはそのチャンネルに格納されているアドレスが指すバッファにデータを入れます。ここで、そのチャンネルのアドレスとカウントレジスタに書き込むことで、バッファの場所とサイズを設定する。

待て、待て、待て。DMACが一度に64Kしか転送できないことを覚えてる?それよりももっと悪い。各チャンネルのベースアドレスも同じ制限を受けるので、DMACは64KのRAMにしかアクセスできないことになります。これは悪い制限だと思いませんか?これを解決するのが、外部ページレジスタです。よく見てみましょう。

拡張ページ・アドレス・レジスタ

ページレジスタは、チャンネルが設定されているメモリロケーションがどのページに存在するかを設定するために使用されます。この8ビットをチャンネルのベースアドレスに追加すると(チャンネルのベースアドレスは0xFFFFF)、実質的に8ビット増えるので、最大16MBのメモリにアクセスできるようになります。これが、ページレジスタの仕組みです。

このページ・レジスタは、そのチャンネルの転送アドレスの上位8ビットだけを格納します。 これは、このページ・レジスタの値が常に64Kの倍数であることを意味するので、重要な特徴です。

案の定、ここでちょっと面倒なことになります。DMACを1つ持つオリジナルのPCは、AT/EISA/MCAや新しいコンピュータとは異なるi/oポートを使用していました。そして、新しいコンピュータは2つのDMACを持っているので、追加のレジスタが追加され、より多くのビットで拡張されました。オリジナルのPCのページレジスタは、4ビット(A16-A19)しか追加されていません。一方、新しいコンピュータでは、ベースチャネルアドレスに8ビット(A16~A23)が追加された。

ISA DMAC Extended Page Address Registers
Port Descripton
0x80 Channel 0 (Original PC) / Extra / Diagnostic port
0x81 Channel 1 (Original PC) / Channel 2 (AT)
0x82 Channel 2 (Original PC) / Channel 3 (AT)
0x83 Channel 3 (Original PC) / Channel 1 (AT)
0x84 Extra
0x85 Extra
0x86 Extra
0x87 Channel 0 (AT)
0x88 Extra
0x89 Channel 6 (AT)
0x8A Channel 7 (AT)
0x8B Channel 5 (AT)
0x8C Extra
0x8D Extra
0x8E Extra
0x8F Channel 4 (AT) / Memory refresh / Slave Connect

さて、ちょっと立ち止まってみましょう。*上の表で気になるのは、ATにリストされたポートだけです。ページレジスタは上位8ビットだけなので、これらのレジスタの値は64kの倍数でなければならないことを意味します。例えば、フロッピーコントローラをプログラミングするとき、フロッピーはDMAチャネル2を使用することが分かっています。64kより低いバッファを格納したい場合、そのチャンネルのアドレスをどこかに設定すればいいのですね?まあ、そんなところです。そのアドレスの上位8ビットを決定するために、ページレジスタを設定する必要があります。そこで、ページレジスタを設定します。

  • 0に設定した場合:Page 0、アドレスに何も追加されません。
  • 1に設定した場合:ページ1、アドレスに64kが追加される
  • 2に設定した場合:ページ2、アドレスに128Kが追加される
  • 255に設定した場合:Page 255 = 255*64K=0xFF0000, 上位8ビットをすべて設定し、16、320Kまたは約16MBがアドレスに追加されます。

ページテーブルでページを変更すると、DMAが読み書きするアドレスが変わることに注目してほしい。これにより、DMACは最大16MBのメモリに効率よくアクセスできるようになります。いいでしょ?まだ少し制限されていますが、64Kに制限されるよりはずっといいと思いませんか?

他のレジスタと同様に、醜いマジックナンバーを隠すことができます。
enum DMA0_PAGE_REG { DMA_PAGE_EXTRA0 = 0x80, //! Also diagnostics port DMA_PAGE_CHAN2_ADDRBYTE2 = 0x81, DMA_PAGE_CHAN3_ADDRBYTE2 = 0x82, DMA_PAGE_CHAN1_ADDRBYTE2 = 0x83, DMA_PAGE_EXTRA1 = 0x84, DMA_PAGE_EXTRA2 = 0x85, DMA_PAGE_EXTRA3 = 0x86, DMA_PAGE_CHAN6_ADDRBYTE2 = 0x87, DMA_PAGE_CHAN7_ADDRBYTE2 = 0x88, DMA_PAGE_CHAN5_ADDRBYTE2 = 0x89, DMA_PAGE_EXTRA4 = 0x8c, DMA_PAGE_EXTRA5 = 0x8d, DMA_PAGE_EXTRA6 = 0x8e, DMA_PAGE_DRAM_REFRESH = 0x8f //!no longer used in new PCs };
これらのレジスタを設定するために必要なことは、どのレジスタに書き込まれるかを(どのチャンネルが渡されたかに基づいて)決定し、そこに値を書き込むことだけです。
void dma_set_external_page_register (uint8_t reg, uint8_t val) { if (reg > 14) return; unsigned short port = 0; switch ( reg ) { case 1: {port = DMA_PAGE_CHAN1_ADDRBYTE2; break;} case 2: {port = DMA_PAGE_CHAN2_ADDRBYTE2; break;} case 3: {port = DMA_PAGE_CHAN3_ADDRBYTE2; break;} case 4: {return;}//! nothing should ever write to register 4 case 5: {port = DMA_PAGE_CHAN5_ADDRBYTE2; break;} case 6: {port = DMA_PAGE_CHAN6_ADDRBYTE2; break;} case 7: {port = DMA_PAGE_CHAN7_ADDRBYTE2; break;} } outportb(port, val); }
注意すべきは、ケース4がコメントされていることです。チャンネル4はマスターDMACとのカスケード接続に使われることを覚えていますか? これがなければ、何も使うことができませんから。上記の各ケースは、ページを設定するチャンネルを表しています。ですから、dma_set_external_page_register (2, 0x1000);のような呼び出しは、チャンネル2ページレジスタに0x1000を設定することができるようになります。どうです?

レジスタ

上記のレジスタに加え、コントローラは以下のレジスタも利用可能です。

コマンドレジスタ

このレジスタは、DMACを制御するために使用されます。このレジスタは次のようなフォーマットになっています。
  • Bit 0: MMT Memory to Memory Transfer
    • 0: Disable
    • 1: Enable
  • Bit 1: ADHE Channel 0 Address Hold
    • 0: Disable
    • 1: Enable
  • Bit 2: COND Controller Enable
    • 0: Disable
    • 1: Enable
  • Bit 3: COMP Timing
    • 0: Normal
    • 1: Compressed
  • Bit 4: PRIO Priority
    • 0: Fixed Priority
    • 1: Normal Priority
  • Bit 5: EXTW Write Selection
    • 0: Late Write Selection
    • 1: Extended Write Selection
  • Bit 6: DROP DMA Request (DREQ)
    • 0: DREQ sense active high
    • 1: DREQ sense active low
  • Bit 7: DACKP DMA Acknowledge (DACK)
    • 0: DACK sense active low
    • 1: DACK sense active high
これらのビットのほとんどは、i86 アーキテクチャでは動作しません。動作するのはビット2だけで、これはコントローラを有効または無効にするために使用されます。メモリからメモリへの直接転送も便利だと思うでしょう。他のビットを使っても、何もできないか、予測できない結果が出るかもしれません。

念のため、これらは本章最後のデモのdma.hヘッダーファイルに含まれています。ここでは、ビットマスクとして記述しています。

enum DMA_CMD_REG_MASK { DMA_CMD_MASK_MEMTOMEM = 1, DMA_CMD_MASK_CHAN0ADDRHOLD = 2, DMA_CMD_MASK_ENABLE = 4, DMA_CMD_MASK_TIMING = 8, DMA_CMD_MASK_PRIORITY = 0x10, DMA_CMD_MASK_WRITESEL = 0x20, DMA_CMD_MASK_DREQ = 0x40, DMA_CMD_MASK_DACK = 0x80 };

モードレジスタ(Write)

このモードは、コントローラのモードを設定します。次のような形式になっています。
  • Bits 0-1: SEL0, SEL1 Channel Select
    • 00: Channel 0
    • 01: Channel 1
    • 10: Channel 2
    • 11: Channel 3
  • Bits 2-3: TRA0, TRA1 Transfer Type
    • 00: Controller self test
    • 01: Write Transfer
    • 10: Read Transfer
    • 11: Invalid
  • Bit 4: AUTO Automatic reinitialize after transfer completes (Device must support!)
  • Bit 5: IDEC
  • Bits 6-7: MOD0, MOD 1 Mode
    • 00: Transfer on Demand
    • 01: Single DMA Transfer
    • 10: Block DMA Transfer
    • 11: Cascade Mode
このレジスタは重要です。チャンネルを設定し、メモリ・ブロックの読み取りまたは書き込みの準備を行うには、このレジスタに動作モードを書き込む必要があります。しかし、このレジスタに書き込む前に、モードを設定したいチャネルをマスクオフ(無効)にしてから、何かを変更することを常に推奨します。これは、現在使用中のチャンネルのモードを変更すると、データの破損などの問題が発生するためです。

まず最初に、私が好きなことは、意味のある名前の後ろに醜い数字を隠すことです。しかし、これは少し違います。これらの列挙は、マスクとフラグの組み合わせです。マスクは上のリストのビットフォーマットにマッチします。フラグは、単純化するために存在します。フラグは、上記のリストの必要なビットをセットしたりクリアしたりするためのもので、オプションをビット単位で OR したりすることができます。例えば、チャンネル番号とチャンネルのモードを組み合わせて、自動初期化でシングル転送を読むように設定するには、次のようにします。 チャンネル | DMA_MODE_READ_TRANSFER | DMA_MODE_MASK_AUTO | DMA_MODE_TRANSFER_SINGLE クールでしょう?

フォーマットはどちらのコントローラでも同じなので、enum は 1 つだけです。

enum DMA_MODE_REG_MASK { DMA_MODE_MASK_SEL = 3, DMA_MODE_MASK_TRA = 0xc, DMA_MODE_SELF_TEST = 0, DMA_MODE_READ_TRANSFER =4, DMA_MODE_WRITE_TRANSFER = 8, DMA_MODE_MASK_AUTO = 0x10, DMA_MODE_MASK_IDEC = 0x20, DMA_MODE_MASK = 0xc0, DMA_MODE_TRANSFER_ON_DEMAND= 0, DMA_MODE_TRANSFER_SINGLE = 0x40, DMA_MODE_TRANSFER_BLOCK = 0x80, DMA_MODE_TRANSFER_CASCADE = 0xC0 };

DMA0_MODE_REGが 0x0b で DMA 0 モードレジスタ、DMA1_MODE_REGが 0xd6 で 2 番目の DMA モードレジスタとすると、特定のチャネルの DMA モードを設定するには、以下のようにすればよいことになります。

void dma_set_mode (uint8_t channel, uint8_t mode) { int dma = (channel < 4) ? 0 : 1; int chan = (dma==0) ? channel : channel-4; dma_mask_channel (channel); outportb ( (channel < 4) ? (DMA0_MODE_REG) : DMA1_MODE_REG, chan | (mode) ); dma_unmask_all ( dma ); } //! prepares channel for read void dma_set_read (uint8_t channel) { dma_set_mode (channel, DMA_MODE_READ_TRANSFER | DMA_MODE_TRANSFER_SINGLE | DMA_MODE_MASK_AUTO); } //! prepares channel for write void dma_set_write (uint8_t channel) { dma_set_mode (channel, DMA_MODE_WRITE_TRANSFER | DMA_MODE_TRANSFER_SINGLE | DMA_MODE_MASK_AUTO); }

このルーチンによって、任意のチャンネルのモードを設定することができます。クールでしょう?例えば、フロッピードライブに書き込む準備をさせたい場合、dma_set_mode (2, 0x5A)を実行すればよいのです。(フロッピーはプライマリDMACのチャンネル2を使うことを覚えていますか?) そして、0x56 = 01010110 バイナリです。上のリストと比較すると、Mode=01 (Single transfer), AutoInit is set (Auto initialize after completion), transfer type=01 (Write), channel 2 (10) となります。

DMA_MODE_MASK_AUTOビットは、便利なビットです。このビットのおかげで、DMACを一度初期化すれば(コントローラをリセットし、チャンネルバッファのアドレスとカウントを設定することで)、再度気にする必要はありません。このビットがセットされていない場合は、読み出しや書き込みのたびにDMACを再初期化する必要があります。

注:AutoInitビット(DMA_MODE_MASK_AUTO)は、Virtual PCではうまくサポートされていないようです。このため、Virtual PCでの移植性を維持するために、AUTOINITを使用するよりも、読み取りまたは書き込み操作のたびにDMACを再初期化することを選択しました。他のエミュレータやマシンではサポートされていないかもしれません。

リクエスト・レジスタ(Write)

このレジスタは、ソフトウェアが直接DMACに送信することを可能にします。最初の2ビットでチャンネルを選択します。 例えば、00=チャンネル0、01=チャンネル1、10=チャンネル2、11=チャンネル3です。3番目のビットは、0ならチャンネルリクエストビットをリセットします。 1ならリクエストビットをセットします。
  • Bit 0-1: Channel select 0
  • Bit 2: 0=reset channel request bit, 1=set request bit
リクエスト・レジスタは、Memory-to-Memoryの操作に使用されます。コマンド・レジスタを思い出すと、i86アーキテクチャでは、メモリ間トランザクションを有効にすることはできませんし、してはいけません。このため、このレジスタは重要ではありません。

チャンネルマスクレジスタ(Write)

このレジスタを使用すると、1つのDMAチャネルをマスクすることができます。0ビットと1ビットでチャンネルを設定します(00=チャンネル0、01=チャンネル1、10=チャンネル2、11=チャンネル3)。ビット4は、チャンネルをマスクするかしないかを決定します。ビット4が0の場合、チャンネルのマスクを解除する。1であれば、マスクする。他のビットは未使用。
  • Bit 0-1: Channel select
  • Bit 2: 0=unmasks channel, 1=masks channel
他のすべてのビットは未使用です。

マスクレジスタ(Write)

このレジスタは、現在どのチャネルがマスクされ、マスクされていないかの情報を含んでいます。この8ビット・レジスタの上位4ビットは常に未使用です。下位4ビットは、4チャンネルのうちの1つをマスクまたはアンマスクするために使用されます。例えば、ビット 0 はチャンネル 0、ビット 1 はチャンネル 1、...といった具合です。注:チャンネル4をマスクすると,カスケード接続によりチャンネル4,5,6,7もマスクされます。
  • Bit 0: Channel select 0
  • Bit 1: Channel select 1
  • Bit 2: Channel select 2
  • Bit 3: Channel select 3
その他のビットは未使用です。

例えば、任意のチャンネルをマスク(無効化)するルーチンを用意すると、それぞれのビットを設定するだけでよい。

void dma_mask_channel(uint8_t channel){ if (channel <= 4) outportb(DMA0_CHANMASK_REG, (1 << (channel-1))); else outportb(DMA1_CHANMASK_REG, (1 << (channel-5))); }
同様に、チャンネルのマスクを解除するには、そのビットをクリアするだけです。
void dma_unmask_channel (uint8_t channel) { if (channel <= 4) outportb(DMA0_CHANMASK_REG, channel); else outportb(DMA1_CHANMASK_REG, channel); }
これらのルーチンは両方とも、DMA0_CHANMASK_REGが0x0a(DMACマスクレジスタのi/oポート)、DMA1_CHANMASK_REGが0xD4(第2のDMACマスクレジスタのi/oポート)であると仮定しています。

複数のチャネルを同時に設定できるため、このレジスタを使用すると、複数のチャネルを同時にマスクしたり、マスクを解除したりすることができます。

ステータスレジスタ

ステータス・レジスタは次のようなフォーマットになっています。
  • Bit 0: TC0 Set if Channel 0 has reached Transfer Complete (TC)
  • Bit 1: TC1 Set if Channel 1 has reached Transfer Complete (TC)
  • Bit 2: TC2 Set if Channel 2 has reached Transfer Complete (TC)
  • Bit 3: TC3 Set if Channel 3 has reached Transfer Complete (TC)
  • Bit 4: REQ0 Set if Channel 0 is pending a DMA Request (DRQ)
  • Bit 5: REQ1 Set if Channel 1 is pending a DMA Request (DRQ)
  • Bit 6: REQ2 Set if Channel 2 is pending a DMA Request (DRQ)
  • Bit 7: REQ3 Set if Channel 3 is pending a DMA Request (DRQ)
このレジスタはあまり有用ではありません。ほとんどの場合、DMACを制御しているデバイスは転送が完了するとIRQを送信するので、情報のためにレジスタをポーリングする必要はありません。 最初の4ビットはそのチャンネルでの転送が完了したかどうかを示し、最後の4ビットはそのチャンネルに保留中のDMA要求があるかどうかを示している。

ISA DMAコマンド

コントローラは、ソフトウェアがコントローラに対してコマンドを送信できるようにするための特別なレジスタを提供します。これらのコマンドは、特定のビットフォーマットを全く必要とせず、単純なI/O操作で起動することができます。

DMACは、アドレスバス(ラインA0-A3)のデータと、ORQ、IOWラインの状態から、コマンドを認識します。

これらのレジスタには何も特別なものはないことに注意してください。これらは、本章の冒頭の汎用レジスタの表にも記載されています。

クリアバイトポインタ・フリップフロップ

これは特別なi/oアドレスポートで、8ビットDMAC(プライマリDMAC)で作業するときに、16ビット転送の間でフリップフロップを制御できるようにするためのものです。

両方のDMACに2つのポートがあります。

ISA DMAC Flip-Flop Ports
Port Descripton
0x0C DMAC 0 (16 bit) Slave (write)
0xD8 DMAC 1 (8 bit) Master (write)

例えば、DMA0_CLEARBYTE_FLIPFLOP_REGが0x0c、DMA1_CLEARBYTE_FLIPFLOP_REGが0xD8とすると、以下のルーチンでフリップフロップをセットまたはクリアすることができます。

void dma_reset_flipflop(int dma){ if (dma < 2) return; //! it doesnt matter what is written to this register outportb( (dma==0) ? DMA0_CLEARBYTE_FLIPFLOP_REG : DMA1_CLEARBYTE_FLIPFLOP_REG, 0xff); }

リセット

よく似た方法で、以下のレジスタに任意の値を書き込むことで、a DMACをリセットすることができます。

ISA DMAC Reset Ports
Port Descripton
0x0D DMAC 0 (16 bit) Slave (write)
0xD8 DMAC 1 (8-bit) Master (write)

例えば、DMA0_TEMP_REGが0x0Dであると仮定した場合。

void dma_reset (int dma){ //! it doesnt matter what is written to this register outportb(DMA0_TEMP_REG, 0xff); }

全レジスタのマスク解除

さらに似たような方法で、このコマンドでも同じコンセプトが適用されます!すべてのハードウェアプログラミングコマンドがこのように簡単であれば、素晴らしいことだと思いませんか?

ISA DMAC UnMask All Ports
Port Descripton
0x0E DMAC 0 (16 bit) Slave (write)
0xDC DMAC 1 (8-bit) Master (write)

つまり、DMA1_UNMASK_ALL_REGを0x0Eとすると、スレーブDMACから全レジスタのマスクを解除することになる。

void dma_unmask_all (int dma){ //! it doesnt matter what is written to this register outportb(DMA1_UNMASK_ALL_REG, 0xff); }

よーし、もう一つのデモの時間だ!悪いニュースは、このデモが前の章と全く同じに見えるということです(BochsとVirtual PCの両方で動作するようになったことを除いて)。良い点は、新しいDMAインターフェイスを使用するようにアップグレードされていることです。

新しいコードのコアは HAL -dma.hdma.cppにあり、この章にあるすべてのコードを含んでいます。しかし、1つだけ小さな変更があります。DMACのModeレジスタのAUTOINITビットはVirtual PCではうまくサポートされていないので、我々のdma_set_readと dma_set_writeルーチンはデモコードでビットをセットしません。

//! prepares channel for read void dma_set_read (uint8_t channel) { dma_set_mode (channel, DMA_MODE_READ_TRANSFER | DMA_MODE_TRANSFER_SINGLE); } //! prepares channel for write void dma_set_write (uint8_t channel) { dma_set_mode (channel, DMA_MODE_WRITE_TRANSFER | DMA_MODE_TRANSFER_SINGLE); }

読み取りセクタ操作の間、フロッピードライバのflpydsk_read_sector_impルーチンは DMAC を初期化し、読み取り操作のために DMAC を準備します。残りのルーチン(編集済み)は前章と同じで、FDCにREADコマンドを送信する役割を担っています。DMA_BUFFERは、DMAC転送に使用できる空きメモリのバッファに過ぎません。dma_initialize_floppyは、新しい DMA ミニドライバを使用して DMAC を初期化し、フロッピードライバが使用できるように準備します。(DMAC を初期化した後、チャネルFDC_DMA_CHANNEL でドライバdma_set_readルーチンを呼び出して、READ 操作のための DMAC を準備します。FDC_DMA_CHANNELはチャンネル2です (FDCがDMACのチャンネル2を使うことを思い出してください?)。

//! read a sector void flpydsk_read_sector_imp (uint8_t head, uint8_t track, uint8_t sector) { uint32_t st0, cyl; //! initialize DMA dma_initialize_floppy ((uint8_t*) DMA_BUFFER, 512 ); //! set the DMA for read transfer dma_set_read ( FDC_DMA_CHANNEL ); //! rest of the code is the same... }

dma_initialize_floppyは、新しいミニドライバを使って DMAC を準備し、フロッピィドライバで使用できるようにする役割を担っています。ここが楽しいところです。

まず、dma_reset()を呼んで、マスターDMACをリセットします。それから、dma_mask_channel() を呼んで、(FDC が使う) チャネル 2 を無効化 (マスク) します。これは、チャネルがもはや使用されていないことを確認し、それを変更できるようにします。

さて、次は楽しいことです。チャネルが使用するアドレスを設定するために、dma_set_addressルーチンを呼び出します。これにより、チャネルのアドレスの下位と上位の部分を設定することができます。メンバのバイト成分にアクセスするのを少し簡単にするために、ユニオンを使用します。つまり、a.l をチャネルが使用するバッファに設定します。ユニオンのおかげで、a.byte[0]はその値の下位バイトを、byte[1]は2バイト目を、といったように参照できるようになりました。バッファのサイズであるさも同じようにします。したがって、dma_set_addressを呼び出してバッファのアドレスを設定し、その下位バイトと上位バイトをバッファのアドレスに設定し、同じようにdma_set_countを呼び出して長さを設定しています。クールでしょう?

でも、dma_reset_flipflopの呼び出しは何ですか?フリップフロップは8ビットDMACで16ビットデータを扱うときだけ使われる。もし16ビットDMACを使うなら、フリップフロップを呼び出す必要はないでしょう。フリップフロップは、16ビットデータの上位バイトと下位バイトを選択するために使用されます。フリップフロップをリセットすると、次のバイトデータがローバイトであることをDMACに伝えることになります。フリップフロップがデフォルトの位置でない場合は、ハイ・バイトとして選択されます。DMACは8ビットのデータバスで16ビットのデータを扱うので、これは選択されなければなりません。このバイトが16ビットデータのどの部分を指しているのか、どうやって知ることができるのでしょうか?

最後に、dma_set_read()を呼んで DMAC を読み出し用に設定し、すべてのチャネルのマスクを外して、デバイスが再び使用できるようにします。これは、FDCがDMACのチャンネル2を使えるようにするために重要です。

bool _cdecl dma_initialize_floppy(uint8_t* buffer, unsigned length){ union{ uint8_t byte[4];//Lo[0], Mid[1], Hi[2] unsigned long l; }a, c; a.l=(unsigned)buffer; c.l=(unsigned)length-1; //Check for buffer issues if ((a.l >> 24) || (c.l >> 16) || (((a.l & 0xffff)+c.l) >> 16)){ #ifdef _DEBUG _asm{ mov eax, 0x1337 cli hlt } #endif return false; } dma_reset (1); dma_mask_channel( FDC_DMA_CHANNEL );//Mask channel 2 dma_reset_flipflop ( 1 );//Flipflop reset on DMA 1 dma_set_address( FDC_DMA_CHANNEL, a.byte[0],a.byte[1]);//Buffer address dma_reset_flipflop( 1 );//Flipflop reset on DMA 1 dma_set_count( FDC_DMA_CHANNEL, c.byte[0],c.byte[1]);//Set count dma_set_read ( FDC_DMA_CHANNEL ); dma_unmask_all( 1 );//Unmask channel 2 return true; }

まとめ

さて、もう1章が終わりましたね。この章は以前の章ほど複雑でも難しくもなかったので、いい息抜きになったのではないでしょうか?

ここからは、ディスクからファイルを読み込む機能がないと、これ以上進めません。ディスクからデータを読み込む機能はありますが、ファイルではありません。これはファイルシステムドライバを使って行います。しかし、ちょっと待ってください。FAT12についてはすでに2回ほど説明しましたね。でも、FAT12はもう2回目なんです!いかに頻繁に書き直す必要があるかがわかります。同じ内容を3回目も取り上げるよりは、もう1つテーマを追加することにしましょう。仮想ファイルシステム(VFS)です。次の章では、デモプログラムを実行できるようにするかもしれません。

また、OSにグラフィカルなタッチを加えたいという読者も多いので、Vesa Bios Extensions (VBE) や Video Graphics Array (VGA) / Super VGA (SVGA) 関連の上級編もリリースするかもしれません。また、現在のシステムを本物のマイクロカーネルにするために、DLLサポート、ドライバ、およびネイティブPEリソースのサポートがあります。

たくさんのクールなものが控えています :)もし、何か取り上げて欲しいトピックがあれば、遠慮なく教えてください。