PR

PICマイコン(PIC18F14K50)でUSBを実装する(SETUP編)

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

PIC18F14K50 を用いて USB HID デバイスを構築するために、SETUP パケット処理とデバイス・コンフィグレーション・HID レポートの各ディスクリプタ実装し、Windows のデバイスマネージャで認識される方法をまとめました。

PIC18F14K50 を実装した基板は AE-USB-I02.0-TYPEC(秋月電子)を使用しています。以下では PIC モジュールとします。

PIC モジュールで USB が挿入されたことを認識できるまでの初期化は下記記事にまとめています。

PICマイコン(PIC18F14K50)でUSBを実装する(初期化編)

EP0 を使って SETUP を実装

SETUPの全体構成
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レジスタ

引用:PIC18F14K50 のデータシート REGISTER 22-3 USTAT
引用:PIC18F14K50 のデータシート REGISTER 22-3 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 でリクエスト

BDT.STATの所有権とDATAトグルの説明
BDT.STATの所有権とDATAトグルの説明

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 バイトを超える場合は 図の③・④のように 複数パケットに分割して送信する必要があります。

バイト項目名説明
0bLength18デバイスディスクリプタのサイズ(固定)
1bDescriptorType1デバイスディスクリプタ
2–3bcdUSB0x0200USB 2.0
4bDeviceClass0クラスなし(インターフェース側で定義)
5bDeviceSubClass0サブクラスなし
6bDeviceProtocol0プロトコルなし
7bMaxPacketSize08EP0 の最大パケットサイズ(PICは8固定)
8–9idVendor0xFFFF学習用のベンダーID
10–11idProduct0x003F製品ID
12–13bcdDevice0x0100デバイスバージョン 1.00
14iManufacturer0文字列なし
15iProduct0文字列なし
16iSerial0文字列なし
17bNumConfigurations1構成は1つ
Device Descriptorまとめ

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 バイトずつ分割して返します

バイト項目名説明
0bLength9ディスクリプタのサイズ
1bDescriptorType0x02Configuration Descriptor
2–3wTotalLengthCONFIG_DSC_SIZEこの構成に含まれる全ディスクリプタの合計サイズ
4bNumInterfaces1インターフェース数(HID 1つ)
5bConfigurationValue1この構成の番号
6iConfiguration0文字列なし
7bmAttributes0x80Bus Powered(自己給電ではない)
8bMaxPower50100mA(値×2mA)
Configuration Descriptor ヘッダーまとめ

CONFIG_DSC_SIZEはエンドポイントの数によって値が変わります。

バイト項目名説明
9bLength9ディスクリプタのサイズ
10bDescriptorType  0x04Interface Descriptor
11bInterfaceNumber0インターフェース番号
12bAlternateSetting0Alternate 設定なし
13bNumEndpoints2EP1 IN / EP1 OUT の2つ
14bInterfaceClass0x03HID クラス
15bInterfaceSubClass0サブクラスなし
16bInterfaceProtocol0プロトコルなし
17iInterface0文字列なし
Configuration Descriptor Interface Descriptorまとめ
バイト項目名説明
18bLength9ディスクリプタのサイズ
19bDescriptorType0x21HID Descriptor
20–21bcdHID0x0111HID Version 1.11
22bCountryCode0国別コードなし
23bNumDescriptors1HID Report Descriptor が1つ
24bDescriptorType0x22Report Descriptor
25–26wDescriptorLengthREPORT_DSC_SIZEHID Report Descriptor のサイズ
Configuration Descriptor HID Descriptorまとめ

REPORT_DSC_SIZEはレポートの内容によって値が変わります。

バイト項目名説明
27bLength7ディスクリプタのサイズ
28bDescriptorType0x05Endpoint Descriptor
29bEndpointAddress0x01EP1 OUT
30bmAttributes0x03Interrupt Transfer
31–32wMaxPacketSize64最大64バイト
33bInterval1ポーリング間隔(1ms)
Configuration Descriptor Endpoint Descriptor(EP1_OUT)まとめ
バイト項目名説明
34bLength7ディスクリプタのサイズ
35bDescriptorType0x05Endpoint Descriptor
36bEndpointAddress0x81EP1 IN
37bmAttributes0x03Interrupt Transfer
38–39wMaxPacketSize64最大64バイト
40bInterval1ポーリング間隔(1ms)
Configuration Descriptor Endpoint Descriptor(EP1_IN)まとめ

エントリーポイント毎に必要に応じて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–20x06, 0x00, 0xFFUsage Page (Vendor Defined)ベンダー独自の用途(0xFF00)
3–40x09, 0x01Usage (1)デバイスの機能を示す(Vendor Usage 1)
5–60xA1, 0x01Collection (Application)アプリケーションコレクション開始
HID Report Descriptor レポート全体の情報まとめ
バイト項目名説明
7–80x09, 0x02Usage (2)入力レポートの用途
9–100x15, 0x00Logical Minimum (0)最小値 0
11–130x26, 0xFF, 0x00Logical Maximum (255)最大値 255
140x75Report Size1要素=8bit
150x08Report Size 値8bit
160x95Report Count要素数
170x08Report Count 値8要素(8バイト)
180x81Input入力レポート
190x02Data, Var, Absデータ/変数/絶対値
HID Report Descriptor Input レポート
バイト項目名説明
20–210x09, 0x03Usage (3)出力レポートの用途
22–230x15, 0x00Logical Minimum (0)最小値 0
24–260x26, 0xFF, 0x00Logical Maximum (255)最大値 255
270x75Report Size1要素=8bit
280x08Report Size 値8bit
290x95Report Count要素数
300x08Report Count 値8要素(8バイト)
310x91Output出力レポート
320x02Data, Var, Absデータ/変数/絶対値
HID Report Descriptor Output レポート
バイト項目名説明
330xC0End Collectionコレクション終了
HID Report Descriptor Collection 終了

スポンサーリンク

動作確認

動作確認用の回路図
動作確認用の回路図

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

デバイスマネージャーの表示
デバイスマネージャの表示

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

USB Device Viewerでディスクリプタ内容を表示
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で学べるソフト開発と標準ライブラリの使い方

Raspberry Pi Picoで学べるソフト開発

PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました