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

View My GitHub Profile

Detect a device

このページでは USB デバイスを USB ポートに接続したことを検出し,ポートを初期化し,Device Slot を割り当てる方法を説明する.

ハブを介した接続は多少複雑なので,このページではホストコントローラに直接接続する場合のみを扱う.

Get the number of ports

ホストコントローラに実装されているポート数を取得する.HCSPARAMS1 レジスタの Number of Ports フィールドを読めばよい.

const uint8_t NUM_PORTS = HCSPARAMS1;

Detect a device attached

ホストコントローラに USB デバイスが接続されるか,接続しっぱなしでホストコントローラのリセットが終わってしばらく経つと,ポートの Connect Status Change ビットが変化する.それを検出する.

while (true)
{
    int port_index = -1;
    for (int i = 0; i < NUM_PORTS; ++i)
    {
        if (port_register_set[i].PORTSC & (1u << 17))
        {
            port_index = i;
            break;
        }
    }
    if (port_index >= 0) break;
}

Connect Status Change ビットが変化したポートを見つけたら port_index にポート番号(から 1 を引いたもの)が入る.xHCI の規格で「ポート番号」は,1 から NUM_PORTS までの値を取る.

Clear CSC flag

2 回同じデバイスが検出されないように,Connect Status Change フラグをクリアする.

x = port_register_set[port_index].PORTSC;
x &= 0x0e00c3e0u;
x |= (1u << 17);  // Write 1 to CSC
x |= (1u << 4);  // Write 1 to PR
port_register_set[port_index].PORTSC = x;

PORTSC レジスタのビットのいくつかは,1 を書き込むことで内容をクリアすることができる.だから,クリアと言いつつ 1 を書き込んでいるのは誤植ではない.不用意に他のビットの内容を消さないよう,ビットマスクが多少複雑になっている.

Connect Status Change をクリアするついでに,Port Reset ビットに 1 を書き込んでポート初期化動作を開始させる.

Assign a Device Slot

ポートの初期化が終わるのを待つ.ポートの初期化が終わると Port Enabled Disabled ビットが 1 になるので,それまで待てばよい.

while ((port_register_set[i].PORTSC & 2u) == 0);

Enable Slot Command を発行することにより Device Slot を割り当てる.Device Slot はホストコントローラがそのデバイスに関する情報を書き込むメモリ領域である.ソフトウェア側からは参照専用.

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

uint8_t assigned_slot_id = 0;

ring_push は Command Ring の末尾に TRB を追加するための関数である.詳しくは後述する.

Command Ring にデータを追加してから Doorbell Register 0 に書き込みを行うことで,ホストコントローラに Command Ring にデータが追記された旨を通知することができる.

Doorbell Register は 0 番から Device Slot 番まで存在する.Doorbell Register 0 はホストコントローラに紐づくレジスタで,Doorbell Register 1..デバイススロット数 はそれぞれの Device Context に紐づくレジスタである.

Enable Slot Command の実行が終わるのを待つ.

while (er_front(0)->cycle_bit != er_cycle_bit);
while (er_front(0)->cycle_bit == er_cycle_bit)
{
    struct TRB* trb = er_front(0);
    if (trb->trb_type == 33) // Command Completion Event
    {
        struct TRB* issue_trb = (struct TRB*)trb->command_trb_pointer;
        if (issue_trb->trb_type == 9) // Enable Slot Command
        {
            assigned_slot_id = trb->slot_id;
        }
    }
    er_pop(0);
}

er_front は Event Ring の先頭の要素を返す関数である.引数 0 は 0 番目の Event Ring(Primary Event Ring)を意味する.Command Ring への書き込みの結果発生したイベントは,すべて 0 番目の Event Ring に送信される.

先頭要素の cycle bit が er_cycle_bit と一致する場合,ソフトウェアがまだ取得していない要素が Event Ring にあるということを意味する.それが Command Completion Event であり,そのイベントの原因となった TRB が Enable Slot Command である場合,その Slot ID を新規に割り当てられた Device Slot の ID だとする.

er_pop は Event Ring の読み出しポインタを進める関数である.引数 0 は 0 番目の Event Ring を意味する.er_front 関数と合わせて詳しくは後述する.

Command Ring の関数

ring_push 関数は次のような実装とする.あくまで疑似コードなので,このままでうまく動くわけではない.

名前が Command Ring 特有(cr_push とか)でなく,一般的な名前になっているのは,この関数が Transfer Ring でも共通に使えるからである.

void ring_copy_to_last(struct RingManager* ring, struct TRB* cmd)
{
    uint32_t* cmd_dwords = (uint32_t*)cmd;
    uint32_t* ring_dwords = (uint32_t*)&ring->buf[ring->write_index];

    uint32_t last_dword = cmd_dowrds[3];
    last_dword &= 0xfffffffeu;
    last_dword |= ring->cycle_bit;

    for (size_t i = 0; i < 3; ++i)
        ring_dwords[i] = cmd_dwords[i];
    ring_dwords[3] = last_dword;
}

void ring_push(struct RingManager* ring, struct TRB* cmd)
{
    ring_copy_to_last(ring, cmd);

    ++ring->write_index;
    if (ring->write_index == RING_SIZE - 1)
    {
        LinkTRB link;
        link.ring_segment_pointer = ring->buf;
        link.toggle_cycle = 1;
        ring_copy_to_last(ring, &link);
        ring->write_index = 0;
        ring->cycle_bit = !ring->cycle_bit;
    }
}

受け取った TRB を Command/Transfer Ring のバッファの最後に追加するコードとなっている.TRB の cycle bit フィールドを ring->cycle_bit で置き換えるのがポイント.

Event Ring の関数

er_front 関数は次のような実装とする.例によって疑似コードである.

TRB* er_front(uint8_t slot_id)
{
    return (TRB*)(interrupter_register_set[slot_id].ERDP & 0xfffffff0u);
}

void er_pop(uint8_t slot_id)
{
    TRB* read_ptr = er_front(slot_id);
    if (read_ptr == &er_segment[ERSEGM_SIZE - 1])
    {
        er_cycle_bit = !er_cycle_bit;
        interrupter_register_set[slot_id].ERDP = (uint64_t)er_segment;
    }
    else
    {
        interrupter_register_set[slot_id].ERDP = (uint64_t)(read_ptr + 1);
    }
}