SSブログ

USBプロジェクト - ファームウェアに立ち返る (6) [USB]このエントリーを含むはてなブックマーク#

1516092

気の向くままに、プログラムを書きなおしてみました。

割り込みハンドラ

割り込みハンドラでは、状態を優先とした分岐をやめました。 SETUP, OUT, IN のそれぞれのトランザクションにより、doSetup(void), doEp0Out(void), doEp0In(void) の三つの関数で処理をさせます。

  // Check RXD at EP0
  if (UIR1_RXD0F) {             // any data received via EP0?
    if (USR0_SETUP) {           // SETUP packet?
      switch (doSetup()) {      // go to setup
        case ERR_STALL:     forceStall();   break;
      }
    } else {                    // or a normal packet
      switch (doEp0Out()) {     // receive it
        case ERR_STALL:     forceStall();   break;
      }
    }
    UCR0_RX0E = 1;              // Activate EP0 Receiver again
  }                               

  // Check TXD at EP0
  if (UIR1_TXD0F) {             // has EP0 sent Data?
    switch (doEp0In()) {
      case ERR_STALL:       forceStall();   break;
    }
    UCR0_RX0E = 1;              // Activate EP0 Receiver again
  }

双方とも最後にRX0E=1が入っているのは、常時SETUPトランザクションを受け入れるためです。

それぞれの関数は、エラーコードを返してくるので、割り込みハンドラ内でSTALL処理を行います。 他の種類のエラー処理があるかも知れませんが、今はこれだけです。

//----------------------------------------------------------------------------
//  Error code
//----------------------------------------------------------------------------
#define     ERR_NONE        (0)
#define     ERR_STALL       (1)

SETUPトランザクションの処理

doSetup(void)では、リクエスト・コードに従って、処理関数を呼び出します。 現在は、SET_ADDRESSリクエストだけ実装しています。

//----------------------------------------------------------------------------
//  Process for SETUP transaction.
//----------------------------------------------------------------------------
byte doSetup(void) {
  UIR2_RXD0FR = 1;              // Acknowledge
  UCR0_RX0E = 0;                // Turn off EP0 receiver

  copySetupBuffer();            // Copy SETUP parameters

  if(USR0_RP0SIZ != 8) {        // SETUP Transaction must be Size=8
    return ERR_STALL;           // otherwise we have an Error Condition
  }
  switch (setupBuffer.bmRequestType & 0x60) {
    case 0x00:  // Standard Request Decoder:
      switch (setupBuffer.bRequest) {
        case SET_ADDRESS:   // 5
          return setAddressSetup();
        default:
          return ERR_STALL;
      }
      break;
    case 0x20:  // Class Request Decoder:
      switch (setupBuffer.bRequest) {
        default:
          return ERR_STALL;
      }
      break;
    default:    // Unsupported Request
      return ERR_STALL;
  }
}

SET_ADDRESSリクエストのSETUP

SET_ADDRESSリクエストのSETUPトランザクションに対する処理を行います。

//----------------------------------------------------------------------------
//  SET_ADDRESS Standard Device Request Handler
//  called by SETUP handler.
//  according to USB2.0 spec page 256
//----------------------------------------------------------------------------
byte setAddressSetup(void) {
  // Confirm STATE
  switch (usbState) {
    case US_DEFAULT:
    case US_ADDRESSED:
      break;
    default:                        return ERR_STALL;
  }
  
  // SET_ADDRESS transaction validation
  if (setupBuffer.wIndex != 0)      return ERR_STALL;
  if (setupBuffer.wLength != 0)     return ERR_STALL;
  if (setupBuffer.wValue >= 128)    return ERR_STALL;
  
  // configure endpoint #0 to send an empty DATA1
  // at the next IN Transaction
  UCR0  = 0b10100000;
  //        ||||++++-- TP0SIZ = 0 reset packet size register
  //        |||+------ RX0E = 0   disable EP0 receiver
  //        ||+------- TX0E = 1   enable EP0 transmitter
  //        |+-------- Reserved (0)
  //        +--------- T0SEQ = 1  set TX sequence bit
}

最初に状態を確認します。 DEFAULTおよびADDRESSED状態i以外の時の処理は、"not specified"(不定)なので、STALLを返すことにしました。

次にSETUPパラメータの値を確認します。 パラメータに誤りがあったら、STALLを返します。

INトランザクションの処理

INトランザクションは、doEp0In(void)関数で処理されます。 まだ、SET_ADDRESSリクエストのステータス・ステージの処理しか実装されていません。

//----------------------------------------------------------------------------
//  Process for EP0-IN transaction.
//----------------------------------------------------------------------------
byte doEp0In(void) {
  UIR2_TXD0FR = 1;              // Acknowledge
  UCR0_TX0E = 0;                // Turn off EP0 transmitter
  
  switch (setupBuffer.bmRequestType & 0x60) {
    case 0x00:          // Standard Request
      switch (setupBuffer.bRequest) {
        case SET_ADDRESS:   // 5
          return setAddressStatus();
        default:
          return ERR_STALL;
      }
      break;
    case 0x20:          // Class Request
      switch (setupBuffer.bRequest) {
        default:
          return ERR_STALL;
      }
      break;
    default:    // Unsupported Request
      return ERR_STALL;
  }
}

SET_ADDRESSリクエストのSTATUS

ここだけは、ほとんど変わっていません。 ステータス・ステージが終了した事実が、SET_ADDRESSリクエストが正しく受信できたことを意味しているので、この関数ではエラーは発生しないはずです。

//----------------------------------------------------------------------------
//  SET_ADDRESS Standard Device Request Handler
//  called by STATUS handler.
//  according to USB2.0 spec page 256
//----------------------------------------------------------------------------
byte setAddressStatus(void) {
    // A STATUS transaction was received.
    // Set the USB address.
    UADDR = (byte)setupBuffer.wValue | UADDR_USBEN_MASK;
    if (setupBuffer.wValue == 0) {
      // make a transition to DEFAULT
      usbState = US_DEFAULT;
    } else {
      // make a transition to ADDRESSED
      usbState = US_ADDRESSED;
    }
    // Reset the request code.
    setupBuffer.bRequest = REQUEST_COMPLETE;
    
    return ERR_NONE;
}

今日のところは、こんな気分です。 明日は、また、変わるかも。

付録 : USBプロジェクト索引

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USBプロジェクト - ファームウェアに立ち返る (5) [USB]このエントリーを含むはてなブックマーク#

1516092

正しいHIDデバイスを追い求めるシリーズ、五の段です。

SET_ADDRESSリクエストの処理

バス・リセットの次は、SET_ADDRESSリクエストが来るはずです。 このリクエストは、エンド・ポイント0にSETUPトランザクションとして届きます。 USBデバイスの状態に応じて、USB割り込みハンドラから以下の関数を呼び出します。

//----------------------------------------------------------------------------
//  Process for Default state.
//----------------------------------------------------------------------------
void doDefaultStateProc(void) {
  if(UIR1_RXD0F) {              // any data received via EP0?
    UIR2_RXD0FR = 1;            // Acknowledge
    UCR0_RX0E = 0;              // Turn off EP0 receiver
    if(USR0_SETUP) {            // SETUP packet?

      copySetupBuffer();        // Copy SETUP parameters

      if(USR0_RP0SIZ != 8) {    // SETUP Transaction must be Size=8
        forceStall();           // otherwise we have an Error Condition
      } else {               
        if((setupBuffer.bmRequestType & 0x60) == 0) {
          // Standard Request Decoder:
          switch(setupBuffer.bRequest) {
            case SET_ADDRESS:   // 5
              setAddressSetup();
              break;
            default:
              forceStall();
              break;
          }
        } else {
          forceStall();         // Unexpected request
        }
      }
    } else {                    // or a normal packet
      forceStall();             // Unexpected transaction
    }
    UCR0_RX0E = 1;              // Activate EP0 Receiver again
  }
  if(UIR1_TXD0F) {              // has EP0 sent Data?
    UIR2_TXD0FR = 1;            // Acknowledge
    UCR0_TX0E = 0;              // Turn off EP0 transmitter

    setAddressStatus();         // Expected SET_ADDRESS
  }
}

受信したSET_ADDRESSに対する処理は、setAddressSetup(void)関数で行われます。 続くステータス・ステージに備える処理もsetAddressSetup(void)関数で記述されています。

DEFAULT状態では、SET_ADDRESSリクエストしか期待されていません。 このため、他のリクエストおよび、SETUPトランザクション以外は、不正なトランザクションとみなされます。 その場合、STALLによる応答をすべく、forceStall(void)が呼び出されます。

//----------------------------------------------------------------------------
//  Force STALL Condition for EP0 (both IN and OUT)
//  as a Response to an invalid SETUP Request
//  Flags will be auto-cleared by the next SETUP Token
//----------------------------------------------------------------------------
void forceStall(void) {
  UCR3_OSTALL0 = 1;
  UCR3_ISTALL0 = 1;
}

forceStall(void)関数は、ステータス・ステージのハンドシェイク・パケットとしてSTALL送るための設定を行います。 このマイコンには、RX0E=0としておくと、ハンドシェイク・パケットとして、NAKを返すという機能もあります。 サンプル・プログラムを見る限りでは、どうやらSTALL設定の方が優先されているようです。

EP0は、いつでもSETUPトランザクションを受け付けられるように、RX0E=1として受信機を有効にしています。 本当に、SETUPトランザクションが続けて発行されるような事態がありうるのだろうか?

SETUPトランザクションが続けてくる事態は、正しく実装されたホストでは、起こりえないそうです。 ただし、ホスト側にバグがあった場合に備える必要はありそうです。 もし、予期せずにSETUPトランザクションが届いたら、処理中のSETUPトランザクションを破棄するのが正しい実装だそうです。

DEFAULT状態では、SETUPステージの後のステータス・トランザクションの処理も行います。 ステータス・トランザクションが終了したら、USBアドレスを更新して、状態を遷移させます。 実際の処理は、setAddressStatus(void)で記述されます。

割り込みハンドラのディスパッチャ

一方、割り込みハンドラのディスパッチャは、このようになります。

  switch (usbState) {
    case US_POWERED:
    case US_DEFAULT:
      doDefaultStateProc();
      break;
    case US_ADDRESSED:
    case US_CONFIGURED:
    case US_SUSPENDED:
    default:
      ;
  }

まだ、DEFAULT状態以外の処理は、未実装です。

doDefaultStateProc(void)関数は、SET_ADDRESSリクエストにのみ反応しますが、GET_DESCRIPTORリクエストは、DEFAULT状態でも有効なリクエストなので、修正が必要です。 他のリクエストについても調べなくては。

このプログラムでは、状態による分岐を優先する書き方をしてみました。 どうしても状態コードとリクエスト・コードのマトリックス条件で処理を分けなくてはならないので、分岐が複雑になってしまいます。 もうちょっと、スマートにならないかな。

USB2.0の仕様書を読んでみると、リクエストごとに各状態で行うべき動作が記述されておりました。 どうも、仕様書と見比べてプログラムを作る分には、これに従ったほうが良さそうです。 次回、方針変更をする予定です。

SET_ADDRESS処理関数

SETUPトランザクションの終了時に実行される処理は、setAddressSetup(void)に記述されます。

//----------------------------------------------------------------------------
//  SET_ADDRESS Standard Device Request Handler
//  called by SETUP handler.
//  according to USB2.0 spec page 256
//----------------------------------------------------------------------------
void setAddressSetup() {
  // SET_ADDRESS transaction validation
  if (
    (setupBuffer.wIndex == 0) &&
    (setupBuffer.wLength == 0) &&
    (setupBuffer.wValue < 128)
  ) {
    // configure endpoint #0 to send an empty DATA1
    // at the next IN Transaction
    UCR0  = 0b10100000;
    //        ||||++++-- TP0SIZ = 0 reset packet size register
    //        |||+------ RX0E = 0   disable EP0 receiver
    //        ||+------- TX0E = 1   enable EP0 transmitter
    //        |+-------- Reserved (0)
    //        +--------- T0SEQ = 1  set TX sequence bit
  } else {
    // response for illegal SETUP packet.
    forceStall();
  }
}

まず、SETUPパケットで送信されたパラメータが正しい値であるかどうかを比較します。 SET_ADDRESSは、wValueに7ビットのUSBアドレスが入り、wIndexとwLengthには、0が入ります。 もし、入っている値が範囲外であれば、forceStall(void)で処理が正常に終了できなかったことを示します。

値が正しいことが確認されたら、次に発生するステータス・トランザクションに備えて、EP0送信機を起動します。

ステータス・トランザクションの終わりが、コントロール転送の無事終了を示しています。 SET_ADDRESSリクエストは、ここで初めて終了するので、ホストから指定されたUSBアドレスを設定することが出来ます。

//----------------------------------------------------------------------------
//  SET_ADDRESS Standard Device Request Handler
//  called by STATUS handler.
//  according to USB2.0 spec page 256
//----------------------------------------------------------------------------
void setAddressStatus(void) {
    // A STATUS transaction was received.
    // Set the USB address.
    UADDR = (byte)setupBuffer.wValue | UADDR_USBEN_MASK;
    if (setupBuffer.wValue == 0) {
      // make a transition to DEFAULT
      usbState = US_DEFAULT;
    } else {
      // make a transition to ADDRESSED
      usbState = US_ADDRESSED;
    }
    // Reset the request code.
    setupBuffer.bRequest = REQUEST_COMPLETE;
    // Activate EP0 Receiver for next request.
    UCR0_RX0E = 1;
}

アドレスとして、"0"が指定されていたら、DEFAULT状態に戻ります。 また、"0"以外の値が指定されていたら、ADDRESSED状態に進みます。 そして、次に発行されるSET_CONFIGURATIONリクエストを受信すべく、EO0受信機をイネーブルにします。

アドレスの変更がステータス・ステージまで待たされる理由は、USB2.0仕様書の256ページに書いてあります。 もし、SETUPステージであわててアドレスを変更したら、続くステータス・トランザクションは、アドレスが異なるために通信することができません。 かならず、トランスファが終了してからアドレスを変更しなくてはなりません。

setAddressStatus(void)が呼び出された後は、 次のセットアップ・トランザクションを待つためにRX0E=1としなくてはならないはずです。 ところが、サンプル・プログラムでもsetAddress(void)でEP0受信機がディセーブルされて以来、イネーブルされた形跡がありません。 何で、動いているように見えるんだろう?

私の見落としでした。 doDefaultStateProc(void)関数の最後でRX0E=1としておりました。

SETUPバッファ構造体に関する方針変更

どうもプログラムを書きにくいので、方針変更し、SetupBuffer構造体を変更しました。

//----------------------------------------------------------------------------
//  Structure of Setup Packet sent during
//  SETUP Stage of Standard Device Requests
//  according to USB2.0 spec page 248
//----------------------------------------------------------------------------
typedef struct {
    byte        bmRequestType;      // Characteristics (Direction,Type,Recipient)
    byte        bRequest;           // Standard Request Code
    word        wValue;             // Value Field
    word        wIndex;             // Index or Offset Field
    word        wLength;            // Number of Bytes to transfer (Data Stage)
}   SetupBuffer;

従来、使用されていたiword構造体を廃止して、16-bit符号無し整数wordを使っています。 エンディアンの変更は、エンド・ポイントをバッファにコピーする時に行っています。

//----------------------------------------------------------------------------
//  Copy SETUP packet into buffer.
//  Intel words are converted into MOTOROLA words.
//----------------------------------------------------------------------------
void copySetupBuffer(void) {
  setupBuffer.bmRequestType     = UE0D0;
  setupBuffer.bRequest          = UE0D1;
  setupBuffer.wValue            = (UE0D3 << 8) | UE0D2;
  setupBuffer.wIndex            = (UE0D5 << 8) | UE0D4;
  setupBuffer.wLength           = (UE0D7 << 8) | UE0D6;
}

HC08のような純粋な8-bitマイコンに16-bit整数を扱わせるのは酷なんだけど、コンパイラにがんばってもらいましょう。

2008-03-24 23:14 コメントでの指摘事項および情報を追加。

付録 : USBプロジェクト索引

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USBプロジェクト - ファームウェアに立ち返る (4) [USB]このエントリーを含むはてなブックマーク#

1516092

正しいHIDデバイスを追い求めるシリーズ、四の段です。

電源投入後の状態設定

USBデバイスは、到着するトランザクションに従って、状態を刻々と変化させて処理を行います。 そのため、プログラムは、ソフトウェアで実現したステートマシンとして動作します。 ステートマシンは、電源投入後にある特定の状態に入ります。 この状態を設定するのが、initUSB(void)という関数です。

//----------------------------------------------------------------------------
//  Make a transition to powered but not reset yet.
//----------------------------------------------------------------------------
void initUsb() {
  // set default address
  UADDR         = 0;

  // disable Interrupts
  UIR0  = 0b00000000;
  //        |||||||+-- RXD0IE = 0   disable RXD0 interrupt
  //        ||||||+--- TXD0IE = 0   disable TXD0 interrupt
  //        |||||+---- Reserved (0)
  //        ||||+----- TXD1IE = 0   disable TXD1 interrupt
  //        |||+------ RXD2IE = 0   disable RXD2 interrupt
  //        ||+------- TXD2IE = 0   disable TXD2 interrupt
  //        |+-------- SUSPEND = 0  disable Suspend function
  //        +--------- EOPIE = 0    disable EOP interrupt

  // clear all Flags in UIR1
  UIR2  = 0b11111111;
  //        |||||||+-- RXD0FR = 0   clear RXD0 flag
  //        ||||||+--- TXD0FR = 0   clear TXD0 flag
  //        |||||+---- RESUMEFR = 1 clear RESUME flag
  //        ||||+----- TXD1FR = 1   clear TXD1 flag
  //        |||+------ RXD2FR = 1   clear RXD2 flag
  //        ||+------- TXD2FR = 1   clear TXD2 flag
  //        |+-------- RSTFR = 1    clear bus-reset flag
  //        +--------- EOPFR = 1    clear EOP flag

  // initialize endpoint #0
  UCR0  = 0b00000000;
  //        ||||++++-- TP0SIZ = 0 reset packet size register
  //        |||+------ RX0E = 0   disable EP0 receiver
  //        ||+------- TX0E = 0   disable EP0 transmitter
  //        |+-------- Reserved (0)
  //        +--------- T0SEQ = 0  reset TX sequence bit
  
  // initialize endpoint #1
  UCR1  = 0b00000000;
  //        ||||++++-- TP1SIZ = 0 reset packet size register
  //        |||+------ FRESUM = 0 disable Force RESUME
  //        ||+------- TX1E = 0   disable EP1 transmitter
  //        |+-------- STALL1 = 0 disable Force EP1 STALL
  //        +--------- T1SEQ = 0  reset TX sequence bit

  // initialize endpoint #2
  UCR2  = 0b00000000;
  //        ||||++++-- TP2SIZ = 0 reset packet size register
  //        |||+------ RX2E = 0   disable EP2 receiver
  //        ||+------- TX2E = 0   disable EP2 transmitter
  //        |+-------- STALL2 = 0 disable Force EP2 STALL
  //        +--------- T2SEQ = 0  reset TX sequence bit

  // initialize others
  UCR3  = 0b01000100;
  //        |||||||+-- ENABLE1 = 0  disable EP1
  //        ||||||+--- ENABLE2 = 0  disable EP2
  //        |||||+---- PULLEN = 1   enable pull-up at D-
  //        ||||+----- Reserved (0)
  //        |||+------ ISTALL0 = 0  disable EP0 Force STALL for IN
  //        ||+------- OSTALL0 = 0  disable EP0 Force STALL for OUT
  //        |+-------- TX1STR = 1   clear TX1ST flag
  //        +--------- Reserved (0)

  // initialize force bits.
  UCR4  = 0b00000000;
  //        |||||||+-- FDM = 0    disable Force D- port
  //        ||||||+--- FDP = 0    disable Force D+ port
  //        |||||+---- FUSBO = 0  disable Force USB output
  //        +++++----- Reserved (00000)

  // make a transition	
  usbState      = US_POWERED;   // powered, but not yet reset

  UADDR_USBEN   = 1;            // enable USB interface
}

初期設定関数は、これまでのサンプル・プログラム例では、main(void)関数から呼び出されていました。 USB関連のレジスタを初期設定したら、ステートマシンの状態をUS_POWEREDに設定します。

次に発生するイベントは、「bus-resetイベントによる割り込み」です。 bus-resetイベントが発生すると、CONFIGレジスタのURSTDビット(USB Reset Disable)の状態によって異なる動作を行います。 URSTDがリセットされている場合、bus-resetイベントによりマイコンはリセットされます。 また、URSTDがセットされている場合、bus-resetイベントによるマイコンのリセットが禁止されているので、代わりにUSB割り込みが発生します。 bus-resetによるリセットおよび割り込みの両方を禁止する方法は無いようです。

USB割り込みハンドラを整備する

MC908JB16マイコンは、USBバス上のイベントやトランザクションの終了ごとに割り込みが発生します。 このため、プログラムは、割り込み発生により状態遷移を繰り返すステート・マシンとして実装されることになります。

//----------------------------------------------------------------------------
// USB Interrupt Handler
// All Interrupt Sources of the JB16's integrated USB peripheral
// will be treated by this ISR
//----------------------------------------------------------------------------
void interrupt VectorNumber_USB isrUSB() {
    :
}

このハンドラの中をうめていきます。 また、長くなりそうだ。

バス・リセットの検出

マイコンがbus-resetイベントを検出したら、割り込みが発生します。 割り込みハンドラの中には、このイベントを検出するディスパッチャを入れておきます。

  if (UIR1_RSTF) {
    // USB Reset Signal State detected
    resetUsb();                 // call bus-reset handler
  }

このバス・リセットというイベントは、発生する頻度が低いので、サンプル・プログラムでは、 一番後ろにディスパッチャが配置されています。

bus-resetイベントに対する処理は、以下の通りです。

//----------------------------------------------------------------------------
//  USB bus-reset handler
//----------------------------------------------------------------------------
void resetUsb(void) {
  initUsb();                    // Soft Reset of USB Systems
  
  // INT enabled for: end of packet
  UIR0  = 0b00000011;
  //        |||||||+-- RXD0IE = 1   enable RXD0 interrupt
  //        ||||||+--- TXD0IE = 1   enable TXD0 interrupt
  //        |||||+---- Reserved (0)
  //        ||||+----- TXD1IE = 0   disable TXD1 interrupt
  //        |||+------ RXD2IE = 0   disable RXD2 interrupt
  //        ||+------- TXD2IE = 0   disable TXD2 interrupt
  //        |+-------- SUSPEND = 0  disable Suspend function
  //        +--------- EOPIE = 0    disable EOP interrupt

  
  // enable EP0 receiver only.
  UCR0_RX0E     = 1;            // EP0 Receive Enable

  // make a transition
  usbState      = US_DEFAULT;   // Device is powered and reset
}

resetUsb()からは、initUsb()を呼び出して確実に初期化を行います。 そして、次に発生するイベントに対応する設定を行います。 次は、SET_ADDRESSリクエストが来るはずなので、EP0受信機をイネーブルし、さらにUS_DEFAULT状態に遷移します。

サンプル・プログラムでは、この段階ですべての割り込みを有効にしていました。 もちろん、各エンド・ポイントを有効にしなければ、割り込みが発生することはありません。 ひとつだけ不明なのは、End Of Packet (EOP)の扱いです。 ここで、有効にすべきか否か。 そもそも、サンプル・プログラムでは、まともに処理がされていなかったイベントです。

ひとまず、ここまで。

付録 : USBプロジェクト索引

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USBプロジェクト - ファームウェアに立ち返る (3) [USB]このエントリーを含むはてなブックマーク#

1516092

正しいUSBデバイスの続きです。 デスクリプタを整備していきます。

デバイス・デスクリプタを整備する

USBデバイスが、自身の素性をホストに知らせるのが、デスクリプタと呼ばれるデータです。 まずは、デバイス・デスクリプタから。

//----------------------------------------------------------------------------
//  Standard Device Descriptor
//  according to USB2.0 spec page 262
//----------------------------------------------------------------------------
typedef struct {
    byte        bLength;            // Size of this Descriptor in Bytes
    byte        bDescriptorType;    // Descriptor Type (=1)
    iword       bcdUSB;             // USB Spec Release Number in BCD
    byte        bDeviceClass;       // Device Class Code
    byte        bDeviceSubClass;    // Device Subclass Code
    byte        bDeviceProtocol;    // Device Protocol Code
    byte        bMaxPacketSize0;    // Maximum Packet Size for EP0
    iword       idVendor;           // Vendor ID
    iword       idProduct;          // Product ID
    iword       bcdDevice;          // Device Release Number in BCD
    byte        iManufacturer;      // Index of String Desc for Manufacturer
    byte        iProduct;           // Index of String Desc for Product
    byte        iSerialNumber;      // Index of String Desc for SerNo
    byte        bNumConfigurations; // Number of possible Configurations
}   DeviceDescriptor;

デバイス・デスクリプタは、固定長のデータです。 そのため、structで宣言する構造体で表現します。 構造体の内部構成は、USB2.0の仕様書の262ページに書いてあります。 仕様書は、 USB.orgここに あります。

でも、待てよ。 ファームウェアからデスクリプタに記述した内容を参照することなどは、まずありえない。 もし、必要なら、別の場所で定義した定数を使えば良いのだから。 従って、構造体など使わずに、ベタに8ビット符号無し整数列で書いても十分なんじゃないかな。

構造体の宣言で使用されているiwordは、"Intel Word"の意味です。 USBでは、LSBが先に配置されます。 そのため、モトローラ由来のアセンブラ・コンパイラとは相性が悪いため、iwordという構造体を定義して使っているというわけです。

//----------------------------------------------------------------------------
//  Intel type WORD description.
//----------------------------------------------------------------------------
typedef struct {                    // Data Type "Intel Word"
	  byte        lo;                 // (High/Low Byte swapped)
	  byte        hi;
}   iword;

デバイス・デスクリプタのデータは、ホストの要求に従って、送出するだけで、加工などは行いません。 そのため、ROMに配置します。

//----------------------------------------------------------------------------
//  Device Descriptor Declaration
//----------------------------------------------------------------------------
const DeviceDescriptor deviceDesc = {
                    // Size of this Descriptor in Bytes
  sizeof(DeviceDescriptor),
  DT_DEVICE,        // Descriptor Type (=1)
  {0x00, 0x02},     // USB Spec Release Number in BCD = 2.00
  0,                // Device Class Code (none)
  0,                // Device Subclass Code	(none)
  0,                // Device Protocol Code (none)
  8,                // Maximum Packet Size for EP0 
  {0x00, 0x01},     // Vendor ID = 0x0100
  {0x00, 0x00},     // Product ID = 0
  {0x00, 0x00},     // Device Release Number in BCD
  1,                // Index of String Desc for Manufacturer
  2,                // Index of String Desc for Product
  0,                // Index of String Desc for SerNo
  1                 // Number of possible Configurations
};                  // end of DeviceDesc

ベンダIDとプロダクトIDは、仮の値が入れてあります。 ベンダIDを入手しない限り、このデバイスは正式なデバイスには成りえないのね。

ストリング・デスクリプタを整備する

デバイス・デスクリプタの中にストリング・デスクリプタのインデックスを入れるフィールドがあります。 ストリング・デスクリプタは、文字列を保持するためのデスクリプタで、ホストから指示されたインデックスにより特定の文字列をホストに返します。 ここでは、言語コードを持たせるインデックス0のデータの他に 二種類の文字列を用意しました。

//----------------------------------------------------------------------------
// String Descriptors
//   0: Language code
//   1: Manufacturer
//   2: Product
//----------------------------------------------------------------------------

// Language IDs
const byte strDescLanguage[4] = {
  sizeof(strDescLanguage), DT_STRING,	      // Size, Type
  0x09, 0x04	                              // LangID Codes
};

// Manufacturer String
const byte strDescManufacturer [24] = {
  sizeof(strDescManufacturer), DT_STRING,   // Size, Type
  'n',0,'o',0,'r',0,'i',0,'t',0,
  'a',0,'n',0,'.',0,'o',0,'r',0,
  'g',0
};

// Product String
const byte strDescProduct [24] = {
  sizeof(strDescProduct), DT_STRING,        // Size, Type
  'L',0,'C',0,'D',0,' ',0,'D',0,
  'i',0,'s',0,'p',0,'l',0,'a',0,
  'y',0,
};

// Table of String Descriptors
const byte * const strDescTable[] = {
  strDescLanguage,
  strDescManufacturer,
  strDescProduct
};

ストリング・デスクリプタで厄介なのは、ユニ・コードが使用されていることです。 このため、ASCII文字列も16ビットのデータとして記述しなければなりません。 逆に言うと、日本語フォントも使えるはずですが、今は見送りとしています。

コンフィグレーション・デスクリプタを整備する

コンフィグレーション・デスクリプタは、複数のデスクリプタの集合です。 それぞれのデスクリプタは、structで宣言して構造体としています。

//----------------------------------------------------------------------------
//  Standard Configuration Descriptor
//  according to USB2.0 spec page 265
//----------------------------------------------------------------------------
typedef struct {
    byte        bLength;            // Size of this Descriptor in Bytes
    byte        bDescriptorType;    // Descriptor Type (=2)
    iword       wTotalLength;       // Total Length of Data for this Conf
    byte        bNumInterfaces;     // No of Interfaces supported by this Conf
    byte        bConfigurationValue;  // Designator Value for *this* Configuration
    byte        iConfiguration;     // Index of String Desc for this Conf
    byte        bmAttributes;       // Configuration Characteristics (see below)
    byte        bMaxPower;          // Max. Power Consumption in this Conf (*2mA)
}   ConfigurationDescriptor;

コンフィグレーション・デスクリプタの最初のデスクリプタは、コンフィグレーション・タイプです。

//----------------------------------------------------------------------------
//  Standard Interface Descriptor
//  according to USB2.0 spec page 268
//----------------------------------------------------------------------------
typedef struct {
    byte        bLength;            // Size of this Descriptor in Bytes
    byte        bDescriptorType;    // Descriptor Type (=4)
    byte        bInterfaceNumber;   // Number of *this* Interface (0..)
    byte        bAlternateSetting;  // Alternative for this Interface (if any)
    byte        bNumEndpoints;      // No of EPs used by this IF (excl. EP0)
    byte        bInterfaceClass;    // Interface Class Code
    byte        bInterfaceSubClass; // Interface Subclass Code
    byte        bInterfaceProtocol; // Interface Protocol Code
    byte        iInterface;         // Index of String Desc for this Interface
}   InterfaceDescriptor;

続いて、インターフェース・デスクリプタ構造体の宣言です。

//----------------------------------------------------------------------------
//  Standard HID Descriptor
//  according to HID1.11 spec page 22
//----------------------------------------------------------------------------
typedef struct {
    byte        bLength;            // Size of this Descriptor in Bytes
    byte        bDescriptorType;    // Descriptor Type (=5)
    iword       bcdHID;             // HID Class specification release number
    byte        bCountryCode;       // Hardware target country
    byte        bNumDesriptors;     // Number of HID class descriptors to follow
    byte        bRepDescriptorType; // Report descriptor type
    iword       wDescriptorLength;  // Total length of Report descriptor
}   HidDescriptor;

HIDデスクリプタは、HIDデバイスに特有のデスクリプタです。

//----------------------------------------------------------------------------
//  Standard Endpoint Descriptor
//  according to USB2.0 spec page 269
//----------------------------------------------------------------------------
typedef struct {
    byte        bLength;            // Size of this Descriptor in Bytes
    byte        bDescriptorType;    // Descriptor Type (=5)
    byte        bEndpointAddress;   // Endpoint Address (Number + Direction)
    byte        bmAttributes;       // Endpoint Attributes (Transfer Type)
    iword       wMaxPacketSize;     // Max. Endpoint Packet Size
    byte        bInterval;          // Polling Interval (Interrupt) in ms
}   EndpointDescriptor;

さらに、エンドポイント・デスクリプタが続きます。

これら、デスクリプタの配置順番は、HID仕様書の決められています。

//----------------------------------------------------------------------------
//  Configuration descriptors
//  according to HID1.11 spec page 48
//----------------------------------------------------------------------------
const ConfigurationDescriptors configurationDescriptors = {

//----------------------------------------------------------------------------
//  Configuration Descriptor
//----------------------------------------------------------------------------
  {	            // Size of this Descriptor in Bytes
    sizeof(ConfigurationDescriptor),
    DT_CONFIGURATION,   // Descriptor Type (=2)
    {
      sizeof(ConfigurationDescriptor) +
      sizeof(InterfaceDescriptor) +
      sizeof(HidDescriptor) +
      sizeof(EndpointDescriptor) * 2,
      0x00},    // Total Length of Data for this Conf
    1,          // No of Interfaces supported by this Conf
    1,          // Designator Value for *this* Configuration
    0,          // Index of String Desc for this Conf
    0xc0,       // Self-powered, no Remote-Wakeup
    1           // Max. Power Consumption in this Conf (*2mA)
  },            // end of ConfigDesc

//----------------------------------------------------------------------------
//  Interface Descriptor
//----------------------------------------------------------------------------
  {             // Size of this Descriptor in Bytes
    sizeof(InterfaceDescriptor),
    DT_INTERFACE,   // Descriptor Type (=4)
    0,          // Number of *this* Interface (0..)
    0,          // Alternative for this Interface (if any)
    2,          // No of EPs used by this IF (excl. EP0)
    0x3,        // IF Class Code (0x03 - HID)
    0,          // No Interface Subclass Code
    0,				  // IF Protocol Code  (0 = None)
    0           // Index of String Desc for this Interface
  },            // end of InterfaceDesc

//----------------------------------------------------------------------------
//  HID Descriptor
//----------------------------------------------------------------------------
  {	            // Size of this Descriptor in Bytes
    sizeof(HidDescriptor),
    DT_HID,    // Descriptor Type (=5)
    {0x01,0x01},    // HID class spec release 1.01
    0x00,      // Country code
    0x01,      // Number of descriptors
    DT_REPORT, // Descriptor type...?
               // Total size of report descriptor
    {sizeof(reportDesc),0}
  },           // end of __hidDesc
  
//----------------------------------------------------------------------------
//  Endpoint #1 Descriptor
//----------------------------------------------------------------------------
  {             // Size of this Descriptor in Bytes
    sizeof(EndpointDescriptor),
    DT_ENDPOINT,    // Descriptor Type (=5)
    0x81,       // Endpoint Address (EP1, IN)
    0x03,       // Interrupt transfer
    {0x08, 0x00},   // Max. Endpoint Packet Size
    10          // Polling Interval (Interrupt) in ms
  },            // end of __endpoint1Desc
  
//----------------------------------------------------------------------------
//  Endpoint #2 Descriptor
//----------------------------------------------------------------------------
  {             // Size of this Descriptor in Bytes
    sizeof(EndpointDescriptor),
    DT_ENDPOINT,    // Descriptor Type (=5)
    0x02,       // Endpoint Address (EP2, OUT)
    0x03,       // Interrupt transfer
    {0x08, 0x00},   // Max. Endpoint Packet Size
    10          // Polling Interval (Interrupt) in ms
  }             // end of endpoint2Desc

};

この構造体の宣言とデスクリプタ値の配置も、HIDのレポートデスクリプタのように、機械にやらせたいな。

付録 : USBプロジェクト索引

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USBプロジェクト - ファームウェアに立ち返る (2) [USB]このエントリーを含むはてなブックマーク#

1516092

ターゲットを液晶モジュール制御に定めて、「正しいHIDデバイス」を作成します。 このデバイスは、インタラプトOUT専用になる予定です。

レポート・デスクリプタを作成する

最初にHIDの通信データ構造であるレポート・デスクリプタを考えます。 このBLOGのコメントでも指摘があったように、「勝手プロトコル」を使うつもりであれば、 レポート・デスクリプタの中身を細かく既定する必要はありません。 そこで、MC908JB16のエンド・ポイントを最大限に使えるように、送受信ともに8バイトのデータを送受信することにしました。 データの意味については、後で考えます。

USAGE_PAGE (Generic Desktop)    06 01 00
USAGE (Vendor Usage 1)          09 01
COLLECTION (Application)        A1 01 
  LOGICAL_MINIMUM (0)           15 00 
  LOGICAL_MAXIMUM (255)         26 FF 00 
  REPORT_SIZE (8)               75 08 
  REPORT_COUNT (8)              95 08 
  USAGE (Vendor Usage 1)        09 01
  INPUT (Data,Var,Abs)          81 02 
  USAGE (Vendor Usage 1)        09 01
  OUTPUT (Data,Var,Abs)         91 02 
END_COLLECTION                  C0

某掲示板にあったものをサイズだけ変更しています。 これのC言語書式をレポート・デスクリプタ構造体としてインスタンスします。

//----------------------------------------------------------------------------
// Report Descriptor
//   Generic 8-byte IN/OUT descriptor
//----------------------------------------------------------------------------
const byte reportDesc[25] = {
    0x06, 0x00, 0xff,              // USAGE_PAGE (Generic Desktop)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, 0x08,                    //   REPORT_COUNT (8)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)
    0xc0                           // END_COLLECTION
};

ここまでは、機械がやってくれるのでラクチンです。

付録 : USBプロジェクト索引

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USBプロジェクト - USB制御液晶モジュール [USB]このエントリーを含むはてなブックマーク#

1516092

インタラプトOUTパイプの使い方を教えてもらっているので、 せっかくだからMC908JB16で液晶モジュールを制御させます。 液晶モジュールには、サンライク社のSC1602Bを使用します。

端子の割り当て

SC1602Bには、4-bitバスモードと8-bitバスモードがあります。 MC908JB16には、多くの端子があるので、贅沢に8-bitモードを使用してみます。

マイコンと液晶モジュールの接続
SC1602B側 USBDEV基板側
#端子名#端子名
1VDDCN4-14VDD
2VSSCN2-14VSS
3VO--
4RSCN2-5PTE0
5R/WCN2-6PTE2
6ECN2-11PTC1
7DB0CN2-1PTA0
8DB1CN2-2PTA1
9DB2CN2-3PTA2
10DB3CN2-4PTA3
11DB4CN2-7PTA4
12DB5CN2-8PTA5
13DB6CN2-9PTA6
14DB7CN2-10PTA7

VOは、チャージポンプを作って供給したいところですが、ひとまず半固定抵抗で間に合わせます。

基板上には、LEDが4個搭載されます。

マイコンとLEDの接続
#端子名LED
CN4-8PTD2
CN4-9PTD3
CN4-10PTD4
-PTD5青(USBDEV上)

ファームウェアとPC側制御プログラムは、まだ、これからです。 プロトコルは、どうしようかな。


USBプロジェクト - ファームウェアに立ち返る (1) [USB]このエントリーを含むはてなブックマーク#

1475712

これまで、「HIDプロトコルを実装したサンプル・プロジェクト」を見よう見まねで使ってきました。 BLOGのコメントにかなり理解の助けになる情報を書いてもらいましたので、 ここで、このサンプル・プロジェクトがUSBデバイス、または、HIDデバイスとしてふさわしいかどうか検証しながら整備していこうと思います。 誤解などありましたら、どしどしコメントをください。 まずは、メモから。

USBの基本:パケット

USBケーブルを流れるすべてのデータは、パケットと呼ばれる単位で表現されます。

MC908JB16では、パケットの処理部分は、ハードウェアで行われ、ファームウェアで関与することはまずありません。

USBの基本:トランザクション (Transaction)

パケットには、 TOKEN, DATA, HANDSHAKE の三つの種類があります。 トランザクションは、これら三つのパケットからなるデータ(payload)送受のための最小単位です。 トランザクションは、 TOKEN パケットの種類によって SETUP, DATA, STATUS の三つの種類があります。

トランザクションは、ホストしか開始することが出来ません。 そのため、USBデバイスは、ホストから到着するトランザクションを延々と待つのが仕事になります。

MC908JB16の場合、トランザクションの終了により割り込みが発生します。 ファームウェアは、到着したトランザクションの種類により、適切な処理を行います。

割り込みが発生するのは、トランザクションの終了時です。 このため、ホストからデータを受け取るときには、受け取ったデータに従った処理を行う事になります。 ところが、ホストにデータを送る場合には、トランザクションの開始前に送信すべきデータをあらかじめバッファに入れておく必要があります。 そのためには、ホストから送られてくるトランザクションを解析して、次に要求されるであろうデータを予測し、しかるべきバッファに配置しておく必要があります。

USBの基本:ステージ (Stage)

トランザクションには、 SETUP, DATA, STATUS の三つの種類があります。 この種類の事をステージと呼んでいます。

USBの基本:トランスファ (Transfer)

トランスファは、任意の長さのデータを送受信するための通信単位です。 トランスファには、三つの種類があり、コントロール、インタラプト、バルクと呼ばれています。

コントロール・トランスファでは、 SETUP, DATA, STATUS のすべてのステージが使用されます。

SETUPステージでは、これから開始されるデータ転送に関する情報がホストから送られてきます。 MC908JB16の場合、 SETUP トランザクションが到着したことを示すフラグSETUPがあり、 SETUP トランザクションに特有の「これから開始される通信の予測」処理が始まります。

  if(UIR1_RXD0F) {      // any data received via EP0?
    if(USR0_SETUP) {    // SETUP packet?
      do_setup();       // go to setup
    } else {            // or a normal packet
      ep0_rxd();        // receive it
    }
  }                               

サンプル・プロジェクトの場合にも到着したトランザクションの種類によって処理が分かれているのがわかります。

SETUP ステージで次のデータの送受信方向が予測できるので、来るべき DATA ステージに備える事ができます。 DATA ステージでホストにデータを送信する必要がある場合には、 SETUP ステージの処理の中でバッファにデータを準備する必要があります。

最後の STATUS ステージでは、送受信されるデータがありません。 そのため、ハードウェアが適切(適当?)に処理を行います。

残る二つのトランスファ、インタラプトとバルクは、 DATA ステージだけで構成されます。 ホストが開始するトランザクションに対して単一方向にデータを送受信します。 サンプル・プロジェクトでは、エンド・ポイント1がインタラプト・トランスファとして実装されています。

  if(UIR1_TXD1F) {      // has EP1 sent Data?
    ep1_txd();
  }

処理の内容は、要求に応じてデータをバッファに入れているだけです。

SETUPトランザクションでの処理

SETUP トランザクションでは、8バイトのデータが送信されてきます。 サンプル・プロジェクトでは、setup_bufferという構造体で表現されます。 SETUP トランザクションでの処理は、これから始まる一連のコントロール・トランスファがどんな種類のトランスファであるかを判断することです。 そのためのパラメータが、bmRequestTypebRequestの二つのフィールドに入っています。

typedef struct {
    byte        bmRequestType;  // Characteristics (Direction,Type,Recipient)
    byte        bRequest;       // Standard Request Code
    iword       wValue;         // Value Field
    iword       wIndex;         // Index or Offset Field
    iword       wLength;        // Number of Bytes to transfer (Data Stage)
}   setup_buffer;

bmRequestTypeの5,6ビット目には、USBとして備えるべき機能であるか、HIDとして備えるべき機能であるかという大分類情報が書かれています。 また、bRequestを検査することによって、トランスファの詳細を特定することが出来ます。

コントロール・トランスファの種別コード
対応bmRequestType[6:5]bRequest種別
YES000x01CLEAR_FEATURE
YES000x05SET_ADDRESS
YES000x06GET_DESCRIPTOR
YES000x09SET_CONFIGURATION
NO010x01GET_REPORT
YES010x09SET_REPORT

HIDでは、必須とされているGET_REPORTは、実装されていません。 コメントに寄せられた情報によると、Windowsでは、インターフェース・デスクリプタにインタラプト・トランスファが宣言されていたら、GET_REPORTは、発行されないのだそうです。 まあ、今の所は、Windows専用ということにしておきましょう。

トランスファの種類によって処理を分ける部分の記述は、以下の通りです。 未知のトランスファに出会ったら、STALL宣言をすることによって、受け付けられなかったことを示します。

    if((SetupBuffer.bmRequestType & 0x60) == 0x00) {
      // Standard Request Decoder:
      switch(SetupBuffer.bRequest) {
        case CLEAR_FEATURE:     // 1
          clearFeature();
          break;
        case SET_ADDRESS:       // 5
          setAddress();
          break;
        case GET_DESCRIPTOR:    // 6
          getDescriptor();
          break;
        case SET_CONFIGURATION: // 9
          setConfiguration();
          break;
        default:
          forceSTALL();
          break;
      } // endswitch
    } else if((SetupBuffer.bmRequestType & 0x60) == 0x20) {
      // Class request
      switch(SetupBuffer.bRequest) {
        case SET_REPORT:        // 9
          setClassReport();
          break;
        default:
          forceSTALL();
          break;
      } // endswitch
    } else {
      forceSTALL();             // Unknown request
    }

またまた、長くなったので続く…

付録 : USBプロジェクト索引

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USBプロジェクト - WindowsでHIDデバイスを扱う定石 (2) [USB]このエントリーを含むはてなブックマーク#

1469810

昨日の続きです。 HIDクラス・デバイスを表す「パス名」のリストからスタートです。

STEP4 : 後片付けを忘れずに

前回、入手した「鍵のリスト」は、「パス名のリスト」が手に入った今となっては不要です。 不要になったからといって、放っておくと、メモリがどんどん消費されてしまいます。 不要になったら、きちんと捨てる。 これが、Winodwsの流儀です。

「鍵のリスト」を廃棄するための関数は、 SetupDiDestroyDeviceInfoList()です。

ここまでの手順は、 DeviceManagement.FindDeviceFromGuidにあります。 何も考えずに使わせてもらっても動きます。

STEP5 : お目当てのデバイスを探せ

「パス名のリスト」が手に入ったので、ここからお目当てのデバイスを探します。 探し出すためには、「パス名」を元にファイルを開き、そのファイルの属性(Attribute)を調べます。

ファイルを開く関数は、一般のファイルと同じCreateFile()です。 宣言は、"FileIODeclaration.vb"に記述されています。

ファイルの属性を取り出す関数は、HidD_GetAttributes()です。 「お目当てのデバイス」を見つけるための手がかりとして使っているのが、 ベンダID (VenderID) とプロダクトID (ProductID) です。 この二つのIDが一致したときに、デバイスが見つかったと判断します。

属性を調べ終わったら、ファイルを閉じておきます。 ファイルを閉じる時に使う関数は、CloseHandle()です。

STEP6 : デバイス取り外し時のイベントを受け付けろ

USBデバイスは、動作中に抜き差ししても良いことになっているので、 抜かれた時に発生するイベントを受信する必要があります。 イベントを受信するオブジェクトを指定する関数は、 RegisterDeviceNotification()です。

この関数の呼び出しには、いくつ物手順を踏む必要があります。 お手本プログラムの"hidclass_vs5"の場合は、 "DeviceManagement.RegisterForDeviceNOtifications()"というメソッドにすべて記述されていますので、 そのまま呼び出すだけで十分です。 イベントが発生したら、フォームのWndProc()というメソッドが呼び出されます。

STEP7 : パイプを開け

HIDデバイスとレポートを送受信するために使用されるのは、 WriteFileReadFileという関数です。 そうです。普通のファイルの読み書きと何ら変わらないのです。

そのため、送受信に使用されるパイプを開くときも 普通のファイルと同様にCreateFile関数が使用されます。

STEP8 : データを送信する

データを送信する場合には、WriteFile関数を使用します。 お手本の"hidclass_vs5"では、 送信すべきレポートがOutputReportクラスとして定義されているので、 インスタンスを作成し、Writeメソッドを呼び出したらおしまいです。

STEP9 : データを受信する

データを受信する場合、ReadFile関数を使用します。 受信の場合には、送信の場合と異なり、データの受信が終わるまで次の処理に進むことが出来ないのが常です。 それまで待っているいるのも無駄なので、「データが受信できたら呼んでね。」という意味の コールバックと呼ばれる関数を定義し、InputReportクラスの Readメソッドを呼び出します。

これら送受信にかかる一連の操作は、 ExchangeInputAndOutputReports()に記述されています。 全部理解しなくても十分に使えています。


こうやって、まとめを書いてみると、レポートの送受信のところの理解がまだ甘い事に気が付きます。 特にDelegateが出てくると、とたんにややこしくなります。 まだ、修行が足りないな。

参考サイト

HIDClass Devices
Micro$oftの解説書は、ここにあります。 日本語版もあるのですが、迷訳なので、英語版の方がおすすめです。
http://i-wanna-metamorphose.blogspot.com/2008/02/so-net-blogusb-reportdescri.html
ここにもがんばっている方がいらっしゃいました。
改訂版 プロフェッショナルVB.NETプログラミング
.NET版のVisual BASIC の使い方が解説されています。読めば読むほど「Javaみたい」だと思うのですよね。

USBプロジェクト - WindowsでHIDデバイスを扱う定石 (1) [USB]このエントリーを含むはてなブックマーク#

3ボタン・マウス・デバイス

ようやく、HIDデバイスを扱うためのPC側アプリケーションが書ける様になったので、HIDデバイスを操作するときの定石についてまとめておきます。 テキストは、EDNの記事、 Using the HID class eases the job of writing USB device drivers です。 日本語版が見つけられませんでした。 もしかしたら、日本には上陸しなかったのかな?

HIDは、ヒューマン・インターフェースのためにあるわけではない

この記事の言わんとするところは、「ヒューマン・インターフェース・デバイス(HID)」という言葉に惑わされること無く、なんにでも使ってしまえというところです。 実際、単純な入出力なら、ボタン入力とLED出力に偽装すれば、どんなものだってHIDで十分に実用になります。 しかも、HIDを使うだけで、プログラム開発が圧倒的に簡単になります。

STEP1 : HIDを探せ

HIDデバイスを扱うための第一歩は、HIDドライバを探すことです。 このステップは簡単です。 HidD_GetHidGuid()という関数を呼び出すとHIDデバイスの識別子(GUID)が入手できます。

関数を呼び出すために必要なこと

この関数は、hid.dllという動的リンク・ライブラリ(Dynamic Link Library : DLL)に入っています。 通常は、C:\WINDOWS\SYSTEM32ディレクトリを探すと見つかります。 そのため、「hid.dllにあるHidD_GetHidGuidを呼び出せ。」とプログラムに書けばすべて解決です。

ところが、いわゆる高級言語でプログラムを作成する場合、「HidD_GetHidGuidは、hid.dllに入っている。」と教えてやる必要があります。 Visual C++ でプログラムを作成する場合、この情報を持っているのが「LIBファイル」です。 そのため、プログラム開発には、関数がすぐそこにあるにもかかわらず、「Windows DDKに入っているhid.libファイル」を取り寄せなくてはなりません。 さらに、「HidD_GetHidGuidは、こんな風に使う。」という情報が「ヘッダファイル」という別のファイルに入っています。 そのため、「Windows DDKに入っているhid.hファイル」を取り寄せなくてはなりません。 このようにVisual C++では、必要なファイルが芋づる式に増えていきます。

一方、Visual BASICの場合はどうかというと、実は、「HidD_GetHidGuidは、こんな風に使う。」という情報を入れたファイルがそもそも存在しません。 では、どうするかというと、自分で書き起こすのです。 でも、ご安心ください。 世界中の人たちが「Visual BASICで関数を使いたい。」と考えているので、ありがたいことにその成果を利用させてもらうことができます。 私がプログラムのお手本にした"hidclass_vs5"の場合にも、関数呼び出しの方法を書いたファイル"HidDeclaration.vb"が用意されています。

<DllImport ("hid.dll")> Sub HidD_GetHidGuid _
  (ByRef HidGuid as System.Guid)
End Sub

必要なのは、これだけです。 しかも、この関数がどの"DLL"ファイルに入っているかという情報も含まれていますので、これだけの記述で十分なのです。 これと同じことがVisual C++でも出来ればよいのですけど。

というわけで、Visual BASICを使うときには、ライブラリ相当のテキストファイルが必要だけれど、Windows DDKを入手する必要はありません。

STEP2 : HIDクラスのデバイスをリストアップしろ

HIDクラスに所属するUSBデバイスは、オペレーティング・システムの作用により、PCに接続されたとたんにHIDドライバの管理下に置かれます。 そのため、HIDドライバに問い合わせをすると、現在接続中のHIDクラス・デバイスのリストを入手することができます。

このリストの問い合わせに使用される関数が、SetupDiGetClassDevs()です。 この関数は、"setupapi.dll"に入っています。 関数の宣言は、"DeviceManagementDeclarations.vb"を見てください。

この関数は、HIDクラス・デバイスにたどり着くための鍵のリストを返します。

STEP3 : 鍵からパス名を割り出せ

出来上がったリストから、アプリケーションが操作しようとしているデバイスを割り出さなくてはならないのですが、鍵にはデバイスを割り出すための詳細な情報は含まれていません。 このため、デバイスを特定する名前である「パス名」のリストに変換します。

パス名を求めるためには、関数SetupDiEnumDeviceInterfaces()SetupDiGetDeviceInterfaceDetail()を使用します。 まず、SetupDiEnumDeviceInterfaces()でインターフェースを示す構造体を構築します。 そして、SetupDiGetDeviceInterfaceDetail()でその詳細なデータを引き出します。 何で、こんな二段構成になっているのかは、理解できません。

SetupDiGetDeviceInterfaceDetail()は、二回呼び出されます。 一回目は、得られるデータの大きさを調べるために呼び出されます。 そして、二回目は、あらかじめ十分な大きさの領域を確保して、そこにデータを書かせます。 領域を確保するには、ヒープ領域が便利です。 ヒープに書かせたデータをVisual BASICのStringにコピーしたら、ヒープは開放されます。

Visual BASICでヒープを割り当てる

Cの場合、ヒープを割り当てるためには、"malloc()"のような関数が使用されました。 ところが、ちょっと前のVisual BASICの場合、もともとヒープのような概念がないため、 何とか誤魔化すしか方法がありませんでした。

現在のVisual BASICは、かなりCに歩み寄っているようで、"Marshal.AllocHGlobal()"という関数(メソッド?)が提供されています。


長くなったので、続く…


USBプロジェクト - Windowsアプリケーションの作成 [USB]このエントリーを含むはてなブックマーク#

3ボタン・マウス・デバイス

PC側アプリケーションの作成で紆余曲折してきたこのシリーズですが、ついに最初のアプリケーションを作成することが出来ました。 やはり、鍵は、Visual BASIC が握っていたようで。 hidclass_vs5プログラムを幅広く利用させていただきました。

これがPC側アプリケーションだ

WS000115.png

マイコン側のハードウェアは、写真の3ボタンマウスを使っています。 また、ファームウェアは、数日前に公開した"HID05"をそのまま使用しています。 アプリケーションの仕事は、チェックボックスの状態をUSBデバイスのLEDに伝え、ボタンの状態を採取してくることです。

プログラム中でオオチャクをしているので、チェックボックスが変更されたタイミングでボタンの状態を採取しています。 以下のファイルを"HidApp01.exe"という名前で保存すると実行ファイルが再現できますので、追試をなさりたい方はご利用ください。 ソースコードは、もう少し整備が進んでから公開します。

HidApp01.exe

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USB プロジェクト - WindowsアプリケーションにはBASICが良く似合う [USB]このエントリーを含むはてなブックマーク#

3ボタン・マウス・デバイス

マイコン側に"Generic HID"デバイスが用意できたので、これを制御するPC側のアプリケーションを作成します。 色々とサンプル・アプリケーションを探し回ったのですが、なかなか適当なのがありません。 古い Visual C++ や Visual BASIC のプロジェクトは、たとえ変換できたとしても、現代のVS2008では、簡単には動きそうにありません。 その中で、Visual BASIC 2005 Express Edition で作ったHIDクラスというものがありました。

Visual BASIC 2005 でHIDを操作する

その期待させるようなアプリケーションがあるのは、ここです。 このページの上から4分の1ほどの所に"HIDClass"というリンクがあります。 コメント欄で hamayan さんに指摘をいただきましたが、 http://www.lvr.com/ というのは、有名なUSB本の原作者の方のサイトなのですね。

Usb Complete: Everything You Need To Develop Custom Usb Peripherals (Complete Guides Series)

Usb Complete: Everything You Need To Develop Custom Usb Peripherals (Complete Guides Series)

  • 作者: Jan Axelson
  • 出版社/メーカー: Lakeview Research
  • 発売日: 2005/06/15
  • メディア: ペーパーバック

苦も無くビルト完了

さっそく、"hidclass_vs5.zip"をダウンロードして、"Visual BASIC 2008 Express Edition"で開いてみました。 いくつかオプションを指定するとプロジェクトが自動的に変換されます。 「無警告」でした。

ビルトすると、アプリケーションが出来ました。 実行ボタンを押してデバッグ開始です。

エラーが出る

WS000113.png

アプリケーション画面で、Vender IDとProduct IDを入れて、"Find My Device"をクリックすると、ちゃんとデバイスを見つけてくれました。 ここで、"Send and Receive Data"にある"Once"をクリックすると、レポートの送信と受信が行われると期待していたのですが、エラーメッセージが出てしまいました。

WS000114.png

何だか難しい日本語ですが、要するに配列の添え字が大きくなりすぎたようです。 問題発生箇所を確認したところ、私が作ったデバイスは、1バイトしかデータを送受信しないのに、2バイト分のデータを取り出そうとしたのが原因であることがわかりました。 そこで、プログラムを修正しました。 以下、1210行辺りの記述です。

lstResults.Items.Add("An Output report has been written.")

'Display the report data in the form's list box.

Debug.WriteLine(" Output Report ID: " & OutputReportBuffer(0))
Debug.WriteLine(" Output Report Data:")
lstResults.Items.Add(" Output Report ID: " & OutputReportBuffer(0))
lstResults.Items.Add(" Output Report Data:")

txtBytesReceived.Text = ""
For Count = 1 To UBound(OutputReportBuffer) '2

    'Add a leading zero to values from 0 to F.

    If Len(Hex(OutputReportBuffer(Count))) < 2 Then

        ByteValue = "0" & Hex(OutputReportBuffer(Count))
    Else
        ByteValue = Hex(OutputReportBuffer(Count))
    End If
    Debug.WriteLine(" " & ByteValue)
    lstResults.Items.Add(" " & ByteValue)
Next Count

再度、挑戦

プログラムを修正して、再度実行します。 "Autoincrement values"をチェックして、"Continuous"ボタンをクリックすると、 約1秒ごとにLEDのパターンが変化し、ボタンを押したときの反応も"Bytes Received"で 確認することが出来ます。

あとは、お好みの機能を実装すれば、PC側アプリケーションは、何とかなりますね。

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本
USB Complete: The Developer's Guide (Complete Guides Series)

USB Complete: The Developer's Guide (Complete Guides Series)

  • 作者: Jan Axelson
  • 出版社/メーカー: Lakeview Research
  • 発売日: 2009/06
  • メディア: ペーパーバック

USB プロジェクト - コンパイル済みプログラムなら何とか [USB]このエントリーを含むはてなブックマーク#

3ボタン・マウス・デバイス

MC9S08JMxxシリーズ用のサンプル・アプリケーションを利用する計画は、 コンパイラがうまく使えないために挫折してしまいました。 でも、同梱されていたコンパイル済みプログラムだったら、何とかつかえるはずです。 そこで、PIDとVIDをこのアプリケーションに合わせたターゲットを作成して、 何とか連動させようと考えました。

ターゲットの改造

Hid05ResourceAssignment.png

ターゲットは、HIDインターフェースを搭載した PID=XXX; VID=XXXX (伏字)のデバイスです。 HIDの上には、"Generic HID"インターフェースを構築します。 使用した"ReportDescriptor"は、これです。

char ReportDescriptor[46] = {
    0x06, 0x00, 0xff,              // USAGE_PAGE (Vendor Defined Page 1)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x05, 0x08,                    //   USAGE_PAGE (LEDs)
    0x09, 0x4b,                    //   USAGE (Generic Indicator)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x95, 0x04,                    //   REPORT_COUNT (4)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x95, 0x04,                    //   REPORT_COUNT (4)
    0x91, 0x03,                    //   OUTPUT (Cnst,Var,Abs)
    0x05, 0x09,                    //   USAGE_PAGE (Button)
    0x19, 0x01,                    //   USAGE_MINIMUM (Button 1)
    0x29, 0x03,                    //   USAGE_MAXIMUM (Button 4)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x95, 0x04,                    //   REPORT_COUNT (4)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x95, 0x04,                    //   REPORT_COUNT (4)
    0x81, 0x03,                    //   INPUT (Cnst,Var,Abs)
    0xc0                           // END_COLLECTION
};

オリジナルから、ボタンの数を増やしました。 たぶん、このくらいは大丈夫だろう。

接続、点火

無限ループにボタンの状態とLEDの状態をやり取りするプログラムを書いたら、 度胸一発、接続、点火。 デバイスをつないだら、単なるHID準拠デバイスとして認識されました。

アプリケーションを起動する

フォルダ"C:\CMXUSB_LITE\usb-peripheral\pc-side\hid-led-demo\Debug"にある "hid-led-demo.exe"を実行します。 そして、開いたウィンドウのLEDボタンを押すと、見事にLEDが点灯しました。 また、ターゲットのボタンを押すとチェック・ボックスが反応しました。 めでたしめでたし。

やっぱり、アプリケーションを作ろう

実験できるのは、ここまで。 あとは、アプリケーション・プログラムを作るしかなさそうです。 でも、HIDが使えるのだったら、楽できそうだな。

プロジェクト・アーカイブは、以下のリンクを"HID05.zip"という名前で保存すると再現できます。

HID05.zip (20446 Bytes)

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USB プロジェクト - 懲りずにWinUSBに手を出す [USB]このエントリーを含むはてなブックマーク#

3ボタン・マウス・デバイス

Windows Vistaには、WinUSBという仕掛けがあるらしい。 Windows XPでも使えるらしいので、手を出してみるか。

WinUSBのサンプル・プロジェクトを取り寄せる

奇特な方がいて、サンプル・プロジェクトを作ってくれています。 このサイトにVisual BASICとVisual C#で 記述したサンプルが置いてあります。 最初は、Visual BASICで試してみます。

コンパイルは、できました

Visual C++ のサンプルと違い、このサンプルはExpress Edition でもコンパイルができました。 おぉ、なかなか有望ではないか。 ここで、初めて"readme.doc"という取扱説明書を読みました。

まず、infディレクトリにある記述を使用するUSBデバイスに合わせて変更する必要があります。 「\USB\VID_何たら\PID_かんたら」 この記述は、以前UUSBDをかじったときに悩んだ形式ですね。 HIDデバイスの場合には、「\HID\VID_何たら\PID_かんたら」になってしまい、 結局HIDデバイスをUUSBDから操作することは出来なかったのでした。 今回も、同じ目に遭いそうな予感。

INFファイルのGUIDとアプリケーションのGUIDを合わせるとのこと。 これは、サンプルINFファイルのままを使うことにしました。

あとは、デバイス・ドライバのインストールで、WDKのファイルが必要だそうです。 何だ、結局、WDKは必要なのね。

HIDは、認識してくれない

INFファイルを書き換えて、デバイスもインストールして、 「さあ、デバイスを認識しろ」と"Find My Device"ボタンをクリックしましたが、音沙汰無し。 INFファイルをUSBからHIDに書き換えましたが、これも効果なし。 しかたがないので、インターフェース・デスクリプタのクラス・コードを0xFF(ベンダ独自インターフェース)に変更して、マイコンのプログラムを書き換えました。 すると、今度は認識は出来ました。

でも、通信プロトコルは全く合っていないので、通信そのものは成り立ちませんでした。 このサンプル・プログラムでは、通信プロトコルの低レベルな部分まで記述されているので、もしかしたら、デバイス・ドライバ並みのアプリケーションが必要になるのかもしれません。 それでは、本末転倒でしょう。

思わぬ収穫?

WinUSBサンプル・プログラムを見ていて、"SetupDiGetClassDevs"などのどこかで見たような関数の宣言が並んでいることに気がつきました。 前回、Visual C++ではどうしても解決できなかった、あのプログラムで使用されていた関数です。 どうやら、Windowsでプログラムを書くときには、Visual BASICの方が適しているようです。 さすがは、ビル。 何処まで行ってもBASICの呪縛からは逃れられないのですね。

これらの関数宣言を使えば、Visual C++のサンプルプログラムをVisual BASICで書き直して使うことが出来るかも知れません。 そうすると、HIDデバイスのまま、インターフェースが出来るかな?

おまけ

http://www.lvr.com のページを探っていて、「Visual BASIC 2005 Express Edition」で作成したHIDデバイスを扱うプログラムがあることに気がつきました。 これを使ってみるのもいいかな。


とにかく、色々と目移りしそうなサンプル・プログラムが沢山あることに気がつきました。 どれもこれもMicro$oftのサイトでは無いというところが、いかにもMicro$oftらしいではありませんか。

参考文献

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

USBハード&ソフト開発のすべて―USBコントローラの使い方からWindows/Linuxドライバの作成まで (TECHI―Bus Interface)

  • 作者: インターフェース編集部
  • 出版社/メーカー: CQ出版
  • 発売日: 2006/07
  • メディア: 単行本

USB プロジェクト - デバイス・ドライバを入れ替える [USB]このエントリーを含むはてなブックマーク#

3ボタン・マウス・デバイス

hamayanさんも、お悩みになったように USBデバイスのプログラムを書き換えたとき、PC側は「PIDとVIDだけを確認して同じデバイスであると認識する」ので、PC側デバイス・ドライバは更新されません。 これでは、デバッグにならないので、PCに「昔のことは忘れて」もらいましょう。

以下、Windows XP の場合の操作例です。

デバイス・マネージャを呼び出す

デバイス・ドライバ入れ替えの第一歩は、デバイス・マネージャの呼び出しから始まります。

コントロールパネル → システム → ハードウェア → デバイスマネージャ

デバイスのツリーが表示されますので、ここから該当するデバイスを探します。 この時、 ツリーには接続されているデバイスしか表示されないので、 当該デバイスはUSBポートに接続しておいてください。

プロパティ・ダイアログを操作する

探し出したデバイスの所で右クリックし、プロパティ・ダイアログを出します。 詳細タブで「デバイスインスタンスID」を確認して当該デバイスであることを確認しておきましょう。

確認したらドライバ・タブを出します。 ここではドライバの変更に関するボタンが並んでいます。

デバイス・ドライバを削除するために、「削除」をクリックします。 HIDデバイスの場合、ここでUSBデバイスから採取された各種デスクリプタの情報が削除されているようです。 逆にいうと、PC側は、USBデバイスを抜き差ししただけでは、デスクリプタを再読み込みしたりしないようです。

USBデバイスを再認識させる

この工程は簡単です。 単にUSBデバイスを抜いて挿しなおします。 すると、オペレーティング・システムが新しいUSBデバイスを認識して、 勝手にデバイス・ドライバができます。

問題はどこに

Windows XPの場合には、以上の手順でデバイスの入れ替えが行われるようなのですが、問題は、当該デバイスが存在しないとドライバの削除も行えないというところです。 昔使った事があるデバイスドライバが山のように残されていて、すでに壊れた・捨てた場合には、二度と削除できないというすばらしいシステムです。

また、USBデバイスを挿しなおしただけではデスクリプタの読み込みや検証はされていないようなので、一度怪しいデバイスドライバを入れられたら、正規デバイスに対しても怪しいデバイスドライバが反応するので、危険な香りがします。

他の方法もある

デバイス・マネージャの右クリックメニューには、「ドライバの更新」や「ハードウェア変更のスキャン」という項目もあって、使える時には使えるようです。 ただ、「役に立たないこともある」ようなので、削除してしまった方が確実であろうと思います。


USBプロジェクト - サンプル・ソフトへの長すぎた道のり (挫折編) [USB]このエントリーを含むはてなブックマーク#

ソースコードがあるからと、気軽に手を出したのですが。

CLRとは、なんぞや?

サンプル・プログラム同様にフォーム上のボタンを操作してLEDを制御するモデルを考えていたので、 GUIつきの空プロジェクトを作ろうと考えました。 いくつかプロジェクトを作成してみて、 Visual C++ Express Editionでフォームが出てくるのは、「新規作成→Visual C++→CLR→Windowsフォーム・アプリケーション」だけだったので、これを雛形として使います。

あとは、"hid_dev.cpp"などのソース・コードと"hid-lib"に入っているファイルをコピーしました。 まだ、"hid_dev.cpp"内の関数を使用するプログラムは書いていません。 これを度胸一発、ビルドすると、

warning LNK4248: 未解決の typeref トークン (0100001C) ('_HIDP_PREPARSED_DATA') です。イメージを実行できません。

をはじめとして、沢山のエラーが出てきました。 これは、ライブラリが指定されていないからだと考え、「プロパティ・ページ」の「リンカ;追加のライブラリ・ディレクトリ」に"hid-lib"ディレクトリを追加しましたが、状況は変わらず。

エラーメッセージを頼りに検索をかけた結果、 "__clrcall"関数から"__stdcall"関数を呼び出しているのが気に入らないご様子。 何ですか?"CLR"って?

敗北宣言

そもそも、英語でもわからないエラーメッセージが、日本語にするともっとわけがわからなくなるので、 英語版のメッセージを探しに行きました。

unresolved typeref token (token) for 'type'; image may not run

前方参照された「型」が気に入らないということなので、"_HIDP_PREPARSED_DATA"が あらかじめ正しく定義されていれば問題ないようです。 でも、この型の宣言がまったく見つかりません。 だいたい、これはリンカのエラーだよな。

というわけで、 現在のGUIベースプログラムと太古のデバイスドライバを組み合わせて使うのは、 割りに合わない労力が必要だと判断して、このあたりでやめます。 早い話、敗北宣言です。

それにしても、 古いソース・コードが通らなくなるような、改造はやめて欲しいな。

収穫が無いわけじゃない

今回の調査で、現代風にUSBデバイスをユーザレベルのプログラムで操作する方法として、 "WinUSB"という概念があることがわかりました。 UUBSDみたいなものが肥大化を続ける自称オペレーティング・システムに入るという事かな? こちらは、VB2005でのサンプルというものがみつかったので、敗北感から立ち直ったら、試してみます。


USBプロジェクト - サンプル・ソフトへの長い道のり(後編のつもりだった) [USB]このエントリーを含むはてなブックマーク#

だんだんと手段が目的になりつつある。

WDKのインストール

前回は、WDKをダウンロードしたところで夜になったので、 今日はインストールからはじめます。 ダウンロードしたISOイメージから出来上がったCD-RWは、660MBほどの容量でした。 これをインストールしたところ、1.65GBの容量を占めるバケモノに変化してしまいました。 こいつも大喰らいだぁ。

サンプルプログラムに無かったファイルは、 "{install_directory}/inc/api"にヘッダ・ファイルが、 "{install_directory}/lib/wxp/i386"にライブラリ・ファイルがありました。 これを"hid-lib"ディレクトリに入れます。

Visual C++でコンパイル

それでは、気を取り直して新規に作り直したディレクトリのソリューションを開きます。 古いバージョンのソリューションだから、変換しますという表示が出て、変換されたようです。 いくつか警告が出ていましたが、

C++ コンパイラの準拠の変更によって、プロジェクトがビルドする前にコード変更が必要になる場合があります。

こんな感じの「日本語の活字で記述された宇宙語の警告」なので、さっぱりわかりません。 しまった、英語版を入れるべきだった。

実のところ、VisualC++に触るのは初めてです。 Windows系列のコンパイラは、Windows1.2の時に挫折して以来なので、 まったくの素人と言っても差し支えないかと思います。 よくわかんないので、とりあえず、"hid_dev.cpp"だけコンパイル。

include ファイルを開けません。'afxwin.h': No such file or directory

これは、MFCだかのライブラリが無いのが原因でしたね。 "stdafx.h"の中の"#include"をコメントアウトして再度挑戦。

fatal error C1083: include ファイルを開けません。'atlstr.h': No such file or directory

これは、"hid_dev.cpp"で"Cstring"クラスが使用されているのが原因なので、 使用するクラスを"std::string"に変更します。

static std::string msg;

今度は、"setupapi.h"ファイルで使われている"GUID"という型が宣言されていないという エラーが出てきました。 使用している箇所は、こんな風になっています。

#ifndef __LPGUID_DEFINED__
#define __LPGUID_DEFINED__
typedef GUID *LPGUID;
#endif

それにしても、このVisualC++についてきたヘッダファイルは醜いですね。 パッチの当てすぎです。 これなんか「定義されていなかったら定義しろ」なんて記述が あちらこちらのヘッダファイルに散在しているという事のあらわれでしょう。 美しくない。

MSDNを調べた結果、GUIDというのは、"windows.h"というヘッダ・ファイルに出てくるらしいことが わかりました。 そこで、"hid_dev.cpp”にヘッダ・ファイル呼び出しを追加しました。

error C2039: 'GetString' : 'std::basic_string<_Elem,_Traits,_Ax>' のメンバではありません。

"CString"の代わりに"std::string"を使うことにしたので、 "GetString()"は、"c_str()"に変更します。 これで、hid_dev.cpp"はコンパイルできるようになりました。

GUIを作り直す

ここから、"hid_dev.cpp"以外の部分を変更していきます。 ところが、VisualC++のバージョンの違いから、GUI部分の変更が膨大になりそうなので、 いまはこの結果だけを使って、GUIは、VisualC++に新たに作成させることにしました。


USB プロジェクト - サンプル・ソフトへの長い道のり(前編) [USB]このエントリーを含むはてなブックマーク#

今日は、USBプロジェクトのための資料集めの覚え書きです。

USB対応Flexisが発表された

2008年2月22日付けで、二つ目のFlexis™チップであるMCF51JM128が公開されました。 同時に評価ボードDEMOJMとそのデモソフトまで公開されました。 それが、CMX_USB-LITE: Complimentary USB stack by CMXです。 このサンプルソフトの中に"Generic HID Device"のマイコン側ファームウェアとPC側ファームウェアが入っていたので、読んでみる事にしました。

PC側ソフトウェアにはVisual C++が必要

PC側のソフトウェアは、Visual C++ で記述されていました。 それなら、Visual C++ Express Editionを持ってきて、すぐにコンパイルできるだろうと、 最新版のVisual Studio 2008 Express EditionのISOイメージをダウンロードして、インストールしてみました。 ISOイメージが1.1GBなので、DVD+RWに焼きました。 インストールに必要なディスク容量が約1GB。 すごい大喰らい。

Express Editionは、不完全だった

インストールして、さっそく、件のPC側ソフトウェアのプロジェクト(Micro$oft用語では、ソリューションと呼ぶらしい)を開いて、ビルドさせてみました。 すると、「afx??.hというヘッダが無い」と返してきました。

何のことだかわからないので、Micro$oftで検索をかけました。 その結果、「Express Editionには、MFCだかATLだかが含まれていないので、このヘッダは存在しない。」ということ事のようです。

わけあって、ライブラリが足りない

きっと、ユーザインタフェースの部分の話だろうと、 「#includeを適当に削除して」ビルドを続けると、今度は、「hid-libディレクトリにヘッダが無い」と返してきました。 hid-libディレクトリは、サンプル・プロジェクトに含まれているディレクトリです。 確かにhid-libディレクトリはありますが、その中には、ヘッダファイルはありません。 代わりにあるのは、"readme.txt"だけです。

"readme.txt"には、「著作権の問題があるから、ヘッダは入れられんかった。」と書いてあり、 「Windows DDKからコピーしてきてチョ。」とあります。 今度は、"Windows DDK"が必要なのですね。

Windows Driver Development Kit を訪ねて

普通は、ユーザ様にプログラムを作成してもらうのだから、 そのための技術資料はポロポロと落ちていそうなものですが、 そこはMicro$oft。 捜せど捜せど、「Windows DDK」がダウンロードできる場所に当たりません。 その中で、見つかったのが、 Which Windows Driver Kit and Test Kit to Useというページでした。

ふむふむ、「Windows Vista WDK」を捜せばよいのだな。 でも、やっぱり出てきません。 このページの中にはリンクがいくつかあって、その一つが、 How to Get the Windows Driver Kit and the Windows Logo Kitでした。

Micro$oft Connect という存在

このページの中には、"Windows Driver Kit"を入手するための二つの方法が紹介してあります。 一つは、「MSDN Subscriberからダウンロード」 "MSDN"が買えるのであれば苦労はしないから、これは論外。 もう一つは、「Micro$oft Connect Webサイトからダウンロード」という方法。 これは、何のサイトですか?

Micro$soft Connectは、 会員制のサイトで、 要するにMicro$oftのベータ版ソフトウェアを無償でデバッグしてやる奇特な人たちのためのサイトのようです。 何だか、だんだん腹が立ってきたぞ。[ちっ(怒った顔)]

まあ、仕方が無いので、660MBの"WDK"をダウンロードしている間に このBLOGの文章が書き終わりました。

次回は、WDKのインストールから。


USB プロジェクト - 3ボタン・マウス [USB]このエントリーを含むはてなブックマーク#

HIDのサンプル・プログラムを改造して、 ついに、3ボタン・マウスを作りました。

概要

この作品は、デジタル入力のみを用いて作られた3-ボタン・マウスです。 マウスの移動には、四つのタクト・スイッチを使います。 移動速度は、ファームウェアにより自由自在です。 ボタンも三つ付いています。 場所が余ったので、LEDによるインジケータも付けました。 今は、ろくな仕事を与えられていません。

キーとLEDの割り当て

キーとLEDは、それぞれポートを一つずつ占有しています。

キーとLEDの割り当て
種別名前/位置ポート機能
キーPTA0ポインタを右に移動します。
キーPTA1ポインタを上に移動します。
キーPTA3ポインタを下に移動します。
キーPTA4ポインタを左に移動します。
キーボタン1PTA7マウスの左ボタンです。
キーボタン2PTA5マウスの右ボタンです。
キーボタン3PTA6マウスの中央ボタンです。
LED左/赤PTD4ボタン1のインジケータです。
LED中/黄PTD3ボタン3のインジケータです。
LED右/緑PTD2ボタン2のインジケータです。

キー入力にPTA2が使われていないのは、間違いではありません。 USBDEV基板上で10kΩの抵抗によってプルダウンされているため、 プルダウンタイプのキー入力には使えなかったのです。 LEDには、それぞれ1kΩの電流制限抵抗を付けています。

即興で作成したので、回路図は、存在しません。

プログラム

プログラムは、キーボードのフリをしていたプログラムをもとに改造を施しています。

レポート・デスクリプタ以外で変更があったのは、 インターフェース・デスクリプタに含まれるプロトコルIDが、 マウス・デバイスになったところです。

  {                     // Size of this Descriptor in Bytes
    sizeof(interface_descriptor),
    DT_INTERFACE,       // Descriptor Type (=4)
    0,                  // Number of *this* Interface (0..)
    0,                  // Alternative for this Interface (if any)
    1,                  // No of EPs used by this IF (excl. EP0)
    0x3,                // IF Class Code (0x03 - HID)
    1,                  // Boot Interface Subclass Code
    2,                  // IF Protocol Code  (2 = Mouse)
    0                   // Index of String Desc for this Interface
  }, // end of InterfaceDesc

これ以外の部分は、すべてキーボード・デバイスのものと同じです。

キーボードのインジケータで使われていたエンドポイントを通じたデータ受信の部分は、 おそらく使われることはないだろうと思いましたが、そのまま残してあります。 エンドポイントを通じたデータの送信の部分は、 送信するデータ長が8バイトであったものを3バイトにしてあります。

#define   RX0_SIZE      (8)     // Byte count of RX0
extern volatile byte    Rx0Buffer[RX0_SIZE];
#define   TX1_SIZE      (3)     // Byte count of TX1
extern volatile byte    Tx1Buffer[TX1_SIZE];

改造のためのヒント

現在は、ポインタの動作が1ドットずつの超低速です。 キーを押しっぱなしにすると、加速していくような仕掛けをファームウェアに仕込むと もっとマウスらしくなるかと思います。

また、LEDは、単にボタンに連動して点灯するようになっているので、 もっと賢い使い方が出来ないか、考えています。

プロジェクト・アーカイブ


USB プロジェクト - マウスのReportDescriptorを見てみよう [USB]このエントリーを含むはてなブックマーク#

キーボード・デバイスのフリができたので、次はマウス・デバイスに挑戦します。 ハードウェアを作る前に、マウス向けReportDescriptorについて調べます。

HID Descriptor Toolにおまかせ

USB.orgで見つけた"HID Descriptor Tool"のサンプル・デスクリプタを見ながら、 考えます。

    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x02,                    // USAGE (Mouse)
    0xa1, 0x01,                    // COLLECTION (Application)

最初は、このデバイスの種類(マウス)を宣言します。 ここから、マウス・デバイスとしてのデータ構造が始まります。

    0x09, 0x01,                    //   USAGE (Pointer)
    0xa1, 0x00,                    //   COLLECTION (Physical)

PhysicalタイプのPointer COLLECTIONが宣言されます。 Physicalが何を意味するのか、今のところ不明です。

    0x05, 0x09,                    //     USAGE_PAGE (Button)
    0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
    0x29, 0x03,                    //     USAGE_MAXIMUM (Button 3)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
    0x95, 0x03,                    //     REPORT_COUNT (3)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)

マウス・デバイスからPCに送られる1バイト目の下3ビットの構造です。 三つのビットがそれぞれボタン1から3に相当しています。

    0x95, 0x01,                    //     REPORT_COUNT (1)
    0x75, 0x05,                    //     REPORT_SIZE (5)
    0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)

1バイト目の残りの5ビットには、定数データ"0"が入ります。

    0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
    0x09, 0x30,                    //     USAGE (X)
    0x09, 0x31,                    //     USAGE (Y)
    0x15, 0x81,                    //     LOGICAL_MINIMUM (-127)
    0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
    0x75, 0x08,                    //     REPORT_SIZE (8)
    0x95, 0x02,                    //     REPORT_COUNT (2)
    0x81, 0x06,                    //     INPUT (Data,Var,Rel)

2バイト目と3バイト目には、(X,Y)座標データが入ります。 それぞれ符号付8ビットの表現になっていて、 しかも相対値の値になるとされています。

    0xc0,                          //   END_COLLECTION
    0xc0                           // END_COLLECTION

最後にPointerとMouseの二つのCOLLECTIONを閉じておしまいです。

なんだか、キーボード・デバイスに比べて簡単に見えます。 扱うデータも3バイトの片方向通信です。 HIDのサンプル・プログラムを使うと、あっという間にできるかも。


USB プロジェクト - ReportDescriptorの解析 [USB]このエントリーを含むはてなブックマーク#

HIDのフリをするプログラムが出来たので、何が書いてあるのか解析します。 (普通は、逆だな。)

参考文献

ReportDescriptorを読む

ファームウェアには、HIDに特有のReportDescriptorと呼ばれるデータが入っていて、 ホストの要求に応じてデータを返します。 このデータは、PCとの送受信に使われるパケットのフォーマットを規定していて、 賢いホストさまが、構文解析を行ってデバイスごとに適切なパケットを送受信してくれるという ありがたい仕組みになっています。 サンプルプログラムのこの部分には、一切コメントが無かったので、 勉強がてら解析することにしました。

05 01 - Global Usage Page = $01 (Generic Desktop)
09 06 - Local Usage = $06 (Keyboard)

Usageは、このデバイスの用途を指定します。 ここでは、Keyboardが指定されています。 Usageは、32ビットのデータで、Usage Pageは、その上位16ビットを指します。 と、仕様書には書いてあるのだけど、これは8ビットデータだよな。 まあ、いいか。

A1 01 - Main Collection = $01 (Application)

ここから、"End Collection"までの部分でパケットのフォーマットを指定します。 実際に送受信されるパケットの構造は、HIDの仕様書に書いてあります。

05 07 - Global Usage Page = $07 (Keyboard/Keypad Page)
19 E0 - Local Usage Minimum = $E0 (Left Control)
29 E7 - Local Usage Maximum = $E7 (Right GUI)
15 00 - Global Logical Minimum = $00 (0)
25 01 - Global Logical Maximum = $01 (1)
75 01 - Global Report Size = $01 (1 bit)
95 08 - Global Report Count = $08 (8 fields)
81 02 - Main Input = $02 (no NULL position; preferred state; linear; no wrap; absolute; variable; data)

最初の部分は、8ビットの送信データです。 Hutで定義されている8個のキーの状態をPCに知らせます。

キーとビットの対応表
ビット位置Usageコード対応するキー
0$E0左のCtrl
1$E1左のShift
2$E2左のAlt
3$E3左のGUI(PC/ATではWindowsキー)
4$E4右のCtrl
5$E5右のShift
6$E6右のAlt
7$E7右のGUI(PC/ATではメニューキー)

このファーム・ウェアでは、 それぞれのキーの状態をKbBuffer[0]に入れると、 それをPCが引き取ってしかるべき動作を行ってくれます。

95 01 - Global Report Count = $01 (1 field)
75 08 - Global Report Size = $08 (8 bits)
81 01 - Main Input = $01 (no NULL position; preferred state; linear; no wrap; absolute; array; constant)

この部分は、KbBuffer[1]に相当します。 8ビットの定数データが入ってくるだけで、PCでは何も処理されないはずです。

95 05 - Global Report Count = $05 (5 fields)
75 01 - Global Report Size = $01 (1 bit)
05 08 - Global Usage Page = $08 (LED)
19 01 - Local Usage Minimum = $01 (Num Lock)
29 05 - Local Usage Maximum = $05 (Kana)
91 02 - Main Output = $02 (non volatile; no NULL position; preferred state; linear; no wrap; absolute; variable; data)

ここから、唐突にPCから送られてくるデータの定義が始まります。 この部分は、LedBufferの下位5ビットに相当するデータです。 ファーム・ウェアがLedBufferの内容に従って、「何か」を行います。 それぞれのビットは、以下のように割り当てられます。

LEDビットの対応表
ビット位置Usageコード対応するLED
0$01Num Lock
1$02Caps Lock
2$03Scroll Lock
3$04Compose
4$05Kana

ComposeKanaは、 最近のキーボードには見当たりませんね。 LEDには、"Spinning"や"Message Waiting"なんていうものもあるので、 「ディスク回ってます」「メールが来たよ」インジケータがすぐに出来てしまうかも知れません。

95 01 - Global Report Count = $01 (1 field)
75 03 - Global Report Size = $03 (3 bits)
91 01 - Main Output = $01 (non volatile; no NULL position; preferred state; linear; no wrap; absolute; array; constant)

次の部分で、残りの上位3ビットを定義しています。 この部分には、定数データ(0)が入ってくるはずです。

95 06 - Global Report Count = $06 (6 fields)
75 08 - Global Report Size = $08 (8 bits)
15 00 - Global Logical Minimum = $00 (0)
25 65 - Global Logical Maximum = $65 (101)
05 07 - Global Usage Page = $07 (Keyboard Keypad Page)
19 00 - Local Usage Minimum = $00 (no event)
29 65 - Local Usage Maximum = $65 (Application)
81 00 - Main Input = $00 (no NULL position; preferred state; linear; no wrap; absolute; array; constant)

再び、PCに送るデータの宣言が入っています。 ここは、KbBuffer[2]からKbBuffer[7]に相当する部分で、それぞれにUsageコードが入ります。 つまり同時に6個のキー入力を伝えることが出来るというわけです。 入れることのできるコードは、102種類で、101タイプ・キーボードのすべてのキーに対応しています。 どのキーも押されていなければ、$00(no event)を入れます。

C0    - Main End Collection

データ(Collection)の終端をあらわす印で、デスクリプタは、完結します。

HIDデバイスに関する情報はこちら

HIDに関する資料などは、このサイトにあります。

USB.org - HID Tools

ここには、"HID Descriptor Tool"なんてものもあって、 16進数と格闘することなくデスクリプタを記述することの出来るツールなどがそろっています。 なんだ、これをそのままコピーしたのか。 納得です。

char ReportDescriptor[63] = {
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x06,                    // USAGE (Keyboard)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
    0x19, 0xe0,                    //   USAGE_MINIMUM (Keyboard LeftControl)
    0x29, 0xe7,                    //   USAGE_MAXIMUM (Keyboard Right GUI)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x95, 0x08,                    //   REPORT_COUNT (8)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x81, 0x03,                    //   INPUT (Cnst,Var,Abs)
    0x95, 0x05,                    //   REPORT_COUNT (5)
    0x75, 0x01,                    //   REPORT_SIZE (1)
    0x05, 0x08,                    //   USAGE_PAGE (LEDs)
    0x19, 0x01,                    //   USAGE_MINIMUM (Num Lock)
    0x29, 0x05,                    //   USAGE_MAXIMUM (Kana)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)
    0x95, 0x01,                    //   REPORT_COUNT (1)
    0x75, 0x03,                    //   REPORT_SIZE (3)
    0x91, 0x03,                    //   OUTPUT (Cnst,Var,Abs)
    0x95, 0x06,                    //   REPORT_COUNT (6)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x65,                    //   LOGICAL_MAXIMUM (101)
    0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
    0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated))
    0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application)
    0x81, 0x00,                    //   INPUT (Data,Ary,Abs)
    0xc0                           // END_COLLECTION
};

デスクリプタはここから来たらしい

HIDの仕様書の最後には、サンプル・デスクリプタが記載されています。 今回調べたファームウェアは、キーボードのサンプル・デスクリプタと一字一句同じでした。

2008-02-06 22:13:05 追記

他のUsageでLEDを操作できるか?

Message Waitingを使おうとしたのだけれど、失敗しました。 Telephonyに入っているところをみると、 どうやら「留守番電話にメッセージあり」という意味だったようです。

Spinningも使えませんでした。 こちらは、Mediaに入っているので、 「Mediaデバイス」のフリをさせないと情報が来ないのだろうと思います。

Muteもダメ。 これも、Consumerデバイスに分類されているので、 それなりのデバイスのフリをさせなくてはならないようです。

だったら、Keyboardに分類されている Shiftならいけるだろうと考えましたが、 そもそも、Shiftインジケータがどんな条件で点灯するのかがわからず、 操作できませんでした。

と、4連発で失敗してしまいました。 HIDインターフェースでLEDを光らせるのは、難しいようなので、 次は、マウス・インターフェースにでも挑んでみます。