2012年07月07日

USBコントローラの設計(2)

前記事で、QFN32 の MachXO2-256 に入る USBコントローラを設計してみた。次は、これをどうやって使うのかソフト編に入りたい。... が結局どうなったのか混乱するので、仕様の整理から。(対応するのは、qfn32samples-12)

BUS版と SPI版

    HOST とのインターフェイスは 2 種類ある。LCD で良く使われる 8080 バス仕様と SPI 仕様。

    BUS 版。自作モジュールのピン配置を示す。

    // XO2-256 UDRV XO2-256
    // 27 DP 1 24 VCC
    // 28 DM 2 23 nBUSY 25
    // 29 nRD 3(TMS) 22 nCRC_ERROR 23
    // 30 nWR 4(TCK) 21 CLK_OUT 21
    // 32 DB0 5(TDI) 20 CLK_4X 20
    // 1 DB1 6(TDO) 19 nDATA_FULL 17
    // 4 DB2 7 18 nDATA_EMPTY 16
    // 5 DB3 8 17 N.C. (JTAGENB)
    // 8 DB4 9 16 nDATA_RDY 14
    // 9 DB5 10 15 nCS 13
    // 10 DB6 11 14 RS 12
    // GND 12 13 DB7 11

    (JTAGENBを除いて)1つのピンも余らない。コンフィグデータを書き込むときには、JTAGENB を H にして、JTAG ポートから書き込む。

    // _ ____________________________________ _____
    // RS _><____________________________________><_____ // // _____ _______ // nCS |________________________________| // // ______________ _____________ // nWR/nRD |_____________| // // WRITE: _________________ // DB[7:0] ----------------<_________________>--------
    // |---| CLK_4X (MAX)
    // |---------------------| CLK_4X x 2 (MIN)
    //
    // READ: _____________
    // DB[7:0] --------------<_____________>----------
    //
    // |-| gate level delay

    アクセスのタイミング。こうはしたが、RS は、nWR/nRD を L にしたときに確定していれば良い。nCS は、単なる出力コントロールなので、nRD と 同時に変化させても構わない。

    nWR は、普通 posedge で採取するものなのだが、内部クロックに同期して採取しているので無理。negedge から間をおいて採取になる。negedge から CLK_4X の 1T (Full-speed なら 20.8 ns) 以内に WRITE データを確定させないといけない。そこから 最小 1T 確定させておかなければならない。 Low-Speed なら 1T が 167 ns にもなる。注意が必要。

    RS は、A0 とも表記される レジスタ選択。L:データ/H:コントロール

    BUS 版を 一応は作ってみたが、実際に使うことは想定していない。

    実際に使うのは、SPI 版

    // XO2-256 UDRV SPI XO2-256
    // 27 DP 1 24 VCC
    // 28 DM 2 23 nBUSY 25
    // 29 nCS(2) 3(TMS) 22 nCRC_ERROR 23
    // 30 SCK(2) 4(TCK) 21 CLK_OUT 21
    // 32 MOSI(2) 5(TDI) 20 CLK_4X 20
    // 1 MISO(2) 6(TDO) 19 nDATA_FULL 17
    // 4 7 18 nDATA_EMPTY 16
    // 5 8 17 N.C. (JTAGENB)
    // 8 9 16 nDATA_RDY 14
    // 9 MISO 10 15 nCS 13
    // 10 MOSI 11 14 RS 12
    // GND 12 13 SCK 11

    ピン配置は、こうした。(2) と書いてあるのは、第二の選択。

    JTAG のポートを空けるならば、オリジナルのピン配置にする。ただ、SPI を通してコンフィグもやりたい(できる)なら、(2) の方が便利。

    SPI自体のビットレートには制限はあまりない。FIFO とのハンドシェークが制限になるだけ。SPI では、FIFO を CLK (12MHz) で駆動している。ハンドシェークに 4 CLK かけるとすると 24 MHz が上限。FIFO のクロックを CLK_4X にすれば、96 MHz までいく。(FPGA の SCK の上限はもっと高い)

共通のピンの説明

    DP/DM

      USB の D+/D- 。Low-Speed デバイスは、DMを 1.5 K でプルアップする。Full-Speed デバイスは DP の方。FPGA の ピン設定では、抵抗値が高く役に立たないので注意。

      HOST 向けでは、両方をプルダウンする。こちらは、FPGA の ピン設定で良い。現状では、Low-Speed/Full-Speed の両方には対応できない。

    CLK_4X/CLK_OUT

      Full-Speed では、48 MHz , Low-Speed では 6MHz を CLK_4X に入力する。CLK_OUT は、反転出力で水晶を直付け可能なように配慮。

    nDATA_RDY

      受信データがあるときに L になる。受信ハンドシェーク用で、割り込み線として使う。

    nBUSY/nCRC_ERROR/nDATA_FULL/nDATA_EMPTY

      RS=1 で READ すれば、同じ情報が読めるので必須ではない。特に nDATA_EMPTY 。nDATA_RDY と論理とタイミングが違うだけなので、信号すら不要。これらは、LED とかでのモニタ用として一応 残してある。

      信号自体は、後で説明。

define と parameter

    変更可能だと思っているものについて説明しておく。

    (define) USE_SPI

    SPI にするか BUS にするかの選択。( USE_BUS は常に define で構わない )

    (parameter) SUPPORT_CRC5S=1

    デバイスでは、CRC-5 を使う トークンパケットを出さないので、CRC-5 生成は不要。
    SUPPORT_CRC5S=0 とすると僅かに規模が減る。

    Full-Speed 用
    (parameter) FS=1
    (parameter) FIFO_DEPTH = 5

    Low-Speed 用
    (parameter) FS=0
    (parameter) FIFO_DEPTH = 4

    Low-Speed では、SOP のみの送信をサポートする。規模が増えるので、FIFO を減らすことで対処する。

    Full-Speed 用の FIFO は 32 バイト。最大パケットサイズ 32 を想定しているが、MCU 側の 応答時間+転送速度によっては、もっと大きなパケットにも対応可能。逆に 32バイトのパケット全部は FIFO に入らない。送信時にちょっと細工がいる。

データ送受信

    BUS 版では、

      RS=0 としてデータを書けば FIFO に入り、読めば FIFO のデータを読める。

      FIFO が溢れれば nDATA_FULL が L になり、FIFO をリセットするまで FULL 状態が維持される。
      データがないのに読めば、前のデータが読める。nDATA_RDY を見てのハンドシェークが必要。

    SPI では、

      (送受信モード = 0 のとき) RS=0 として データを書けば FIFO に入る。DATA_FULL の動作は同じ。
      (送受信モード = 1 のとき) RS=0 として データを読めば FIFO のデータを読める。nDATA_RDY との関係は同じ。

    送受信モード, FIFO リセット については、次で説明する。

コントロールレジスタ

    RS=0 にして書き込むことで 以下の状態を制御する。

      0x80 送受信モードを 0 (受信モード)
      0x82 送受信モードを 1 (送信モード)
      0x81 送信要求を 1 (送信モード+送信スタート)

    RS=0 にして読み込むと以下の状態が読める。

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

    送信では、送受信モード=0 にしてから、FIFO にデータを送る。ある程度送ったら 送信要求=1 。残りのデータを FIFO にデータを送る。
    FIFO のサイズを大きく超えるデータ量だと、この制御は難しい。規模に余裕があるので、DATA_EMPTY をフロー制御用の信号に置き換えるべき。


      assign I_DATA_EMPTY = (i_addr == o_addr);

      wire [DEPTH-2:0] data_len = (i_addr[DEPTH-1:1] - o_addr[DEPTH-1:1]);
      assign I_DATA_EMPTY = (data_len < 2**(DEPTH-2) );

      誤差 -1 だから 引き算の結果は、±1 。FIFO のデータ量が
      0 〜 14 なら 1
      (15 〜 16 なら どちらになるか分からない)
      17 〜 31 なら 0

      という制御なら入った。後日変更しよう。

    (まとめ終わり・本題が続く)

V-USB

    V-USB は、AVR のみで Low-Speed デバイスにするドライバで、商業利用でないなら、GPL v2 のもとに利用できる。(USB の ID を利用するのは、また別)

    AVR のみで動くのならば、必要な処理すべてが入っているわけだ。一部置き換えることで、作ったコントローラ用になるはず。

    一体どこをどう変えるのか? ... 目星は付けた。

    V-USB は、アセンブラと C に分かれている。インターフェイスを見たがどうも、アセンブラを置き換えるだけで済むようだ。C を変更することになったとしても変更は僅かになりそう。

    コンフィグは、実にややこしくなっている。ここは変更すべきところが出てくる。

    まずは、この方針で V-USB というものを理解する。

V-USB のアセンブラコード・インターフェイス(1)

    udbdrv.c: usbPoll() をまず見よう。受信後どんなデータを見ているのかが分かる。

    uchar usbRxToken;
    uchar usbCurrentTok;
    volatile schar usbRxLen;
    uchar usbInputBufOffset;
    uchar usbRxBuf[2*USB_BUFSIZE];
    :
    len = usbRxLen - 3;
    if(len >= 0){
    usbProcessRx(usbRxBuf + USB_BUFSIZE + 1 - usbInputBufOffset, len);
    usbRxLen = 0;
    }

    usbProcessRx:
    if(usbRxToken < 0x10){
    usbFunctionWriteOut(data, len);
    return;
    }
    if(usbRxToken == (uchar)USBPID_SETUP){
    :
    if(type != USBRQ_TYPE_STANDARD){
    replyLen = usbFunctionSetup(data);
    }else{
    :

    大胆にロジックを削った。

    受信データがある場合、usbRxLen に PID , CRC-16 を含めたデータサイズが格納される。
    ただし、0 オフセットではなく、USB_BUFSIZE - usbInputBufOffset から格納。

    受信データとは何かというと DATA0/1 パケット。このパケットの前には、SETUP か OUT のトークンが来ている。それは、usbRxToken に格納され、後で処理の切り分けに使う。OUT でのエンドポイント番号は、usbCurrentTok に格納されるらしい。

    まずは、ここまで。

    どうも usbRxLen がカナメらしい。これをセットすることで上位レイヤーの受信処理が動き出す。

    自作 USBコントローラ(以下 udrv) で対応させる場合、ルールさえ守れば後は自由にして良さそうだ。

    まず、どの契機で動かすか ... Full-Speed では、次々にデータが来るから nDATA_RDY=L を割り込みの契機にする。一度フレームが来たら、1 フレーム分 全部処理し終わるまで 割り込みから抜けない。というもので良さそう。

      ただ、Low-Speed でそれでは面白くない。余裕があるからだ。受信完了を契機にしたいところ。nBUSY は、送信完了で H になるのだが、L になるようセットしておけば、受信完了でも H になる。

      Full-Speed では、FIFO が溢れる恐れがあるし、連続受信で、L になるようセットするのが間に合わないケースが出てくる。Low-Speed では問題ないようにできるはず。

      Full-Speed でも、FIFO が 半分を超えるか、受信完了で 割り込むという手がある。送信の場合は、FIFO が 半分を切れば追加のデータを送るのが良さそう。送信完了自体は検出する必要はない。そういう信号線(IRQ)が作れないか検討してみよう。

      DATA_EMPTY はなくしても良いし、nCRC_ERROR と nDATA_FULL はエラーが起きたという意味にして 1 つにまとめても良いし、なくしてさえ良い。nBUSY は動いているのを目視したいから残したいが、送信中+受信中に変えたい。

      //assign nBUSY = r_start;
      assign nBUSY = ~(RECV_EN | SEND_EN);
      assign nIRQ = ~r_irq;
      always @(posedge CLK)
      begin
      r_irq &= ~UNDER_HALF ^ r_start ^ i_recv_mode;
      end

      こんな風に変えるのなら可能だった。(UNDER_HALF は、変更後の I_DATA_EMPTY )

      送信モードでは、nIRQ の 最初の状態は H 。
      r_start == 0 すなわち 送信データ詰め込み中ならば、半分埋まったところで L になる。
      r_start = 1 すなわち 送信をスタートさせると nIRQ は反転。
      半分以上埋めているなら、半分を切れば L になる。(半分以下なら、r_start=1 で L )
      送信完了後は、受信モードになる。最初は 空 だから L のまま。
      ここで r_start = 1にすると H に反転。
      受信データが半分を超えるか、受信が完了すれば L になる。

      受信完了後、自動で r_start = 1にすることは可能だった。

      if (r_se0)
      begin
      `ifdef AUTO_RECV_AFTER_SEND
      if (~r_recv_mode) r_recv_mode <= 1;
      `endif
      r_start <= 1'b0;
      end
      else if (~r_recv_en1 & r_recv_en)
      r_start <= 1'b1;

      上記(2ヶ所ある)の最後の else が追加分。

      そこまでやると、H → L の変化だけが重要になる。送信完了後にやるべきことはないのだ。

        送信で重要なのは、途切れずにデータを送り込むこと。
        半分を切れば割り込みが起きるから、16 バイト+αを送り込む。送り込む速度が 12Mbps よりかなり速くないと 64 バイトのパケット送信は無理だが。送り込んでしまえば、あとは受信待ち。

        受信で重要なのは、FIFO を溢れさせないことと 確実な 受信完了の受け取り。両方 H → L 。

    さて、受信データは、usbRxBuf に貯めこんでいく。usbRxLen が 0 になっていたら、貯めるインデックスを 0 にする。負の値にならないようにするなら、最大パケット長に 128 以上は取れない。Full-speed でも 64 が精々。

    受信データの前に SETUP か OUT が来ている。これは、PID を usbRxToken に格納して、廃棄? -- データをそのまま残しても良いかも知れない。

V-USB のアセンブラコード・インターフェイス(2)

    1 パケットの受信完了で何をしているか ... アセンブラを眺めてみると

    受け取ったパケットのPID によって次の処理を行う。

      DATA0/1 の場合 : handleData
      ( usbDeviceAddr を見て チェック : ignorePacket )
      IN の場合 : handleIn
      OUT の場合 : handleSetupOrOut
      SETUP の場合 : handleSetupOrOut
      どれでもない場合 : ignorePacket

    handleSetupOrOut では、一旦リターンする。(思っていたのと違った)

    handleIn は、送るべきデータが用意されていなければ NAK を送ってリターン
    ある場合は、usbSendAndReti これが送信ルーチンだが、終了後 usbNewDeviceAddr を見て usbDeviceAddr を更新している。

    udrv の場合、usbSendAndReti に相当する部分は、かなり簡略化できる。データをそのまま送れば良いからだ。

    どうすれば良いか大分イメージできた。次は、送信データを受け取るところを見てみよう。あと、ハンドシェーク。ACK/NAK/STALL をどういう場合に送っているか(あるいは送らないか)もチェックしよう。

V-USB のアセンブラコード・インターフェイス(3)

    typedef struct usbTxStatus{
    volatile uchar len;
    uchar buffer[USB_BUFSIZE];
    }usbTxStatus_t;

    extern usbTxStatus_t usbTxStatus1, usbTxStatus3;
    #define usbTxLen1 usbTxStatus1.len
    #define usbTxBuf1 usbTxStatus1.buffer
    #define usbTxLen3 usbTxStatus3.len
    #define usbTxBuf3 usbTxStatus3.buffer

    volatile uchar usbTxLen = USBPID_NAK;
    uchar usbTxBuf[USB_BUFSIZE];

    usbBuildTxBlock:

    usbTxBuf[0] ^= USBPID_DATA0 ^ USBPID_DATA1; /* DATA toggling */
    len = usbDeviceRead(usbTxBuf + 1, wantLen);
    if(len <= 8){
    usbCrc16Append(&usbTxBuf[1], len);
    len += 4; /* length including sync byte */
    }else{
    len = USBPID_STALL;
    }
    usbTxLen = len;

    usbDeviceRead:
    if(usbMsgFlags & USB_FLG_USE_USER_RW){
    len = usbFunctionRead(data, len);
    }else{
    if(usbMsgFlags & USB_FLG_MSGPTR_IS_ROM){ /* ROM data */
    :
    }else{ /* RAM data */
    :
    usbSetInterrupt:
    usbGenericSetInterrupt(data, len, &usbTxStatus1);

    usbSetInterrupt3:
    usbGenericSetInterrupt(data, len, &usbTxStatus3);

    とりあえず、送信データは、3 種類ある。usbTxBuf と usbTxBuf1/usbTxBuf3。 これらのフォーマットはすべて同じで、PID と CRC-16 を含むデータ。usbTxLen には、NAK , STALL などの PID が入る場合がある。 そうでない場合は、SYNC の分を含む 1 多い値。

    IN がきた時 endpoint が 0 なら usbTxBuf を使う。USB_CFG_EP3_NUMBER に一致すれば usbTxBuf3 , それ以外なら usbTxBuf1を使う。

    udrv に対応させる場合、CRC-16 は余計なのだが、インターフェイスを変えない。内部で -2 すれば良いだけの話だからだ。オーバヘッドが大きければ、CRC-16 の計算を止める。

    こうしておくと udrv の送信で CRC-16 の計算をサポートしないという選択枝が出来る。

    ついでに気がついたのでメモ: データサイズが 8 バイト以内だという のがハードコーディングされている所がある。ここは、Full-Speed 対応で変更しないと。

パケットのフロー制御

    パケットが来たら ACK/NAK/STALL のどれかを送ったり、あるいは送らないという処理が必要。

    いったい V-USB ではどうしているのだろう。

    まずは、予備知識

      HOST | device | HOST
      ---------------+--------------------------+----------
      SETUP DATA0 | ACK/(none) |
      | |
      OUT DATA0/1 | ACK/NAK/STALL/(none) |
      | |
      IN | DATA0/1 or NAK/STALL | ACK/(none)

    代表的なのは、仕様上こういうことらしい。だが、実際のコードはもっと細い。どういう風になっているか まとめておかないと、コードが作れないのだ。

    SETUP/OUT の後 の DATA0/1
    NAK : (usbRxLun != 0) のとき 受信バッファが空いていないので受け取らない。
    ACK : 0 バイト受信のとき 、受信バッファの状態を変更せず ACK
    STALL : 該当ケースなし。
    (none) : 前が SETUP/OUT でないとき
    IN
    NAK : (usbRxLun != 0) のとき 受信バッファが空いていないので送らない。
    ( すぐに ACK が来るが受け取れない )
    送信するデータがないとき。
    上位から指定されたとき
    STALL : 上位から指定されたとき

    (その他)
    SETUP/OUT (none)
    受信バッファオーバーフロー (none)
    想定外のトークン / エンドポイント 範囲外 (none)

    どうも、これだけのようだ。CRC エラーはないが、たぶん想定外と同じ。

    受信バッファが空いていないので受け取らない。というのは、若干面倒。FIFO には受け取った上で NAK を送信すれば良いのだが、受信完了を待つのが ちょっと。

    IN での NAK は、自力では、思いつかなさそう。
    あと、SETUP の後、NAK は送らないのだが ... 送るケースがあるような ...これは、そういうことがないようにしている?

    usbCurrentTok にストアするタイミング

    SETUP/OUT (none)
    受信バッファオーバーフロー (none)
    想定外のトークン / エンドポイント 範囲外 (none)

    これらは、ACK/NAK を返さないだけでなく、usbCurrentTok を更新している。ちなみに DATA0/1 の後の (none) では、更新しない。わざわざそうしているから、理由があるのかも。

ドライバーの設計

    ようやく本題に入る。ドライバーをどうやって作るのか検討を始めよう。

    その前に 仕様が大分変わった。
     ・ udrv-13-spec.txt
    変更案をいろいろ書いたが、このスペックをもとにする。

    ISR(INT0_vect) {
    static uint8_t recv_offset = 0;
    static int8_t send_len = 0;
    static uint8_t *send_ptr;
    uint8_t i;

    まずは、C で全部記述することにする。AVR だとこんな感じで割り込みを定義できる。
    初期化で、negedge nIRQ で INT0割り込みが 起きるようにしておく。

    if (send_len > 0) {
    :
    return;
    }

    送信で、送りそこなった分があれば送るコードが必要かも知れない。割り込みは、FIFO が半分になったとき起きる。ただ、SPI が十分速くないと最大パケットサイズを大きくできない。遅いと 最大パケットサイズ を 32 にせざるを得ないが、そうなると、送信をキックするところで全部送れてしまう。たぶん必要ないので、これは後回しにしよう。

    次、受信だが、割り込みは、受信完了か FIFO が半分を超えたところで起きる。

    if (bit_is_clear(nDATA_RDY_PIN, nDATA_RDY_BIT)) {
    re_recv:
    do {
    if (recv_offset < USB_BUFSIZE) {
    usbRxBuf[recv_offset++] = spi_read();
    } else
    spi_read();
    } while (bit_is_clear(nDATA_RDY_PIN, nDATA_RDY_BIT));
    }
    i = ctrl_write(CTRL_READ_STATUS);
    if (i & ST_BUSY) return; // RECV in progress
    if (i & ST_DATA_RDY) goto re_recv;
    if (i & ST_CRC_ERROR) {
    recv_offset = 0;
    return;
    }
    if (i & ST_DATA_FULL) {
    recv_offset = 0;
    ctrl_write(CTRL_SEND_MODE);
    ctrl_write(CTRL_RECV_MODE);
    return;
    }

    nDATA_RDYが L で buffer に入る限りは、とにかく入れる。H になれば、FIFO が空ということ。ループを抜けてチェックにはいる。ctrl_write() でステータスが読めるとしておく。

    ST_BUSY なら、まだ受信が続いているということ。割り込みを一旦抜けて、次の割り込みを待つ。
    受信が終了したとしても、僅かな期間で 最後のバイトが受信されたかも知れない。ST_DATA_RDY ならば、もう一度 読み込む。

    エラーが起きたら、無視することになっている。ST_DATA_FULLだと、一旦 送信モードにしないとステータスが消えない。

    ここで バッファに受信データが入った。次はパケットの解析。

    usbRxToken = usbRxBuf[0];
    if ((usbRxToken == USBPID_SETUP) | (usbRxToken == USBPID_OUT)) {
    if ( recv_offset < 4 ) {
    if (usbRxLen) recv_offset = USB_BUFSIZE + 1;
    return; // RECV DATA0/1
    }
    if (recv_offset == USB_BUFSIZE + 1) { // buffer busy
    recv_offset = 0;
    goto send_nak;
    }
    if (recv_offset == USB_BUFSIZE ) { // buffer overrun
    recv_offset = 0;
    return;
    }
    // data top : USB_BUFSIZE - usbInputBufOffset
    usbInputBufOffset = USB_BUFSIZE - 3;
    usbRxLen = recv_offset - 3;
    recv_offset = 0;
    send_handshake(USBPID_ACK);
    return;

    SETUP か OUT の場合、次に DATA0/1 パケットが来る。続けて 読み込んで バッファにくっつけてしまう。ただし、usbRxLen が 0 でなければ、受け取った後、NAK を送る。recv_offset = USB_BUFSIZE + 1 として後で分かるようにしておく。DATA0/1 パケットを受け取った後、バッファが溢れていたら NAK は送らない。

    このコードで DATA0/1 トークンのチェックはしていない。後で入れておく必要がある。
    あと、RxLen が 0 でないときでも、DATA パケットが開始されるまでに SETUP/OUT を受け取らないと、DATA パケットを受け取ってしまう。ここは直すの面倒。

    最後に、usbRxLen をセットして、ACK を送り割り込みを抜ける。

    IN が来たら送信。usbRxLen が 0 以外なら NAK を送るのは、SETUP/OUT と同じ。アドレスが違うときも NAK 。
    次に エンドポイント番号によって、送信バッファを選択。send_len には、SYNC と CRC-16が含まれるので udrv では、-3 する。これで送信の準備は出来た。ちなみに、これ以外のパケットが来ても何もしない。

    } else if ( usbRxToken == USBPID_IN ) {
    if ( usbRxLen < 0 ) goto send_nak;
    i = (usbRxBuf[1] >> 1);
    if (i & (i != usbDeviceAddr)) goto send_nak;
    i = (usbRxBuf[1] << 3) & 0x08 | (usbRxBuf[2] >> 5) & 0x07;
    if (i == 0) {
    send_len = usbTxLen;
    send_ptr = usbTxBuf;
    usbTxLen = USBPID_NAK;
    } else if ( i == USB_CFG_EP3_NUMBER) {
    send_len = usbTxLen3;
    send_ptr = usbTxBuf3;
    usbTxLen3 = USBPID_NAK;
    } else {
    send_len = usbTxLen1;
    send_ptr = usbTxBuf1;
    usbTxLen1 = USBPID_NAK;
    }
    if (send_len > 3) send_len -= 3; // remove SYNC , CRC-16
    usbDeviceAddr = usbNewDeviceAddr;
    (送信処理)
    } else {
    recv_offset = 0;
    return;
    }

    いよいよ送信にはいる。まず、send_len が負の場合 ACK などのハンドシェークの PID を送る仕様。0 なら、送るものがないので NAK 。
    いよいよ送信だが、まずは、送信モードにする。最初は FIFO が空で nIRQ は H 。詰めていって 半分を超えると L になる。ここまで詰めたら送信スタート。半分に満たない場合は、スタートと共に nIRQ が L になる。

    割り込み要求ビットが立っているので、送信スタートの直後にクリア。次にまだまだ詰められるので 20 を上限に詰める。
    最後に 全部詰められたら、割り込み要求ビットをクリア。

    if (send_len < 0) {
    send_handshake(send_len);
    send_len = 0;
    return;
    }
    if (send_len == 0) goto send_nak;
    ctrl_write(CTRL_SEND_MODE);
    bit_clear(nCS_PORT, nCS_BIT);
    do {
    spi_write(*send_ptr++);
    while ( (--send_len) && bit_is_set(nIRQ_PIN, nIRQ_BIT) );
    while (spi_busy())
    ;
    bit_set(nCS_PORT, nCS_BIT);
    ctrl_write(CTRL_SEND_START);
    bit_set(EIFR, INTF0); // clear INT0 req
    re_send:
    if (send_len > 0) {
    bit_clear(nCS_PORT, nCS_BIT);
    i = 20;
    if (send_len < i) i = send_len + 1;
    while (--i) {
    spi_write(*send_ptr++);
    --send_len;
    }
    while (spi_busy())
    ;
    }
    bit_set(nCS_PORT, nCS_BIT);
    if (send_len == 0)
    bit_set(EIFR, INTF0); // clear INT0 req
    return;

    だいたいこんな感じだが、送信はちょっとグダグダ。SPI が遅ければどうせ全部詰めることになるのだ。nIRQ など見ないで 24 など 規定数詰めたらスタート 。残りを順次詰めて、最後に 割り込み要求ビットをクリアで良いと思う。

    まぁ AVR 以外も想定してロジックを考えてみたのだが、24MHz 以上とかの 高速なSPI とかじゃないと意味なさそう。... Low-Speed を忘れていたが、こちらは最大パケット長が 8 なので余裕。全部詰めてスタートで良い。

追記: バグがいくつか

    送信で、NAK などハンドシェークパケットを送る際に Length の方にセットする仕様なのだが、変更する必要があった。
    ハンドシェーク の PID は以下のとおり

    /* handshake */
    #define USBPID_ACK 0xd2 /* 1101 0010 */
    #define USBPID_NAK 0x5a /* 0101 1010 */
    #define USBPID_STALL 0x1e /* 0001 1110 */

    Low-Speed では、DATA パケットを送る場合 の Length の最大値は 12 なのだ。V-USB では、Length の bit4 が 1 だと ハンドシェークと判断する。この仕様だと 32 バイトのパケットサイズすら対応できない。

    PID の bit0 が 0 なのを利用して、0x80 | (PID >> 1) とすることにした。

    ただ、こうやって仕様変更すると usbdrv.c の方を結構修正しないといけない。10 ヶ所ぐらいある。あと usbTxLen & 0x10 などとして判断しているところもあってこれも 0x80 に変更しないといけない。

    次に受信のほう。パケットを受けられるだけ受けるというのは、どうも無理がありそうだ。

      SOF → SETUP → DATA0

    例えばこんな風にパケットが来たらくっつけてしまうやりかただと対応が面倒。

    どうも 受信中にパケットの切れ目を判断してパケット毎に処理しないとダメそうだ。判断は簡単だが、ループの内側に if 分が 2 つほど入ることになる。ちょっと面白くないが、とりあえずは、やむを得ないということにしよう。

    IN (device → host) のデータ再送未対応。

    V-USB も分かっていながら未対応にしている。作っているのは、サイズの制限など気にしてないので、対応させておきたい。

    バスリセット 未対応

    ホストがバスリセットすると、たぶんおかしくなる。

ACK や NAK を返すタイミングについて

    どうも気になって ググってみると ... ACK や NAK を返すタイミングは相当にタイトらしい。Low-Speed で 18 クロックだとか。( USB の仕様書を見てみたが、 Full-Speed も同じだった)

    いつまでも待っていたら HOST が次を始められない。それはそうだ。

    データがぶつかるのは、IN の後 デバイスから送られる DATA0/1 と NAK/STALL ハンドシェーク。HOST が DATA0/1 を送った後の デバイスから送られる ACK/NAK/STALL ハンドシェーク。

    ... となると いままで作ってきた udrv の仕様では全然ダメだ。全然間に合わない。変更するにも規模が足りない。

    DATA0/1 を受けとれない状態なら NAK か STALL。受け取った後 エラーが起きなければ ACK 。こういうものだけなら、規模がもうすこしあれば行けそうな気がする。だが、DATA を送信するのは一体どうするのだろう?最初からデータを入れとかないと無理なのでは?

      あと、Low-Speed 専用ならどうだろう? 1.5 MHz で 18 clk ということは、20 MHz で 240 clk 。受信完了し続いての動作だから、可能なやりかたはありそうだ。

      だが、12MHz で 18clk は、20 MHz で 30 clk 。SPI では 1バイトを送る時間もなさそう。これは諦める。対応する余裕はない。

      Full-Speed device に対応するには、エンドポイント毎の バッファを持ったモジュールを MCU の代わりに入れて、高レベルのコントローラにするしかないような気がする。いま作っているのは、単なる物理層だから無駄にはならない。だが、高レベルのコントローラまで設計するつもりは今はない。

      ただ、Low-Speed でも 受信完了してから 受信データを受け取り ACK を返すのでは遅すぎる。受信したら即受け取りでないと ダメかも知れない。

    逆に HOST 専用ならどうだろう? HOST が制御しているのだから、送信同士がぶつからないように制御はできる。USB の仕様書でも、送信同士がぶつかるケースについてだけ言及しているように見える。

    ところで、device 側から見ると、ACK が来なかったら、(たぶん)次の要求は再送。このあたり、V-USB ではどうなっているのだろう? 気になってきた。

      ; Comment about when to set usbTxLen to USBPID_NAK:
      ; We should set it back when we receive the ACK from the host. This would
      ; be simple to implement: One static variable which stores whether the last
      ; tx was for endpoint 0 or 1 and a compare in the receiver to distinguish the
      ; ACK. However, we set it back immediately when we send the package,
      ; assuming that no error occurs and the host sends an ACK. We save one byte
      ; RAM this way and avoid potential problems with endless retries. The rest of
      ; the driver assumes error-free transfers anyway.

    なんかわざわざコメントが書いてあった。やっぱり 送信後 バッファを捨ててしまうのはまずそうな ... たぶん ACK が来たときに usbTxLen を更新してバッファを捨てれば良い。

    さて、device では タイムアウト が必要ということではなさそうだ。デバイス依存になるかも知れないが、Full-Speed の HOST は可能かも知れない。

ホストの検討

    なかなか問題が多そうだが、デバイス側はどうすれば良いか分かってきた。では、ホストはどうしたら良いのだろう。これも問題がありそうだが、強引に検討だけはしておこう。

      実現できたとしても、実用上の利点というのは、ほとんどなさそう。デバイス側が USB の勉強になったように、これもまた勉強。

      作るためには、ホストの制御を基本から理解する必要がある。逆にこれが使えるぐらいの知識が出来たら、他の OTG 付き MCU で制御できるようになるはず。V-USB の HOST 版というものもたぶん作れるようになる。

      OTG 付きの MCU というと、秋月の PIC32MX220F032B がわずか 220 円で手に入るようになった。これを使いこなせば実にいろいろできそうな気がする。

    さて、HOST にしてなにを制御するのか? 汎用的なものは一切考えない。それは PC でやるべきことだ。一応電子工作として テーマを 2 つ

      (1) キーボード や マウスの制御

      これができるようになると、PS/2 変換器が作れそう。ただ、V-USB のような 直接制御が可能そうなので、実際には udrv で作る必要はないだろう。あと、HUB には対応するのは、面倒そうだが ... いったいどうするのか 後回しにするがいずれ検討したい。

      (2) BlueTooth ドングル

      USB BlueTooth ドングルは、妙に安いので これが作れると嬉しいような気がする。実際に作るなら、PIC32MX220F032B で変換器を作るのが最も安くあがりそうだが ... まずは udrv で検討する。

      はたして、可能なのかどうか? 最初の壁はパケットサイズ。MaxPS=64 みたいなので、結構厳しいものがある。

    目標はできたが、どのように設計するのかについて条件がある。まずはそこから。

      (1) V-USB のアセンブラコードを最小限度の変更で利用できるようなインターフェイスにする。

      Low-Speed の HOST なら ピンを直接制御しても 作れそう。ただ、送信・受信をするコードを 一から作れるような気はしないので、V-USB のコードを流用するにはどうしたら良いかについて、最初から考慮しておく。

      (2) PIC32MX220F032B が最終ターゲット

      AVR にも HOST 付きのものはあるが、どうも 最近は device only のものばかりになっている。XMEGA にも USB 付きが出てきているが device ばかり。

      まだ、AVR を中心に使おうとは思っているのだが、PIC32MX を専用IC にしてしまうような使い方なら 使ってみたいとは思っている。なにしろ値段が安い。220 円ならホストインターフェイスを買うより安くなってしまう。

      (3) MicroChip の USB Framework は使わない。

      これを使うと MicroChip 以外のものを扱うとき困ってしまう。API も合わせない。作りたいのは、上記の 2 つぐらいだし、理解するために試しに作ってみるだけなので、いきなり高い完成度のものは狙わない。

      もっと言うと、MicroChip が出している開発ツールを一切使わないで開発できるようにしたい。なかなか厳しそうではあるが、使いたい MIPS チップは他にもある。ちなみに、書き込みは、 OpenOCD が既に対応しているそうだ。問題なのはライブラリだが、(他のMIPSチップ向けだが)作りかけたものがある。

    こういう条件で作り出すのは良いのだが、さてどうしよう。

    最初に PIC32MXの USB インターフェイスがどういうものなのか、あたりを付けないといけないだろう。
     ・ 秋月の PIC32MX220F032B
    ここにデータシートがあるので 見てみる。で、レジスタの名前 例えば U1OTGIR で検索すると ... PIC24F の 日本語ドキュメント がヒットする。

    構成図を見ると、PIC32MX と PIC24F の USB モジュールは実に似ている。どうもほとんど同じようだ。なら、調査は、PIC24Fの方で済ますことにする。

    つらつらと眺めてみたが、HOST の場合、それほど高機能ではなさそう。エンドポイントも 1 つだけを使う。なら udrv と大差ないんじゃないかと言う気がしてきた。udrv 向けに普通に作れば対応可能だろう。

      あと気になったのは、U1SOF レジスタ。HOST のみの機能で、送信同士のぶつかりを防ぐためのタイムアウト値を格納する。最大値は 255 。なるほど、変更できるかどうか別にして、HOST では、タイムアウトが必ずあるものだと思った方が良さそうだ。逆に、デバイスで ACK 待ちのタイムアウト値はレジスタとしてはない。デバイスでは、タイムアウトという概念はなくても良いのかも知れない。

    次に、HOST がどうやって送信するのか? V-USB の device のコードは、見てきたわけだが、HOST から来る 指令に対応するだけのものなのだ。だからこの知識だけでは、HOST が どのような送信の仕方をするのかは全然分からない。

    ぐぐってみると PICFUN さんのところの、
     ・ http://www.picfun.com/usb03.html
    が引っかかった。随分前にも見たことがあるのだが、理解する必要もなかったので忘れていた。

       ・ 概念的には、SOF で始まるフレームがある。
       ・ フレームのなかには、SETUP - DATA0 - ACK とか IN - DATA0/1 - ACK とか一連の シーケンスからなる トランザクションがある。
       ・ バルク転送というのは、本来 フレーム内で必要なトランザクションを行ったあと、余った帯域で 行うもの。(優先度が低い)
       ・ インタラプト転送というのは、優先度が高いが、頻繁に行うものではない。
       ・ フレームについて概念的にと書いたのは、デバイスから見ればフレームがあってもなくても処理に大差ないため。今まで作ったもののレベルだと、リセットするのを防ぐとかその程度の意味しかないような感じ。

    今の理解はこんな感じ。分からないのは、

       ・ Low-Speed では、EOP のみを送るルールがある。
       ・ そうなるとフレームはなくても良さそうなもの。
       ・ SOF は送らなくとも良いのか or 送ってはダメなのか ?

ホストの設計

    随分前置きが長くなった。さぁ、設計をしてみよう。

    Low-Speed では、必ずしもフレームを構成しなくても良さそうなのだが、定期的に EOP を送らなければならない。そこから考えると、1ms 周期のタイマ割り込みを作りそこから、一連の 送受信処理につなげていく 構造が良さそうだ。

    Full-Speed では、最初に SOF を送る。Low-Speed では、SOF は送らない? 結局送信すべきものがないと EOP だけ送る。

    トランザクションの途中では、かならず受信待ちが入るわけだが、ここで割り込みを一旦抜ける。返ってこなかったら、次の 1ms のタイマ割り込みで タイムアウトの処理を行うことにする。これで良いのか? という気もするのだが、動作ポイントは、タイマ割り込みと 受信割り込みということで進めよう。

    次にバッファの受け渡し。受信待ちに入れば、タスクの方が動作可能になる。まず送信について考えてみると、そこで 次のデータを準備したりできるわけだ。だが、ACK が返らない限り バッファが開放できない。送信では(少なくとも)ダブルバッファが必要そうに思える。

    そもそも 1 フレームで 1 つしか送信できないとすると、MaxPS=64 でも 64KB/sec にしかならない。BlueTooth のことを考えると これじゃ帯域が低すぎるような気がする。ダブルバッファでは足りず FIFO にしないといけないかも知れないが、まずは ダブルバッファということにしておこう。

    では受信ではどうなのか? データを受け取り ACK を返せば受信成功だ。で、すぐに次の受信待ちになる。このときに、タスクで受信処理をできるかも知れない。やっぱりダブルバッファは必要そう。

    バッファの構造はどうしよう。

      V-USB では、受信バッファに 全部収めて、さらに usbRxLen / usbRxToken とかの情報を設定するルールだった。ここは、ダブルバッファにするが、同じようなものにすればよさそうだ。

      送信は、3 つのバッファがあって、IN を受信すると エンドポイントに対応したバッファを選んで送信する仕組みだった。送るデータは ACK/NAK などを送るか、バッファに格納されたデータを送るかの 2 種類。

      HOST の場合、ACK を 受信完了で送ることはあるが、NAK/STALL を送信することはない。そのかわり SETUP/OUT に続いて DATA0/1 を送ることになる。また、IN/OUT/SETUP のトークンを送るには、アドレスとエンドポイントの情報が必要。ついでに CRC-5 を生成しないといけなくなった。

    V-USB の送信コードを有効に使うならば、Length とバッファのアドレスを指定して送信する機能と ACK を送信する機能を使うことになる。どちらも 割り込みから抜けてしまうことになっているが、そこは 戻ってきてもらわないと困る。

    あと、SETUP/OUT + DATA で 送信バッファを 2 つ使うのではバッファの効率が悪い。1 つのバッファに詰めることにする。トークンより大きいデータが入っていたら 2 回に分けて送信するようなイメージ。call したい ところは 2 ヵ所だが、サブルーチンにする必要はないかもしれない。

    一方 ACK を返すのは、たったの一ヶ所。こちらは無条件ジャンプで事足りる。

    ここまでがドライバの仕事。要するにバッファの受け渡しで 事が進む。タスク側の処理がこれに加えて必要になる。あと、接続しただとか、切断しただとか状態管理も別途必要で、これについては後で検討しよう。まずは、中核となる部分だが、ここのイメージはできた。

状態監視とか

    接続後の動作しか検討していなかったのだが、気になってきた。今の udrv で 接続や切断、リセットに対応できるのだろうか?

    なにも接続されていない状態では、D+/D- ともに L で SE0 状態。接続されていれば idle 状態。

      この状態をホストがどのように検出できるか?

      受信をスタートさせると SE0 なのですぐに bit0: 送受信要求 が 0 になる。これは、nIRQ が L になることでも検出できる。続けて 2 回実行しても同じ結果なら、接続されていないことが分かる。

      受信をスタートさせても、nIRQ が H なら、接続はされていて idle 状態。

      これを定期的にポーリングしてやれば良いのだろう。

    逆に、ホストがリセットしたい場合、50ms 以上 SE0 状態にする。

      device 側が、これを検出する仕組みは、ホストと同じ。だが、ホスト側で 強制的に SE0 状態にする仕組みを作っていない。

      実をいうと -13 版では、コントロールレジスタに 0x84 を書き込むことで、ENABLE ピンを L にする 仕組みを入れた。このピンでプルアップすることで、device では、切断状態を作り出せる。

      ホストでは、0x88 を書き込むことで、D+ , D- ともに L 出力ということにしようかと思う。ただ、この程度の変更すら規模的に厳しい。ただ、既に SUPPORT_CRC5S=0 としていてそのおかげで入った。要するに ホストで使うトークンや SOF に対して CRC-5 は自動生成しない。生成済みのデータを送る必要があるが、僅か 11 bit 分の CRC 計算だし重くはないだろう。

関連記事
posted by すz at 15:51| Comment(0) | TrackBack(0) | CPLD

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

2012年06月21日

DACを設計してみよう

QFN32 パッケージの MachXO2-256 という FPGA でなにが出来るのか、検討しているのだが。... DAC が作れそうな気がする。FPGA は大概、コア用の電源と IO 用の電源に分かれている。この QFN32 も 例外ではないどころか、VCCIO が 4 つもある。ちゃんと電源を設計すれば、ノイズ的に有利かも知れない。ただ、オーディオDAC に匹敵するようなものには、できないだろう。どこまでのものが作れるかも追求しない。256 に入れられて 検証できるものを目指す。

AVR など PWM を持った MCU は 多いわけだが、ΣΔ変調の DAC を持っているものは、殆ど無い。同じ bit幅でも、ΣΔ変調だと ノイズを減らせるそうだ。ちょっと試してみたい。

ΣΔ変調の原理

    1 bit の ON/OFF だけで DAC を作ると 256 クロック使って ようやく 8bit 幅になる。このことは、PWM と変わらない。ノイズを減らせるのは、どうしてかというと PWM では 0 が続いたあと 1 にするだけのところを、1 を分散させて ノイズの周波数を 上げるためらしい。

    どうやって そういう 0/1 を作るのだろう?

    なかなか理解できなかったのだが、3bit の DAC を使って 8bit 出力することを考えてみたら、分かったような気がする。

    例えば、8'b01011010 を 3bit の DAC で出力するには ... 上位 3bit の 3'b010 をまず出力して 時々 3'b011 も出力することで 中間値を表現する。

    3'b010 を DACで出力すれば 5'b11010 が出力されなかった分として残る。次も 3'b010 なら 5'b11010 + 5'b11010 が残るのだが ... 桁上がりして、6'b110100 になる。だから 3'b011 を DAC で出力して、5'b10100を出力されなかった分として 覚え直す。

    要するに、ずっと 5'b11010 を加算していって、桁上がりしたときに 3'b011 を DACに出力すれば良いのだ。

    では、0/1 だけで表現するには ... 8'b01011010 を ずっと 加算していって、桁上がりしたときに 1 を出力すれば良い。256 回加算すれば、16'b 01011010 00000000 になるから、確かに 8'b01011010 個の 1 を出力したことになる。

    フィルタがどうとか に囚われてよく分からなかったのだが、どうも原理は簡単な話のようだ。

作ってみよう

    module dac # (
    parameter WIDTH = 8
    , parameter WITH_LATCH = 0
    ) (
    input CLK
    ,input STB // active high
    ,input [WIDTH-1:0] I_DATA
    ,output A_OUT
    );

    reg r_stb = 1'b0;
    reg [WIDTH-1:0] r_data = 0;
    reg [WIDTH-1:0] fraction = 0;
    reg [WIDTH-1:0] count = 0;
    reg r_out = 1'b0;

    always @(posedge CLK)
    begin
    r_stb <= STB;
    if (&count | (STB & ~r_stb))
    begin
    count <= 0;
    fraction <= I_DATA;
    r_out <= 0;
    if (WITH_LATCH)
    r_data <= I_DATA;
    end
    else
    begin
    { r_out, fraction } <= { 1'b0 ,fraction }
    + { 1'b0, (WITH_LATCH ? r_data : I_DATA) };
    count <= count + 1;
    end
    end

    assign A_OUT = r_out;

    endmodule

    取り敢えず書いてみたのがこれ。ラッチあり/なし を選べたり ビット幅を指定できるようにしてみた。そうそう、新しい呪文を覚えた。&count は、count の各ビットを AND する意味で all 1 のとき 1 になる。

    書いてみて思ったのだが、毎回 Nbit の加算をしている。これはクロックを上げられないのではないか?

    例えば 16 bit の出力 を 50kHz でするとする。65536 倍のクロック が必要で 計算すると 3.2768 GHz で駆動することに。とりあえず 3bit の DAC を使うとすれば ... 409.6 MHz 。これでも無理な話だが、周波数が減る上に 加算する bit 数も減る。

    さて、この 3bit の DAC もまた、ΣΔで作ってやれば、どうなるのだろう? 同じ 1bit のΣΔでも最大周波数が上がるのではないだろうか?

N bit ΣΔ

    module dac5 # (
    parameter AWIDTH = 5
    , parameter WIDTH = 16
    ) (
    input CLK
    ,input STB // active high
    ,input [WIDTH-1:0] I_DATA
    ,output [AWIDTH-1:0] A_OUT
    );

    reg r_stb = 1'b0;
    reg [WIDTH-1:0] r_data = 0;
    reg [WIDTH-AWIDTH-1:0] fraction = 0;
    reg [WIDTH-AWIDTH-1:0] count = 0;
    reg [AWIDTH-1:0] r_out = 0;

    wire [WIDTH-AWIDTH-1:0] WK = 0;
    wire [AWIDTH-1:0] A_ZERO = 0;
    wire [AWIDTH-1:0] A_WK = ~A_ZERO -1;
    wire [WIDTH-1:0] I_MAX = { A_WK , ~WK };
    wire [WIDTH-1:0] I_DATA2 = (&I_DATA[WIDTH-1:WIDTH-AWIDTH])
    ? I_DATA : I_MAX;

    always @(posedge CLK)
    begin
    r_stb <= STB;
    if (&count | (STB & ~r_stb))
    begin
    count <= 0;
    r_data <= I_DATA2;
    fraction <= I_DATA2[WIDTH-AWIDTH-1:0];
    r_out <= I_DATA2[WIDTH-1:WIDTH-AWIDTH];
    end
    else
    begin
    { r_out, fraction } <= { A_ZERO , fraction } + r_data;
    count <= count + 1;
    end
    end

    assign A_OUT = r_out;

    endmodule

    例えば 3bit DAC では、3'b111 より大きな値は、出力できないので 上限値を設けてやる。後は最初に説明したとおり。

    すなおに、N bit の R-2R ラダー DAC を使っても良いのだが、これと 1bit ΣΔを組み合わせてみたい。

    module dacx # (
    parameter WIDTH = 8
    ) (
    input CLK
    ,input STB // active high
    ,input [WIDTH-1:0] I_DATA
    ,output A_OUT
    );

    reg [2:0] hi_clk;
    wire [2:0] hi_out;
    wire lo_stb = CLK;

    always @(negedge CLK)
    begin
    hi_clk <= hi_clk + 1;
    end

    dac #( .WIDTH(3) , .WITH_LATCH(0) ) dac_lo ( .CLK(CLK), .STB(lo_stb)
    , .I_DATA(hi_out), .A_OUT(A_OUT) );
    dac5 #( .AWIDTH(3) , .WIDTH(WIDTH-3) ) dac_hi ( .CLK(hi_clk[2])
    , .STB(STB), .I_DATA(I_DATA) , .A_OUT(hi_out) );

    endmodule

    こうかな?

    ところで、ラッチなしにして、2 ** N 回クロックを待たないで、適時値を変更しても、それなりに追従する。dac5 の方もラッチなしに対応しておいた方が応用が効きそうだ。

    あとは、フィルタがどうとか ... だが、クロックはたっぷりある。簡単なものなら加算だけで作れそうな気がする。これはまた別途考えてみたい。

    それとは別に、データをどうやって受け取るかという問題もある。パラレルは無理だしなにより面倒。やっぱり SPI ? 実は、JTAG 経由の通信というのも検討中。

R-2R ラダー DAC

    簡単に説明すると

    A_OUT_2 --- R ---+----- Analog
    |
    2R
    |
    A_OUT_1 --- R ---+
    |
    2R
    |
    A_OUT_0 --- R ---+
    |
    2R
    |
    GND

    こんな回路。抵抗が 2 種類しかいらないが、値が厳密。あと電流は取れないから、オペアンプを使ってボルテージ・フォロワを組む。

      FPGA 出力には、(数十Ωの)抵抗分がある。これにより R と 2R の比が崩れるので注意。また、出力電流を 4段階ぐらいで調整できる(たぶん 抵抗分が変わる)ので、チューニングに使えるかも。

    秋月で 1608 をリール売りしているから、1K/2K , 1.2K/2.4K , 1.5K/3K とかの組みを選ぶと良さそう。2500 個もあるから、選別しほうだい。

    選別には、SMDテスターが便利そう。3000 カウントだから 3K は避けたほうが良いか。

動作周波数について

    2 つの モジュールにすることで、周波数を上げられないか? やってみたのだが、4bit でも 16bit でも 150MHz ぐらいで、あまり変わらない。2 つの モジュールにしても、やっぱり あまり変わらない。要するに CLK 自体が ボトルネック。150MHz ということは、12bit で 36.6 kHz が上限。3bit の R-2R DAC + 10 bit ΣΔあたりが妥当?

    ところで、100 MHz で 駆動したとして 例えば 010101 というパターンが出てきたとき素直に駆動して良いものなのだろうか? 1bit ならともかく、多ビットだと 周波数を落としたくなる。

    PWM だと 周期が低くなりすぎるし ... PWM + ΣΔ が良いのだろうか? PWM でも Phase Correct MODE というのが AVR にある。UP していって TOP まで行ったら 逆に DOWN していく。こうすると スイッチング周波数は、普通の PWM の 1/2 。例えば 4bit Phase Correct MODE PWM なら CLK が 100 MHz でも スイッチング周波数は、3 MHz ぐらいまで落とせる。ちょっとこれを検討してみたい。2 つの モジュール は、既に出来ているから、片方を PWM にするだけ -- 難しいことはない。

    3bit の R-2R DAC で PWM を使い 、さらに上位を ΣΔにするのは、ちょっと難しい。これも 挑戦してみよう。

    PWM を使うのとは別に 010101 というパターンを 例えば 000111 というのに変換してしまうのはどうだろう?

    考えたのは、次のやりかた。

      今出力しているのが 1 だとする。次に出力するのが、01 だと分かっているなら 10 にしてしまうのだ。(0 についても同様)

      1 01 → 1 10
      0 10 → 0 01

      要するに 単独の 1/0 を出ないようにしてしまうわけだから 絶対に 2bit は続くわけだ。3 bit に拡張するなら、上記に加えて

      1 001 → 1 100
      0 110 → 0 011

      とする。これで 3 つ以上連続することが保証される。

      ただ、この処理には問題がある。例えば 2 を出力する場合 、わざわざ離れた位置に 1 を配置するわけだが、これを くっつけてしまう。連続した数を数えておいて 、くっつける処理をキャンセルする必要がある。

      なにやら面倒な話になってきた。やはり PWM の方が良さそうな。

SPI 通信の例

    秋月で売っている DAC MCP4922 や ADC の MCP3002 は、SPI で通信する。プロトコルを調べてみた。

    DAC :
     ・ 1 データ 送受信の度に CS を 上げ下げする必要がある。
     ・ 1 データは、MSB first , 16 bit 単位

    bit15 ~ A/B チャネル選択
    bit14:12 config
    bit11:0 データ[11:0]

    ADC:
     ・ 1 データ 送受信の度に CS を 上げ下げする必要がある。
     ・ 1 データは、MSB first , 24 bit 単位

    bit23 START (1)
    bit22:20 config (チャネル含む)
    (ADC → Master)
    bit19 NULL (0)
    bit18:9 データ[9:0]
    bit8:0 データ[1:9]

     ・ START を 遅らせて 16 bit 単位 にすることも可能

    bit15 (0)
    bit14 START (1)
    bit13:11 config
    (ADC → Master)
    bit10 NULL (0)
    bit9:0 データ[9:0]

    CS を 上げ下げする必要があるのは、FPGA 側としては、都合が良いかも。SPI というからには、MSB first にした方が自然。バイト単位になるような配慮も必要。変換タイミングは、正確にしたいだろうから 別途ということになりそう。(AVR だと PWM を使う)

    もっと大きな問題があった。クロックをどうしよう。変換タイミングを完全に正確にするには、送信側から CLK をもらうか、DAC 側のタイミングに 合わせてもらうか ...

    送信側から CLK をもらうにしても 50 MHz 〜 100 MHz というのは厳しい。規模が大きい FPGA だと PLL があるが、MachXO2-256 にはない。

    今回のは、DAC 側のタイミングに 合わせてもらうことにしよう。それにしても 問題が。AVR を使うならなんとかなるのだろうが、PC と通信する場合は、どうしよう。

    普通にシリアルで CTS/RTS 制御で良いのかな? 115Kbps だと 11.5 Kbytes/sec になってしまって 性能が足りない。1M bps とか できたら 3M bps とか欲しいところだが ... 途切れなく送り込めるのかどうかが問題。

    DAC を設計するなら MachXO2-1200 ぐらいは 欲しいところで、PC とのインターフェイスに FT2232H を使いたいということになりそうだ。 そういうものとして、『MachXO2 Breakout ボード』があるのだが ... 今回は 256 を使いこなすのがテーマなのでパス。とにかく AVR で使えるものだけを考えよう。

    ここまでの検討で
     ・ SPI を使う。
     ・ データリクエストを DAC 側が出す。
     ・ 16bit 単位
     ・ DAC データは、12bit-14bit の範囲
    ということに決めた。

    プロトコルは、別途。

ソースコード

関連記事
posted by すz at 22:55| Comment(0) | TrackBack(0) | CPLD

2012年06月17日

MCPU -- A Minimal 8 Bit CPU

MCPU -- A Minimal 8 Bit CPU in a 32 Macrocell CPLD

    というのを思い出した。これは、CPLD で実装できる 極端にシンプルな 8bit CPU 。

    実際に CPLD に実装しても メモリを別に用意しなくてはならない 。FPGA だとメモリがあるのが普通なので、全部を実装可能だ。でも、どうせならもっと規模が大きい CPU を実装したい ... ということで今までは興味がなかった。

    128スライスしかない MachXO2-256 ならどうだろう? メモリを載せて実装できるのだろうか? 試してみたくなった。

    まずは、資料の入手から。"MCPU - A Minimal 8 Bit CPU in a 32 Macrocell CPLD" でググると PDF ファイルがヒットする。まずは、これを入手。

  • http://opencores.org/project,mcpu

    どうも プロジェクトホームページは、ここらしい。アクセスできる人は、ここから入手するのが良さそうだ。

MCPU の概要

    ドキュメントを読むと 命令は なんと 4 つしかない。

    そんなものでプログラムを組めるのだろうか? という疑問にマクロを示すことで答えている。

      .. 確かにプログラムを書けそうな気はする。

    ソースコードは、VHDL版、Verilog 版が、ドキュメントの最後に添付されている。

      私は、VHDL版だけ見て、自己流の Verilog 版を作ることにした。... というより Verilog 版があるのを知らないで作ってしまった。まぁ、後でいじるつもりだし著作権的には自己流の方が都合が良いかも知れない。

      -- 著作権は、アイディア(ロジック)を保護するものではなく、表現を保護するもの。言語が違うから、表現も違う。オリジナルは尊重すべきだが、どうもこういうことらしい。

      verilog 版をよく見たらバグがあるようだ。要注意。

最初のインプリメント

    さて ... 規模をまずは見たい。メモリを付けて合成してみよう。

    メモリ空間は、なんと 6bit 64バイト分しかない。初期値付きの RAM として合成してみたら 59/128 スライス しか消費しなかった。

      module sram # (
      parameter SIZE = 64
      ) (
      input CLK,
      input WEB,
      input [5:0] ADDRB,
      input [7:0] DIB,
      output [7:0] DOB
      );
      reg [7:0] mem [0:SIZE-1];

      reg [7:0] r_addr;
      always @(negedge CLK)
      begin
      if (WEB) mem[ADDRB] <= DIB;
      r_addr <= ADDRB;
      end
      assign DOB = mem[r_addr];

      initial
      begin
      $readmemh("rom_data.mem" , mem, 0 , SIZE-1);
      end
      endmodule


    つけた RAMは、こういうもの。negedge で駆動。アドレスラッチ付き。

構想

    まずアドレス空間が狭すぎる。ハーバードアーキテクチャにしよう。これで RAM 専用空間に 64 バイト割り当てられる。

    で、何をしたいかと言うと I2C やらタイマがある EFB を割り当てたい ... ただ EFB の空間は 256 バイトもある。

EFB の調査

    MachXO2 和訳テクニカルノート にある
     
  • TN1205 ユーザフラッシュメモリ(UFM)と組み込み機能ブロック(EFB)の使用ガイド
    を見てみよう。

    まずは、アドレスマップ

     プライマリI2C   0x40 〜 0x49
     セカンダリI2C  0x4A 〜 0x53
     SPI       0x54 〜 0x5D
     タイマ/カウンタ  0x5E 〜 0x6F

    一応 64 バイト以内に収まっている。0x70 〜 0x7f の 16 バイトも空いている。あと、使わない機能のレジスタは RAM としても使えるはず。

    独自の I/O ポートを付けたいなら どこかを削らなければならない。これは、後で考えよう。

    次に EFB との接続インターフェイス。

    EFB は、WISHBONE インターフェイス。この説明は、TN1205 に載っている。

  • wb_clk_i posedge で動作
  • wb_rst_i active high の同期動作
  • wb_adr_i {2'b01, adreg}
  • wb_dat_i EFB の入力
  • wb_dat_o EFB の出力
  • wb_we_i 1 で Write だが レベルセンシティブ
  • wb_stb_i 1 で EFB を選択 ( adreg[5:4] != 2'b11 )
  • wb_cyc_i 1 でバス有効?
  • wb_ack_o

    こんな感じだが、ライトもリードも 最低3サイクル必要と書いてある。

    WISHBONE 用のクロックをまず生成し、それを3分周してマスタクロックにすることにしよう。

MCPU の拡張 (1) ハーバードアーキテクチャ

    これは、実はすごく簡単。(元が簡単だし)

    メモリから READ しているもの のいくつかを プログラムメモリに変えれば良い。
     
  • adreg にロードしている所
     
  • 上位 2bit だけをロードしている所

MCPU の拡張 (2) アドレス空間の拡張

    ハーバードアーキテクチャ に変更しても プログラムメモリは、64B しかない。MachXO2-256 は、128B までいけるはずなのだ。もう少し増やせないか?

    空間を増やすためには、プログラムメモリのビット幅を増やせば良い。分散メモリの場合、使わない bit は 最適化で消えるから、気前よく 12 bit にしてしまおう。

    で、アドレス空間自体は、7bit にする。これで規模がどうなるか。

    Number of Inst. 64(org) 64 96 128
    Design Summary
    Number of registers: 32 34
    Number of SLICEs: 51 92 107 124
    SLICEs(logic/ROM): 32 32
    SLICEs(logic/ROM/RAM): 19 60 75 92
    As RAM: 6 24
    As Logic/ROM: 13 36 51 68
    Number of logic LUT4s: 77 126 156 188
    Number of distributed RAM: 6 24
    Number of ripple logic: 5 5
    Total number of LUT4s: 99 184 214 246

    分散メモリは、ROM に格納するデータによって規模が変わる。64B 分しか使わないなら 128B 全部使うのと比べ 32 スライスも減った。

    同じ 64B でも ハーバードアーキテクチャ版から随分増えた。これは、メモリ(RAM) を 16B から 64B に増やしたのが主な理由。-- メモリが 16B しか使えないという制限で良いなら、変に拡張しないほうが良い。

      ちなみに、オリジナルは 57 スライスで、ハーバードアーキテクチャ版は規模が減っている。

      メモリは、RAM 64B だったのが、ROM 64B + RAM 16B になったわけだ。2 種類に増えた上、トータルの容量も増えた。さらに EFB まで付けて 減ったわけだ。

      オリジナルは、ハーバードアーキテクチャ版を元に書きなおした。分散メモリの RAM実装が 重いということになるのだろう。

アドレスマップ と I/O ポート

    何種類か設計したわけだが、アドレスマップを整理して I/O ポートをどうしたら良いのか考えてみよう。


    オリジナル EFB版 MCPU EFB + アドレス拡張
    0x00 - 0x2F : プログラム EFB EFB
    0x30 : 0xFF (allone) 0xFF (allone) 0xFF (allone)
    0x31 : 0x00 (zero) 0x00 (zero) 0x00 (zero)
    0x32 : 0x01 (one) 0x01 (one) 0x01 (one)

    0x34 - 0x3F : RAM RAM
    0x40 - 0x7F : --- ---  拡張RAM

    プログラムROM : ---- 64B 64B - 128B

    マクロを見ると 0xFF,0x00,0x01 がメモリ上に必要なのだ。そして書き換えられるのは都合が悪い。アドレスは、EFB 版とアドレスを合わせることにすると 0x30 〜の 3 バイトが良さそうだ。

    実際に付けるかどうかは、別にして ... 出力ポートは、0x30 - 0x32 の 3バイトを割り当てることにする。入力ポートは 0x33 の 1 バイトのみ ということにしたらどうだろう。

ソースコード

  • qfn32samples-04.zip

      バグなど
    • mcpu.v , mcpu_efb.v , mcpu2_efb.v の sram モジュールで WEB の論理が逆になっていた。


追記: シミュレータ(iVerilog) でデバッグ

    iVerilog を使ってデバッグした。いろいろ修正したが、どうやら動かせるようになったようだ。

    動かせたのは、EFB を組み込んだりした拡張版。オリジナルに一番近いのは、まだ。

    テストコードは、PDF に載っていたサンプルコード。どうも 最大公約数をもとめるものらしい。あと、アセンブラもどきを perl スクリプトで組んだりもしている。

    作った MCPU のバージョンは4つある。

      (1) mcpu.v オリジナルをもとに作ったもの
      (2) mcpu_efb.v ハーバードアーキテクチャ版 にした上で EFB を組み込んだもの。
      (3) mcpu1_efb.v 拡張しやすいように、rewrite したもの。
      (4) mcpu2_efb.v アドレス空間を拡張したもの。

    (1) だけ動いていない。

    アセンブラもどきは、後方参照した場合、アドレスが分からないというレベルのもので、出力を参考にしてコードを転記している。

    規模は、(3) が、53スライス、(4) が 103 スライス。EFB を組み込んだ上で、adreg, O_DATA などをピンに出力している。(4) は、LDI 命令を追加しての規模で、なしにすると 92。なにか使い物になりそうな気もしてきたのだが ...

    よくよく考えると bit 操作すらできない代物だった。(3) は随分余裕があるようなので、これを元に ALU を付けたCPUを設計したほう方が実用的だろう。

    あと RAM のアクセスの仕方に難がある。分散メモリ専用ならこれでも良いのだが、今はブロックRAM を使えない。一方 ROM の方は問題ないようだ。

    (3) について、ちょっとメモっておこう。


    ADD 命令 + STA 命令
    _______ _______ _______
    CLK ______| |_______| |_______| |______|

    code_data 01AAAAAA | 01BBBBBB |

    inst 000 | 101 | 000 | 110
    ______________
    WE _______________________________________| |__

    @
    (RAM_USE_CLK)
    ______________
    WE_mem ______________________________| |__
    @ @ @ @

    分かりにくいと思うが、2 クロックで 1 命令実行 。posedge で CPU は動作している。最初の状態は、000 で アドレスだけは確定している。

    code_data は ROM 出力で negedge で変化する。アドレスは、ラッチを持たず ROM データをそのまま使っている。

    で、ROM データは、毎クロック読み出しているが、inst==0 の時だけの方が良いかも知れない。

    問題の RAM は、WE の立ち上がりで書き込みとしていた。一般的なコードにするとすれば ...

    posedge で動作させることにして、WE = 1 で書き込み。ただし WE は半クロック前に確定させないといけない。

    実際にやってみたら動いたのだが、規模は +7 スライスの増加。

     ・ qfn32samples-05.zip

新たな CPU へ

    mcpu2 では、命令用メモリを 12bit にした。そのうち 下位 7bit はアドレス。命令用に 5bit 使えるわけだが今は 2bit しか使っていない。ここを再構成し、ALU を入れることによって 別の CPU にしたい。

    といっても基本は変えない。演算する、ストアする、条件ジャンプするという枠組みは出来ているのだ。枠組みを壊さなければ、書きなおすところが圧倒的に減る。

    改造の要点

      10 : STA -- 変更しない。
      00 : NOR -- 演算命令 8 つ に変更

      使う ALU は、自作AVRコア用 ALU 。ちょうど 8bit だし、都合が良い。
      00XXX -- 3bit 使えるが まるまる ALU の S に使う。

      01 : ADD -- 即値演算命令 4 つに変更

      即値演算にも ALU を使う。ただし、論理演算のみ。

      01XX -- 即値は、2bitしか使えないのだ。命令の割り当ては、演算命令との兼ね合いで決める。

       即値は、01 S[1] S[0] 8bit即値 (S[2] = 1) とすることにしよう。
       演算は、00 S[1] S[0] S[2] 7bitアドレス

      11 : JCC -- 条件を拡張+α

      条件ジャンプ命令は、3bit 使える。C or Z で 1bit ,条件を逆にするのに 1bit 。あと 1bit は、C の値としておこう。 C をロードする命令がないのだ。

    動くかどうか分からないのだが、ROM 64 words , RAM 16 bytes なら 97 スライスに収まった。これならいけそう。

    命令表

      (inst == 100)
      00 00 0 ADD
      00 01 0 ADC (with carry)
      00 10 0 SUB
      00 11 0 SBC (with carry)
      00 00 1 AND
      00 01 1 XOR
      00 10 1 OR
      00 11 1 LD

      (inst == 101)
      01 00 ANDI
      01 01 XORI
      01 10 ORI
      01 11 LDI

      (inst == 110)
      10 XX X ST

      (inst == 111)
      11 00 C JCS jump if carry set
      11 01 C JCC jump if carry clear
      11 10 C JNE jump if zero-flag set
      11 11 C JEQ jump if zero-flag clear

      carry,zero-flag が変化するのは、ADD などの 算術演算命令のみ。

    全部で 17 命令。... だがコード上は 4 種類のまま。ALU が何をしているか関知してないから。

    シフト命令は .. ないなぁ。
    これを付けるとすれば ... 001 〜 011 の inst を使うことになる。

      (inst == 100)
      10 00 STA
      (inst == 001)
      10 01 拡張命令 1
      (inst == 010)
      10 10 拡張命令 2
      (inst == 011)
      10 11 拡張命令 3

    どんな命令にするか別にして、こういう割り当てにする。

    だいたい作った上で動作周波数を見てみた。4 グレードで 20 MHz 〜 25 MHz ぐらい。クロックは、EFB を操作する都合で 3 倍クロックを入力している。命令実行は 2 クロックに 1 回なので 1/6 ... 4 MIPS ぐらい?

    多分なにか間違っている。制約の設定が足りないのだろう。だが、EFB のクロック関係は適当すぎたかも。

    EFB を外して CLK で駆動したら 22.9 MHz になった。これでも 違うはずだが、この 2 倍は無理だろう。
    改造前の mcpu2 だと 45.5MHz と出た。ALU 周りがボトルネックと見做されたわけだ。だが、1.5 クロックかけて演算している。これを 0.5 クロックと見られたら 低い結果になりそう。

      どうも、MULTICYCLE制約というので設定するらしいので試してみた。

      FREQUENCY PORT "CLK" 36.000000 MHz ;
      MULTICYCLE TO CELL "alu_impl_I/alu_impl/alu_lo/akku_i0" 1.500000 X ;
      MULTICYCLE TO CELL "alu_impl_I/alu_impl/alu_lo/akku_i1" 1.500000 X ;
      MULTICYCLE TO CELL "alu_impl_I/alu_impl/alu_lo/akku_i2" 1.500000 X ;
      MULTICYCLE TO CELL "alu_impl_I/alu_impl/alu_lo/akku_i3" 1.500000 X ;
      MULTICYCLE TO CELL "alu_impl_I/alu_impl/alu_hi/akku_i4" 1.500000 X ;
      MULTICYCLE TO CELL "alu_impl_I/alu_impl/alu_hi/akku_i5" 1.500000 X ;
      MULTICYCLE TO CELL "alu_impl_I/alu_impl/alu_hi/akku_i6" 1.500000 X ;
      MULTICYCLE TO CELL "alu_impl_I/alu_impl/alu_hi/akku_i7" 1.500000 X ;
      MULTICYCLE TO CELL "C_68" 1.500000 X ;
      MULTICYCLE TO CELL "Z_69" 1.500000 X ;

      ... と 22.9 MHz が 57.0 MHz まで上がった。 いくらなんでも 上がりすぎのような ...

      FREQUENCY PORT "CLK" 36.000000 MHz ;
      DEFINE CELL GROUP "akku" "alu_impl_I/alu_impl/alu_hi/akku_i7"
      "alu_impl_I/alu_impl/alu_lo/akku_i1"
      "alu_impl_I/alu_impl/alu_lo/akku_i2"
      "alu_impl_I/alu_impl/alu_lo/akku_i3"
      "alu_impl_I/alu_impl/alu_lo/akku_i0"
      "alu_impl_I/alu_impl/alu_hi/akku_i6"
      "alu_impl_I/alu_impl/alu_hi/akku_i4"
      "alu_impl_I/alu_impl/alu_hi/akku_i5"
      "Z_69"
      "C_68" ;
      DEFINE CELL GROUP "code" "rom_impl/r_addr__i5"
      "rom_impl/r_addr__i4"
      "rom_impl/r_addr__i3"
      "rom_impl/r_addr__i2"
      "rom_impl/r_addr__i6"
      "rom_impl/r_addr__i1" ;
      MULTICYCLE FROM GROUP "code" TO GROUP "akku" 1.500000 X ;

      よく分かってないのだが、GROUP というのを定義して、その間を設定するやり方もある。指定したいのは、こっちのような気がする。基本的に rom の アドレスが変わることによって 結果が出るのだ。これだと 37.6 MHz ...

      mcpu2 だと 45.5MHz が 63.0 MHz に。

追記

    mcp3 の 命令の仕様を決めて、簡易アセンブラを 作った。ちょっと改良して後方参照も 見るようにした。やはり、ないとデバッグが不便。あと 出力ポート追加。

      MCPU3 命令表(全 21 命令)

      (inst == 100)
      00 00 0 ADD
      00 01 0 ADC (with carry)
      00 10 0 SUB
      00 11 0 SBC (with carry)
      00 00 1 AND
      00 01 1 XOR
      00 10 1 OR
      00 11 1 LD

      (inst == 101)
      01 00 ANDI
      01 01 XORI
      01 10 ORI
      01 11 LDI

      (inst == 110)
      10 00 X ST
      (inst == 001)
      10 01 ASR
      (inst == 010)
      10 10 ROL
      (inst == 011)
      10 11 ROR
      (inst == 111)
      11 00 C JCS jump if carry set
      11 01 C JCC jump if carry clear
      11 10 C JNE jump if zero-flag set
      11 11 C JEQ jump if zero-flag clear

      (MACRO)
      SEC set carry JCS *+1 (C=1)
      CLC clear carry JCC *+1 (C=0)
      JMP addr JCC adr (C=0), JCC adr (C=0)
       ・carry が変化するのは、ADD などの 算術演算命令
        と JCC 等のブランチ命令, シフト命令
       ・zero-flag は、論理演算やロード LD,LDI でも変化

    これで コードは作れるようになった。最初に作ったのは、MCPU のサンプルの GCD の移植。単純変換したら、1命令しか減らなかった。

    さて、次は EFB にアクセスしてみたい ... のだが、ちょっと出来が悪かったので再考。

      ・ まず 3 倍速というのがダメ。CPU の CLK が 2:1 になり ボトルネックになる。2 倍速にする。
      ・ 2 倍速 を検討したのだが、CPU は、ちょうど 2 クロック毎の処理になっている。
      ・ ROM を非同期アクセスに変更すると具合が良いことが分かった。

      READ サイクル
      _____ _____
      CLK | |_____| |_____|

      inst | 000 | 100 |
      ____________
      wb_cyc ______| |_____
      @ @
      ADDR DATA @
      ラッチ 実行
      WRITE サイクル

      _____ _____
      CLK | |_____| |_____|

      inst | 000 | 110 |
      _______________________
      wb_we | |_
      ____________
      wb_cyc ______| |_____
      @
      ADDR
      DATA

      READ/WRITE サイクルを図示するとこんな感じ。
      READ では、wb_cyc=1 と同時に アドレスが決まってないといけない。アドレスは、命令に含まれ、CLK 立ち上がりで読み込み開始。次の CLK_2X で データを読み込めるが、ここでラッチしないといけない。

      WRITE は、命令を読みこめばただちに分かる。DATA も用意しなければならないが、akku(アキュームレータ)を WRITE するだけなので、命令と同じタイミングで読める。

    あと、制約を書くのに C_68 とか変なサフィックスになるので、reg [1:0] flags に変更。

    だいぶ仕上がって来た。いままで作ってきたサンプルは、MachXO2-256 をターゲットにしている。当然 『MachXO2 breakout ボード』でも 動くはず。EFB アクセスはこれで確認してみたい。



      シミュレーション結果。赤い不定値部分は、RAM の範囲外ということ。EFB を読み込んでいるわけだ。(演算命令なので、値は使わないが)。 タイミングは上に書いた通りになっている。アクセスする時だけ wb_cyc をアサートしているので、wb_we のタイミングも想定通り。... なのだが、仕様を勘違いしているかも。

      ところで、EFB を使ったときの 最大周波数が低く出てしまう。赤い不定値部分のとなり -- RAM への書き込みをしているが、WE_mem が 1 になって、値が書き込んだときの値に変わっている。どうも ここがボトルネックと解析されているのだが、この値はだれも使わない。from WE_mem を 6X と設定したら CLK_2X が 61.5MHz になった。(命令実行は 1/4 なので 15 MIPS といったところ -- たいしたことはない)。

    あと作っておきたいのは、JTAG 経由の通信 。

関連記事
posted by すz at 10:34| Comment(109) | TrackBack(0) | CPLD

2012年06月13日

FPGA時計の設計

QFN32の FPGA』 を使ったなにかを設計してみたい。この際 AVR 使った方が有利で楽なものでも構わないが、実際に動かせるもの。ALU では、それを使ったものの敷居が高い。

で、時計はどうだろう? プロセッサなしで動かすには一体どうするのか? 実は意外にも簡単なことかも知れない。

構想1

    まず出力。今は 4桁の 7セグが入手できるからそれを使う。時計の : がないが、とりあえずは気にしない。気にする場合は、(配線が面倒になるのと引換に) 2桁の 7セグ x2 で間に LED を 2個入れられるようにして置けば良い。

    この FPGA だが ... DRIVE 能力の設定がある。LVCMOS33 だと デフォルト 8mA 、最大では 24mA の設定ができる。ただ、24 mA 流せてもコモンは 7 倍になるから 足りない。ドライバは必須。カソードコモンだと NPN トランジスタ が使えて楽そうな気もするのだが、7セグ側を H にして点灯させないといけない。そうなると白色 LED を使うのは厳しくなる。

    一応アノードコモンを使い 7セグ側を L で点灯ということにしておきたい。コモンのドライブも L 。こういったものは、出来た後見直すことにしよう。

    ピン数の合計 7 + 4 + 1(2) = 12(13)

      使える I/O は 21 しかない。ちゃんと入るものなのかどうか、常に気にするようにしよう。

    次にクロック。ダイナミック点灯や ボタンでのオペレーションを考えて 128 Hz を基本としておこう。 8MHz なら 128 で割り切れる。32768 Hz を原発信にしたい気もする。これは、あくまで最初の想定。出来上がってしまえば、対応できる範囲というのも分かってくるだろう。

    さて、これはどうやって入力するか -- 。取り敢えずは、外部のオシレータを想定すれば良い。ただ、水晶発振回路を内蔵したい。ピン数は、2 と見積もっておく。

    ピン数の合計 14(15)

    入力。時刻の設定は出来ないと困るだろう。最も簡単なのは、SEL と UP の 2 つ。DOWN も付けると、よくある時計のオペレーションになるはず。一応 3 つと見積もる。

    ピン数の合計 17(18) --- 残り 4(3)

    あと最大 4 つしか余らない。まぁ機能など付ける余裕もなさそう。JTAG はできれば空けときたいので、4 つ余らせることにしよう。

構想2 カウンタと表示

    ここからは、内部をどうするか ...

    まず時計というからにはカウンタが必要だ。それをどういう風に設計するか?

    前提としてダイナミック点灯するわけだ。ならば、出力は 4bitの BCD 1つで良い。これを 7seg エンコーダを通して 表示させるわけだ。

    考えたのだが、0〜59 や 0〜24 の 2 桁カウンタのモジュールをまず作ろうと思う。最大値は モジュールのパラメータで指定。あと 出力は 4bit で HI/LO を切り替える SEL 入力を持つことにする。

    入力は、128Hz のクロック と カウンタ動作の指定 UP(/DOWN)。あと上位に対する UP(/DOWN)。まずは、DOWN は後回し。回路が入るようなら検討。

    7seg エンコーダは、分散メモリを使ったテーブル ... にしたかったのだが Lattice の場合、妙な Warning が出るので assign と ? 演算子だけで何とかしようかと思う。(テーブルと同じ効果になるはず)。

構想3 カウンタと表示(続き)

    上のカウンタモジュールを 3 つ使い 時分秒に割り当てる。出力も 3 つ。とりあえず、時分 と 分秒 を表示する 2 つのモードを作ろう。

    セレクタでのこの 3 つの出力を切り替えられるようにして、モジュール内の HI/LO を切り替え を合わせてダイナミック点灯させる。

      ダイナミック点灯の 2bitカウンタがあるとする。下位は HI/LO に割り当てる。上位だけ考えれば良いのだ。

      0 1
      disp_mode 0 HH MM
      disp_mode 1 MM SS

      こうなるようにすれば良い。

    さて、これだけか? というと違う。時刻の設定である。時刻の設定ではセレクトされたものがブリンクする。設定モードについて考えておこう。

    モード自体の定義は、次のようにしよう。

      set_mode 0 : 通常表示 (HH:MM)
      set_mode 1 : 時設定 (HH:MM) で HH ブリンク
      set_mode 2 : 分設定 (HH:MM) で MM ブリンク
      set_mode 3 : 秒設定 (MM:SS) で SS ブリンク

      blink off blink on
      0 1 0 1
      set_mode 0 : HH MM HH MM
      set_mode 1 : HH MM -- MM
      set_mode 2 : HH MM HH --
      set_mode 3 : MM SS MM --

      ( -- : blank )

    だいぶややこしくなってきた。が、所詮これだけ。全部で 16 通りしかない。あと blink 信号は、クロックから 1Hz を取り出せば良い。

構想4 ボタン

    次は、どうやってボタンから入力をするかに飛ぼう。

    例えば、SELボタンを押したときは、set_mode をインクリメントするだけで良い。だが、どうやって押したと認識するのか? スイッチの入力をそのまま入れたのでは、128 Hz で set_mode が回転するだけだ。また、チャタリング対策というものも必要になる。

      (128 Hz で次の処理を行う)
      N 回 同じ入力が続いたら ボタンが押された、離されたと認識する。
      押された、離された という状態を作り、離された→押された と変化したとき ボタンON。
      使う側が、ボタンONを認識して リセット を送ってきたら (非同期に)ボタンOFF。

    こういう処理をするボタンモジュールを作ろうと考えている。

    N 回 同じ入力というのは、チャタリング対策。これを を認識するのに N -1 bit の レジスタが必要。N=4 にしたいなら、 128 Hzは、周波数が高すぎるかも知れない。これは、後で要調整。
 
実装編 カウンタ

    以上で重要な部分は説明した。後は実際のコードを示していこう。

    module clock_counter # (
    // parameter MAX = 60
    parameter MAX = 24
    ) (
    input CLK
    , input I_UP
    , input SEL // select output
    , output [3:0] O
    , output O_UP
    );

    reg [3:0] lower;
    reg [3:0] upper;
    reg r_ovr;

    assign O[3:0] = SEL ? lower[3:0] : upper[3:0];
    assign O_UP = r_ovr;

    always @(negedge CLK)
    begin
    if (I_UP)
    if ((upper >= (MAX-1)/10) & (lower >= (MAX-1)%10))
    begin
    r_ovr <= 1'b1;
    upper <= 0;
    lower <= 0;
    end
    else if (lower >= 9)
    begin
    r_ovr <= 1'b0;
    upper <= upper + 1;
    lower <= 0;
    end
    else
    begin
    r_ovr <= 1'b0;
    lower <= lower + 1;
    end
    else
    r_ovr <= 1'b0;
    end
    endmodule

    説明したとおりのもの。規模は1つ 10スライスのようだ。3つ使うから、これだけで 30 スライス。

実装編 ボタン

    module button
    (
    input CLK_128HZ
    , input I
    , output O
    , input R
    );

    reg [3:0] i_stat;
    reg prev_stat;
    reg r_out;

    always @(R, negedge CLK_128HZ)
    begin
    i_stat <= { i_stat[2:0] , I } ;
    if ( { i_stat[2:0] , I } == 4'b0000 )
    prev_stat <= 1'b0;
    else if ( { i_stat[2:0] , I } == 4'b1111 )
    prev_stat <= 1'b1;
    if (R) r_out <= 1'b0;
    else if (~prev_stat & ( { i_stat[2:0] , I } == 4'b1111 ))
    r_out <= 1'b1;
    end
    assign O = r_out;
    endmodule

    これもまた説明どおり。これは、4スライス。最低 2 つ使う。

一応完成

  • qfn32samples-03.zip

    Diamond プロジェクト込みのソースを置いておく。iVerilog シミュレータも一応通している。

    // XO2-256 modified ROM/RAM XO2-256
    // 27 B 1 24 VCC
    // 28 G 2 23 DIG1 25
    // 29 3 22 DIG2 23
    // 30 4 21 DIG3 21
    // 32 5 20 DIG4 20
    // 1 6 19 CLK_1HZ 17
    // 4 C 7 18 BTN_DN 16
    // 5 D 8 17 N.C.
    // 8 E 9 16 BTN_UP 14
    // 9 A 10 15 BTN_SEL 13
    // 10 F 11 14 CLK 12
    // GND 12 13 CLK_OUT 11

    ピン配置はとりあえずこうした。DIGは、LVCMOS33 の 24mA に設定。ACTIVE_HIGH に変更して、3mA 程度まで電流を減らせば直接ドライブできるかも。

    機能としては、
  • down ボタンを付けた
  • 7セグ制御の正論理・負論理を切り替えられるようにした。
  • クロックは、128HZ の倍数なら OK なようにした。

    一応説明で言及したものは、入れたことになる。-- これで規模は 86/128 スライス。
    機能追加はなかなか厳しそうだが、アラームクロックにするのは ... 可能かも。time_couner に比較用のレジスタを作って ... まぁ今後の課題にしておこう。

    追記: アラーム機能を付けてみた。

    .. といっても ポートを増やしたくなかったので、秒を示す LED を 1分間 高速点滅させるだけ。機能もちょっと見なおしている。


      set_mode 0 : 通常表示 (HH:MM)
      set_mode 1 : 時設定 (HH:MM) で HH ブリンク (アラーム)
      set_mode 2 : 分設定 (HH:MM) で MM ブリンク (アラーム)
      set_mode 1 : 時設定 (HH:MM) で HH ブリンク
      set_mode 2 : 分設定 (HH:MM) で MM ブリンク
      set_mode 3 : 秒表示 で SS ブリンク

      blink off blink on
      0 1 0 1
      set_mode 0 : HH MM HH MM
      set_mode 1 : HH MM -- MM
      set_mode 2 : HH MM HH --
      set_mode 3 : SS -- (秒表示のみ)

      ( -- : blank )

     ・ qfn32samples-04.zip (ソースコード)

関連記事
posted by すz at 20:03| Comment(0) | TrackBack(0) | CPLD

2012年06月10日

TTL ALU 74181

前の記事『QFN32の FPGA』から派生。

    FPGA MachXO2 の QFN32 パッケージが出るので、DIP にする基板を作ってみたい -- というところから、74HC181 (SN74LS181) の代替ができるものを .. となった。

    この IC は ALU(Arithmetic Logic Unit) で、部品を組み合わせて CPU を作ったりするのに欠かせない。有名な『CPU の創りかた』 でも 74HC181 を使っているとのこと。

    ただ入手がとても難しい。では、DIP にするついでにピン互換機能を持たせられるようにすれば、どうだろう。... 調べてみると、電子工作レベルで、ピン互換にできそうなものはあまりない。

    ならば、やってみようというのが動機。

手始めに コードを作ってみよう。

データシートを見てみる。

    www.alldatasheet.jp で検索してみる。
     
  • *4HC181
     
  • *4LS181

    とりあえず、TI の SN74LS181NE4 を参考書として使うことにする。

    で、見てみたのだが、難解。キャリー入力(Cn) が L と H で計算式が違う。これは面倒だなと思ったのだが、どうも ACtive-Low の表が、そういうことらしく、ACtive-High なら見れなくはない。

    ただ、キャリー出力がどうなるのかサッパリ分からない。結局頼りになるのは、回路図。回路図を読みこなすならば、再現してしまった方が 簡単のような気がしてきた。

      例えば、S == 3 では、キャリーありで F = 0 / キャリーなしで F = -1 と なっているのだが ..

      8bit に拡張した場合のことを考えると、0 を出力したときにキャリー出力が 1 (負論理なので L) にならないと変だ。だが、合っているのか? こういうことを、いちいち 回路図を眺めて確認するのは面倒すぎる。

回路図を元にコードを作る。

    回路図を見ると A/B を入力にして 2 つの出力にしている部分(左)と それを元に F などの出力を生成している部分(右)に分かれるようだ。

    左の出力を C/D という中間出力にして論理式にしてみよう。

    wire [3:0] C;
    wire [3:0] D;

    assign C[0] = ~( A[0] & ~B[0] & S[2] | A[0] & B[0] & S[3] );
    assign C[1] = ~( A[1] & ~B[1] & S[2] | A[1] & B[1] & S[3] );
    assign C[2] = ~( A[2] & ~B[2] & S[2] | A[2] & B[2] & S[3] );
    assign C[3] = ~( A[3] & ~B[3] & S[2] | A[3] & B[3] & S[3] );

    assign D[0] = ~( A[0] | B[0] & S[0] | ~B[0] & S[1] );
    assign D[1] = ~( A[1] | B[1] & S[0] | ~B[1] & S[1] );
    assign D[2] = ~( A[2] | B[2] & S[0] | ~B[2] & S[1] );
    assign D[3] = ~( A[3] | B[3] & S[0] | ~B[3] & S[1] );

    上の部分を C , 下の部分を D とするとこうなった。なんか随分すっきりしている。調子に乗って次。

    assign F[0] = ~( ~M & C_IN ) ^ D[0] ^ C[0];
    assign F[1] = ~( ~M & ( D[0] | C_IN & C[0] )) ^ D[1] ^ C[1];
    assign F[2] = ~( ~M & ( D[1] | D[0] & C[1] | C_IN & C[0] & C[1]))
    ^ D[2] ^ C[2];
    assign F[3] = ~( ~M & ( D[2] | D[1] & C[2] | D[0] & C[1] & C[2]
    | C_IN & C[0] & C[1] & C[2])) ^ D[3] ^ C[3];

    wire Yinv = D[3] | C[3] & ( D[2] | C[2] & ( D[1] | C[1] & D[0]));
    wire C_OUT = Yinv | C[3] & C[2] & C[1] & C[0] & C_IN;
    wire EQ = F[0] & F[1] & F[2] & F[3];

    wire X = ~( C[3] & C[2] & C[1] & C[0] );
    wire G = ~ Yinv;
    wire P = X;

    なにかバグがあるかもしれないが、こんな感じ。

    これを合成してみると ...

    Number of SLICEs: 9 out of 128 (7%)
    SLICEs(logic/ROM): 9 out of 32 (28%)
    SLICEs(logic/ROM/RAM): 0 out of 96 (0%)
    As RAM: 0 out of 96 (0%)
    As Logic/ROM: 0 out of 96 (0%)
    Total number of LUT4s: 17
    Number of PIO sites used: 20 out of 22 (91%)

    なんと 9/128 スライス。楽勝ではないか。P/G は生成していないが、C_OUT(Cn+4) の計算で使っているし、生成しても 僅かの差のはず。

    ただ、17 LUT で作れてしまうものなのだろうか? C/D 生成は、4 入力1出力 が 8 個だから 8LUT で確かにできる。残り 9 個だが .. 配線のために消費しなければ、出来そうな 気もする。

    訂正: F3 の括弧の位置が違った。直したら さらに規模が減った。

    ソースコード (Diamond プロジェクト込み)

     
  • qfn32samples-02.zip (2012/06/12 最新版に更新)
     
  • qfn32samples.zip (ここで説明したもの 旧版)

    ちなみに以前作った 74281 (レジスタ付き ALU = アキュームレータ) P/G なし (上記ソースコードに添付)

    Number of registers: 4
    Number of SLICEs: 39 out of 128 (30%)
    SLICEs(logic/ROM): 32 out of 32 (100%)
    SLICEs(logic/ROM/RAM): 7 out of 96 (7%)
    As RAM: 0 out of 96 (0%)
    As Logic/ROM: 7 out of 96 (7%)
    Total number of LUT4s: 78
    Number of PIO sites used: 20 out of 22 (91%)

    こちらは、高機能だし、最適化された回路図なしだから効率が悪いのかも。それにしても楽勝なのは変わりない。確か 64 マクロセルの XC2C64A(CoolRunner II) にようやく入れられたような 規模だったはずなんだが ...

      追記: これなのだが、後述の 74181 拡張をしてそれをベースにしたら 規模が多少小さくなった。そうであれば自作AVRコアにも適用したくなるし、いずれは 32bit CPU も作りたいので 整理して使いやすくしておこうかと思う。

      自作AVRコアにも適用しても確かに規模は減る。ただ、規模が減れば速くなるものかどうか? あと厳密に機能を作らないといけないのでデバッグが面倒。

検証するには

    規模のチェックは、これぐらいで良いだろう。次はどうやって検証するか。

    C のコードに変換するのは、簡単だ。で、実際にいくつかのパターンを入力して OK そうか見る。大丈夫そうになったら、真理値表を作る。

    確か誰かが全入力の真理値表を作っていたような ... 探してみよう。答え合わせができれば、完了。

    追記: 実際に C 化してみた。

    #define _C(v) (((v)&1)?'1':'0')

    #define wire
    #define assign

    void alu181() {
        (上記の Verilog コード)
    printf("M %c ", _C(M) );
    printf("S = %c%c%c%c ", _C(S[3]), _C(S[2]), _C(S[1]), _C(S[0]) );
    printf("C_IN %c ", _C(C_IN) );
    printf("A = %c%c%c%c ", _C(A[3]), _C(A[2]), _C(A[1]), _C(A[0]) );
    printf("B = %c%c%c%c ", _C(B[3]), _C(B[2]), _C(B[1]), _C(B[0]) );
    printf("F = %c%c%c%c ", _C(F[3]), _C(F[2]), _C(F[1]), _C(F[0]) );
    printf("C_OUT %c", _C(C_OUT) );
    printf("\n");
    }

    こんな風にすると、Verilog のコードを触らなくて済む。

      M 1 S = 0000 C_IN 1 A = 0011 B = 0111 F = 1100 C_OUT 1
      M 0 S = 0000 C_IN 1 A = 0011 B = 0111 F = 0011 C_OUT 1
      M 0 S = 0000 C_IN 0 A = 0011 B = 0111 F = 0100 C_OUT 1

      M 1 S = 0001 C_IN 1 A = 0011 B = 0111 F = 1000 C_OUT 1
      M 0 S = 0001 C_IN 1 A = 0011 B = 0111 F = 0111 C_OUT 1
      M 0 S = 0001 C_IN 0 A = 0011 B = 0111 F = 1000 C_OUT 1

      M 1 S = 0011 C_IN 1 A = 0011 B = 0111 F = 0000 C_OUT 1
      M 0 S = 0011 C_IN 1 A = 0011 B = 0111 F = 1111 C_OUT 1
      M 0 S = 0011 C_IN 0 A = 0011 B = 0111 F = 0000 C_OUT 0

      M 1 S = 0110 C_IN 1 A = 0011 B = 0111 F = 0100 C_OUT 1
      M 0 S = 0110 C_IN 1 A = 0011 B = 0111 F = 1011 C_OUT 1
      M 0 S = 0110 C_IN 0 A = 0011 B = 0111 F = 1100 C_OUT 1

    幾つか試したが、合っているようだ。 (最初は合わなかったので、括弧の間違いが見つかったわけだが)

所感

    実を言うと、MachXO2-1200 とか使っていても もっと規模が欲しい。256 ぐらいで何が出来るのか? と思っていたりしたのだが ... 制限内で何かを作るのも面白いかも知れない。CPLD なんかよりずっと回路が入りそう。それでも複雑なものは作れないから、単機能のものをじっくりやることになる。お手軽に使えるように環境を整えて、いろいろ設計してみようかと思う。

74181 の拡張

    74181 は、ものすごく高機能な印象があったのだが、74281 の ALU や 74381 をこれを元に実装しようとすると機能が足りないことが分かった。

    281 の ALU や 381 では、" B - A " があるのだが、181 では無理なのだ。さらに 281 の ALU では、" ~B - 1" なんてものまである。

    で、どうしようか 作ったのを眺めて考えた。181 では、前段の C,D を作るところだけで、機能セレクトを使っている。ようく見ると A と B は対称ではないが、機能セレクトを追加すれば、対称にできる。

    assign C[0] = ~((~A[0] & S[6] | A[0] & S[7])&(~B[0] & S[2] | B[0] & S[3]));
    assign C[1] = ~((~A[1] & S[6] | A[1] & S[7])&(~B[1] & S[2] | B[1] & S[3]));
    assign C[2] = ~((~A[2] & S[6] | A[2] & S[7])&(~B[2] & S[2] | B[2] & S[3]));
    assign C[3] = ~((~A[3] & S[6] | A[3] & S[7])&(~B[3] & S[2] | B[3] & S[3]));

    assign D[0] = ~( A[0] & S[4] | ~A[0] & S[5] | B[0] & S[0] | ~B[0] & S[1] );
    assign D[1] = ~( A[1] & S[4] | ~A[1] & S[5] | B[1] & S[0] | ~B[1] & S[1] );
    assign D[2] = ~( A[2] & S[4] | ~A[2] & S[5] | B[2] & S[0] | ~B[2] & S[1] );
    assign D[3] = ~( A[3] & S[4] | ~A[3] & S[5] | B[3] & S[0] | ~B[3] & S[1] );

    こうするのだ。S が 4bit から 8bit になってしまうわけだが、使い方は次のようにする。

    S[7:4] S[3:0]
    1 0 0 1 FS[3:0] --- (1)
    FS[3:0] 1 0 0 1 --- (2)

    0 1 1 0 FS[3:0] --- (3)
    FS[3:0] 0 1 1 0 --- (4)

    新設の上位 4bit を 1001 にすると ... 今までと互換(1)。 で、上位ビットと下位ビットを入れ替えると ... A と B が入れ替わる(2)。これは計算式を眺めれば分かるはずだ。

    さらに (1) の 1001 を 0110 にすれば A がビット反転する(~A)。 A と B を入れ替えた上で B をビット反転するには、(4) のようにする。

    これでようやく 281 の ALU や 381 の機能を作れるようになった。

メモ: (ピン)互換モジュールのピンアサイン案

    XO2-256 74181 74181 XO2-256
    27 B0 1 24 VCC
    28 A0 2 23 A1 25
    29 S3 3 22 B1 23
    30 S2 4 21 A2 21
    32 S1 5 20 B2 20
    1 S0 6 19 A3 17
    4 ~Cn 7 18 B3 16
    5 M 8 * 17 N.C. 26
    8 F0 9 16 ~Cn+4 14
    9 F1 10 * 15 SWAP 13
    10 F2 11 14 EQ 12
    GND 12 13 F3 11

    前の記事にも書いたが、実際は有効なピンが足りず互換にできない。P/G は使わないだろうということで、17 は N.C. 扱い。(N.C. とは書いたが無接続にすること)

    15 は、入力 A と B を入れ替える機能にしようかと思う。どうせ回路は余っているのだ。9 スライスが 13 スライスになったが さほどのことはない。

    あ、あたかもモジュールが存在するような書き方をしたが、部品も入手できておらずあくまで予定。

    181 の 機能

    S3 S2 S1 S0 M = 1 C_IN = 1 C_IN = 0
    logic (no carry) (with carry)

    0 0 0 0 ~A A A + 1
    0 0 0 1 ~(A | B)
    0 0 1 0
    0 0 1 1 0 -1 0
    0 1 0 0 ~(A & B)
    0 1 0 1 ~B
    0 1 1 0 A ^ B A - B - 1 A - B
    0 1 1 1
    1 0 0 0 ~A
    1 0 0 1 ~(A ^ B) A + B A + B + 1
    1 0 1 0 B
    1 0 1 1 A & B
    1 1 0 0 1 A + A A + A + 1
    1 1 0 1
    1 1 1 0 A | B
    1 1 1 1 A A - 1 A

    使う可能性があるものをピックアップしてみた。結構スカスカ。SWAP を追加しても M = 1 は結果が同じ。役に立ちそうなのは、B - A , B - 1 , B + 1 ぐらい。

オーバーフラグ(OVR)の 追加

    AVRコアだと H フラグ, V フラグというのがある。H フラグは、4bit のキャリーフラグで 181 ベースでの ALU なら 出力するだけで済む。(自作のコアでは、Verilog の演算をしていたので、別途生成しないといけなかった)

    さて、V フラグは どういうものか ... 現状のものは、次のような式を使っている。

    assign V_out = ~M &
    ( ~(S[1]) & (F[7] & ~A[7] & ~B[7] | ~F[7] & A[7] & B[7])
    | (S[1]) & (F[7] & ~A[7] & B[7] | ~F[7] & A[7] & ~B[7]) )
    ;

    M=1 が論理演算で、~(S[1]) は、算術加算。 で S[1] が算術減算。... 要するに A/B/F の最上位ビットを 比べているわけだが、加算/減算で論理が違う。

    一方 74LS382 (381 のモディファイ版) には、OVR という出力があって、こんな計算はしていない。よくわからなかったのだが、どうも C_OUT(n) ^ C_OUT(n-1) という計算をしているようだ。

    C_OUT(n) は、既にあるわけだが、181 は内部で C_OUT(n-1) も作っているはず。そうであれば、比較的簡単に OVR を作れるのではないか?

    wire C_OUT1 = ~( D[0] | ~C_IN & C[0]);
    wire C_OUT2 = ~( D[1] | C[1] & D[0] | ~C_IN & C[0] & C[1]);
    wire C_OUT3 = ~( D[2] | C[2] & ( D[1] | C[1] & D[0])
    | ~C_IN & C[0] & C[1] & C[2]);

    assign P = ~( C[3] & C[2] & C[1] & C[0] );
    assign G = ~(D[3] | C[3] & ( D[2] | C[2] & ( D[1] | C[1] & D[0])));
    assign C_OUT = ~(~G | ~C_IN & C[0] & C[1] & C[2] & C[3]);

    assign F[0] = ~( ~M & ~C_IN ) ^ D[0] ^ C[0];
    assign F[1] = ~( ~M & ~C_OUT1 ) ^ D[1] ^ C[1];
    assign F[2] = ~( ~M & ~C_OUT2 ) ^ D[2] ^ C[2];
    assign F[3] = ~( ~M & ~C_OUT3 ) ^ D[3] ^ C[3];

    assign OVR = ~M & (C_OUT3 ^ C_OUT);
    assign EQ = F[0] & F[1] & F[2] & F[3];

    どうもこうらしい。これで、たいぶすっきりした。

    で、この修正を入れたら、15 スライスにまで増えた。( ただし 使わなければ 元と同じ )

8bit への拡張

    この alu を 2 つ使って 8bit 版を作ってみている。C_OUT と 上位 4bit の C_IN をつないだもの。

    35 スライス / 68 LUT になった。だいぶでかくなった。

    P/G の生成は、

    assign P = PL | PH;
    assign G = GH & (PH & GL );

    こんな風にやるものらしい。PH , GH を出力すれば良いものではないようだ。8bit 版単体で使うなら関係ないのだが、いずれは、32bit CPU を作りたいので対応しておいた。また、8bit 版 になるとピンが増えるので、想定しているモジュールでは合成できない。

    最初の目的は、 自作AVRコアに適用すること。まだ機能が厳密に合っているのかどうか分からないが .. 42 スライス(83 LUT) が、31 スライス(62 LUT) にまで減った。問題は、規模よりは 速度のボトルネック。もとのやつより速いと良いのだが...

  • ここまでのソースコード (Diamond プロジェクト込み) → qfn32samples-04.zip

     2012/06/17 更新: iVerilog 用テストベンチを追加し、自作AVR コアに適用できるようデバッグ。

     
  • rtavr では、C_OUT/HALF フラグの意味が 減算では反転している。( 1 で ボロー )
     
  • 論理演算では、C_OUT/HALF フラグ は無視してよい。( 結果が違っても良い) -- 実際違うので テストで比較に入れない。
     
  • EQ の意味を取り違えていた。
      ( F == 1111 で EQ = 1 だから Zero Flag とは関係なし。)
     
  • (alu4.v 自体は無変更)

関連記事
posted by すz at 13:27| Comment(131) | TrackBack(0) | CPLD

2011年05月17日

CPLD ALU

TTL ALU 74281』の記事の続き。

前記事はグダグダになってしまったので、仕切りなおし。

経緯 :

    もともと ALU の勉強として、TTL の ALU 78281 を作り、それを元にしてなにか作ろうともくろんでいたのだが、どんどん興味が変わっていって、結局 自作コアの rtavr の ALU になった。

    その後、rtavr の ALU は、機能拡張をして、機能的に過不足のないものが出来た。

    rtavr には関係ないのだが、可変ビット長への対応もいれている。

で、この ALU に アキュームレータを付けたものを作ってみたら、CoolRunner II CPLD (XC2C64A-VQ44) にうまく収まった。これをベースになにか作るのも面白いかも知れないので紹介。

インターフェイスと機能:

    module rtavr_alu_reg # (
    parameter ALU_WIDTH = 8
    ) (
    // ALU
    input C_IN // Carry Input
    , input Z_IN // zero Input
    , input [ALU_WIDTH-1:0] B // DATA IN
    , output [ALU_WIDTH-1:0] F // DATA OUT
    , output C_OUT // carry-look-ahead
    , output H_OUT // half carry-look-ahead
    , output Z_OUT // zero
    , output OVR // overflow
    , input [2:0] S // Function Select
    , input [1:0] ES // Function Select (Extended )
    // アキュームレータ
    , input [1:0] RS // Reg. Select
    , input CP // Clock
    , inout SIO0 // RI/LO
    , inout SIO3 // LI/RO

    ALU の A 入力は、アキュームレータの出力に接続されている。基本的には、

    F = A 演算 B;
    A <= F;

    という使い方をする。フラグは、次の 4 種類で、AVR で必要なもの。

    C : キャリーフラグ
    H : ハーフキャリー
    Z : ゼロ
    OVR : オーバフロー

    S は演算の指定。ES は、入力に定数を使うオプションで、INC/DEC や 2 の補数に変換する NEG などに使う。

    // S == 0 : ADD (wo carry)
    // S == 1 : ADC (with carry)
    // S == 2 : SUB (wo carry)
    // S == 3 : SBC (with carry)
    // S == 4 : AND
    // S == 5 : XOR
    // S == 6 : OR
    // S == 7 : MOV
    //
    // S = 0 1 2 3 4 5 6 7
    // C_out : Carry[8] o o o o x x x x
    // H_out : Carry[4] o o o o x x x x
    // V_out : Over Flow o o o o 0 0 0 x
    // Z_out : Zero o o o (o) - - - x
    // (o) if (Z_in == 1)
    // - no change
    // x don't care
    // EXTENDED Ops
    // ES[0] : -1 を B の代わりに使う
    // ES[1] : 0 を A の代わりに使う
    // (※ A を B の代わりに使う)
    // NEG : 0 - B (A == B) : S = 2, ES[1] = 1
    // COM : A ^ -1 : S = 5, ES[0] = 1
    // INC : A - -1 : S = 2, ES[0] = 1
    // DEC : A + -1 : S = 0, ES[0] = 1


      ※ AVR では、 ES[1] は、NEG を実装するときにしか使わない。ALU単独で使っているから B に A と同じ値を 入力している。アキュームレータ付きでは、 ES[1]のとき、 A を B の代わりに使うように仕様を変更した方が良い。ただし、XC2C64A に入らない。

      Implement の結果を書いておく。(変更後 のものは、XC2C128 で算出)

      変更前 変更後 トータル
      Macrocells Used 45 49 64
      Pterms Used 204 249 224
      Registers Used 8 8 64
      Pins Used 32 32 33
      Function Block Inputs Used 117 134 160


    アキュームレータは操作は RS を使い posedge CP で駆動する。

    RS == 0 : F の値を A にロード
    RS == 1 : F を 左シフト LSB は、SIO0 から入力して、MSB を SIO3 に出力
    RS == 2 : F を 右シフト LSB は、SIO0 に出力して、MSB を SIO3 から入力
    RS == 3 : HOLD
     (※)シフトは、F をシフトしたものを A にロードする。


処理コード :


    wire WC = (S[0] & C_in);
    wire [ALU_WIDTH-1:0] AX = (ES[1]) ? 0 : A;
    wire [ALU_WIDTH-1:0] BX = (ES[0]) ? -1 : B;
    ( ※ wire [ALU_WIDTH-1:0] BX = (ES[1]) ? A :(ES[0]) ? -1 : B; )
    wire [ALU_WIDTH:0] WCX = WC; // change bit width
    wire [ALU_WIDTH:0] ALU_OUT = ((S == 0)|(S == 1))
    ? ({1'b0, AX} + ({1'b0, BX } + WCX))
    : ((S == 2)|(S == 3))
    ? ({1'b0, AX} - ({1'b0, BX } + WCX))
    : (S == 4) ? {1'b0, (AX & BX)}
    : (S == 5) ? {1'b0, (AX ^ BX)}
    : (S == 6) ? {1'b0, (AX | BX)}
    : {1'b0, BX } ;
    assign Z_out = (S == 3) ? (Z_in & (ALU_OUT[ALU_WIDTH-1:0] == 0))
    : (ALU_OUT[ALU_WIDTH-1:0] == 0);

    assign C_out = ALU_OUT[ALU_WIDTH];

    assign F = ALU_OUT[ALU_WIDTH-1:0];

    // ((S == 4)|(S == 5)|(S == 6)) ? 0 // (S ==7) Not care
    assign V_out = (S[2]) ? 0:
    ( ~(S[1]) & (F[ALU_WIDTH-1] & ~AX[ALU_WIDTH-1] & ~BX[ALU_WIDTH-1]
    | ~F[ALU_WIDTH-1] & AX[ALU_WIDTH-1] & BX[ALU_WIDTH-1])
    | (S[1]) & (F[ALU_WIDTH-1] & ~AX[ALU_WIDTH-1] & BX[ALU_WIDTH-1]
    | ~F[ALU_WIDTH-1] & AX[ALU_WIDTH-1] & ~BX[ALU_WIDTH-1]) )
    ;
    assign H_out =
    ( ~(S[1]) & ( ~F[(ALU_WIDTH-1>>1)] & A[(ALU_WIDTH-1>>1)]
    | ~F[(ALU_WIDTH-1>>1)] & BX[(ALU_WIDTH-1>>1)]
    | AX[(ALU_WIDTH-1>>1)] & BX[(ALU_WIDTH-1>>1)] )
    | (S[1]) & ( F[(ALU_WIDTH-1>>1)] & ~AX[(ALU_WIDTH-1>>1)]
    | F[(ALU_WIDTH-1>>1)] & BX[(ALU_WIDTH-1>>1)]
    | ~AX[(ALU_WIDTH-1>>1)] & BX[(ALU_WIDTH-1>>1)] ) )
    ;

    中身は意外と短い。ALU を全部載せてもこれだけ。

    wire SO0;
    wire SO3;
    wire SI0;
    wire SI3;

    assign SO0 = F[0];
    assign SO3 = F[ALU_WIDTH-1];

    always @(posedge CP)
    begin
    acc <= (RS == 0) ? F
    : (RS == 1) ? { F[ALU_WIDTH-2:0] , SI0 }
    : (RS == 2) ? { SI3 , F[ALU_WIDTH-2:0] }
    : acc;
    end
    assign SIO0 = (RS == 2) ? SO0 : 1b'Z ;
    assign SIO3 = (RS == 1) ? SO3 : 1b'Z ;

    assign SI0 = SIO0;
    assign SI3 = SIO3;

    こっちはアキュームレータ。ALU は確認済みだが、こちらは書いただけ。

これを使って、なにか簡単なプロセッサを設計してみようと思う。

    実をいうと、大昔に設計したものを再現しようと思っている。同じものにはなるはずもないが、形にして残しておきたい。

    詳細は思い出せないのだが、こんな感じだった。

    • マイクロプログラミング方式で、8 bit の INST を INDEX にして マイクロプログラム用ROM(以下 μROM) に接続。

    • μROMの出力 は、各 bit が ES,S, RS や その他の制御線に接続されている。(全部で 24bit 以内)。

    • マイクロプログラム用の PC(以下 μPC) は、次の INST を読み込むまでインクリメント。この制御のための制御線も μROM の出力に接続されている。

    • 16bit の インデックスレジスタ、PC が 1 つづつ。それらの制御線も μROMの出力が接続されている。

    • B 入力は、バスのラッチで、制御線は同様になっている。

    • 総命令数は、16 前後で、マイクロプログラムは、64 前後だった。

    アドレス 6bit x 24bit 幅なら 分散RAM で 96 LUT 。ブロック RAM に割り付ければコストは無視できる。レジスタ用の FF は、PC 16 , INDEX 16, LATCH 8, と μPC 8 (6 ?) で、40 。

    FPGA なら、それなりにコンパクトなものになるようだ。

設計してみよう。



    wire [7:0] load_data;
    reg [5:0] UPC ;
    reg [23:0] UROM [0:63];
      wire [23:0] urom_out = UROM[UPC];

    というのをまず考えてみる。


    wire u_loadpc = urom_out[7];
    always @(negedge CLK)
    begin
    if (RESET)
    begin
    UPC <= 0;
    end
    else
    begin
    if (u_loadpc) UPC <= load_data[5:0];
    else UPC <= UPC + 1;
    end
    end

    マイクロプログラムの制御はこれだけ。
    メモリから LATCH に INST を ロードするには、

    wire c_bus_rd = urom_out[8];
    wire c_bus_wr = urom_out[9];
    wire c_addr_sel = urom_out[10]; // 0: PC , 1: INDEX
    wire c_load_latch = urom_out[11];
    wire c_load_index_l = urom_out[12];
    wire c_load_index_h = urom_out[13];
    wire c_load_pc_l = urom_out[14];
    wire c_load_pc_h = urom_out[15];
    wire c_inc_pc = urom_out[16];

    reg [7:0] LATCH ;
    reg [15:0] PC ;
    reg [15:0] INDEX ;

    wire [15:0] BUS_ADDR = (c_addr_sel) ? INDEX: PC;
    always @(negedge CLK)
    begin
    if (RESET)
    begin
    LATCH <= 0;
    PC <= 0;
    INDEX <= 0;
    end
    else
    begin
    if (c_load_latch) LATCH[7:0] <= BUS_DATA;
    if (c_load_index_l) INDEX[7:0] <= BUS_DATA;
    if (c_load_index_h) INDEX[15:8] <= BUS_DATA;
    if (c_load_pc_l) PC[7:0] <= BUS_DATA;
    else if (c_load_pc_h) PC[15:8] <= BUS_DATA;
    else if (c_inc_pc) PC <= PC + 1;
    end
    end
    // +pPiIlswr
    UROM[n+0] <= 24'bxxxxxxx1000010010xxxxxxx;
    UROM[n+1] <= 24'bxxxxxxx0000000001xxxxxxx;

    こんな風に組み立てていく。
    ... 思い出した。INST の読み込みは、各命令の実行後必要だし、μプログラムにも JUMP ぐらいは作ったのだった。


    wire u_loadpc = urom_out[7];
    wire u_jump = urom_out[6];
    always @(negedge CLK)
    begin
    if (RESET)
    begin
    UPC <= 0;
    end
    else
    begin
    if (u_loadpc) UPC <= load_data[5:0];
    else if (u_jump) UPC <= urom_out[5:0];
    else UPC <= UPC + 1;
    end
    end

    こうだったような。ちなみに、urom_out[5:0] は、u_jump == 1 のときだけ使っているから、それ以外のときは、別の目的に使える。何に使っていたか思い出せないが 分岐命令とかの 制御だったような...

    条件分岐命令をインプリメントするときに、μ命令の方も 条件による分岐が必要になる。が、u_jump の条件をいじれば良い。... これは 後で考えよう。

    次は、ALU を組み込む。

    wire [7:0] alu_b = LATCH;
    wire [7:0] alu_f;
    assign store_data = alu_f[7:0];
    wire [1:0] alu_rs = urom_out[23:22];
    wire [1:0] alu_es = urom_out[21:20];
    wire [2:0] alu_s = urom_out[19:17];

    reg C;
    reg Z;
    reg V;
    reg H;

    rtavr_alu ALU (
    .B(alu_b), .F(alu_f)
    , .S(alu_s) , .ES(alu_es)
    , .CP(CLK) , .RS(alu_rs)
    , .C_in(C) , .Z_in(Z)
    , .C_out(C_OUT) , .H_out(H_OUT)
    , .V_out(OVR) , .Z_out(Z_OUT)
    );
    always @(negedge CLK)
    begin
    if (RESET)
    begin
    C <= 0;
    Z <= 0;
    end
    else
    begin
    C <= C_OUT;
    Z <= Z_OUT;
    end
    end

    全部割り当てたら 24 bit 使いきってしまった。条件分岐のために、あと 1 bit 欲しい。24bit にこだわる必要はないのだが ... 整理して浮かそう。

    ... 思い出した。ALU の S (演算選択) を 8 bit ある命令空間の下位 3 bit にしたのだった。(以下 INST_ATTR )

    で、UPC を 上位にもっていく。UPC[0] は、重なるから 常に 0 をロード。-- 要するに 奇数番地から始まる命令は作れない。

    このINST_ATTR は他の目的にも使える。ひとつが、分岐命令の条件選択。最大 8 通り作れるわけだ。フラグは 4 つあるから、0 のとき 分岐 と 1 のとき分岐の組み合わせで 合計 8 通り。

    これで、3 bit 減って 1 bit の追加で、合計 22 bit 。

    もうひとつは、シフトのときの動作の指定。シフト動作は、RS として UROM に割り当てているが、C になにをロードするかの指定と、SHIFT_IN するデータを何にするかの組み合わせがある。INST_ATTR から 3 bit 割り当てて、足りなければ UROM を割り当てることにする。

zsp8-0.1

  • zsp8-0.1.tar.gz

    ここまでの成果を zsp8-0.1 として置いておく。μプログラムが出来ていないが Implement だけはやってみた。


        Macrocells Used Pterms Used Registers Used Pins Used Function
    Block Inputs Used
    ベース 135/256 565/896 60/256 36/80 322/640
    拡張版 140/256 586/896 60/256 36/80 361/640


    これは、CPLD XC2C256-VQ100 への Implement 結果。拡張版は、ES[1] == 1 のとき A 入力を B に持っていく拡張。

    ひょっとしたら XC2C128 に入るかもと思ったがちょっと無理のようだ。


しばらく、この記事はお休み。気が向いたら続きを書くかも。
posted by すz at 21:12| Comment(0) | TrackBack(0) | CPLD

2011年05月09日

TTL ALU 74281

74 シリーズの中には、Arithmetic Logic Unit (ALU) がいくつかある。

  • TTLのみでコンピュータを自作する(第7話)

    ここのサイトに ALU 一覧がある。74181 は、そのなかでも有名で、現在でもなんとか入手できる。

    気になるのは、78281 。アキュームレータ付きで、シフトもできる。TTL で CPU を作るなら便利そうなのだが、こいつは、ググってもほとんど情報がない。

78281 互換の module を作り、それを使ったなにか簡単な CPU を作ってみたい。

    AVR互換コアは作ったが、ALUありきの設計にはしなかった。独立したモジュールさえ作らなかったのだ。

    それで、ALU には少々負い目を感じている。簡単なもので良いから、ALU というものを作っておきたい。

さて、74281 とは、実際どういうものだろう?

74281 加減算、AND/OR/EXOR、その他計15種 4bit ALU出力にシフトレジスタ付

説明はこうなっている。

データシート は見つかった。ただ、紙媒体をスキャンしたもので読みにくい。

74281 の仕様



    ピン配置

    74281
    A1 1 24 VCC
    A2 2 23 A0
    RS1 3 22 CP
    RS0 4 21 SIO0
    RC 5 20 AS0
    SIO3 6 19 AS1
    A3 7 18 AS2
    Cn 8 17 M
    ~G 9 16 F0
    Cn+4 10 15 F1
    ~P 11 14 F2
    GND 12 13 F3

    入出力

    input Cn // Carry Input
    input [3:0] A // DATA IN

    output [3:0] F // DATA OUT
    output Cn+4 // carry-look-ahead
    output ~P // carry-propagate
    output ~G // carry-generate

    input M // Mode Control
    input [2:0] AS // Function Select
    input [1:0] RS // Reg. Select
    input RC // Reg. Control
    input CP // Clock
    inout SIO0 // RI/LO
    inout SIO3 // LI/RO

    ( reg [3:0] B ) // internal register

    TABLE1 ARITHMETIC FUNCTIONS (M == 0)

    AS[2:0]
    0 0xe + Cn
    1 B - A - ~Cn
    2 A - B - ~Cn
    3 A + B + Cn
    4 B + Cn
    5 ~B + Cn
    6 A + Cn
    7 ~A + Cn

    TABLE2 LOGIC FUNCTIONS (M == 1)

    AS[2:0]
    0 0
    1 A ^ B
    2 ~( A ^ B )
    3 A ^ B
    4 A & B
    5 ~( A | B )
    6 ~( A & B )
    7 A | B

    TABLE3 SHIFT MODE FUNCTIONS (posedge CP)

    RS[1:0] , RC Breg SIO0 SIO3
    0/1 Load B Reg ( F->B ) F0 F1 F2 F3 Z Z
    2 Shift UP SIO0 F0 F1 F2 Z F3
    3 Arith Shift UP SIO0 F0 F1 B3 Z F2
    4 Shift Down F1 F2 F3 SIO3 F0 Z
    5 Arith Shift Down F1 F2 SIO3 B3 F0 Z
    6/7 Hold B0 B1 B2 B3 Z Z

    仕様を書き写すとこういうものであるらしい。

    入力は、4bit の A で、出力は F 。B は内部のレジスタ -- アキュームレータ。

    ALU は 算術演算と論理演算の 2 つのモードがある。

    シフトも 算術シフトができる。

    機能としては、十分で、アキュームレータ 1 つしかない CPU を作るなら、配線が少なくて済む。

    これだけの情報で大体は分かるが ~P, ~G の説明はない。

    このあたりをみれば、分かる。設計するための情報は揃った。

    追記:データシート AM25LS281XC に P/G の論理式が載っていた。使い方の例は、 Am2902 との接続方法のみ。一方 データシート DM74S281N には、182 との接続方法のみ(詳しく)載っている。

    P,G は、182 (や Am2902) を使って高速化する場合のみ必要で、2 個を使った 8bit 版程度では不要。16 bit 版でも カスケード接続はできる。また、P,G を生成するより 最初から 16bit 版(など)を作った方が記述が楽。

74281 の設計



    完全に同じものを作ることは、できるだろう。だが、TOP モジュールにするとすれば、CPLD に入れるにしても 規模が小さすぎる。せめて x2 の 8bit 版にしたい。

    TOP モジュールにしないとすれば、シフトレジスタ部の SIO0/SIO3 が 双方向で使いにくい。また、ALU 部と シフトレジスタ部は分離したいところ。

    というわけで、ALU 部 と シフトレジスタ部 を別々に作り、TOP モジュールで組み合わせることにする。また、シフトレジスタ部の SIO0/SIO3 は、IN/OUT 別々にする。

    モジュールの定義は、

    alu_74281(ALU)
    input C_IN // Carry Input
    input [3:0] A // DATA IN
    input [3:0] B // DATA IN
    output [3:0] F // DATA OUT
    output C_OUT // carry-look-ahead
    output P // carry-propagate
    output G // carry-generate

    input M // Mode Control
    input [2:0] AS // Function Select

    acc_74281(shift reg.)
    output [3:0] B // DATA OUT
    input [3:0] F // DATA IN

    input [1:0] RS // Reg. Select
    input RC // Reg. Control
    input CP // Clock
    input SI0 // RI/LO
    output SO0 // RI/LO
    input SI3 // LI/RO
    output SO3 // LI/RO


    こんな感じか。

ALU を 74382 に変更

    74281 の ALU を作ってみたのだが、選択が 4 bit もあり、結構スライスを使う。382 は、3bit で、必要十分のように思える。
    データシートを見ると 真理値表もある。

    • 74F381 ( P , G の真理値表 )
    • 74F382 ( OVR , Cn4 の真理値表 )

    真理値表をみながら考えたのだが、FPGA はもともと LUT で論理を合成する。すなおに LUT を使うと ...

    • 真理値表に書いてある入力は、S 3bit + Cn + A + B で 6bit 。
    • 出力は、F 4bit + OVF + Cn+1 で 6bit と内部 Cn 生成に 3bit 。
    • ( 6bit 入力 = 4 LUT ) x 9 = 36 LUT (18 slices)
    • 8bit 化なら ( 6bit 入力 = 4 LUT ) x 17 = 68 LUT (34 slices)
    • Spartan-6 ならもっと効率が良く 1/4 で済む。
    • LUT_4 しかない FPGA だと、後半の OVR , Cn は LUT を使わずに 0 固定にすれば ちょっと規模を減らせる。( 互換ではなくなるけど .. 使うとは思えない )

    これだけで済むとは思えないが、結構コンパクトになりそうだ。うまく行くなら AVR互換コア(rtavr) にフィードバックして、コアを小さくできるかも知れない。

      rtavr の ALU は、8bit で 出力は OV と Cn[4] , Cn[8] が必要。機能的にも 8bit 化 382 で十分。Cn[4] は、内部用として生成しているから、規模は同じと見積もれる。

    で、やってみた。最初分散メモリを使おうとしたのだが、うまくいかないので、強引に if 文 (正確には ? 演算子のネスト) で 書いてみた。

    by LOGIC by CASE(full) by CASE(shrinked)
    Number of Slice Flip Flops 8 8 8
    Number of 4 input LUTs 73 82 77
    Number of occupied Slices 37 45 39
    Total Number of 4 input LUTs 74 82 77
    Number used as a route-thru 1 0 0
    Number of bonded IOBs 30 30 30
    Number of BUFGMUXs 1 1 1

    Maximum frequency(MHz): 126.646 70.967 115.221

    結果はこれ。74281 のアキュームレータを組み合わせている。(そうしないと周波数が出ないので) 。
    ロジックで書いたのは、論理演算の OVR , C_OUT を計算していないから、shrinked version に相当する。残念ながら ちょっと負けている。ロジックの方はバグっているかも知れないので、最終的な結果ではないものの、残念。

    だが、良く良く考えて見れば、rtavr の ALU部は、H フラグの生成で無駄がある。8bit の演算をしていて、Cn[4] を作れば良いだけなのに、計算とは別に H フラグを生成しているのだ。

    ロジックで組むのと遜色ないのであれば、この無駄が省けるかも知れない。... というわけで、何度も方針変更になってしまうが、AVR の ALU をまじめに作る方針に変更。同じように、74281 のアキュームレータと組み合わせて比較してみよう。

rtavr_alu

    まだロジックでの演算のみだが、rtavr の ALU をモジュールとして独立させたのが出来た。評価環境があるから、正しく出来たかどうかの検証が楽。3 スライスだが、規模が小さくなって嬉しかったりするが、それはサテオキこんな仕様。

    module rtavr_alu (
    input [7:0] A
    , input [7:0] B
    , output [7:0] F

    , input C_in
    , input H_in
    , input V_in
    , input Z_in
    , output C_out
    , output H_out
    , output V_out
    , output Z_out

    , input [2:0] S
    );
    // S == 0 : ADD (wo carry)
    // S == 1 : ADC (with carry)
    // S == 2 : SBC (with carry)
    // S == 3 : SUB (wo carry)
    // S == 4 : AND
    // S == 5 : XOR
    // S == 6 : OR
    // S == 7 : MOV
    //
    // S = 0 1 2 3 4 5 6 7
    // C_out : Carry[8] o o o o - - - -
    // H_out : Carry[4] o o o o - - - -
    // V_out : Over Flow o o o o - - - -
    // Z_out : Zero o o o (o) - - - -
    // (o) if (Z_in == 1)

    S は、こんな割り当て。余ったので、加減算に carry ありなしを割り当て 計 8種類。フラグは変更しない OP があるので 全部 IN/OUT がある。C/V は 382 と同じようなものだが、Half carry と zero フラグがある。

    zero フラグは、演算結果の後に付くから、いままで説明してきたのとは扱いが違う。Half carry は内部状態を外に出すかんじ。

    実は、zero フラグを モジュールに持ってきたため、階層が増えて遅くなっている。だが、Z_out を入力から直接生成するような 専用コードを書くことで逆に速くできるかも知れない。

    ゴードは小さいので全部書くと

    wire [8:0] ALU_OUT = ((S == 0) | (S == 1)) ? ({1'b0, A} + {1'b0, B }
    + {8'b0, (S[0])? C_in : 1'b0 })
    : ((S == 2) | (S == 3)) ? ({1'b0,A} - {1'b0, B }
    - {8'b0, (S[0]) ? 1'b0: C_in })
    : (S == 4) ? {C_in, (A & B) }
    : (S == 5) ? {C_in, (A ^ B) }
    : (S == 6) ? {C_in, (A | B) }
    : {C_in, B } ;

    assign F = ALU_OUT[7:0];

    assign C_out = ALU_OUT[8];

    assign Z_out = (S == 7) & Z_in
    | ( (S == 2) & Z_in | ~((S == 2)|(S == 7)) )
    & (ALU_OUT[7:0] == 0);

    assign V_out = ((S == 0)|(S == 1)) & (F[7] & ~A[7] & ~B[7]
    | ~F[7] & A[7] & B[7] )
    | ((S == 2)|(S == 3)) & ( F[7] & ~A[7] & B[7]
    | ~F[7] & A[7] & ~B[7] )
    | (S == 7) & V_in ;
    // ((S == 4)|(S == 5)|(S == 6)) ? 0

    assign H_out = ((S == 0) | (S == 1)) & (
    ~F[3] & A[3]
    | ~F[3] & B[3]
    | A[3] & B[3] )
    | ((S == 2) | (S == 3)) & (
    F[3] & ~A[3]
    | F[3] & B[3]
    | ~A[3] & B[3] )
    | (S[2]) & H_in;

    こう。これを仕様として、LUT 版を作ってみよう。

    LUT を引くのは、前半の算術演算のみ。LUT も 382 で作っているので同じように作る。サイズは 32 エントリ。
    問題は、Z フラグ。論理演算は、キャリーを使わないから、ボトルネックにならない。
    算術演算の Z フラグの計算で、上位 1bit か 2bit を直接演算すれば良いのではないか?

    ただ上位 1bit でも (Z_in) が加わるので 64 エントリになる。2bit なら 256 エントリ。

    とりあえずテーブルの元。

    // input: { S , C_in , A[0], B[0] }
    // ADD ADC SBC SUB
    // F 01100110 10101001 01101001 01100110
    // C 00010001 00010111 01001101 01000100
    // OV 00010001 00011000 01000010 01000100
    // (not used) (not used)
    // input: { Z_in, S[1:0] , C_in , A[0], B[0] }
    // Z 10011001 10010110 00000000 10011001 10011001 10010110 10010110 10011001
    // input: { Z_in, S[1:0] , C_in , A[1:0], B[1:0] }
    // Z 1010000010100000101000001010000010100000101000000000000000000000
    // 0000000000000000000000000000000010100101101001011010010110100101
    // 1010000010100000101000001010000010100000101000000000000000000000
    // 1010010110100101000010100000101010100101101001011010010110100101

    これ動かすことだけはできたが、全然ダメだ。遅くなって規模も増えた。

    うまく合成してくれないのも理由のひとつではあるが、設計上の失敗がいくつかある。

    ひとつの理由は、キャリーあり/なし を INDEX に組み込んでしまったこと。最下位ビットしか関係なく、テーブルが無駄になる。

    もうひとつの理由は、1桁づつ計算すること。普通に加減算のコードを書くと、ISE は、2桁づつ計算するように合成するようだ。ISE にまかせた方がよさそうな気がする。

    モジュール化自体は、規模が減ったし良かった。他にもやりたいことがあるので、今回は、これぐらいにしておこう。

もう少し検討してみる。

    もう、テーブル引きによる演算はあきらめた。そんなことより、zero の検出の高速化の方が重要。

    説明しておくと、rtavr での ボトルネックは、次のパスに決まっている。

      S1 でブランチ命令をデコードしていて、1つ前の命令が 加減算中で Z Flag の確定を待って 次の PC(プログラムカウンタ)を確定させる。

      加減算が一番遅く、現在は、その結果を元に Z Flag の計算をしているのだ。

    Z Flag が早く確定すれば、ボトルネックが解消するのだ。

    さて、加算で、結果が 0 になるケースを考えてみよう。

    キャリー C_in が 1 のとき、(A ^ B) == 8'b11111111 ときだけ結果が 0 。
    キャリー C_in が 0 なら (A ^ (B-1)) が 8'b11111111 のとき。

    では、減算はどうだろう。

    減算は、B の 2 の補数 を加算する。2 の補数は、(~B + 1) だから

    C_in が 1 のとき、(A ^ (~B + 1)) == 8'b11111111 ときだけ結果が 0 。
    C_in が 0 なら (A ^ (~B)) が 8'b11111111 のとき。これは、(A ^ B) == 0 に置き換えられる。

    これをインプリメントしてみよう。... しかし 、ここまでするのなら演算自体もまとめられそうな ...

    その前に仕様変更。

    // S == 0 : ADD (wo carry)
    // S == 1 : ADC (with carry)
    // S == 2 : SBC (with carry)
    // S == 3 : SUB (wo carry)

    2 と 3 を入れ替えたほうが良さそうだ。

    // S == 0 : ADD (wo carry)
    // S == 1 : ADC (with carry)
    // S == 2 : SUB (wo carry)
    // S == 3 : SBC (with carry)

    こうすることで、キャリー付きが S[0] & C_in で表現できる。

ALU_Z_TUNE_1


    wire WC = (S[0] & C_in);
    wire [8:0] F_a =
    (~S[1]) ? ({1'b0, A} + {1'b0, B } + {8'b0, WC })
    : ({1'b0, A} - {1'b0, B } - {8'b0, WC });

    wire [7:0] R0 = (A ^ B);
    wire [7:0] R1 = (A ^ (B-1));
    wire [7:0] R2 = (A ^ (~B+1));

    wire F_a_z = ~S[1] & WC & (R0 == 8'hff)
    | ~S[1] &v ~WC & (R1 == 8'hff)
    | S[1] & WC & (R2 == 8'hff)
    | S[1] & ~WC & (R0 == 8'h00) ;

    wire [7:0] F_l = (S[1:0] == 0) ? (A & B)
    : (S[1:0] == 1) ? (A ^ B)
    : (S[1:0] == 2) ? (A | B)
    : B;

    assign F = (~S[2]) ? F_a[7:0] : F_l[7:0];

    assign C_out = (~S[2]) ? F_a[8] : C_in;

    assign Z_out = (~S[2]) ? (Z_in | (S[1:0] != 3)) & F_a_z
    : (S[1:0] == 3) & Z_in | (S[1:0] != 3) & (F_l == 0);

    (V_out/H_out は変更なし)

    これで +42 スライスも増えてしまった。そして肝心の性能は落ちたり... 上がったケースもあったのだが、どの変更が関係しているか分からなく...

    さて、2 の補数を作っているところがある。もったいなく感じて、演算もまとめってみた。


    wire [8:0] B2 = S[1] ? ~{1'b0, B} : {1'b0, B};
    wire [8:0] B2I = B2 + 1;
    wire [8:0] F_a = {1'b0, A} + (( S[1] ^ WC ) ? B2I : B2);

    wire [7:0] R0 = (A ^ B);
    wire [7:0] R1 = (A ^ (B-1));
    wire [7:0] R2 = (A ^ B2I);

    (あとは同じ)

    この変更で、わずかに規模が増えて、さらに遅く.... なかなか難しいものだ。

    wire F_a_z = (~S[1] & WC) ? (R0 == 8'hff)
    : (~S[1] & ~WC) ? (R1 == 8'hff)
    : ( S[1] & WC) ? (R2 == 8'hff)
    : (R0 == 8'h00) ;

    これに変えると、少し速くなった。だが、ノーマルより少し速いだけ。

(続く)
posted by すz at 02:37| Comment(92) | TrackBack(0) | CPLD

2010年09月23日

SDRAMの使い方(メモ)

中華PMPのメモリ換装用に16bit幅のSDRAMを確保した。換装すると 128Mbit の SDRAMが (いくつか)出てくる。

この SDRAM は、どういうことに使えるのだろう?

    プロセッサに付ける -- というのはとりあえずなし。外したわけだし。

    CPLD/FPGA に付ける -- まぁ基本これしかないのだが、FPGA でプロセッサを作るというのはとりあえずなし。理由は上記とおなじような感じがするから。

プロセッサでないものに付けた場合、なにができるか考えてみたのだが、フレームバッファのように定期的に出力するものか、ビデオキャプチャや ロジアナのように定期的に取り込むものしか思いつかなかった。

ここでは、ロジアナを想定して 定期的に取り込めるものなのか検討してみることにする。

    16bit 幅 128Mb SDRAMだから、8M 個もサンプリングできてしまう。こんな 大量にデータを採取できたとして何に役立つのか? という疑問はある。でも まぁデータ量が多いほうが高級そうだから良いのだ。

    あと、うまく作れれば 最高で 166M sps なわけだが、これで充分なのだろうか?

    高速ADC をつなげればオシロも作れる。2ch 欲しいから ADC は 8bit しか選択できない。8bit 80M sps なら秋月で 400円で買える AD9283BRS-80 がある。デジキーで買える あまり高価でない 8bit のものは、ADC08100 , AD9283 で 100Msps 1000円ぐらいのクラス。 無理をしても 200Msps の ADC08200 (1500円ぐらい) が上限。-- まぁ充分といえよう。

    ロジアナとして使った時はどうだろう? FULL スピードの USB でも 48MHz なら充分そうだし、普通の電子工作なら 数十 MHz までで充分なはず。PC 関係の機器は高速すぎてどうせ無理。USB でも HIGH スピードなら 480 bps だし。

    そういえば USB 解析するなら 64 バイトのパケット 100 個分採取するとして 6400 x 8 x 4 で 200K 個ぐらいサンプリングできたら良いかも知れない。-- 余裕があるのは良いことなのだ。

    追記: うまく作れず 166MHz品でも 最高周波数は、133 MHz になった。

まずは、SDRAM は、どういう風に使うものなのかお勉強。

教科書として、128Mb 16bit幅 の SDRAM W9812G6JH のデータシート(pdf)を使う。-- 余ってくる SDRAM がこれなのだ。

参考資料は、XAPP851(日本語,pdf)。-- XAPP851 は、日本語なのが嬉しい。DDR の説明だが SDRAM と全然違うわけでもないので参考になりそう。

コマンド



SDRAM は、クロックの↑で動作する。このタイミングでの /RAS /CAS /WE の組み合わせで動作を指示する。コマンドは以下の 8 つ。

    /RAS /CAS /WE
    LOAD MODE REGISTER L L L
    AUTO REFRESH L L H (Auto refresh or Self refresh)
    PRECHARGE L H L (Deactive row)
    ACTIVE L H H (Select bank / Active row)
    WRITE H L L (Select bank/col , start write)
    READ H L H (Select bank/col , start read)
    BURST TERMINATE H H L
    NOP H H H

おおざっぱに言うと、MODE REGISTER で動作モードを設定。ACTIVE で row(上位アドレス)を設定し、READ/WRITE で col(下位アドレス)とバンクを設定して、READ/WRITE を開始する。... ということらしい。

BURST TERMINATE というのは、連続動作を止めることで、XAPP851 には記載されていない。

たとえば 128Mbit 16bit 幅 SDRAM の場合、row は A0-A11 の 12bit , col は A0-A8 の 9bit 。合計 21bit で 2M のアドレスがある。バンクが 4 つ、16bit 幅だから 16MB (128M bit) 。

モードレジスタは、A0-A11 の 12bit で指定する。



バンクとは何かというと、独立したメモリ。... 4つのメモリがあって、同時に動作させることが出来る。ただしコマンドは同時に 1 つしか指定できないから、クロックをずらして動作させることになる。あと、バンクは独立した信号線 BA0-BA1 で指定する。

READ は、コマンドで指定してから数クロック後にデータが取得できる。 1 回の READ コマンドで、MODE REGISTER で設定したバースト長分のデータが読み込める。シーケンシャルモードでは、1 ページの範囲(= col で指定できる範囲 x bank)で BURST TERMINATE を発行するまで連続して読み込むことも出来る。(最後まで読むと オフセット 0 に戻る。そこで ROW を 切り替えないといけないので途切れることになり 今回の目的には使えない)

バースト長は、1/2/4/8 。

複数のデータの読み込みを複数のバンクで(勝手に)行うと データバスが競合してしまう。競合しないようにコマンドを組み立てないといけない。

あと、PRECHARGE 。これは? Deactive row -- close のようなものらしい。ちなみに、READ/WRITE のときに、A10 を H にすると AUTO PRECHARGE モードになって、明示的に PRECHARGE コマンドを発行しなくて良くなる。

READの概要


READ は、

  • 1. ACTIVE コマンド (ROW と BANK を指定)
  • 2. READ コマンド (COL と BANK を指定)
  • 3. BUERST RERMINATE (シーケンシャルモードのみ)

の手順。ACTIVE コマンドを発行したら、数クロック後に READ コマンドを発行できるようになる。READ コマンドを発行したら 数クロック(CAS レイテンシ)後に(バースト長分連続で)データが出てくる。

バースト長ずらして、違うバンクに対して READ を行うことで途切れなくデータを読み出せる。

シーケンシャルモードのとき、BUERST TERMINATE を指定してもすぐには止められない。次のクロックが最後のデータ。

    今回は使わない想定だが、フレームバッファでは使えるかも知れない。COL 9bit(512) x 4 bank = 2048 の 16bit データが 連続で出てくる。

WRITEの概要


WRITE は、

  • 1. ACTIVE コマンド (ROW と BANK を指定)
  • 2. WRITE コマンド (COL と BANK を指定)
  • 3. PRECHARGE コマンド

の手順。WRITE コマンドのときに、最初のデータを指定し、以降バースト長分連続でデータを指定する。データを書き終わったら PRECHAGE コマンドを発行。数クロック後に、次の ROW を指定できるようになる。

あと、REFRESH をどうするか考えなくてはならないが、だいたいはこんなところ。

    使っていないときは、オートリフレッシュモードにしておくのが良さそう。

連続取り込みの設計



8 クロックを 1 周期として考え、バースト長 2 で次のようにすれば良いのではないか?


0 BA0 WRITE BA0 DATA
1 BA0 DATA BA2 ACTIVE
2 BA1 WRITE BA1 DATA
3 BA1 DATA BA3 ACTIVE
4 BA2 WRITE BA2 DATA
5 BA2 DATA BA0 ACTIVE
6 BA3 WRITE BA3 DATA
7 BA3 DATA BA1 ACTIVE

このシーケンスだと、BA0 に注目して見ると WRITE から ACTIVE まで 5 クロック、ACTIVE から WRITE まで 3 クロックある。
これで、SDRAM の仕様を満たすかチェックしないといけないが、OK なら メモリ分連続で書き込むことができる。



    これは、Write のタイミングの一部。tRCD (RAS to CAS delay time) を満たせば良いらしい。データシートでは、166MHz品 が 15ns , 133MHz品 が 20ns。最高周波数で、3 clock 必要。(OK)



    これも、Write のタイミングの一部(CL2)。tWR(Write Recovery time) と tRP(Prechage to Active Command period) を満たす必要がある。tWR は2 clock 。tRP は、166MHz品 で 15ns, 133MHz品で 20ns (最高クロックで 3 クロック) 。-- こういう条件があるから CL3 しか高クロックでは使えない。

    合計 5 クロックだから OK. .. じゃなかった。最後のデータを 書いてからだから 6 クロックになってしまう。

    5 クロックにしないと成り立たないから、最高クロックが 133MHz になる。Write で CL2/CL3 は関係ないと思うが 133MHz に落とすなら CL2 にできるから CL2 にすることにする。

    あと tRC (ROW cycle time - Active to Active Command Period) という条件があり 166MHz品で 60ns 。133MHz 8 クロック(60ns)はぎりぎり OK。



    ついでに READ のタイミング。これも図は CL2 。

    最後のデータの次から ACTIVE を発行できると理解すれば良いようだ。

最高クロックは分かったが、最低クロックはどれぐらいだろう?

    いちいち ACTIVE で ROW を指定しているわけだから、下位アドレスに ROW をもってくれば、最短で一巡できる。一巡したら COL をインクリメントするとして... 一巡する時間を リフレッシュの条件を満たすようにする。

    • W9812G6 は、COL 9 bit ROW 12 bit x 4bank x16bit の128Mb SDRAM。2M x 4 x 2byte = 16MB 。
    • 8 クロックで 1 セットだが、この間 同じ ROW値なのに注意。
    • メモリ全部は使わないという設計なら、最低周波数は落とせる

    4096 の ROW を 一巡するのを 64ms 以内にする。この条件から 512K Hz が最低クロック。(ちなみに このクロックなら 8 秒間 持つ。)

取り込んだデータを取ってくるのはどうしよう。

    AT90USB162(or Mega32u2) を使うとすれば SPI 経由で取り込むことになる。スループットは 200K byte/sec ぐらい。

    これだと最低クロックの条件を満たせないから、途中でリフレシュしないといけない。

      SPI インターフェイスを付けるとして、どうコマンドを発行するか設計しなければならないわけだが、どうせほとんどの時間は IDLE になる。

      (セルフリフレッシュとは別に)、READ 処理のシーケンスにオートリフレッシュを入れれば良いのだろう。

    HIGH speed 対応で パラレル インターフェイスが使える FTDI FT2232H を使うなら 平均 512KHz (1MB/sec) で取り込むのは楽勝だろう。これなら最低クロック クリアのための工夫はいらない。

クロックの切り替えはどうすれば良いのだろう。

    FPGA なら FIFO を作れるから 1 セット分 8 x 16bit 溜ったら 書き込めば良い... それなら途切れないように取り込む必要もなくなる。

    でも、それじゃ、CPLD で作れる可能性がなくなって面白くない。

    ちょっと考えてみたい。

      セルフリフレッシュモードに移行したら、クロックを止められる。

      取り込みも READもしていない時は、 セルフリフレッシュモードに移行することにして、その間にクロックを切り替えれば良さそうだ。


消費電流はどれぐらい?

  • 最初に見たのは W9812G6IH operating current は、166MHz品 120mA, 133MHz品 100mAとなっているのだが、オートリフレッシュでは、これより多い 166MHz品 200mA, 133MHz品 190mA 。
    取り込み中は、PRECHAGE しまくりだから 中間ぐらいのはず。

  • W9812G6JH では、166MHz品で operating current 70mA、オートリフレッシュ 60mA と随分減っている。ただ手持ちは どちらでもない W9812G6PH -- 情報がないので良くわからないのだ。

  • 256Mb/512Mb だと 容量に比例はしないものの もっと増える。
     - 512Mb のリフレッシュは 400mA ぐらい。

  • セルフリフレッシュモードでは、2mA 。

SDRAM の信号線



    ちょっと 他の目的で作ったピンアサインの図を流用。
    これは、512Mb の SDRAM 。128Mb x16 は、A12 だけ NC。


    10 PB16 DCS(SDRAM) → CS#
    11 PB17 RAS(SDRAM) → RAS#
    12 PB18 CAS(SDRAM) → CAS#
    13 PB19 SDWE(SDRAM)/BUFD → WE#
    14 PB20 WE0(SDRAM) → DQML
    15 PB21 WE1(SDRAM) → DQMU
    16 PB24 CKO(SDRAM) → CLK
    17 PB25 CKE(SDRAM) → CKE

    D0-D15 → D0-D15
    A0-A12 → A0-A12
    A13 → BA0
    A14 → BA1


    これは、中華PMP で実際に配線している信号の例

    信号線の本数だけを問題にすると、データ線 16本以外に 23 本。
    DQML/DQMU は共通にでき -1, A12 は 128Mb x16 SDRAM では NC なので -1。
    FPGA(/CPLD) と接続する線は、37本。

FPGA(/CPLD) は何を使う?

    0.5mm ピッチ 100pin の VQ100 の XC3S50A/XC3S200A などはどうだろう? 値段は思ったより安く 50A で 528円。200A でも 1229円。

      Spartan-3A ファミリデータシート(日本語,pdf)を見ると SPI FLASH でも コンフィグできるようだ。詳細は UG332(日本語,pdf)で、見ると M25P をサポートしているが、SST25L/SST25V/MX25/A25L なども使える。これらは A25L080の使い方なんて記事を書いたぐらいで、入手済み (最近も SST25VF032B を 単価 230円で購入)。
       - READ(03h)/FAST READ(0Bh) のコマンド体系なら OK らしい。
       - 必要な容量は、50A が 512kbit 200A が 2Mbit。
       - Spartan-3E も 使えて 100E が 1Mbit/250E が 2Mbit -- だだしちょっと仕様が違う。
      そういえば、以前 RSコンポーネンツのセールで、M25P05(512Kbit) を 単価 20 円で購入していた。テストにしか使えないと思っていたが 50A なら使える!
       - ただし最高クロックは低く 20MHz (SST25VF032B は、READ 25MHz/FAST READ 80MHz)

      本題とは関係ないのだが、付録 FPGA ボードなどは、core 電圧用に 安価な レギュレータを使っている。中華PMP では (3.3V はレギュレータだが) core 電圧用は、バッテリー駆動することもあり例外なく 電圧下降型 DC/DC コンバータ。

      ちょっと 1.8V 用を調べてみたのだが、外付けダイオードが不要な同期整流タイプは、 Torex XC9236 , Fairchild FAN5307 , Semtech SC189 が見つかった。
      いずれもピン配置は同じで

         (1) Vin Lx (5)
      (2) GND
      (3) EN Vout (4)

      自作するならこういうのを使ってみるのも良いかも知れない。

    DWM 付録だといろいろある。が、付録は再入手が難しいかも。



    ちょっと高いがマルツで互換品が販売されているものもある。

    • MFPGA-SPAR3E Spartan-3E(XC3S250E-4VQG100C) ボード 5250円。
       - DWM 2007年7月号付録基板互換
       - XCF02SVO20C搭載済み、80 ピン基板用ピンヘッダー[40ピン×2列]、3端子レギュレータ LM317T、50V 10μF電解コンデンサ(各1個ずつ添付)
    • MFPGA-CYCLONE CYCLONE(EP1C3T100C8N) ボード 5250円。
       - 上記の CYCLONE版
       - EPCS1搭載済み、80 ピン基板用ピンヘッダー[40ピン×2列]、3端子レギュレータ LM317T、50V 10μF電解コンデンサ(各1個ずつ添付)

    できたらそういうものを選択したほうが無難かも知れない。

    あるいは、今でも買える 【FPGA超入門】ディジタル・デザイン・テクノロジNo.1 (LatticeXP2) 2979円 とか。



    あと安いのは、オプティマイズの MAX2 CPLD ボードとMAX2 mini。なんと 1600円(mini 1500円) -- CPLDだからコンフィグROM も不要。FIFO を使わないならこれでも作れるはず。

      実は Cool Runner II 64マクロセル x2 で作れないかと思っている。それは無理かも知れないのだが、これなら作れるだろう。

      mini は、PIC32ボード (2300円) とスタックして使う設計らしい。



    ちなみに、SDRAM が載るピッチ変換ボードは、秋月の AE-SOP-56 200円。

あと本気でロジアナを作るなら クロック生成 とか レベル変換も考えないといけないが、ここでは SDRAM 周りの話題に留めるつもり。実際の回路(含 VHDL)については(進展があれば)別記事にする。

追記: SRAM のように使えるか?

    SDRAM を ランダムアクセスするとしたら、どれぐらいの速度になるのだろう?

    CL3/CL2 で何クロック必要なのか見積もってみよう。

    • WRITE

      COMMAND BUS WAIT-REASON
      0 ACTIVE
      1 IDLE tRCD
      2 IDLE tRCD
      3 WRITE DATA
      4 IDLE tWR
      5 IDLE(AP)
      6 IDLE tRP
      7 IDLE tRP
      8 (次の)ACTIVE

      CL3 だと 8クロック かかる。( CL2 だと 6 クロック。)
    • READ

      COMMAND BUS WAIT-REASON
      0 ACTIVE
      1 IDLE tRCD
      2 IDLE tRCD
      3 READ
      4 IDLE(AP) (CAS Latancy)
      5 IDLE (CAS Latancy)
      6 IDLE DATA
      7 (次の)ACTIVE

      CL3 だと 7クロック かかる。( CL2 だと 5 クロック。)

    実を言うと tRC/tRFC (Row cycle time/Refresh cycle time - Refresh/Active to Refresh/Active Command Period) という条件があって 166MHz品で 60 ns だそうだ。

    結局 tRC/tRFC の条件のほうが厳しいので、

    • 166MHz CL3 で 60 ns (10 クロック)
    • 133MHz CL3 で 60 ns (8 クロック)
    • 100MHz CL2 で 60 ns (6 クロック)

    リフレッシュサイクルを定期的にいれないといけないが、概ね SRAM 程度の速度で使えることになる。-- 10ns とかの 高速 SRAM 並は全然無理。

      FPGA上 の (キャッシュがない)CPU 用メモリとして使う場合、CPU のメモリクロックは最大 16.6 MHz になってしまう。( しかも、リフレッシュのために時々止まるのが前提。)

      AVR のようなアーキテクチャで プログラム用メモリ+RAM に使うとすれば、最大 16.6 MHz で時々 1 クロック止まるものになる。しかも RAM は常に 1 WAITがはいる。

      止めないようにするには、FLASHアクセス と RAMアクセス(or リフレッシュ)を時分割するしかなく CPU クロックが 8.3 MHz まで落ちる。

    SDRAM が得意なのは、バースト転送。無限長なら 10ns 毎にデータを書けるし、(検証してないが)読める。

    常に 連続した 8 つのデータ を読み書きするなら、上記 +7 クロックで済む。

    • 166MHz CL3 で 90 ns (15 クロック)
    • 133MHz CL2 で 97.5 ns (13 クロック)
       - 133MHz でも tRC の条件 に引っかからない

    この前提だと、4倍前後のスループットになる。
    SDRAM を付けるなら、CPU の方も L1 キャッシュ付きにしたいところ。
posted by すz at 18:11| Comment(91) | TrackBack(0) | CPLD

2010年03月17日

PLL発振器メモ

XMEGAボードで CPLD を使っているが、たぶんマクロセルが余る。ピンも余っていて、L24A3 なら若干スペースもある。

それなら、PLL発振器を仕込んでおきたい。回路は、PLL を CPLD で作った記事: 「PLL発振器の製作」「PICマイコンを使ったPLL方式ステレオFMラジオ」を参考にしようかと思う。

あと、VCO をどうするか。3.3V で動作するものにしたいし、表面実装部品を使いたい。

... 容量可変ダイオードを使った、LC タンク回路の例は、リンク先にある。... のだが使いたいのは 1SV322



だいたい 8p -50p の範囲。これと手持ちの インダクタの共振周波数を計算してみると ...

    50pF 8pF
    6.8uH 8.6 MHz 21.6 MHz
    10uH 7.1 MHz 17.8 MHz
    22uH 4.8 MHz 12.0 MHz
    47uH 3.3 MHz 8.2 MHz

このあたり。だが 100 MHz ぐらいで発振させたい。


    50pF 8pF
    0.1uH 71 MHz 178 MHz
    0.33uH 39 MHz 98 MHz


どうもこのあたりを使わないといけなさそうだ。

共立で LAL02 の 0.27uH なら扱っている。デジキーで探すと、LK1608が見つかった。



100MHz 超えでも 大丈夫そうな感じ。

FM 向けだと、発振回路は、FETを使った例が多いのだが、ここは LVC2GU04 を使ったフランクリン発振回路を使いたい。以前自分でも記事を書いた覚えがあるが、「LC共振周波数を利用したインダクタンスメータ」の回路にしようかと思う。



回路図を書いてみた。4 線を CPLD に接続するわけだが、出力は GCK に割り当てる。CPLD の中は別途検討する。

出力以外も接続しているが、INV0 は発振を止めておくため。INV1 は不要だが、LVC2GU04 なしの実験をするため一応。

D1 は、1SV322 を予定していたが、SCV710 にするかも。D2 は 直列になっているだけだから 0.1u で良さそうな気もする。あと 数 MHz なら L を変えれば良いのだが、数十 MHz 以上で使えるかどうかについては、自信なし。



回路パターンは、こんな風にした。インバータは LVC2GU04 の予定で、SOT23-6 を予定していたが、0.65mm ピッチのものにするかも。インダクタは、CB2518 のパターンを 使用。低周波数に、CB2518 を使い、高周波数には NLHV25T を使う予定。

さて PLL 発振器が無事つくれたとして、何につかうのか?

ひとつはテスト用。きれいな正弦波が出るらしいので、オシロ機能で確認する。もうひとつは周波数カウンタのテスト。
-- インダクタを差し替えられるようにしておくと良いかも。

あとは、ダイレクトコンバージョン用のクロック生成。外部回路が必要だが、ラジオを作ってみたい。うまくすれば、AM だけでなく FM もいけるかも知れない。

追記: 購入した部品での周波数みつもり。

容量可変コンデンサ SVC710 と NLHV25T,NLCV25T を購入したので、見積もりしなおしてみる。

SVC710 は、だいたい 5p -40p の範囲。直列だと 5p - 20ぐらいか。これと インダクタの共振周波数を計算してみると ...

    20pF 5pF 自己共振周波数
    0.12uH 103 MHz 205 MHz NLHV25T 700 MHz
    0.33uH 62 MHz 124 MHz NLHV25T 400 MHz
    0.68uH 43 MHz 86 MHz NLHV25T 180 MHz
    3.3uH 19 MHz 39 MHz NLCV25T 55 MHz
    6.8uH 14 MHz 27 MHz NLCV25T 39 MHz
    15uH 9.2 MHz 18 MHz NLCV25T 21 MHz
    33uH 6.2 MHz 12.4 MHz NLCV25T 16 MHz
    :
    150uH 2.9 MHz 5.8 MHz (参考)
    220uH 2.4 MHz 4.8 MHz (参考)


上記の回路図の定数で果たしてどれぐらいまで発振するのだろうか?

NLCV25T で自己共振周波数が近いということは、インダクタの C の要素が 5pF に近いのと同じ? ならば 5pf - 20pF ではなく、10pF - 25pF のようなもの? 最高周波数が 落ちることは問題ないとしても、 最高周波数/最低周波数が 2 以上ないと困る...

やはり D2 を普通のコンデンサにしたい。

SVC710 を使った回路図をさがしてみたら LV2285VB という FMトランスミッタIC のデータシートにあった。(pdf)



FM 変調をかけるから、アノードは出力に接続されている。PLL では GND で OK 。PLL のループフィルタ相当がカソードに接続されている。

D2 相当はただのコンデンサ。数十pF を使っているが FM 変調の都合なのだろう。0.1uF ぐらいにしようかと思っていたが ... ちょっと考え直してみよう。

例えば 100pF を直列にすると 5pF - 40pF が 4.7pF - 28.5 pF になる。これに 5pF 並列にしたとすると 9.7pF - 33.5 pF 。

最大/最小 は 4 以上必要なので、×。

220pF なら 同じ計算で 38.8pF/9.8pF で ちょうど 4 倍ぐらい。-- 余裕をみて 330pF ぐらいが良さそう。1000pF でも良いように思うけれども一応。

さて、100kHz ステップで 周波数を設定できるようにするには、AVR の PWM で 100kHz を基準周波数として出力させる。最大 200MHz までとすると 1/1 〜 1/2000 で分周できるようにする必要がある。1/2048 は 11bit で比較値を格納するレジスタも 11bit 必要。合計 22bit も使うわけだが XC2C64A に入れられるだろうか?

AM用に考えると、最大 2MHz で 9kHz きざみ? これ自体は 8bitx2 で済みそうだが、33uH 使っても 12MHz 。6 倍の54kHz を PWM で出力して、最後に 1/6 すれば良い。たとえば 1/1 , 1/6 選択のレジスタ と 3bit のカウンタで 4bit 。合計で 20bit 。

PWM も周波数を変えられるわけだから、工夫することで bit数を減らせるはず。




ようやく、FreeHDL で PLL の回路を作ってシミュレーションできた。上は、REFCLK より PLLCLKの周波数が低いケースで、PLLCTRL が Hi-Z か H になっている。そうすると電圧が上がり、電圧が上がれば、容量可変コンデンサの容量が減り、周波数が上がるので OK 。下はその逆。参考にしたのは、CD74HC4046データシートのブロック図

  • ソース : pll-01.zip

    テストベンチと 分けてコンパイルする方法が分からなかったのだが、くっつけて 1 つにすれば 動くので Makefile で cat してビルドするようにしている。

    たぶん configuration を使うのだが、拾ってきた正しいサンプルでも エラーになるので諦めた。
posted by すz at 19:41| Comment(0) | TrackBack(0) | CPLD

2010年03月12日

CPLDの回路設計(2)

(CPLDの回路設計の続き)

CPLDで作る回路について


SPI をパラレルに変換して LCD にアクセスするような回路をCPLD で作ろうとしている。そのことは前記事のCPLDの回路設計で書いた。シフトレジスタで組む単純な シリアルパラレル変換と違うのは、制御バイト を前置することで、SPI だけで 他の信号線 を制御できることだ。

そして、LCD だけ制御するのではなく、ラッチの制御や プリスケーラの機能も付けようと思っている。

そろそろ、具体的な仕様を決めていこうと思う。

入出力


先に基板を設計したので、入出力の信号(名)とピンアサインは既に決めた。

ピンアサインを定義する "spi2par.ucf" の内容は、次のようにした。

    //
    #PINLOCK_BEGIN
    #Fri Mar 05 01:38:45 2010
    NET "EXTCLK" LOC = "S:PIN1";
    NET "LEOUT" LOC = "S:PIN20";
    NET "OEOUT" LOC = "S:PIN19";
    NET "EVOUT" LOC = "S:PIN28";
    NET "EVIN" LOC = "S:PIN27";
    NET "CS" LOC = "S:PIN30";
    NET "MOSI" LOC = "S:PIN31";
    NET "SCK" LOC = "S:PIN43";
    NET "DB<0>" LOC = "S:PIN12";
    NET "DB<1>" LOC = "S:PIN13";
    NET "DB<2>" LOC = "S:PIN14";
    NET "DB<3>" LOC = "S:PIN16";
    NET "DB<4>" LOC = "S:PIN29";
    NET "DB<5>" LOC = "S:PIN36";
    NET "DB<6>" LOC = "S:PIN37";
    NET "DB<7>" LOC = "S:PIN38";
    NET "MISO" LOC = "S:PIN33";
    NET "RD" LOC = "S:PIN23";
    NET "RS" LOC = "S:PIN21";
    NET "WR" LOC = "S:PIN22";
    #PINLOCK_END

GTS とか GTK という機能を持ったピンがあるので、CS と SCK,EXTCLK をそれらに割り当て、あとは配線するのに都合が良いように割り当てている。

それぞれのピンの機能について

  • CS,SCK,MOSI,MISO -- SPIポート

    マスターとの接続。CS を H にしたら 通信 関係のレジスタを 初期化することで、バイト単位の同期は取れるようにする。通信フォーマットは後述。

    あと、CS はプルアップしている。MISO は、CS が H のとき Hi-Z にした方が良いかも知れないが(今は)未サポート。

  • DB[7..0], RD,WR,RS -- LCD 制御

    LCD の制御線。(LCD の)RESET と CS がない。

    RESET は、ソフトRESET を使う。(ソフトRESET が使えるか確認必要)。LCD の RESET はプルアップしている。

    LCD の CS は、CS と接続。

  • OEOUT -- ラッチのトライステート制御ピン

    ラッチの OE に接続。状態は SPI で設定する。OE = H にすると Hi-Z (初期状態)。Hi-Z にすることで、ポートを別の目的に使えるようにする。

  • LEOUT -- ラッチ制御ピン

    ラッチの LE(CLK) に接続。↓で取り込み。 設定によって制御方法を変えられる。

  • EXTCLK

    ラッチを取り込むトリガ用の信号線(外部)。他に 周波数カウンタ用の入力ピンにも使う。CPLD で作るプリスケーラを通すことで、高い周波数もカウント出来るようにする。

  • EVIN

    ラッチを取り込むトリガ用の信号線(マスター)。定期的に取り込む場合などは、マスター側で制御する。

    XMEGA の事象出力を接続することで、すべての事象をトリガにできる。事象は、タイマーを使っての定期的な取り込み だけに使う予定。

  • EVOUT

    XMEGA への事象入力用。この信号でデータを取り込む。事象は、タイマーのクロック元にもできるので、周波数カウンタの入力としても使う。

ついでに XMEGA のボード L28A3 の XMEGA(A3) への接続を書いておくと ..

    MOSI PD7(DTXD1)
    MISO PD6(DRXD1)
    SCK PD5(DXCK1)
    CS PD4

    ※ USART の SPI マスタ機能を使う

    EVOUT PE6
    EVIN PE7(EVOUT)

    ( ラッチ出力 PORTF )


通信プロトコル



制御バイトを前置すろことと、I2Cキャラクタディスプレーの仕様の拡張にすることは既に決めた。どのように拡張するか検討することにする。

基本のプロトコル

  • 送信:

    CS を L にして [制御バイト] [データ] [制御バイト] [データ] ... と 交互に送る。制御バイトの bit7 は CONT で これを一旦 0 にすると、以降データのみになる。

    CS を H にすると CONT の状態はリセットされる。

  • 受信:

    CS を L にして [制御バイト] [ダミーデータ] [制御バイト] [ダミーデータ] ... と 交互に送る。

    制御バイトに READ を指示するビットがあり、ダミーデータ送信のときに READ したデータを返す。

    CONT を 0 にすると 以降無効なデータが返る。この様にしたのは、先読みをするが 終了を指示できないため 1 バイト余計に READしてしまうため。

  • RS の制御

    RS は、LCD にデータかコマンドかを指示する信号線。1bit のアドレス線と思っても良い。

    制御バイトの bit6 は RS で、READ または WRITE する前に RS線に出力する。

以上は基本。これを拡張して機能を付ける。

注意点としては、LCD の制御ルーチンでは、他の機能を考慮したくない .. ということ。-- 制御バイトに 他の機能の情報を混ぜないと LCD が制御できないような仕様はダメ。


  • 現状案

    制御バイト
    bit7 CONT (前述)
    bit6 RS (前述)
    bit[5..4] CMD
    "00" WRITE
    "01" READ
    "10" 装置制御
    "11" 未定義
    bit[3..0] アトリビュート
           装置制御:
    bit[3..2] EVOUT 選択
    "00" EXTCLK
    "01" not EXTCLK
    "10" EVIN
    "11" 1/64 EXTCLK (プリスケーラ)
    bit1 LEOUT 選択
    '1' EVOUT
    '0' '1' (透過モード)
    bit0 OEOUT 選択
    '1' '0' (ラッチ有効)
    '0' '1' (ラッチ無効 - Hi-Z)

    補足:

    装置制御では、続くデータは使用しないが、基本どおりにダミーデータを付ける。CONT を '0' にすると、READ と同じく以降無効になる。

    EXTCLK は、外部入力との間にインバータを入れる予定で、"01" not EXTCLK で正論理。"00" は負論理でこれを選択すると ↑ でデータ採取。

    さらなる拡張:

    装置制御で、続くデータも使用する拡張は簡単にできる。CONT を 0 にして 連続でデータを送るのもあり。

    ただし、LCD のような 独立した装置だとその仕様では使いにくい。"11" 未定義 を使って 4bit のアトリビュートで装置を選択したり 動作を指定するのが良さそう。

    ちなみに、RS は LCD 用で出力されてしまうが READ か WRITE しないと意味がないので、拡張用に使える。

デバッグの方法


好きなことを書いたが、実をいうと、どうやってデバッグするのか知らないレベルだったりする。

とりあえずググってしらべて見ると、FreeHDL が Linux でも使用できて良さそうな感触。

コマンドラインで使えるのが実は嬉しい。あと GUI で使いたいときは、qucs で使えるらしいのだが .. qucs は 教育用回路シミュレータ的な使い方が多く 論理シミュレータでどう使えるのかイマイチわからなかった。

ただ、セットで使うものなので、qucs に FreeHDL がもれなくついてくる。-- ソースは ここ。Windows バイナリは ここ

    FreeHDL Windows バイナリを インストールして見たのだが、gvhdl は perl スクリプトだった。perl は入れていないし freehdl-v2cc.exe を直接使うしかなさそう。

    普通はこんな風に使えるらしいのだが...

      gvhdl -c testbench.vhdl
      gvhdl spi2par.vhdl testbench.o --libieee


FreeHDL の使い方が皆目見当がつかなかったのだが、このページを見たら理解できた。

あとは、ファイルから状態を読み込みできれば、デバッグできる。.. とりあえず このページを見たら、なんとなく分かった。

MinGW+MSYS環境での FreeHDLビルド


qucs のところにある FreeHDL をインストールしてみたが全然動かないので、ビルドしてみた。

MinGW も MSYS も少々古いうえ、ちょっと変則的な環境なので、あまち参考にはならないかも。

  • 1. MinGW バイナリの追加インストール

    Source Forge の http://sourceforge.net/projects/mingw-install/files/mingw_dir/current/ から

    libtool-2.2.6b-20100118-1.tar.gz
    pkg-config-0.23-20100118-1.tar.gz
    libiconv-1.13.1-20100119-1.tar.gz/download
    gettext-0.17-20100119-2.tar.gz/download
    glib-bin-2.22.3-1-win32.tar.gz
    glib-dev-2.22.3-1-win32.tar.gz

    http://sourceforge.net/projects/mingw/files/MinGW%20expat から

    libexpat-2.0.1-1-mingw32-dll-1.tar.gz

    をダウンロードして /mingw に展開する。

    実は、glib-dev, gettext, libiconv, libexpat は、必須ではない。が、glib や pkg-config をビルドするのに必要なので、入れておいた方が良い。

  • 2. regex のインストール

    http://gnuwin32.sourceforge.net/packages/regex.htm から

    regex-2.7-bin.zip

    をダウンロードして "c:/Program Files/Regex" に展開する。

    良く分からないのだが、 /mingw の方にもコピー。

  • 3. Strawberry Perl のインストール

    perl を入れてなかったので、Strawberry Perlを入れて見ることにした。

実をいうと MSYS を最新にすれば、regex も perl もパッケージがある。それを使えばもっと楽だと思う。MinGW も同様で手動でインストールしなくて良い。

次は、FreeHDL のビルドとインストール。

$ ./configure --prefix=/mingw
$ make
$ make install

だけで、インストール自体はできる。環境変数の設定が必要で、

$ export FREEHDL=/mingw

して使う。

ただ、環境が変則的なので、少々変更が必要。

  • /mingw/bin/gvhdl の編集 と libtool のコピー

    • 1. FreeHDL の libtool を /mingw/bin/freehdl-libtool に名前を変えてコピー

    • 2. /mingw/bin/gvhdl の以下のところを変更

      my $libtool = "libtool";

      my $libtool = "c:/msys/1.0/bin/sh c:/MinGW/bin/freehdl-libtool";

    gvhdl は perl スクリプトで その中で libtool を実行するのだが、libtool は、シェルスクリプトで sh 自体は MSYS のコマンド。この環境のため、普通に libtool としても実行できない。

    あと、どうもビルドしたときの libtool を使わないと上手く動かないようだ。

    追記:
    仮の環境で 最新の MSYS をインストールしてみた。MSYS のファイルはみな lzma で圧縮されているが、MSYS の base に lzma が含まれている。

    MSYS のバイナリは、/usr にインストールする。perl と libregex の bin と dev 、あと libcrypt の dll をインストールすれば OK 。上記の 設定の問題はなくなり make install した状態で動作する。

実行できるかのテスト

上のリンクの easy.vhd を作り

$ gvhdl easy.vhd --libieee

とすると....


    gvhdl: FreeHDL root path is 'c:/MinGW'.
    gvhdl: executing 'c:/MinGW/bin/freehdl-v2cc -m easy._main_.cc -L c:/MinGW/share/freehdl/lib -o easy.cc easy.vhd'
    easy.vhd:26: unit easy (IIR_EntityDeclaration)
    gvhdl:
    gvhdl: ================================
    gvhdl: Compiling 'easy.cc'...
    gvhdl: ================================
    gvhdl: g++ -I c:/MinGW/include -c easy.cc
    gvhdl:
    gvhdl: ================================
    gvhdl: Compiling simulator main file 'easy._main_.cc'...
    gvhdl: ================================
    gvhdl: g++ -I c:/MinGW/include -c easy._main_.cc
    gvhdl: Linking simulator 'easy'...
    gvhdl: c:/msys/1.0/bin/sh c:/MinGW/bin/freehdl-libtool --mode=link g++ easy._main_.o easy.o -lm c:/MinGW/lib/libfreehdl-kernel.la c:/MinGW/lib/libfreehdl-std.la c:/MinGW/lib/freehdl/libieee.la -o easy
    linker: g++ easy._main_.o easy.o -o .libs/easy /mingw/lib/libfreehdl-kernel.a c:/progra~1/Regex/lib/libregex.dll.a -LD:/Progra~1/GnuWin32/lib /mingw/lib/libintl.dll.a -lwsock32 -lole32 -luuid -lmsvcp60 /mingw/lib/libfreehdl-std.a /mingw/lib/freehdl/libieee.a -Lc:/progra~1/Regex/lib -L/mingw/lib
    linker: creating easy
    gvhdl: ================================
    gvhdl: Simulator 'easy' created.
    gvhdl: ================================

とメッセージが出て easy.exe が出来る。

実行すると ...

default component instantiation for unit ':tb_arch:tb_q:e0'. Using 'work.easy'!
Available commands:
h : prints list of available commands
c : execute cycles = execute simulation cycles
n : next = execute next simulation cycle
q : quit = quit simulation
r

というメッセージが出て、プロンプトが出る。

> dc -f wave.vcf
> d
> r 60ns
> q

で、結果が出力された。

wave.vcf は、テキストファイルだが、まぁ読めるような感じではない。GtkWave で見ることが出来るらしい。Windows 版もある。(参考)

あと、dump しないで、show コマンドで見ていくという使い方もできるし、見やすいフォーマットで標準出力に出す テストベンチを作っても良さそう。

追記:



ようやく、spi2par の回路(のベース)をシミュレーションできた。
このチャートは、2 バイトのデータを WRITE するときのもの。CMD = 0xff だと、CONT=0 で 以降 WRITE データ。あと RS=0 。

結局 GtkWave が便利ということが分かった。あとデータのパターンは 直接書いている。ファイルからの読み込みをすると、パターン変更のとき便利ではある。... が 基本的なところが動いてから。
posted by すz at 02:05| Comment(0) | TrackBack(0) | CPLD

2010年03月04日

CPLD の回路設計

(前の記事:2.8インチ液晶 EGO028Q02から分岐)

ようやく CPLD で作りたいものが決まった。なにかというと SPI to パラレル変換器。単純なものは シフトレジスタ + αで作れるわけだが、多少機能を付ける。

機能は、制御バイト + データ のプロトコル のサポート。

データを送る前に 制御バイトを送る。こうすることで SPI だけで制御できるようになり XMEGA だと DMA が使えるようになる。AVR の SPI でも パラレル出力 並の性能にできる。

制御バイトを使うやり方は、i2c液晶の設計で参考にした ST7032i のものをベースに拡張する。

制御バイトの定義:

    bit7 CONT 0: CS=L の間は 次からデータのみ。(制御バイト固定)
    bit6 RS RS ピンに出力 ( LCD では 1: DATA 0: CMD )
    :
    bit2 RD 1: READ 0: WRITE

    補足:

    RD のビットが離れているのは、RD を受け取った時点で処理するため。機能拡張する場合 bit5 - bit3 で定義して 状態を確定させておかないといけない。
    また、制御バイトが来たとき 1 回 READ するだけなので、CONT=0 は READ には使えない。


さてこういうものを作りたいわけだが、まず前提として

ということにする。

まずは、外部仕様の定義:

    SCK : in STD_LOGIC;
    MOSI : in STD_LOGIC;
    MISO : out STD_LOGIC;
    CS : in STD_LOGIC;
    RS : out STD_LOGIC;
    WR : out STD_LOGIC;
    RD : out STD_LOGIC;
    DB : inout STD_LOGIC_VECTOR (7 downto 0) bus

8080 インターフェイスと SPI だから 基本的な説明はしない。ここでは制御の仕方のポイントだけ。

  • DB は、tri-state で、CS=H のとき 無条件に Z にする。出力は、WRITE するときのみ。
  • CS は L で active 。H にする毎に初期状態にする。
  • READ の場合、前のバイトで RD を↓ にしておいて、読み込みを指示する。最後に読み込んで RD ↑ 。
  • 使おうとしている LCD は、WR のパルス幅 50ns 以上。RD のパルス幅 150 ns 以上。という条件がある。SPI のクロックを 16MHz にした場合これをクリアするには、WR 1 クロック、RD 3 クロックのパルスにする必要がある。

次に内部状態の定義:

    signal spireg : std_logic_vector(7 downto 0) := "00000000";
    signal rdreg : std_logic_vector(7 downto 0) := "00000000";
    signal spicnt : std_logic_vector(2 downto 0) := "000";
    signal sWR : std_logic := '1'; -- WRITE 指示、負論理
    signal sOE : std_logic := '1'; -- 出力 指示、負論理
    signal sRD : std_logic := '1'; -- READ 指示、負論理
    signal sRS : std_logic := '1'; -- RS のラッチ
    signal sCB : std_logic := '1'; -- 制御バイトかどうか
    signal sCONT : std_logic; -- CONT のラッチ

READ 用 レジスタを分けている。うまくすれば不要だと思えるが、とりあえず。spireg は、SPI からの 入力専用。spicnt は、何 bit 目かを数えるカウンタ。

スタティックな制御

    WR <= sWR;
    RD <= sRD;
    RS <= sRS;
    DB <= spireg when (sOE = '0') else "ZZZZZZZZ";
    MISO <= rdreg(7);

基本的に内部状態がそのまま外に出ている。DB は tri-state なので、上記のように記述。

次は、同期動作。process (CS, SCK)

まず CS が H になったときの動作から

    sWR <= '1';
    sOE <= '1';
    sRD <= '1';
    sRS <= '1';
    sCB <= '1';
    spicnt <= "000";
    spireg <= "00000000";
    rdreg <= "00000000";

-- 初期状態にしているだけ。sOE が 1 になるので、DB も Z になる。

次に SCK が H になったとき(↑)

    -- シフト動作
    spireg(7 downto 1) <= spireg(6 downto 0);
    spireg(0) <= MOSI;

    -- RS の 受け取り (= 出力変更)
    if ((spicnt = 1) and (sCB = '1')) then
    sRS <= MOSI;
    end if;
    -- RD の制御
    if ((spicnt = 5) and (sCB = '1')) then
    sRD <= not MOSI;
    end if;
    if (spicnt = 0) then
    sRD <= '1';
    end if;
    -- CB と CONT の制御
    if (spicnt = 7) then
    if (sCB = '1') then
    sCB <= '0';
    sCONT <= spireg(6);
    else
    sCB <= sCONT;
    end if;
    end if;
    -- sOE の 制御
    if ((spicnt = 7) and (sRD = '1') and (sCB = '0')) then
    sOE <= '0';
    else
    sOE <= '1';
    end if;

    補足:
  • RS の 受け取り (= 出力変更)

    RD を変化させる前に 変化させておく必要があるので、データが来たらすぐに変化させる。

  • RD の制御

    データの読み込みをしておいて、次の バイトで送り出したいので、データが来たらすぐに変化(↓)させる。↑ するタイミングは、次の バイトに入ったとき。

    RD のパルス幅は 3 クロック。例えば ↓を 1 クロック遅らせるには、(spicnt = 6) のとき、 入力を spireg(0) にして処理する。

  • CB と CONT の制御

    最後のデータが来たとき、次の制御を決める。sCB = 1 なら、次は sCB = 0 でデータ処理。sCB = 0 で データ処理が終わったなら、次は、sCONT で決める。sCONT = 0 なら次もデータ処理。

  • sOE の 制御

    最後のデータが来たら、RD でも CB でもなければ、DBに出力するために、sOE = 0 にする。


最後、SCK が L になったとき(↓)

    -- カウンタ ++
    spicnt <= spicnt + 1;
    -- rdreg の ロード/シフト動作
    if (spicnt = 7) and (sCB = '0') and (sRD = '0') then
    rdreg <= DB;
    else
    rdreg(7 downto 1) <= rdreg(6 downto 0);
    rdreg(0) <= '0';
    end if;
    -- sWR の 制御
    if ((spicnt = 6) and (sRD = '1') and (sCB = '0')) then
    sWR <= '0';
    else
    sWR <= '1';
    end if;

    補足:
  • sWR の 制御

    最後のデータの1つ前で、RD でも CB でもなければ、WR のパルスを出すために、sWR = 0 にする。

    WR の↑で データが採取される。L にする 期間は 1 クロック。↑のあと 半クロックデータを保持する必要がある。(15ns 以上)

以上は、とりあえず書いてみただけのもの。合成できたものを ベースにしているが、バグっているかも。思った通りに動作するのかどうかの検証はこれから。

タイミングの検証


    CS ~|________________________________________...._|~
    _ _ _ _ _ _ _ _ _
    SCK ___| |_| |_| |_| |_| |_| |_| |_| |_| |_
    : :
    MOSI |MSB| D6| D5| D4| D3| D2| D1| D0|
    spireg S S S S S S S S S (S: SHIFT)
    : : :
    spicnt 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 0
    rdreg S S S S S S S L (L: LOAD)
    MISO MSB| D6| D5| D4| D3| D2| D1| D0| MSB
    : : : :
    (WRITE) : : : :
    ________________________________ _____
    sOE : : |___|
    _____________________________ _____
    WR : : |___|
    : :
    DB[7..0] ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ| |ZZZZZ
    : : spireg
    (READ) : : :
    ________________________ _____
    RD : |___________|

    DB[7..0] ZZZZZZZZZZZZZZZZZZZZZZZ| READ DATA |ZZZZZ
    :
    _______ ________________________
    RS _______X________________________



バグを直したらこのページも直しておく予定。

ソースコードは、spi2par.vhd だが、これも直したら ファイル名そのままで 更新する予定。

あと、ピンアサイン spi2par.ucf

    ピン19 〜 38 の bank 2 メインで割り当ててみた。順番は LCD の ピン 1 からの順番。ただし、DB[765] は離れているので間に SPI の信号を割り込ませている。あと、CS は、GCR で SCK は GCK から選んでいる。

    あと、変更した設定は、I/O Voltage Standard を LVCMOS33 に。(→ 参考)


結果:

************************* Mapped Resource Summary **************************

Macrocells Product Terms Function Block Registers Pins
Used/Tot Used/Tot Inps Used/Tot Used/Tot Used/Tot
26 /32 ( 81%) 38 /112 ( 34%) 37 /80 ( 46%) 25 /32 ( 78%) 15 /33 ( 45%)
posted by すz at 19:09| Comment(0) | TrackBack(0) | CPLD