Edited at
NetBSDDay 18

USBのペリフェラル側をサポートするフレームワーク: 経過報告

More than 3 years have passed since last update.

このエントリーは、NetBSD Advent Calendar 2015の 18日目の記事です。


はじめに

このエントリーの話題は、NetBSD箱を USBデバイスとして動作させるためのフレームワークです。

NetBSDが動作しているデバイスをUSBでPCなどのホストに接続すると、そのデバイスがUSBストレージになったり、USBシリアルに見えたり、という機能を実現します。USBホストじゃなくてUSBデバイスとして接続するので、それ用のインターフェイスを持っている必要がありますから、主にSoCを搭載した携帯デバイスや組込み向けプラットフォームなどが対象になります。

長年、個人的ToDoリストに浮遊していた案件なんですが、一念発起して今年の夏頃に手を付けました。大体どんな事をやりたいのかは、7月の NetBSD BoFで話しました。その時は作業を始めたばかりで、簡単なデモをする所までも到達しなかったので、経過報告という形でした。

この記事でも依然として経過報告です。先は長い。


何事かを成し遂げたいなら、やり終えるまでそれを人に話してはならない

まだちっとも動いてないのに BoFで発表したのは、披露することで自分へのプレッシャーにして、最後までやり切ろうとしたからでした。でも、それは逆効果であると、このTEDプレゼンで知りました。

TED: Keep your goals to yourself

最初にこのプレゼンを見た時は懐疑的な印象を持った(誰にも言わなければ無かったことにできちゃうし、プレッシャーがあった方が捗るんじゃね?)のですが、実際BoFでの発表の後ずっと停滞してしまったので、なるほど一理あるかなと。

とか言いつつ、人生の真理に気付いたはずなのに性懲りもなくまた途中経過の発表をやっているのは、他にすぐ書けるネタがなかったからです。

停滞から抜けだすきっかけも欲しかったし。

まあここは、Advent Calendar駆動の力を信じてみましょう。


OpenBSD の ペリフェラル側USBサポート

プロジェクトの開始時に、先行案件がないかNetBSDのコードベースを探ってみたところ、arch/arm/xscale に PXA270 のUSBデバイスコントローラのドライバーが存在しているのを発見しました。 ただし、USBデバイスとして動作させるためのものではなく、USBホストコントローラを動作させるために必要な部分だけのようでした。(USB A-Bポートでホストとデバイスを切り替えて使えるようになっている)

このコードにOpenBSDのライセンスが付いていたので、今度はOpenBSDのソースツリーを探ってみると、Zaurus をUSBデバイスとして接続するためのコードが揃っていましたので、プロジェクトの最初のステップとして、まずはOpenBSDからこれを移植して動かしてみることにしました。

OpenBSDで実装されているのは、次のようなものです。

1. USBデバイスとしての基本動作 (コンフィギュレーションなど)。 usbf というデバイスとして実装されています

2. PXA270のUSBデバイスコントローラーのドライバー

3. usbfにぶらさがってCDC EEM (Ethernet Emulation Model) (pdf)を実現するネットワークインターフェイス cdcef

Linux Zaurus が提供している、USB経由で母艦に接続する機能と同等のものを提供するための必要最小限が実装されている格好になります。かと言って、Zaurus専用とか CDC EEMだけ動かすという実装でなくて、ちゃんと拡張可能なようになっていてありがたいのですが、2007年のOpenBSDへの登場以降、サポートするUSBデバイスの種類も、デバイスコントローラも増えていないようです。需要なかったのかな。

usbf/ファンクションドライバー(CDC EEM など)/USBデバイスコントローラー の三層の間は、autoconf(9) で接続しています。


conf/ZAURUS

# USB controllers

pxaudc0 at pxaip? # USB Device Controller
# USB function support
usbf* at pxaudc? # USB logical device
cdcef* at usbf? # CDC ethernet function


usbf

フレームワークの中核です。

デバイスがホストに接続されて認識されるまでのプロトコルを処理します。

デバイスがどんな機能を持ったUSBデバイスであるかは、コンフィギュレーション・ディスクリプター、ストリング・ディスクリプター、インターフェイス・ディスクリプターなどによってホストに通知されますが、usbf自体は何にでもなれるデバイスなので、それらの情報を持っていません。下位のデバイスによってあらかじめ設定された情報を使って通信します。

USBパケットの送受信は、デバイスコントローラーのドライバーが行いますが、デバイスコントローラーの仕様はプラットフォームによって千差万別なの1で、例によって関数テーブルを介して呼出すようになっています。


usbfvar.h

struct usbf_pipe_methods {

usbf_status (*transfer)(struct usbf_xfer *);
usbf_status (*start)(struct usbf_xfer *);
void (*abort)(struct usbf_xfer *);
void (*done)(struct usbf_xfer *);
void (*close)(struct usbf_pipe *);
};

struct usbf_bus_methods {
usbf_status (*open_pipe)(struct usbf_pipe *);
void (*soft_intr)(void *);
usbf_status (*allocm)(struct usbf_bus *, usb_dma_t *, u_int32_t);
void (*freem)(struct usbf_bus *, usb_dma_t *);
struct usbf_xfer *(*allocx)(struct usbf_bus *);
void (*freex)(struct usbf_bus *, struct usbf_xfer *);
};



CDC EEM

sys/dev/usb/if_cdcef.c で実装されています。

主な機能は:

- attach 時に、USBコンフィギュレーションに必要な諸々を設定

- ネットワークスタックから渡された送信データを INパイプへ流す。

- OUTパイプから受け取った受信データを mbuf に組んでネットワークスタックへ渡す。

- その他、ネットワークインターフェイスとしての色々 (ioctl とか)

で、かなりシンプルです。

CDC EEM デバイスが接続された時のホスト側の対応は、ネットワークインターフェイス cdce が担当しますが、これはNetBSDにもあります。 (2004年に OpenBSDから移植)


USBデバイスコントローラー

OpenBSDでは、Zaurusに載っているPXA270のUSB Client Controller をサポートしています。

PXA250では USB Device Controller と呼ばれていますが、名前を変えたんですね。略称は PXA270でも UDC のままです。


  • USB 1.1 Full-speed 対応。 2.0 以降には対応していません。

  • 各エンドポイント用のFIFOを介して送受信します。

  • USBリクエストのうちいくつかは、コントローラーが自動的に応答し、CPUの介入を必要としません。 (SET_ADDRESSなど)

  • USB OTG サポート

ドライバーの基本的な動作は、




受信


割り込みが入ったらステータスを確認して、FIFOにデータがあったら取り出して usbf に通知


送信


FIFOに空きがあればデータを突っ込む。送信完了は割り込みで確認




です。DMAにはまだ対応していません。

また、現状では コントロール転送とバルク転送だけがサポートされています。


ネットワーク系ドライバーをOpenBSDからNetBSDに移植するときのメモ

OpenBSDからの移植は初めての経験だったので、気付いた点を書いておきます。

たしか、@h_kenken のToDoリストに myx(4)の移植が入っているはず。参考になるかな?


struct ifnet

struct ifnet 内の (* if_なんたら)() メソッド2の種類が異ります。

OpenBSD:


if_var.h

    /* procedure handles */

SRPL_HEAD(, ifih) if_inputs; /* input routines (dequeue) */

/* output routine (enqueue) */
int (*if_output)(struct ifnet *, struct mbuf *, struct sockaddr *, struct rtentry *);

/* link level output function */
int (*if_ll_output)(struct ifnet *, struct mbuf *, struct sockaddr *, struct rtentry *);
/* initiate output routine */
void (*if_start)(struct ifnet *);
/* ioctl routine */
int (*if_ioctl)(struct ifnet *, u_long, caddr_t);
/* timer routine */
void (*if_watchdog)(struct ifnet *);
int (*if_wol)(struct ifnet *, int);


NetBSD:


if.h

    /*

* Procedure handles. If you add more of these, don't forget the
* corresponding NULL stub in if.c.
*/

int (*if_output) /* output routine (enqueue) */
(struct ifnet *, struct mbuf *, const struct sockaddr *,
struct rtentry *);
void (*if_input) /* input routine (from h/w driver) */
(struct ifnet *, struct mbuf *);
void (*if_start) /* initiate output routine */
(struct ifnet *);
int (*if_ioctl) /* ioctl routine */
(struct ifnet *, u_long, void *);
int (*if_init) /* init routine */
(struct ifnet *);
void (*if_stop) /* stop routine */
(struct ifnet *, int);
void (*if_watchdog) /* timer routine */
(struct ifnet *);
void (*if_drain) /* routine to release resources */
(struct ifnet *);

この関数ポインター群を適切に初期化しておかないと、思わぬところでカーネルが死にます。特に、OpenBSDになくてNetBSDにはある (* if_init)()に嵌りました。

NetBSDでは、ネットワークスタックの上位層が適宜 (* if_init)()を呼んでくれますが、OpenBSDでは、


if_cdcef.c

        if (!(ifp->if_flags & IFF_RUNNING))

cdcef_init(sc);

みたいなコードが各所に必要ということでしょうかね。

NetBSDでは、 使わないならif_nullなんたらという関数を入れておくのが正しいみたいだけど、if_watchdog は 色んな所でNULLかどうか見てたりするのも、ちょっと良くわからないですね。


ml_enqueue

OpenBSDには、struct mbuf_list と ml_enqueue()関数がありますが、NetBSDにはありません。

デバイスから上がってきたデータをネットワークスタックに突っ込む所は、OpenBSDでは、


if_cdcef.c

    m->m_pkthdr.len = m->m_len = total_len;

bcopy(sc->sc_buffer_out, mtod(m, char *), total_len);

ml_enqueue(&ml, m);

......

s = splnet();
if_input(ifp, &ml);
splx(s);


NetBSDでは、


if_cdcef.c

    m->m_pkthdr.rcvif = ifp;

m->m_pkthdr.len = m->m_len = total_len;
bcopy(sc->sc_buffer_out, mtod(m, char *), total_len);

......

if (m != NULL) {
s = splnet();
ifp->if_input(ifp, m);
splx(s);
}


みたいになります。 ifp->if_inputether_ifattach()ether_input に初期化されています。


autoconf(9) の違い

NetBSDでは、数年前に softc3の先頭にstruct device を置くのをやめて、struct device と softc を分離したり、cfなんたら構造体を定義するためのマクロが導入されたりしましたが、OpenBSDの方はそういう変更はなくて、昔のままのようです。


OpenBSD

int   usbf_match(struct device *, void *, void *);

void usbf_attach(struct device *, struct device *, void *);

struct cfattach usbf_ca = {
sizeof(struct usbf_softc), usbf_match, usbf_attach
};

struct cfdriver usbf_cd = {
NULL, "usbf", DV_DULL
};



NetBSD

int  usbf_match(device_t, cfdata_t, void *);

void usbf_attach(device_t, device_t, void *);
extern struct cfdriver usbf_cd;

CFATTACH_DECL3_NEW(usbf, sizeof(struct usbf_softc),
usbf_match, usbf_attach, NULL, NULL, NULL, NULL, 0);


のような書き換えが要ります。

また、config_found 系の関数の戻りも softc でなくて device_t なので、USBファンクションドライバーを見つける部分は、


OpenBSD

    struct device *dv;

dv = config_found(parent, &uaa, NULL);
dev->function = (struct usbf_function *)dv;



NetBSD

   device_t dv;

dv = config_found(parent, &uaa, NULL);
dev->function = (struct usbf_function *)device_private(dv);



struct arpcom

NetBSDには struct arpcom (netinet/if_ether.h) はなくて、struct ethercom (net/if_ether.h) が相応します。

また、ether_ifattach(9) に2番目の引数があります。


他にもあった気がする

のですが、思い出せないのでコードを整理した時にでも追加します。

とりあえず現状では、PXA250 ベースの評価ボードをUSBで母艦と接続すると、cdcef が生えて通信できる、という所まではできるようになりました。


PXA250 UDC

OpenBSDでは PXA270の USB Client Controller をサポートしていますが、手持ちにNetBSDがすぐ動作する PXA270なプラットフォームがなかったので、PXA250ベースの評価用ボードを使いました。

このボードは 古き良き USB B ポートが付いているので、USBホストとの共用とかを考えなくていいのも助かりました。

同じPXAなので、似たようなものだろうと思ったのですが、意外と苦戦しました。

一番大きな違いは、PXA250ではエンドポイント毎に使い道があらかじめ決っている点です。

転送種別
向き
エンドポイント

コントロール

0

バルク
IN
1, 6, 11

バルク
OUT
2, 7, 12

アイソクロナス
IN
3, 8, 13

アイソクロナス
OUT
4, 9, 14

インタラプト
IN
5, 10, 15

PXA270にはこのような制限はないので、OpenBSDのフレームワークではファンクションドライバーがどのエンドポイントを使うか決めるようになっていましたが、使いたい転送種別に対して利用可能なエンドポイントをコントローラードライバーに問合せるように、フレームワークから変更する必要があります。

現状は、CDC EEMが使用するエンドポイント番号をいじって誤魔化してますが。


今後の作業

フレームワークとして充実させていく


もっと多くのデバイスクラスをサポートする

ソフトウェア次第で何にでもなれるのが売りのフレームワークなので、色々増やしたいです。

今すぐにでもあると喜ばれるのは、

- USBシリアル

- umass(4) デバイス

- DFU (pdf) (ファームウェアアップデート用プロトコル)

あたりでしょうか。

USBシリアルデバイスになれると、シリアルポートがない/出ていないデバイスでシリアルコンソールを作ることができるので、うれしい。

最近、NetBSDのホスト側は割り込みを使わずポーリングで動けるようになったそうなので、同じようにやれば、ペリフェラル側USBシリアルもブート直後やDDBでも使えるようにできるでしょうか。


もっと多くのデバイスコントローラをサポートする

コントローラの仕様はデバイス毎に千差万別なので、色々なデバイスをサポートしてみると、usbfとコントローラーデバイスドライバーの間のインターフェイスの不足がわかると思います。

とりあえず、手元にある評価ボードとかNetwalkerとかZaurusとかを順にやって行きたいです。


USBファンクションの変更・追加・削除

カーネルは動かしたままで、別のUSBデバイスに生れ変れる。

USBのプロトコルにはそのような変化はないので、一旦ホストと切断して再接続することになります。

USBデバイスコントローラの機能として提供されていたり、プルアップ抵抗の制御で実現できたりしますが、デバイス依存なので、コントローラデバイスのメソッドテーブルに何か追加する必要があるでしょう。

切断・再接続ができないデバイス構成がもしあったら、ファンクションの変更・追加・削除の要求はエラーにするか、手動で切断されるまでペンディングにするのかな。コントローラデバイスを一旦ディスエーブルすれば良さそうな気もしますが。


マルチファンクション・デバイス

ペリフェラル側USBの機能はファンクション単位で定義することになるので、一つのデバイスに複数のファンクションを定義したら、自然にマルチファンクション・デバイスになって欲しいところです。インターフェイス・ディスクリプターは、持っているファンクションに応じて自動的に調整したい。

NetBSDのホスト側のUSBサポートは、マルチファンクションデバイスをあまりうまく扱えてなかったような記憶があります4が、色んなファンクションを勝手に組合せて作れるようになれば、ホスト側の対応も進めやすいかも知れませんね。


OTGサポート

最近のプラットフォームだと、ペリフェラル側USBは A-B ポートにつながっていることが多いです。従って、USBホストとポートを共有することになるので、切り替えのための仕組みも必要です。

最終的には USB OTGの仕様に則って、相手と接続した時にどちらがホストになるのか決められるようにします。


ファンクションをユーザーランドに実装

デバイスドライバーとしてUSBファンクションを実装するのに加えて、ユーザーランドのプログラムによっても実装できるようにします。


  1. USBペリフェラル制御用デバイス(/dev/usbp0 とか?)をオープン

  2. ioctlで エンドポイントの確保、コンフィギュレーションディスクリプタ等の構築を行う

  3. エンドポイント用デバイスノード(/dev/usbp0.03 とか?) をオープンする

  4. エンドポイント毎に送受信。USBパケットの解釈はユーザーランドで行う。

というような手順でどうかな、と考えています。


まとめ

USBのペリフェラル側をサポートするフレームワークをNetBSDに実装する、という最終目標に対して、 OpenBSDのコードを移植して、CDC EEM を使った通信ができる、という所まで来ました。

実は、この記事を書き始める前は、 cdcefが動かせてなかった5ので、Advent Calendar 駆動によって半歩位前進したことになります。

現状のコードは、github に置いてあります。

新な駆動力を見つけて最後まで完成させたいです。






  1. USBホストコントローラには、UHCI、OHCI、EHCIの標準規格がありますが、デバイス側のコントローラーにはそういうものはありません 



  2. コメントには procedure handlesと書かれていますが、それで通じるのかな 



  3. デバイスドライバーのデバイスインスタンス毎の内部データ領域。 fooデバイス用は、struct foo_softc と名付けるのが習慣。 



  4. 最近の動向はちゃんと追ってないので、間違ってたらスミマセン 



  5. つまり今夏から何も進んでなかった