2012年08月16日

USBデバイスドライバ メモ

以前 AVR 向けに作った USB デバイスドライバを PIC32MX に移植しようとしている。これについてのメモ。

    jzlib や pic32progの改造とか 半端な状態なのだが、これもまた 半端。まぁ、全部揃わないと意味ないし、並列にやっていくのだ。『ブートローダの検討』なんて記事も書いたのだが、既存の HID ブートローダは、動けばラッキーみたいに思っていて積極的にデバッグする気は今はない。jzlib がきちんとなれば動くはずなので、後回し。手をかけたいのは、やっぱりこっちの方。

仕様について

    もともとは、V-USB 用のコードを AT90USB162 に移植したくて作ったもの。だから API も V-USB の仕様をベースにしている。

    移植するにあたり、どういうものだったか 思い出さないといけないので、整理しておこう。

    usbPoll()
    __usb_ctrl_recv(setup_data, 8);
    if (pid == _PID_SETUP)
    if (request_type == USB_RQ_TYPE_STANDARD)
    :
    :
    else if (request_type == USB_RQ_TYPE_VENDOR)
    r = (usb_vendor_setup)(setup_data,reply_data);
    else
    r = (usb_user_setup)(setup_data,reply_data);

    if (r == 255) send_zlp();
    else if (r == 254) reply_stall()
    else usb_reply_setup(...);

    else // (pid == _PID_DATA0)
    r = (usb_user_out)(setup_data,len);

    if (r == 255) send_zlp();
    else if (r == 254) reply_stall();

    usbPoll()を定期的に call することで、(コントロールエンドポイントについては) すべてが動作する。

    usb_vendor_setup あるいは usb_user_setup に callback 関数を設定すると、標準以外の SETUP パケットを受け取ったら これらの関数を call する。これらの関数が、reply_data を セットすれば、その後 IN パケットで reply_data を PC に送信することになっている。

    V-USB だと、reply_data のハンドリングも 使う側が行わなければならないのだが、8 バイト毎に データを区切って生成するコードを作るのが面倒なので、MAX_REPLY_LEN までをバッファリングできるようにしている。(MAX_REPLY_LEN は usbconfig.h で指定)

    一方 SETUP に続いて OUT パケットが来るような 使い方があるのだが、こちらの方は、V-USB と同じ。 user_out を設定すると、8 バイト毎に 区切られて user_out が call される。あらかじめ 何バイト来るかは分からないので、やむを得ない。

    ここまで書いて、usb_user_out が 1 つしかないのはまずいと思った。これだと いくつかの プロトコルをサポートするのに不便なのだ。幸い user_out は、SETUP の request に関連づけられる。usb_user_setup が来てから usb_user_out を 設定すれば良いのだ。SETUP が来たらまず NULL に設定するようにすれば、関係ない request に対して usb_user_out が call されることもない。仕様を変更しておこう。

      基本的に、いくつも OUT パケットが来るケースというのは、ライタでいうと 書き込みデータ。処理自体はそれほど難しいわけではないようだ。

    ところで、reply_data を送信するのに、ローカルループを使っている。ここで usbPoll() をリカーシブに call 。今回は、stack の使用量がちょっと多いので、少々まずいと思っている。動きだしたら、どうするか再検討したい。メモ。

コンフィグ

    const char usbDescrDevice[] = { .... };
    const char usbDescrConfig[] = { .... };
    const int16_t usbDescrString0[] = { .... };
    const int16_t usbDescrString1[] = { .... };
    const int16_t usbDescrString2[] = { .... };
    const int16_t usbDescrString3[] = { .... };
    const int16_t *usbDescrStringTable[4] = {
    usbDescrString0,
    usbDescrString1,
    usbDescrString2,
    usbDescrString3
    };

    V-USB と同じシンボル名で const データを作成しておくのだが、V-USB のような縛りはない。すなわち このデータを生成しておきさえすれば良い。別のソースコードにすることもできる。

    void usbInit();

    で、各エンドポイントの設定を含む初期化を行うのだが、usbDescrConfig・usbDescrConfig を解析して 設定を行う。ただし、全部自分で作るのは、面倒なので 定形フォーマットを desc_asp.c と desc_cdcmsc.c に用意している。これを使うにあたりルールが出来ていて、いくつか usbconfig.h に設定する必要がある。

    #define USE_IAD

    #define EP_INT 3 //
    #define EP_TX 1
    #define EP_RX 2
    //#define EP_MS_OUT 4
    //#define EP_MS_IN 5
    #define MAX_EP (EP_INT+1)

    #define EP_SIZE 8
    #define EP_INT_SIZE 8
    #define EP_TX_SIZE 32
    #define EP_RX_SIZE 32
    #define EP_MS_OUT_SIZE 32
    #define EP_MS_IN_SIZE 32
    #define USB_BUFFER_SIZE ((EP_SIZE + EP_SIZE\
    + EP_TX_SIZE + EP_RX_SIZE \
    + EP_INT_SIZE) * 2)


    これは、desc_cdcmsc.c を使うときの例。MAX_EP と USB_BUFFER_SIZE は PIC32MX 移植での新設パラメータ。PIC32MX では RAM から エンドポイントの定義 や バッファーをメモリから切り出すので、必要になった。( デフォルトでも良いが適当に設定してしまうのでメモリが無駄になる。)

    #define EP_SIZE 8
    #define MAX_EP 1

    #define USB_BUFFER_SIZE (EP_SIZE * 4)

    これは、desc_asp.c 関係。ずいぶん単純になる。

    メモ: usbDescrString0 などを送信する部分がバグっていた。

    const int16_t usbDescrString3[] = {
    0 | (3<<8),
    };

    たとえば、こんな風に最初に 文字列部分のバイト数が入るのだが、この情報自体は含まれない。だが、usb_reply_setup() で送るバイト数が p[0] となっている。要するに 2 バイト足りない。これは! AVR の版も同じはず。良く動いていたものだ。

    あと、const を付けているにも関わらず Data の方に行ってしまっている。

    a.out
    a0000020 D usbDescrString0
    a0000024 D usbDescrString3
    a0000028 D usbDescrStringTable

    desc_asp.o
    00000014 R usbDescrConfig
    00000000 R usbDescrDevice
    00000000 G usbDescrString0
    00000028 R usbDescrString1
    00000048 R usbDescrString2
    00000004 G usbDescrString3
    00000000 D usbDescrStringTable

    -G 0 を付けると G の部分が R に変わるのだが ... D は変わらず。どういうことなのだろう?

    とりあえず、アセンブラコードを生成させてチェック。
     .rdata, .sdata, .data
    このどれかのセクションにしているようだ。大きいデータは、read-only なら .rdata になるが、小さいデータは read-only でも .sdata になり結果 .data に割り付けられる。昔あったワークステーション向けの割付けで、キャッシュの効率だとかページの効率だとかを優先しているのだろう。組み込みチップに最適化してくれているわけでは無さそうだ。

HID について

    USB の仕様に詳しい人なら判るかも知れないが、実は いまの枠組みで HID にも対応できる。デスクリプタで HID_REPORT の定義を入れて usb_user_setup で

    if (request_type == USB_RQ_HID_SET_REPORT)

    この判断をすれば良いのだ。ブロックデータを送る場合も 253 バイトまでなら対応できるし、受信は usb_user_out で対応できる。

    いままで対応して来なかったのは、HID_REPORT の定義 自体が面倒だったため。だが、最近はわりと Windows を使っていて、ドライバなしで良いというのが魅力的に思えるようになってきた。今回の移植にあたっては、HID にも対応したいと思う。

CDC (シリアル) や MSC(Mass Storage Class) について

    ここまでなんの説明もしていなかったが、AT90USB162 では、CDC や MSC の対応もしていた.
    いままで説明して来たのは、usb162 (PIC32MX では usb220) モジュールの話で、CDC や MSC のコードは コンフィグ以外 usb162 とは切り離されている。

    これらは、独立した エンドポイント を持っていて、勝手に それらを使う。チップ依存のコードも CDC や MSC 側に入っている。usbPoll() については、

    void usbcdc_poll() {
    モジュール固有の処理
    :
    usbPoll();
    }

    まぁこんな感じで usbPoll() を call している。

    AVR では、それで良かったのだ。エンドポイントの扱いは すごく簡単だったし、コード量的にも性能的にも 有利だった。だが、PIC32MX はすごく面倒。共通関数化を考えた方が良いかも知れない。

PIC32MX でのエンドポイントの扱い方。

    今は、結構コードが組み上がって来ていて、ほんのわずかだが、動き出している。ただ、エンドポイントの扱いが難しく、SETUP の受信がいくつか出来ただけで、デスクリプタの送信がうまく行っていない状況。

    ちょっと AVR でどのように扱えていたか紹介しておこう。

    unsigned char usbcdc_getc(void) {
    unsigned char ret;
    UENUM = EP_TX;
    while (bit_is_clear(UEINTX,RWAL)) {
    usbcdc_poll();
    UENUM = EP_TX;
    }
    ret = UEDATX;
    return ret;
    }

    void usbcdc_putc(unsigned char data) {
    UENUM = EP_RX;
    while (bit_is_clear(UEINTX,RWAL)) {
    usbcdc_poll();
    UENUM = EP_RX;
    }
    UEDATX = data;
    }

    AVR だとこんな風にして アクセスできるのだ。UENUM で エンドポイントを指定してバンクを切り替えて、UEDATX で FIFO のアクセスをする。( RX も TX も同じレジスタ。)。ダブルバッファでも やり方は変わらない。

    PIC32MX では、バッファは全部 RAM に置く。そして、バッファの定義も RAM に置くようになっている。

    struct _usbotg_buffer_desc
    {
    volatile uint8_t ctl;
    volatile uint8_t rfu;
    volatile uint16_t len;
    uint32_t buf;
    };
    struct _usbotg_bdt
    {
    struct _usbotg_buffer_desc rx[2];
    struct _usbotg_buffer_desc tx[2];
    };
    extern struct _usbotg_bdt _usbotg_bdt[16]; /* MAX : 512B */

    struct _usbotg_bdt _usbotg_bdt[16] __attribute__((section(".bdt")));

    memset((void *)_usbotg_bdt, 0, 512); /* need clear first */
    uint32_t p = vtop((unsigned)_usbotg_bdt);
    U_BDTP1 = __ext_bits( p, 8, 8);
    U_BDTP2 = __ext_bits( p, 16, 8);
    U_BDTP3 = __ext_bits( p, 24, 8);

設定はこんな感じ。16 個までの エンドポイントの定義があって、それぞれ 受信用、送信用のバッファが 2 セットづつある。この定義を BDT -- Buffer Discriptor Table と言う。BDT のアドレス(物理アドレス)を USB コントローラに 渡すしくみ。

    PIC32MX は常に 2 個づつ使うのだが、PIC24F では、PPB の指定で、1 個づつにすることもできる。なにもかも同じではないので、注意

面倒なのは、512B アラインされてないといけないということ。しょうがないので、新たな セクションを RAM の先頭に定義して、そこに置くことにした。__attribute__ を使えば、C で定義を書くこともできる。

それぞれのバッファーは、バイトアラインでよい。送信バッファーは、FLASH 上に置くこともできる。... のだが、持っているコードの都合上 AVR と同じように使いたい。全部 RAM に割り当てて、メモリコピーにした。

このバッファは、交互に切り替えて使うらしい。どちらを使っているかを示すレジスタはなく、ずれるとまずい。

そこは、まだ良いのだ。DATA パケットは、送信も受信も DATA0/DATA1 の 2 種類があって、こちらの方も指定してやらないといけない。これは、2つのバッファとは独立。これの指定がよく分かってなくて混乱している。

V-USB のコードを見てみた。( 送信だけ、とりあえず。)

    初期値は DATA1 にして、送信前に切り替え (最初の送信は、DATA0)
    初期値を設定しなおすタイミングは、
     usbInit()
     USBRQ_SET_INTERFACE
     USBRQ_CLEAR_FEATURE, USBRQ_SET_FEATURE
      ( feature 0 == HALT for endpoint == 1 )
    の 3 ケース。後は常にトグル。

今のコード

    void usbcdc_putc(unsigned char data) {
    struct usb_buffer_ctl *bc = &usb_buffer_ctl[EP_RX];
    struct _usbotg_buffer_desc *cur_desc;
    int which;
    uint8_t ret;
    uint8_t *buf;

    while (bc->tx_ptr >= bc->buf_size) {
    usbcdc_poll();
    }
    which = which_buf_tx(bc);
    cur_desc = &_usbotg_bdt[EP_RX].tx[which];
    buf = (uint8_t *)to_kseg1(cur_desc->buf);

    buf[bc->tx_ptr++] = data;
    if (bc->tx_ptr >= bc->buf_size) {
    usbcdc_poll();
    }
    }

    いまは、こんなひどいコードになっている。usbcdc_poll() でバッファが空くまで待つ。bc は、グローバル変数を持っていればこんな計算は不要。buf も アドレス変換済みのものを bc で参照できるようにすれば良い。最後にある 2 回目の usbcdc_poll()は、送信を始めるために必要。

    if ( (bc->tx_ptr >= bc->buf_size)
    || bit_is_set(GPIOR2, USB220_CDC_SEND_REQ)
    || bit_is_set(GPIOR2, USB220_CDC_SEND_EMPTY) ) {
    bc= &usb_buffer_ctl[EP_RX];
    which = which_buf_tx(bc);
    next = which ^ 1;

    cur_desc = &_usbotg_bdt[EP_RX].tx[next];
    if (bit_is_clear(cur_desc->ctl, U_BD_UOWN)) {
    cur_desc = &_usbotg_bdt[EP_RX].tx[which];
    len = cur_desc->len = bc->tx_ptr;
    ctl = cur_desc->ctl & _BV(U_BD_DATA01);
    cur_desc->ctl = ctl | _BV(U_BD_DTS) | _BV(U_BD_UOWN);
    bc->tx_ptr = 0;
    bc->stat ^= _BV(UBC_RX_WHICH);
    bit_clear(GPIOR2, USB220_CDC_SEND_REQ);
    if (!len) bit_clear(GPIOR2, USB220_CDC_SEND_EMPTY);
    }
    }

    こちらは、usbcdc_poll() の中で実際に送信するコード。なんだか長い。ちなみに、USB220_CDC_SEND_REQ は強制的に 送信するための リクエスト。USB220_CDC_SEND_EMPTY は、さらに 0 バイトを送信させるためのもの。GPIOR2 は、AVR では、汎用レジスタでビット操作が効率良くできるため採用。PIC32MX では、ただの グローバル変数。ただ、PIC32MX でも どこか 空いているレジスタがあれば、使ってみたいとは思っている。

参考文献

     ・ PIC32MX220F032B 製品ページのリンク リファレンスマニュアル 『27章 USB On-The-Go』
     ・ PIC24F の『27章 USB On-The-Go』(日本語)

    PIC24F と PIC32MX では、USB OTG の部分はほとんど変わらないので 日本語のリファレンス・マニュアルが便利。

    以下の説明 で U_CON となっているところは、_ を 1 に置き換えて読む。( U1CON ) 。また、レジスタのフィールドは、たとえば U_CON_PPBRST と表現している。 あと U1EPn は、U_EP(n) と記述。

    以下参考文献から転載 (jzlib 用に名称を変更)

    27.4.1 デバイスモードの有効化
    1. PPBRST ビット (U_CON_PPBRST) を一旦セットしてからクリアする事によって、ピンポン
    バッファ ポインタをリセットする。
    2. 全てのUSB 割り込みを無効にする(U_IE = 0 および U_EIE = 0)。
    3. 既存の割り込みフラグがある場合はクリアする (U_IR = 0xFFおよび U_EIR = 0xFF)。
    4. VBUS が存在する事を確認する(OTG 以外のデバイスのみ )。
    5. USBENビット (U_CON_USBEN) をセットしてUSB モジュールを有効にする。
    6. OTGEN ビット (U_OTGCON_OTGEN)を「1」にセットする。
    7. 最初のセットアップ パケットを受信できるように、U_EP_EPRXEN および U_EP_EPHSHK ビット
    (U_EP(0)) をセットし、エンドポイント 0 バッファを有効にする。
    8. USBPWR ビット(U_PWRC_USBPWR)をセットし、USBモジュールに電力を供給する。
    9. DPPULUP ビット(U_OTGCON_DPPULUP = 1)をセットして D+ラインのプルアップ抵抗を有
    効にし、接続を通知する。

    (注) 6,9 の操作は、デバイス専用なら不要。
    (注) U_IR, U_EIRは、1 を書くとクリアされる。

    27.4.2 デバイスモードでの IN トークンの受信
    1. USBホストに接続し、エニュメレーション(USB 2.0仕様書第9章参照)を実行する。
    2. データバッファを作成する。このバッファにホストへ送信するデータを格納する。
    3. 目的のエンドポイントの適切な(EVEN またはODD) TX BDで次の設定を行う。
    a) ステータス レジスタ (BDnSTAT) に正しいデータトグル (DATA0/1) 値とデータバッ
    ファのバイトカウントを設定する。
    b) アドレス レジスタ(BDnADR)にデータバッファの開始アドレスを設定する。
    c) ステータス レジスタのUOWN ビットを「1」にセットする。
    4. USBモジュールは、IN トークンを受信すると自動的にバッファ内のデータを送信する。
    送信が完了すると、USBモジュールはステータス レジスタ (BDnSTAT)を更新し、転送
    完了割り込みビットTRNIF (U_IR U_I_TRN) をセットする。

    27.4.3 デバイスモードでの OUT トークンの受信
    1. USBホストに接続し、エニュメレーション(USB 2.0仕様書第9章参照)を実行する。
    2. ホストからの受信が予想されるデータサイズのデータバッファを作成する。
    3. 所定のエンドポイントの適切な(EVEN またはODD) TX BDで次の設定を行う。
    a) ステータス レジスタ (BDnSTAT) に正しいデータトグル (DATA0/1) 値とデータバッ
    ファのバイトカウントを設定する。
    b) アドレス レジスタ(BDnADR)にデータバッファの開始アドレスを設定する。
    c) ステータス レジスタのUOWN ビットを「1」にセットする。
    4. USB モジュールは、OUT トークンを受信するとホストがバッファに送信したデータを
    自動的に受信する。受信が完了すると、USB モジュールはステータス レジスタ
    (BDnSTAT)を更新し、転送完了割り込みビット TRNIF (U_IR U_I_TRN)をセットする。

    (注) BDnSTAT は、メモリ上(BDT)にある。
posted by すz at 19:47| Comment(0) | TrackBack(0) | PIC32MX
この記事へのコメント
コメントを書く
お名前: [必須入力]

メールアドレス: [必須入力]

ホームページアドレス:

コメント: [必須入力]

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


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

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