こんにちは、ENGかぴです。
PIC18F14K50 を用いて USB HID デバイスを構築するために、SETUP パケット処理とデバイス・コンフィグレーション・HID レポートの各ディスクリプタ実装し、Windows のデバイスマネージャで認識される方法をまとめました。
PIC18F14K50 を実装した基板は AE-USB-I02.0-TYPEC(秋月電子)を使用しています。以下では PIC モジュールとします。
PIC モジュールで USB が挿入されたことを認識できるまでの初期化は下記記事にまとめています。
PICマイコン(PIC18F14K50)でUSBを実装する(初期化編)
EP0 を使って SETUP を実装

この図は、PC が PIC18F14K50 に対して USB の初期化を行う際の通信手順を示しています。 ホスト(PC)は最初にデバイスの存在確認として GET_DESCRIPTOR を送り、PIC モジュール はデバイスディスクリプタの先頭 8 バイトを応答します。
続いてホストは Bus Reset を発行し、PIC モジュールがこれを検出すると通信状態を初期化します。 その後、ホストは SET_ADDRESS によって PIC モジュールに新しいアドレスを割り当て、PIC モジュールは ACK(0 バイト)で応答し、内部のアドレスを更新します。
アドレスが確定すると、ホストは再度 GET_DESCRIPTOR を発行し、PIC モジュールはデバイスディスクリプタ全体を 8 バイト単位に分割して順番に応答します。続けて、ホストのリクエストに対応してConfiguration Descriptor や HID Report Descriptor など、各種ディスクリプタで応答します。
USB の制御転送では、複数パケットに分割して応答する際に DATA0 と DATA1 をトグルしながら送信するため、PIC モジュールは BDT の STAT フィールドと所有権(CPU / SIE)を切り替えながら応答します。詳細な BDT の動作やディスクリプタの分割処理は、後続で説明します。
PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール
USTATレジスタ

USTAT は直前の USB トランザクションがどのエンドポイントで、IN トークンなのかOUT/SETUP トークンなのかを通知するレジスタです。このレジスタを使って処理を分岐させます。
bit2 の DIR ビットを確認してトランザクションの向きを判定します。
- 1 = IN トークン(ホストがデータを要求)
- 0 = OUT / SETUP トークン(ホストがデータを送る)
bit5-3 は対象のエンドポイントを判定します。SETUP の場合は EP0 を使用するため 000 になります。EP1 の場合は 001 になります。
uint8_t stat = USTAT;
switch(stat & 0x7C)
{
case 0x00:
// EP0 OUT
break;
case 0x04:
// EP0 IN
break;
}
}
USTAT は読み込むごとに FIFO が更新されるため一度読み込んだときに変数( stat )に情報を移して、トークンの向きとエンドポイントを判定します。
エンドポイントは SETUP であれば EP0 になるため、IN トークンであれば 0x00、OUT / SETUP トークンであれば 0x04 になるのでこの条件で分岐します。
ホストからのリクエストは各エンドポイントに対応する BDT.STAT を参照して確認します。
BDT の STAT でリクエスト

BDnSTAT(以下 BDT.STAT とします) は、USB モジュールが各バッファの状態を管理するためのステータスレジスタです。SIE モードはホストからの受信待ちをどのように条件で行うかを設定します。
PICマイコン(PIC18F14K50)でUSBを実装する(初期化編)
上記記事で BDT の SIE モードの初期化及び BDT の CPU モードの初期化について説明していますが、BDT.STAT は所有権が SIE にある場合と CPU にある場合で読み方が変わります。
PIC モジュールは基本的にホストからのリクエストを待機するため、OUT トークンの BDT の所有権は SIE が持ちます。リクエスト応答を管理する IN トークンの BDT は CPU が所有権を持ちます。リクエストを受信すると所有権を一時的に切り替えて受信/送信を行います。
図の ② で OUT トークンを受信すると BDT の所有権が SIE から CPU に切り替わるため、パケットの内容(ep0_out_buffer[])を確認します。ホストからは DATA0 のパケットから送信されるため、次の OUT トークンは DATA1 を選択して所有権を SIE に渡します。
リクエストの応答は IN トークンの BDT で行います。所有権は CPU が持っているので BDT に送信するデータ数を指定し、USB(SIE) に所有権を渡します。
ディスクリプタの応答など複数パケットに分割して応答する場合、BDT.STAT の DATA ビットを反転させながら DATA1 → DATA0 → DATA1 とトグルして送信します。この様子は図の③と④で示しています。
OUT トークン、IN トークンともにパケットを分割して応答する場合は、DATA0 / DATA1 をトグルしながら新規のパケットであること通知しながらパケットの続きを受信/送信します。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
パケットの確認と次のOUTトークンを待機
void EP0_ProcessSetup(void)
{
uint8_t bmRequestType = ep0_out_buffer[0];
uint8_t bRequest = ep0_out_buffer[1];
uint16_t wValue = ep0_out_buffer[2] | ((uint16_t)ep0_out_buffer[3] << 8);
uint16_t wIndex = ep0_out_buffer[4] | ((uint16_t)ep0_out_buffer[5] << 8);
uint16_t wLength = ep0_out_buffer[6] | ((uint16_t)ep0_out_buffer[7] << 8);
/* Control転送開始 */
ep0_ptr = NULL;
ep0_len = 0;
switch(bRequest)
{
case 0x06: /* GET_DESCRIPTOR */
ep0_ptr = device_dsc;
ep0_len = sizeof(device_dsc);
if(ep0_len > wLength)
{
ep0_len = wLength;
}
ep0_data_toggle = 1;
ep0_state = EP0_DATA_IN;
/* 次のSETUP/Status OUTを受信できるようにする */
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA1 | _UOWN;
UCONbits.PKTDIS = 0;
EP0_SendNextPacket();
}
break;
}
}
受信した OUT トークンのパケットは ep0_out_buffer[] に格納されています。SETUP パケットは 8 バイトで構成されており、2~6 行目のようにリクエストのタイプ、番号、ディスクリプタの番号、ディスクリプタのタイプサイズなどの情報を含んでいます。リトルエンディアンで受信しているので値を生成する場合は注意が必要です。
9~10 行目は EP0 の内部状態を初期化し、これから始まる制御転送に備えます。ホストはデバイス情報を取得するため GET_DESCRIPTOR( 0x06 ) からスタートします。
15~16 行目でディスクリプタの先頭アドレスとサイズを設定し、ホストが要求した長さ( wLength )が短い場合はそれに合わせてサイズを調整(18~21 行目)します。
26~28 行目は BDT[EP0_OUT] を以下の設定で所有権を SIE に渡して、次の SETUP または Status OUT を受信できる状態にします。
- CNT = 8 → SETUP は常に 8 バイト固定
- STAT = DATA1 | UOWN → DATA1にトグルして所有権を SIE に渡す
BDT[EP0_OUT]を更新し、UCONbits.PKTDIS = 0 で トークンとパケットの処理が有効にすると SIE は受信待機になります。リクエストの応答は自作の関数の EP0_SendNextPacket() で行います。
PR:キャリア形成でお悩みのあなたへ!初回無料相談で相談できる[coachee]
リクエスト応答
void EP0_SendNextPacket(void)
{
uint8_t send;
uint8_t i;
uint8_t stat;
if(ep0_ptr == NULL)
return;
if(ep0_len == 0)
return;
/* 今回送るバイト数 */
if(ep0_len > 8)
{
send = 8;
}
else
{
send = (uint8_t)ep0_len;
}
/* EP0 INバッファへコピー */
for(i = 0; i < send; i++)
{
ep0_in_buffer[i] = ep0_ptr[i];
}
/* 次回送信位置へ進める */
ep0_ptr += send;
ep0_len -= send;
/* パケットサイズ */
bdt[EP0_IN].CNT = send;
/* DATA0 / DATA1 */
if(ep0_data_toggle)
{
stat = _DATA1 | _UOWN;
bdt[EP0_IN].STAT = stat;
ep0_data_toggle = 0;
}
else
{
stat = _DATA0 | _UOWN;
bdt[EP0_IN].STAT = stat;
ep0_data_toggle = 1;
}
UCONbits.PKTDIS = 0;
}
EP0_SendNextPacket() は、EP0 の IN トークンに対して ディスクリプタを 8 バイトずつ分割して送信するための処理を行う関数です。 USB の制御転送では、複数パケットに分割して応答する場合に DATA0 / DATA1 をトグルしながら送信する必要があります。
USB の制御転送では、SETUP の後の最初の IN 応答は必ず DATA1 から始まります。 図の ③ IN 応答(DATA1)に対応し、次の ④ IN 応答(DATA0)に対応しています。
7~11 行目は送信するデータの先頭アドレスと残りの送信バイト数が存在するかを確認しています。送信する対象がなければ送信処理に進まず関数から抜けます。
13~20 行目は USB EP0 の最大パケットサイズは 8 バイトのため、 ディスクリプタが長い場合は 8 バイトずつ分割して送信します。
23~29 行目は今回送る 8 バイトを IN バッファに格納し、ディスクリプタの残りの部分を次の IN トークンで送るためポインタと残りのデータ数を更新します。
32 行目はパケットサイズを BDT に設定しています。
35~45 行目は DATA0 / DATA1 をトグルしながら BDT.STAT に指定します。最初の IN 応答(36~38行目)は DATA1 からスタートします。次の送信は DATA0 を使用するため ep0_data_toggle = 0 に更新し、次の IN 応答 で42~44 行目に遷移するようにします。
BDT.STAT は DATA0 / DATA1 の区分と UOWN によって所有権を USB(SIE) に渡し、UCONbits.PKTDIS = 0 で トークンとパケットの処理が有効にします。SIE が送信を完了すると UOWN がクリアされ所有権が CPU に戻ります。
ディスクリプタ応答を生成する
USB デバイスは、ホストに接続した瞬間からホストから「どのようなデバイスなのか」を尋ねるリクエストに応答する必要があります。この情報はディスクリプタ ( Descriptor ) と呼ばれる構造体群として定義されており、ホストは USB の初期化処理の中で、これらを順番に要求します。
USB のディスクリプタは階層構造になっており、ホストは次の順番で情報を取得します。
デバイスディスクリプタ(Device Descriptor)
const uint8_t device_dsc[DEVICE_DSC_SIZE] = {
18, // bLength
1, // bDescriptorType
0x00, 0x02, // bcdUSB 2.0
0x00, // class
0x00, // subclass
0x00, // protocol
8, // bMaxPacketSize0 (EP0=8)
0xFF,0xFF, // idVendor
0x3f,0x00, // idProduct
0x00,0x01, // bcdDevice
0, // iManufacturer
0, // iProduct
0, // iSerial
1 // bNumConfigurations
};
この配列は USB の デバイスディスクリプタを表しており、ホストが最初に GET_DESCRIPTOR を送ったときに PIC モジュールが返す 18 バイトの固定情報です。 USB デバイスの「名刺」に相当し、デバイスの種類・USB バージョン・ベンダー ID ・製品 ID などが含まれます。
USB の初期化処理では、ホストはまず GET_DESCRIPTOR を送信し、 デバイスはこのディスクリプタを 8 バイトずつ分割して応答します。( USB用のRAM領域にEP0用で ep0_out_buffer[8] 及び ep0_in_buffer[8] のように8バイトで区切っているため 8 バイトずつの分割で応答します。 )
デバイスディスクリプタのように 8 バイトを超える場合は 図の③・④のように 複数パケットに分割して送信する必要があります。
| バイト | 項目名 | 値 | 説明 |
|---|---|---|---|
| 0 | bLength | 18 | デバイスディスクリプタのサイズ(固定) |
| 1 | bDescriptorType | 1 | デバイスディスクリプタ |
| 2–3 | bcdUSB | 0x0200 | USB 2.0 |
| 4 | bDeviceClass | 0 | クラスなし(インターフェース側で定義) |
| 5 | bDeviceSubClass | 0 | サブクラスなし |
| 6 | bDeviceProtocol | 0 | プロトコルなし |
| 7 | bMaxPacketSize0 | 8 | EP0 の最大パケットサイズ(PICは8固定) |
| 8–9 | idVendor | 0xFFFF | 学習用のベンダーID |
| 10–11 | idProduct | 0x003F | 製品ID |
| 12–13 | bcdDevice | 0x0100 | デバイスバージョン 1.00 |
| 14 | iManufacturer | 0 | 文字列なし |
| 15 | iProduct | 0 | 文字列なし |
| 16 | iSerial | 0 | 文字列なし |
| 17 | bNumConfigurations | 1 | 構成は1つ |
PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール
コンフィグレーションディスクリプタ(Configuration Descriptor)
const uint8_t config_dsc[CONFIG_DSC_SIZE] = {
9, // bLength
0x02, // bDescriptorType (Configuration)
CONFIG_DSC_SIZE, 0x00, // wTotalLength
1, // bNumInterfaces
1, // bConfigurationValue
0, // iConfiguration
0x80, // bmAttributes (Bus powered)
50, // bMaxPower (100mA)
// Interface Descriptor
9, // bLength
0x04, // bDescriptorType (Interface)
0, // bInterfaceNumber
0, // bAlternateSetting
2, // bNumEndpoints (EP1 IN OUT)
0x03, // bInterfaceClass (HID)
0x00, // bInterfaceSubClass
0x00, // bInterfaceProtocol
0, // iInterface
// HID Descriptor
9, // bLength
0x21, // bDescriptorType (HID)
0x11, 0x01, // bcdHID 1.11
0x00, // bCountryCode
1, // bNumDescriptors
0x22, // bDescriptorType (Report)
REPORT_DSC_SIZE, 0x00, // wDescriptorLength
/* Endpoint Descriptor (OUT) */
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x01, // bEndpointAddress (OUT, EP1)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize (64 bytes)
0x01, // bInterval
// Endpoint Descriptor (EP1 IN, Interrupt)
7, // bLength
0x05, // bDescriptorType (Endpoint)
0x81, // bEndpointAddress (IN, EP1)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize
1 // bInterval (10ms)
};
デバイスディスクリプタの次に要求されるのが コンフィグレーションディスクリプタ です。 これは「デバイスがどのような機能を持つか」をまとめた構造体で、複数のディスクリプタを束ねています。
コンフィグレーションディスクリプタ の中には:
- Configuration Descriptor(本体)
- Interface Descriptor(機能の種類)
- HID Descriptor(HID デバイスの場合)
- Endpoint Descriptor(IN/OUT のエンドポイント情報)
などが階層的に含まれています。
ホストは GET_DESCRIPTOR(Configuration) を送り、デバイスはこの構造体全体を 8 バイトずつ分割して返します。
| バイト | 項目名 | 値 | 説明 |
|---|---|---|---|
| 0 | bLength | 9 | ディスクリプタのサイズ |
| 1 | bDescriptorType | 0x02 | Configuration Descriptor |
| 2–3 | wTotalLength | CONFIG_DSC_SIZE | この構成に含まれる全ディスクリプタの合計サイズ |
| 4 | bNumInterfaces | 1 | インターフェース数(HID 1つ) |
| 5 | bConfigurationValue | 1 | この構成の番号 |
| 6 | iConfiguration | 0 | 文字列なし |
| 7 | bmAttributes | 0x80 | Bus Powered(自己給電ではない) |
| 8 | bMaxPower | 50 | 100mA(値×2mA) |
CONFIG_DSC_SIZEはエンドポイントの数によって値が変わります。
| バイト | 項目名 | 値 | 説明 |
|---|---|---|---|
| 9 | bLength | 9 | ディスクリプタのサイズ |
| 10 | bDescriptorType | 0x04 | Interface Descriptor |
| 11 | bInterfaceNumber | 0 | インターフェース番号 |
| 12 | bAlternateSetting | 0 | Alternate 設定なし |
| 13 | bNumEndpoints | 2 | EP1 IN / EP1 OUT の2つ |
| 14 | bInterfaceClass | 0x03 | HID クラス |
| 15 | bInterfaceSubClass | 0 | サブクラスなし |
| 16 | bInterfaceProtocol | 0 | プロトコルなし |
| 17 | iInterface | 0 | 文字列なし |
| バイト | 項目名 | 値 | 説明 |
|---|---|---|---|
| 18 | bLength | 9 | ディスクリプタのサイズ |
| 19 | bDescriptorType | 0x21 | HID Descriptor |
| 20–21 | bcdHID | 0x0111 | HID Version 1.11 |
| 22 | bCountryCode | 0 | 国別コードなし |
| 23 | bNumDescriptors | 1 | HID Report Descriptor が1つ |
| 24 | bDescriptorType | 0x22 | Report Descriptor |
| 25–26 | wDescriptorLength | REPORT_DSC_SIZE | HID Report Descriptor のサイズ |
REPORT_DSC_SIZEはレポートの内容によって値が変わります。
| バイト | 項目名 | 値 | 説明 |
|---|---|---|---|
| 27 | bLength | 7 | ディスクリプタのサイズ |
| 28 | bDescriptorType | 0x05 | Endpoint Descriptor |
| 29 | bEndpointAddress | 0x01 | EP1 OUT |
| 30 | bmAttributes | 0x03 | Interrupt Transfer |
| 31–32 | wMaxPacketSize | 64 | 最大64バイト |
| 33 | bInterval | 1 | ポーリング間隔(1ms) |
| バイト | 項目名 | 値 | 説明 |
|---|---|---|---|
| 34 | bLength | 7 | ディスクリプタのサイズ |
| 35 | bDescriptorType | 0x05 | Endpoint Descriptor |
| 36 | bEndpointAddress | 0x81 | EP1 IN |
| 37 | bmAttributes | 0x03 | Interrupt Transfer |
| 38–39 | wMaxPacketSize | 64 | 最大64バイト |
| 40 | bInterval | 1 | ポーリング間隔(1ms) |
エントリーポイント毎に必要に応じてOUTとINを追加します。
HID レポートディスクリプタ(HID Report Descriptor)
const uint8_t hid_rpt_desc[REPORT_DSC_SIZE] = {
0x06, 0x00, 0xFF,
0x09, 0x01,
0xA1, 0x01,
0x09, 0x02,
0x15, 0x00,
0x26, 0xFF, 0x00,
0x75, 0x08,
0x95, 0x08,
0x81, 0x02,
0x09, 0x03,
0x15, 0x00,
0x26, 0xFF, 0x00,
0x75, 0x08,
0x95, 0x08,
0x91, 0x02,
0xC0
};
HID デバイスの場合、ホストはさらに HID レポートディスクリプタ を要求します。これは HID デバイスの「データ形式」を定義するディスクリプタで、 キーボードならキーコードの並び、マウスなら X/Y の移動量などが記述されています。
ホストは GET_DESCRIPTOR(HID Report) を送り、 デバイスはこのレポートディスクリプタを同じく 8 バイトずつ分割して返します。
| バイト | 値 | 項目名 | 説明 |
|---|---|---|---|
| 0–2 | 0x06, 0x00, 0xFF | Usage Page (Vendor Defined) | ベンダー独自の用途(0xFF00) |
| 3–4 | 0x09, 0x01 | Usage (1) | デバイスの機能を示す(Vendor Usage 1) |
| 5–6 | 0xA1, 0x01 | Collection (Application) | アプリケーションコレクション開始 |
| バイト | 値 | 項目名 | 説明 |
|---|---|---|---|
| 7–8 | 0x09, 0x02 | Usage (2) | 入力レポートの用途 |
| 9–10 | 0x15, 0x00 | Logical Minimum (0) | 最小値 0 |
| 11–13 | 0x26, 0xFF, 0x00 | Logical Maximum (255) | 最大値 255 |
| 14 | 0x75 | Report Size | 1要素=8bit |
| 15 | 0x08 | Report Size 値 | 8bit |
| 16 | 0x95 | Report Count | 要素数 |
| 17 | 0x08 | Report Count 値 | 8要素(8バイト) |
| 18 | 0x81 | Input | 入力レポート |
| 19 | 0x02 | Data, Var, Abs | データ/変数/絶対値 |
| バイト | 値 | 項目名 | 説明 |
|---|---|---|---|
| 20–21 | 0x09, 0x03 | Usage (3) | 出力レポートの用途 |
| 22–23 | 0x15, 0x00 | Logical Minimum (0) | 最小値 0 |
| 24–26 | 0x26, 0xFF, 0x00 | Logical Maximum (255) | 最大値 255 |
| 27 | 0x75 | Report Size | 1要素=8bit |
| 28 | 0x08 | Report Size 値 | 8bit |
| 29 | 0x95 | Report Count | 要素数 |
| 30 | 0x08 | Report Count 値 | 8要素(8バイト) |
| 31 | 0x91 | Output | 出力レポート |
| 32 | 0x02 | Data, Var, Abs | データ/変数/絶対値 |
| バイト | 値 | 項目名 | 説明 |
|---|---|---|---|
| 33 | 0xC0 | End Collection | コレクション終了 |
動作確認

PIC モジュールにソフトを書き込んで USB を接続します。通常動作では ホスト ( PC ) と PIC モジュール間で SETUP の通信が始まります。1~2 秒くらいで接続に成功しデバイスマネージャに「HID 準拠ベンダー定義デバイス」が表示されます。

デバイスを認識させた後は LED を点灯/消灯を 500ms 毎にトグルする動作に移ります。LED が点灯/消灯しない場合は SETUP に失敗してループから抜けられない状態になります。USB Device Viewer でディスクリプタの情報を表示して確認しました。

「ディスクリプタ応答を生成する」で説明した。配列のデータが反映されて表示できていることが確認できました。
デバイスマネージャ、USB Device Viewer の表示と LED が点灯/消灯することで SETUP に成功したことが確認できました。
デバイスが認識できたので、次は EP1 を使って双方向通信にチャレンジしようと思います。
PR:外資系・IT業界などハイクラスの転職に強い【AXIS Agent】
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
main.c のソースコード:
#include "mcc_generated_files/system/system.h"
#include "usbmain.h"
#include "usb_descriptors.h"
int main(void)
{
SYSTEM_Initialize();
USB_Init(); // USB 初期化
while( ep0_state != EP0_END )
{
USB_Task(); // USB 処理ループ
}
while(1)
{
IO_RC0_Toggle();
__delay_ms(2000);//PLLでメインクロックが4倍になっているので×4の値
}
}
usbmain.c のソースコード:
#include "mcc_generated_files/system/system.h"
#include "usb_descriptors.h"
#include "usbmain.h"
typedef struct {
uint8_t STAT;
uint8_t CNT;
uint8_t ADDR_L;
uint8_t ADDR_H;
} BDT_ENTRY;
// BDTを0x200に配置
volatile BDT_ENTRY bdt[16] __at(0x200);
// バッファを少し離して配置 (安全のため0x280から開始)
uint8_t ep0_out_buffer[8] __at(0x280);
uint8_t ep0_in_buffer[8] __at(0x288);
uint8_t ep1_out_buffer[32] __at(0x290);
uint8_t ep1_in_buffer[32] __at(0x2B0);
#define EP0_OUT 0
#define EP0_IN 1
#define EP1_OUT 2
#define EP1_IN 3
#define _UOWN 0x80
#define _DTS 0x40
#define _DTSEN 0x08
#define _BSTALL 0x04
#define _DATA0 (_DTSEN)
#define _DATA1 (_DTS | _DTSEN)
const uint8_t *ctrl_src;
uint16_t ctrl_len;
uint8_t pending_addr = 0;
uint8_t cnt = 0;
uint8_t ep0_tx_data = _DATA1;
const uint8_t *ep0_ptr;
uint16_t ep0_len;
uint8_t ep0_data_toggle;
bool usb_configured = false;
void USB_Init(void) {
UCON = 0;
UIR = 0;
UIE = 0;
UADDR = 0;
UCFG = 0x14; // FSEN=1, UPUEN=1
UEP0 = 0x16; // EPHSHK=1, EPOUTEN=1, EPINEN=1
UEP1 = 0x16; // EPHSHK=1, EPINEN=1, EPINEN=1
UCONbits.USBEN = 1;
__delay_ms(10); // 念のためのウェイト
for(uint8_t i = 0; i < 16; i++)
{
bdt[i].STAT = 0;
bdt[i].CNT = 0;
bdt[i].ADDR_L = 0;
bdt[i].ADDR_H = 0;
}
bdt[EP0_OUT].ADDR_L = (uint8_t)((uint16_t)ep0_out_buffer);
bdt[EP0_OUT].ADDR_H = (uint8_t)(((uint16_t)ep0_out_buffer) >> 8);
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _UOWN;
bdt[EP0_IN].ADDR_L = (uint8_t)((uint16_t)ep0_in_buffer);
bdt[EP0_IN].ADDR_H = (uint8_t)(((uint16_t)ep0_in_buffer) >> 8);
bdt[EP0_IN].STAT = 0;
// EP1 OUT 初期化
bdt[EP1_OUT].ADDR_L = (uint8_t)((uint16_t)ep1_out_buffer);
bdt[EP1_OUT].ADDR_H = (uint8_t)(((uint16_t)ep1_out_buffer) >> 8);
bdt[EP1_OUT].CNT = 32;
bdt[EP1_OUT].STAT = _UOWN;
bdt[EP1_IN].ADDR_L = (uint8_t)((uint16_t)ep1_in_buffer);
bdt[EP1_IN].ADDR_H = (uint8_t)(((uint16_t)ep1_in_buffer) >> 8);
bdt[EP1_IN].CNT = 32;
bdt[EP1_IN].STAT = 0; // 最初はCPU所有
UIEbits.TRNIE = 1; // トランザクション割り込みを許可
UIEbits.URSTIE = 1; // リセット割り込みを許可
PIR2bits.USBIF = 0; // USB割り込みフラグをクリア
PIE2bits.USBIE = 1; // 周辺機能割り込み(USB)を有効化
}
void USB_Task(void)
{
if(UIRbits.URSTIF)
{
UIRbits.URSTIF = 0;
UADDR = 0;
UEP0 = 0x16; // EPHSHK=1, EPOUTEN=1, EPINEN=1
UEP1 = 0x12; // EPHSHK=1, EPINEN=1
ep0_state = EP0_IDLE;
bdt[EP0_OUT].ADDR_L = (uint8_t)((uint16_t)ep0_out_buffer);
bdt[EP0_OUT].ADDR_H = (uint8_t)(((uint16_t)ep0_out_buffer) >> 8);
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _UOWN;
bdt[EP0_IN].ADDR_L = (uint8_t)((uint16_t)ep0_in_buffer);
bdt[EP0_IN].ADDR_H = (uint8_t)(((uint16_t)ep0_in_buffer) >> 8);
bdt[EP0_IN].STAT = 0;
// EP1初期化
bdt[EP1_OUT].ADDR_L = (uint8_t)((uint16_t)ep1_out_buffer);
bdt[EP1_OUT].ADDR_H = (uint8_t)(((uint16_t)ep1_out_buffer) >> 8);
bdt[EP1_OUT].CNT = 32;
bdt[EP1_OUT].STAT = _UOWN;
bdt[EP1_IN].ADDR_L = (uint8_t)((uint16_t)ep1_in_buffer);
bdt[EP1_IN].ADDR_H = (uint8_t)(((uint16_t)ep1_in_buffer) >> 8);
bdt[EP1_IN].CNT = 32;
bdt[EP1_IN].STAT = 0; // 最初はCPU所有
return;
}
if(!UIRbits.TRNIF)
return;
uint8_t stat = USTAT;
UIRbits.TRNIF = 0;
switch(stat & 0x7C)
{
case 0x00: // EP0 OUT
if(ep0_state == EP0_STATUS_OUT)
{
EP0_StatusOut();
}
else
{
EP0_ProcessSetup();
}
break;
case 0x04: // EP0 IN
EP0_StatusIn();
break;
}
}
void EP0_ProcessSetup(void)
{
uint8_t bmRequestType = ep0_out_buffer[0];
uint8_t bRequest = ep0_out_buffer[1];
uint16_t wValue = ep0_out_buffer[2] | ((uint16_t)ep0_out_buffer[3] << 8);
uint16_t wIndex = ep0_out_buffer[4] | ((uint16_t)ep0_out_buffer[5] << 8);
uint16_t wLength = ep0_out_buffer[6] | ((uint16_t)ep0_out_buffer[7] << 8);
ep0_ptr = NULL;
ep0_len = 0;
switch(bRequest)
{
case 0x06: /* GET_DESCRIPTOR */
uint8_t desc_type = (uint8_t)(wValue >> 8);
/* ★ Device Qualifier (6) / Other Speed Configuration (7) は ZLP で返す */
if (desc_type == 6 || desc_type == 7)
{
/* IN: ZLP */
bdt[EP0_IN].CNT = 0;
bdt[EP0_IN].STAT = _DATA1 | _UOWN;
/* 次の SETUP 受信用に OUT を DATA0 で再アーム */
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA0 | _UOWN;
UCONbits.PKTDIS = 0;
ep0_state = EP0_STATUS_IN;
return;
}
switch(desc_type)
{
case 1: /* Device */
ep0_ptr = device_dsc;
ep0_len = sizeof(device_dsc);
break;
case 2: /* Configuration */
ep0_ptr = config_dsc;
ep0_len = sizeof(config_dsc);
break;
case 0x22: /* HID Report */
ep0_ptr = hid_rpt_desc;
ep0_len = sizeof(hid_rpt_desc);
usb_configured = true;
break;
default:
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA0 | _UOWN;
bdt[EP0_IN].CNT = 0;
bdt[EP0_IN].STAT = _UOWN | _BSTALL;
UCONbits.PKTDIS = 0;
break;
}
if(ep0_ptr != NULL)
{
if(ep0_len > wLength)
{
ep0_len = wLength;
}
ep0_data_toggle = 1;
ep0_state = EP0_DATA_IN;
/* 次のSETUP/Status OUTを受信できるようにする */
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA1 | _UOWN;
UCONbits.PKTDIS = 0;
EP0_SendNextPacket();
}
break;
case 0x05: /* SET_ADDRESS */
pending_addr = (uint8_t)wValue;
ep0_state = EP0_STATUS_IN;
bdt[EP0_IN].CNT = 0;
bdt[EP0_IN].STAT = _DATA1 | _UOWN;
UCONbits.PKTDIS = 0;
break;
case 0x09: /* SET_CONFIGURATION */
ep0_state = EP0_STATUS_IN;
bdt[EP0_IN].CNT = 0;
bdt[EP0_IN].STAT = _DATA1 | _UOWN;
UCONbits.PKTDIS = 0;
break;
case 0x0A: // HID SET_IDLE
ep0_state = EP0_STATUS_IN;
bdt[EP0_IN].CNT = 0;
bdt[EP0_IN].STAT = _DATA1 | _UOWN;
UCONbits.PKTDIS = 0;
break;
default:
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA0 | _UOWN;
bdt[EP0_IN].CNT = 0;
bdt[EP0_IN].STAT = _UOWN | _BSTALL;
UCONbits.PKTDIS = 0;
break;
}
}
void EP0_SendNextPacket(void)
{
uint8_t send;
uint8_t i;
uint8_t stat;
if(ep0_ptr == NULL)
return;
if(ep0_len == 0)
return;
/* 今回送るバイト数 */
if(ep0_len > 8)
{
send = 8;
}
else
{
send = (uint8_t)ep0_len;
}
/* EP0 INバッファへコピー */
for(i = 0; i < send; i++)
{
ep0_in_buffer[i] = ep0_ptr[i];
}
/* 次回送信位置へ進める */
ep0_ptr += send;
ep0_len -= send;
/* パケットサイズ */
bdt[EP0_IN].CNT = send;
/* DATA0 / DATA1 */
if(ep0_data_toggle)
{
stat = _DATA1 | _UOWN;
bdt[EP0_IN].STAT = stat;
ep0_data_toggle = 0;
}
else
{
stat = _DATA0 | _UOWN;
bdt[EP0_IN].STAT = stat;
ep0_data_toggle = 1;
}
UCONbits.PKTDIS = 0;
}
void EP0_StatusIn(void)
{
switch(ep0_state)
{
case EP0_DATA_IN:
if(ep0_len > 0)
{
/* 次のパケット送信 */
EP0_SendNextPacket();
}
else
{
ep0_state = EP0_STATUS_OUT;
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA1 | _UOWN;
UCONbits.PKTDIS = 0;
if( usb_configured )
{
ep0_state = EP0_END;
}
}
break;
case EP0_STATUS_IN:
/* SET_ADDRESSの完了 */
if(pending_addr)
{
UADDR = pending_addr;
pending_addr = 0;
}
ep0_state = EP0_IDLE;
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA0 | _UOWN;
UCONbits.PKTDIS = 0;
break;
default:
bdt[EP0_OUT].CNT = 0;
bdt[EP0_OUT].STAT = _UOWN | _BSTALL;
UCONbits.PKTDIS = 0;
break;
}
}
void EP0_StatusOut(void)
{
switch(ep0_state)
{
case EP0_STATUS_OUT:
ep0_state = EP0_IDLE;
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA0 | _UOWN;
UCONbits.PKTDIS = 0;
break;
default:
bdt[EP0_OUT].CNT = 8;
bdt[EP0_OUT].STAT = _DATA0 | _UOWN;
UCONbits.PKTDIS = 0;
break;
}
}
usbmain.h のソースコード:
#ifndef USBMAIN_H
#define USBMAIN_H
typedef enum
{
EP0_IDLE,
EP0_DATA_IN,
EP0_STATUS_OUT,
EP0_STATUS_IN,
EP0_END
} EP0_STATE;
EP0_STATE ep0_state = EP0_IDLE;
void USB_Init(void);
void USB_Task(void);
void EP0_ProcessSetup(void);
void EP0_SendNextPacket(void);
void EP0_StatusIn(void);
void EP0_StatusOut(void);
#endif
usbdescriptors.h のソースコード:
#ifndef USB_DESCRIPTORS_H
#define USB_DESCRIPTORS_H
#include <stdint.h>
#define DEVICE_DSC_SIZE 18
#define CONFIG_DSC_SIZE 41
#define REPORT_DSC_SIZE 34
// デバイスディスクリプタ(EP0=8)
const uint8_t device_dsc[DEVICE_DSC_SIZE] = {
18, // bLength
1, // bDescriptorType
0x00, 0x02, // bcdUSB 2.0
0x00, // class
0x00, // subclass
0x00, // protocol
8, // bMaxPacketSize0 (EP0=8)
0xFF,0xFF, // idVendor
0x3f,0x00, // idProduct
0x00,0x01, // bcdDevice
0, // iManufacturer
0, // iProduct
0, // iSerial
1 // bNumConfigurations
};
// コンフィグ + インターフェース + HID + EP1 IN
const uint8_t config_dsc[CONFIG_DSC_SIZE] = {
9, // bLength
0x02, // bDescriptorType (Configuration)
CONFIG_DSC_SIZE, 0x00, // wTotalLength
1, // bNumInterfaces
1, // bConfigurationValue
0, // iConfiguration
0x80, // bmAttributes (Bus powered)
50, // bMaxPower (100mA)
// Interface Descriptor
9, // bLength
0x04, // bDescriptorType (Interface)
0, // bInterfaceNumber
0, // bAlternateSetting
2, // bNumEndpoints (EP1 IN OUT)
0x03, // bInterfaceClass (HID)
0x00, // bInterfaceSubClass
0x00, // bInterfaceProtocol
0, // iInterface
// HID Descriptor
9, // bLength
0x21, // bDescriptorType (HID)
0x11, 0x01, // bcdHID 1.11
0x00, // bCountryCode
1, // bNumDescriptors
0x22, // bDescriptorType (Report)
REPORT_DSC_SIZE, 0x00, // wDescriptorLength
/* Endpoint Descriptor (OUT) */
0x07, // bLength
0x05, // bDescriptorType (Endpoint)
0x01, // bEndpointAddress (OUT, EP1)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize (64 bytes)
0x01, // bInterval
// Endpoint Descriptor (EP1 IN, Interrupt)
7, // bLength
0x05, // bDescriptorType (Endpoint)
0x81, // bEndpointAddress (IN, EP1)
0x03, // bmAttributes (Interrupt)
0x40, 0x00, // wMaxPacketSize
1 // bInterval (10ms)
};
// 超シンプルな HID レポート(8バイト)
const uint8_t hid_rpt_desc[REPORT_DSC_SIZE] = {
0x06, 0x00, 0xFF,
0x09, 0x01,
0xA1, 0x01,
0x09, 0x02,
0x15, 0x00,
0x26, 0xFF, 0x00,
0x75, 0x08,
0x95, 0x08,
0x81, 0x02,
0x09, 0x03,
0x15, 0x00,
0x26, 0xFF, 0x00,
0x75, 0x08,
0x95, 0x08,
0x91, 0x02,
0xC0
};
#endif
usbmain.c、usbmain.h、usbdescriptors.h をプロジェクトに追加する必要があります。
ディスクリプタのUSBベンダーID(VID)についてですが、本記事で使用している VID=0xFFFF は、USB-IF が定める テスト用・非商用向けの予約領域 です。個人開発、学習用途、ブログ公開など市販しないデバイスの場合に使用できます。
市販製品として出荷する場合は USB-IF にベンダーを登録して自分専用の VID を取得する必要があります。
本ソースコードは MPLAB X IDE に MCC のプラグインをインストールしていることが前提となります。MCC をインストールする方法は下記記事を参考にしてください。
MPLAB Code Configurator(MCC)の追加と使い方
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
最後まで、読んでいただきありがとうございました。
