Linux USBドライバー開発(後編)デバイス挿入時のKernel処理フローの詳細解説

  1. USBバスドライバー(ホストドライバー)の役割

USBバスドライバーは、LinuxカーネルにおけるUSBサブシステムの核心的なコンポーネントです。その主要な機能は次の3点に集約されます。

1.1 USBデバイスの識別

デバイス識別プロセスは、複数のステップから構成されています。まず、カーネルは新デバイスに一意のアドレスを割り当てます。次に、割り当てられたアドレスをデバイスに通知するSET_ADDRESSコマンドを送信します。最後に、デバイス記述子を取得するための要求を発行し、デバイスの詳細情報を取得します。

1.2 デバイスドライバーの検索とインストール

カーネルは、識別したデバイスの情報に基づいて、適切なデバイスドライバーを検索します。ドライバーが見つかった場合、当該ドライバーをカーネルにロードし、デバイスとの関連付けを行います。

1.4 USBリード/ライト関数の提供

USBデバイスとのデータ転送を実現するため、読み取りおよび書き込みのための標準的な関数インターフェースをアプリケーション層に提供します。

  1. USBデバイスの動作フロー

LinuxカーネルにはUSBドライバーの基本実装が標準で付属しています。実験として、USBキーボードを開発ボードに接続し、カーネルログを確認すると、重要な情報が表示されます。

カーネルログ出力の起点はdrivers/usb/core/hub.cの2186行目付近です。このhubとは、USBホストコントローラーのハブ機能であり、複数のUSBポート统一的に管理するための機構です。

2.1 デバイス挿入時のカーネル処理シーケンス

hub_irq_handler
    └─> khubd_workqueue_dispatch()
          └─> hub_monitor_task()
                └─> hub_process_events()
                      └─> hub_port_connected()
                            ├─> usb_allocate_device(hub_dev, bus, port)
                            │     dev->sysdev.bus = &usb_bus_type;
                            ├─> assign_device_number(device);
                            ├─> hub_port_initialization()
                            │     ├─> hub_set_address_request();
                            │     ├─> usb_fetch_descriptor(device, 8);
                            │     └─> usb_fetch_full_descriptor();
                            └─> usb_register_new_device()
                                  ├─> usb_enumerate_configurations();
                                  ├─> usb_parse_all_descriptors();
                                  └─> device_model_registration()
                                        usb_interface_registration()
                                        usb_driver_matching()

2.2 詳細な処理段階

ハードウェア層:ホストコントローラー

USBデバイスが物理的に接続されると、ホストコントローラーが接続検知信号を検出します。これはハードウェア割り込みとして処理され、カーネルのUSBサブシステムに通知されます。

カーネル層:USBコア層

USBコア層は、データ構造とAPI関数群から構成されています。上位層に対してはデバイスドライバーのためのプログラミングインターフェースを提供し、下位層に対してはホストコントローラーとのインターフェースを抽象化します。

カーネル層:デバイスドライバー層

実際のデバイス制御は、個別のデバイスドライバーによって実装されます。driverモジュールのprobe関数とusb_register_driverマクロが中心的な役割を果たします。

デバイスアドレスの割り当てと登録

割り込み処理の過程で、カーネルはデバイスに固有のアドレスを割り当てます。usb_device構造体がUSBバスに登録され、デバイスモデルに統合されます。

デバイス記述子の取得と解析

カーネルはデバイスに対して記述子要求を発行し、デバイスに関する基本情報を取得します。この情報にはベンダーID、、製品ID、デバイスクラスなどが含まれます。

コンフィグレーション記述子の取得と解析

デバイス記述子に続き、コンフィグレーション記述子が取得されます。この記述子には、インターフェース数、エンドポイント構成、電力要件などの情報が含まれています。

インターフェースの登録

USBデバイスは複数のインターフェースを持つことができます。例えば、ウェブカメラはビデオストリーミング用とオーディオ録音用の2つのインターフェースを持つ場合があります。各インターフェースは独立したusb_interface構造体として登録され、device_addを通じてデバイスモデルに追加されます。

インターフェースドライバーのマッチング

カーネルはusb_device_match関数を呼び出して、適切なドライバーを見つけます。マッチングが成功した場合、ドライバーのprobe関数が呼び出され、実際のデバイス初期化処理が開始されます。

  1. USB記述子の階層構造

USBデバイスには、階層的に整理された複数の記述子が存在します。この階層構造を理解することは、USBプログラミングにおいて極めて重要です。

3.1 記述子の階層

USBデバイスの最上位にはデバイスが配置されます。デバイス記述子には、デバイスのベンダーID、製品ID、USBバージョンなどの基本情報が格納されています。デバイスは複数のコンフィグレーションを持つことができ、通常は動作モードごとに異なるコンフィグレーションが定義されます。各コンフィグレーションには、複数のインターフェースが含まれます。インターフェースは、デバイスの機能単位を表し、例えばサウンドデバイスでは再生インターフェースと録音インターフェースが別々に定義されます。各インターフェースには、複数のエンドポイントが関連付けられます。エンドポイントは、USBデータ転送の実際の通信 채널です。

3.2 デバイス記述子の構造

include/linux/usb/Ch9.hファイルに、デバイス記述子の定義が記載されています。

struct usb_device_descriptor {
    __u8  bLength;                  // 記述子サイズ(18バイト固定)
    __u8  bDescriptorType;          // 記述子タイプ(DEVICE = 0x01)
    __le16 bcdUSB;                  // USB仕様バージョン(例:0x0200はUSB 2.0)
    __u8  bDeviceClass;            // デバイスクラスコード
    __u8  bDeviceSubClass;         // デバイスサブクラスコード
    __u8  bDeviceProtocol;         // デバイスプロトコルコード
    __u8  bMaxPacketSize0;         // コントロールエンドポイント0の最大パケットサイズ
    __le16 idVendor;               // ベンダーID(USB-IF登録)
    __le16 idProduct;              // 製品ID(ベンダー割り当て)
    __le16 bcdDevice;              // デバイスバージョン
    __u8  iManufacturer;           // メーカー名文字列インデックス
    __u8  iProduct;                // 製品名文字列インデックス
    __u8  iSerialNumber;           // シリアル番号文字列インデックス
    __u8  bNumConfigurations;      // 利用可能なコンフィグレーション数
} __attribute__ ((packed));

3.3 コンフィグレーション記述子の構造

struct usb_config_descriptor {
    __u8  bLength;                  // 記述子サイズ
    __u8  bDescriptorType;          // 記述子タイプ(CONFIGURATION = 0x02)
    __le16 wTotalLength;            // このコンフィグレーションの全記述子サイズ
    __u8  bNumInterfaces;           // このコンフィグレーションのインターフェース数
    __u8  bConfigurationValue;      // SetConfigurationコマンドへの引数値
    __u8  iConfiguration;           // コンフィグレーション名文字列インデックス
    __u8  bmAttributes;             // 電源特性(バス電源、自己給電など)
    __u8  bMaxPower;                // 最大消費電流(単位:2mA)
} __attribute__ ((packed));

3.4 インターフェース記述子の構造

struct usb_interface_descriptor {
    __u8  bLength;                  // 記述子サイズ
    __u8  bDescriptorType;          // 記述子タイプ(INTERFACE = 0x04)
    __u8  bInterfaceNumber;         // このコンフィグレーション内のインターフェース番号
    __u8  bAlternateSetting;        // 代替設定番号(同一インターフェースの異なるモード)
    __u8  bNumEndpoints;            // このインターフェースで使用するエンドポイント数
    __u8  bInterfaceClass;          // インターフェスクラス(HID、AUDIOなど)
    __u8  bInterfaceSubClass;       // インターフェースサブクラス
    __u8  bInterfaceProtocol;       // インターフェースプロトコル
    __u8  iInterface;               // インターフェース名文字列インデックス
} __attribute__ ((packed));

3.5 エンドポイント記述子の構造

各エンドポイントには、データ転送の方向、転送タイプ、同期モードなどの属性が定義されます。エンドポイント0はコントロール転送専用であり、すべてのUSBデバイスに必須です。

  1. USBマッチングメカニズムの詳細

4.1 デバイスマッチング関数の実装

USBデバイスのマッチングは、usb_device_match関数によって処理されます。この関数は、デバイスとドライバーの互換性を判定するための中心的な役割を果たします。

static int usb_device_match(struct device *dev, struct device_driver *drv)
{
    /* USBデバイスとUSBインターフェースは別々に処理される */
    
    if (is_usb_device(dev)) {
        struct usb_device *udev;
        struct usb_device_driver *udrv;

        /* インターフェースドライバーはデバイスとマッチングしない */
        if (!is_usb_device_driver(drv))
            return 0;

        udev = to_usb_device(dev);
        udrv = to_usb_device_driver(drv);

        /* id_tableもmatch関数も持たないドライバーは常にマッチング */
        if (!udrv->id_table && !udrv->match)
            return 1;

        return usb_driver_applicable(udev, udrv);

    } else if (is_usb_interface(dev)) {
        struct usb_interface *intf;
        struct usb_driver *usb_drv;
        const struct usb_device_id *id;

        /* エネルギードライバーはインターフェースとマッチングしない */
        if (is_usb_device_driver(drv))
            return 0;

        intf = to_usb_interface(dev);
        usb_drv = to_usb_driver(drv);

        /* 静的なIDテーブルによるマッチングを試行 */
        id = usb_match_id(intf, usb_drv->id_table);
        if (id)
            return 1;

        /* 動的IDによるマッチングを試行 */
        id = usb_match_dynamic_id(intf, usb_drv);
        if (id)
            return 1;
    }

    return 0;
}

4.2 マッチング処理の詳細

マッチング処理は、デバイスの種類に応じて異なるロジックを採用しています。USBデバイスの場合、デバイスドライバーとの照合が行われ、USBインターフェースの場合、インターフェースドライバーとの照合が行われます。照合には、静的なid_table、match関数、動的IDの3つの手法が段階的に適用されます。

  1. USBドライバーのprobe関数

5.1 プローブ処理の実例

UVC(USB Video Class)ドライバーを例に挙げると、probe関数は複雑な初期化処理を行います。video_device構造体の割り当てから始まり、最終的にvideo_register_deviceによるデバイスノードの作成までの一連の処理が実施されます。

static int uvc_probe_function(struct usb_interface *interface,
                               const struct usb_device_id *id)
{
    struct uvc_device *uvc_dev;
    struct video_device *vdev;
    int ret;

    /* video_device構造体のメモリ割り当て */
    uvc_dev = kzalloc(sizeof(*uvc_dev), GFP_KERNEL);
    if (!uvc_dev)
        return -ENOMEM;

    /* 初期化チェーンの開始 */
    ret = uvc_init_chain(uvc_dev, interface);
    if (ret < 0)
        goto free_device;

    /* ビデオデバイスの設定 */
    vdev = &uvc_dev->vdev;
    vdev->v4l2_dev = &uvc_dev->stream.dev->v4l2_dev;
    vdev->fops = &uvc_video_operations;
    vdev->ioctl_ops = &uvc_ioctl_handlers;
    vdev->release = uvc_video_release;
    vdev->queue = &uvc_dev->stream.vidq.vidq;

    /* V4L2デバイスとしての登録 */
    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    if (ret < 0)
        goto free_uvc;

    return 0;
}
  1. 重要な概念の補足

6.1 デバイスとインターフェースの階層関係

USBデバイスには、usb_device構造体とusb_interface構造体という2つの重要なデータ構造が存在します。usb_deviceはデバイス全体を表現し、製造情報、アドレス、接続状態などの全体情報を管理します。一方、usb_interfaceはデバイスの機能単位を表現し、特定のドライバーにバインドされる単位となります。

USB規格の考え方として、1つの物理デバイスが複数の論理機能を持つことができます。例えば、複合USB機器は Mass Storage、HID、Bluetooth の複数のインターフェースを持つことが可能です。各インターフェースは独立したドライバーに制御されます。

6.2 デバイスドライバーとインターフェースドライバーの違い

usb_device_driverは、USBデバイス全体を単一ユニットとして制御します。USBデバイス全体を単一のドライバーで管理する必要がある場合に使用されます。usb_driverは、usb_interface単位でのマッチングが行われ、USBデバイスの各機能ごとに個別のドライバーがバインドされます。

6.3 なぜUSBはデバイスツリーを使用しないのか

USBは熱挿拔を前提とした規格であり、接続されるデバイスは実行時に動的に変化します。デバイスツリーは静的なハードウェア記述に適していますが、実行時に接続状態が変化するデバイスには適していません。USBデバイスが接続されると、LinuxのUSBサブシステムは自動的にデバイス検出とドライバーマッチングを行います。

一方、PCIeやplatformデバイスのように、システム起動時にハードウェア構成が決定している場合は、デバイスツリーによる静的記述が適しています。組み込みシステムにおけるオンチップペリフェラルなどは、dtsファイルで記述するのが一般的です。

  1. USBホットプラグメカニズム

7.1 ハードウェアレベルでの検知

USBポートには、デバイス接続を検知するための専用信号線が存在します。VBUS電源ラインの電圧変化、D+およびD-データラインの状態変化が、接続検知のトリガーとなります。ホストコントローラーはこれらの信号を継続的にモニタリングし、変化を検出するとカーネルに通知します。

7.2 ソフトウェアレベルでの処理

デバイスの検出と列挙

デバイス接続が検知されると、USBホストコントローラーはデバイスにリセット信号を送信します。これによりデバイスが既知の状態に遷移し、標準的な通信が可能になります。リセット完了後、ホストはデバイス記述子を取得し、デバイスの種類と機能を特定します。

ドライバーのロード

取得した記述子情報に基づいて、カーネルは適切なドライバーを検索します。デバイスとドライバーのマッチングが成功すると、ドライバーのprobe関数が呼び出され、デバイスの初期化が行われます。ストレージデバイスの場合はファイルシステムがマウントされ、入力デバイスの場合はイベントハンドラーが登録されます。

イベント通知

デバイスの挿入・移除時に、カーネルはhotplugイベントを生成します。udevデーモンがこのイベントを検知し、デバイスのPermissions設定、デバイスノードの作成、必要なスクリプトの実行などの後処理を行います。

7.3 Linuxカーネルにおけるホットプラグ対応サブシステム

USB Coreサブシステムは、デバイスの検出、列挙、基本通信を処理します。USB Host Controller Drivers(HCD)は、EHCI、OHCI、UHCIなどの具体的なハードウェア実装を抽象化し、Electrical信号処理とデータ転送を担当します。udevはユーザー空間のデーモンとして、カーネルからのデバイスイベントを処理し、動的なデバイス管理を実現します。

7.4 ホットプラグ処理の全体フロー

デバイス挿入時の処理フローは、Electrical信号検知から始まります。ホストコントローラーがVBUS電圧変化を検知すると、デバイスリセット信号を送信します。リセット完了後、デバイス記述子の取得と解析が行われ、ドライバーのマッチングとprobe関数の呼び出しが実施されます。

デバイス移除時の処理フローは、同様にElectrical信号検知から始まります。VBUS電圧降下を検知すると、ホストコントローラーは移除信号を送信します。カーネルはドライバーのdisconnect関数を呼び出し、リソースを解放します。最後にudevに通知され、クリーンアップ処理が完了します。

7.5 実践における注意点

ストレージデバイスを移除する際には、データの完全性を確保するためにアンマウント操作を事前に実行する必要があります。書き込み中のデータを中断すると、ファイルシステム破損の原因となります。また、ホットプラグ操作においては電源管理を適切に行い、Electricalサージやデバイス損傷を防止することが重要です。

タグ: linux-kernel usb-driver hotplug device-driver usb-subsystem

5月14日 18:09 投稿