USB HID Device (STM32)

(2015.6.14 作成)

(2015.10.10 更新)

(2017.8.5 更新)

(2020.11.28 全面改訂)

 このページではSTM32F103C8T6基板を使用してHIDプロトコルでUSB通信をするプログラムの作成法を紹介しています。

 HIDプロトコルなので本来はマウスやキーボード用の通信を行うためのプロトコルですが、ここでは64バイトまでの任意のデータをPCと送受信する方法を紹介しています。もしレポートディスクリプタを適切に変更すれば本来の使い方ができると思います。

 一方のPC側の受信ソフトについてもこちらで紹介していますので、必要な場合は併せて参照ください。

ハードウェア

 F103C8T6基板の場合はコネクタ、プルアップ抵抗とも実装されているので改造する必要はありません。

 しかし例えばNucleo基板の場合USBデバイス用のコネクタが搭載されていませんので、自分で接続する必要があります。下は秋月電子さんのUSBコネクタキットを使用した場合の例です。


 D+ピンを1.5KΩで3.3VにプルアップすることでハイスピードUSBデバイスとして認識させます。またSTM32の種類によってはプルアップ抵抗が内蔵されているものもあります。

CubeMXを使用した方法

 GUI操作でプログラムのひな型を作ってくれるCubeMXを使えばHID設定の大部分を行うことができます。ただし設定を変えて再出力するたびに以下の改変したコードが書き換えられてしまいますので、管理人の場合は下に示す一つのまとまったライブラリとして扱うようにしています。

 細かな設定やCubeMXの使用法等は他サイト様の解説に期待するとしてここでは主要部分だけ示します。

CubeMXでの設定

何を置いてもまずはUSB Device(FS)にチェックを入れて機能を有効にします。F103はFS=Full speedしか対応していませんが、F4などだとHS = High speedに対応することもできると思います。

 次にMiddlewareからCustomHIDを選びます。

 Device DescripterのVIDとPIDはデフォルト値をそのまま使用したほうが良いと思います。ここを変えるにはUSB規格団体(USB-IF)に上納金を奉納する必要があるのではないかと思います。

 次はパラメータの変更を行います。この時のパラメター設定は以下の通りです。

CUSTOM_HID_FS_BINTERVAL: 1

    マスタからのポーリング希望間隔。単位はmsです。USBは接続されているデバイスをマスタが順にチェックしてゆき、デバイスが手を挙げていると通信が開始される規格です。このときマスタがチェックする時間間隔をデバイス側が提案するのですが、その希望値がこの値です。当然短くすると応答が早くなりますが、マスタやバスの占有率は上がります。

USBD_CUSTOM_HID_REPORT_DESC_SIZE: 32

 下に示すレポートディスクリプタのサイズです。今回(任意データ送受信)の場合は32バイトになります。

USBD_CUSTOMHID_OUTREPORT_BUF_SIZE: 64

 送受信するデータサイズです。64バイトまでの任意のサイズです。

 設定が終われば書き出すのですが、この際リンクではなく必要なファイルをコピーするようにしてください。ライブラリ本体のファイル(usbd_customhid.h/.c)を編集するためです。

設定ファイルの編集

 プロジェクトに吐き出されるファイルを手動でいくつか編集する必要があります。

〇usbd_customhid.hの変更

#define CUSTOM_HID_EPIN_SIZE 0x02U

#define CUSTOM_HID_EPOUT_SIZE  0x02U

の0x02UをUSBD_CUSTOMHID_OUTREPORT_BUF_SIZEに変更。

 

struct _USBD_CUSTOM_HID_Itfの定義を
  int8_t (* OutEvent)(uint8_t event_idx, uint8_t state);
    → int8_t (* OutEvent)(uint8_t* inData);

〇 usbd_customhid.cの変更

以下の2つの関数

USBD_CUSTOM_HID_DataOutと
USBD_CUSTOM_HID_EP0_RxReady

の以下の行
((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf[0], hhid->Report_buf[1]);
を以下のとおり変更
((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf);

〇usbd_custom_hid_if.cの変更

USBD_CUSTOM_HID_SendReport_FSのコメントアウト解除+static宣言を外す。

 

関数
static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state);
を以下のとおり変更
static int8_t CUSTOM_HID_OutEvent_FS(uint8_t* inData);
関数の中はPCからの受信データに合わせて編集します。

 

レポートディスクリプタ(CUSTOM_HID_ReportDesc_FS)を置き換えます。置き換えるディスクリプタは後に示します。

 

〇usbd_custom_hid_if.hの変更

先ほどコメントアウト解除したUSBD_CUSTOM_HID_SendReport_FSをエキスポート宣言します。具体的には

int8_t USBD_CUSTOM_HID_SendReport_FS(uint8_t *report, uint16_t len);

をEXPORTED_FUNCTIONSの欄に追記します。

<レポートディスクリプタ>

usbd_custom_hid_if.c内で置き換えるディスクリプタは以下のものです。そのままコピペでいいと思います。

__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END
=
{ 0x06, 0xFF, 0x00, /* USAGE_PAGE (Vendor Page: 0xFF00) 0x06=0b000001_10=global_USAGE_PAGE_2byte, 0xFF00= Vendor defined*/
0x09, 0x01, /* USAGE (Demo Kit)                 0x09=0b000010_01=local _USAGE_1byte, 0x01=???*/
0xa1, 0x01, /* COLLECTION (Application)       */

/* In */
0x09, 0x01, /*     USAGE (ベンダ定義)         local 0x09=0b000010_01=local _USAGE_1byte, 0x01=???*/
0x15, 0x00, /*     LOGICAL_MINIMUM (0)            global */
0x25, 0xFF, /*     LOGICAL_MAXIMUM (255)          global */
0x75, 0x08, /*     REPORT_SIZE (8)                global 8bit*/
0x95, USBD_CUSTOMHID_OUTREPORT_BUF_SIZE, /*     REPORT_COUNT               global except Report ID*/
0x81, 0x82, /*     Input (Data,Var,Abs,Vol)  0b00000_0010*/

/* Out */
0x09, 0x02, /*     USAGE (ベンダ定義)         local 0x09=0b000010_01=local _USAGE_1byte, 0x01=???*/
0x15, 0x00, /*     LOGICAL_MINIMUM (0)            global */
0x25, 0xFF, /*     LOGICAL_MAXIMUM (255)          global */
0x75, 0x08, /*     REPORT_SIZE (8)                global 8bit*/
0x95, USBD_CUSTOMHID_OUTREPORT_BUF_SIZE, /*     REPORT_COUNT               global except Report ID */
0x91, 0x82, /*     Output (Data,Var,Abs,Vol)  0b00000_0010*/

0xc0 /*     END_COLLECTION                 */
};

送信コード

 送りたいところでUSBD_CUSTOM_HID_SendReport_FSをよびだすだけです。例えばボタンを押された際の割り込みで送信する場合以下のようなプログラムになります。

void EXTI3_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI3_IRQn 0 */

  /* USER CODE END EXTI3_IRQn 0 */
  if (LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_3) != RESET)
  {
    LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_3);
    /* USER CODE BEGIN LL_EXTI_LINE_3 */
    uint8_t res[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE];
    for (uint8_t i=0;i < USBD_CUSTOMHID_OUTREPORT_BUF_SIZE;i++)
        res[i]=cnt+i;
    cnt++;
    USBD_CUSTOM_HID_SendReport_FS(res,USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);

    /* USER CODE END LL_EXTI_LINE_3 */
  }
  /* USER CODE BEGIN EXTI3_IRQn 1 */

  /* USER CODE END EXTI3_IRQn 1 */
}

受信コード

受信時にはusbd_custom_hid_if.cのCUSTOM_HID_OutEvent_FSが呼び出されます。例えばLEDの点灯を制御する場合は以下の通り編集します。

static int8_t CUSTOM_HID_OutEvent_FS(uint8_t* inData)
{
  /* USER CODE BEGIN 6 */
        if ((inData[0] == 00) && (inData[1] == 01))
                LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_6);
        else
                LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_6);
  return (USBD_OK);
  /* USER CODE END 6 */
}

ふぅ~。お疲れさまでした。管理人の環境では以上の変更で動くようになりました。ただ、毎回変更は大変ですよね。しかも今回書き直した設定のいくつかはCubeMXで設定値を変更するたびに上書きされて消えてしまいます。

そこで管理人は以下の通りの編集を行って別ライブラリを作成しています。

独立ライブラリの作成

 上述の設定を含んだ形で管理人は独立したHID USBライブラリを作成しています。こちらからダウンロードできます(DKSlib_USBHID_F103xB.zip)。

 インクルードディレクトリやソースディレクトリは毎回設定しないといけませんが関数の中身については編集する必要がないので個別具体的なことを覚えていないでも使えるようになるところがメリットです。

 具体的に何をどうしたかは管理人の忘備録も兼ねて最後に示しますが、まずはデータの送受信を含めた使い勝手は以下のような感じになります。動作としては上記のCubeMXを使用したものと同一です。


#include "DKS_Common_F103xB.h"
#include "DKS_USBHID_F103xB.h"
#include "DKS_GPIO_F103xB.h"

DKS::DigitalOut pa6;
DKS::InterupptIn pa3;
uint8_t cnt;

int8_t DataReceive(uint8_t *InData)
{
        if ((InData[0] == 0) && (InData[1] == 1))
                pa6.write(1);
        else
                pa6.write(0);
        return DKS::DKS_OK;
}

int main(void)
{
        DKS::InitSystem();
        DKS::Usb::Hid::Init(&DataReceive);
        pa6.Init(GPIOA, LL_GPIO_PIN_6, DKS::Push_Pull, DKS::No_Pull);
        pa3.Init(GPIOA, LL_GPIO_PIN_3, DKS::No_Pull, DKS::IT_Rising);
        cnt=0;
        pa3.enable_irq();
        for (;;) ;
}

extern "C" void EXTI3_IRQHandler(void)
{
        if (LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_3) != RESET)
        {
                LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_3);
                uint8_t res[USBD_CUSTOMHID_OUTREPORT_BUF_SIZE];
                for (uint8_t i = 0; i < USBD_CUSTOMHID_OUTREPORT_BUF_SIZE; i++)
                        res[i] = cnt + i;
                cnt++;
                DKS::Usb::Hid::SendReport(res, USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);
        }
}

どうでしょうか?全体だとさすがに長いですが、CubeMXを使用するよりずいぶんすっきり見やすくなったのではないかと思います。複数のファイルにプログラムが分散するCubeMXよりよほど使いやすいと思っています。

具体的な変更点

管理人の備忘録のようなものです。

上記修正に加え、

-)stm32f1xx_it.cから以下の関数を削除
    void USB_LP_CAN1_RX0_IRQHandler(void)
-) usbd_custom_hid_if.c/h ファイルの削除
-) usb_device.cから
    ・以下の宣言を削除
    #include "usbd_custom_hid_if.h"
    ・以下の宣言を追加
    extern USBD_CUSTOM_HID_ItfTypeDef USBD_CustomHID_fops_FS;
    #include "DKS_Common_STM32HAL.h"
    ・ファイル名をusb_device.cppに変更

-) usbd_conf.hから

    ・#include "main.h"を削除

-) DKS_USBHID_F103xB.h/cppを新規作成

注意点

USB割り込みの関数には以下の2種類の名前のどちらかが用いられます。どういう基準かわかりませんが、同じHALドライバでも違うことがあります。謎のエラーが出たときは関数名を疑ってみてください。

USB_LP_CAN_RX0_IRQHandler(void)
USB_LP_CAN1_RX0_IRQHandler(void)

ふぅ~。いかがでしょうか?文字ばかりでなんのこっちゃ?という感じかと思いますが、誰かの参考になればと思いました。

コメント: 0 (ディスカッションは終了しました。)
    まだコメントはありません。