2020年2月7日金曜日

STM32F042K6T6 で USB マウス & キーボード & LED を実装する

USB キーボード & マウスになっている STM32F042K6T6 ブレークアウト基板(右端の LED が CAPS 状態)

STM32CubeIDE を使って USB マウスや USB キーボードを作る方法は、既に色々なサイトで分かり易く解説されているので、自分でもそれに倣って動かすことが出来た。

だけど、一歩進んで、

  • マウスとキーボードの両方として動かすには?
  • CAPS ロックなどのインジケータの状態を取得するには?

というところはすぐには分からず、ちょっとつまづいたので、色々調べて試行錯誤して分かった方法をここにメモしておく。

動いてしまえばなんてことないんだけど、動くまでが大変だよね。


はじめに


USB マウスや USB キーボードを作る時は、STM32CubeMX で HID Class を指定している例が多い。しかし、ここでは Custom HID Class を使って雛型コードを生成する。その理由は、HID Class だと、

  1. CubeMX でコード生成をする度に、レポート デイスクリプタが HID マウスのものに毎回書き戻されてしまう
  2. HID の雛型コードでは、OUT 方向(ホストからデバイスの向き)のエンドポイント(データ入出力の FIFO)である EPOUT が作られないので、LED 表示のデータを受け取れない

という(難)点があるからだ。ただし、1については、Custom HID Class を使っても、完全には書き戻しを避けられない。


環境


以下では、

  • macOS Catalina 10.15.2
  • STM32CubeIDE Version 1.2.0
  • STM32F042K6T6 ブレークアウト基板(自作)

を使用している。


ボード


冒頭の写真にあるように、STM32F042K6T6 ブレークアウト基板に、外付けタクトスイッチを 4 個付けたものを使った。

ブレークアウト基板自体には、BOOT0 兼用スイッチと、表示用 LED が 2 個付いている。

そして、SWD 無しの最小構成で、DFU 経由でバイナリを書き込む。これだとデバッガは使えないけど、USB ケーブル 1 本で作業が出来る。


CubeMX での設定

USB の設定


"Connectivity" の "USB" で、"Device (FS)" にチェックを入れる。


また、"Middleware" の "USB_DEVICE" で、"Custom Human Interface Device Class (HID)" を選択し、


以下のパラメータを変更する:

  • CUSTOM_HID_FS_BINTERVAL = 0x1
  • USBD_CUSTOM_HID_REPORT_DESC_SIZE = 119

最初のは HID イベントを送る周期で、単位は ms。ここではホストから 1ms でポーリングしてもらう設定にする。2 つ目については、後述するキーボードとマウスのレポート ディスクリプタのサイズを指定している。


クロックの設定


"System Core" の "RCC" で、"CRS SYNC Source USB" を選択する。


また、ここで "Clock Configuration" タブをクリックすると、"Do you want to run automatic clock issues solver?" と聞かれるので、"Yes" をクリックする。
HSI48 が使われていれば問題ないと思う。これで USB に同期した 48MHz クロックで動作するのだと思う。


タイマーの設定


"Pinout & Configuration" タブに戻り、"Timers" の "TIM14" の "Activated" をチェックし、1ms 毎に処理を行いたいので、以下のパラメータを変更する。

  • Presaler = 48 - 1
  • Counter Period = 1000 - 1


また、"NVIC Settings" で Enabled をチェックして割り込みを許可する。



GPIO の設定


"System Core" の "GPIO" で、タクトスイッチと LED 用の設定をする。


SW0 は BOOT0 兼用で、ブレークアウト基板にプルアップ抵抗と共に備付けられているタクトスイッチ。そのため、SW0 はプルアップもプルダウンも無しにする。

それ以外のタクトスイッチは、すべてプルアップに設定する。各スイッチの反対側は GND に落としている。

また、ブレークアウト基板上の LED 2 つ用の出力ピン LED1, LED2 も設定する。


USB 関連コードの変更

レポート ディスクリプタの定義


マウス用とキーボード用のそれぞれを盛り込むんだレポート ディスクリプタを定義する。基本は、 usb.org の

Device Class Definition for HID 1.11

にある hid1_11.pdf の

  • p.69 Appendix E.6 Report Descriptor (Keyboard)
  • p.71 Appendix E.10 Report Descriptor (Mouse)

からのコピペだけど、マウスなのかキーボードなのかを識別するために、ReportID をそれぞれの先頭に追加する。

USB_DEVICE/App/usbd_custom_hid_if.c の 91 行目にある CUSTOM_HID_ReportDesc_FS を以下のように書き換える。

/** Usb HID report descriptor. */
__ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END =
{
  /* USER CODE BEGIN 0 */
  /* 47+18 */
  0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
  0x09, 0x06,                    // USAGE (Keyboard)
  0xa1, 0x01,                    // COLLECTION (Application)
  0x85, 0x01,                    //   REPORT_ID (1)
  0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
  0x19, 0xe0,                    //   USAGE_MINIMUM (Keyboard LeftControl)
  0x29, 0xe7,                    //   USAGE_MAXIMUM (Keyboard Right GUI)
  0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
  0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
  0x75, 0x01,                    //   REPORT_SIZE (1)
  0x95, 0x08,                    //   REPORT_COUNT (8)
  0x81, 0x02,                    //   INPUT (Data,Var,Abs)
  0x95, 0x01,                    //   REPORT_COUNT (1)
  0x75, 0x08,                    //   REPORT_SIZE (8)
  0x81, 0x03,                    //   INPUT (Cnst,Var,Abs)

  0x95, 0x05,                    //   REPORT_COUNT (5)
  0x75, 0x01,                    //   REPORT_SIZE (1)
  0x05, 0x08,                    //   USAGE_PAGE (LEDs)
  0x19, 0x01,                    //   USAGE_MINIMUM (Num Lock)
  0x29, 0x05,                    //   USAGE_MAXIMUM (Kana)
  0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)
  0x95, 0x01,                    //   REPORT_COUNT (1)
  0x75, 0x03,                    //   REPORT_SIZE (3)
  0x91, 0x03,                    //   OUTPUT (Cnst,Var,Abs)

  0x95, 0x06,                    //   REPORT_COUNT (6)
  0x75, 0x08,                    //   REPORT_SIZE (8)
  0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
  0x25, 0x65,                    //   LOGICAL_MAXIMUM (101)
  0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
  0x19, 0x00,                    //   USAGE_MINIMUM (Reserved (no event indicated))
  0x29, 0x65,                    //   USAGE_MAXIMUM (Keyboard Application)
  0x81, 0x00,                    //   INPUT (Data,Ary,Abs)

  0xc0,                          // END_COLLECTION

  /* 54 */
  0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
  0x09, 0x02,                    // USAGE (Mouse)
  0xa1, 0x01,                    // COLLECTION (Application)
  0x09, 0x01,                    //   USAGE (Pointer)
  0xa1, 0x00,                    //   COLLECTION (Physical)
  0x85, 0x02,                    //     REPORT_ID (2)
  0x05, 0x09,                    //     USAGE_PAGE (Button)
  0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
  0x29, 0x03,                    //     USAGE_MAXIMUM (Button 3)
  0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
  0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
  0x95, 0x03,                    //     REPORT_COUNT (3)
  0x75, 0x01,                    //     REPORT_SIZE (1)
  0x81, 0x02,                    //     INPUT (Data,Var,Abs)
  0x95, 0x01,                    //     REPORT_COUNT (1)
  0x75, 0x05,                    //     REPORT_SIZE (5)
  0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
  0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
  0x09, 0x30,                    //     USAGE (X)
  0x09, 0x31,                    //     USAGE (Y)
  0x09, 0x38,                    //     USAGE (Wheel)
  0x15, 0x81,                    //     LOGICAL_MINIMUM (-127)
  0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
  0x75, 0x08,                    //     REPORT_SIZE (8)
  0x95, 0x03,                    //     REPORT_COUNT (3)
  0x81, 0x06,                    //     INPUT (Data,Var,Rel)
  0xc0,                          //   END_COLLECTION
//0xc0,                          // END_COLLECTION
  /* USER CODE END 0 */
  0xC0    /*     END_COLLECTION              */
};

/* USER CODE BEGIN PRIVATE_VARIABLES */

これ全体で 119 バイトなので、先にディスクリプタのサイズ USBD_CUSTOM_HID_REPORT_DESC_SIZE にこの値を指定したのだった。


EPIN サイズを 9 に変更


次に、ホストへ送るエンドポイント EPIN のサイズを、マウスのレポート構造体と、キーボードのレポート構造体の、サイズの大きい方に書き換える。ここでは、キーボードの構造体の先頭に ReportID を付け加えた、9 バイトを指定する。

Middlewares/ST/STM32_USB_Device_Library/Class/CustomHID/Inc/usbd_customhid.h の 45 行目の CUSTOM_HID_EPIN_SIZE の値を 0x09U に変更する。

/** @defgroup USBD_CUSTOM_HID_Exported_Defines
  * @{
  */
#define CUSTOM_HID_EPIN_ADDR                 0x81U
#define CUSTOM_HID_EPIN_SIZE                 0x09U

この値は、CubeMX がコードを生成する度にデフォルト値の 2 に上書きされてしまうので注意!

そこで、書き戻された時に、すぐ気付けるよう、#error を仕込んでおこう。

USB_DEVICE/App/usbd_custom_hid_if.c の 65 行目に、

/* USER CODE BEGIN PRIVATE_DEFINES */
#if CUSTOM_HID_EPIN_SIZE != 0x09U
#error "CUSTOM_HID_EPIN_SIZE is not 0x09U!"
#endif
/* USER CODE END PRIVATE_DEFINES */

としておいて、この値が 9 じゃなかったら、コンパイルエラーになるようにしておく。


EPOUT コールバック関数の実装


一方、LED ステータス用のデータにも、上記 ReportID が付加されて返ってくるようなので、EPOUT 側は ReportID とステータスを合わせた 2 バイトが必要だけど、こちらは CUSTOM_HID_EPOUT_SIZE のデフォルト値が 2 になっているので、変更は要らない。

Middlewares/ST/STM32_USB_Device_Library/Class/CustomHID/Inc/usbd_customhid.h の 48 行目に定義されている。

#define CUSTOM_HID_EPOUT_ADDR                0x01U
#define CUSTOM_HID_EPOUT_SIZE                0x02U

そして、PC から LED ステータスが送られてきた時の処理は、
USB_DEVICE/App/usbd_custom_hid_if.c の 245 行目の CUSTOM_HID_OutEvent_FS() に書く。

/**
  * @brief  Manage the CUSTOM HID class events
  * @param  event_idx: Event index
  * @param  state: Event state
  * @retval USBD_OK if all operations are OK else USBD_FAIL
  */
static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state)

この関数は 2 つの引数を持っているけど、この関数を呼んでいる
Middlewares/ST/STM32_USB_Device_Library/Class/CustomHID/Src/usbd_customhid.c(642)
を見ると、これら 2 つの引数は、受信したデータの 1 バイト目と 2 バイト目になっていることが分かる。

static uint8_t  USBD_CUSTOM_HID_DataOut(USBD_HandleTypeDef *pdev,
                                        uint8_t epnum)
{

  USBD_CUSTOM_HID_HandleTypeDef     *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData;

  ((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->OutEvent(hhid->Report_buf[0],
                                                            hhid->Report_buf[1]);

  USBD_LL_PrepareReceive(pdev, CUSTOM_HID_EPOUT_ADDR, hhid->Report_buf,
                         USBD_CUSTOMHID_OUTREPORT_BUF_SIZE);

  return USBD_OK;
}

今回の状況では、1 バイト目がキーボードの ReportID の 1 になっていて、2 バイト目が、CAPS ロックなどの LED ステータスが入っているようだ。

この LED ステータスの中身はというと、先のキーボードのレポート ディスクリプタの "USAGE PAGE (LEDs)" で指定されたものになっている。今度は usb.org の

HID Usage Tables 1.12

に置かれている hut1_12v2.pdf の p.61 "11 LED Page (0x08)" に、各ビットが何を意味するかが定義されている。

普通は最初の 5 ビットを送ってもらうようにするようだ。つまり、2バイト目 state の最初の 5 ビットには、

  1. Num Lock
  2. Caps Lock
  3. Scroll Lock
  4. Compose
  5. Kana

のステータスが格納されている。

今回は、CAPS がオンの時に、LED2 を点灯させることにする。ちょっとかっこ悪いけど、変更量を最小限にするために、まず
USB_DEVICE/App/usbd_custom_hid_if.c の 26 行目で main.h をインクルードし、LED2 の情報が取れるようにしておいて、

/* USER CODE BEGIN INCLUDE */
#include "main.h"
/* USER CODE END INCLUDE */

USB_DEVICE/App/usbd_custom_hid_if.c の 245 行目で、

static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state)
{
  /* USER CODE BEGIN 6 */
  HAL_GPIO_WritePin( LED2_GPIO_Port, LED2_Pin, (state & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET );
  return (USBD_OK);
  /* USER CODE END 6 */
}

というように、state の 2 ビット目が立っていたら、LED2 を点灯するようにする。


main.c の変更

HID クラス


28 行目で usbd_custom_hid_if.h をインクルードし、
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "usbd_custom_hid_if.h"
/* USER CODE END Includes */

48行目で usb デバイスのインスタンスを extern 宣言する。

/* USER CODE BEGIN PV */
extern USBD_HandleTypeDef hUsbDeviceFS;
/* USER CODE END PV */

レポート用構造体の定義


マウスとキーボードのレポート用の構造体には、それぞれ先頭に ReportID のフィールドを付加して、ディスクリプタ通りの固定値を書いておく。

少し戻って、30 行目で構造体を定義。
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
struct keyboardHID_t {
  uint8_t report_id;
  uint8_t modifiers;
  uint8_t reserved;
  uint8_t key[6];
};
struct mouseHID_t {
  uint8_t report_id;
  uint8_t buttons;
  int8_t x;
  int8_t y;
  int8_t wheel;
};
/* USER CODE END PTD */

そして、main() 関数の最初で初期化する。

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
  struct keyboardHID_t keyboardHID;
  keyboardHID.report_id = 1;
  keyboardHID.modifiers = 0;
  keyboardHID.reserved = 0;
  for (int8_t i = 0; i < 6; ++i) {
    keyboardHID.key[i] = 0;
  }

  struct mouseHID_t mouseHID;
  mouseHID.report_id = 2;
  mouseHID.buttons = 0;
  mouseHID.x = 0;
  mouseHID.y = 0;
  mouseHID.wheel = 0;
  /* USER CODE END 1 */


タイマー待ち


73 行目に、TIM14 を待って USB 処理を開始するための volatile 変数と、割り込みハンドラを記述する。

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
static volatile uint8_t timer_intr = 0;
void HAL_TIM_PeriodElapsedCallback( TIM_HandleTypeDef* htim )
{
  if (htim->Instance == TIM14) {
    timer_intr = 1;
  }
}
/* USER CODE END 0 */

そして、while ループの手前で、タイマを開始する。

  /* USER CODE BEGIN 2 */
  HAL_TIM_Base_Start_IT( &htim14 );
  /* USER CODE END 2 */


while ループ


最後に、メインの while ループの中を以下のように実装する。

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  uint8_t idx = 0;
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    while (timer_intr == 0);
    timer_intr = 0;

    const uint8_t sw0_on = (HAL_GPIO_ReadPin( SW0_GPIO_Port, SW0_Pin ) == GPIO_PIN_SET)   ? 1 : 0;
    const uint8_t sw1_on = (HAL_GPIO_ReadPin( SW1_GPIO_Port, SW1_Pin ) == GPIO_PIN_RESET) ? 1 : 0;
    const uint8_t sw2_on = (HAL_GPIO_ReadPin( SW2_GPIO_Port, SW2_Pin ) == GPIO_PIN_RESET) ? 1 : 0;
    const uint8_t sw3_on = (HAL_GPIO_ReadPin( SW3_GPIO_Port, SW3_Pin ) == GPIO_PIN_RESET) ? 1 : 0;
    const uint8_t sw4_on = (HAL_GPIO_ReadPin( SW4_GPIO_Port, SW4_Pin ) == GPIO_PIN_RESET) ? 1 : 0;

    idx += 1;
    if (idx & 1) {// keyboard
      keyboardHID.key[4] = (sw3_on) ? 0x04 : 0; // 'a'
      keyboardHID.key[5] = (sw4_on) ? 0x39 : 0; // CAPS
      USBD_CUSTOM_HID_SendReport( &hUsbDeviceFS, (uint8_t*) &keyboardHID, sizeof( struct keyboardHID_t ) );
    } else {// mouse
      mouseHID.buttons = sw0_on;
      mouseHID.x = sw1_on - sw2_on;
      USBD_CUSTOM_HID_SendReport( &hUsbDeviceFS, (uint8_t*) &mouseHID, sizeof( struct mouseHID_t ) );
    }
    const uint8_t led1_on = sw1_on ^ sw2_on;
    HAL_GPIO_WritePin( LED1_GPIO_Port, LED1_Pin, (led1_on) ? GPIO_PIN_SET : GPIO_PIN_RESET );
  }
  /* USER CODE END 3 */
}

ここでは、各タクトスイッチを以下の操作に対応させてみた。

  • SW0:マウスの左クリック
  • SW1:マウスカーソルの右移動
  • SW2:マウスカーソルの左移動
  • SW3:キーボードの a キー
  • SW4:キーボードの CAPS キー

また、テスト目的で、キーボードの構造体の最後の方(インデックスが 4 と 5)を使うようにしてみた。EPIN のサイズが足りていないと、ここが送られないので、SW3, 4 が動かなくなることが確認できる。

LED1 については、マウスカーソルが移動する時に点灯するようにしてみた。

あと、どうするのがベストなのかは分からないけど、上ではキーボードとマウスのレポートを交互に送信するようにしてみた。


ファームウエアの DFU 書き込み


では、Release バイナリをビルドして書き込もう。

まず、MPU を BOOTLOADER モードにするために、BOOT0 スイッチを押しながら、Reset スイッチを長めに(2〜3秒?)押す。そして、システム情報の USB の一覧で
のように出ていること確認する。もしダメなら USB ケーブルを一旦抜いて、挿し直す。

BOOTLOADER モードになっていることが確認できたら、以下のコマンドをコマンドラインから実行する。

/Applications/STM32CubeIDE.app/Contents/Eclipse/plugins/com.st.stm32cube.ide.mcu.externaltools.cubeprogrammer.macos64_1.1.0.201910081157/tools/bin/STM32_Programmer_CLI -c port=usb1 -w STM32CubeIDE/workspace_1.2.0/UsbKeyMouse/Release/UsbKeyMouse.elf

以下に実際の実行画面を示す。
そして、システム情報の USB の一覧が
のようになっていることを確認する。Reset をかけるか、再度 USB ケーブルの抜き差しが必要かも知れない。


動作テスト


上記の実行画面で、最後のプロンプトの "AAaaAAaa" という文字列が選択されている部分が、今回のボードの各スイッチを使って、地味に

  1. 大文字小文字の a を入力
  2. マウスの横移動
  3. 左ダブルクリックで全選択

をしたもの(笑)

あとは、SW4 の CAPS を押して、LED の点灯・消灯を確認できたら完成!

macOS と Windows では、CAPS キーがキーボードごとに個別か、全キーボードで共通か、の違いがあるようだった。


参考サイト





0 件のコメント:

コメントを投稿