osdev-jpでは、OS開発に有用な情報を収集し公開しています

View My GitHub Profile

Configure a device

このページでは USB デバイスを通信できるように設定する方法を説明する.

Set address to a device

デバイスに対し Device Slot の割り当てが済んだら,次は Default Control Pipe が使えるように設定する.

Input Context を用意して,Slot Context と Endpoint Context 0 を有効化する.

struct InputContext inp_ctx;
inp_ctx.input_control_context.add_context_flags = 0x00000003u;

struct InputContext の具体的な構造は xHCI Spec 6.2.5 を参照.

Add Context Flags は 32 ビット幅である.ビット位置と Slot Context および Endpoint Context が対応付いていて,有効化したい Context のビットを 1 にすると有効化される.ビット位置と Context の関係は次の通り.

ビット位置(DCI) 対応する Context
0 Slot Context
1 EP Context 0
2 EP Context 1 OUT
3 EP Context 1 IN
30 EP Context 15 OUT
31 EP Context 15 IN

今回は Slot Context と EP Context 0 を有効化したいので Add Context Flags に 3 を設定する.

次に各 Context に値を設定する.まず Slot Context から.

inp_ctx.slot_context.route_string = 0;
inp_ctx.slot_context.root_hub_port_num = port_index + 1;
inp_ctx.slot_context.context_entries = 1;
inp_ctx.slot_context.speed =
    (port_register_set[port_index].PORTSC >> 10) & 0xfu;

Route String は,ハブを介さずホストコントローラにつながったデバイスなら 0 となる.Route String の詳しいフォーマットは USB Spec Figure 8-32 を参照. Context Entries は有効にする Endpoint Context の数を設定する.今は Endpoint Context 0 だけ有効化するので 1 とする. Speed に設定すべき値は,規格書を読んでも複雑で良くわからないので,取りあえず PORTSC レジスタの Port Speed フィールドの値をコピーしておく.(コピーしておけばエラーは起きないようだ)

次に Endpoint Context 0 用の Transfer Ring を用意する.Transfer Ring は Command Ring と基本的に同じものである.

alignas(64) struct RingManager tr_ep0;
memset(tr_ep0.buf, 0, sizeof(tr_ep0.buf));
tr_ep0.cycle_bit = 1;
tr_ep0.write_index = 0;

次に Endpoint Context 0 の設定を行う.

int dci = 1;
inp_ctx.ep_contexts[dci-1].ep_type = 4;
inp_ctx.ep_contexts[dci-1].tr_dequeue_pointer = (uint64_t)tr_ep0.buf;
inp_ctx.ep_contexts[dci-1].dequeue_cycle_state = tr_ep0.cycle_bit;
inp_ctx.ep_contexts[dci-1].max_burst_size = 0;
inp_ctx.ep_contexts[dci-1].mult = 0;
inp_ctx.ep_contexts[dci-1].interval = 0;
inp_ctx.ep_contexts[dci-1].max_primary_streams = 0;
inp_ctx.ep_contexts[dci-1].error_count = 3;

if (inp_ctx.slot_context.speed == 4) // Super Speed
    inp_ctx.ep_contexts[dci-1].max_packet_size = 512;
else if (inp_ctx.slot_context.speed == 3) // High Speed
    inp_ctx.ep_contexts[dci-1].max_packet_size = 64;
else
    inp_ctx.ep_contexts[dci-1].max_packet_size = 8;

Max Burst Size * Mult はホストコントローラが一度に送受信するパケットの数を決める数値だ.ただ,Low/Full Speed のポートでは Max Burst Size は 0 でなければならない. キーボードは恐らく Low/Full Speed で実装されているだろうから,ここは 0 を設定しておく.

Interval はポーリング間隔を指定する.実際に有効な値が xHCI Spec Table 65 に示されているが,値の選択基準はよく分からない.筆者の環境だと 0 にしても取りあえず動作はした.

Max Packet Size はそのエンドポイントが一度に送受信できるパケットの大きさ(バイト数)である.Default Control Pipe ではポートのスピードによって設定できる値が決まる.

Slot Context と Endpoint Context 0 を設定したら Address Device Command を発行する.

struct AddressDeviceCommandTRB cmd;
cmd.input_context_pointer = (uint64_t)&input_context;
cmd.slot_id = assigned_slot_id;

ring_push(&cr, &cmd);
DOORBELL[0] = 0;

実行が完了するのを待つ.

while (er_front(0)->cycle_bit != er_cycle_bit);
while (er_front(0)->cycle_bit == er_cycle_bit) er_pop(0);

Get a Device descriptor

Address Device Command の実行が成功すると,そのデバイスの Default Control Pipe が使えるようになる. するとデバイスの情報を取得してデバイス種別(HID とか)を判別したり,必要な Endpoint に関する情報を得られる.

まずは Device Descriptor を得る.

#define DESC_DEVICE 1
#define DESC_CONFIGURATION 2
#define DESC_INTERFACE 4
#define DESC_ENDPOINT 5

uint8_t buf[128];
get_descriptor(assigned_slot_id, DESC_DEVICE, 0, buf, sizeof(buf));

get_descriptor は指定したディスクリプタを配列に読み込む関数で,何度か使うので関数とした.定義は後述. この関数の実行が終わると,バッファ buf に Device ディスクリプタが書き込まれた状態になっている.

Device ディスクリプタの構造は USB Spec Table 9-11 にある.

Device ディスクリプタのオフセット 4,5 はそれぞれクラスコード,サブクラスコードである. クラスコードの一覧は USB.org の Defined 1.0 Class Codes にある. それによればキーボードやマウスなどのデバイスを表す HID クラスのコードは 03h である.

uint8_t dev_class_code = buf[4];
uint8_t dev_subclass_code = buf[5];

dev_class_code は 0 となることがある. クラスコード 0 は Interface ディスクリプタ上のクラスコードを参照せよ,という意味だ. 同じデバイスが複数の機能を持つ場合,複数の Interface ディスクリプタが存在し,それぞれでクラスコードが設定される. (1 つの機能しかなくても,Device ディスクリプタではなく Interface ディスクリプタを使う場合もあるだろうが.)

HID Spec の 5.1 節によれば,HID デバイスの場合はクラスコードは必ず 0 となる. HID デバイスは Device ではなく Interface ディスクリプタでクラスコードを定義することになっているのである.

HID デバイスの Device ディスクリプタの中で最も重要な値は Configuration ディスクリプタの数だ. この値は Configuration ディスクリプタを読み込むのに必要なので変数で覚えておく.

uint8_t num_configs = buf[17];

Get Configuration descriptors

Device ディスクリプタの次は Configuration ディスクリプタを読む. Configuration ディスクリプタの構造は USB Spec Table 9-22 にある.

uint8_t boot_keyboard_config_value = 0;
uint8_t ep_key_in[7];

for (uint8_t desc_index = 0; desc_index < num_configs; ++desc_index)
{
    get_descriptor(assigned_slot_id, DESC_CONFIGURATION, desc_index, buf, sizeof(buf));
    uint16_t total_length = buf[2] | (uint16_t)buf[3] << 8;
    uint8_t configuration_value = buf[5];
    
    // 処理
}

USB デバイスは複数の Configuration を持つ. のちほど希望の Configuration を有効化するために Configuration Value が必要となるので変数に記録しておく.

また,目的のキーボード設定を探すために boot_keyboard_config_value 変数を初期化しておく. さらにそのキーボードのデータ入力用 Endpoint 設定をコピーしておく変数 ep_key_in を用意する.

Search a boot interface keyboard

Configuration ディスクリプタを読み込むと,その Configuration に属する Interface や Endpoint ディスクリプタも一緒に読み込まれる. 各ディスクリプタは buf に詰めて配置されるので,buf の先頭からディスクリプタのサイズを足しながら走査する. 次のコード片は,上の「処理」の部分に相当する.

uint8_t* p = buf + buf[0];
while (p < buf + total_length)
{
    uint8_t desc_type = p[1];
    if (desc_type == DESC_INTERFACE)
    {
        uint8_t interface_class_code = p[5];
        uint8_t interface_subclass_code = p[6];
        uint8_t interface_protocol = p[7];
        if (boot_keyboard_config_value == 0
            && interface_class_code == 3 // HID device
            && interface_subclass_code == 1 // Boot Interface
            && interface_protocol == 1) // Keyboard
        {
            boot_keyboard_config_value = configuration_value;
        }
    }
    else if (desc_type == DESC_ENDPOINT)
    {
        uint8_t ep_addr = p[2];
        uint8_t direction = ep_addr >> 7;
        uint8_t attr = p[3];
        uint8_t interrupt = (attr & 0x03u) == 3;
        if (boot_keyboard_config_value != 0
            && direction == 1 // IN
            && interrupt == 1) // Interrupt
        {
            memcpy(ep_key_in, p, 7);
        }
    }
    p += p[0];
}

やっているのは,まず Interface ディスクリプタのうち,Boot Interface なキーボードを探す. そのような Interface が見つかれば boot_keyboard_config_value に値を記憶しておく.

次に Boot Interface なキーボードに属する Endpoint のうち,Interrupt かつ IN 方向の Endpoint を探す. 見つかれば ep_key_in にディスクリプタをコピーしておく.

Boot Interface なインターフェースとは,BIOS がきちんと読めるように送信データの先頭構造が規格化されたインターフェースのことだ. キーボードであれば先頭 8 バイト,マウスであれば先頭 3 バイトの構造が規格により決められている. Boot Interface でないキーボードやマウスのデータを正しく読むには Report ディスクリプタをきちんと解析し,その定義に従う必要があり,プログラムが複雑になる. 殆どすべてのキーボードやマウスが Boot Interface を持つはずなので,上記のコードではそれを検索するようにした.

Set the Configuration

Boot Interface なキーボードが見つかったら,その Configuration を有効化する.

set_configuration(assigned_slot_id, boot_keyboard_config_value);

set_configuration 関数の定義は後述. USB デバイスに対し,有効化する Configuration の値を伝える.

Configure endpoints

Configuration を有効化したら,次に Endpoint を設定することでその USB デバイスを Addressed 状態から Configured 状態に遷移させることができる.

まず,Boot Interface なキーボードの Endpoint 番号を Endpoint ディスクリプタから取得し,その値を使って DCI を計算する.

uint8_t ep_key_in_num = ep_key_in[2] & 0x0fu;
uint8_t ep_key_in_dci = 2 * ep_key_in_num + 1; // 2*num + direction

この後の処理は Address Device Command の場合と似ている. 違うのは,対象の Endpoint が Default Control Pipe ではなく,Endpoint ディスクリプタに設定されている Endpoint である点である.

Input Context を生成し,Slot Context と Endpoint に対応する Add Context Flags のビットを 1 にする.

struct InputContext inp_ctx;
inp_ctx.input_control_context.add_context_flags = 1u | (1u << ep_key_in_dci);

Slot Context には Device Context の Slot Context をコピーして,必要なフィールドのみを上書きするようにする. 上書きの必要があるのは Context Entries だけである. Context Entries には,有効化したい Endpoint の DCI の最大値を設定する.

memcpy(&inp_ctx.slot_context, dcbaa[assigned_slot_id],
    sizeof(struct SlotContext));
inp_ctx.slot_context.context_entries = ep_key_in_dci;

Endpoint 用の Transfer Ring を生成する.

alignas(64) struct RingManager tr_ep_key_in;
memset(tr_ep_key_in.buf, 0, sizeof(tr_ep_key_in.buf));
tr_ep_key_in.cycle_bit = 1;
tr_ep_key_in.write_index = 0;

Endpoint Context を設定する. Endpoint Type を 7 (Interrupt In) にするのが大切だ.

inp_ctx.ep_contexts[ep_key_in_dci-1].ep_type = 7; // Interrupt In
inp_ctx.ep_contexts[ep_key_in_dci-1].tr_dequeue_pointer = (uint64_t)tr_ep_key_in.buf;
inp_ctx.ep_contexts[ep_key_in_dci-1].dequeue_cycle_state = tr_ep_key_in.cycle_bit;
inp_ctx.ep_contexts[ep_key_in_dci-1].max_packet_size = 8;
inp_ctx.ep_contexts[ep_key_in_dci-1].max_burst_size = 0;
inp_ctx.ep_contexts[ep_key_in_dci-1].mult = 0;
inp_ctx.ep_contexts[ep_key_in_dci-1].interval = xxxx; // コラム参照
inp_ctx.ep_contexts[ep_key_in_dci-1].max_primary_streams = 0;
inp_ctx.ep_contexts[ep_key_in_dci-1].error_count = 3;
inp_ctx.ep_contexts[ep_key_in_dci-1].average_trb_length = 1;

最後に Configure Endpoint Command を発行する.

struct ConfigureEndpointCommandTRB cmd;
cmd.input_context_pointer = (uint64_t)&input_context;
cmd.slot_id = assigned_slot_id;

ring_push(&cr, &cmd);
DOORBELL[0] = 0;

while (er_front(0)->cycle_bit != er_cycle_bit);
while (er_front(0)->cycle_bit == er_cycle_bit) er_pop(0);

処理が完了すると晴れて USB デバイスとデータのやり取りができるようになる.

コラム:Interval の設定値

その後の調査で Interval に設定する値の決め方が分かったのでここに記す.

まず,Endpoint ディスクリプタに記載されている bInterval の値と xHCI の Endpoint Context に設定する Interval の値の関係を整理すると次の通り.(参考資料 [_USB_ENDPOINT_DESCRIPTOR structure Microsoft Docs](https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/usbspec/ns-usbspec-_usb_endpoint_descriptor))

bInterval は,USB デバイスが「自分は X 秒の周期でポーリングして欲しい(されるべき)」とホスト側に伝えるための項目であり,ホスト側はなるべくその希望に近い周期でポーリングするようにすべきである.

Interval は実際に xHC がポーリングを行う周期を設定する.xHCI 規格書 6.2.3.6 によると,2^Interval * 125(us) の周期でポーリングするらしい.

さて,ここで bInterval の値の意味が通信速度(Low, Full, High, Super)やエンドポイント種別(Interrupt/Isochronous)で異なる.詳しくは xHCI 規格書の表 65 “Endpoint Type vs. Interval Calculation” に従う.ただ,この表を見ただけだと具体的な計算は分かりにくいので計算式を次に示す.実際の計算コードは EDK II の xHCI 実装 が参考になる.

get_descriptor 関数の定義

void get_descriptor(
    uint8_t slot_id, uint8_t desc_type, uint8_t desc_index,
    uint8_t* buf, size_t buf_len)
{
    struct SetupStageTRB setup;
    setup.request_type = 0b10000000;
    setup.request = 6; // GET_DESCRIPTOR
    setup.value = ((uint16_t)desc_type << 8) | desc_index;
    setup.index = 0;
    setup.length = (uint16_t)buf_len;
    setup.transfer_type = 3; // IN Data Stage
    setup.immediate_data = 1;
    setup.trb_transfer_length = 8;

    struct DataStageTRB data;
    data.data_buffer_pointer = (uint64_t)buf;
    data.trb_transfer_length = (uint16_t)buf_len;
    data.td_size = 0;
    data.direction = 1; // IN
    data.interrupt_on_completion = 1;

    struct StatusStageTRB status;
    status.direction = 0;

    ring_push(&tr_ep0, &start)
    ring_push(&tr_ep0, &data);
    ring_push(&tr_ep0, &status);
    DOORBELL[assigned_slot_id] = 1; // DCI of the Default Control Pipe

    while (er_front(0)->cycle_bit != er_cycle_bit);
    while (er_front(0)->cycle_bit == er_cycle_bit) er_pop(0);
}

ちょっと長いが,やっていることは 3 つの TRB を準備し,順番に Default Control Pipe に送信しているだけだ.

Transfer Ring に送信する TRB は一般に,Interrupt On Completion を 1 に設定しない限り成功時にイベント通知がなされないので注意.

set_configuration 関数の定義

void set_configuration(uint8_t slot_id, uint8_t configuration_value)
{
    struct SetupStageTRB setup;
    setup.request_type = 0b00000000;
    setup.request = 9; // SET_CONFIGURATION
    setup.value = configuration_value;
    setup.index = 0;
    setup.length = 0;
    setup.transfer_type = 0; // No Data Stage
    setup.immediate_data = 1;
    setup.trb_transfer_length = 8;

    struct StatusStageTRB status;
    status.direction = 1;
    status.interrupt_on_completion = 1;

    ring_push(&tr_ep0, &start)
    ring_push(&tr_ep0, &status);
    DOORBELL[assigned_slot_id] = 1; // DCI of the Default Control Pipe

    while (er_front(0)->cycle_bit != er_cycle_bit);
    while (er_front(0)->cycle_bit == er_cycle_bit) er_pop(0);
}

Set Configuration は Data Stage が無い.