-
Notifications
You must be signed in to change notification settings - Fork 5
USB Memo_002
このページでは USB デバイスを USB ポートに接続したことを検出し,ポートを初期化し,Device Slot を割り当てる方法を説明する.
ハブを介した接続は多少複雑なので,このページではホストコントローラに直接接続する場合のみを扱う.
ホストコントローラに実装されているポート数を取得する.HCSPARAMS1 レジスタの Number of Ports フィールドを読めばよい.
const uint8_t NUM_PORTS = HCSPARAMS1;
ホストコントローラに 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
までの値を取る.
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 を書き込んでポート初期化動作を開始させる.
ポートの初期化が終わるのを待つ.ポートの初期化が終わると 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
関数と合わせて詳しくは後述する.
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
で置き換えるのがポイント.
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);
}
}