2012年06月28日

USBコントローラの設計

USBコントローラを設計してみたい。-- これは、FPGA を始めたときから思っていたこと。そろそろ、それが出来るレベルになったかも。少なくともどうやって設計すべきなのか見当も付かなかったのが、今は見当ぐらいは付くようになった。

最初は、規模が小さい MachXO2-256 で作れるものとして検討してみよう。もちろん、規模が小さいわけだから、一般的なコントローラの機能は作れない。では何を作るのか?

    レベル 0)
    パケットを送出し、受け取る。そういうものならまずは可能だろう。ホストから来た 0/1 のシーケンスをそのまま送るのは、難しくなさそうだ。

    受信を考えると、まず相手のクロックに同期させなければならない。4 倍クロックを使って、最初の 信号の立ち上がりに合わせようかと思う。ここのところは、少々難しそうだが、USART などと似た様なものだから、それほどでもないだろう。

      パケットの開始(SOP)は、送信側からみると、入力状態から、アイドル状態と同じ値を一旦出力することらしい。アイドル状態は、出力しない状態で Low-Speed では、D- がプルアップされているわけだから D- = H , D+ = L 。Full-Speed では、逆になる。

      続いて SYNC データを送出する。データは LSB first で 0x80 。これを NRZI (Non Return Zero Invert) エンコードすると H-L-H-L-H-L-H-H (Low-Speed での D+/Full-Speed D-)。 最初に L → H と変わるから ここを拾って 受信クロックを同期させるわけだ。 クロックが同期してなければ、1 つ遅延させる。 6 回チャンスがあるから、最後の H-H までに同期するはず。

      パケットの終了(EOP)は D- , D+ 共に L 出力 x 2クロックの後 、アイドル状態と同じ値を一旦出力して、入力状態。受信では、D- , D+ 共に Lを検出したら、1 クロック待って 受信を終了させれば良い。

    あと難しそうなのが、FIFO が必須ということ。途切れなく 送受信しなければならないので、データが来たからといって、すぐに 送信すれば良いものでもない。受信では、FULL にならないようにする配慮も必要。

    ちなみに、Full-Speed での最大パケットサイズは、512 Bytes だったと思う。MachXO2-256では、合計で 64B ぐらいしか取れそうにない。なかなか考えどころ。

    レベル1)
    USB では、NRZI (Non Return Zero Invert) エンコーディング というのを行なっている。レベル1では、エンコード/デコードする 。後で説明するが、NRZI を 論理レベルに変換すると、MCU 側の処理が楽になる。コントローラ側でも 送受信の単位が バイト単位になって FIFO まわりが少し楽になる。

    レベル2)
    パケットの形式を知った上で、いくつかの機能をサポート。パケット ID(PID) が正しいかどうかの検出や、CRC のチェック/生成など。

    このレベルより高度なものは規模的に無理。このレベルでも怪しい。一応目標にはするが、レベル1になるかも。

さて、こういうものを作るとして、MCU との 送受信インターフェイスはどうしよう。FIFO を使うから基本は、8bit パラレル。

ただ、パラレルでは、ピンを消費する上に AVR なんかだと SPI より遅くなる。やはり SPI かと考えている。まぁこれは、出来た後でも良い。

ところで、USB PHY のインターフェイスは、UTMI というものが定義されているらしい。SMSC では、これのピン数を減らした ULPI( UTMI + Low Pin Interface) の PHY を製品化している。これに合わせることは、考えていないが、出来た後、再度検討してみよう。まずは、上記のものを設計しなければ。

USBでの信号の扱い

    基本は、差動信号だが、上で出てきたように例外がある。(両方 L レベル)

    FPGA は一般に 差動出力も持っているのだが、これをサポートするため、シングルエンド x 2 で操作すれば良いだろう。受信側も 同じ事情。差動入力もできるが、シングルエンドも必要。FPGA は 1つのポートで 複数の入力の仕方ができるものなのだろうか? その上 出力にも切り替えられないといけないのだが ...

    多分無理だと考えて、差動入力をパラレルに接続することにしよう。ポートが勿体なければ、シングルエンドで 値を読むことにしても良い。

    さて、デバイスとして構成するなら 1.5K のプルアップが必要だ。ポートが余っていれば 2つのポートに抵抗を付けることで、設定可能にしても良いだろう。ホスト側は、15K のプルダウン。これは、FPGA のプルダウン機能でいけそう。

    こういうことなので、最大は D-,D+ にそれぞれ 3 つづつ 計 6 個のポートが必要。

LRZI について

    簡単に説明してしまうと 論理 0 出力は反転、論理 1 出力は 状態保持。
    そして、同じ状態が 6 クロック続くと 次に 0 を挿入する。-- 要するに論理 11111 は、0 または 1 の状態が 6 クロック続くので 0 が挿入される。

    Full-Speed は、物理的な差動出力が逆になるわけだが、受信側から見ると、同じ値になる。初期状態が違うだけで、反転-保持の関係は変わらないからだ。

以上の理解で、レベル1は設計できるはず。さあ設計してみよう。

クロックジェネレータ

    48 MHz を入力して、12 MHz を作る。Low-Speed の場合は、6MHz 入力で 1.5 MHz を作る。

    PLL のない MachXO2-256 で 48 MHz をどうするのか? という問題があるが、とりあえず考えない。あとクロックは、送信中、受信中のみ 生成することにしよう。

    module udrv_clkgen (
    input CLK_4X
    , input DP
    , input SYNC_REQ
    , output SYNC_ACK
    , output CLK
    );
    reg [1:0] clk_ph = 0;
    reg r_dp = 1'b0;
    reg r_done = 1'b0;

    assign CLK = clk_ph[1];
    assign SYNC_ACK = r_done;

    always @(posedge CLK_4X)
    begin
    r_dp <= DP;
    if (~SYNC_REQ | ~(r_dp ^ DP) | (clk_ph == 3) )
    clk_ph <= clk_ph + 1;
    if (~SYNC_REQ)
    r_done <= 1'b0;
    else if ((r_dp ^ DP) & (clk_ph == 3))
    r_done <= 1'b1;
    clk_ph <= clk_ph + 1;
    end
    endmodule

    // _______
    // CLK |_______|
    //
    // clk_ph 3 | 0 | 1 | 2 | 3 | 0
    // _____________
    // DP >< ><
    //
    // _________
    // SYNC_ACK __|
    //

    SYNC_REQ は、上位モジュールからの受信要求で、SYNC_ACK が 1 になると 受信準備が出来たということで、受信モジュールを enable するつもり。

    (clk_ph == 3) 以外で DP が変化すると 1/4 クロック遅らせる。OK になれば、SYNC_ACK が 1 になるので、(上位モジュールが) SYNC_REQ を 0 にする。

受信モジュール

    DP 以外に SE0 という入力信号を作ることにした。これは、DP-,DP+ を シングルエンドとして見て共に 0 のとき 1 にする。RECV_EN で受信開始だが、SYNC の途中なので、1 - 1 または 0 - 0 が来た次から 採取開始。データは、FIFO に入れることにする。FIFO が溢れても関知しない。

    module udrv_reciever (
    input CLK
    , input RECV_EN
    , input DP
    , input SE0
    , output [7:0] RECV_DATA
    , output RECV_DATA_RDY
    , input RECV_DATA_ACK
    );

    reg recv_stat = 0;
    reg r_dp = 0;
    reg [7:0] data_out;
    reg data_rdy = 0;
    reg [7:0] r_data;
    reg [2:0] data_count = 0;
    reg [2:0] one_count = 0;

    assign RECV_DATA[7:0] = r_data[7:0];
    assign RECV_DATA_RDY = data_rdy;

    always @(posedge CLK)
    begin
    r_dp <= DP;
    if (~RECV_EN | SE0)
    begin
    recv_stat <= 0;
    data_count <= 0;
    r_data <= 0;
    one_count <= 0;
    end
    else if (recv_stat == 0)
    begin
    if (~(r_dp ^ DP)) // end of SYNC
    recv_stat <= 1;
    end
    else
    begin
    if (~(r_dp ^ DP)) one_count <= one_count + 1;
    else one_count <= 0;

    if (one_count <= 5)
    begin
    r_data <= { ~(r_dp ^ DP), r_data[7:1] };
    data_count <= data_count + 1;

    if (data_count == 7)
    data_out <= { ~(r_dp ^ DP), r_data[7:1] };
    end
    end

    if ( RECV_EN & ~SE0 & (recv_stat == 1)
    & (one_count <= 5) & (data_count == 7) )
    data_rdy <= 1'b1;
    else if (RECV_DATA_ACK)
    data_rdy <= 1'b0;

    end
    endmodule

送信モジュール

    SEND_EN のとき送信開始。まず SYNC を送出し、次からデータを送る。データが途切れると送信終了。
    送信側は、シングルエンドとしてのポート状態を生成する。ちょっと変なのだが、(SEND_DONE != 0) のとき DP,DM を出力することを想定している。

    一応 受信モジュールとのペアで、パラレルの通信ができるようにした。送信が途切れると パケットを終了し、再開すると自動的に SYNC が付く。CRC などはないから、再送できないので実用的ではないが、ロジックのデバッグには役立ちそうだ。

    send_stat は、6 状態になった。シーケンスがあるので、そんなものだろう。

      send_stat = 0 : アイドル
      send_stat = 1 :
      send_stat = 2 : SYNC とデータ
      send_stat = 3 : SOP 1 (SE0)
      send_stat = 4 : SOP 2 (SE0)
      send_stat = 5 : SOP 3

    module sender (
    input CLK
    , input SEND_EN
    , output SEND_DONE
    , output DP
    , output DM
    , input [7:0] SEND_DATA
    , input SEND_DATA_RDY
    , output SEND_DATA_ACK
    );

    reg r_dp;
    reg r_dm;
    reg [2:0] send_stat = 0;
    reg r_out = 0;
    reg [6:0] data_in;
    reg data_ack = 0;
    reg [7:0] data_count = 0;
    reg [7:0] one_count = 0;

    assign DP = r_dp;
    assign DM = r_dm;
    assign SEND_DONE = (send_stat == 0);
    assign SEND_DATA_ACK = data_ack;

    always @(posedge CLK)
    begin
    if (~SEND_EN)
    begin
    send_stat <= 0;
    end
    if ( (send_stat == 0) & SEND_EN )
    begin
    send_stat <= 1;
    data_count <= 7;
    data_in[7:0] <= 7'b1000000;
    r_out <= 1'b0;
    end
    else if (send_stat == 1)
    send_stat <= 2;
    else if ( (send_stat == 2) & (one_count <= 5) & (data_count == 0) )
    if (SEND_DATA_RDY)
    begin
    data_count <= 7;
    data_in[6:0] <= SEND_DATA[7:1];
    r_out <= SEND_DATA[0];
    end
    else // exit sending
    send_stat <= 3;
    else if ( (send_stat == 2) & (one_count <= 5) )
    begin
    data_count <= data_count - 1;
    data_in[6:0] <= { 1'b0, data_in[6:1] };
    r_out <= data_in[0];
    end
    else if (send_stat == 3) send_stat <= 4;
    else if (send_stat == 4) send_stat <= 5;
    else if (send_stat == 5) send_stat <= 0;

    if ( (send_stat == 3) | (send_stat == 4) ) // SE0
    begin
    one_count <= 0;
    r_dp <= 1'b0;
    r_dm <= 1'b0;
    end
    else if (send_stat == 2)
    begin
    if ( (~r_out) | (one_count > 5) )
    begin
    one_count <= 0;
    r_dp <= ~r_dp;
    r_dm <= r_dp;
    end
    else one_count <= one_count + 1;
    end
    else // if ( (send_stat == 1) | (send_stat == 5) )
    begin
    one_count <= 0;
    r_dp <= (FS) ? 1'b1 : 1'b0;
    r_dm <= (FS) ? 1'b0 : 1'b1;
    end

    data_ack <= SEND_EN & SEND_DATA_RDY & ( (send_stat == 1)
    & (one_count <= 5) & (data_count == 0) );
    end
    endmodule

FIFOモジュール

    FIFOモジュールも自分で作る。Dual PORT の分散RAM ベース。32 バイトで 30 スライス 、64 バイトだと 50 スライス。たぶん 32 バイトしか選べない。

      32 バイト増えて 20 スライス増えている。2 バイトあたり 1 スライス だから 16bit x 1bit が 2 LUT の計算。Dual PORT ならこの値が適切。

      送受信は、同時には行わない。FIFO を 2 つ持つのは、もったいないので可能なら共用にしたい。

    module udrv_fifo # (
    parameter DEPTH = 5
    // parameter DEPTH = 6
    ) (
    input CLK
    , input RST
    , input [7:0] I_DATA
    , input I_DATA_RDY
    , output I_DATA_ACK
    , output I_DATA_FULL
    , output [7:0] O_DATA
    , output O_DATA_RDY
    , input O_DATA_ACK
    );
    reg [5:0] mem [0:2**DEPTH-1];

    reg [DEPTH-1:0] i_addr = 0;
    reg [DEPTH-1:0] o_addr = 0;
    wire [DEPTH-1:0] i_next = i_addr + 1;
    wire [DEPTH-1:0] o_next = o_addr + 1;
    reg i_ack = 0;
    reg o_rdy = 0;
    assign I_DATA_FULL = (i_addr != o_next);
    assign I_DATA_ACK = i_ack;
    assign O_DATA_RDY = o_rdy;

    always @(negedge CLK)
    begin
    if (RST)
    begin
    i_addr <= 0;
    o_addr <= 0;
    i_ack <= 1'b0;
    o_rdy <= 1'b0;
    end
    else if (I_DATA_RDY & ~i_ack)
    begin
    if (~I_DATA_FULL)
    begin
    i_addr <= i_next;
    mem[i_next] <= I_DATA;
    end
    i_ack <= 1'b1;
    end
    else
    i_ack <= 1'b0;

    if ( O_DATA_ACK & (i_addr != o_next))
    begin
    o_addr <= o_next;
    o_rdy <= 1'b1;
    end
    else if (i_addr != o_next)
    o_rdy <= 1'b1;
    else if ( O_DATA_ACK )
    o_rdy <= 1'b0;
    end
    assign O_DATA = mem[o_addr];

    endmodule

    以上が今回作ったもの。シミュレーションとかデバッグは未だ。あとインターフェイスに SPI を付けなければならない。そうしないとピンが多すぎて、QFN32 に収まらないのだ。

    で、専用の SPI モジュールを作ったのだが、問題がある。パケットより小さな FIFO で、Full-Speed に対応するには、12 M bps 前後で通信できないといけない。なかなかに厳しい条件だが、それは置いておく。問題は、SPI の SCK が CLK より早い場合にも対応しないといけないところ。FIFO との ハンドシェークが鍵なのだが、うまく行っていないかも。

    とりあえず QFN32 に収めるために、8bit データを inout に変更するのが良さそう。

    ところで、作ったは良いがどうやって使うのか? ... V-USB のコードを改造すればうまく制御できるのではないかと思っている。V-USB のなかで見たくもない部分は、通信のところで、アセンブラになっている。これをまるごと置き換えてしまえば 良さそうなのだ。ただ、Full-Speed に対応するには、48 MHz が必要になってしまう。Low-Speed で良いなら V-USB で良いし。結局のところ 規模の大きいFPGA のコアで使うぐらいか。

    なかなか使いドコロがないが、CRC のコードも作りたいし、それだけ追加してみて、シミュレータでチェックして、ひとまずの完成としよう。

    この後、送信をシミュレータでデバッグした。


    Full-Speed(FS) の例。FS では、D+(DP) がプルアップされているわけだから DP = H がアイドル。Hi-Z から DP = H なって、LHLHLHLL と SYNC コードを送出している。次に 0x33 を送るのだが、 ちゃんと LLHLLLHL となっている。

    終わりは、DP/DM 共に L を 2 クロックで、次に アイドルと同じ DP = H で最後に Hi-Z 。

    結構バグがあったが、なんとかデータを送ってくれるようになった。上で貼り付けたコードは、バグがあるままなので注意。正しいのは、最新のソースコードのみである。

      送信でインターフェイスを若干変更。ひとつは、送信STARTを指示するフラグ。FIFO にパケットを全部送ってから、送信というのが妥当だと思えたので。-- 以前に AT90USB162 のコードを作ったが 最大パケットサイズは、32 か 64 にしか出来なかった。それで十分でもあった。その程度ならば FIFOに全部入るのだ。

      もうひとつは、DP/DM を出力にするのを指示するフラグ。SEND 中というフラグで代用しようと思っていたのだが 1 クロックずれる。

    次は受信をデバッグしよう。送信データを受信側とつなぐのだが、CLK がずれるので、ちょっと工夫がいる。


    受信も一応動かせた。受信要求を出すと SYNC_REQ が 1 になって、CLK をずらす。確かに 180°ずれて SYNC が完了しているのだが、SYNC_REQ が ON/OFF を繰り返していて、ぎりぎりで 受信開始になっている。... まずそう。
    それは、おいおい直すとして ... 今回は、送信用、受信用 2 つのモジュールを接続したのだが、pull-up/pull-down が分からなかったので論理レベルで接続することにした。このため、LB_TEST という define を追加。


    0x33,0x34,0x35,0x36 を送信しているのだが、その 4つをちゃんと受け取って、受信終了。その後、RECV_REQ が ON/OFF を繰り返している。(受信前は、SYNC_REQ)。一応動きはしたが、やっぱりまずそうだ。

CRC について(基礎編)

    まずは基礎から。

    USBでは、CRC-5 と CRC-16 の2通りの CRC を使っている。CRC-5 は SYNC 込み 32bit のパケット専用で、それより大きいパケットでは CRC-16 を使う。CRC-16 と言っても、計算式はひと通りしかないわけではないらしい。USB での方法を知らないといけないのだ。

    (16bit) x^15 + x^14 + x^2 + 1
    (5bit) x^5 + x^2 + 1

    生成多項式にはこれを使う。初期値には all 1 を使い、出力は ビット反転した上で LSB と MSB も反転。

    コードで示した方が早そうだ。

    CRC-16:
    unsigned int r_crc = 0xffff;
    unsigned int In;
    for (i=0; i< XX; i++) {
    w = (r_crc ^ In) & 1;
    r_crc = (r_crc >> 1) & 0x7fff;
    if (w) {
    r_crc ^= (1<<0);
    r_crc ^= (1<<13);
    r_crc ^= (1<<15); // w
    }
    In = (In >> 1);
    printf("%04x r %04x\n", r_crc, ~r_crc & 0xffff);
    }
    CRC-5:
    unsigned int r_crc = 0x1f;
    unsigned int In;

    for (i=0; i< XX; i++) {
    w = (r_crc ^ In) & 1;
    r_crc = (r_crc >> 1) & 0xf;
    if (w) {
    r_crc ^= (1<<4);
    r_crc ^= (1<<2);
    }

    In = (In >> 1);
    printf("%02x r %02x\n", r_crc, ~r_crc & 0x1f);
    }

    出力は、~r_crc (bit反転したもの)なので注意。

    さて、受信で CRC値が含まれているが、これも含めて CRC 計算してしまうと ... 特定の値になる。CRC-5 では 5'b11001 , CRC-16 では 16'h4FFE 。(bit反転前の値(r_crc) なので注意)

CRC について(応用編)

    応用編というより実装編。

    module udrv_crc # (
    parameter WIDTH=16
    // parameter WIDTH=5
    , parameter SEQUENTIAL_MATCHING=0
    ) (
    input CLK
    , input [1:0] SEL
    , input I // LSB first
    , output O // LSB first
    , output MATCH
    , output [WIDTH-1:0] CRC
    );

    SEL で 動作を指定するのだが、基本的に 2 つのモードがある。ひとつは、01 で CRC 計算。もうひとつは、10 で、計算結果をビットストリームにして出力 -- 送信などで使う。
    00 は、なにもしない。-- USB では、0 が挿入される場合があるから、これが必要なのだ。あと、11 は、初期値にする 。
    パラレルの CRC は、シミュレータのチェック用で使わないのが基本。

    同時に CRC の一致検出 もつけた。やりかたには 2 通りある。ひとつは、計算結果を出力して、入力と比べるというやりかた(SEQUENTIAL_MATCHING=1)。もうひとつは、最後まで計算して特定の値になったかどうか チェックするやりかた(SEQUENTIAL_MATCHING=0)。計算結果を出力してしまうと、以降計算はできないので両立はできない。

    (SEQUENTIAL_MATCHING=0) は、受信での CRC-16 チェックに使う。USB ではパケットを受け取り終わってはじめて、2 バイト前が CRC だったと分かる。だから、これを使う以外にはない。

    (SEQUENTIAL_MATCHING=1) は、CRC-5 に使おうかと思っている。

    reg [WIDTH-1:0] r_crc = (-1);
    wire I2 = (r_crc[0] ^ I);
    assign CRC [WIDTH-1:0] = ~r_crc[WIDTH-1:0] ;
    assign O = ~r_crc[0];
    reg r_match = 0;
    assign MATCH = (SEQUENTIAL_MATCHING) ? r_match
    : (WIDTH == 5) ? ( ~r_crc == 5'b11001 )
    : (WIDTH == 16) ? ( ~r_crc == 16'h4FFE )
    : 0;

    always @(negedge CLK)
    begin
    if ( SEL == 2'b11 ) // RST
    begin
    r_crc <= (-1);
    r_match <= 1;
    end
    else if ( SEL == 2'b10 ) // Extract bitstream
    begin
    r_match <= (I ^ r_crc[0]) & r_match;
    r_crc <= { 1'b0, r_crc[WIDTH-1:1] };
    end
    else if ( SEL == 2'b01 ) // Calc.
    if (WIDTH == 16)
    r_crc <= { I2 , r_crc[15]
    , ( r_crc[14] ^ I2) , r_crc[13:2]
    , ( r_crc[ 1] ^ I2) } ;
    else if (WIDTH == 5)
    r_crc <= { I2, r_crc[4]
    , ( r_crc[ 3] ^ I2), r_crc[2:1] };
    end
    endmodule

    実装はこれ。短いとみると長いとみるか...

    これを 送受信に組み込むことは成功した。送信・受信でモジュールを兼用。-- わずか 3 スライスほどだが、節約できた。合計の規模は、110 になった。ただし、FIFO のサイズを 32 バイト にしたときの話。これで FIFO のサイズを 64 バイトにするのは無理になった。

    実は、ピンの数の方が切実。JTAGENB を使うようにすれば、21 pin 使えるのだが、既に 19 pin 使っている。受信では CRC エラーの出力でさらに 1pin 増えた。送信では、CRC を生成するかどうかの 設定を 2 種類入れたかったのだが、無理なので自動で 生成することにした。

      CRC-5 は、計算結果を入れるかどうかの判断をするタイミング(3バイト目 残り 5bit)で、次のデータがあるかどうか を見て なければ、入れる。そこできちんと終わらせないといけない。次のデータがあれば CRC-16 を入れることにする。( データを受け取り終わったら CRC-16 の 2 バイトが挿入される。)

    こういうロジックを入れて 110 スライスなのだ。CRC をサポートせず生のデータを送信・受信するなら 79 スライスになった。31 スライスも使っているが、CRC 自体は小さい。制御が大変なのだ。

    さて、入れたいメカニズムは入ったのだが ... 入出力をどうするのか まだ。
    SPI はひとつの案だが、詳細は詰めないと。それに今の 入出力 。これも一般的な仕様にする必要がある。

    ところで、48 MHz の件、 『クリスタルオシレータ(48MHz) SG-8002DC(3.3V)』 -- PLL なしだとこれを使うしかないようだ。

入出力のハンドシェーク

    これまで、全部が CLK に同期して動いていた。だが、別クロック系統のものと接続するには、具合が悪い。パラレルで MCU と接続する場合も今のままではダメだし、SPI と接続する場合も同様。

    まずは、パラレルのところの整備から始める。

    // ______________________ _______
    // DATA <______________________><_______ // // _______ _____ // RDY ________| |______________| // // ________ // ACK _____________| |________________ // // // CLK | | | | | //

    今考えているのを図にしてみた。
     1. ACK が 0 のとき DATA をセットできる。セットしたら RDY を 0 にする。
     2. DATA が確定したので、RDY を 1 にする。
     3. 受け取る方は、受信後、ACK を 1 にする。
    4. ACK が 1 になったら、RDY を 0 にする。
     5. 受け取る方は、RDY が 0 になったら ACK を 0 にする。
     最短 3 クロック。

    このルールにしたがうようにまず変更した。規模は 110 で変わらない。

    さて、BUS を使うなら、一般的なデバイス -- RD/WR で制御できるもの と同じようにしておきたい。ついでにコントロールレジスタも付けて ピンも減らしたい。

    RDについては、

    assign RECV_DATA_ACK = ~RD;

    で制御できる。データがあれば、ACK の前後で 安定している。なければ、前のデータが 読めるだけ。RD が H になった後 次のデータがセットされるので、読み込みサイクルが早すぎるとマズイ。なんなら FIFO のクロックを 4倍速にしたり マスター側に合わせて良い。それが出来るように ハンドシェークを見直したのだ。

    RECV_DATA_RDY は、BUS に接続するなら コントロールレジスタから読めると便利。だが、AVR などでポート制御するなら、ピンに出しておいた方が良い。

    assign DB[7:0] = (~RD & ~RS) ? RECV_DATA[7:0]
    : (~RD & RS) ? CTRL_REG[7:0]
    : 8'bz;
    assign SEND_DATA[7:0] = DB[7:0];

    出力制御は、こんな風にする。

    次に WRのほう。

    assign SEND_DATA_RDY = ~WR;

    WR を L にしたときデータが安定しているという条件なら、これでも良い。ACK は無視。勝手に L→ H→ L になる。不安なら、WR を遅延させるのも良いかも知れない。

    ここまで作ったところ 115 スライスになった。わずかな増加だが、これを元に SPI を付けるのだ。残り はわずか 13 スライス。

SPI インターフェイス

    これで、 SCK が CLK より早かろうが遅かろうが問題なくなった。いよいよ最後の機能として SPI を付ける。

    module udrv_spi (
    input MOSI
    , input SCK
    , input CS
    , output MISO

    , input [7:0] SPI_DATA_I
    , output SPI_DATA_I_ACK
    , input SPI_DATA_I_RDY

    , output [7:0] SPI_DATA
    , output SPI_DATA_RDY
    , input SPI_DATA_ACK
    );

    モジュール単体は、こう。SPI を パラレルに変換しているだけ。CLK もない。
    続いて本体

    reg [3:0] r_count = 0;
    reg [7:0] r_spi;
    reg [7:0] spi_out;
    reg r_mosi = 1'b0;

    reg o_data_ack = 1'b0;
    reg o_data_set = 1'b0;
    reg o_data_rdy = 1'b0;
    assign SPI_DATA_RDY = o_data_rdy;

    reg i_data_ack = 1'b0;
    assign SPI_DATA_I_ACK = i_data_ack;

    assign SPI_DATA = spi_out[7:0];
    assign MISO = r_spi[7];

    always @(posedge SCK)
    begin
    r_mosi <= MOSI;
    if (r_count == 1)
    i_data_ack <= 1'b1;
    else if (~SPI_DATA_I_RDY)
    i_data_ack <= 1'b0;
    end

    always @(CS , negedge SCK)
    begin
    if (CS) // LOAD
    begin
    r_count <= (-1);
    r_spi <= SPI_DATA_I;
    end
    else if (r_count == 7) // LOAD
    begin
    r_count <= 0;
    r_spi <= SPI_DATA_I;
    spi_out <= {r_spi[6:0], r_mosi };
    end
    else // SHIFT
    begin
    r_count <= r_count + 1;
    r_spi <= {r_spi[6:0], r_mosi };
    end

    o_data_ack <= SPI_DATA_ACK;
    o_data_set <= (~CS & (r_count == 7));
    if (o_data_set)
    o_data_rdy <= 1'b1;
    else if (~o_data_ack & SPI_DATA_ACK)
    o_data_rdy <= 1'b0;
    end
    endmodule

    悩ましいのは、動作のタイミングが足りないところ。

      通常、最後のタイミングは、negedge SCK で (r_count == 7) 。ここでデータ自体は受け取れる。o_data_set を 1 にすることも出来る。だが、o_data_rdy を 1 に出来ない。
      CS の変化も条件に入れてようやく 1 にできる。ただし、0 には出来ない。

      これでもシミュレータでは動く。だが、always @(CS , negedge SCK) という記述を期待通りにやってくれるかどうか? 不安だったりする。前に Xilinx で試したときは、ダメだったような ...

    それも問題だが、上位レイヤーでどう使うか?

    コントロールレジスタをサポートしたいのだが、どういうプロトコルにするか ... RS が邪魔なのだ。まぁ、余裕もなさそうだから、ピンから入力することにしよう。

    送信の場合、RS=0 にして、パケット分のデータを 送り、RS=1 にして、コントロールレジスタに書き込むことで、SEND_START を 1 にする 。FIFO が空になれば、一定時間後に 送信完了。

    受信が問題。通常は受信モード。FIFO が空でなければ、受信中。一定時間後に 受信は完了する。... なんだか苦しいが、ピンもロジックも足りない。内部ロジックには、送信中、受信中というのがあるので、コントロールレジスタをポーリングすれば、分かるようにしておこう。

    こんなところか。で、これを実際に作りこむと .. 124 スライスにまでなった。

      128 スライスなど小さいと思っていたのだが、udrv.v は、なんと 859 行もある。コメントはそれなりにはあるが、コード自体に冗長な部分はない。メモリを使わずにコードだけで埋めるのは結構しんどい。



    シミュレータで、送信側を確認。

      シミュレータでは、negedge CS , posedge CS が動作ポイントに入るのは確認できた。
      r_count は、4bit 。最初に negedge CS で動いたときに 7 にするのは まずい。F にしている。
      negedge CS で F それから 0 1 ... 7 でデータ出力。最後は、negedge SCK で データ出力できている。posedge CS でかろうじて o_data_rdy を 1 。これでデータは受け取ってもらえる。ACK は半端な状態だが、次に送信するときに、 続きから処理される。

      o_data_rdy を 1 クロック早めるのは、データを確実に渡せない気がする。ただ、FIFO 側で 遅延して受け取るような変更なら出来る。決めたルールに例外を持ち込むことになるが、このようなケースなら止むを得ないか。

      追記: 間違っていた。


      always @(CS , negedge SCK)
      begin
      if (CS) // LOAD
      begin
      r_count <= (-1);
      r_spi <= SPI_DATA_I;

      この部分なのだが、先頭の negedge CS で条件が成立していない。
      受信の動作確認で分かった。

      reg r_cs = 0;
      always @(CS or negedge SCK)
      begin
      r_cs <= CS;
      if (r_cs) // LOAD
      begin
      r_count <= 0;
      r_spi <= SPI_DATA_I;

      こう変えたら動いたのだが、どうも釈然としない。 negedge SCK だけの場合は、変更前の SCK が見えたのに、こう書くと変更後?



      受信しているところ。送信側も SPI だから両方ちゃんと動いている。

      FIFO に遅延読み込みのパラメータを付け o_data_rdy を 1 SCK クロック 前にした。

    入れたい機能は入れた。QFN32 の 128 スライスという条件をほぼフルに使った。もう満足だったりする。随分と勉強にはなったが、この後どうしよう?

    Full-Speed/Low-Speed の切り替えを付けたい。あと差動入力を使うオプション。だが、これ以上のロジック追加は無理だろう。今回は、MachXO2-256 でどこまで出来るか? というテーマなので、いずれ別の機会にしよう。

      思い出したので、メモ。
      FIFO は、Dual-Port の RAM を使っている。いま想定している使い方だとパケットを受け取り終わってから、READ することになりそう。送信の場合も同じ。なら Single-Port でも良さそうな気がする。制御ロジックが似た様なものなら 2 倍の容量が使える。FIFO まわりを差し替えるだけで済むなら検討してみたい。
       ... やってみたが、却って規模が増えた。.. 失敗。

      あと FIFO は、同期 FIFO を非同期で使えるようにしている。非同期 FIFO というものもある。それと差し替えるとどうなるか試してみたい。 (参考: 『FPGAの部屋:同期FIFOと非同期FIFO』)

    残っているものとしては、JTAG 通信と ADC 。DAC は一応 設計してみたのだから、ADC もその程度のものはやりたい。

検討もれ

    ひといきつきたいのだが、検討もれがあるかないかが気になっている。マズそうなものをリストアップしていこう。ただし、すぐには直さないかも。

    (EOP だけのパケット)

      Low-Speed では、これがあるそうだ。

      まずこれを送るインターフェイスがない。送信では、SYNC が自動で付加される。0 バイトを送出することもできない。最短は、SYNC - PID - EOP 。

      受信でも検出できない。だけでなく、マズイケースがありそうだ。受信中にまで行くと EOP でともかく状態を抜けるのだが、少々不安。あと、受信は完了したが、0 バイトというインターフェイスがいる。

      (タイムアウト)
      反応が遅すぎてタイムアウトになる可能性がある。 3ms バスが アイドル状態だとサスペンドだそうだ。
      心配だったのだが、AVR の USB だと ACK/NAK/STALL を返すのは、上位レイヤーだったことを思い出した。... たぶん大丈夫ではないかと思う。

      ただちょっと確認。1 ms というのは、1.5 MHz クロックだとして 1500 クロック。8 バイトのパケットは、SYNC , PID , CRC を含まないそうだから 1(SOP) + 14 x 8 + 3(EOP) の 96 クロックが 最短。Low-Speed だとしても 1ms に対して 十分に短い 感じ。反応が悪いと性能が出ない。

      ところで、FIFO は 32 バイトだが、PID + データ を収めないといけない。だが 32 バイトのパケットとは、データのサイズが 32 バイトのようだ。なら収まらない。これもなんとか対処する方法を考えないと。
      幸い、受信部分と FIFO の間には、1 バイトのバッファがある。最後のデータを待たせればなんとか。送信も SPI だとバッファがある。

追記:使い方編

    使い方を考えながら、インターフェイスを見なおした。

    まずは、BUS の信号

    // _ ____________________________________ _____
    // RS _><____________________________________><_____
    //
    // _____ _______
    // nCS |________________________________|
    //
    // ______________ _____________
    // nWR/nRD |_____________|
    //
    // _______________
    // DB[7:0] --------------<_______________>----------
    //

    LCD とかのタイミングを参考にするとこんな感じか。信号名も nWR とか nXX にしてみた。

    READ は、これで問題ない。WRITE はどうしたものか。どうも posedge nWR でデータ採取しているみたいに見えるのだが ... ここは negedge nWR から遅延させて採取にしたい。

    SPI は、普通のタイミング ... だが 、最後のデータは、CS を H にしたとき 、上位に 受け取られる。

    とりあえずは、これでいこうと思っている。

      ところで、FIFO 付きの BUS/SPI コードを作ったことになる。DAC で欲しかったものだ。DAC もこれをくっつけようと思う。

    さて、どう使うかまとめてみた。

    ステータスレジスタ

      bit7: CRC_ERROR
      bit6: DATA_FULL
      bit5: DATA_EMPTY
      bit4: DATA_RDY
      bit3: RECV_EN (受信中)
      bit2: SEND_EN (送信中)
      bit1: r_recv_mode (送受信モード ) (0: 送信 / 1: 受信)
       bit0: r_start (送受信スタート) (1: スタート / 0: 完了)

      内部の状態をステータスレジスタとして見せる。この内いくつかは ピンにも出力している。

      対応するピン出力( 負論理 )
      nCRC_ERROR
      nDATA_EMPTY
      nDATA_FULL
      nDATA_RDY
      nBUSY ( ~r_start )

    信号の説明

      r_recv_mode (送受信モード )

       状態は、0: 送信モード か 1: 受信モードのどちらかに大別される。
       どちらでもない状態は存在しない。

      r_start/BUSY (送受信スタート)

       1 にすると 送信モードでは送信要求、受信モードでは 受信要求。
       送信が完了すると、続いて受信スタートになる。(r_recv_mode = 1)
      受信が完了 すると 0 になる。

      CRC_ERROR :
       受信完了後有効で、1 になると CRC_ERROR が起きたことを示す。

      DATA_FULL :
       1 で、FIFO が溢れたことを示す。(データロスト)
       FIFO をリセットするまで有効

      DATA_EMPTY :
      1 で FIFO が空であることを示す。データが入れば 0 になる。

      DATA_RDY :
      1 で 受信データがあることを示す。
       ハンドシェーク用で、DATA_EMPTY とは変化のタイミングが異なる。

      ※) FIFO は、受信モード→送信モードに切り替えたときと、受信中になったタイミングでリセットされる。

      ※) ステータスレジスタの読み込み:
      (SPI) RS=1 で 0x00 を書き込む。
      (BUS) RS=1 で読み込む

    コントロールレジスタ

      bit7: 書き込み要求
      bit1: r_recv_mode (送受信モード ) (0: 送信 / 1: 受信)
       bit0: r_start (送受信スタート) (1: スタート / 0: 完了)

      動作をさせる信号は、コントロールレジスタとした。書き込み要求は、SPI のみで必須だが、インターフェイスを合わせるために、BUS でも必須ということにしておく。

      ※) コントロールレジスタの書き込み:
       1)送信 モード
      (SPI) RS=1 で 0x80 を書き込む。
      (BUS) RS=1 で 0x80 を書き込む。
      2)送信スタート
      (SPI) RS=1 で 0x81 を書き込む。
      (BUS) RS=1 で 0x81 を書き込む。
       3) 受信スタート
      (SPI) RS=1 で 0x83 を書き込む。
      (BUS) RS=1 で 0x83 を書き込む。

      サポートするのは上記の 3 つ。

    送信方法

       1) 送信 モードに する
       2) PID+データを書き込む。(合計 31 バイトまで)
       3) 送信 スタート にする
       4) (タイミングを見て) 追加のデータを書き込む。

      やはり、規模的に これが限界。最大パケットサイズを 32 バイトにすれば、追加のデータは、高々 2 バイト。すぐに送信が始まることは保証できるので、ステータスを見なくても タイミングは計れるはず。空になる前に送り込めるだろう。



      シミュレータで確認したが、こんな感じ。



      続いて送信されるわけだが、FIFO が空になると CRC を付けて 送信完了。CRC-5 を使う場合も自動で元データと CRC を入れ替える。

      ※ シミュレータ結果で i_recv_mode が 1 になっているが、過渡のものなので気にしないで欲しい。

    受信方法

       1) 受信 スタート にする



      データが来ると nDATA_RDY が 0 になるので データを受け取る。nBUSY が 0 になった後は、新たなデータは来ない。 nBUSY = 0 かつ nDATA_EMPTY = 0 を確認して終わる。

      終わるときに、nDATA_FULL = 0 か nCRC_ERROR = 0 なら、エラーにする。



      FIFO が溢れなければ良いので、高々 2 バイト余分に受け取ることは可能だろう。

        nDATA_RDY=0 で割り込みをかけるとする。ここから 12 MHz で 8 x 31 クロックもある。この間に SPI で 2 バイト 受け取れば溢れない。AVR なら問題なさそうに思える。

    以上だが、パケットの間隔というのが気になる。送信→受信、受信→受信で、データを受け取り損ねるとマズイ。特に受信→受信。このケースには、SETUP や OUT がある。

    まずいなら、仕様を変更しないとならない。... どうもその必要がありそうだ。

    ソースコードは、qfn32samples-11.zip の予定(準備中)。

追記: ソースコードのブラッシュアップとバグ修正

    仕様を変更するにも、まずは規模を減らさないといけない。無駄なところがないかチェックしてみた。

    そうしたら、... 送信での CRC 生成がエンバグしていた。
    後で use_i16 , use_i5 という レジスタを作ったのだが、全く不要なばかりか、バグの原因になっていた。

    次、CRC のモジュールは 送信・受信で共有している。特に CRC-5 は、配線の切り替えの方が重いような気がしたので、2 つに分けてみた .... これは 1 スライスの増加に終わった。残念。

    次、受信での CRC の制御。こっちもバグっていた。まず、CRC-5 か CRC-16 のどちらの結果を取るか示す MATCH_SEL というレジスタがあるのだが、完了でリセットされる。これではまずいので 状態を残すように修正。

    次、MATCH_SEL 自体を良くみたら 受信 4 バイト以上という条件だったので、data_bytes の bit を 1 つ増やして data_bytes[2] に変更。

    次、FIFO リセットの条件を 受信モード・送信モードを切り替えたときに変更。受信→受信に対応するベースにする。FIFO_FULL は、受信モードでずっと残るが、その方が都合が良いようにも思える。その上で、受信モードは常に受信待ちということにした。r_start は、1つめを受信したときに 0 になる。完了が必要ならば、r_startを再度 1 にする。

    さて、 送信→受信のパターン。DATA0/DATA1 を送ったあとの ACK が相当するようだ。タイミングによって ACK を取りこぼしてしまう。やはり送信後すかさず受信モードにすることにした。r_start は、0 になるので、次の受信の完了は検出できない。あと、送信時の FIFO_FULL も検出できない。

    ここまでやった。そして ... なんとか 128 スライスに収まった。

まだ続く

    もう FIX したいのだが、まだ手直しするところが ...

    どれぐらいのクロックで動くのかの確認をするついでに、DCMA モジュールというのを入れてみた。これは、クロックセレクタで、ここを通すとグローバルクロックにつなげられる。で、やってみると DCMA に入力できない。... ピン配置を変えると OK みたいなので、ピン配置を見なおした。

    ついでに RS ... データとコントロールレジスタの選択だが、一般に RS=0 が コントロールレジスタみたいなので論理を逆転。

    あと見直していたら、CRC-5 を 2 個使うようになっていた。これで問題なさそうなので 2 個使う USE_DUAL_CRC5
    を標準にした。

    ついでに SUPPORT_SEND_EOP というのを作りかけていたのだが、ヤメ。コード削除。

    で、動作周波数の確認。... ボトルネックを見ていたら、r_recv_mode1 が引っかかった。1 クロック遅延させるつもりが、半クロックになっていたので変更。さらに FIFO のクロックを CLK_4X にしていたのだが、SPI では必要ないので通常の CLK に。

    最終的に CLK_4X は、67.7 MHz になった。本来 48 MHz だが、これぐらい余裕があれば安心。 規模は 127 スライスでぎりぎり OK。

    あと、CLK_OUT を付けた。CLK_4X の反転出力。水晶があれば、発振させられるかも。ソースコードは、-11 を上書き。

ソフト側の構想

    V-USB を改造すればいけるんじゃないかと上で書いたが、少し確認してみよう。本格的にやる場合は別記事にするが、とりあえず、あたりを付けよう。

    extern usbRxBuf, usbDeviceAddr, usbNewDeviceAddr, usbInputBufOffset
    extern usbCurrentTok, usbRxLen, usbRxToken, usbTxLen
    extern usbTxBuf, usbTxStatus1, usbTxStatus3
    extern usbSofCount

    ; extern unsigned usbCrc16(unsigned char *argPtr, unsigned char argLen);
    ; extern unsigned usbCrc16Append(unsigned char *data, unsigned char len);
    ; extern unsigned usbMeasurePacketLength(void);

    アセンブラで extern を grep すれば、こういうのが見つかる。
    下の 3 つは、アセンブラから呼び出すサブルーチンで無視して良い。CRC などは機能にあるし、usbMeasurePacketLength というのも NRZI 関係だろう。

    さて、上は全部変数。この変数を制御しているのが、usbdrv.c 。同じインターフェイスにすれば、わずかな変更でいけそうだ。たぶんアセンブラは INT0 割り込みで動作する。udrv では、なにか受信すると DATA_RDY が L になる。この変化を INT0(か 1) で拾えば同じような処理ができるはず。

    受信は割り込みベース、送信はポーリングでも良いかも。12 MHz でデータが来るが、16 バイト受信ぐらいまでに応答するとすれば、10 us 以内。20 MHz なら 200 クロックもある。これなら受信ルーチンも C で良いかも知れない。

    memo : 受信側

    uchar usbRxBuf[2*USB_BUFSIZE];
    uchar usbInputBufOffset;
    volatile schar usbRxLen;
    uchar usbCurrentTok;
    uchar usbRxToken;

    このあたりが受信で重要そう。

    len = usbRxLen - 3;
    if(len >= 0){
    usbProcessRx(usbRxBuf + USB_BUFSIZE + 1 - usbInputBufOffset, len);

    こういうコードがある。usbRxLen には、先頭の PID と 最後の CRC-16 が含まれるから -3 する。
    PID の場所は、usbRxBuf + USB_BUFSIZE - usbInputBufOffset ということらしい。

    CRC-16があるということは、DATA0/1 パケット。その前に SETUP か OUT が来ているはずで、それが、その前に入っているのかも。この udrv でも同じ仕様が都合が良さそう。

    usbRxToken は、OUT or SETUP (の PID)が入っている。usbCurrentTok は、OUT だった場合に エンドポイント番号が格納される。

    あと、受信では 1 フレームの送受信を一気にやってしまうようだ。IN が来たら予め用意されたデータを送る。OUT or SETUP では、ACK などの応答を返してしまう。

なんと、まだ続く

    V-USB のコードを眺めていたら CRC-16 は、DATA0/1 パケットに使うことが分かった。サイズは関係ない -- 誤解してた。あと、CRC-5 の送信側のコードがない。.... デバイスは 指示パケットを送信しないのであった。

    仕様を誤解していたのだから、まずは修正。CRC-5 の送信側のフラグは、crc5_extract というものなのだが、ややこしいので、整理したら規模が少し減った。さらにデバイス専用にも出来るようにして、少し規模を削れるように parameter を導入したところ、論理が変わらないのに規模が減った。今は 123 スライス。デバイス専用だと 121 スライス。

    ついでなので、0 バイト送信で SOP のみを送る機能は、Low-speed のみなので FS パラメータ(1 で Full-speed) に連動するように変更。さらに DP , SE0 状態を negedge CLK でサンプリングするよう変更。こうしておかないと 受信毎にクロックを 180°ずらすことになる。ソースコードは、-12 版準備中。

    最終的な仕様を記録しないとわけが分からなくなりそうだ。新記事を起こそう。

関連記事

     ・ 『QFN32の FPGA
     ・ 『TTL ALU 74281
     ・ 『FPGA時計の設計
     ・ 『MCPU -- A Minimal 8 Bit CPU
     ・ 『DACを設計してみよう
     ・ 『USBコントローラの設計

    最新ソースコード (2012/07/1)
     ・ qfn32samples-10.zip

      qfn32samples-10.zip で一応の完成とした。最終的な規模を記録しておこう。GSR は、1 つしかないリソースで、SPI で使った非同期リセットで 使われたらしい。

      BUS SPI
      Number of registers: 117 126
      PFU registers: 115 123
      PIO registers: 2 4
      Number of SLICEs: 116 122 /128
      SLICEs(logic/ROM): 32 32 /32
      SLICEs(logic/ROM/RAM): 84 90 /96
      As RAM: 12 12 /96
      As Logic/ROM: 72 78 /96
      Number of logic LUT4s: 200 214
      Number of distributed RAM: 12 12
      Total number of LUT4s: 224 238
      Number of PIO sites used: 20 14 /22
      Number of GSRs: 0 1 /1

    最新ソースコード (2012/07/05)
     ・ qfn32samples-11.zip

    最新ソースコード (2012/07/06)
     ・ qfn32samples-12.zip

      今度こそ.. ちなみに、実際に動かそうと思えるレベルかどうかが、『一応の完成』の基準。(動かせばバグは出るだろう)
      規模を記録しておく。僅かながら変更する余裕ができた。SUPPORT_CRC5S=0 とすると、送信時 CRC-5 は生成しない(device では不要) が、-2 スライスになる。

      BUS SPI (CRC 分)
      Number of registers: 123 148 45
      PFU registers: 121 145 45
      PIO registers: 2 3
      Number of SLICEs: 112 123 /128 27
      SLICEs(logic/ROM): 32 32 /32
      SLICEs(logic/ROM/RAM): 80 91 /96 33
      As RAM: 12 12 /96
      As Logic/ROM: 68 79 /96
      Number of logic LUT4s: 197 218 64
      Number of distributed RAM: 12 12
      Total number of LUT4s: 221 242 64
      Number of PIO sites used: 21 14 /22
      Number of GSRs: 0 1 /1


参考文献(データシート等)
 ・ 『MachXO2 ファミリデータシート(日本語 pdf)
 ・ 『MachXO2 テクニカルノート(日本語)
 ・ 『Lattice Diamond 1.4マニュアル
posted by すz at 00:24| Comment(0) | TrackBack(0) | CPLD
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/56722396
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック