T3PA-PRO用ロードセルブレーキを5000円以内で作る

最近レースコミュニティ内でFanatec先輩を使う人が増え、ロードセルなピーポーが多くなってきました。
T300RSの購入からそろそろ2年、バネブレーキの感触にはいまいち満足できなくなっており、ロードセルを使う人が増えると試してみたくなるもの。
しかし、Thrustmasterのペダルには公式のロードセルキットなど存在しません。

海外サイトでT3PA-PRO用の非公式ロードセルキットが販売されてはいるのですが、お値段が1万円をこえてくるのでお高いです。
そこまでの値段になるならもうFanatecのCSL Elite Pedals LCを買うよという話です。

いっそ本当にFanatecのペダルを買ってしまおうかと思いながらぐぐっていると、ArduinoでUSB入力デバイスを作成できるという情報を入手しました。
以前から電子工作には興味があったので、これはやるしかあるまいという流れになるわけです。

材料

※価格は私が購入したときのもので、現在は変更されている可能性があります。

ブレッドボード EIC-801/ 270円@秋月電子

ロードセル 20kg SC133/540円@秋月電子

HX711 ロードセル用ADコンバータ/350円@秋月電子

ジャンパーワイヤー/180円@秋月電子

ピンソケット/80円@秋月電子(スイッチは使っていない)

ATMega32U4 5V/16MHz Aruduino Leonardo互換/770円@Amazonマーケットプレイス

TRUSCO ジョイント金具 24型フラット 150mm TK24-F3C x 2/484円@ヨドバシ.com(お取り寄せ)

サンワサプライ KU-RMCB2 [両面挿せるマイクロUSBケーブル(MicroB) ブラック](2m)/550円@ヨドバシ.com
(画像なし)

加工

ArduinoとHX711に添付されているピンとコネクタをハンダづけします。

HX711のチャンネルBは使わないので、マニュアルに従って裏面のJ3とJ4のジャンパをハンダで繋げます。

次に、HX711を80Hz化します。
HX711は標準では10Hz(SPS)動作なので、ロードセルからのデータを1秒あたり10回しか取得でません。
これではデータ量が少なすぎてブレーキとしては使い物にならないので、80Hz化して8倍速にします。
やり方はチップの15番ピンをカットして基板から浮いた状態にし、15番ピンのチップ側に残った部分と16番ピンをハンダで繋げて接続します。
ここは比較的繊細な作業なので、ハンダづけが苦手な人には難しいかもしれません。
基板上に80Hz化できるジャンパがあるタイプのHX711もあるようなので、そちらを購入するのも良いでしょう。

組み立て

T3PA-PROに付属のブレーキmod用金具に、ジョイント金具を2枚重ねて固定します。1枚では強度が足りなくて曲がる恐れがあります。
ブレーキmod用金具には出っ張りがあり、そのままではジョイント金具が折れそうなので、平らにするために厚さ3mmのゴムを入れました。

ジョイント金具にロードセルを取り付けます。
ロードセルの荷重を受ける部分がペダルの根元側を向くようにします。
荷重がかかったときにジョイント金具と接触しないように、ワッシャなどを入れてスキマを作ります。
使用するボルトはM5です。
なお、ちょうどいい長さのボルトが無かったのでM6ナットで長さを強引に調整しています。

ペダルと接触する部分にスポンジなどを貼ります。
これがペダルのフィーリングを決めますので、好みの感触になるようにいろいろ試してください。

(オプション)ロードセルのむき出しの配線を端子化します。

ロードセルから出ている配線は芯線がとても細いです。
HX711のコネクタとの接続も大変ですし、うっかり引っ張ると簡単に切れます。
そこで、ピンソケットの金具を抜いたものとハンダづけし、補強のためにビニールテープを巻きました。
熱収縮チューブあたりを使うともっとキレイになるでしょう。

ロードセルとHX711を接続します。
私が買ったものでは、CN2の1が赤(VDD)、2が黒(GND)、CN3の1が白(INA-)、2が緑(INA+)でした。

ArduinoとHX711を接続します。
今回はデータ用として10番、クロック用に14番を使いました。
Arduino(VCC) <-> HX711(1) 赤 +5V
Arduino(10) <-> HX711(2) 青 DAT
Arduino(14) <-> HX711(3) 緑 CLK
Arduino(GND) <-> HX711(6) 黒 GND

80Hz化したせいなのか、HX711はデータ取得のタイミングがとてもシビアで、動作が不安定になりやすいようです。
HX711をできるだけArduinoに近い位置に置く、接続するワイヤを短くするなどで安定するみたいです。

ロードセルを組み込んだブレーキmod用金具をT3PA-PROに取り付けます。

制御ソフト

https://www.arduino.cc/en/Main/Software からArduino IDEをダウンロードしてインストールします。
寄付を求められますので、ダウンロードだけしたい方はJUST DOWNLOADを押しましょう。

https://github.com/MHeironimus/ArduinoJoystickLibrary からArduino Joystick Libraryをダウンロードしてインストールします。

  • Clone or DownloadからDownload ZIPを選択
  • zipファイルのArduinoJoystickLibrary-masterにあるJoystickフォルダをドキュメント\Arduino\librariesにコピー

以下のロードセルブレーキ用プログラムを、拡張子をinoにした任意の名前で保存します。
DAT、CLKの数値は必要に応じて書き換えてください。
なお、HX711ライブラリは動作が少しあやしかったので使用していません。
ロードセルからのデータ取得部分は https://www.denshi.club/cookbook/sensor/g/1-2.htmlを参考にしました。

※ 2019/10/26 更新

#include <EEPROM.h>
#include <Joystick.h>

#define DAT 10
#define CLK 14
#define EEPAD_HL 0
#define EEPAD_DZ 2
#define EEPAD_HB 4
#define EEPAD_ASST 6

bool isDebug=false;
/* load cell */
float offset=0;
int times=2;
int loadCellHardLimit=20000;
int loadCellDeadZone=1000;
int loadCellHalfBrake=6250;
int loadCellSoftLimit=11500;
int loadCellAssist=0;
int resetCount=0;
int resetLoad=0;

/* Joystick */
Joystick_ Joystick = Joystick_(
  0x03,                    // reportid
  JOYSTICK_TYPE_GAMEPAD,   // type
  1,                       // button count
  0,                       // hat switch count
  false,                    // x axis enable
  false,                    // y axis enable
  false,                   // z axis enable
  false,                   // right x axis enable
  false,                   // right y axis enable
  false,                   // right z axis enable
  false,                   // rudder enable
  false,                   // throttle enable
  false,                   // accelerator enable
  true,                   // brake enable
  false                    // steering enable
  );

void setup() {
    Serial.begin(9600);

    // wait open serial
    if (isDebug) {
      while (!Serial) ; 
      Serial.println("start");
    }
    pinMode(CLK, OUTPUT);
    pinMode(DAT, INPUT);
    offset = Read(30);
    getEEPROM();
    info();
    CalcSoftLimit();
    if (isDebug) {
      Serial.print("offset=");Serial.println(offset);
    }
    Joystick.begin();
}

void getEEPROM() {
  int tmp;

  // Hard Limit
  EEPROM.get(EEPAD_HL, tmp);
  if (tmp > 0) {
    loadCellHardLimit = tmp;
  } else {
    EEPROM.put(EEPAD_HL, loadCellHardLimit);
  }

  // Dead Zone
  EEPROM.get(EEPAD_DZ, tmp);
  if (tmp > 0) {
    loadCellDeadZone = tmp;
  } else {
    EEPROM.put(EEPAD_DZ, loadCellDeadZone);
  }

  // Half Brake
  EEPROM.get(EEPAD_HB, tmp);
  if (tmp > 0) {
    loadCellHalfBrake = tmp;
  } else {
    EEPROM.put(EEPAD_HB, loadCellHalfBrake);
  }

  // Assist
  EEPROM.get(EEPAD_ASST, tmp);
  if (tmp >= 0) {
    loadCellAssist = tmp;
  } else {
    EEPROM.put(EEPAD_HB, loadCellAssist);
  }
}

void info() {
  Serial.println("Hard Limit: " + String(loadCellHardLimit));
  Serial.println("Soft Limit: " + String(loadCellSoftLimit));
  Serial.println("Dead Zone: " + String(loadCellDeadZone));
  Serial.println("Half Brake: " + String(loadCellHalfBrake));
  Serial.println("Assist: " + String(loadCellAssist));
}

void loop() { 
    float load;
    float assist;
    int brake;
    load = Read(times);
    assist = Assist(load);
    brake = Brake(load+assist);
    if (isDebug) {
      Serial.print(brake);Serial.print(" / ");
      Serial.print(load+assist,2);Serial.print("g(+ ");
      Serial.print(assist,2);Serial.println(")");
    }
    Command();
    //delay(1);
}
 
float Read(int t){
    long sum = 0;
    for (int i = 0; i < t; i++) {
        long data=0;
        while(digitalRead(DAT)!=0);
        for(char i=0;i<24;i++) {
            digitalWrite(CLK,1);
            delayMicroseconds(1);
            digitalWrite(CLK,0);
            delayMicroseconds(1);
            data = (data<<1)|(digitalRead(DAT));
        }
        digitalWrite(CLK,1); //gain=128
        delayMicroseconds(1);
        digitalWrite(CLK,0);
        delayMicroseconds(1);
        data = data^0x800000;
        sum += data;
    }
    float data = sum /t;
    float volt;float gram;
    volt =data*(4.2987/16777216.0/128);
    gram=volt/(0.001*4.2987/20000.0);
    return gram-offset;
}

int Brake(float load) {
  int brakeValue = (load - loadCellDeadZone) / (loadCellSoftLimit - loadCellDeadZone) * 1023;
  if (brakeValue < 0) {
    brakeValue = 0;
  }
  if (brakeValue > 1023) {
    brakeValue = 1023;
  }
  Joystick.setBrake(brakeValue);
  return brakeValue;
}
int Assist(float load) {
  float assist = 0;
  if (loadCellAssist > 0) {
    float m = load / loadCellAssist / 10000 + 1.0;
    if (m > 1.0 ) {
      assist = pow(load, m) - load;
    }
  }
  return assist;
}

int Command() {
  if (Serial.available() > 0) {
    String buf = Serial.readString();
    char com = buf.charAt(0);
    int val = buf.substring(1).toInt();
    
    switch(com) {
      case 'D': // Debug
        isDebug = !isDebug;
        Serial.println("Debug mode set to " + String(isDebug));
        break;
      case 'l': // Hard Limit
        loadCellHardLimit = val;
        EEPROM.put(EEPAD_HL, loadCellHardLimit);
        Serial.println("Hard Limit -> " + String(val));
        CalcSoftLimit();
        info();
        break;
      case 'd': // dead zone
        loadCellDeadZone = val;
        EEPROM.put(EEPAD_DZ, loadCellDeadZone);
        Serial.println("Dead Zone -> " + String(val));
        CalcSoftLimit();
        info();
        break;
      case 'h': // half brake
        loadCellHalfBrake = val;
        EEPROM.put(EEPAD_HB, loadCellHalfBrake);
        Serial.println("Half Brake -> " + String(val));
        CalcSoftLimit();
        info();
        break;
      case 'a': // assist
        loadCellAssist = val;
        EEPROM.put(EEPAD_ASST, loadCellAssist);
        Serial.println("Assist -> " + String(loadCellAssist));
        CalcSoftLimit();
        info();
        break;
      case 'r': // reset
        offset = 0;
        offset = Read(30);
        if (isDebug) {
          Serial.print("offset=");Serial.println(offset);
        }
        CalcSoftLimit();
        info();
        break;
      default:
        info();
        break;
    }
  }

}

void CalcSoftLimit() {
  loadCellSoftLimit = (loadCellHalfBrake-loadCellDeadZone)*2 + loadCellDeadZone;
  if (loadCellSoftLimit > loadCellHardLimit) {
    loadCellSoftLimit = loadCellHardLimit;
  }
}

ArduinoをUSBケーブルでPCに接続します。
Arduino IDEを起動し、

  • ツール->ボードでArduino Leonardoを選択
  • ツール->シリアルポートでCOMxを選択(xの数字は環境によって異なります)
  • ファイル->開くで先ほど保存したプログラムを開く
  • 上部の右矢印ボタンでプログラムをArduinoに書き込む

Arduino Leonardoという名前で、ブレーキ1個だけのゲームコントローラーが認識されれば成功です。

ブレーキ調整

このブレーキの調整はArduino IDEのシリアルコンソールで行います。
シリアルコンソールは右上のシリアルモニタボタンで開きます。

調整コマンド

調整に使用できるコマンドは以下のとおりです。

l xxxxx
ロードセルの上限値(Hard Limit)をグラムで設定します。
初期値:20000

d xxxxx
ロードセルのデッドゾーン(Dead Zone)をグラムで設定します。この重さ以下の入力は無視されます。
初期値:1000

h xxxxx
ハーフブレーキになる重さ(Half Brake)をグラムで設定します。
フルブレーキの重さ(Soft Limit)は(ハーフブレーキ – デッドゾーン) x 2 + デッドゾーン に設定されます。
初期値:6250

a xxx
ブレーキのアシスト係数を設定します。
このアシストはブレーキの 入力値をフルブレーキに近くなるほど大きく増幅し、感度が上がっていくように動作します。
数値が小さいほど効果が大きくなりますが、0でアシストが無効となります。
おそらく100~150あたりが適正値です。
初期値:0

D
デバッグモードをON/OFFします。(トグル)
デバッグモードでは現在のブレーキ入力値がリアルタイムで表示されます。
仕様上、リアルタイム表示にリソースを奪われてブレーキの反応が悪くなりますので、ゲームを遊ぶ際には必ずOFFにしてください。
初期値:OFF

r
ブレーキをリセットします(再キャリブレーション)

i
現在の設定値を表示します。
実はここまでのコマンド以外の入力はすべて設定値表示コマンドとみなされます。

使用上の注意点

ペダルとブレーキが別デバイスとなりますので、同じデバイスでないと許してくれないゲームでは使用できません。

また、ペダル本体のブレーキが生きたままですので、デバイスの割り当て時にペダルを踏むと2重入力になってうまくいかないことがあるかもしれません。
その時はロードセルを直接押すなどで、ペダル側のブレーキが反応しないようにしてあげてください。

おまけ

動作確認はしていませんが、仮組みしてみた感じではおそらくT3PAでも使えるでしょう。