学校では教えてくれないこと

DMA対応と言われたら(2)

2011.2

やあみんな、ビリーだよ。

今回は、USBコントローラを例にして、いよいよDMA転送の具体的な動作について説明していくよ。

USBコントローラの動作例

例として、DMAスレーブ型のUSBコントローラの内蔵FIFOに対して、汎用DMACで書き込みを行うケースを考えてみよう。通常、USBコントローラに内蔵されるFIFOの容量はそれほど大きくなくて、せいぜいパケット2個分(Bulkの場合、512バイト*2個=1Kバイト)くらいが一般的なんだ。

転送のパフォーマンスアップのためには、DMACを使って転送するサイズを、できるだけ(少なくともFIFOの容量よりも)大きくしたいよね。でも、FIFOが一杯になったらどうするんだろう?この場合、USBコントローラがDMACに連絡してDMA転送の進み具合をコントロールしないといけない。

一般に、DMAスレーブ機能を持つI/Oデバイスは、DMA転送要求(DREQ)という出力信号を持っているんだ。DMACは、DREQ信号が有効な間はDMA転送を行うけど、I/Oデバイス側がDREQ信号を取り下げると転送を中断する。このような動作が、前回書いた「デマンド転送」だよ。

今回のUSBコントローラの例では、「FIFOが一杯になった」らDREQを取り下げ、USBコントローラがデータを読み出して「FIFOに空きができた」らDREQを有効にするという制御が行われているんだ。読み出しの場合も同じで、「FIFOが空になった」らDREQを取り下げるということになるよね。

簡単なタイミングチャートはこんな感じ。

タイミングチャート図

デマンド転送を行いたい場合は、DREQ信号をDMACと接続するようにハードウェアの回路を設計しないといけないんだよ。デバイスドライバを作る側も回路図とにらめっこして接続を確認する必要があるね。

最近は、I/OデバイスとDMACの両方を内蔵したCPUが増えてきているんだけど、内蔵I/Oデバイスと内蔵DMACの間でDMA転送をするのであれば、外部回路の設計は考えなくていいよね。この場合は、どのような方式でDMA転送を行うかを設定するレジスタが用意されているんだ。ただ、その内蔵モジュールの構成や仕様はCPUごとに異なるので、データシートをよく調べないといけないね。

割り込みタイミングの違い

DMAバスマスタになることができるI/Oデバイスの場合は、1系統の割込みをハンドリングするだけでかまわないんだよ。デバイスドライバは、内部で自動的に行われるDMA転送を意識する必要がないんだ。

でも、外部の汎用DMACを使用するスレーブデバイスの場合は、通常、対象I/Oデバイスの割込みに加えて、汎用DMACの転送完了割込みもハンドリングする必要があるんだ。つまり、2系統の割込みが入ることを考慮して、 デバイスドライバの処理シーケンスを設計しなくてはならないということだね。

ここで注意しなければならないポイントが、それぞれの割込みが通知されるタイミングの違いなんだ。先ほどのUSBコントローラのデータ送信(FIFO書き込み)の例を考えてみよう。下記2つのイベントが発生することになるけど、これらのタイミングは同時におきるわけではないんだ。

  • (a)DMACのDMA転送完了割込み
  • (b)USBコントローラのUSB転送完了割込み

(a)は、DMACが指定したサイズのデータをFIFOへ書き込んだ時点で通知されるよね。でも、この時点では、まだUSBバス上にデータが送出されていないかもしれない。USBコントローラがFIFOからデータを読み出して、実際にUSBバス上に送り出し終えるまでのタイムラグがあるよね。

つまり、パケット送信完了を保証するためには、(b)のタイミングでデバイスドライバからアプリケーションに通知する必要があるんだ。

※補足

パフォーマンスを重視する場合は、(a)のタイミングで送信完了とみなす場合もあります。この場合は、コントローラがUSBパケット処理を行っている間に、並行してデバイスドライバ(アプリケーション)が次の送信準備(データコピーなど)を行うことができ、処理効率が向上します。ただし、実際に次のDMA転送を起動するタイミングでは、直前のUSB転送完了を待ち合わせる必要があります。

以下に簡単なシーケンス図を示してみるね。

シーケンス図

逆に、データ受信(FIFOからの読み出し)の場合は、(a)のタイミングに同期させる必要があるんだよ。なんでなのか分かるかな?

この場合は、USBコントローラによって受信データがFIFOに格納された後でDMACがそれを読み出すという順番になるからだね。このような、タイミングに起因する不具合はよくあるんだ。具体的にはこんなものがある。

転送方向不具合現象の例想定される原因
送信Write関数が戻ってきた直後に、次のデータを書き込んだらUSBバス上のパケットの内容が化けていた。あるいはサイズが異なっていた。USBコントローラによるパケット送信処理が完了していなかったため。
受信Read関数が戻ってきた直後に、バッファを参照すると受信データの内容が化けていた。あるいは欠落していた。DMACによるDMA転送が完了していなかったため。

実際に不具合となるかどうかは、USBコントローラのアーキテクチャにもよるんだけど、少なくともタイミングの違いがあることは頭においておかないといけないね。

次に、DMA転送データを格納するバッファ(以下、DMAバッファ)に関して、注意しなければならない点について説明してみよう。

DMAバッファとキャッシュ

DMAバッファは、一般的に「キャッシュ不可領域」に配置するんだ。なぜ、CPUのキャッシュメモリを意識しなければならないかわかるかな。

DMAバッファは、CPUとDMAC(あるいはDMAバスマスタデバイス)の両方からアクセスされるメモリ領域だよね。仮に、CPUのデータキャッシュがコピーバック(ライトバック)モードで動作している状態で、 DMAバッファをキャッシュ可能な領域に確保したとしてみよう。

※補足

キャッシュの動作モードについては、「拾弐の巻:キャッシュは諸刃の剣」も参照ください。

CPUからDMAバッファに対してデータを書き込んだ後でDMACを起動しても、おそらく正しいデータは転送されない。書き込んだデータはキャッシュメモリにだけ格納されて、実メモリであるDMAバッファに反映されていない可能性が高いんだ。逆に読み出しの場合、DMACがDMAバッファにデータを格納した後でCPUがDMAバッファを参照しても、CPUはキャッシュされているデータを読み出してしまうので、ゴミデータしか読めないかもしれないね。

このようなメモリの不整合を防ぐために、DMAバッファを「キャッシュ不可領域」に確保するんだ。

実は、パフォーマンス向上のために、あえてDMAバッファを「キャッシュ可能領域」に確保する場合もある。大量のデータをDMAバッファにコピーする場合などは、キャッシュ可能な方が処理時間を短縮できるはずだよね。ただし、この場合は、デバイスドライバが明示的にキャッシュメモリの制御を行う必要があるんだ。

操作機能概要
flushキャッシュの吐き出し指定した領域に該当するキャッシュメモリの内容を実メモリに書き出す。
invalidateキャッシュの無効化指定した領域に該当するキャッシュメモリの内容を無効にする。

書き込みの場合の説明図

読み出しの場合の説明図

具体的には、以下のように実装するよ。

書き込み
  1. DMAバッファにデータを書き込む。
  2. DMAバッファの領域をキャッシュflushする。
  3. DMA転送を起動する。
読み出し
  1. DMA転送の完了を待つ。
  2. DMAバッファの領域をキャッシュinvalidateする。
  3. DMAバッファからデータを読み出す。

要は、DMACはキャッシュメモリの存在を知らないので、デバイスドライバの側で適切にケアしてやる必要があるということだね。このような処理を「キャッシュコヒーレンシをとる」というんだ。

キャッシュメモリの仕様はCPUアーキテクチャに依存しているから、一般的にその制御ライブラリはBSPと呼ばれるCPU初期化モジュールの中で実装されることが多い。 デバイスドライバを設計する時に、使用する開発環境で利用可能かどうかを確認しておく必要があるから、気をつけないとね。

DMAバッファとアドレスアライメント

DMAバッファに関しては、もうひとつ「アドレスアライメント」の制約に注意しないといけないんだ。アライメント(alignment)はアラインメントと書くこともあって、「整列」という意味の単語なんだけど、ここでは「境界」という意味で使われているよ。アドレスアライメント制約は2通りある。

(1) DMACアーキテクチャに依存した制約

汎用DMACあるいはDMAバスマスタデバイスのデータシートには、たいてい「●KB境界のバッファを指定してください」という注意書きが記載されている。DMAバッファはこれに従って配置しなくてはならないんだ。例えば、EHCIというUSBホストコントローラの仕様では「4KB境界とせよ」という規定があるんだよ。

(2) キャッシュラインサイズに依存した制約

DMAバッファをキャッシュ可能領域に配置する場合は、そのアドレスをキャッシュラインサイズの境界に一致させなくてはいけない。この場合は、キャッシュのinvalidate/flushが必要ということを言ったばかりだけど、このflushの動作に関係する制約なんだ。

キャッシュのflushは、「キャッシュライン」単位に行われるんだ。DMAバッファがキャッシュラインサイズの境界に一致していないと、キャッシュをflushしたときにDMAバッファ以外のメモリ領域を壊してしまう恐れがあるんだ。

説明図

同じことなんだけど、DMAバッファのサイズは「キャッシュラインサイズの整数倍」としておく必要がある。これは、DMAバッファの終端付近のアドレスに対するflushが発生したときに、やっぱりメモリ破壊が起きてしまうからだね。

キャッシュラインのサイズはCPUアーキテクチャによって違うんだ。例えば、同じルネサスエレクトロニクスのSHシリーズでも、SH-2Aでは16バイト、SH-4Aでは32バイトなんだよ。注意しておく必要があるね。

終わりに

これで「DMA対応と言われたら」の巻は完結だよ。どうだったかな?DMACとかキャッシュメモリとか他のハードウェアのこともよく調べないといけないんだな、ということを感じてくれたらいいんだけど。

次回からは新シリーズになるよ。何の話をしようかな・・・

ページのトップへ