📌

libusb-1.0のざっくりとした使い方

2021/04/03に公開

libusbは、USBデバイスを扱うプログラムをOSを意識せずに書けるCのライブラリ。現在はバージョン1.0系の開発が続いているが、バージョン0.1系とは互換性がない。しかし、ネット上で公開されているプログラムやチュートリアルにはバージョン0.1系のものも多く、また、たとえばDebianのように「libusb-dev」という名前でインストールできるパッケージが0.1系のプラットフォームもあるので、2021年4月現在はちょっと混乱しやすい状況にある気がする。

なお、2021年4月現在、libusb-1.0のDebianにおけるパッケージ名は「libusb-1.0-0-dev」である。

libusb-1.0の使い方は、いきなりAPIを見てもよくわからない。USBの仕様を片手に"Modules"のセクションからドキュメントを読んでいくとなんとなくわかってくる。

プログラム全体の流れ

libusbでは、特定のUSBデバイスを操作するアプリケーションを、こんな感じの流れで書く。

  1. libusbを初期化(セッションを開く)
  2. 接続しているデバイスの一覧を取得
  3. 一覧の中から関心のあるデバイスを見つけて開く
  4. 開いたデバイスで何かする(メッセージをやり取りする)
  5. 開いたデバイスを閉じる
  6. デバイスの一覧を破棄する
  7. セッションを閉じる

以下、それぞれの具体的な説明。

libusbの初期化(セッションを開く)

初期化には libusb_init() という関数を使う。

libusb_context *context;
libusb_init (&context);

libusbでは、単一のアプリケーションで複数のUSBとのやり取りを扱えるように、1つのプロセス中で複数のlibusbをリンクできるようになっている。その際、それぞれの状態は libusb_context という構造体型の変数で管理される。この変数によって区別されるlibusbの1つの実体が「セッション」であり、セッションを識別する変数のほうは「コンテキスト」と呼ばれている。libusb_init() 関数にもコンテキスト(へのポインタ(がある場所))を引数として指定する。

ただし、ふつうはアプリケーションでlibusbを同時にいくつも使わないだろうから、そういうときのためにデフォルトのコンテキストが用意されている。libusb_init() の引数に NULL を指定すれば、このデフォルトのコンテキストでセッションが開かれる。

libusb_init (NULL);

接続しているデバイスの一覧を取得

libusbのセッションを初期化したら、USBポートに接続しているデバイスを探す。そのために、まずはデバイスの一覧を取得する。これには libusb_get_device_list() という関数を使う。

libusb_get_device_list() 関数の第1引数にはコンテキストを指定する。第2引数には、デバイス一覧を格納する配列を指定する。libusbでは、個々のUSBデバイスを libusb_device という構造体型で表すので、そのポインタを格納する配列を指定することになる。

libusb_get_device_list() 関数の返り値は、取得したデバイスの数。取得に失敗したら -1 が返る。

以上より、デフォルトのコンテキストではこんな感じにデバイスの一覧を取得すればよいとわかる。

libusb_device **list;
int cnt = libusb_get_device_list(NULL, &list);

デバイス一覧から関心のあるデバイスを見つけて開く

libusb_get_device_list() 関数で取得したデバイスの一覧のうち、アプリケーションで操作したいUSBデバイスだけを探して開くために、デバイス一覧を走査する。走査にあたって目的のデバイスを特定する際には、USBの仕様で定められている「デバイスのデスクリプタ」の情報を使う。

「デバイスのデスクリプタ」は、USBデバイスに関する一般的な情報を示すもので、USB 2.0(1.0および1.1も包含されてる)でもUSB 3.0でも仕様書のセクション9.6.1で規定されている。libusbでは libusb_device_descriptor という構造体で表されている。この構造体のフィールドは下記で参照できる。

libusb_device_descriptor のフィールドうち、デバイスを特定するのに使えそうなのは下記の4つ。なお、USB-IFというのは “USB Implementers Forum” のことで、要するにUSBの規格を策定している団体。

フィールド 説明
idVendor USB-IFから割り当てられている識別子(Vendor ID
idProduct Vendor IDを持っているベンダーが製品に割り当てるID(Product ID)
iManufacturer ベンダーについて説明した文字列
iProduct 製品について説明した文字列

デバイスを特定するには、取得したデバイス一覧に含まれている各デバイスについてデスクリプタを取り出し、上記のフィールドの値を読み出して、それを調べればよい。デスクリプタの取り出しには libusb_get_device_descriptor という関数が用意されているので、だいたい以下のような流れでやっていく。

struct libusb_device_descriptor desc;
libusb_device_handle *handle;

for (i = 0; i < cnt; i++) {
  libusb_device *dev = list[i];
  libusb_get_device_descriptor(device, &desc);
  libusb_open(devive, &handle)

  /* handle と desc を使って何かする */
  ...
}

上記には、libusb_device_handle 型の handle という変数へのポインタが出てくるが、これはデバイスハンドラと呼ばれており、デバイスに対する実際のI/O操作などの対象になる。デバイス一覧として得られるのが libusb_device 型の変数へのポインタたちで、これを直接いじれそうな気がするかもしれないが、こちらは内部構造を参照できない。デスクリプタを参照する際も、libusb_device 型の変数へのポインタを libusb_open() して得られる libusb_device_handle 型の変数が必要になる。

じゃあ、libusb_device 型の変数は何のためにあるかというと、これにはデバイスの参照カウンタという役割がある。参照カウンタは、デバイス一覧を取得した時点で「1」が割り当てられ、そのあとはプログラマーが増やしたり減らしたりできる。参照カウンタがゼロになったデバイスはそのセッションで破棄される。

ちなみに、Vendor IDとProduct IDを指定して一気にデバイスハンドラを得られる libusb_open_device_with_vid_pid() という関数もある。これにコンテキストとVendor IDとProduct IDを引数に指定して実行すれば、返り値として libusb_device_handle 型の変数へのポインタが得られる。

int vendorId, productId;
libusb_device_handle *handle;

handle = libusb_open_device_with_vid_pid(NULL, vendorId, productId)

開いたデバイスで何かする(メッセージをやり取りする)

デバイスハンドラとデスクリプタを手に入れたら、いよいよそのデバイスを使ってやりたいことを書く。たとえば iProduct を標準出力に表示するなら、全体はこんな感じになる。

#include <stdio.h>
#include <libusb-1.0/libusb.h>

main () {
  libusb_device **list;
  struct libusb_device_descriptor desc;
  libusb_device_handle *handle;
  int i, ret;
  unsigned char text[512];

  /* 初期化 */
  libusb_init (NULL); 
  
  /* デバイスの一覧を取得 */
  int cnt = libusb_get_device_list(NULL, &list); //
  
  /* 一覧を走査して関心のあるデバイスを開く */
  for (i = 0; i < cnt; i++) {
    libusb_device *dev = list[i];
    libusb_get_device_descriptor(device, &desc);
    ret = libusb_open(devive, &handle)
  
    /* handle と desc を使って何かする */
    if (ret == 0) {
      libusb_get_string_descriptor_ascii(
          handle, desc.iProduct, text, sizeof(text));
      printf ("%s\n", text);
    }
  }

  /* クロージング */
  ...
}

libusb_get_string_descriptor_ascii() は、文字列のデスクリプタをASCII表現で取り出してくれるありがたいlibusbの関数。USBの仕様では、iProductiManufacturer のような文字列のデスクリプタはUTF-16LEで格納されることになっており、多バイト文字も使えるようになっているんだけど、そうはいってもASCIIが利用されるわけで、そのままC言語で出力したりデバイス特定のために文字列比較したりするのが単純にめんどくさい。この関数を使えば、取得したバイト列をそのままASCII文字列として扱える。

あと上記で注意しないといけないのは、libusb_open() の戻り値でエラーハンドリングしていること。もっとちゃんとしたエラー処理をするほうがよいだろうけど、とりあえず libusb_open() はデバイスが接続されていて権限やメモリの割り当てに成功した場合(つまり返り値が0の場合)にのみデバイスハンドルを有効にするので、ここだけはエラー処理しないとデバイスハンドラを使う関数(上記の例では libusb_get_string_descriptor_ascii())でだいたいセグフォしてしまう。

クロージング

終了は基本的に以下の手順に従う。

  1. 開いたデバイスを閉じる
  2. デバイスの一覧をクリアする
  3. セッションを閉じる

ただし、実行する関数は libusb_free_device_list()libusb_exit() の2つでよいらしい。これは、 libusb_free_device_list() の第2引数に1を設定することで上記の1と2を同時にやってくれるから、ということらしいのだけど、第2引数に1以外を設定してもこの程度のアプリケーションでは特に問題もなく終了するっぽいので、よくわからない。libusb_device 型の変数の参照カウンタが減らず、生きたままになることで何か問題が起きるケースがあるのだろうけれど…。

なお、libusb_exit() にはコンテキストを指定する必要がある(これにより他のコンテキストのセッションは終了されない)。これはもちろんデフォルトのコンテキストを使っているなら NULL でいい。

全体はこんな感じになる。

#include <stdio.h>
#include <libusb-1.0/libusb.h>

main () {
  libusb_device **list;
  struct libusb_device_descriptor desc;
  libusb_device_handle *handle;
  int i, ret;
  unsigned char text[512];

  /* 初期化 */
  libusb_init (NULL); 
  
  /* デバイスの一覧を取得 */
  int cnt = libusb_get_device_list(NULL, &list); //
  
  /* 一覧を走査して関心のあるデバイスを開く */
  for (i = 0; i < cnt; i++) {
    libusb_device *dev = list[i];
    libusb_get_device_descriptor(device, &desc);
    ret = libusb_open(devive, &handle)
  
    /* handle と desc を使って何かする */
    if (ret == 0) {
      libusb_get_string_descriptor_ascii(
          handle, desc.iProduct, text, sizeof(text));
      printf ("%s\n", text);
    }
  }

  /* クロージング */
  libusb_free_device_list(list, 1);
  libusb_exit(NULL);
  return 0;
}

「handle と desc を使って何かする」の部分でもっといろいろやるには、libusb_control_transfer()libusb_bulk_transfer() といった関数を使う。これらの関数の使い方はUSBデバイスのほうの実装によるので、ここでは省略。

コンパイルと実行

以下はDebianに自作のV-USB機器を指したときの例。

$ gcc -O -Wall libusb-sample.c -lusb-1.0
$ ./a.out


Template

$

「Template」というのが、V-USBでデバイスを作ったときにデフォルトで利用されるiProductの値。iProductが設定されているデバイスは意外と少ない。

Discussion