火曜日, 3月 19, 2013

JavaでJTAG通信

新しいターゲットボードDE0-Nanoですが、本体の機能だけでホストPCと通信する場合には、ダウンロードケーブルを利用したJTAG通信を利用することになります。しかし、このJTAG通信に関するまとまった情報がなかなか入手しづらく、DE0-Nano付属のデモアプリケーションでも明らかにホストPCと通信しているのですが、なぜかこれに関しても技術資料が一切ありません。で、こちらで独自に調査した結果をこれから述べたいと思います。

DE0-Nanoは本体にUSB Blaster互換の回路が載っているので、USB Blasterのドライバをそのまま使ってボードへのアクセスが可能になっています。この互換回路にはFTDI社のFT245BLが使われていますので、USB Blasterのドライバの正体は、実はFTDI社のD2XXドライバだと思われます。その証拠に、ドライバ付属のDLL(ftd2xx.dll)を使ってそのままボードにアクセスが可能だったりします。というわけで、ハードウェアアクセスのための資料はD2XXドライバのドキュメントという形で間接的に入手が可能です。

では、次にどういったデータを流せばいいのかというレベルの話になりますが、これに関してはUrJTAG関連で詳細なドキュメントがあります。かいつまんで言うと、USBのパケット長は最大64バイトなので、そこにデータを詰め込んで送信するわけですが、そのフォーマットはJTAGの全信号を制御できるbit bangingモードとデータを高速で送れるbyte shiftモードの2種類を使い分けることになります。

そして、JTAGを使ってどのようにしてホストPCとの通信を確立するのかというレベルの話になりますが、これはオンチップ・デバッグ用のVirtual JTAGメガファンクションを利用するのがおそらく最も簡単です。では、実際に回路を作ってみましょう。

jtag_led_bdf.png

これはQuartus IIの回路図エディタでsld_virtual_jtagメガファンクションに入出力ピンをつないだものです。とりあえず8ビットのデータを入出力しますので、パラメータsld_ir_widthを8にセットし、念のためsld_auto_instance_indexを”YES”にします。入力ピンkey[7..0]をir_outに接続し、ir_inを出力ピンled[7..0]に接続します。ledにはボード上のLEDのピン(A15, A13, B13, A11, D1, F3, B1, L3)を割り当てますが、keyは下位2ビットの押しボタン(E1, J15)以外は特に使わないので、適当にDIPスイッチ(M15, B9, T8, M1)やGPIO入力ピン(B8, A8)に割り当てます。

入力が面倒な方のために、回路図ファイル(jtag_led.bdf)と論理合成済みのsofファイル(jtag_led.sof)を用意してあります。ただし、回路図ファイルの方はピン番号とデバイス(EP4CE22F17C6)の指定が必要になります。

上の回路を予めDE0-Nanoに書き込んでおいた上で、JNAをインストールした環境上で以下のJavaプログラムを実行します。JNAのインストールやJavaのコンパイルが面倒な方は、JNA同梱済みのJarファイル(jtag_led.jar)を実行してください。

実行環境はWindows XPでのみ動作確認をしていますが、32ビット環境であればたぶん動くと思います。64ビット環境の方は未確認ですが、おそらくDLLの名前を変更すれば動くと思われます。(”usbblstr32” → “usbblstr64”)

import com.sun.jna.*;
import com.sun.jna.ptr.*;
public class JTAG_LED {
    static native int FT_OpenEx(String pArg1, int flags,
        PointerByReference pHandle);
    static native int FT_Close(Pointer ftHandle);
    static native int FT_Write(Pointer ftHandle, Pointer lpBuffer,
        int dwBytesToWrite, IntByReference lpdwBytesWritten);
    static native int FT_Read(Pointer ftHandle, Pointer lpBuffer,
        int dwBytesToRead, IntByReference lpdwBytesReturned);
    static {
        Native.register("usbblstr32");
    }
    Pointer ftHandle = null;
    public JTAG_LED(String description) {
        PointerByReference pHandle = new PointerByReference();
        if (FT_OpenEx(description, 2, pHandle) != 0) {
            throw new RuntimeException("FT_OpenEx failed.");
        }
        ftHandle = pHandle.getValue();
    }
    public void close() {
        FT_Close(ftHandle);
    }
    Memory memory = new Memory(64);
    IntByReference ref = new IntByReference();
    public static final short L = 0x2D2C;
    public static final short H = L | 0x1010;
    public static final short TMS = L | 0x0202;
    public static final short TMS_H = TMS | H;
    public static final short OFF = 0x0D0C;
    public void write(short... data) {
        memory.write(0, data, 0, data.length);
        FT_Write(ftHandle, memory, data.length << 1, ref);
    }
    public static final int WR = 0x81;
    public static final int RD = 0xC1;
    public static short byteshift(int data, int mode) {
        return (short) ((data << 8) | mode);
    }
    public byte read() {
        FT_Read(ftHandle, memory, 1, ref);
        return memory.getByte(0);
    }
    public static void main(String[] args) throws InterruptedException {
        JTAG_LED jtag = new JTAG_LED("USB-Blaster");
        jtag.write(TMS, TMS, TMS, TMS, TMS); // TEST_LOGIC/RESET
        jtag.write(L, TMS, TMS, L, L); // SHIFT_IR
        jtag.write(byteshift(0x0e, WR), L, TMS); // USER1 instruction
        jtag.write(TMS, TMS, L, L); // SHIFT_DR
        jtag.write(byteshift(0, WR), TMS_H, TMS, TMS, L, L); // Dummy write
        int lfsr = 1;
        for (int b = 1; (b & 1) != 0;) {
            jtag.write(byteshift(lfsr, RD), TMS_H, TMS, TMS, L, L);
            if (((lfsr <<= 1) & 256) != 0) lfsr ^= 0x171;
            b = jtag.read() & 255;
            System.out.println(Integer.toBinaryString(b));
            Thread.sleep(((b & 2) != 0) ? 10 : 100);
        }
        jtag.write(byteshift(0, WR), TMS_H, TMS, OFF);
        jtag.close();
    }
}

うまくいくと、写真では伝わりにくいのですが、LEDが激しく点滅するようになります。この点滅パターンはLFSRで発生しているので全くランダムというわけではなく、時々左に流れるように見えたりします。左側の押しボタンを押すと、点滅がゆっくりになります。そして、右側の押しボタンを押すと、プログラムが停止してLEDが消灯します。コンソールで実行すると入力ピンの状態を2進数表示しますので、押しボタンやDIPスイッチの状態を確認することができます。

これらの制御は全てホストPCによるものです。その証拠に、普段は点灯しないはずのLOAD LED(D4)が点灯しているのがわかると思います。

de0nano_jtag.jpg

このサンプルプログラムはsld_virtual_jtagメガファンクションの仮想命令レジスタ(VIR)をデータの送受信に利用したもので、実はかなり変則的な使い方になります。仮想命令レジスタは最大32ビットなので、データをその単位でやり取りするのであれば、この方法で十分です。本来は仮想命令レジスタでチャネルを選択した上で、仮想データレジスタ(VDR)を使ってデータの送受信を行いますが、この場合データ長の制限がない代わりに、自前でシフトレジスタを実装する必要があります。

仮想命令レジスタの書き込みは、実際にはJTAGの命令レジスタ(IR)にUSER1命令(0000001110)をセットし、データレジスタ(DR)に送信データとインスタンスIDをセットします。インスタンスIDはメガファンクションの生成数が1つだけであれば必ず1ビット長の”1”になりますので、送信データに”1”を付加して送出します。データの受信は送信と同時に行われますが、インスタンスIDによるチャネル選択が反映されるのは次の受信データからになります。したがって、最初に1回だけダミーデータを送出してチャネル選択を確定する必要があります。

このサンプルは最も単純な例なのでほとんどメガファンクション任せですが、実際にはFIFOフロー制御等、プロトコルレベルでの設計が必要になると思われます。JTAG系はクロックドメインが異なるのでメタスタビリティに配慮する必要がありますが、DCFIFOを使えばそれがクリアできる上にFIFOも実装できて一石二鳥です。

土曜日, 3月 16, 2013

ターゲットボード(DE0-Nano)

理論的な事ばかりをだらだらと書いていても仕方がないので、そろそろ実際の製作に入りたいと思います。

以前使っていたCQ出版のボードはさすがにもう設計が古く、パラレルポートが必要、初代Cycloneが最新の開発ツール(Quartus II Web Edition)のサポート対象外、といった問題があるので別のターゲットボードを探していたのですが、まさにうってつけのボードが見つかりました。

Terasic社のDE0-Nanoです。

この一万円前後の激安ボードに載っているCyclone IVはかなり大きな回路が入るので、8コアや16コア位はなんとかいけそうです。あと、A/Dコンバータや加速度センサ、SDRAMが付いているので、本体だけでも色々と遊べそうです。

ただ、惜しむらくは表示手段がボード上のLED 8個しかなく、またホストPCとの通信手段も基本的にJTAGで統合環境のみからのアクセスとなります。JTAG-UARTを使えば一応通信は可能だと思われますが、ゆくゆくはXilinxにも対応していきたいのでなるべくSOPC Builderは使いたくありません。Arduinoではダウンロードケーブルでそのままシリアル通信ができるのですが…。

マルチコアプロセッサの論理合成はそれなりに時間が掛かりますので、プログラム書き換えのたびに論理合成からやり直すのでは話になりません(無償のWeb Editionはインクリメンタル・コンパイル未対応)。ホストPCとの通信ができれば論理合成をせずにプログラムだけダウンロードすることも可能になるので、開発の効率が断然違います。また、ホストPCの自作プログラムとリアルタイムで通信できれば、さらに応用が広がることが期待されます。色々と調べてみると統合環境のコンソール入出力を無理やりリダイレクトするといった涙ぐましいものまであり、これは無理かなと一旦諦めかけたのですが、UrJTAGというフリーのJTAGツールからボードにアクセス可能なことがわかり、ここから調べていってようやくJavaからJNA経由でUSB Blasterを制御できるようになりました。(ただし、あくまでもDE0-Nano上の互換機能のみで、本物は未テスト)

こういった情報もこれから徐々に公開していきたいと思います。

火曜日, 2月 12, 2013

マルチプロセッサの回路設計

それではまず、レジスタレスアーキテクチャの基本要素となる、アキュムレータマシンの回路を実際に設計してみましょう。

データの流れに従って、大まかにブロック図を描いてみます。アキュムレータマシンの内部状態は基本的に、プログラムカウンタ(PC)とアキュムレータ(Acc)のみになります。その他の状態フラグ等はとりあえず省略します。

dataflow_block.png

プロセッサは、古い内部状態(PC, Acc)をもとにメインメモリから命令とデータを同時に読み出し、演算命令やジャンプ命令を実行して新しい内部状態(PC’, ACC’)に更新します。また、PUT命令を実行する場合はメインメモリの書き換えも同時に実行します。新しい内部状態をまた古い内部状態の入力に接続すると、そのままシングルプロセッサの出来上がりです。簡単ですね。

メモリアクセスの出力とタイミングを合わせるため、内部状態は同じクロック数だけ遅延された上でプロセッサに入力されます。演算命令はアキュムレータの変更、ジャンプ命令はプログラムカウンタの変更として実現できるので、プロセッサ内部は基本的に組み合わせ回路のみで構成可能となります。そして、以下の前提のもとで、CPI(命令実行あたりのクロックサイクル数)を1にすることができます。

  1. メインメモリがデュアルポート(命令読み出しとデータ書き込みが同時に可能)
  2. メモリアクセスの遅延が1クロックのみ
  3. プロセッサ内部が組み合わせ回路のみで構成

最近のFPGAの埋め込みメモリはデュアルポートで1クロックアクセス可能なものが普通なので、上の条件は特に難しくありません。CPIを1とすることはプロセッサアーキテクチャにおける一つの目標であり、これを超えるには命令の先読みや命令レベル並列化といった複雑な技法が必要となります。したがって、プロセッサ単位ではこの程度の単純な構造に留め、あとはマルチプロセッサ化による性能改善を図ることにします。

このプロセッサを複数、内部状態の入出力を接続してリング状に結合すると、マルチプロセッサが出来上がります。メインメモリは、各プロセッサにバンク分割したものを割り当てますが、この構造のままでは、PUT命令による書き込みの際にプロセッサと同じバンクにしか書き込めないという問題が生じます。そういった不便さに目をつぶってなんとか使うという手もありますが、本質的にプロセッサ間でのデータ転送手段がアキュムレータ以外にないことになるので、単に不便なだけでなく、このままでは処理性能もあまり期待できないことになります。

dual_bad.png

実は、こういったリング状の構造(ネットワーク・トポロジー)を持つマルチプロセッサでは、回路がシンプルになる反面、ランダムアクセスに弱いという欠点があります。PUT命令はランダムアクセス書き込みのための命令ですが、他のバンクへの書き込みも間接的なプロセッサ間通信手段なので、できれば実現したいものです。とはいえ、単純に各プロセッサから全てのバンクに書き込み信号を接続したのでは、各バンクにプロセッサ数だけの書き込みポートが必要となり、せっかくリング状でシンプルな構造にしたのにこれでは台無しです。

quad_bad.png

実は、ランダムアクセス書き込みに関しては、メモリ一貫性の条件を緩和することで、この問題を回避することができます。例えば、ハードディスクの書き込みはヘッドのシークや回転待ちの関係もあって、即座に実行するのは難しいのですが、その間コンピュータが止まっていては話にならないので、大抵のOSは書き込みデータを一旦メモリに溜めて、後でまとめて遅延書き込みを行います。このマルチプロセッサでも、例えばバンク0への書き込みを、次にバンク0の読み出しを実行するまで遅延させても実際の動作には影響ありません。そこで、他のバンクへの書き込みを一旦保留して、そのバンクに到着した時についでに書き込みを行う方式を考えます。

dual_block.png

この方式では、プログラムが自分自身で書き込んだ内容を、ちゃんと読み出せることが保障されます。ただし、他のプログラムが書いた内容が、即座にメモリ内容に反映されるわけではありません。メモリ一貫性でいえば、プログラム内でのメモリ一貫性は保障されるものの、複数プログラム間でのメモリ一貫性は保障されないことになります。ただ、書き込み側のプログラムがプロセッサを一周する間にはメモリ内容にも反映されますので、こういった遅延を配慮したプログラムを書けば問題ありません。

quad_block.png

これは4プロセッサの例ですが、バンク書き込み信号が増えた分、配線は複雑になりますが、十分なプロセッサ間データ転送量を確保できる上に、各バンクメモリはデュアルポートで済むようになります。

ただし、ランダムアクセス読み出しに関しては、もう少し考える必要があります。

(つづく)

火曜日, 1月 22, 2013

アキュムレータマシンと自己書き換え

レジスタレスアーキテクチャによるマルチプロセッサは、ちょうどこんな風に、プログラムがプロセッサの上をぐるぐる追いかけっこしながら実行するイメージになります。

smp_image.png

プログラムがプロセッサ間を移動するためにはできるだけ身軽な方が良いので、なるべくレジスタの少ないアーキテクチャを採用することになります。こういったアーキテクチャの代表格に、アキュムレータマシンがあります。アキュムレータマシンは、例えば以下の様な形式の命令を実行します。

ADD メモリアドレス

これは実際には、唯一の汎用レジスタであるアキュムレータAccの値に、メモリアドレスで指定されたメモリの内容を足す命令になります。

Acc ← Acc + (メモリの内容)

アキュムレータマシンは回路が非常にコンパクトになる反面、メモリアクセスが多くなるため高速化にはあまり向きません。例えば上の「ADD メモリアドレス」の場合、まずADD命令とメモリアドレスの読み出しで最低1回、そしてメモリ内容の読み出しでもう1回、最低でも計2回のメモリアクセスが必要となります。

現在のプロセッサは、なるべくメモリアクセスを減らす方向に進化を続けていますので、それに反するアキュムレータマシンは古典的なものとしてほとんど忘れ去られてしまいました。しかし、この弱点を除けば、小規模マルチプロセッサの要素として望ましい性質を備えています。そこで、アキュムレータマシンを改良して、小規模マルチプロセッサに組み込むことを考えます。

もしアキュムレータ以外にレジスタがあれば、メモリアドレスの代わりにレジスタ番号を指定して

ADD レジスタ番号 { Acc ← Acc + レジスタ }

とすることで、メモリアクセスの1つをレジスタ読み出しに置き換えることができます。それでも、レジスタ読み出しにはまずレジスタ番号が確定する必要があるので、命令読み出しと同時には実行できません。実際には、以下の4つの段階に分けて命令が実行されます。

  1. 命令読み出し
  2. レジスタ読み出し
  3. 足し算の計算
  4. 結果の書き込み

現在多くのプロセッサは、これらの段階をパイプライン処理で同時に実行して、高性能をたたき出しています。しかし、実際にこの方法で性能を発揮させるには様々な工夫が必要であり、現在のプロセッサの複雑化の一因となっています。

結局、レジスタを使ったとしても、命令実行のためのパラメータ(足し算の場合は+の左辺と右辺)が命令読み出しと同時に確定しない限り、こういった問題はどうしても避けられません。手立てはただ一つ、命令のパラメータを固定値(イミディエイト/即値)に限定することです。足し算命令の場合はこうなります。

ADD 固定値 { Acc ← Acc + 固定値 }

命令とパラメータを同時に読み出せれば、メモリアクセスは1回で済みます。その後すぐに足し算ができますので、基本的に複雑なパイプラインは不要となります。もちろん、パラメータが固定値だけではプロセッサとして使い物になりませんが、これについてはコードの書き換えを用いることで解決が可能です。具体的には、パラメータで指定されたアドレスにある命令のパラメータを、現在のアキュムレータの値に書き換えるPUTという命令を用意します。

PUT 命令アドレス { [命令アドレス] ← Acc }

PUT命令自体の命令アドレス指定も他のPUT命令で書き換えられるので、配列や可変アドレスでも大丈夫です。この命令があることで、あらゆる命令のパラメータを、事実上の汎用レジスタとして利用することが可能になります。

コードの自己書き換えは、現在のプロセッサでは特別な場合を除いてほとんど使われないテクニックです。セキュリティやメモリ保護の問題もありますが、何よりもキャッシュメモリやパイプライン処理において、コード書き換えの処理が非常に効率が悪いというのが理由だと考えられます。このように現在ではすっかり主流から外れてしまった自己書き換えという手法を、レジスタレスアーキテクチャでは積極的に利用します。

まとめると、レジスタレスアーキテクチャは、アキュムレータマシンと自己書き換えという、現在主流から外れてしまったテクニックを、小規模マルチプロセッサの要素として復活させたものになります。

(つづく)

金曜日, 1月 18, 2013

マルチプロセッサとメモリアクセス

近年、ノートPCや携帯機器までマルチコアが普通に載るようになってきて、今ではスマートフォンにまでクアッドコア(4CPU)が登場するようになりました。だったらもっと載せればいいじゃないという話になりそうですが、実際はそう簡単にはいきません。

世の中のプログラムは必ずしも並列化できない(アムダールの法則)というのもありますが、もし並列化できたとしても、メモリアクセスの限界(フォン・ノイマン・ボトルネック)で、どうしても性能が抑えられてしまいます。CPUが計算するためには、まずメモリからプログラムとデータを読んで、計算結果をメモリに書き込む必要があります。CPUだけがどんなに速くても、メモリアクセスが間に合わなければ、全く意味がないわけです。

では、現在のマルチプロセッサはどのようにしてこの問題に対処しているのでしょうか?

cache_memory.png

現在、世の中に出回っている大部分のマルチコアプロセッサが、基本的にはこのような分散キャッシュメモリを持つ構成になります。この構成では各プロセッサは必要となるプログラムやデータの一部分をキャッシュメモリにコピーして使うので、共有メモリへのアクセスは格段に減ります。マルチコアプロセッサでは通常、キャッシュメモリと共有バスは外部の共有メモリよりも遥かに高速で動作しますので、ちゃんとプロセッサが増えただけの性能を発揮することができます。

キャッシュメモリの容量はプロセッサの性能に直結するので、今やプロセッサの中で最も電力と面積を必要とする部品となっています。キャッシュメモリは元々、外部メモリとプロセッサ内部の速度差を埋める役割を果たしていたのですが、マルチコアになるとキャッシュ一貫性によりメモリの同時アクセスのために欠かせない機構となりました。でも、メモリを内蔵する構成のプロセッサにおいては、当然ながら無用の長物です。

では、メモリを内蔵する小規模なマルチプロセッサは、どのような構成にすればよいでしょうか? まず考えられるのは、力技です。

shared_memory.png

マルチポートメモリを利用することにより、共有メモリとプロセッサを直結することが理論的には可能です。ですが、面積効率が悪く(ポート数の2乗に比例するといわれている)、またFPGAでは利用できないため(大抵は2ポートまで)、この方式は却下となります。

もう一つ考えられるのは共有メモリを諦めてプロセッサ毎に専用のメモリを持たせ、代わりに通信用のバスを用意する方式です。ハードウェアの設計が楽なのでこの構成は比較的よく見られ、例えばPS3Cellプロセッサはこれに近い構造となっています。

local_memory.png

これはハードウェア設計は楽なのですが、ソフトウェアの方は大変です。特に小規模の場合、ただでさえ少ないメインメモリが、プロセッサ毎に分割されてしまうのは論外といえます。無理やり他のプロセッサのメモリをアクセスする構成も考えられなくはないですが、プロセッサに負荷が掛かる分、複雑な割に効率が悪そうです。

共有メモリの分割をプロセッサ毎ではなく、アドレス領域に応じたメモリバンク分割にすることにより、また違った構成が考えられます。

crossbar_switch.png

クロスバースイッチにより、複数のプロセッサが別々のメモリバンクにアクセスする限りにおいて、同時アクセスが可能となります。ただし、複数のプロセッサが同一のバンクにアクセスしようとした場合には、一つを除いて他のアクセスがブロックされます。プロセッサが増えるとクロスバー機構が急激に肥大化するのが問題ですが、それをうまく簡略化するための研究が昔から盛んなようです。(例えばこことか)

もしここで、アドレスをプロセッサ数で割った余りでメモリバンク分割すると、面白いことが起きます。例えば上の図でメモリバンク0~3を、アドレスを4で割った余りが0から3の領域のメモリとします。もしある時点で4つのプロセッサの実行アドレスが100, 201, 302, 403とすると、それぞれ4で割った余りは0, 1, 2, 3となるので同時に別々のメモリから命令読み出しアクセスができます。次の時点では実行アドレスが101, 202, 303, 404となるので余りは1, 2, 3, 0となり、また同時アクセスが可能です。こうして、ジャンプ命令を実行しない限り、4つのプロセッサは同時アクセス可能な状態を維持し続けます。

この状況で、プロセッサ自体を回転させることができれば、クロスバー機構がまったく要らなくなります。回転といってもIC内部で物理的に回転させるわけにはいかないので、実際には全ての内部状態を信号としてつないで転送することになります。

cyclic_smp.png

プロセッサの内部状態というと、当然全てのレジスタの値も含まれますので、レジスタがたくさんある場合にはそれも全部隣に送らないといけません。つまりこの場合、レジスタが少なければ少ないほど有利なわけです。

ここで、レジスタレスアーキテクチャによる小規模マルチプロセッサの可能性が浮かび上がってきます。

(つづく)