見出し画像

自作 ハンコン(ESP32-S2/S3使用)

ESP32というマイコンボードが、arduinoより性能が高くて安いと知ったので、 あれこれと検索していたらハンドルコントローラをESP32で作った ページ があって、ダンボールを重ねてハンドルにしたり、とても楽しそうだったので作ってみました。
ESP32ボードをアルミケースに入れると、BluetoothでのPCとの接続状態が悪化するように思えたので、ESP32をUSB接続してジョイパッドとして動かせるのかを調べました。検索していたら、 こちらのページ が見つかりました。
ESP32-S2/S3であれば、ライブラリが揃っていました。
ここまでわかれば、なんとかなります。

ハンドル部の作成

ロータリーエンコーダの6mmシャフトに取り付ける部分の試作です。下の板はダイソーで買ったクリップボードからクリップ部を外した物です。
木片に6mmの穴を開けて、ここに通したシャフトを上からネジで押さえるようにしてみました。写真で見えているナットは、穴にはめ込んだだけです。穴径をナットより若干小さ目にしているので、取り外すのが難しいくらいにきつくはまりました。これを丸形に切った板にねじ止めしたのが、試作1号でした。ロータリーエンコーダのシャフトとハンドル部の接続は、下に記載したように別な方法に変更しています。
ロータリーエンコーダは直径+αの穴に通して、その上側を ネジで締めこんで固定するようにしました。ロータリーエンコーダは参考にしたページで使っていた600パルス/回転の物を選びました。amazonでも売られていますが、値段が安いのは AliExpressです。送料を足しても尚安いです。ただ、店の当たり外れは あるみたいです。
木材と木材の接続はいつもは木工用ボンドを使うのですが、今回は試しに作ってみるだけなので木ネジを多用しています。
正面
上から
机への固定は、コの字型の木片とスペーサーでぴったりとはまるようにしてみました。木目に注目。コの字の形の縦に木目があります。あとで出てきますが、これだと強度が足りませんでした。
MDF材(クリップボードの板)から直径20cmで切り出したハンドルをエンコーダに取り付けたところ。 少し実現性が見えてきたので、ハンドルに厚みを持たせました。 材料はダイソーで売っていた桐材です。外側はノコギリで8角形に切ってからヤスリ掛け。 それほど堅い木ではないので、ヤスリでゴリゴリと削れます。 内側を切り抜くのはジグソーを使いました。MDF材だけよりちょっとだけリアル感が増えました。
ハンドルとエンコーダは、木片を適当に切って穴の中心が揃うようにしたカプラーのような構造物で接続しました。

しばらくこの状態で遊んでいたら、机に固定する部分が ポッキリと折れてしまいました。木目と力の方向が合っていませんでした。

改良版です。 コの字型は同じですが、2ピースにしてフリーの下側の木片を蝶ネジで締めこむ構造にしました。
いい感じで机に固定できてます。

ESP32関係


ESP32-S3-DevKitC

ESP32-S3-DevKitCはAliexpressのこちらのショップから購入しました。

ロータリーエンコーダの電源は+5Vが必要です。しかしESP32-S3 devcitCは USBコネクタの右側の外から2ピン目に5Vの端子(上の写真の右下部)があるのですが、ここはデフォルトでは5Vが出ていないため、捺印12の上の IN-OUTのPADをショートさせる必要があります。PADのIN-OUT捺印側はレギュレータ AMS1117(BOOTスイッチの下の部品)の右側の端子(VINつまり5V)とつながっており、PADのUSBコネクタ側は、5V捺印の端子につながっています。IN-OUTのPADをショートさせることで、5V捺印の端子から取り出した5Vをロータリーエンコーダへの電源として供給できました。

ESP32のプログラムもこちらを参考にしました。
できたmain.cppを載せておきます。割り込み内での処理はフラグを設定するだけにして、mainの中でこのフラグを見てCWとCCWの処理を行っています。ハンドル以外にアクセルとブレーキもお試しで接続したときのコードなので、これらの部分は現物に合わせた補正計算が入っています。


#include <Joystick_ESP32S2.h>

#define ENCA GPIO_NUM_14
#define ENCB GPIO_NUM_13
#define AD1_3 GPIO_NUM_4
#define AD1_4 GPIO_NUM_5
#define SW1 GPIO_NUM_1
#define SW2 GPIO_NUM_2
#define SW3 GPIO_NUM_42
#define SW4 GPIO_NUM_41

#define encdelta 70

#define USECCOUNT 10000

#define J_BUTTON 4
#define J_HATSW 0
#define J_XAXIS true
#define J_YAXIS false
#define J_ZAXIS false
#define J_RXAXIS true
#define J_RYAXIS true
#define J_RZAXIS false
#define J_THROTTLE false
#define J_RUDDER false
#define J_ACCEL false
#define J_BRAKE false
#define J_STEERING false


volatile int Enc = 0;
volatile int SW1_changed = 0;
volatile int SW2_changed = 0;
volatile int SW3_changed = 0;
volatile int SW4_changed = 0;
volatile int Timerflag = 0;
int encdata = 0;

hw_timer_t * tm = NULL;

// Create Joystick
Joystick_ Joystick(
  JOYSTICK_DEFAULT_REPORT_ID, 
  JOYSTICK_TYPE_GAMEPAD,
  J_BUTTON,  J_HATSW,
  J_XAXIS,  J_YAXIS,  J_ZAXIS,  J_RXAXIS,  J_RYAXIS,  J_RZAXIS,
  J_RUDDER,  J_THROTTLE,  J_ACCEL,  J_BRAKE,  J_STEERING );
  

//--------------------------------------INTERRUPT
void IRAM_ATTR INT_ENCODER() {
  if (digitalRead(ENCA) == 0) {
    if (digitalRead(ENCB) == 0) {
      Enc = 1;
    }
    else {
      Enc = -1;
    }
  }
}

void IRAM_ATTR INT_SW1() {
  SW1_changed = 1;
}

void IRAM_ATTR INT_SW2() {
  SW2_changed = 1;
}

void IRAM_ATTR INT_SW3() {
  SW3_changed = 1;
}

void IRAM_ATTR INT_SW4() {
  SW4_changed = 1;
}

#if 1
void IRAM_ATTR INT_TIMER() {
  Timerflag=1;
}
#endif



void setup() {

  // USB
  USB.PID(0x8211);
	USB.VID(0x303b);
	USB.productName("ESP32-USBPAD");
	USB.manufacturerName("ky01");
	USB.begin();

  // wheel LeftThumb X
  Joystick.setXAxisRange(-32767, 32767);
  Joystick.setXAxis(0);
  // accele LeftTrigger
  Joystick.setRxAxisRange(0, 32767);
  Joystick.setRxAxis(0);
  // break RightTrigger
  Joystick.setRyAxisRange(0, 32767);
  Joystick.setRyAxis(0);

	Joystick.begin();
  
  // 入力ピン設定
  pinMode(ENCA, INPUT_PULLUP);
  pinMode(ENCB, INPUT_PULLUP);
  pinMode(SW1, INPUT_PULLUP);
  pinMode(SW2, INPUT_PULLUP);
  pinMode(SW3, INPUT_PULLUP);
  pinMode(SW4, INPUT_PULLUP);
  pinMode(AD1_3, ANALOG);
  pinMode(AD1_4, ANALOG);
  
  // ロータリーエンコーダー割り込み設定
  attachInterrupt(ENCA, INT_ENCODER, FALLING);

  // GPIO割り込み設定
  attachInterrupt(SW1, INT_SW1, CHANGE);
  attachInterrupt(SW2, INT_SW2, CHANGE);
  attachInterrupt(SW3, INT_SW3, CHANGE);
  attachInterrupt(SW4, INT_SW4, CHANGE);

  // Timer割り込み設定
  tm = timerBegin(0,80,true);   //80MHz / 80 -> 1usec
  timerAttachInterrupt(tm, INT_TIMER, true);
  timerAlarmWrite(tm, USECCOUNT, true); // 1usec x 10000 -> 10msec
  timerAlarmEnable(tm);


  // ADCの解像度を12bit(0~4095)に設定
  analogSetAttenuation(ADC_11db); // ATT -11dB

  Serial.begin(115200);

}

//----------------------------------------------
void loop() {
  int16_t accelarator;
  int16_t breaking;
  int16_t numave=3;
  long ad13Millivolt = 0;
  long ad14Millivolt = 0;
  long adave[numave];

    //-----------------rotally encoder
    if (Enc == 1) {
      if (encdata > -32767 + encdelta) {
        encdata -= encdelta;
        Joystick.setXAxis(encdata);
      }
      Enc = 0;
    }

    if (Enc == -1) {
      if (encdata < 32767 - encdelta) {
        encdata += encdelta;
        Joystick.setXAxis(encdata);
      }
      Enc = 0;
    }


    //-------------------- timer
    if ( Timerflag == 1) {

    //-----------------accel
      ad13Millivolt = analogReadMilliVolts(AD1_3);
      // 2270 500
      if( ad13Millivolt > 2200 ) ad13Millivolt=2200;
      accelarator = (int)(32767-  (ad13Millivolt - 500) * 32767 / (2200 - 500));  // 3.3V PU  A/D ValR GND
      accelarator = int(accelarator / 300) * 300;

      if( accelarator > 32767 ) accelarator = 32767;
      if( accelarator < 0 ) accelarator = 32767;
      Joystick.setRxAxis(accelarator);

    //-----------------break
      ad14Millivolt = analogReadMilliVolts(AD1_4);
      // 3114 60
      breaking = (int)(32767 - (ad14Millivolt - 50) * 32767 / (3114 - 50));
      if( breaking > 32767 ) breaking = 32767;
      Joystick.setRyAxis(breaking);

      Timerflag = 0;
    }


//-------------------------------SW1
    if (SW1_changed == 1) {
      if (digitalRead(SW1) == 0) {
        Joystick.pressButton(0);
      }
      else {
        Joystick.releaseButton(0);
      }
      SW1_changed=0;
    }

//-------------------------------SW2
    if (SW2_changed == 1) {
      if (digitalRead(SW2) == 0) {
        Joystick.pressButton(1);
      }
      else {
        Joystick.releaseButton(1);
      }
      SW2_changed=0;
    }

//-------------------------------SW3
    if (SW3_changed == 1) {
      if (digitalRead(SW3) == 0) {
        Joystick.pressButton(2);
        Joystick.setXAxis(0);
      }
      else {
        Joystick.releaseButton(2);
      }
      SW3_changed=0;
    }

//-------------------------------SW4
    if (SW4_changed == 1) {
      if (digitalRead(SW4) == 0) {
        Joystick.pressButton(3);
      }
      else {
        Joystick.releaseButton(3);
      }
      SW4_changed=0;
    }


} // end of loop()

platformio.iniの中身はこのようにしました。

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
lib_deps = schnoog/Joystick_ESP32S2@^0.9.4

Windows上のgamepadのプロパティを見ながらハンドルを回すと目印が動くのですが、なんか遅い。ふと見ると、ハンドルを回すとVSCodeのログ画面に大量のメッセージが出てました。調べていくと、USBHID.cppの中にメッセージを出力するコードがありました。
USBHID.cppは
.platformio\packages\framework-arduinoespressif32\libraries\USB\src
にありました。

USBHID.cppの一部を引用。345~363行目の部分です。

    if(!res){
        log_e("not ready");
    } else {
        // The semaphore may be given if the last SendReport() timed out waiting for the report to
        // be sent. Or, tud_hid_report_complete_cb() may be called an extra time, causing the
        // semaphore to be given. In these cases, take the semaphore to clear its state so that
        // we can wait for it to be given after calling tud_hid_n_report().
        xSemaphoreTake(tinyusb_hid_device_input_sem, 0);

        res = tud_hid_n_report(0, id, data, len);
        if(!res){
            log_e("report %u failed", id);
        } else {
            if(xSemaphoreTake(tinyusb_hid_device_input_sem, timeout_ms / portTICK_PERIOD_MS) != pdTRUE){
                log_e("report %u wait failed", id);
                res = false;
            }
        }
    }

上記の中でメッセージを出力しているのは、
log_e("report %u wait failed", id);
の部分だったので、メッセージを出さないようにコメントアウトしたところ、メッセージは出なくなってgamepadのデバイスのプロパティの目印の動作速度が爆速になりました。何かしらの良くないことが起きているのかもしれませんが、コードの深読みができないので根本原因の解決は放置です。
もしかしたら私の環境だけで発生していたのかもしれませんが、何かしらの参考にでもなればと掲載しました。

ケース

ジャンク箱にあったケースにむりやりESP32とスイッチやコネクタを組み込んで完成です。

使用感

このハンコンでRush Rallyを遊んでみましたが、実に楽しいです。試作のアクセルとブレーキは足での操作に耐えられずに壊れてしまったので遊べた期間は数日だったのですが、微妙に曲がっている高速ストレートもぎりぎりのイン側を攻められるし、タイトなカーブの出口での逆ハンもスムーズにできて、各コースで良いタイムを出せました。FFBはついて無いのでくるくる回るだけのハンドルですが、市販のハンコンでなくても十分に楽しめました。


いいなと思ったら応援しよう!