「電車でGO」コントローラをWindows11で使えるようにする変換器の製作 2024.2.23

    1.はじめに

     孫が最近電車にハマり出したので、昔に遊んだ「電車でGO」が最新のパソコンで動けば喜ぶだろうと思って、Windows11のパソコンにインストールしてみたら簡単にインストールできて問題なく動きました。

     ゲームCDの他に専用のコントローラも保存していましたのでこのコントローラがWindows11で使えれば更に面白いのですが、コントローラはUSBインタフェースではなく15pinのゲームポートインタフェースです。

     今時のPCにゲームポートなんかありません。ゲームポートの付いた古いサウンドカードを手に入れてみた所でうまく動くか判りません。

     Windows11で動きはしたものの操作はあくまでもキーボードです。そこで、専用のコントローラの信号をUSBキーボードに変換する変換器を作れば、専用コントローラで遊べるはずです。

     ということで、変換器の製作を始めることにしました。

    「電車でGO!」CD-ROM for windows98
    Windows11で動きました!
    専用コントローラ、これを使いたい!
    残念ながら15pinゲームポートインタフェースです

     
    2.専用コントローラの回路調査と少しの改造

     専用コントローラには、「マスコンレバー」、「ブレーキレバー」、「Aボタン」、「Bボタン」、「Cボタン」があります。

     裏ブタを開けると、メイン基板とブレーキユニット、マスコンユニットがありました。ボタンはメイン基板上にあります。

    マスコンとブレーキ、ABCのボタン
    メイン基板と各ユニット

     回路を調べて見ました。その結果の回路図が次になります。

     ボタンBとボタンCは分離されていませんでした。ボタンA、ボタンB・Cは、それぞれ47Kohmで+5Vにプルアップされています。マイコンのデジタル入力に直結できます。

     ブレーキとマスコンは、+5Vとの間の抵抗値がレバーの位置によって変化するようになっています。

     各レバーの位置における+5Vとの間の抵抗を測定してみました。回路図から計算で求められますが確認を込めて実測もしてみました。

      マスコン
      位置抵抗値(Ω)
      37.5K
      1
      33.2K
      2
      28.1K
      3
      22.8K
      4
      16.3K
      5
      8.8K
         
      ブレーキ
      位置抵抗値(Ω)
      解除
      8.9K
      1
      16.3K
      2
      23.0K
      3
      28.0K
      4
      33.5K
      5
      37.3K
      6
      42.2K
      7
      44/7K
      8
      46.8K
      非常
      49.7K
         
      メイン基板裏に抵抗2本を取付

     この抵抗の変化を電圧の変化にしてやればマイコンのアナログ入力に直結できます。
     そこで、電圧変化に変換するために27KΩ[(8.9+49.7)/2]と22KΩ[(8.8+37.5)/2]の抵抗を各信号ラインとGND間に取り付けました。


    3.ARDUINO UNO R4を使う

     マイコンは、いつもはPICを使うのですが、今回はお手軽に済ませたかったのでARDUINO UNO R4を使ってみました。

    3-1.キーの割り付け

     「電車でGO!」のコントロールでは、キー入力パターンを選ぶことができます。

     スタートメニューの設定で、
      キーボード1 は、 [マスコン] カーソル上下  [ブレーキ] カーソル左右
      キーボード2 は、 [マスコン] キーボードA・Z [ブレーキ] カーソル左右
      キーボード3 は、 [マスコン] キーボード左上の1〜5(長押し)  [ブレーキ] テンキーの0〜9(解除〜非常)
      キーボード4 は、 [マスコン・ブレーキ] カーソル上下
     いずれのパターンも [警笛] スペース [ポーズ] エスケープ [選択] リターンキー

     となっています。

     カーソル上下や左右の入力をマイコンでエミューレートしようとするとマイコン内部に現在の値を覚えておく必要があります。 これは結構面倒です。マイコン側の認識とゲームソフト側の認識がずれておかしな結果になるかもしれません。

     今回は、専用コントローラのレバー位置とキー入力のデータが一致しているパターン3をエミュレートします。
    これだと認識がずれる心配はありません。ゲームのスタートメニューでキーボード3を選択してから遊ぶことにします。

     ボタンAは、警笛用のスペースを、ボタンB・Cは、リターンキーをエミュレートすることにしました。

    3-2.UNO R4との接続

     ボタンAは、ARDUINOのデジタル入力 D2 に、ボタンB・Cは、D3 に接続しました。

     D2,D3にしたのは、ピンの変化割込みが使えるからです。スケッチの作成では、ポーリングによるピン入力の読み込みにしましたので他のピンでもよかったです。

     マスコンとブレーキは、アナログ入力のA0、A1に接続しました。

     コントローラの+5VとGNDは、ARDUINOの+5VとGNDピンに接続しました。

    UNO R4 との接続
    UNO R4 との接続
    3-3.スケッチの作成

     UNO R4では、Keyboardライブラリを使うことができます。このためUSBのキーボードを簡単にエミュレートできます。

    今回作成したスケッチは次のとおりです。

    /*
     ********************************************************
     *  DenGo controller 
     * 2023.2.23 Suwa-Koubou
     ********************************************************
    */
    
    #include "Keyboard.h"
    
    /*=======================================================
      definition
    ========================================================*/
    
    // pin assign
    #define   MASCON     A0
    #define   BRAKE      A1
    #define   BUTTON_A   D2
    #define   BUTTON_BC  D3
    
    
    /*=======================================================
      controller initialize
    ========================================================*/
    
    void controller_init(void){
      // Digital IN
      pinMode(BUTTON_A, INPUT);
      pinMode(BUTTON_BC, INPUT);
    
      // USB HID keyborad
      Keyboard.begin();
    
      // open the serial port for DEBUG print:
      Serial.begin(115200);
    
    }
    
    
    /*=======================================================
       A button: send SPACE key
       BC button: send RETURN key 
    ========================================================*/
    
    int btn_A_flg = 0;
    int btn_BC_flg = 0;
    
    void doAbutton(void) {
    
      if(btn_A_flg == 0 && digitalRead(BUTTON_A) == 0){
        Keyboard.press(' ');
        btn_A_flg = 1;
      }
    
      if(btn_A_flg == 1 && digitalRead(BUTTON_A) == 1){
        Keyboard.release(' ');
        btn_A_flg = 0;
      }
    }
    
    void doBCbutton(void) {
    
      if(btn_BC_flg == 0 && digitalRead(BUTTON_BC) == 0){
        Keyboard.press(KEY_RETURN);
        btn_BC_flg = 1;
      }
    
      if(btn_BC_flg == 1 && digitalRead(BUTTON_BC) == 1){
        Keyboard.release(KEY_RETURN);
        btn_BC_flg = 0;
      }
    
    }
    
    
    /*=======================================================
      MASCON
    ========================================================*/
    
    struct {
      int min;
      int max;
      char chr;
    } mascon[6] = {
      {360, 399, '0' },  // 切 376-394
      {399, 432, '1',},  // 1  404-421
      {432, 476, '2',},  // 2  443-460
      {476, 542, '3',},  // 3  493-510
      {542, 650, '4',},  // 4  574-590
      {650, 780, '5',},  // 5  711-725
    };
    
    int curMascon = 0;
    void doMascon(void) {
      int n0, n1;
    
      n0 = oneReadMascon();
      for(;;){
        delay(10);
        n1 = oneReadMascon();
        if(n0 == n1) break;
        n0=n1;
      }
    
      // Changed Mascon
      if(curMascon != n0){
        if(curMascon >= 1){
          Keyboard.release(mascon[curMascon].chr);
        }
        if(n0 >= 1){
          Keyboard.press(mascon[n0].chr);
          Serial.println(n0);
        }
        curMascon = n0;
      }
    }
    
    int oneReadMascon(){
      int val;
      int n;
    
      while(1){
        val = analogRead(MASCON);
        for(n=0;n<6;n++){
          if(val > mascon[n].min && val < mascon[n].max) return n;
        }
      }
    }
    
    
    /*=======================================================
      Brake
    ========================================================*/
    
    struct {
      int min;
      int max;
      char chr;
    } brake[10] = {
      {676, 760, KEY_KP_0,},  // 解除 740
      {571, 676, KEY_KP_1,},  // 1  612
      {507, 571, KEY_KP_2,},  // 2  531
      {462, 507, KEY_KP_3,},  // 3  484
      {427, 462, KEY_KP_4,},  // 4  440
      {400, 427, KEY_KP_5,},  // 5  415
      {379, 400, KEY_KP_6,},  // 6  386
      {368, 379, KEY_KP_7,},  // 7  373
      {356, 368, KEY_KP_8,},  // 8  363
      {340, 356, KEY_KP_9,},  // 9  350
    };
    
    int curBrake = 0;
    void doBrake(void) {
      int n0, n1;
    
      n0 = oneReadBrake();
      for(;;){
        delay(10);
        n1 = oneReadBrake();
        if(n0 == n1) break;
        n0=n1;
      }
    
      // Changed Brake
      if(curBrake != n0){
        //Keyboard.write(brake[n0].chr);
        Keyboard.press(brake[n0].chr);
        delay(50);
        Keyboard.release(brake[n0].chr);
        Serial.println(n0);
        curBrake = n0;
      }
    }
    
    int oneReadBrake(void){
      int val;
      int n;
    
      while(1){
        val = analogRead(BRAKE);
        for(n=0;n<10;n++){
          if(val > brake[n].min && val < brake[n].max) return n;
        }
      }
    }
    
    
    /*=======================================================
      setup()
    ========================================================*/
    
    void setup() {
    
      // DenGO controller initialize
      controller_init();
    
      delay(100);
    }
    
    
    /*=======================================================
      loop()
    ========================================================*/
    
    void loop() {
      int val;
    
      // Button polling
      doAbutton();
      doBCbutton();
    
      // MASCON read
      doMascon();
      
      // BRAKE read
      doBrake();
    
      // loop time 50ms
      delay(50);
    }
    
    /***** end of file *************************************/