STM32F103で10ボタンのみのUSBジョイパッドを作る

電子工作
この記事は約16分で読めます。

背景

AliExpressの怪しいマイコンを購入

STM32F103C6T6

2022/10/24に購入し、11/5に到着

こちら商品を購入

※色選択操作でマイコンが異なります。
「STM32F103C6T6」を選択
円安の時期を考慮すると割高ですがそれでも2022/10/24時点で1個188円、送料とのバランスを考えて10個注文しました。

画像
画像

価格については、このIC自体は生産終了品みたいなのでそれがどこぞから流れてきたのかもしれません、知らんけど。

CubeIDEプロジェクトの立ち上げ

CubeIDEのバージョン:1.10.1
(Pleiadesによる日本語化をしています)

  • ファイル -> 新規 -> STM32 Project
  • MCU/MPU Selector タブ

    Commercial Part Nuber ドロップダウンボックスから「STM32F103C6T6A」を選択

  • 右ペインの「MCUs/MPUs List」

    「STM32F103C6T6A」を選択して「次へ」<

  • プロジェクトのセットアップウインドウが出てくるので

    プロジェクト名を設定 (ここでは「GamePad_F103」とする)
    Targeted Languageはお好みで(C++は使わないのでここでは「C」を選択)
    Targeted Binary Typeは「実行可能」
    Targeted Project Typeは「STM32Cube」
    あとは既定で良いので「完了」をボタンを押下

初期設定

  • まずはデバッグの設定から
    • System Core -> SYS から
      Debug を Serial Wire
      Timebase Source を SysTick に設定。
  • 続いてクロックソースの設定
    • System Core -> RCC から
      High Speed Clock (HSE) を Crystal/Ceramic Resonator に設定
  • USBの設定
    • Connectivity -> USB から
      Device (FS) にチェック
  • 続けてUSBミドルウェアの設定
    • Middleware -> USB_DEVICE から
      Class For FS IP を Human Interface Device Class (HID) を選択
  • RCCでHSEを設定することにより、Clock Configration で外部クロックを選択できるようになるので、クロックを設定していきます。
    特に省電力を求めるとかでなければ絞る必要は無いので最大速度にします。

    USBを設定した時点でクロック設定がおかしい(初期値ではUSB供給クロックが正しくない)ので Clock Configration を開いた時点で、自動で補正するかとダイアログが出てきますがどとみち設定するのでここでは「No」を選択しておきます。
    (Yesを選択してしまっても大丈夫です)
    • ①Clock Configration タブに切り替え
      ②PLL Source Mux を HSE に設定
      ③PLLMul を x9 に設定
      ④USB Prescaler を /1.5 に設定
      ⑤System Clock Mux を PLLCLK に設定
      ⑥APB1 Prescaler を /2 に設定
  • メインループ調整用にタイマーも設定しておきます。
    適当に待ちを入れても良いのですが、時間調整が難しくなるのでタイマーを使います。
    • Timers -> TIM3 から
      Internal Clock にチェック
      Configration の NVICタブから Enabled にチェック
    • Configration の Parameter Settingsタブから
      Prescailer を 72-1 に設定 (APB1TimerClockを72MHzに設定しているので1MHzにする)
      Counter Period を 1000-1 に設定 (1000カウントで1ms毎に割込を発生させる)
  • あとはPC13にLEDがオンボードされてますのでGPIO出力にしておくのと、
    動作テスト用の入力にPA0をGPIO入力にしておきます。
       
  • プルアップ/ダウンはハードによりお好みですが、ここでは簡易にGNDに落とすだけでONとなるように内蔵プルアップを設定しています。
    • System Core -> GPIO から
      PA0を選択し、
      Configration の GPIO Pull-ip/Pull-down を Pull-up に設定。
  • ここまで設定したら一旦保存してコード自動生成を走らせます。

ハードの変更

と、ここでコーディングに入る前に、高い確率でハードの変更が必要です。

R10のチップ抵抗に 103 と書かれています。
R10はUSBのD+ピンで、USBデバイス側はFull-speed時にはD+ピンに1.5kΩでプルアップする必要があるのに10kΩでプルアップされています。

この状態で認識してくれるかどうかはホスト次第ですが私の環境ではUSBとして認識してくれなかった(デバイスマネージャも反応なしだった)ので素直に対策します。

R10を取り外して1.5kΩに差し替えるのも良いですが、如何せん難易度が高いと思われますので簡易的にはPA12を1.8kΩで3.3vにプルアップすれば抵抗の並列接続となり、1.525…kと大体1.5kΩになります。

私は1.8kΩは手元に持ち合わせていなかったので近い2.2kΩでプルアップしましたが認識してくれましたので、持ち合わせが無くとりあえずお試しであればそれっぽい値で試してみるのも良いかと思われます。
(2.2kΩを並列接続した場合は約1.8kΩ)

A12から2.2kΩを3.3vに接続

マウスデバイスを作って動作確認

まずは記述量の少ないマウスデバイスでUSBデバイスとして動作するかテストしてみます

プロジェクトエクスプローラーから
GamePad_F103(上記で作成したプロジェクト名) -> Core -> Src -> main.c を開く。

USBのAPIを呼ぶために、USER CODE BEGIN Includes に以下をインクルード

/* USER CODE BEGIN Includes */
#include "usbd_def.h"
#include "usbd_hid.h"
/* USER CODE END Includes */

ループ制御用の変数の宣言と、
USBデバイスのハンドルをexternしておきます。
externはお作法的にはあまりよろしくはありませんが、CubeMXが吐き出すコードを有効に使うにはやむを得ませんので責任をもって管理します。

/* USER CODE BEGIN PV */
static volatile uint8_t interrupt;
extern USBD_HandleTypeDef hUsbDeviceFS;
/* USER CODE END PV */

割り込みのコールバックを記述します。
割り込みが発生したら interrupt 変数を 1 にします。
メインループ内で interrupt 変数が 1 になったことを検知したら0に戻してループが進むようにして一定時間毎にループするように制御します。

/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if (htim->Instance == TIM3) {
		interrupt = 1;
	}
}
/* USER CODE END 0 */

メイン関数を記述していきます。
4行目以降はマウス用の構造体・変数宣言なので後で消します。

	/* USER CODE BEGIN 1 */
	interrupt = 0;        // ループ制御用
	
	struct mouseHID_t {
		uint8_t buttons;
		int8_t x;
		int8_t y;
		int8_t wheel;
	};
	struct mouseHID_t mouseHID;
	mouseHID.buttons = 0;
	mouseHID.x = 0;
	mouseHID.y = 0;
	mouseHID.wheel = 0;
	/* USER CODE END 1 */

ペリフェラル等の初期化が終わった後に割込モードでタイマーをスタートさせます

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

メインループの処理を記述していきます
PA0がGNDに接続(Low)になったときに、マウスポインタを右に5動かすようにしています。

	/* USER CODE BEGIN WHILE */
	while (1)
	{
		while(interrupt == 0);		// 割り込みで1になるまで待つ
		interrupt = 0;
		
		if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == 0) {
			mouseHID.x = 5;
			HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13, 1);
		} else {
			HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13, 0);
		}
		USBD_HID_SendReport(&hUsbDeviceFS, (uint8_t *)&mouseHID, sizeof(struct mouseHID_t));
		mouseHID.x = 0;
		/* USER CODE END WHILE */

書き込み&実行

ST-Linkを使ってSWDで書き込みます。
ST-Link V3はマイコンボードのデバッグピンと順番が違うので注意。
マイコンボード側は下図上から GND/CLK/DIO/VCC です。

ST-Link側のVCCはデバイスの電源検知用で、ST-Linkからは給電されませんので、ST-Link、マイコンボードの両方にmicro-USBを接続してあげてください。

ビルド前にビルド設定を「Release」にしておきます。
CubeIDEのメニューバーから
プロジェクト(P) -> ビルド構成 -> アクティブにする -> Release を選択

STM32F103C6T6はROMが32kBしかありませんので、USBミドルウェアを抱え込んだ状態でDebugビルドするとそれだけで32kB近く使うのでちょっとしたコード追加でたちまちビルドできなくなってしまいます。
どうしてもデバッグしたい場合は最適化をかけてROM使用量を減らしてください。
但し最適化をかけると意図した所でブレーク出来なかったり、コード通りにステップ実行出来なくなるので注意。
デバッグ実行が出来ないのであれば、後は空いているシリアル通信やGPIOを使った実働デバッグしか無いのでうまいこと活用しましょう。

メニューバーから
実行(R) -> 実行(S) -> STM32 C/C++ Application を選択
または、ツールバーの実行を選択します

初回実行時は、起動構成のプロパティ編集ウインドウが出てきます。
ビルド設定を「Release」に変更した際には、操作によっては書き込みのターゲットがDebugのままになっていたりするので、書き込み対象ファームが意図したものになっているか確認しておくと良いと思います

デバッガ設定はST-Linkを使っていれば今回の構成では特に変更する必要は無いはずですが、よくあるミスとして「デバッグプローブ」と「インターフェース」の設定が意図したものになっているかは確認しておくと良いと思います。
別の接続方法の場合はその環境に合った設定をしてください。

OKボタンを押下すると、ビルドが行われ、エラーがなければそのまま書き込まれます。
環境によってはマイコンボードのUSBを一旦外して再接続しないとうまく動かないかもしれません。

うまくいけばPC13(緑LED)が点灯し、PA0をGNDに繋げると緑LEDが消灯し、マウスポインタが右に動きだします。

ジョイパッドを作る

ハードに問題が無いことが確認できたので、ジョイパッドを作る作業に入ります。

  • プロジェクトエクスプローラーから「GamePad_F103.ioc」を開き、
    • Middleware -> USB_DEVICE の Class For FS IP を
      Human Interface Device Class (HID) から Custom Human Interface Device Class (HID) に変更。
      Parameter Settings の USBD_CUSTOM_HID_REPORT_DESC_SIZE を 39 に設定します。

      この「39」という数字は後で設定するUSBのレポートディスクリプタのサイズです。設定しようとしているレポートディスクリプタに応じてこの値を設定してください。
    • 設定したら保存してコード生成を走らせます

コードも色々変更が必要になります。
HIDからカスタムHIDに変わったのでインクルードも変更します。

/* USER CODE BEGIN Includes */
#include "usbd_def.h"
#include "usbd_customhid.h"        // ← 変更
/* USER CODE END Includes */

メイン関数の宣言部も、確認用だったマウスの処理を削除してジョイパッド用に必要な変数を用意します。
多分これが一番楽だと思います。

  /* USER CODE BEGIN 1 */
	  interrupt = 0;            // ループ制御用

	  uint8_t old_input = 0;    // 入力テスト用
	  uint8_t new_input = 0;    // 入力テスト用
	  uint8_t buttons[2];       // USBに通知するジョイパッドデータ
  /* USER CODE END 1 */

メインループは以下のようにして、入力の度に次のボタンがONされるようにして全ボタンテストします。

  /* USER CODE BEGIN WHILE */
	while (1)
	{
		// ループ制御
		while(interrupt == 0);
		interrupt = 0;

		// 入力
		new_input = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
    HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13, new_input);
		// OFF→ONに変わったときだけ処理
		if ((old_input != 0)
		 && (new_input == 0)
		) {
			// ジョイパッド入力を1つずらす
			if ((buttons[0] == 0) && (buttons[1] == 0)) {
				buttons[0] = 1;
			} else if (buttons[0] != 0) {
		    	buttons[0] <<= 1;
		    	if (buttons[0] == 0) {
		    		buttons[1] = 1;
		    	}
		    } else {
		    	buttons[1] <<= 1;
		    	if (buttons[1] >= 4) {
		    		buttons[1] = 0;
		    	}
		    }
		}
		old_input = new_input;
		
		// USBに通知
		USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t *)buttons, sizeof(buttons));

    /* USER CODE END WHILE */

最後にレポートディスクリプタを記述します。
プロジェクトエクスプローラーから、
USB_DEVICE -> App -> usbd_custom_hid_if.c
を開くと、Usb HID report descriptor. を記述する箇所があります。

  /* USER CODE BEGIN 0 */
  0x05, 0x01,	//	05:USAGE_PAGE		01:Generic Desktop
  0x09, 0x05,	//	09:USAGE			  05:Game Pad
  0xA1, 0x01,	//	a1:COLLECTION		01:Application
  0x05, 0x09,	//			05:USAGE_PAGE		    09:Button
  0x19, 0x01,	//			19:USAGE_MINIMUM	  01:Button1
  0x29, 0x08,	//			29:USAGE_MAXIMUM	  08:Button8
  0x15, 0x00,	//			15:LOGICAL_MINIMUM	00:0
  0x25, 0x01,	//			25:LOGICAL_MAXMUM   01:1
  0x95, 0x08,	//			95:REPORT_COUNT		  08:8
  0x75, 0x01,	//			75:REPORT_SIZE		  01:1bit
  0x81, 0x02,	//			81:INPUT			      02:Data,Var,Abs
  0x05, 0x09,	//			05:USAGE_PAGE		    09:Button
  0x19, 0x09,	//			19:USAGE_MINIMUM	  09:Button9
  0x29, 0x0a,	//			29:USAGE_MAXIMUM	  0a:Button10
  0x15, 0x00,	//			15:LOGICAL_MINIMUM	00:0
  0x25, 0x01,	//			25:LOGICAL_MAXMUM	  01:1
  0x95, 0x08,	//			95:REPORT_COUNT		  08:2
  0x75, 0x01,	//			75:REPORT_SIZE		  01:1bit
  0x81, 0x02,	//			81:INPUT			      02:Data,Var,Abs
  /* USER CODE END 0 */

USER_CODEの外にある END_COLLECTION を含めて 39 byte です。
ここのサイズを変える場合は、CubeMXの USBD_CUSTOM_HID_REPORT_DESC_SIZE もサイズに合わせて変える必要があります。

個人的に躓いたポイントとして、ボタン10個はまとめて設定することが出来ず、8ボタン毎に分ける必要があるようです。
LOGICAL_MINIMUM/MACIMUMを01~0a、REPORT_COUNT を 0a にしてまとめて設定しようとしていましたがこれは駄目なようです。
(デバイスマネージャで「レポートがバイト配列ではありませんでした。」と怒られました)

結果

チャタリング対策をしていないので、入力の当て方によってはスキップされたかのように見えることもありますが、
入力の度にボタン1~10が入力されている状態を確認できると思います

あとは用途に応じてポート設定とメインループをいじるだけです。

本記事では10ボタンのUSBジョイパッドの作例ですが、軸入力やアナログ値などもレポートディスクリプタの設定と、送信データの部分を変更すれば追加できます。
そのあたりは私が今必要としていないので今回は省略します。

参考文献

本記事制作にあたり、以下の文献を参考にさせていただきました。

USB HID Device (STM32)
TOP > 共通ライブラリ > USB HID > STM32
USB HIDジョイスティックでPS3をコントロールする | Mbed
HIDについて調べものメモ
タイトルとURLをコピーしました